Skip to content

Feat/angular roadmap #424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open

Feat/angular roadmap #424

wants to merge 16 commits into from

Conversation

dokolyski
Copy link
Contributor

@dokolyski dokolyski commented Apr 4, 2025

Summary by CodeRabbit

  • New Features
    • Introduced a comprehensive interactive roadmap with SVG visualization, zoom/pan controls, and layered node structures.
    • Added multiple Angular components, directives, and pipes for rendering nodes, clusters, layers, connectors, and controls.
    • Enabled dynamic loading of roadmap data with localization support for English and Polish.
    • Developed shell components integrating header, roadmap, and footer with reactive language and UI behavior.
    • Added JSON data for roadmap tiles and node structures.
  • Documentation
    • Created README files with usage and testing instructions for roadmap and UI libraries.
  • Chores
    • Added ESLint, Jest, and TypeScript configs enforcing strict code quality, testing, and build standards.
    • Updated project configs and workspace paths for new roadmap and UI libraries.
    • Included new dependency for SVG pan and zoom functionality.
  • Style
    • Added SCSS styles with animations and gradient effects for roadmap nodes, connectors, and hover effects.
  • Bug Fixes
    • N/A (no bug fixes included in this release).

Copy link

coderabbitai bot commented Apr 4, 2025

Walkthrough

This set of changes introduces a comprehensive Angular "roadmap" feature for a blog application. It adds new feature and UI libraries, components, directives, pipes, configuration files, localization, and assets to visualize a hierarchical Angular learning roadmap. The update also configures testing, linting, and workspace settings, and integrates the roadmap into the application's routing.

Changes

File(s) / Path(s) Change Summary
libs/blog/roadmap/feature-roadmap/*
libs/blog/roadmap/ui-roadmap/*
libs/blog/roadmap/ui-roadmap-node/*
Introduced new Angular libraries for the roadmap feature and UI components. Added components for roadmap visualization, roadmap layers, SVG controls, node types (primary, secondary, cluster, angular-love), and connection directives. Included SCSS styles, test setup, Jest configs, TypeScript configs, ESLint configs, project configs, and README files.
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/*
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/*
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-svg-controls/*
libs/blog/roadmap/ui-roadmap/src/lib/components/vertical-connector-arrow/*
Added Angular components and directives for rendering connected nodes, roadmap layers, SVG controls, and vertical connector arrows. Included templates, styles, and logic for dynamic rendering and interaction.
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connection-point.directive.ts
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts
Added Angular directives to manage node connection points and groups, computing dynamic SVG paths to visually connect roadmap nodes horizontally with arcs and vertical lines.
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/left-slice.pipe.ts
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/right-slice.pipe.ts
Added Angular pipes to slice arrays into left and right halves for layout purposes in roadmap layers.
libs/blog/roadmap/ui-roadmap/src/lib/types/roadmap-node.ts Added TypeScript interfaces and types for roadmap nodes, including base, standard, cluster nodes, and a union type.
libs/blog/roadmap/ui-roadmap/src/index.ts
libs/blog/roadmap/ui-roadmap-node/src/index.ts
Added index files to re-export components, types, and modules for simplified imports.
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts
feature-roadmap.component.html
feature-roadmap.component.scss
Added the main Angular component for the roadmap feature, including logic for loading and transforming roadmap data, SVG pan/zoom integration, and rendering layers. Included template and fade-in animation styles.
libs/blog/roadmap/feature-roadmap/src/index.ts Re-exported the main feature component for external use.
libs/blog/roadmap/feature-roadmap/src/lib/build-roadmap-layers-from-dto.ts Added a module to transform roadmap node DTOs into structured roadmap layers, including logic to order nodes by previousNodeId and distinguish node types (primary, secondary, cluster).
apps/blog/src/assets/roadmap-tiles.json Added a comprehensive JSON file defining the hierarchical structure and metadata for the Angular learning roadmap.
apps/blog/src/assets/i18n/en.json
apps/blog/src/assets/i18n/pl.json
Added new localization keys for roadmap page controls in English and Polish.
libs/blog/shell/feature-shell-web/src/lib/blog-shell.routes.ts Added a new route for the roadmap feature, loading the shell component asynchronously.
libs/blog/shell/feature-shell-web/src/lib/roadmap-shell.component.ts Added a shell component for the roadmap page, integrating header, roadmap feature, and footer, with localization and ad banner logic.
libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts Updated the root shell component to set layout CSS classes on the host element.
tsconfig.base.json Added TypeScript path aliases for the new roadmap feature and UI libraries.
nx.json Updated Angular component generator defaults to set changeDetection to OnPush.
package.json Added the svg-pan-zoom dependency for interactive SVG roadmap features.
libs/blog-contracts/roadmap/* Added a new contracts library for roadmap node DTOs, including interfaces, ESLint and Jest configurations, project setup, test setup, and README.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Router
    participant RoadmapShellComponent
    participant FeatureRoadmapComponent
    participant HttpClient
    participant SVGPanZoom

    User->>Router: Navigates to '/roadmap'
    Router->>RoadmapShellComponent: Instantiates
    RoadmapShellComponent->>FeatureRoadmapComponent: Displays feature roadmap
    FeatureRoadmapComponent->>HttpClient: Loads roadmap-tiles.json
    HttpClient-->>FeatureRoadmapComponent: Returns roadmap data
    FeatureRoadmapComponent->>FeatureRoadmapComponent: Transforms data into layers
    FeatureRoadmapComponent->>SVGPanZoom: Initializes SVG pan/zoom
    User->>FeatureRoadmapComponent: Interacts with roadmap controls (zoom, reset)
    FeatureRoadmapComponent->>SVGPanZoom: Handles zoom/reset events
Loading

Poem

🐇
A roadmap unfolds, with nodes all in rows,
Connections and clusters, the learning path grows.
SVGs zoom and pan, with a click or a tap,
New pipes slice arrays, and styles overlap.
With a hop and a leap, the rabbit declares:
"Angular journeys—now mapped out with care!"
🌱


<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=HouseOfAngular/angular-love&utm_content=424):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@valueadd-robot
Copy link

PR is detected, will deploy to dev environment

@valueadd-robot
Copy link

Deployed to dev environment
Branch: feat/angular-roadmap
BFF URL: https://dd388320-blog-bff-dev.contact-ef8.workers.dev
Deploy URL: https://9b9cfb05.angular-love-client.pages.dev
Alias URL: https://feat-angular-roadmap.angular-love-client.pages.dev

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (15)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-angular-love-node.component.ts (1)

9-12: Consider extracting CSS variables to a shared file

The CSS variables defined inline are duplicated across multiple node components (primary, secondary, and this one) as seen in the relevant code snippets.

Consider extracting these variables to a shared styles file to maintain consistency and make theme changes easier. This would reduce duplication across your node components.

- style="
-      --primary-color: #B3004A; --secondary-color: #66002B; --gradient-color: #481CAB; --on-hover-border-1: #923CFF; --on-hover-border-2: #FF006A"
+ class="roadmap-node-theme"

Then define these variables in a shared SCSS file that all node components can import.

libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.spec.ts (1)

1-22: Basic test structure looks good, but could benefit from more comprehensive test coverage.

The test file correctly sets up the TestBed configuration and verifies component creation. However, for a component that manages a complex roadmap structure with SVG visuals, consider adding more test cases to verify:

  1. Proper rendering of different node types
  2. Correct layer calculations
  3. Edge cases like empty layers or single nodes
// Example of additional tests you might want to add:
it('should display layers correctly when data is provided', () => {
  // Setup test data
  component.roadmapData = mockRoadmapData;
  fixture.detectChanges();
  
  // Assert expected DOM elements
  const layerElements = fixture.nativeElement.querySelectorAll('.layer');
  expect(layerElements.length).toBe(mockRoadmapData.length);
});

it('should handle empty roadmap data gracefully', () => {
  // Test with empty data
  component.roadmapData = [];
  fixture.detectChanges();
  
  // Assert appropriate handling
  expect(fixture.nativeElement.querySelector('.no-data-message')).toBeTruthy();
});
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-primary-node.component.ts (1)

5-21: Component template could benefit from improved accessibility and maintainability.

The component is well-structured, but there are potential improvements:

  1. The hardcoded colors in the style attribute should be moved to a theme file for better maintainability
  2. Consider adding ARIA attributes for improved accessibility
  3. Color contrast should be verified for WCAG compliance
@Component({
  selector: 'al-ui-roadmap-primary-node',
  template: `
    <div
      class="roadmap-hover-border-gradient relative w-fit text-nowrap rounded-lg bg-[#FDF5FD] text-[#FDF5FD]"
-      style="
-      --primary-color: #B3004A; --secondary-color: #66002B; --gradient-color: #481CAB; --on-hover-border-1: #923CFF; --on-hover-border-2: #FF006A"
+      [ngClass]="'theme-primary'"
+      role="listitem"
+      aria-label="Primary roadmap node: {{node().title}}"
    >
      <div
        class="relative z-10 m-[4px] rounded-lg bg-[--primary-color] px-6 py-4"
      >
        <div class="text-[24px]">{{ node().title }}</div>
      </div>
    </div>
  `,
  styleUrls: ['./roadmap-hover-border-gradient.scss'],
})

Then define theme classes in your SCSS file:

// In roadmap-hover-border-gradient.scss
.theme-primary {
  --primary-color: #B3004A;
  --secondary-color: #66002B;
  --gradient-color: #481CAB;
  --on-hover-border-1: #923CFF;
  --on-hover-border-2: #FF006A;
}
libs/blog/roadmap/feature-roadmap/src/lib/slice.pipes.ts (3)

3-11: Good implementation but consider making the pipe standalone.

The LeftSlicePipe implementation is clean and functional. For modern Angular applications (v14+), it's recommended to make pipes standalone.

@Pipe({
  name: 'leftSlice',
+  standalone: true,
})
export class LeftSlicePipe implements PipeTransform {
  transform<T>(value: T[]): T[] {
    const halfLength = Math.ceil(value.length / 2);
    return value.slice(0, halfLength);
  }
}

13-21: Good implementation but consider making the pipe standalone.

The RightSlicePipe implementation is clean and functional. For modern Angular applications (v14+), it's recommended to make pipes standalone.

@Pipe({
  name: 'rightSlice',
+  standalone: true,
})
export class RightSlicePipe implements PipeTransform {
  transform<T>(value: T[]): T[] {
    const halfLength = Math.ceil(value.length / 2);
    return value.slice(halfLength);
  }
}

7-10: Consider handling edge cases in pipe implementations.

While the pipe implementations work for valid arrays, they don't handle edge cases like null or undefined values.

// For LeftSlicePipe
transform<T>(value: T[]): T[] {
+  if (!value || !Array.isArray(value)) {
+    return [];
+  }
  const halfLength = Math.ceil(value.length / 2);
  return value.slice(0, halfLength);
}

// For RightSlicePipe
transform<T>(value: T[]): T[] {
+  if (!value || !Array.isArray(value)) {
+    return [];
+  }
  const halfLength = Math.ceil(value.length / 2);
  return value.slice(halfLength);
}

Also applies to: 17-20

libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html (1)

65-66: Type casting with $any() indicates a potential type mismatch.

The use of $any() for type casting suggests that the node type doesn't match the expected cluster type. This should be resolved with proper typing.

// In the component class, add a type guard:
isCluster(node: RoadmapNode): node is RoadmapCluster {
  return node.nodeType === 'cluster';
}

// Then in the template:
<al-ui-roadmap-cluster
-  [cluster]="$any(node)"
+  [cluster]="node"
  *ngIf="isCluster(node)"
></al-ui-roadmap-cluster>

Also applies to: 77-78

libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-secondary-node.component.ts (1)

5-21: Consider accessibility and theming improvements

The component implementation is clean, but consider:

  1. The hardcoded color values like #B3004A, #66002B, etc. might be better placed in a central theme file for easier maintenance
  2. Verify that the text colors provide sufficient contrast with the background for accessibility (WCAG compliance)

You could refactor the inline styles to use application-wide CSS variables:

- style="
- --primary-color: #B3004A; --secondary-color: #66002B; --gradient-color: #481CAB; --on-hover-border-1: #923CFF; --on-hover-border-2: #FF006A"
+ style="
+ --primary-color: var(--theme-primary-color, #B3004A); --secondary-color: var(--theme-secondary-color, #66002B); --gradient-color: var(--theme-gradient-color, #481CAB); --on-hover-border-1: var(--theme-hover-border-1, #923CFF); --on-hover-border-2: var(--theme-hover-border-2, #FF006A)"
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-cluster.component.ts (2)

5-26: Well-structured template with modern Angular syntax

Great use of modern Angular control flow with the @for loop and proper tracking by node ID. The component structure is clean and follows Angular best practices.

As with the other component, consider:

  1. Extracting color values to a theme file
  2. Verifying color contrast for accessibility

27-32: Consider centralizing styling variables

The host binding approach is good, but the inline color variables could be moved to a central theme:

- style:
-   '--primary-color: #B3004A; --secondary-color: #66002B; --gradient-color: #481CAB; --on-hover-border-1: #923CFF; --on-hover-border-2: #FF006A',
+ style:
+   '--primary-color: var(--theme-primary-color, #B3004A); --secondary-color: var(--theme-secondary-color, #66002B); --gradient-color: var(--theme-gradient-color, #481CAB); --on-hover-border-1: var(--theme-hover-border-1, #923CFF); --on-hover-border-2: var(--theme-hover-border-2, #FF006A)',
libs/blog/roadmap/feature-roadmap/src/lib/ui/roadmap-hover-border-gradient.scss (1)

1-32: Well-implemented hover gradient effect with animation

The gradient border effect implementation with the ::before pseudo-element and rotation animation is clean. The exclusion of .cluster-node elements from the hover effect is handled appropriately.

Consider if the 2000px dimensions for the pseudo-element are necessary - while it works, a smaller size might be more performance-friendly, especially on mobile devices.

-    width: 2000px;
-    height: 2000px;
-    top: calc(50% - 1000px);
-    left: calc(50% - 1000px);
+    width: 1000px;
+    height: 1000px;
+    top: calc(50% - 500px);
+    left: calc(50% - 500px);
libs/blog/shell/feature-shell-web/src/lib/roadmap-shell.component.ts (3)

20-30: Simplified template without top banner or router outlet

The template only includes header, roadmap feature, and footer components, which is simpler than the RootShellComponent shown in the relevant snippets. However, the component still initializes and processes ad banner data that isn't used in the template.

If ad banners aren't intended to be shown in this view, consider removing the related code to simplify the component.


59-60: Hardcoded adBannerVisible value

The adBannerVisible computed value is hardcoded to always return false, which means the effect to set viewport offset will always set it to [0, 80].

Consider simplifying this since the value never changes:

-  // todo: temporary solution to keep in mind how banner influence the layout
-  protected readonly adBannerVisible = computed(() => false);
+  // Banner is not displayed in roadmap view
+  protected readonly adBannerVisible = false;

1-89: Component structure duplicates RootShellComponent

This component shares significant code with RootShellComponent from the provided context snippets, suggesting potential duplication.

Consider extracting common functionality to a base class or shared service to avoid duplication between shell components. Alternatively, evaluate if this component could extend RootShellComponent and override only what's different.

libs/blog/roadmap/feature-roadmap/src/lib/temp.component.ts (1)

46-49: Multiple calls to generatePath could accumulate paths in pathData.
Each call appends a new path to pathData. If the intended behavior is to render multiple paths, this is fine. Otherwise, consider clearing or resetting the array before subsequent calls to avoid unintentional duplication.

ngOnInit(): void {
-  this.generatePath();
-  this.generatePath(120);
-  this.generatePath(300);
+  this.pathData = [];
+  [0, 120, 300].forEach((len) => this.generatePath(len));
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 860a9dc and 1fc466b.

📒 Files selected for processing (23)
  • libs/blog/roadmap/feature-roadmap/.eslintrc.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/README.md (1 hunks)
  • libs/blog/roadmap/feature-roadmap/jest.config.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/project.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/index.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.spec.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/slice.pipes.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/temp.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/roadmap-hover-border-gradient.scss (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-angular-love-node.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-cluster.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-primary-node.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-secondary-node.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/test-setup.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/tsconfig.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/tsconfig.lib.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/tsconfig.spec.json (1 hunks)
  • libs/blog/shell/feature-shell-web/src/lib/blog-shell.routes.ts (1 hunks)
  • libs/blog/shell/feature-shell-web/src/lib/roadmap-shell.component.ts (1 hunks)
  • libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts (1 hunks)
  • tsconfig.base.json (1 hunks)
🧰 Additional context used
🧬 Code Definitions (5)
libs/blog/shell/feature-shell-web/src/lib/roadmap-shell.component.ts (2)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (1)
  • Component (39-214)
libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts (1)
  • Component (22-109)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-primary-node.component.ts (3)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-angular-love-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-secondary-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (2)
  • Component (39-214)
  • RoadmapNode (23-27)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-angular-love-node.component.ts (3)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-primary-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-secondary-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (2)
  • Component (39-214)
  • RoadmapNode (23-27)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-secondary-node.component.ts (3)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-angular-love-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-primary-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (2)
  • Component (39-214)
  • RoadmapNode (23-27)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (4)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-angular-love-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-primary-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-cluster.component.ts (1)
  • Component (5-37)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-secondary-node.component.ts (1)
  • Component (5-24)
🪛 Biome (1.9.4)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts

[error] 106-106: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 161-161: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 180-180: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🔇 Additional comments (23)
libs/blog/roadmap/feature-roadmap/src/index.ts (1)

1-1: Clear Public API Export

The export statement correctly re-exports all public members from the FeatureRoadmapComponent, making it easier to import from a single entry point. This meets the goal of simplifying the public API for the roadmap feature.

libs/blog/roadmap/feature-roadmap/README.md (1)

1-8: Informative README File

The README provides a succinct description of the library and clear instructions on running the unit tests (i.e., using nx test feature-roadmap). Including the Nx reference is helpful for developers who might be new to this setup.

libs/blog/roadmap/feature-roadmap/src/test-setup.ts (1)

1-6: Robust Test Environment Configuration

The use of setupZoneTestEnv with strict options (errorOnUnknownElements and errorOnUnknownProperties set to true) establishes a solid testing foundation. This configuration will help catch unintended template errors and is aligned with best practices for Angular testing.

libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts (1)

57-59: Effective Host Styling Addition

The new host property that applies the classes 'flex flex-col min-h-screen' is a clean solution to enforce the desired layout on the component’s host element. This change is well-targeted and integrates seamlessly with the component’s styling strategy.

tsconfig.base.json (1)

191-193: Accurate Module Path Mapping

The newly added path alias "@angular-love/feature-roadmap" correctly points to libs/blog/roadmap/feature-roadmap/src/index.ts. This change facilitates easier imports and enhances TypeScript module resolution for the roadmap feature.

libs/blog/roadmap/feature-roadmap/tsconfig.lib.json (1)

1-17: Configuration looks appropriate for an Angular library

The TypeScript configuration extends the base config and sets up standard library options:

  • Output directory correctly points to the dist folder
  • Declaration files and source maps are enabled for better debugging
  • Test files are properly excluded
  • Only TypeScript files from src are included

This follows standard practices for Angular libraries built with Nx.

libs/blog/roadmap/feature-roadmap/project.json (1)

1-20: Project configuration follows Nx standards

The configuration properly defines:

  • Project name, schema, and source root paths
  • Angular component prefix "al" for consistent component naming
  • Library project type
  • Appropriate tags for monorepo organization
  • Test and lint targets with correct executors

This setup aligns with Nx workspace best practices and will integrate well with the existing architecture.

libs/blog/shell/feature-shell-web/src/lib/blog-shell.routes.ts (1)

24-30: Route implementation follows Angular best practices

The new roadmap route:

  • Is correctly added to the commonRoutes array
  • Uses lazy loading with async/await pattern
  • Matches the module structure pattern used by other routes

This implementation maintains consistency with the existing routing architecture.

libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-angular-love-node.component.ts (3)

1-4: Imports look correct

Angular core imports and the RoadmapNode interface are properly imported.


5-21: Component definition follows Angular standards

The template implementation:

  • Uses Angular's standalone component pattern
  • Follows the project's naming conventions with the 'al-' prefix
  • Includes appropriate styling with CSS variables for theming
  • Uses consistent styling approach with the related node components

The gradient styling and CSS variables approach allows for flexible theming.


22-24: Input property correctly implemented

The component:

  • Uses Angular's modern input API
  • Properly marks the node input as required
  • Correctly types the input with the RoadmapNode interface

This ensures type safety and clear component API design.

libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-primary-node.component.ts (1)

22-24: Component class implementation looks good.

The implementation of the component class with a required input is clean and follows Angular's best practices for modern input syntax.

libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html (1)

1-88: Overall structure is complex but well-organized.

The SVG-based roadmap visualization is complex but follows a logical structure. Consider adding comments to explain the purpose of different sections for improved maintainability.

libs/blog/roadmap/feature-roadmap/.eslintrc.json (1)

1-36: ESLint configuration looks good

The configuration properly extends the root ESLint config and sets up appropriate rules for Angular components and directives. The prefix "al" is used consistently across the feature components, which is a good practice for maintaining a clear namespace.

libs/blog/roadmap/feature-roadmap/jest.config.ts (1)

1-21: Jest configuration is properly set up

The configuration correctly sets up Jest for Angular testing with appropriate transformations, serializers, and coverage reporting. The setup aligns with best practices for Angular libraries.

libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-secondary-node.component.ts (2)

1-4: Imports look good

The component correctly imports the necessary dependencies and the RoadmapNode type from the feature component.


22-24: Input usage looks good

The component correctly uses Angular's input() API with required type checking, which is a modern approach.

libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-cluster.component.ts (2)

1-4: Imports look good

The component correctly imports the necessary dependencies and the RoadmapCluster type.


33-37: Input usage looks good

The component correctly uses Angular's input() API with required type checking for the cluster property.

libs/blog/roadmap/feature-roadmap/tsconfig.spec.json (1)

1-16: Configuration looks good for Jest testing in Angular

The TypeScript configuration properly extends the base config and includes all necessary settings for Jest-based testing, including correct module settings, target ECMAScript version, and type definitions.

libs/blog/roadmap/feature-roadmap/tsconfig.json (1)

1-28: Good TypeScript configuration with strict settings

The TypeScript configuration appropriately enables strict type checking and follows Angular best practices with the compiler options. The references to both lib and spec configurations are correctly set up.

libs/blog/roadmap/feature-roadmap/src/lib/temp.component.ts (2)

52-88: Looks good!
The SVG path generation logic is solid, and dynamically adjusting the SVG's dimensions to fit the path is a nice touch.


23-33:

❓ Verification inconclusive

Potential structural directive concern with @for.
This @for (path of pathData; track $index) syntax is non-standard in Angular unless you're intentionally using experimental features or a custom structural directive. Ensure it's functioning as expected in your application.

Run this command to confirm whether a custom structural directive for @for is defined:


🏁 Script executed:

#!/bin/bash
fd -e ts --exec rg "@Directive" 

Length of output: 226


Attention: Verify Custom @for Directive Implementation

The template syntax @for (path of pathData; track $index) remains non-standard in Angular. Although our initial scan returned multiple occurrences of @Directive({ in the codebase, we found no clear evidence of a custom structural directive specifically implemented for @for.

  • Please confirm that a custom directive supporting the @for syntax is intentionally defined (e.g., with a selector like [for]) and documented.
  • If this syntax was not intended, consider reverting to the standard Angular *ngFor syntax to prevent potential runtime issues.

Comment on lines +18 to +40
@Component({
selector: 'al-root-shell',
template: `
<al-header
class="block w-full"
[language]="language()"
(languageChange)="onLanguageChange($event)"
>
<al-search />
</al-header>
<al-feature-roadmap class="flex-1 basis-0" />
<al-footer />
`,
imports: [
HeaderComponent,
FooterComponent,
SearchComponent,
FeatureRoadmapComponent,
],
host: {
class: 'flex flex-col min-h-screen',
},
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Selector name doesn't match component name

The selector al-root-shell doesn't match the component class name RoadmapShellComponent. This could lead to confusion since typically selectors should reflect the component name.

Change the selector to match the component name:

@Component({
-  selector: 'al-root-shell',
+  selector: 'al-roadmap-shell',
  template: `
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Component({
selector: 'al-root-shell',
template: `
<al-header
class="block w-full"
[language]="language()"
(languageChange)="onLanguageChange($event)"
>
<al-search />
</al-header>
<al-feature-roadmap class="flex-1 basis-0" />
<al-footer />
`,
imports: [
HeaderComponent,
FooterComponent,
SearchComponent,
FeatureRoadmapComponent,
],
host: {
class: 'flex flex-col min-h-screen',
},
})
@Component({
selector: 'al-roadmap-shell',
template: `
<al-header
class="block w-full"
[language]="language()"
(languageChange)="onLanguageChange($event)"
>
<al-search />
</al-header>
<al-feature-roadmap class="flex-1 basis-0" />
<al-footer />
`,
imports: [
HeaderComponent,
FooterComponent,
SearchComponent,
FeatureRoadmapComponent,
],
host: {
class: 'flex flex-col min-h-screen',
},
})

Comment on lines +80 to +88
constructor(viewport: ViewportScroller) {
// todo: temporary solution to keep in mind how banner influence the layout
effect(() => {
this.adBannerVisible()
? viewport.setOffset([0, 160])
: viewport.setOffset([0, 80]);
});
this.sliderStore.getData();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unnecessary data fetching for unused banners

The component calls this.sliderStore.getData() but doesn't display the data in the template.

If the ad banner data isn't needed in this view, remove the data fetch:

  constructor(viewport: ViewportScroller) {
    // todo: temporary solution to keep in mind how banner influence the layout
    effect(() => {
      this.adBannerVisible()
        ? viewport.setOffset([0, 160])
        : viewport.setOffset([0, 80]);
    });
-    this.sliderStore.getData();
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor(viewport: ViewportScroller) {
// todo: temporary solution to keep in mind how banner influence the layout
effect(() => {
this.adBannerVisible()
? viewport.setOffset([0, 160])
: viewport.setOffset([0, 80]);
});
this.sliderStore.getData();
}
constructor(viewport: ViewportScroller) {
// todo: temporary solution to keep in mind how banner influence the layout
effect(() => {
this.adBannerVisible()
? viewport.setOffset([0, 160])
: viewport.setOffset([0, 80]);
});
}

Comment on lines +42 to +55
protected readonly sliderStore = inject(AdBannerStore);
protected readonly slides = computed<AdImageBanner[] | undefined>(() =>
this.sliderStore.slider()?.slides.map((slide) => ({
url: slide.url,
alt: slide.alt,
action: {
type: 'url',
url: slide.navigateTo,
},
})),
);
protected readonly msPerSlide = computed(
() => this.sliderStore.slider()?.slideDisplayTimeMs,
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Unused ad banner functionality

The component processes ad banner data (slides and msPerSlide) but doesn't use it in the template.

Since the template doesn't display an ad banner carousel, remove the unused code:

-  protected readonly slides = computed<AdImageBanner[] | undefined>(() =>
-    this.sliderStore.slider()?.slides.map((slide) => ({
-      url: slide.url,
-      alt: slide.alt,
-      action: {
-        type: 'url',
-        url: slide.navigateTo,
-      },
-    })),
-  );
-  protected readonly msPerSlide = computed(
-    () => this.sliderStore.slider()?.slideDisplayTimeMs,
-  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected readonly sliderStore = inject(AdBannerStore);
protected readonly slides = computed<AdImageBanner[] | undefined>(() =>
this.sliderStore.slider()?.slides.map((slide) => ({
url: slide.url,
alt: slide.alt,
action: {
type: 'url',
url: slide.navigateTo,
},
})),
);
protected readonly msPerSlide = computed(
() => this.sliderStore.slider()?.slideDisplayTimeMs,
);
protected readonly sliderStore = inject(AdBannerStore);

@valueadd-robot
Copy link

PR is detected, will deploy to dev environment

@valueadd-robot
Copy link

Deployed to dev environment
Branch: feat/angular-roadmap
BFF URL: https://412cd3ad-blog-bff-dev.contact-ef8.workers.dev
Deploy URL: https://f9fa86df.angular-love-client.pages.dev
Alias URL: https://feat-angular-roadmap.angular-love-client.pages.dev

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (5)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html (1)

29-38: ⚠️ Potential issue

SVG marker ID could cause conflicts with multiple component instances.

The hardcoded id="arrowhead" could cause conflicts if multiple instances of this component exist on the same page.

Generate unique IDs for SVG markers by incorporating the layer's parent node ID:

-    id="arrowhead"
+    [attr.id]="'arrowhead-' + layer.parentNode.id"

And update the reference:

-    marker-end="url(#arrowhead)"
+    [attr.marker-end]="'url(#arrowhead-' + layer.parentNode.id + ')'"
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (4)

52-52: ⚠️ Potential issue

Incorrect Angular configuration with styleUrl.

Angular expects styleUrls (plural) as an array instead of the singular styleUrl property.

- styleUrl: './feature-roadmap.component.scss',
+ styleUrls: ['./feature-roadmap.component.scss'],

103-103: 🛠️ Refactor suggestion

Avoid spread syntax in reducers for better performance.

Using spread syntax in reducers causes O(n²) time complexity which can impact performance with larger datasets.

- (acc, node) => ({ ...acc, [node.id]: node }),
+ (acc, node) => {
+   acc[node.id] = node;
+   return acc;
+ },
🧰 Tools
🪛 Biome (1.9.4)

[error] 103-103: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


157-161: 🛠️ Refactor suggestion

Optimize the cluster creation reducer.

Using spread syntax in reducers causes O(n²) time complexity which can impact performance with larger datasets.

- (acc, primaryNodeId) => ({
-   ...acc,
-   [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']: primaryNodeId,
- }),
+ (acc, primaryNodeId) => {
+   acc[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode'] = primaryNodeId;
+   return acc;
+ },
🧰 Tools
🪛 Biome (1.9.4)

[error] 158-158: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


176-180: 🛠️ Refactor suggestion

Minimize object creation overhead in layer mapping.

Using spread syntax in reducers causes O(n²) time complexity which can impact performance with larger datasets.

- (acc, primaryNodeId) => ({
-   ...acc,
-   [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']: primaryNodeId,
- }),
+ (acc, primaryNodeId) => {
+   acc[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode'] = primaryNodeId;
+   return acc;
+ },
🧰 Tools
🪛 Biome (1.9.4)

[error] 177-177: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🧹 Nitpick comments (3)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (3)

56-99: Consider externalizing roadmap data.

Hard-coding the roadmap data directly in the component makes it difficult to maintain, update, or translate.

Consider moving this data to a separate service or configuration file:

// roadmap-data.service.ts
@Injectable({
  providedIn: 'root'
})
export class RoadmapDataService {
  getRoadmapNodes(): RoadmapNodeDTO[] {
    return [
      {
        id: '2',
        title: 'Components',
      },
      // ... other nodes
    ];
  }
}

// Then in your component:
constructor(private roadmapDataService: RoadmapDataService) {
  this.nodesDto.set(this.roadmapDataService.getRoadmapNodes());
}

115-118: Avoid array spread for better performance.

The spread operator creates a new array on each iteration, which is inefficient for large datasets.

- clusterMap[parentClusterNodeDto.id] = [
-   ...(clusterMap[parentClusterNodeDto.id] ?? []),
-   nodeDto.id,
- ];
+ if (!clusterMap[parentClusterNodeDto.id]) {
+   clusterMap[parentClusterNodeDto.id] = [];
+ }
+ clusterMap[parentClusterNodeDto.id].push(nodeDto.id);

130-133: Avoid array spread for better performance.

The spread operator creates a new array on each iteration, which is inefficient for large datasets.

- layerMap[nodeDto.parentNodeId] = [
-   ...(layerMap[nodeDto.parentNodeId] ?? []),
-   nodeDto.id,
- ];
+ if (!layerMap[nodeDto.parentNodeId]) {
+   layerMap[nodeDto.parentNodeId] = [];
+ }
+ layerMap[nodeDto.parentNodeId].push(nodeDto.id);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc466b and 8956a43.

📒 Files selected for processing (4)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/secondary-arrow.pipe.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss
🧰 Additional context used
🧬 Code Definitions (1)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (4)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-angular-love-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-secondary-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-primary-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-cluster.component.ts (1)
  • Component (5-37)
🪛 Biome (1.9.4)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts

[error] 103-103: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 158-158: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 177-177: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🔇 Additional comments (1)
libs/blog/roadmap/feature-roadmap/src/lib/secondary-arrow.pipe.ts (1)

19-26: LGTM! Implementation is clear and well-documented.

The pipe effectively handles different cases for arrow generation with clear documentation. The arc calculation is properly implemented to create accurate curved corners.

xmlns:svg="http://www.w3.org/1999/html"
>
<svg:foreignObject
style="transform: translateX(calc(50% - {{ layoutEl.clientWidth / 2 }}px))"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Template reference variable used before definition could cause rendering issues.

The translateX calculation references layoutEl.clientWidth before the #layoutEl element is fully rendered, which may cause incorrect initial positioning or flickering.

Consider using Angular's AfterViewInit lifecycle hook with ViewChild to calculate the translation after the view is fully rendered:

// In the component class
@ViewChild('layoutEl') layoutEl: ElementRef;
private translationOffset = signal(0);

ngAfterViewInit() {
  // Calculate after view is initialized
  this.translationOffset.set(this.layoutEl.nativeElement.clientWidth / 2);
}

Then update your template:

- style="transform: translateX(calc(50% - {{ layoutEl.clientWidth / 2 }}px))"
+ style="transform: translateX(calc(50% - {{ translationOffset() }}px))"

Comment on lines 63 to 65
@let shift =
(allChildNodesEl.clientWidth / 2 - leftChildNodesEl.clientWidth ||
0) - 32;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Layout calculation uses template references before initialization.

The calculation for shift uses allChildNodesEl and leftChildNodesEl template references potentially before they're fully initialized, which could lead to incorrect positioning.

Consider using a reactive approach with ViewChildren and AfterViewInit:

@ViewChildren('allChildNodesEl') allChildNodesElements: QueryList<ElementRef>;
@ViewChildren('leftChildNodesEl') leftChildNodesElements: QueryList<ElementRef>;
private nodeShifts = signal<{[key: string]: number}>({});

ngAfterViewInit() {
  // Calculate shifts after view is initialized
  this.calculateChildNodeShifts();
}

private calculateChildNodeShifts() {
  // Calculate shifts for each layer's child nodes
  // Store in nodeShifts signal for reactive updates
}

Then in your template:

- @let shift =
-   (allChildNodesEl.clientWidth / 2 - leftChildNodesEl.clientWidth || 0) - 32;
+ @let shift = nodeShifts()[layer.parentNode.id] || 0;

Comment on lines 101 to 242
if (nodeDto.parentNodeId) {
if (nodeDtoMap[nodeDto.parentNodeId].parentNodeId) {
const parentClusterNodeDto = nodeDtoMap[nodeDto.parentNodeId];

clusterMap[parentClusterNodeDto.id] = [
...(clusterMap[parentClusterNodeDto.id] ?? []),
nodeDto.id,
];

if (nodeMap[nodeDto.parentNodeId]) {
nodeMap[parentClusterNodeDto.id].nodeType = 'cluster';
} else {
nodeMap[parentClusterNodeDto.id] = {
id: parentClusterNodeDto.id,
nodeType: 'cluster',
title: parentClusterNodeDto.title,
};
}
} else {
layerMap[nodeDto.parentNodeId] = [
...(layerMap[nodeDto.parentNodeId] ?? []),
nodeDto.id,
];
}
if (!nodeMap[nodeDto.id]) {
nodeMap[nodeDto.id] = {
id: nodeDto.id,
nodeType: 'secondary',
title: nodeDto.title,
};
}
} else {
nodeMap[nodeDto.id] = {
id: nodeDto.id,
nodeType: 'primary',
title: nodeDto.title,
};
if (!layerMap[nodeDto.id]) {
layerMap[nodeDto.id] = [];
}
}
});

// setup clusters
Object.entries(clusterMap).forEach(([clusterNodeId, childrenNodeIds]) => {
const previousClusterNodeIdToNodeIdMap = childrenNodeIds.reduce(
(acc, primaryNodeId) => ({
...acc,
[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']:
primaryNodeId,
}),
{} as { [previousNodeId: string | 'initialNode']: string },
);

const clusterNode = nodeMap[clusterNodeId] as RoadmapCluster;
clusterNode.clusteredNodes = [];
let nextNodeId = previousClusterNodeIdToNodeIdMap['initialNode'];
while (nextNodeId) {
clusterNode.clusteredNodes.push(nodeMap[nextNodeId]);
nextNodeId = previousClusterNodeIdToNodeIdMap[nextNodeId];
}
});

// setup layers
const previousLayerNodeIdToNodeIdMap = Object.keys(layerMap).reduce(
(acc, primaryNodeId) => ({
...acc,
[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']:
primaryNodeId,
}),
{} as { [previousNodeId: string | 'initialNode']: string },
);

const layers: {
parentNode: RoadmapNode;
childNodes: RoadmapNode[];
}[] = [];
let nextParentNodeId = previousLayerNodeIdToNodeIdMap['initialNode'];
while (nextParentNodeId) {
layers.push({
parentNode: nodeMap[nextParentNodeId],
childNodes: layerMap[nextParentNodeId].map(
(childrenNodeId) => nodeMap[childrenNodeId],
),
});
nextParentNodeId = previousLayerNodeIdToNodeIdMap[nextParentNodeId];
}

return [
{
parentNode: {
id: '1',
title: 'Angular.Love Roadmap Introduction',
nodeType: 'angular-love',
},
childNodes: [],
},
...layers,
];
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Refactor complex roadmap calculation into smaller functions.

The roadmap calculation logic is very complex and contained in a single computed function, making it difficult to understand and maintain.

Break down the logic into smaller, focused functions:

protected readonly roadmapLayers = computed<RoadmapLayer[]>(() => {
  const nodeDtoMap = this.createNodeDtoMap();
  const { layerMap, clusterMap, nodeMap } = this.createMaps(nodeDtoMap);
  
  this.setupClusters(clusterMap, nodeMap, nodeDtoMap);
  const layers = this.setupLayers(layerMap, nodeMap, nodeDtoMap);
  
  return [this.createRootLayer(), ...layers];
});

private createNodeDtoMap(): { [nodeId: string]: RoadmapNodeDTO } {
  // Extract node map creation logic
}

private createMaps(nodeDtoMap: { [nodeId: string]: RoadmapNodeDTO }): {
  layerMap: { [parentNodeId: string]: string[] },
  clusterMap: { [clusterNodeId: string]: string[] },
  nodeMap: { [nodeId: string]: RoadmapNode }
} {
  // Extract maps creation logic
}

// Additional methods for other parts of the logic
🧰 Tools
🪛 Biome (1.9.4)

[error] 103-103: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 158-158: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 177-177: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (4)

78-78: Use the correct Angular property for component styles.

In Angular, the correct property is styleUrls (plural), not styleUrl. Using styleUrl can lead to build or runtime issues.

- styleUrl: './feature-roadmap.component.scss',
+ styleUrls: ['./feature-roadmap.component.scss'],

169-169: Avoid spread syntax in a .reduce() accumulator for better performance.

Constructing a new object on each iteration using spread (...acc) is potentially O(n^2). Use a mutable approach (direct assignment) to improve performance.

- (acc, node) => ({ ...acc, [node.id]: node }),
+ (acc, node) => {
+   acc[node.id] = node;
+   return acc;
+ },
🧰 Tools
🪛 Biome (1.9.4)

[error] 169-169: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


224-224: Avoid spread syntax in a .reduce() accumulator for better performance.

Same rationale: every iteration triggers a new object creation, leading to suboptimal time complexity.

- (acc, primaryNodeId) => ({
-   ...acc,
-   [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']: primaryNodeId,
- }),
+ (acc, primaryNodeId) => {
+   acc[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode'] = primaryNodeId;
+   return acc;
+ },
🧰 Tools
🪛 Biome (1.9.4)

[error] 224-224: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


243-243: Avoid spread syntax in a .reduce() accumulator for better performance.

Repeated usage of spread on an accumulator can degrade performance at scale. Switching to direct assignment is more efficient.

- (acc, primaryNodeId) => ({
-   ...acc,
-   [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']: primaryNodeId,
- }),
+ (acc, primaryNodeId) => {
+   acc[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode'] = primaryNodeId;
+   return acc;
+ },
🧰 Tools
🪛 Biome (1.9.4)

[error] 243-243: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🧹 Nitpick comments (2)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-svg-control.component.ts (2)

14-14: Remove the this. notation in the template for readability.

In Angular templates, referencing signal calls with this. is unconventional. You can safely remove this. for brevity since it’s implicit in the template context.

- <fast-svg [name]="this.iconName()" [size]="this.size()" />
+ <fast-svg [name]="iconName()" [size]="size()" />

19-21: Rename the "event" property to avoid confusion.

Using event as a property name can be confusing since it’s commonly associated with the DOM event object. Consider naming it something more descriptive, like resizeAction, to clarify its purpose.

- readonly event = input.required<EventType>();
+ readonly resizeAction = input.required<EventType>();

- (click)="resizeRoadmap.emit(this.event())"
+ (click)="resizeRoadmap.emit(this.resizeAction())"
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8956a43 and 9a78f90.

⛔ Files ignored due to path filters (5)
  • apps/blog/src/assets/icons/circle-center.svg is excluded by !**/*.svg
  • apps/blog/src/assets/icons/zoom-in.svg is excluded by !**/*.svg
  • apps/blog/src/assets/icons/zoom-out.svg is excluded by !**/*.svg
  • apps/blog/src/assets/icons/zoom-reset.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-svg-control.component.ts (1 hunks)
  • package.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html
🧰 Additional context used
🧬 Code Graph Analysis (2)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-svg-control.component.ts (1)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (2)
  • Component (64-290)
  • EventType (25-25)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (4)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-cluster.component.ts (1)
  • Component (5-37)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-svg-control.component.ts (1)
  • Component (6-24)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-primary-node.component.ts (1)
  • Component (5-24)
libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-secondary-node.component.ts (1)
  • Component (5-24)
🪛 Biome (1.9.4)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts

[error] 169-169: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 224-224: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 243-243: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🔇 Additional comments (1)
package.json (1)

66-66:

✅ Verification successful

Confirm and verify the security status of the newly added package.

The addition of "svg-pan-zoom": "^3.6.2" appears beneficial for the SVG pan-and-zoom functionality. However, let's make sure there are no known security advisories or deprecated versions.

Please run the following script to check known vulnerabilities or advisories for this NPM package:


🏁 Script executed:

#!/bin/bash
# Description: Query GitHub for security vulnerabilities related to the "svg-pan-zoom" package in the npm ecosystem

gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: NPM, package: "svg-pan-zoom") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'
'

Length of output: 462


Action: Verify Vulnerability Query for "svg-pan-zoom" Package

The initial security check for "svg-pan-zoom": "^3.6.2" resulted in an empty vulnerabilities list but also produced a quoting error (unexpected EOF while looking for matching '\''), which prevents us from confidently confirming the package’s security status. Please re-run the query with corrected quoting or verify manually to ensure no vulnerabilities or deprecations exist. For instance, you can try this revised script:

  • File: package.json, Line 66: "svg-pan-zoom": "^3.6.2"
  • Next Steps:
    • Re-run the above script (or an equivalent manual check) to verify that no vulnerabilities are reported for this package.
    • Confirm that the query now returns a valid response (ideally with an empty nodes array) without any syntax errors.

Once you’ve confirmed the security status manually, please update the review accordingly.


🏁 Script executed:

#!/bin/bash
# Revised script to query GitHub for known security vulnerabilities for the "svg-pan-zoom" package using a corrected quoting approach.
gh api graphql -f query="$(cat <<'EOF'
{
  securityVulnerabilities(first: 5, ecosystem: NPM, package: "svg-pan-zoom") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}
EOF
)"

Length of output: 473


Action: Confirm Package "svg-pan-zoom" Security Status

The GitHub GraphQL API query executed with the revised script returned an empty vulnerabilities list, indicating that no known security issues were found for "svg-pan-zoom": "^3.6.2". Please ensure that this verification covers your security standards, but based on the query, the package appears secure for use.

  • File: package.json, Line 66: "svg-pan-zoom": "^3.6.2"

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (8)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html (3)

10-12: Template reference variable used before definition could cause rendering issues.

The translateX calculation references layoutEl.clientWidth before the #layoutEl element is fully rendered, which may cause incorrect initial positioning or flickering.

Consider using Angular's AfterViewInit lifecycle hook with ViewChild to calculate the translation after the view is fully rendered:

// In the component class
@ViewChild('layoutEl') layoutEl: ElementRef;
private translationOffset = signal(0);

ngAfterViewInit() {
  // Calculate after view is initialized
  this.translationOffset.set(this.layoutEl.nativeElement.clientWidth / 2);
}

Then update your template:

- style="transform: translateX(calc(50% - {{
-   layoutEl.clientWidth / 2
- }}px))"
+ style="transform: translateX(calc(50% - {{ translationOffset() }}px))"

30-39: ⚠️ Potential issue

SVG marker ID could cause conflicts with multiple component instances.

The current implementation uses a hardcoded ID for the SVG arrowhead marker, which could cause conflicts if multiple instances of the component are rendered on the same page.

<marker
-  id="arrowhead"
+  [attr.id]="'arrowhead-' + layer.parentNode.id"
  markerWidth="6"
  markerHeight="8"
  refX="0"
  refY="4"
  orient="auto"
>
  <polygon points="0 0, 6 4, 0 8" fill="#FDF5FD" />
</marker>

And update the reference:

<line
  [attr.y2]="layerHeightWithGap"
  x1="50"
  y1="0"
  x2="50"
  stroke="white"
  stroke-width="4"
-  marker-end="url(#arrowhead)"
+  [attr.marker-end]="'url(#arrowhead-' + layer.parentNode.id + ')'"
/>

64-66: ⚠️ Potential issue

Layout calculation uses template references before initialization.

The calculation for shift uses allChildNodesEl and leftChildNodesEl template references potentially before they're fully initialized, which could lead to incorrect positioning.

Consider using a reactive approach with ViewChildren and AfterViewInit:

@ViewChildren('allChildNodesEl') allChildNodesElements: QueryList<ElementRef>;
@ViewChildren('leftChildNodesEl') leftChildNodesElements: QueryList<ElementRef>;
private nodeShifts = signal<{[key: string]: number}>({});

ngAfterViewInit() {
  // Calculate shifts after view is initialized
  this.calculateChildNodeShifts();
}

private calculateChildNodeShifts() {
  // Calculate shifts for each layer's child nodes
  // Store in nodeShifts signal for reactive updates
}

Then in your template:

- @let shift =
-   (allChildNodesEl.clientWidth / 2 - leftChildNodesEl.clientWidth || 0) - 32;
+ @let shift = nodeShifts()[layer.parentNode.id] || 0;
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (5)

129-242: Refactor complex roadmap calculation into smaller functions.

The roadmap calculation logic is very complex and contained in a single computed function, making it difficult to understand and maintain.

Break down the logic into smaller, focused functions:

protected readonly roadmapLayers = computed<RoadmapLayer[]>(() => {
  const nodesDto = this.nodesDto();
  if (!nodesDto) return [];
  
  const nodeDtoMap = this.createNodeDtoMap(nodesDto);
  const { layerMap, clusterMap, nodeMap } = this.createMaps(nodesDto, nodeDtoMap);
  
  this.setupClusters(clusterMap, nodeMap, nodeDtoMap);
  const layers = this.setupLayers(layerMap, nodeMap, nodeDtoMap);
  
  return [this.createRootLayer(), ...layers];
});

private createNodeDtoMap(nodesDto: RoadmapNodeDTO[]): { [nodeId: string]: RoadmapNodeDTO } {
  // Extract node map creation logic
  return nodesDto.reduce((acc, node) => {
    acc[node.id] = node;
    return acc;
  }, {} as { [nodeId: string]: RoadmapNodeDTO });
}

private createMaps(nodesDto: RoadmapNodeDTO[], nodeDtoMap: { [nodeId: string]: RoadmapNodeDTO }): {
  layerMap: { [parentNodeId: string]: string[] },
  clusterMap: { [clusterNodeId: string]: string[] },
  nodeMap: { [nodeId: string]: RoadmapNode }
} {
  // Extract maps creation logic
  // ...
}

// Additional methods for other parts of the logic
🧰 Tools
🪛 Biome (1.9.4)

[error] 133-133: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 188-188: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 207-207: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


77-77: ⚠️ Potential issue

Likely Angular configuration issue with styleUrl.

Angular expects a styleUrls array instead of styleUrl. This could cause runtime or compilation issues.

- styleUrl: './feature-roadmap.component.scss',
+ styleUrls: ['./feature-roadmap.component.scss'],

132-135: 🛠️ Refactor suggestion

Performance issue with spread operator in reducer.

Using the spread syntax inside .reduce(...) can degrade performance at scale with O(n²) time complexity.

- const nodeDtoMap = nodesDto.reduce(
-   (acc, node) => ({ ...acc, [node.id]: node }),
-   {} as { [nodeId: string]: RoadmapNodeDTO },
- );
+ const nodeDtoMap = nodesDto.reduce(
+   (acc, node) => {
+     acc[node.id] = node;
+     return acc;
+   },
+   {} as { [nodeId: string]: RoadmapNodeDTO },
+ );
🧰 Tools
🪛 Biome (1.9.4)

[error] 133-133: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


186-193: 🛠️ Refactor suggestion

Performance issue with spread operator in reducer.

Using the spread syntax inside .reduce(...) can degrade performance at scale with O(n²) time complexity.

- const previousClusterNodeIdToNodeIdMap = childrenNodeIds.reduce(
-   (acc, primaryNodeId) => ({
-     ...acc,
-     [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']:
-       primaryNodeId,
-   }),
-   {} as { [previousNodeId: string | 'initialNode']: string },
- );
+ const previousClusterNodeIdToNodeIdMap = childrenNodeIds.reduce(
+   (acc, primaryNodeId) => {
+     acc[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode'] = primaryNodeId;
+     return acc;
+   },
+   {} as { [previousNodeId: string | 'initialNode']: string },
+ );
🧰 Tools
🪛 Biome (1.9.4)

[error] 188-188: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


205-212: 🛠️ Refactor suggestion

Performance issue with spread operator in reducer.

Using the spread syntax inside .reduce(...) can degrade performance at scale with O(n²) time complexity.

- const previousLayerNodeIdToNodeIdMap = Object.keys(layerMap).reduce(
-   (acc, primaryNodeId) => ({
-     ...acc,
-     [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']:
-       primaryNodeId,
-   }),
-   {} as { [previousNodeId: string | 'initialNode']: string },
- );
+ const previousLayerNodeIdToNodeIdMap = Object.keys(layerMap).reduce(
+   (acc, primaryNodeId) => {
+     acc[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode'] = primaryNodeId;
+     return acc;
+   },
+   {} as { [previousNodeId: string | 'initialNode']: string },
+ );
🧰 Tools
🪛 Biome (1.9.4)

[error] 207-207: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🧹 Nitpick comments (3)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (3)

145-148: Performance issue with array spread operator.

Using the spread operator to append elements to an array can be inefficient, especially in a loop or with large datasets.

- clusterMap[parentClusterNodeDto.id] = [
-   ...(clusterMap[parentClusterNodeDto.id] ?? []),
-   nodeDto.id,
- ];
+ if (!clusterMap[parentClusterNodeDto.id]) {
+   clusterMap[parentClusterNodeDto.id] = [];
+ }
+ clusterMap[parentClusterNodeDto.id].push(nodeDto.id);

160-163: Performance issue with array spread operator.

Using the spread operator to append elements to an array can be inefficient, especially in a loop or with large datasets.

- layerMap[nodeDto.parentNodeId] = [
-   ...(layerMap[nodeDto.parentNodeId] ?? []),
-   nodeDto.id,
- ];
+ if (!layerMap[nodeDto.parentNodeId]) {
+   layerMap[nodeDto.parentNodeId] = [];
+ }
+ layerMap[nodeDto.parentNodeId].push(nodeDto.id);

244-255: Improve error handling for SVG Pan Zoom initialization.

The current SVG Pan Zoom initialization lacks error handling, which could lead to silent failures if the library fails to load or initialize properly.

private async initSvgPanZoom() {
+ try {
    const svgPanZoomModule = await import('svg-pan-zoom');
    const svgPanZoom: SvgPanZoom.Instance =
      (svgPanZoomModule as any)['default'] || svgPanZoomModule;

    const svgRoadmap = this._svgRoadmap();
    if (svgRoadmap) {
      this._svgPanZoom = svgPanZoom(svgRoadmap.nativeElement, {
        ...svgPanZoomInitialConfig,
      });
    }
+ } catch (error) {
+   console.error('Failed to initialize SVG Pan Zoom:', error);
+ }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9a78f90 and 5e64047.

📒 Files selected for processing (3)
  • apps/blog/src/assets/roadmap-tiles.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/blog/src/assets/roadmap-tiles.json
🧰 Additional context used
🪛 Biome (1.9.4)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts

[error] 133-133: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 188-188: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 207-207: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

@dokolyski dokolyski force-pushed the feat/angular-roadmap branch from 5e64047 to 268958c Compare April 26, 2025 15:30
@valueadd-robot
Copy link

PR is detected, will deploy to dev environment

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (5)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (5)

77-77: ⚠️ Potential issue

Fix Angular property naming for styles.

Angular expects a styleUrls array instead of styleUrl. This could cause runtime or compilation issues.

- styleUrl: './feature-roadmap.component.scss',
+ styleUrls: ['./feature-roadmap.component.scss'],

132-135: 🛠️ Refactor suggestion

Optimize node mapping for better performance.

Using spread syntax inside reducers leads to O(n²) time complexity. Direct property assignment is more efficient.

- const nodeDtoMap = nodesDto.reduce(
-   (acc, node) => ({ ...acc, [node.id]: node }),
-   {} as { [nodeId: string]: RoadmapNodeDTO },
- );
+ const nodeDtoMap = nodesDto.reduce(
+   (acc, node) => {
+     acc[node.id] = node;
+     return acc;
+   },
+   {} as { [nodeId: string]: RoadmapNodeDTO },
+ );
🧰 Tools
🪛 Biome (1.9.4)

[error] 133-133: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


186-193: 🛠️ Refactor suggestion

Optimize cluster mapping reducer.

Using spread syntax inside reducers leads to O(n²) time complexity. Direct property assignment is more efficient.

- const previousClusterNodeIdToNodeIdMap = childrenNodeIds.reduce(
-   (acc, primaryNodeId) => ({
-     ...acc,
-     [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']:
-       primaryNodeId,
-   }),
-   {} as { [previousNodeId: string | 'initialNode']: string },
- );
+ const previousClusterNodeIdToNodeIdMap = childrenNodeIds.reduce(
+   (acc, primaryNodeId) => {
+     acc[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode'] = primaryNodeId;
+     return acc;
+   },
+   {} as { [previousNodeId: string | 'initialNode']: string },
+ );
🧰 Tools
🪛 Biome (1.9.4)

[error] 188-188: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


205-212: 🛠️ Refactor suggestion

Optimize layer mapping reducer.

Using spread syntax inside reducers leads to O(n²) time complexity. Direct property assignment is more efficient.

- const previousLayerNodeIdToNodeIdMap = Object.keys(layerMap).reduce(
-   (acc, primaryNodeId) => ({
-     ...acc,
-     [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']:
-       primaryNodeId,
-   }),
-   {} as { [previousNodeId: string | 'initialNode']: string },
- );
+ const previousLayerNodeIdToNodeIdMap = Object.keys(layerMap).reduce(
+   (acc, primaryNodeId) => {
+     acc[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode'] = primaryNodeId;
+     return acc;
+   },
+   {} as { [previousNodeId: string | 'initialNode']: string },
+ );
🧰 Tools
🪛 Biome (1.9.4)

[error] 207-207: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


129-242: 🛠️ Refactor suggestion

Refactor complex roadmap calculation into smaller functions.

The roadmap calculation logic is very complex and contained in a single computed function, making it difficult to understand and maintain.

Break down the logic into smaller, focused functions:

protected readonly roadmapLayers = computed<RoadmapLayer[]>(() => {
  const nodesDto = this.nodesDto();
  if (!nodesDto) return [];
  
  const nodeDtoMap = this.createNodeDtoMap(nodesDto);
  const { layerMap, clusterMap, nodeMap } = this.createMaps(nodesDto, nodeDtoMap);
  
  this.setupClusters(clusterMap, nodeMap, nodeDtoMap);
  const layers = this.setupLayers(layerMap, nodeMap, nodeDtoMap);
  
  return [this.createRootLayer(), ...layers];
});

private createNodeDtoMap(nodesDto: RoadmapNodeDTO[]): { [nodeId: string]: RoadmapNodeDTO } {
  return nodesDto.reduce((acc, node) => {
    acc[node.id] = node;
    return acc;
  }, {} as { [nodeId: string]: RoadmapNodeDTO });
}

private createMaps(nodesDto: RoadmapNodeDTO[], nodeDtoMap: { [nodeId: string]: RoadmapNodeDTO }): {
  layerMap: { [parentNodeId: string]: string[] },
  clusterMap: { [clusterNodeId: string]: string[] },
  nodeMap: { [nodeId: string]: RoadmapNode }
} {
  // Extract maps creation logic
}

// Additional methods for other parts of the logic
🧰 Tools
🪛 Biome (1.9.4)

[error] 133-133: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 188-188: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 207-207: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e64047 and 268958c.

⛔ Files ignored due to path filters (5)
  • apps/blog/src/assets/icons/circle-center.svg is excluded by !**/*.svg
  • apps/blog/src/assets/icons/zoom-in.svg is excluded by !**/*.svg
  • apps/blog/src/assets/icons/zoom-out.svg is excluded by !**/*.svg
  • apps/blog/src/assets/icons/zoom-reset.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (27)
  • apps/blog/src/assets/roadmap-tiles.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/.eslintrc.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/README.md (1 hunks)
  • libs/blog/roadmap/feature-roadmap/jest.config.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/project.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/index.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.spec.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/secondary-arrow.pipe.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/slice.pipes.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/roadmap-hover-border-gradient.scss (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-angular-love-node.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-cluster.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-primary-node.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-secondary-node.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-svg-control.component.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/test-setup.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/tsconfig.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/tsconfig.lib.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/tsconfig.spec.json (1 hunks)
  • libs/blog/shell/feature-shell-web/src/lib/blog-shell.routes.ts (1 hunks)
  • libs/blog/shell/feature-shell-web/src/lib/roadmap-shell.component.ts (1 hunks)
  • libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts (1 hunks)
  • package.json (1 hunks)
  • tsconfig.base.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (25)
  • libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts
  • tsconfig.base.json
  • libs/blog/roadmap/feature-roadmap/tsconfig.lib.json
  • libs/blog/roadmap/feature-roadmap/README.md
  • libs/blog/roadmap/feature-roadmap/project.json
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.spec.ts
  • libs/blog/roadmap/feature-roadmap/src/index.ts
  • libs/blog/roadmap/feature-roadmap/src/test-setup.ts
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/roadmap-hover-border-gradient.scss
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-angular-love-node.component.ts
  • libs/blog/shell/feature-shell-web/src/lib/blog-shell.routes.ts
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-svg-control.component.ts
  • libs/blog/roadmap/feature-roadmap/src/lib/slice.pipes.ts
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss
  • libs/blog/roadmap/feature-roadmap/jest.config.ts
  • libs/blog/roadmap/feature-roadmap/.eslintrc.json
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-cluster.component.ts
  • libs/blog/roadmap/feature-roadmap/tsconfig.json
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-secondary-node.component.ts
  • libs/blog/shell/feature-shell-web/src/lib/roadmap-shell.component.ts
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html
  • libs/blog/roadmap/feature-roadmap/src/lib/secondary-arrow.pipe.ts
  • apps/blog/src/assets/roadmap-tiles.json
  • libs/blog/roadmap/feature-roadmap/tsconfig.spec.json
  • libs/blog/roadmap/feature-roadmap/src/lib/ui/ui-roadmap-primary-node.component.ts
🧰 Additional context used
🪛 Biome (1.9.4)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts

[error] 133-133: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 188-188: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 207-207: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

Comment on lines 160 to 163
layerMap[nodeDto.parentNodeId] = [
...(layerMap[nodeDto.parentNodeId] ?? []),
nodeDto.id,
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Optimize array handling in layer map.

Similar to the cluster map, use push instead of spread operator for better performance.

- layerMap[nodeDto.parentNodeId] = [
-   ...(layerMap[nodeDto.parentNodeId] ?? []),
-   nodeDto.id,
- ];
+ if (!layerMap[nodeDto.parentNodeId]) {
+   layerMap[nodeDto.parentNodeId] = [];
+ }
+ layerMap[nodeDto.parentNodeId].push(nodeDto.id);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
layerMap[nodeDto.parentNodeId] = [
...(layerMap[nodeDto.parentNodeId] ?? []),
nodeDto.id,
];
if (!layerMap[nodeDto.parentNodeId]) {
layerMap[nodeDto.parentNodeId] = [];
}
layerMap[nodeDto.parentNodeId].push(nodeDto.id);

Comment on lines 145 to 148
clusterMap[parentClusterNodeDto.id] = [
...(clusterMap[parentClusterNodeDto.id] ?? []),
nodeDto.id,
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve array handling in cluster map.

Using spread operator for array concatenation creates unnecessary copies. Use push for better performance.

- clusterMap[parentClusterNodeDto.id] = [
-   ...(clusterMap[parentClusterNodeDto.id] ?? []),
-   nodeDto.id,
- ];
+ if (!clusterMap[parentClusterNodeDto.id]) {
+   clusterMap[parentClusterNodeDto.id] = [];
+ }
+ clusterMap[parentClusterNodeDto.id].push(nodeDto.id);

Comment on lines +86 to +91
private readonly nodesDto = rxResource({
loader: () =>
this._http.get<RoadmapNodeDTO[]>('assets/roadmap-tiles.json', {
responseType: 'json',
}),
}).value.asReadonly();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for HTTP requests.

The current implementation doesn't handle potential errors in HTTP requests. Add error handling to provide feedback to users.

private readonly nodesDto = rxResource({
  loader: () =>
    this._http.get<RoadmapNodeDTO[]>('assets/roadmap-tiles.json', {
      responseType: 'json',
    }),
+ errorHandler: (error) => {
+   console.error('Failed to load roadmap data:', error);
+   return []; // Provide fallback or empty data
+ },
}).value.asReadonly();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private readonly nodesDto = rxResource({
loader: () =>
this._http.get<RoadmapNodeDTO[]>('assets/roadmap-tiles.json', {
responseType: 'json',
}),
}).value.asReadonly();
private readonly nodesDto = rxResource({
loader: () =>
this._http.get<RoadmapNodeDTO[]>('assets/roadmap-tiles.json', {
responseType: 'json',
}),
errorHandler: (error) => {
console.error('Failed to load roadmap data:', error);
return []; // Provide fallback or empty data
},
}).value.asReadonly();

Comment on lines 244 to 255
private async initSvgPanZoom() {
const svgPanZoomModule = await import('svg-pan-zoom');
const svgPanZoom: SvgPanZoom.Instance =
(svgPanZoomModule as any)['default'] || svgPanZoomModule;

const svgRoadmap = this._svgRoadmap();
if (svgRoadmap) {
this._svgPanZoom = svgPanZoom(svgRoadmap.nativeElement, {
...svgPanZoomInitialConfig,
});
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for SVG Pan-Zoom initialization.

The dynamic import and initialization of SVG Pan-Zoom lacks error handling, which could lead to unexpected behavior if the library fails to load or initialize.

private async initSvgPanZoom() {
-  const svgPanZoomModule = await import('svg-pan-zoom');
-  const svgPanZoom: SvgPanZoom.Instance =
-    (svgPanZoomModule as any)['default'] || svgPanZoomModule;
-
-  const svgRoadmap = this._svgRoadmap();
-  if (svgRoadmap) {
-    this._svgPanZoom = svgPanZoom(svgRoadmap.nativeElement, {
-      ...svgPanZoomInitialConfig,
-    });
-  }
+  try {
+    const svgPanZoomModule = await import('svg-pan-zoom');
+    const svgPanZoom: SvgPanZoom.Instance =
+      'default' in svgPanZoomModule ? svgPanZoomModule.default : svgPanZoomModule;
+
+    const svgRoadmap = this._svgRoadmap();
+    if (svgRoadmap) {
+      this._svgPanZoom = svgPanZoom(svgRoadmap.nativeElement, {
+        ...svgPanZoomInitialConfig,
+      });
+    }
+  } catch (error) {
+    console.error('Failed to initialize SVG Pan-Zoom:', error);
+    // Consider providing fallback behavior or notifying the user
+  }
}

@valueadd-robot
Copy link

Deployed to dev environment
Branch: feat/angular-roadmap
BFF URL: https://42df8ac9-blog-bff-dev.contact-ef8.workers.dev
Deploy URL: https://6f93aaf6.angular-love-client.pages.dev
Alias URL: https://feat-angular-roadmap.angular-love-client.pages.dev

Extract additional UI components, introduce UI libs,
update roadmap layering logic - use css-based connectors
(instead of svg-based) and simplify styling.
@valueadd-robot
Copy link

PR is detected, will deploy to dev environment

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (8)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (8)

91-94: 🛠️ Refactor suggestion

Optimize reducer to avoid O(n²) complexity.

Using the spread operator in reducers leads to O(n²) time complexity as it creates a new object on each iteration.

- const nodeDtoMap = roadmapNodesDto.reduce(
-   (acc, node) => ({ ...acc, [node.id]: node }),
-   {} as { [nodeId: string]: RoadmapNodeDTO },
- );
+ const nodeDtoMap = roadmapNodesDto.reduce(
+   (acc, node) => {
+     acc[node.id] = node;
+     return acc;
+   },
+   {} as { [nodeId: string]: RoadmapNodeDTO },
+ );
🧰 Tools
🪛 Biome (1.9.4)

[error] 92-92: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


104-107: 🛠️ Refactor suggestion

Optimize array handling in cluster map.

Using the spread operator for array concatenation creates unnecessary copies and can lead to performance issues.

- clusterMap[parentClusterNodeDto.id] = [
-   ...(clusterMap[parentClusterNodeDto.id] ?? []),
-   nodeDto.id,
- ];
+ if (!clusterMap[parentClusterNodeDto.id]) {
+   clusterMap[parentClusterNodeDto.id] = [];
+ }
+ clusterMap[parentClusterNodeDto.id].push(nodeDto.id);

119-122: 🛠️ Refactor suggestion

Optimize array handling in layer map.

Similar to the cluster map, use push instead of spread operator for better performance.

- layerMap[nodeDto.parentNodeId] = [
-   ...(layerMap[nodeDto.parentNodeId] ?? []),
-   nodeDto.id,
- ];
+ if (!layerMap[nodeDto.parentNodeId]) {
+   layerMap[nodeDto.parentNodeId] = [];
+ }
+ layerMap[nodeDto.parentNodeId].push(nodeDto.id);

145-152: 🛠️ Refactor suggestion

Optimize the cluster creation reducer.

Using the spread syntax inside .reduce(...) can degrade performance at scale.

- const previousClusterNodeIdToNodeIdMap = childrenNodeIds.reduce(
-   (acc, primaryNodeId) => ({
-     ...acc,
-     [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']:
-       primaryNodeId,
-   }),
-   {} as { [previousNodeId: string | 'initialNode']: string },
- );
+ const previousClusterNodeIdToNodeIdMap = childrenNodeIds.reduce(
+   (acc, primaryNodeId) => {
+     acc[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode'] = primaryNodeId;
+     return acc;
+   },
+   {} as { [previousNodeId: string | 'initialNode']: string },
+ );
🧰 Tools
🪛 Biome (1.9.4)

[error] 147-147: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


166-173: 🛠️ Refactor suggestion

Minimize object creation overhead in layer mapping.

Same as previous reducers, using spread in reducers causes O(n²) complexity.

- const previousLayerNodeIdToNodeIdMap = Object.keys(layerMap).reduce(
-   (acc, primaryNodeId) => ({
-     ...acc,
-     [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']:
-       primaryNodeId,
-   }),
-   {} as { [previousNodeId: string | 'initialNode']: string },
- );
+ const previousLayerNodeIdToNodeIdMap = Object.keys(layerMap).reduce(
+   (acc, primaryNodeId) => {
+     acc[nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode'] = primaryNodeId;
+     return acc;
+   },
+   {} as { [previousNodeId: string | 'initialNode']: string },
+ );
🧰 Tools
🪛 Biome (1.9.4)

[error] 168-168: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


59-64: 🛠️ Refactor suggestion

Add error handling for HTTP requests.

The current implementation doesn't handle potential errors in HTTP requests. This could lead to unexpected behavior if the request fails.

private readonly nodesDto = rxResource({
  loader: () =>
    this._http.get<RoadmapNodeDTO[]>('assets/roadmap-tiles.json', {
      responseType: 'json',
    }),
+ errorHandler: (error) => {
+   console.error('Failed to load roadmap data:', error);
+   return []; // Provide fallback or empty data
+ },
}).value.asReadonly();

200-210: 🛠️ Refactor suggestion

Add error handling for SVG Pan-Zoom initialization.

The dynamic import and initialization of SVG Pan-Zoom lacks error handling, which could lead to unexpected behavior if the library fails to load.

private async initSvgPanZoom() {
-  const svgPanZoomModule = await import('svg-pan-zoom');
-  const svgPanZoom: SvgPanZoom.Instance =
-    'default' in svgPanZoomModule
-      ? (svgPanZoomModule.default as SvgPanZoom.Instance)
-      : svgPanZoomModule;
-
-  this._svgPanZoom = svgPanZoom(this._svgRoadmap().nativeElement, {
-    ...svgPanZoomInitialConfig,
-  });
+  try {
+    const svgPanZoomModule = await import('svg-pan-zoom');
+    const svgPanZoom: SvgPanZoom.Instance =
+      'default' in svgPanZoomModule
+        ? (svgPanZoomModule.default as SvgPanZoom.Instance)
+        : svgPanZoomModule;
+
+    this._svgPanZoom = svgPanZoom(this._svgRoadmap().nativeElement, {
+      ...svgPanZoomInitialConfig,
+    });
+  } catch (error) {
+    console.error('Failed to initialize SVG Pan-Zoom:', error);
+    // Consider providing fallback behavior or notifying the user
+  }
}

83-198: 🛠️ Refactor suggestion

Complex roadmap calculation logic needs refactoring.

The buildRoadmapLayers method is quite complex and contains multiple responsibilities. As noted in the TODO comment, this should be extracted into smaller, more focused functions.

Consider refactoring into separate methods such as:

- // TODO - maybe extract to util function and rewrite this to be more readable
- private buildRoadmapLayers(
-   roadmapNodesDto: RoadmapNodeDTO[] | undefined,
- ): RoadmapLayer[] {
-   // ... complex implementation
- }
+ private buildRoadmapLayers(
+   roadmapNodesDto: RoadmapNodeDTO[] | undefined,
+ ): RoadmapLayer[] {
+   if (!roadmapNodesDto) {
+     return [];
+   }
+   
+   const nodeDtoMap = this.createNodeDtoMap(roadmapNodesDto);
+   const { layerMap, clusterMap, nodeMap } = this.createMaps(roadmapNodesDto, nodeDtoMap);
+   this.setupClusters(clusterMap, nodeMap, nodeDtoMap);
+   const layers = this.setupLayers(layerMap, nodeMap, nodeDtoMap);
+   
+   return [this.createRootLayer(), ...layers];
+ }
+ 
+ private createNodeDtoMap(nodes: RoadmapNodeDTO[]): { [nodeId: string]: RoadmapNodeDTO } {
+   const map: { [nodeId: string]: RoadmapNodeDTO } = {};
+   for (const node of nodes) {
+     map[node.id] = node;
+   }
+   return map;
+ }
+ 
+ // Additional helper methods for maps creation, cluster setup, layer setup, etc.
🧰 Tools
🪛 Biome (1.9.4)

[error] 92-92: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 147-147: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 168-168: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🧹 Nitpick comments (4)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.ts (1)

1-37: Well-structured component with OnPush change detection.

The component is well-implemented with a clear responsibility for rendering a cluster of roadmap nodes. The use of OnPush change detection is appropriate for performance optimization.

Consider extracting hard-coded color values.

The template contains hard-coded color values like #FDF5FD and #100F15 that would be better maintained as CSS variables.

- class="relative z-10 m-[-2px] rounded-lg border-2 border-[#FDF5FD] bg-[--secondary-color] px-6 py-4"
+ class="relative z-10 m-[-2px] rounded-lg border-2 border-[--border-color] bg-[--secondary-color] px-6 py-4"

Consider adding semantic HTML elements and ARIA attributes.

For improved accessibility, consider using semantic HTML elements and adding appropriate ARIA attributes to clarify the component's role in the roadmap visualization.

libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.ts (2)

12-12: Remove extra space in class attribute.

There's an extra space between "rounded-lg" and "bg-gradient-to-r" in the class attribute.

-        class="relative z-10 m-[4px] rounded-lg  bg-gradient-to-r from-[--secondary-color] to-[--gradient-color] px-6 py-4"
+        class="relative z-10 m-[4px] rounded-lg bg-gradient-to-r from-[--secondary-color] to-[--gradient-color] px-6 py-4"

1-23: Maintain consistency in styling approach across node components.

Unlike the RoadmapPrimaryNodeComponent, this component doesn't use host bindings for styling. Consider adopting a consistent styling approach across all node components.

Extract hard-coded color values to CSS variables.

For better maintainability and theming support, extract hard-coded color values like #FDF5FD to CSS variables.

libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.ts (1)

1-23: Ensure consistent styling across node components.

This component uses a margin of 2px, while the primary and angular-love nodes use 4px. Similarly, the text size here is 20px compared to 24px in other components. Consider standardizing these values or documenting the intentional differences.

Extract hard-coded color values to CSS variables.

For better maintainability and theming support, extract hard-coded color values like #FDF5FD to CSS variables.

Consider a shared base class for roadmap nodes.

The roadmap node components share similar templates and functionality. Consider creating a base component class to reduce code duplication across the different node types.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 268958c and a86ce37.

📒 Files selected for processing (49)
  • libs/blog/roadmap/feature-roadmap/README.md (1 hunks)
  • libs/blog/roadmap/feature-roadmap/jest.config.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/project.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/.eslintrc.json (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/README.md (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/jest.config.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/project.json (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/index.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.scss (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.scss (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.scss (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.scss (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/style/_roadmap-hover-border-gradient.scss (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/types/roadmap-node.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/test-setup.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/tsconfig.json (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/tsconfig.lib.json (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/tsconfig.spec.json (1 hunks)
  • libs/blog/roadmap/ui-roadmap/.eslintrc.json (1 hunks)
  • libs/blog/roadmap/ui-roadmap/README.md (1 hunks)
  • libs/blog/roadmap/ui-roadmap/jest.config.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/project.json (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/index.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-node/connected-node.component.html (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-node/connected-node.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/node-connector-line/node-connector-line.component.scss (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/node-connector-line/node-connector-line.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/left-slice.pipe.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/right-slice.pipe.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.html (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.scss (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-svg-controls/roadmap-svg-controls.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/vertical-connector-arrow/vertical-connector-arrow.component.scss (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/vertical-connector-arrow/vertical-connector-arrow.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/test-setup.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/tsconfig.json (1 hunks)
  • libs/blog/roadmap/ui-roadmap/tsconfig.lib.json (1 hunks)
  • libs/blog/roadmap/ui-roadmap/tsconfig.spec.json (1 hunks)
  • libs/blog/shell/feature-shell-web/src/lib/roadmap-shell.component.ts (1 hunks)
  • nx.json (1 hunks)
  • tsconfig.base.json (1 hunks)
✅ Files skipped from review due to trivial changes (31)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/vertical-connector-arrow/vertical-connector-arrow.component.ts
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.scss
  • libs/blog/roadmap/ui-roadmap/src/index.ts
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.scss
  • libs/blog/roadmap/ui-roadmap/README.md
  • libs/blog/roadmap/ui-roadmap-node/README.md
  • libs/blog/roadmap/ui-roadmap-node/src/test-setup.ts
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.scss
  • libs/blog/roadmap/ui-roadmap/tsconfig.spec.json
  • libs/blog/roadmap/ui-roadmap-node/tsconfig.json
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-node/connected-node.component.html
  • libs/blog/roadmap/ui-roadmap/src/test-setup.ts
  • libs/blog/roadmap/ui-roadmap/tsconfig.lib.json
  • libs/blog/roadmap/ui-roadmap/src/lib/components/vertical-connector-arrow/vertical-connector-arrow.component.scss
  • libs/blog/roadmap/ui-roadmap-node/tsconfig.lib.json
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.scss
  • libs/blog/roadmap/ui-roadmap-node/.eslintrc.json
  • nx.json
  • libs/blog/roadmap/ui-roadmap-node/jest.config.ts
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.scss
  • libs/blog/roadmap/ui-roadmap-node/tsconfig.spec.json
  • libs/blog/roadmap/ui-roadmap/project.json
  • libs/blog/roadmap/ui-roadmap/tsconfig.json
  • libs/blog/roadmap/ui-roadmap-node/project.json
  • libs/blog/roadmap/ui-roadmap/src/lib/components/node-connector-line/node-connector-line.component.scss
  • libs/blog/roadmap/ui-roadmap/.eslintrc.json
  • libs/blog/roadmap/ui-roadmap-node/src/index.ts
  • libs/blog/roadmap/ui-roadmap-node/src/lib/style/_roadmap-hover-border-gradient.scss
  • libs/blog/roadmap/ui-roadmap-node/src/lib/types/roadmap-node.ts
  • libs/blog/roadmap/ui-roadmap/jest.config.ts
  • tsconfig.base.json
🚧 Files skipped from review as they are similar to previous changes (6)
  • libs/blog/roadmap/feature-roadmap/jest.config.ts
  • libs/blog/roadmap/feature-roadmap/project.json
  • libs/blog/roadmap/feature-roadmap/README.md
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html
  • libs/blog/shell/feature-shell-web/src/lib/roadmap-shell.component.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/right-slice.pipe.ts (1)
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/left-slice.pipe.ts (1)
  • Pipe (3-11)
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/left-slice.pipe.ts (1)
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/right-slice.pipe.ts (1)
  • Pipe (3-11)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.ts (5)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.ts (1)
  • Component (5-23)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.ts (1)
  • Component (5-23)
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.ts (1)
  • Component (24-48)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (1)
  • Component (42-211)
libs/blog/roadmap/ui-roadmap-node/src/lib/types/roadmap-node.ts (1)
  • RoadmapNode (15-15)
🪛 Biome (1.9.4)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts

[error] 92-92: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 147-147: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)


[error] 168-168: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🔇 Additional comments (7)
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/right-slice.pipe.ts (1)

1-11: Clean and well-implemented pipe!

This pipe correctly extracts the right half of an array by calculating the midpoint and returning the elements from that point to the end. The use of generics ensures type safety, and the implementation follows Angular's best practices.

libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/left-slice.pipe.ts (1)

1-11: Clean implementation complementing the right-slice pipe!

This pipe correctly extracts the left half of an array, using the same midpoint calculation as the RightSlicePipe. The consistent use of Math.ceil() ensures that for odd-length arrays, the left half gets one more element than the right half, which maintains consistent behavior when these pipes are used together.

libs/blog/roadmap/ui-roadmap/src/lib/components/node-connector-line/node-connector-line.component.ts (1)

1-23: Excellent use of modern Angular patterns!

This component uses several modern Angular best practices:

  • Empty template with host bindings for CSS class application
  • OnPush change detection for better performance
  • The new input() API with default values
  • Clear type definition with a dedicated type alias

The component serves as a visual connector with different styles based on its position, efficiently implemented without unnecessary DOM elements.

libs/blog/roadmap/ui-roadmap/src/lib/components/connected-node/connected-node.component.ts (1)

1-34: Well-designed component with good use of Angular signals!

This component effectively uses:

  • OnPush change detection for performance
  • Modern input() API with default values
  • The computed() signal for deriving the connector type
  • Clear, readable conditionals

The component intelligently determines the appropriate connector type based on the node's position in the sequence, creating a clean visual hierarchy for the roadmap.

libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.ts (1)

1-48: Well-structured component using modern Angular features.

This component is well-organized, using Angular's modern signals-based input API, OnPush change detection, and standalone component architecture. The clear interface definition and component organization follow good practices.

libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.html (1)

1-51: Good use of modern Angular template syntax and layout structure.

The template effectively uses Angular's new control flow syntax (@if, @for) and properly utilizes tracking for performance optimization. The layout is structured well with clear separation between parent node, layer connector, and child nodes.

libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-svg-controls/roadmap-svg-controls.component.ts (1)

1-55: Well-implemented controls component with type safety.

The component is focused and well-organized with clear typing for events and controls. Good use of modern Angular features including output signals and the @for loop syntax.

@valueadd-robot
Copy link

Deployed to dev environment
Branch: feat/angular-roadmap
BFF URL: https://b54bc930-blog-bff-dev.contact-ef8.workers.dev
Deploy URL: https://64a799d3.angular-love-client.pages.dev
Alias URL: https://feat-angular-roadmap.angular-love-client.pages.dev

@valueadd-robot
Copy link

PR is detected, will deploy to dev environment

@valueadd-robot
Copy link

Deployed to dev environment
Branch: feat/angular-roadmap
BFF URL: https://aadf5faf-blog-bff-dev.contact-ef8.workers.dev
Deploy URL: https://254c260e.angular-love-client.pages.dev
Alias URL: https://feat-angular-roadmap.angular-love-client.pages.dev

@valueadd-robot
Copy link

PR is detected, will deploy to dev environment

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.ts (1)

8-10: Extract hardcoded color values to CSS variables.

For better maintainability and theming support, consider extracting the hardcoded color value #FDF5FD to a CSS variable in your SCSS file.

- class="roadmap-hover-border-gradient relative w-fit text-nowrap rounded-lg bg-[#FDF5FD] text-[#FDF5FD]"
+ class="roadmap-hover-border-gradient relative w-fit text-nowrap rounded-lg bg-[--background-color] text-[--background-color]"

Then define the variable in your SCSS file:

:host {
  --background-color: #FDF5FD;
}
🧹 Nitpick comments (5)
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connection-point.directive.ts (2)

20-25: Consider making the parent‐directive injection optional

inject(NodeConnectionsGroupDirective) will throw a NullInjectorError if somebody uses alNodeConnectionPoint outside of alNodeConnectionsGroup. Making the injection optional (or using @Host + a clear runtime error) will fail fast in development instead of crashing the whole app at runtime.

-  private readonly _nodesConnectionsGroupDirective = inject(
-    NodeConnectionsGroupDirective,
-  );
+  private readonly _nodesConnectionsGroupDirective = inject(
+    NodeConnectionsGroupDirective,
+    { optional: true }
+  )!;

Then guard the two method calls with a null-check.


34-44: Potential timing issue – unsubscribe / cancel afterNextRender

afterNextRender() cannot be cancelled, so if a node is destroyed before the callback fires (e.g. during fast list updates) the directive will still touch a destroyed element. Consider guarding the callback with destroyRef.onDestroy() or use afterNextRender(this.destroyRef, …) once you migrate to Angular 18.

No immediate bug, but worth tracking.

libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.html (1)

15-22: SVG width of 1 may clip the path

The SVG element has width="1" while the generated path can extend hundreds of pixels left or right. Although overflow="visible" alleviates most issues, some browsers still apply hit-testing/clipping based on the viewport box. Setting width="0" (auto-sizing) or a large enough width avoids corner-cases.

libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.ts (1)

28-30: Duplicate magic numbers – promote to a shared constant

connectorHeight = 40 and connectorStrokeWidth = 3 are hard-coded here and in NodeConnectionsGroupDirective. Extract them into a shared constants.ts file to keep the values in sync.

libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts (1)

77-88: Arc direction calculation can flip for negative offsets

radiusOffset = offsetX > 0 ? -radius : radius assumes positive offsetX corresponds to “to-right”, but that relation reverses when direction() is 'to-left'. This occasionally produces a kink in the path for mixed positive/negative offsets.
Consider basing the offset on directionModifier instead of the raw sign of offsetX.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between a86ce37 and d2d7a16.

📒 Files selected for processing (12)
  • apps/blog/src/assets/i18n/en.json (1 hunks)
  • apps/blog/src/assets/i18n/pl.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.html (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connection-point.directive.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.html (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-svg-controls/roadmap-svg-controls.component.ts (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • apps/blog/src/assets/i18n/en.json
  • apps/blog/src/assets/i18n/pl.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.ts
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.html
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.ts
  • libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-svg-controls/roadmap-svg-controls.component.ts
🧰 Additional context used
🧬 Code Graph Analysis (3)
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.ts (4)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.ts (1)
  • Component (5-23)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.ts (1)
  • Component (5-23)
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.ts (1)
  • Component (21-41)
libs/blog/roadmap/ui-roadmap-node/src/lib/types/roadmap-node.ts (1)
  • RoadmapNode (15-15)
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connection-point.directive.ts (1)
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts (1)
  • Directive (17-92)
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts (1)
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connection-point.directive.ts (2)
  • Directive (16-52)
  • NodeConnectionPoint (12-14)
🔇 Additional comments (5)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.ts (4)

1-3: LGTM! Good use of modern Angular features.

The component uses the new signal-based input API from Angular with proper imports.


11-15: LGTM! Good nested structure.

The inner div with z-index and proper spacing looks good, and correctly uses the CSS variable for primary color.


18-20: LGTM! Good use of OnPush change detection.

Using OnPush change detection strategy is a best practice for performance optimization.


21-23: LGTM! Well-structured component API.

Good use of the new signal-based input API with the required decorator to ensure the component receives the necessary data.

libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts (1)

60-63: Early return yields empty path string; consumers should handle it

groupConnectionPath() returns '' when no points exist. Ensure the template guards against drawing an empty path; otherwise SVG renders a 0-length path that may still consume pointer events.

Comment on lines 25 to 31
<div
class="flex gap-[32px]"
[class]="{
'ms-[50px]': nodesDrawingDirection() === 'to-right',
'me-[50px]': nodesDrawingDirection() === 'to-left',
}"
>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use ngClass instead of [class] when binding an object

[class]="{ … }" converts the object to the literal string "[object Object]", so the dynamic margins will never be applied.

-    class="flex gap-[32px]"
-    [class]="{
+    class="flex gap-[32px]"
+    [ngClass]="{
       'ms-[50px]': nodesDrawingDirection() === 'to-right',
       'me-[50px]': nodesDrawingDirection() === 'to-left',
     }"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
class="flex gap-[32px]"
[class]="{
'ms-[50px]': nodesDrawingDirection() === 'to-right',
'me-[50px]': nodesDrawingDirection() === 'to-left',
}"
>
<div
class="flex gap-[32px]"
[ngClass]="{
'ms-[50px]': nodesDrawingDirection() === 'to-right',
'me-[50px]': nodesDrawingDirection() === 'to-left',
}"
>
🤖 Prompt for AI Agents
In
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.html
around lines 25 to 31, the code uses [class] binding with an object, which
results in the object being converted to a string "[object Object]" and prevents
dynamic margin classes from applying. Replace the [class] binding with [ngClass]
to correctly apply the conditional classes based on nodesDrawingDirection().

Comment on lines 12 to 22
@Component({
selector: 'al-connected-nodes',
templateUrl: 'connected-nodes.component.html',
imports: [
NodeConnectionPointDirective,
NodeConnectionsGroupDirective,
RoadmapClusterComponent,
RoadmapSecondaryNodeComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing standalone: true results in Angular compilation error

The component supplies an imports array, which is only allowed for standalone components. Without the standalone: true flag the build will fail with “Component metadata expected a module id…” error.

 @Component({
+  standalone: true,
   selector: 'al-connected-nodes',
   templateUrl: 'connected-nodes.component.html',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Component({
selector: 'al-connected-nodes',
templateUrl: 'connected-nodes.component.html',
imports: [
NodeConnectionPointDirective,
NodeConnectionsGroupDirective,
RoadmapClusterComponent,
RoadmapSecondaryNodeComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
@Component({
standalone: true,
selector: 'al-connected-nodes',
templateUrl: 'connected-nodes.component.html',
imports: [
NodeConnectionPointDirective,
NodeConnectionsGroupDirective,
RoadmapClusterComponent,
RoadmapSecondaryNodeComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
🤖 Prompt for AI Agents
In
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.ts
between lines 12 and 22, the component defines an imports array but is missing
the standalone: true flag. To fix the Angular compilation error, add standalone:
true to the @Component decorator metadata so Angular treats this as a standalone
component.

Comment on lines 30 to 35
private readonly connectionPoints: Signal<NodeConnectionPoint[]> = computed(
() =>
this._connectionPointDirectives()
.map((cp) => cp.nodeConnectionPoint())
.filter((cp) => cp !== undefined),
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Type narrowing lost – filter does not remove undefined

Array.filter without a user-defined type predicate keeps the union type, so connectionPoints() is inferred as (NodeConnectionPoint | undefined)[]. This breaks strict‐null builds and IntelliSense later on.

-        .map((cp) => cp.nodeConnectionPoint())
-        .filter((cp) => cp !== undefined),
+        .map((cp) => cp.nodeConnectionPoint())
+        .filter((cp): cp is NodeConnectionPoint => cp !== undefined),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private readonly connectionPoints: Signal<NodeConnectionPoint[]> = computed(
() =>
this._connectionPointDirectives()
.map((cp) => cp.nodeConnectionPoint())
.filter((cp) => cp !== undefined),
);
private readonly connectionPoints: Signal<NodeConnectionPoint[]> = computed(
() =>
this._connectionPointDirectives()
.map((cp) => cp.nodeConnectionPoint())
.filter((cp): cp is NodeConnectionPoint => cp !== undefined),
);
🤖 Prompt for AI Agents
In
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts
around lines 30 to 35, the filter used to remove undefined values does not
narrow the type properly, causing connectionPoints() to be inferred as
(NodeConnectionPoint | undefined)[]. Fix this by providing a user-defined type
predicate in the filter callback to explicitly assert that the filtered values
are NodeConnectionPoint, ensuring correct type narrowing and compatibility with
strict-null checks.

@valueadd-robot
Copy link

Deployed to dev environment
Branch: feat/angular-roadmap
BFF URL: https://787153a1-blog-bff-dev.contact-ef8.workers.dev
Deploy URL: https://84934dd4.angular-love-client.pages.dev
Alias URL: https://feat-angular-roadmap.angular-love-client.pages.dev

Dawid Gruszczyński added 2 commits May 20, 2025 14:14
Additionally:
* update horizontal connections drawing issues and simplify logic
@valueadd-robot
Copy link

PR is detected, will deploy to dev environment

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (3)

40-41: styleUrl should be styleUrls (array) – Angular will ignore the current property

Angular component metadata expects the plural styleUrls array. Using the singular form will silently drop your stylesheet in AoT builds and break styling.

-  styleUrl: './feature-roadmap.component.scss',
+  styleUrls: ['./feature-roadmap.component.scss'],

67-72: Missing error-handling for roadmap HTTP fetch

The rxResource loader lacks an errorHandler, so a network / CORS failure will propagate an unhandled error and keep the signal in “loading” forever. The exact same concern was raised previously and still applies.

Consider:

loader: () =>
   this._http.get<RoadmapNodeDTO[]>('assets/roadmap-tiles.json', {
     responseType: 'json',
   }),
+errorHandler: (err) => {
+  console.error('[Roadmap] Failed to load tiles:', err);
+  return [];            // or a domain-specific fallback
+},

133-144: Wrap dynamic svg-pan-zoom import in try/catch to avoid white-screen on load failure

A network error or SSR hydration race will currently throw and break the component. The same improvement was suggested before but isn’t present yet.

-private async initSvgPanZoom() {
-  const svgPanZoomModule = await import('svg-pan-zoom');
+private async initSvgPanZoom() {
+  try {
+    const svgPanZoomModule = await import('svg-pan-zoom');
     const svgPanZoom: SvgPanZoom.Instance =
       'default' in svgPanZoomModule
         ? (svgPanZoomModule.default as SvgPanZoom.Instance)
         : svgPanZoomModule;
 
     const svgPanZoomInstance = svgPanZoom(this._svgRoadmap().nativeElement, {
       ...svgPanZoomInitialConfig,
     });
     this.svgPanZoom.set(svgPanZoomInstance);
+  } catch (err) {
+    console.error('[Roadmap] Failed to bootstrap svg-pan-zoom:', err);
+  }
 }
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.ts (1)

12-23: ⚠️ Potential issue

Missing standalone: true results in Angular compilation error

The component supplies an imports array, which is only allowed for standalone components. Without the standalone: true flag the build will fail with "Component metadata expected a module id…" error.

 @Component({
+  standalone: true,
   selector: 'al-connected-nodes',
   templateUrl: 'connected-nodes.component.html',
   styleUrl: 'connected-nodes.component.scss',
   imports: [
     NodeConnectionPointDirective,
     NodeConnectionsGroupDirective,
     RoadmapClusterComponent,
     RoadmapSecondaryNodeComponent,
   ],
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts (1)

25-29: ⚠️ Potential issue

Type narrowing lost – filter does not remove undefined

Array.filter without a user-defined type predicate keeps the union type, so connectionPoints() is inferred as (NodeConnectionPoint | undefined)[]. This breaks strict‐null builds and IntelliSense later on.

-    this._connectionPointDirectives()
-      .map((cp) => cp.nodeConnectionPoint())
-      .filter((cp) => cp !== undefined),
+    this._connectionPointDirectives()
+      .map((cp) => cp.nodeConnectionPoint())
+      .filter((cp): cp is NodeConnectionPoint => cp !== undefined),
🧹 Nitpick comments (4)
libs/blog/roadmap/feature-roadmap/src/lib/build-roadmap-layers-from-dto.ts (1)

120-141: Guard against cyclic previousNodeId chains to prevent infinite loops

getOrderedNodeIdsList will spin forever if a cycle sneaks into the data (e.g., A → B → A). Add a visited-set safeguard:

  const orderedNodeIds: string[] = [];
-  while (nextNodeId) {
+  const visited = new Set<string>();
+  while (nextNodeId) {
+    if (visited.has(nextNodeId)) {
+      console.warn('[Roadmap] Detected circular linked list starting at', nextNodeId);
+      break;
+    }
+    visited.add(nextNodeId);
     orderedNodeIds.push(nextNodeId);
     nextNodeId = chainedNodeIdsMap[nextNodeId];
   }

This prevents accidental infinite loops on malformed data.

libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connection-point.directive.ts (3)

25-36: Consider handling position updates for dynamic layouts

The directive calculates its position once after the initial render using afterNextRender. This works for static layouts, but won't update if the element's position changes dynamically (e.g., during resize events or when parent containers change).

Consider adding a resize observer or event listeners if dynamic repositioning is needed:

 constructor() {
   afterNextRender(() => {
+    this.updateConnectionPoint();
+    
+    // Optional: Add resize observer for dynamic updates
+    const resizeObserver = new ResizeObserver(() => {
+      this.updateConnectionPoint();
+    });
+    resizeObserver.observe(this._elementRef.nativeElement.offsetParent || document.body);
+  });
+}
+
+private updateConnectionPoint(): void {
     const elementOffsetLeft = this._elementRef.nativeElement.offsetLeft;
     const elementOffsetWidth = this._elementRef.nativeElement.offsetWidth;
     const roadmapCenterPoint =
       (this._elementRef.nativeElement.offsetParent?.clientWidth || 0) / 2;

     // Offset is calculated relative to the center of the roadmap
     const offsetX =
       elementOffsetLeft + elementOffsetWidth / 2 - roadmapCenterPoint;
     this.nodeConnectionPoint.set({ offsetX: Math.abs(offsetX) });
   });
 }

35-35: Consider preserving directionality information

Using Math.abs(offsetX) discards the sign of the offset, which could be useful for determining if the node is to the left or right of center. This might limit flexibility if direction matters in the future.

- this.nodeConnectionPoint.set({ offsetX: Math.abs(offsetX) });
+ this.nodeConnectionPoint.set({ offsetX });

1-38: Add OnDestroy implementation for cleanup

While there are no explicit subscriptions to clean up, it's a best practice to implement OnDestroy for directives that might need cleanup in the future (e.g., if you add observers or event listeners).

 import {
   afterNextRender,
   Directive,
   ElementRef,
   inject,
+  OnDestroy,
   signal,
 } from '@angular/core';

 export interface NodeConnectionPoint {
   offsetX: number;
 }

 @Directive({
   selector: '[alNodeConnectionPoint]',
 })
-export class NodeConnectionPointDirective {
+export class NodeConnectionPointDirective implements OnDestroy {
   private readonly _elementRef = inject<ElementRef<HTMLElement>>(
     ElementRef<HTMLElement>,
   );

   readonly nodeConnectionPoint = signal<NodeConnectionPoint | undefined>(
     undefined,
   );
+
+  // For potential future observers or event listeners
+  ngOnDestroy(): void {
+    // Cleanup resources if needed
+  }

   constructor() {
     // ... existing code
   }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to data retention organization setting
Knowledge Base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 16fc308 and 765abfd.

📒 Files selected for processing (23)
  • libs/blog-contracts/roadmap/.eslintrc.json (1 hunks)
  • libs/blog-contracts/roadmap/README.md (1 hunks)
  • libs/blog-contracts/roadmap/jest.config.ts (1 hunks)
  • libs/blog-contracts/roadmap/project.json (1 hunks)
  • libs/blog-contracts/roadmap/src/index.ts (1 hunks)
  • libs/blog-contracts/roadmap/src/lib/roadmap-node.ts (1 hunks)
  • libs/blog-contracts/roadmap/src/test-setup.ts (1 hunks)
  • libs/blog-contracts/roadmap/tsconfig.json (1 hunks)
  • libs/blog-contracts/roadmap/tsconfig.lib.json (1 hunks)
  • libs/blog-contracts/roadmap/tsconfig.spec.json (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/build-roadmap-layers-from-dto.ts (1 hunks)
  • libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/types/roadmap-node.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.html (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.scss (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connection-point.directive.ts (1 hunks)
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts (1 hunks)
  • tsconfig.base.json (2 hunks)
✅ Files skipped from review due to trivial changes (11)
  • libs/blog-contracts/roadmap/tsconfig.lib.json
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.scss
  • libs/blog-contracts/roadmap/README.md
  • libs/blog-contracts/roadmap/src/index.ts
  • libs/blog-contracts/roadmap/src/test-setup.ts
  • libs/blog-contracts/roadmap/jest.config.ts
  • libs/blog-contracts/roadmap/src/lib/roadmap-node.ts
  • libs/blog-contracts/roadmap/tsconfig.json
  • libs/blog-contracts/roadmap/tsconfig.spec.json
  • libs/blog-contracts/roadmap/.eslintrc.json
  • libs/blog-contracts/roadmap/project.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.ts
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.ts
  • libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.html
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.ts
  • libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.ts
  • libs/blog/roadmap/ui-roadmap-node/src/lib/types/roadmap-node.ts
  • tsconfig.base.json
🧰 Additional context used
🧬 Code Graph Analysis (4)
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connection-point.directive.ts (1)
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts (1)
  • Directive (10-65)
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts (1)
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connection-point.directive.ts (2)
  • Directive (13-38)
  • NodeConnectionPoint (9-11)
libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.ts (6)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.ts (1)
  • Component (5-26)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.ts (1)
  • Component (5-39)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.ts (1)
  • Component (5-26)
libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.ts (1)
  • Component (5-26)
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.ts (1)
  • Component (21-41)
libs/blog/roadmap/ui-roadmap-node/src/lib/types/roadmap-node.ts (1)
  • RoadmapNode (15-15)
libs/blog/roadmap/feature-roadmap/src/lib/build-roadmap-layers-from-dto.ts (3)
libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.ts (1)
  • RoadmapLayer (16-19)
libs/blog-contracts/roadmap/src/lib/roadmap-node.ts (1)
  • RoadmapNodeDTO (1-6)
libs/blog/roadmap/ui-roadmap-node/src/lib/types/roadmap-node.ts (3)
  • RoadmapNode (15-15)
  • RoadmapClusterNode (10-13)
  • RoadmapStandardNode (6-8)
🪛 Biome (1.9.4)
libs/blog/roadmap/feature-roadmap/src/lib/build-roadmap-layers-from-dto.ts

[error] 26-26: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🔇 Additional comments (8)
libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts (1)

118-130: focusSelectedNode mixes DOM pixels with SVG world units – verify panning accuracy

getBoundingClientRect() returns CSS-pixel coordinates, whereas svgPanZoom.pan() expects SVG viewport units (after current zoom). Unless the SVG has a 1:1 CSS-to-SVG scaling, the node may be panned to an unexpected offset.

Please confirm behaviour on various zoom levels; if misaligned, convert screen coords to SVG coordinates via svgPanZoom.getRelativePoint() or svgPanZoom.getSizes().

libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connection-point.directive.ts (2)

17-19: Good use of dependency injection

The directive properly uses Angular's inject function to get the ElementRef, following modern Angular practices.


21-23: Well-implemented reactive pattern

Using a signal for the connection point data allows for reactive updates and integration with the computed values in the connections group directive.

libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/connected-nodes.component.ts (2)

25-27: Good use of reactive inputs

Using Angular's new input() API creates more readable, declarative inputs and provides a clear default value.


29-30: Protected values match directive defaults

The connectorHeight value of 40 matches the default in the NodeConnectionsGroupDirective, ensuring consistency. Consider using a shared constant if these values need to be synchronized across components.

libs/blog/roadmap/ui-roadmap/src/lib/components/connected-nodes/node-connections-group.directive.ts (3)

10-13: Good directive organization with exportAs

Using exportAs allows template reference variables to access the directive instance, which is useful for accessing computed properties like groupConnectionPath.


19-23: Effective content children query with descendants option

The directive properly uses contentChildren to query all NodeConnectionPointDirective instances, including those in nested components (via descendants: true).


35-64: SVG path generation logic is well structured

The path generation logic is well-organized and handles the connection points effectively:

  1. Sorts points by their x-offset
  2. Draws horizontal lines between points
  3. Adds an arc at the last point
  4. Adds vertical lines to connect to the actual nodes

Comment on lines +25 to +28
const allNodeDtosMap = roadmapNodesDto.reduce(
(acc, node) => ({ ...acc, [node.id]: node }),
{} as { [nodeId: string]: RoadmapNodeDTO },
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid O(n²) object spreading inside reduce

Each iteration creates a brand-new object, giving quadratic time & memory for large datasets. Mutate the accumulator instead:

-const allNodeDtosMap = roadmapNodesDto.reduce(
-  (acc, node) => ({ ...acc, [node.id]: node }),
-  {} as { [nodeId: string]: RoadmapNodeDTO },
-);
+const allNodeDtosMap: { [id: string]: RoadmapNodeDTO } = {};
+for (const dto of roadmapNodesDto) {
+  allNodeDtosMap[dto.id] = dto;
+}

This satisfies Biome’s noAccumulatingSpread rule and scales linearly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const allNodeDtosMap = roadmapNodesDto.reduce(
(acc, node) => ({ ...acc, [node.id]: node }),
{} as { [nodeId: string]: RoadmapNodeDTO },
);
const allNodeDtosMap: { [id: string]: RoadmapNodeDTO } = {};
for (const dto of roadmapNodesDto) {
allNodeDtosMap[dto.id] = dto;
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 26-26: Avoid the use of spread (...) syntax on accumulators.

Spread syntax should be avoided on accumulators (like those in .reduce) because it causes a time complexity of O(n^2).
Consider methods such as .splice or .push instead.

(lint/performance/noAccumulatingSpread)

🤖 Prompt for AI Agents
In libs/blog/roadmap/feature-roadmap/src/lib/build-roadmap-layers-from-dto.ts
around lines 25 to 28, the reduce function uses object spreading inside the
accumulator which causes O(n²) time and memory complexity. To fix this, avoid
spreading the accumulator on each iteration and instead directly mutate the
accumulator object by assigning the new key-value pair. This will improve
performance to linear time and satisfy the noAccumulatingSpread rule.

@valueadd-robot
Copy link

Deployed to dev environment
Branch: feat/angular-roadmap
BFF URL: https://34623bea-blog-bff-dev.contact-ef8.workers.dev
Deploy URL: https://560aadb9.angular-love-client.pages.dev
Alias URL: https://feat-angular-roadmap.angular-love-client.pages.dev

@valueadd-robot
Copy link

PR is detected, will deploy to dev environment

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/blog/src/assets/roadmap-tiles.json (1)

1-813: Add a JSON Schema and TypeScript typings for validation
This 800+ line JSON defines the core roadmap structure. To catch missing fields, type mismatches, or property-ordering issues, introduce a JSON Schema (e.g. roadmap-tiles.schema.json) and generate corresponding TypeScript interfaces:

export interface RoadmapNode {
  id: string;
  title: string;
  articleSlugs: string[];
  parentNodeId?: string | null;
  previousNodeId?: string | null;
}

Validating against a schema in CI or at compile time will greatly improve maintainability and guard against shape changes.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 765abfd and 215d453.

📒 Files selected for processing (1)
  • apps/blog/src/assets/roadmap-tiles.json (1 hunks)
🔇 Additional comments (1)
apps/blog/src/assets/roadmap-tiles.json (1)

667-686:

✅ Verification successful

Sanitize id values containing special characters
The nodes with "id": "deployment-&-ci/cd" and "id": "bundling-&-optimization" include & and /. If these IDs map directly to HTML id attributes, CSS selectors, or router fragment IDs, they can break valid selectors or URL parsing.

Please verify downstream usage handles these characters safely (e.g., via encoding or sanitization), or consider normalizing IDs (e.g., replace & with and, remove /). You can search for problematic IDs with:


🏁 Script executed:

rg -Hn '"id": ".*[&/].*"' -A2 apps/blog/src/assets/roadmap-tiles.json

Length of output: 1281


Let’s verify how these IDs are consumed—specifically whether they’re bound to HTML id attributes or used in routing/fragments:


🏁 Script executed:

#!/bin/bash
# 1. Find where the JSON is imported or fetched
rg -Hn "roadmap-tiles.json" -g "*.ts" -g "*.js" -g "*.html"

# 2. Search for usages of the `id` property in code and templates
rg -Hn "tile\.id" -g "*.ts" -g "*.html"

# 3. Look for direct Angular bindings to HTML `id` attributes
rg -Hn "\[id\]" -g "*.html"

Length of output: 1387


No need to sanitize id values in roadmap-tiles.json
I searched the roadmap feature for any bindings of tile.id to HTML attributes or router fragments and found none. These id fields are only used internally (not rendered as DOM IDs or URL hashes), so the & and / characters pose no risk in the current implementation.

Comment on lines +12 to +23
"id": "styling",
"title": "Styling",
"articleSlugs": [
"angular-material-theming-application-with-material-3",
"theming-angular-app-its-libraries",
"angular-styles-masterclass-2",
"lets-implement-a-theme-switch-like-the-angular-material-site",
"switch-themes-like-a-fox-based-on-ambient-light-in-your-angular-apps",
"techniques-to-style-component-host-element-in-angular"
],
"parentNodeId": "components"
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ensure explicit previousNodeId for first‐child nodes
The "styling" node defines a parentNodeId: "components" but omits previousNodeId, whereas most branch starters explicitly set "previousNodeId": null. This inconsistency can lead to undefined vs. null checks in your rendering logic.

Add "previousNodeId": null to any node that begins a new sequence under its parent for consistency and to avoid runtime edge cases.

🤖 Prompt for AI Agents
In apps/blog/src/assets/roadmap-tiles.json between lines 12 and 23, the
"styling" node specifies a parentNodeId but does not include a previousNodeId,
unlike other branch starter nodes that explicitly set previousNodeId to null.
Add "previousNodeId": null to the "styling" node to ensure consistency and
prevent potential runtime issues with undefined vs. null checks in your
rendering logic.

@valueadd-robot
Copy link

Deployed to dev environment
Branch: feat/angular-roadmap
BFF URL: https://c6b2887d-blog-bff-dev.contact-ef8.workers.dev
Deploy URL: https://fe6031e7.angular-love-client.pages.dev
Alias URL: https://feat-angular-roadmap.angular-love-client.pages.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants