diff --git a/apps/blog/src/assets/i18n/en.json b/apps/blog/src/assets/i18n/en.json index 1b7ec03c..ecd661d3 100644 --- a/apps/blog/src/assets/i18n/en.json +++ b/apps/blog/src/assets/i18n/en.json @@ -26,6 +26,7 @@ "meetups": "Angular Meetups", "become_author": "Become an author", "navLinks": "Navigation links", + "roadmap": "Roadmap", "languagePicker": { "pl": "Polish", "en": "English" @@ -105,6 +106,14 @@ "description": "From now on you will be up to date with all the information we will make available on the blog.", "button": "Return to the home page" }, + "roadmapPage": { + "roadmapControls": { + "zoomInRoadmapButton": "Zoom in roadmap", + "restoreRoadmapViewButton": "Restore roadmap default view", + "restoreRoadmapZoomButton": "Restore roadmap original zoom", + "zoomOutRoadmapButton": "Zoom out roadmap" + } + }, "notFoundPage": { "title": "404", "description": "It seems like you've lost your way. The page you provided does not exist or the link has expired and is no longer available.", @@ -142,7 +151,8 @@ "home": "Blog and community for Angular fans", "aboutUs": "About us", "becomeAuthor": "Become an author", - "notFound": "Not found" + "notFound": "Not found", + "roadmap": "Roadmap" }, "adBanner": { "registerButton": "Download for free", diff --git a/apps/blog/src/assets/i18n/pl.json b/apps/blog/src/assets/i18n/pl.json index 4cd8996e..4ee0f196 100644 --- a/apps/blog/src/assets/i18n/pl.json +++ b/apps/blog/src/assets/i18n/pl.json @@ -26,6 +26,7 @@ "meetups": "Angular Meetups", "become_author": "Zostań autorem", "navLinks": "Menu", + "roadmap": "Roadmap", "languagePicker": { "pl": "Polski", "en": "Angielski" @@ -108,6 +109,14 @@ "description": "Od teraz będziesz na bieżąco ze wszystkimi informacjami jakie będziemy udostępniać na blogu.", "button": "Powrót na stronę główną" }, + "roadmapPage": { + "roadmapControls": { + "zoomInRoadmapButton": "Przybliż roadmapę", + "restoreRoadmapViewButton": "Przywróć domyślny widok roadmapy", + "restoreRoadmapZoomButton": "Przywróć domyślne powiększenie roadmapy", + "zoomOutRoadmapButton": "Oddal roadmapę" + } + }, "notFoundPage": { "title": "404", "description": "Wygląda na to, że zgubiłeś drogę. Strona, której szukasz, nie istnieje lub link wygasł i jest już niedostępny.", @@ -145,7 +154,8 @@ "home": "Blog dla sympatyków Angulara", "aboutUs": "O nas", "becomeAuthor": "Zostań autorem", - "notFound": "Nie znaleziono" + "notFound": "Nie znaleziono", + "roadmap": "Roadmap" }, "adBanner": { "registerButton": "Pobierz za darmo", diff --git a/apps/blog/src/assets/icons/circle-center.svg b/apps/blog/src/assets/icons/circle-center.svg new file mode 100644 index 00000000..5d508592 --- /dev/null +++ b/apps/blog/src/assets/icons/circle-center.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/blog/src/assets/icons/zoom-in.svg b/apps/blog/src/assets/icons/zoom-in.svg new file mode 100644 index 00000000..84f01134 --- /dev/null +++ b/apps/blog/src/assets/icons/zoom-in.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/blog/src/assets/icons/zoom-out.svg b/apps/blog/src/assets/icons/zoom-out.svg new file mode 100644 index 00000000..25efefc9 --- /dev/null +++ b/apps/blog/src/assets/icons/zoom-out.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/blog/src/assets/icons/zoom-reset.svg b/apps/blog/src/assets/icons/zoom-reset.svg new file mode 100644 index 00000000..9134e8e9 --- /dev/null +++ b/apps/blog/src/assets/icons/zoom-reset.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/blog/src/assets/roadmap-tiles.json b/apps/blog/src/assets/roadmap-tiles.json new file mode 100644 index 00000000..be90401e --- /dev/null +++ b/apps/blog/src/assets/roadmap-tiles.json @@ -0,0 +1,2154 @@ +[ + { + "id": "components", + "title": "Components", + "previousNodeId": null, + "description": "Components are the building blocks of Angular applications, encapsulating the template, logic, and styles to define a self-contained unit of the user interface.", + "resources": [ + { + "name": "heres-what-you-should-know-when-creating-flexible-and-reusable-components-in-angular", + "type": "article", + "url": "heres-what-you-should-know-when-creating-flexible-and-reusable-components-in-angular" + }, + { + "name": "compliant-components-declarative-approach-in-angular", + "type": "article", + "url": "compliant-components-declarative-approach-in-angular" + } + ] + }, + { + "id": "styling", + "title": "Styling", + "parentNodeId": "components", + "description": "Angular components support scoped styles, allowing developers to apply CSS that affects only the component it is defined in, preventing style leaks.", + "resources": [ + { + "name": "angular-material-theming-application-with-material-3", + "type": "article", + "url": "angular-material-theming-application-with-material-3" + }, + { + "name": "theming-angular-app-its-libraries", + "type": "article", + "url": "theming-angular-app-its-libraries" + }, + { + "name": "angular-styles-masterclass-2", + "type": "article", + "url": "angular-styles-masterclass-2" + }, + { + "name": "lets-implement-a-theme-switch-like-the-angular-material-site", + "type": "article", + "url": "lets-implement-a-theme-switch-like-the-angular-material-site" + }, + { + "name": "switch-themes-like-a-fox-based-on-ambient-light-in-your-angular-apps", + "type": "article", + "url": "switch-themes-like-a-fox-based-on-ambient-light-in-your-angular-apps" + }, + { + "name": "techniques-to-style-component-host-element-in-angular", + "type": "article", + "url": "techniques-to-style-component-host-element-in-angular" + } + ] + }, + { + "id": "sass", + "parentNodeId": "styling", + "title": "Sass", + "previousNodeId": null, + "description": "Sass is a CSS preprocessor supported by Angular CLI that offers features like variables, nesting, and mixins, improving the maintainability of complex styles.", + "resources": [ + { + "name": "migrate-from-css-to-scss-stylesheets-for-an-existing-angular-project", + "type": "article", + "url": "migrate-from-css-to-scss-stylesheets-for-an-existing-angular-project" + } + ] + }, + { + "id": "angular-material", + "parentNodeId": "styling", + "title": "Angular Material", + "previousNodeId": "sass", + "description": "This is the \"Angular Material\"'s description.", + "resources": [ + { + "name": "angular-material-theming-application-with-material-3", + "type": "article", + "url": "angular-material-theming-application-with-material-3" + }, + { + "name": "custom-theme-for-angular-material-components-series-part-1-create-a-theme", + "type": "article", + "url": "custom-theme-for-angular-material-components-series-part-1-create-a-theme" + }, + { + "name": "custom-theme-for-angular-material-components-series-part-2-understand-theme", + "type": "article", + "url": "custom-theme-for-angular-material-components-series-part-2-understand-theme" + }, + { + "name": "custom-theme-for-angular-material-components-series-part-3-apply-theme", + "type": "article", + "url": "custom-theme-for-angular-material-components-series-part-3-apply-theme" + }, + { + "name": "faster-perceived-response-time-with-angular-material-to-tackle-need-for-speed", + "type": "article", + "url": "faster-perceived-response-time-with-angular-material-to-tackle-need-for-speed" + }, + { + "name": "stop-using-shared-material-module", + "type": "article", + "url": "stop-using-shared-material-module" + } + ] + }, + { + "id": "view-encapsulation", + "parentNodeId": "styling", + "title": "View Encapsulation", + "previousNodeId": "angular-material", + "description": "View encapsulation determines how styles are applied to components, ensuring that styles are scoped appropriately using Shadow DOM, Emulated, or None strategies.", + "resources": [ + { + "name": "techniques-to-style-component-host-element-in-angular", + "type": "article", + "url": "techniques-to-style-component-host-element-in-angular" + } + ] + }, + { + "id": "lifecycle", + "parentNodeId": "components", + "title": "Lifecycle", + "previousNodeId": "styling", + "description": "Angular components and directives have lifecycle hooks that let you tap into key moments of their existence, such as creation, updates, and destruction.", + "resources": [ + { + "name": "the-essential-difference-between-constructor-and-ngoninit-in-angular", + "type": "article", + "url": "the-essential-difference-between-constructor-and-ngoninit-in-angular" + }, + { + "name": "component-initialization-without-ngoninit-with-async-pipes-for-observables-and-ngonchanges", + "type": "article", + "url": "component-initialization-without-ngoninit-with-async-pipes-for-observables-and-ngonchanges" + }, + { + "name": "complete-guide-angular-lifecycle-hooks", + "type": "article", + "url": "complete-guide-angular-lifecycle-hooks" + }, + { + "name": "get-to-know-the-destroyref", + "type": "article", + "url": "get-to-know-the-destroyref" + }, + { + "name": "get-to-know-the-afterrendereffect", + "type": "article", + "url": "get-to-know-the-afterrendereffect" + }, + { + "name": "takeuntildestroy-in-angular-v16", + "type": "article", + "url": "takeuntildestroy-in-angular-v16" + } + ] + }, + { + "id": "animations", + "parentNodeId": "components", + "title": "Animations", + "previousNodeId": "lifecycle", + "description": "Angular provides a powerful animation API that lets developers create complex transitions and visual effects using a declarative syntax.", + "resources": [ + { + "name": "in-depth-guide-into-animations-in-angular", + "type": "article", + "url": "in-depth-guide-into-animations-in-angular" + }, + { + "name": "controlling-angular-animations-programmatically", + "type": "article", + "url": "controlling-angular-animations-programmatically" + }, + { + "name": "add-support-for-reduced-motion-in-angular-animations", + "type": "article", + "url": "add-support-for-reduced-motion-in-angular-animations" + } + ] + }, + { + "id": "change-detection", + "parentNodeId": "components", + "title": "Change Detection", + "previousNodeId": "animations", + "description": "Change detection is the process Angular uses to track changes in the application state and update the DOM to reflect those changes efficiently.", + "resources": [ + { + "name": "change-detection-big-picture-unidirectional-data-flow", + "type": "article", + "url": "change-detection-big-picture-unidirectional-data-flow" + }, + { + "name": "change-detection-big-picture-rendering-cycle", + "type": "article", + "url": "change-detection-big-picture-rendering-cycle" + }, + { + "name": "change-detection-big-picture-operations", + "type": "article", + "url": "change-detection-big-picture-operations" + }, + { + "name": "change-detection-big-picture-overview", + "type": "article", + "url": "change-detection-big-picture-overview" + }, + { + "name": "change-detection-and-component-trees-in-angular-applications", + "type": "article", + "url": "change-detection-and-component-trees-in-angular-applications" + }, + { + "name": "angular-ivy-change-detection-execution-are-you-prepared", + "type": "article", + "url": "angular-ivy-change-detection-execution-are-you-prepared" + }, + { + "name": "what-every-front-end-developer-should-know-about-change-detection-in-angular-and-react", + "type": "article", + "url": "what-every-front-end-developer-should-know-about-change-detection-in-angular-and-react" + }, + { + "name": "a-gentle-introduction-into-change-detection-in-angular", + "type": "article", + "url": "a-gentle-introduction-into-change-detection-in-angular" + }, + { + "name": "the-difference-between-ngdocheck-and-asyncpipe-in-onpush-components", + "type": "article", + "url": "the-difference-between-ngdocheck-and-asyncpipe-in-onpush-components" + }, + { + "name": "deep-dive-into-the-onpush-change-detection-strategy-in-angular", + "type": "article", + "url": "deep-dive-into-the-onpush-change-detection-strategy-in-angular" + }, + { + "name": "the-latest-in-angular-change-detection-zoneless-signals", + "type": "article", + "url": "the-latest-in-angular-change-detection-zoneless-signals" + }, + { + "name": "optimization-techniques-onpush-strategy", + "type": "article", + "url": "optimization-techniques-onpush-strategy" + }, + { + "name": "from-zone-js-to-zoneless-angular-and-back-how-it-all-works", + "type": "article", + "url": "from-zone-js-to-zoneless-angular-and-back-how-it-all-works" + }, + { + "name": "everything-you-need-to-know-about-change-detection-in-angular", + "type": "article", + "url": "everything-you-need-to-know-about-change-detection-in-angular" + }, + { + "name": "do-you-still-think-that-ngzone-zone-js-is-required-for-change-detection-in-angular", + "type": "article", + "url": "do-you-still-think-that-ngzone-zone-js-is-required-for-change-detection-in-angular" + }, + { + "name": "Angular RxJS and Signals: Better Together", + "type": "video", + "url": "https://www.youtube.com/watch?v=KSFPOIauEPU" + } + ] + }, + { + "id": "component-interactions", + "parentNodeId": "components", + "title": "Component Interactions (input/output)", + "previousNodeId": "change-detection", + "description": "Component interaction techniques such as Input/Output decorators, services, and local references allow data and events to flow between components.", + "resources": [ + { + "name": "how-to-cancel-a-component-event-from-output-properties-in-angular", + "type": "article", + "url": "how-to-cancel-a-component-event-from-output-properties-in-angular" + }, + { + "name": "router-data-as-components-inputs-in-angular-v16", + "type": "article", + "url": "router-data-as-components-inputs-in-angular-v16" + }, + { + "name": "required-inputs-in-angular-v16", + "type": "article", + "url": "required-inputs-in-angular-v16" + } + ] + }, + { + "id": "dynamic-components", + "parentNodeId": "components", + "title": "Dynamic Components", + "previousNodeId": "component-interactions", + "description": "Dynamic components are created and inserted into the DOM programmatically at runtime, useful for building flexible and interactive UI patterns.", + "resources": [ + { + "name": "dynamic-components-what-they-are-part-ii", + "type": "article", + "url": "dynamic-components-what-they-are-part-ii" + }, + { + "name": "here-is-what-you-need-to-know-about-dynamic-components-in-angular", + "type": "article", + "url": "here-is-what-you-need-to-know-about-dynamic-components-in-angular" + }, + { + "name": "dynamically-loading-components-with-angular-cli", + "type": "article", + "url": "dynamically-loading-components-with-angular-cli" + }, + { + "name": "rendering-dynamic-components-by-selector-name-in-ivy", + "type": "article", + "url": "rendering-dynamic-components-by-selector-name-in-ivy" + }, + { + "name": "deferred-components-vs-dynamic-components-in-angular", + "type": "article", + "url": "deferred-components-vs-dynamic-components-in-angular" + } + ] + }, + { + "id": "templates", + "title": "Templates", + "parentNodeId": "components", + "previousNodeId": "dynamic-components", + "description": "Templates define the component’s HTML structure and layout, combining standard HTML with Angular’s template syntax for dynamic rendering.", + "resources": [ + { + "name": "using-angular-in-the-right-way-template-syntax", + "type": "article", + "url": "using-angular-in-the-right-way-template-syntax" + }, + { + "name": "angular-template-let-variable-hot-or-not", + "type": "article", + "url": "angular-template-let-variable-hot-or-not" + } + ] + }, + { + "id": "data-binding", + "parentNodeId": "templates", + "title": "Data Binding", + "previousNodeId": null, + "description": "Data binding allows synchronization between the component class and the template, enabling interactive and reactive UI updates.", + "resources": [ + { + "name": "bindon-lesser-known-angular-template-features", + "type": "article", + "url": "bindon-lesser-known-angular-template-features" + }, + { + "name": "the-mechanics-of-property-bindings-update-in-angular", + "type": "article", + "url": "the-mechanics-of-property-bindings-update-in-angular" + } + ] + }, + { + "id": "control-flow", + "parentNodeId": "templates", + "title": "Control Flow", + "previousNodeId": "data-binding", + "description": "Control flow syntax like @if, @for, and @switch helps manage rendering logic directly within Angular templates.", + "resources": [ + { + "name": "diving-into-the-new-angular-control-flow-internals", + "type": "article", + "url": "diving-into-the-new-angular-control-flow-internals" + }, + { + "name": "new-syntax-for-control-flow-in-angular", + "type": "article", + "url": "new-syntax-for-control-flow-in-angular" + }, + { + "name": "build-a-pokemon-gallery-with-new-control-flow-in-angular-17", + "type": "article", + "url": "build-a-pokemon-gallery-with-new-control-flow-in-angular-17" + } + ] + }, + { + "id": "content-projection", + "parentNodeId": "templates", + "title": "Content Projection", + "previousNodeId": "control-flow", + "description": "Content projection enables components to receive and display dynamic external content using , facilitating flexible component composition.", + "resources": [] + }, + { + "id": "modules", + "title": "Modules", + "label": "optional", + "previousNodeId": "components", + "description": "Modules organize Angular applications into cohesive blocks of functionality, making it easier to manage and reuse code across the app.", + "resources": [ + { + "name": "avoiding-common-confusions-with-modules-in-angular", + "type": "article", + "url": "avoiding-common-confusions-with-modules-in-angular" + }, + { + "name": "asynchronous-modules-and-components-in-angular-ivy", + "type": "article", + "url": "asynchronous-modules-and-components-in-angular-ivy" + } + ] + }, + { + "id": "pipes", + "title": "Pipes", + "previousNodeId": "modules", + "description": "Pipes transform data in templates for display purposes, such as formatting dates, numbers, or filtering and sorting lists", + "resources": [ + { + "name": "new-possibilities-with-angulars-push-pipe-part-1", + "type": "article", + "url": "new-possibilities-with-angulars-push-pipe-part-1" + }, + { + "name": "new-possibilities-with-angulars-push-pipe-part-2", + "type": "article", + "url": "new-possibilities-with-angulars-push-pipe-part-2" + }, + { + "name": "the-essential-difference-between-pure-and-impure-pipes-in-angular-and-why-that-matters", + "type": "article", + "url": "the-essential-difference-between-pure-and-impure-pipes-in-angular-and-why-that-matters" + }, + { + "name": "how-pure-and-impure-pipes-work-in-angular-ivy", + "type": "article", + "url": "how-pure-and-impure-pipes-work-in-angular-ivy" + } + ] + }, + { + "id": "directives", + "title": "Directives", + "previousNodeId": "pipes", + "description": "Directives are used to add custom behavior to elements and components, either by manipulating the DOM or by extending component functionality.", + "resources": [ + { + "name": "angular-self-saving-dropdowns-yet-another-directive", + "type": "article", + "url": "angular-self-saving-dropdowns-yet-another-directive" + }, + { + "name": "create-a-directive-for-free-dragging-in-angular", + "type": "article", + "url": "create-a-directive-for-free-dragging-in-angular" + } + ] + }, + { + "id": "attribute-directives", + "parentNodeId": "directives", + "title": "Attribute Directives", + "previousNodeId": null, + "description": "Attribute directives change the appearance or behavior of an element, component, or another directive by modifying its attributes.", + "resources": [] + }, + { + "id": "structural-directives", + "parentNodeId": "directives", + "title": "Structural Directives", + "previousNodeId": "attribute-directives", + "description": "Structural directives shape the layout of the DOM by adding or removing elements dynamically, typically using * syntax like *ngIf and *ngFor.", + "resources": [] + }, + { + "id": "ng-template-ng-container", + "parentNodeId": "directives", + "title": "ng-template, ng-container", + "previousNodeId": "structural-directives", + "description": " and are structural elements used to control rendering logic without adding extra elements to the DOM.", + "resources": [ + { + "name": "ngtemplateoutlet-the-secret-to-customisation", + "type": "article", + "url": "ngtemplateoutlet-the-secret-to-customisation" + } + ] + }, + { + "id": "directive-composition", + "parentNodeId": "directives", + "title": "Directive Composition", + "previousNodeId": "ng-template-ng-container", + "description": "Directive composition allows combining multiple directives on a single element to build rich, reusable UI behaviors and features.", + "resources": [ + { + "name": "work-smart-not-hard-use-directive-composition-api", + "type": "article", + "url": "work-smart-not-hard-use-directive-composition-api" + } + ] + }, + { + "id": "routing", + "title": "Routing", + "previousNodeId": "directives", + "description": "Routing in Angular allows users to navigate between different views or pages while staying in a single-page application context.", + "resources": [ + { + "name": "angular-router-everything-you-need-to-know-about", + "type": "article", + "url": "angular-router-everything-you-need-to-know-about" + }, + { + "name": "how-to-reuse-common-layouts-in-angular-using-router", + "type": "article", + "url": "how-to-reuse-common-layouts-in-angular-using-router" + }, + { + "name": "improved-navigation-in-angular-7-with-switchmap", + "type": "article", + "url": "improved-navigation-in-angular-7-with-switchmap" + }, + { + "name": "angular-scroll-position-restoration", + "type": "article", + "url": "angular-scroll-position-restoration" + }, + { + "name": "router-data-as-components-inputs-in-angular-v16", + "type": "article", + "url": "router-data-as-components-inputs-in-angular-v16" + } + ] + }, + { + "id": "configuration", + "parentNodeId": "routing", + "title": "Configuration", + "previousNodeId": null, + "description": "Routing configuration defines the mapping between application URLs and components, supporting nested routes, redirects, and lazy loading.", + "resources": [ + { + "name": "external-configurations-in-angular", + "type": "article", + "url": "external-configurations-in-angular" + }, + { + "name": "dynamic-configuration-leveraging-app-initializer", + "type": "article", + "url": "dynamic-configuration-leveraging-app-initializer" + } + ] + }, + { + "id": "guards-resolvers", + "parentNodeId": "routing", + "title": "Guards, Resolvers", + "previousNodeId": "configuration", + "description": "Guards and resolvers control access and fetch data before a route is activated, helping protect routes and preload necessary information.", + "resources": [] + }, + { + "id": "routerlink", + "parentNodeId": "routing", + "title": "routerLink, routerLinkActive, ...", + "previousNodeId": "guards-resolvers", + "description": "Router directives like routerLink and routerLinkActive enable navigation and active link styling in Angular templates.", + "resources": [] + }, + { + "id": "router-outlets", + "parentNodeId": "routing", + "title": "Router Outlets", + "previousNodeId": "routerlink", + "description": " is a directive that acts as a placeholder for rendering the component associated with the current route.", + "resources": [ + { + "name": "angular-router-series-secondary-outlets-primer", + "type": "article", + "url": "angular-router-series-secondary-outlets-primer" + } + ] + }, + { + "id": "dependency-injection", + "title": "Dependency Injection", + "previousNodeId": "routing", + "description": "Angular’s dependency injection system provides a powerful way to manage services and dependencies, improving modularity and testability.", + "resources": [ + { + "name": "dependency-injection-in-angular-everything-you-need-to-know", + "type": "article", + "url": "dependency-injection-in-angular-everything-you-need-to-know" + }, + { + "name": "make-the-most-of-angular-di-private-providers-concept", + "type": "article", + "url": "make-the-most-of-angular-di-private-providers-concept" + }, + { + "name": "a-deep-dive-into-injectable-and-providedin-in-ivy", + "type": "article", + "url": "a-deep-dive-into-injectable-and-providedin-in-ivy" + }, + { + "name": "angular-di-getting-to-know-the-ivy-nodeinjector", + "type": "article", + "url": "angular-di-getting-to-know-the-ivy-nodeinjector" + }, + { + "name": "a-curious-case-of-the-host-decorator-and-element-injectors-in-angular", + "type": "article", + "url": "a-curious-case-of-the-host-decorator-and-element-injectors-in-angular" + }, + { + "name": "what-you-always-wanted-to-know-about-angular-dependency-injection-tree", + "type": "article", + "url": "what-you-always-wanted-to-know-about-angular-dependency-injection-tree" + }, + { + "name": "leveraging-dependency-injection-to-reduce-duplicated-code-in-angular", + "type": "article", + "url": "leveraging-dependency-injection-to-reduce-duplicated-code-in-angular" + }, + { + "name": "how-to-avoid-angular-injectable-instances-duplication", + "type": "article", + "url": "how-to-avoid-angular-injectable-instances-duplication" + }, + { + "name": "what-is-forwardref-in-angular-and-why-we-need-it", + "type": "article", + "url": "what-is-forwardref-in-angular-and-why-we-need-it" + } + ] + }, + { + "id": "forms", + "title": "Forms", + "previousNodeId": "dependency-injection", + "description": "Angular provides two main approaches to building forms: Template-Driven and Reactive, both supporting validation and dynamic form controls.", + "resources": [ + { + "name": "a-thorough-exploration-of-angular-forms", + "type": "article", + "url": "a-thorough-exploration-of-angular-forms" + }, + { + "name": "angular-forms-useful-tips", + "type": "article", + "url": "angular-forms-useful-tips" + }, + { + "name": "angular-forms-why-is-ngmodelchange-late-when-updating-ngmodel-value", + "type": "article", + "url": "angular-forms-why-is-ngmodelchange-late-when-updating-ngmodel-value" + }, + { + "name": "the-updateon-option-in-angular-forms", + "type": "article", + "url": "the-updateon-option-in-angular-forms" + } + ] + }, + { + "id": "template-driven-forms", + "parentNodeId": "forms", + "title": "Template-Driven Forms", + "previousNodeId": null, + "description": "Template-driven forms rely on directives in the template to create and manage form controls, offering a simple and declarative way to build forms.", + "resources": [] + }, + { + "id": "control-value-accessor", + "parentNodeId": "forms", + "title": "Control Value Accessor", + "previousNodeId": "template-driven-forms", + "description": "Control Value Accessors are used to create custom form controls that integrate seamlessly with Angular forms and validation.", + "resources": [ + { + "name": "never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms", + "type": "article", + "url": "never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms" + }, + { + "name": "how-to-use-controlvalueaccessor-to-enhance-date-input-with-automatic-conversion-and-validation", + "type": "article", + "url": "how-to-use-controlvalueaccessor-to-enhance-date-input-with-automatic-conversion-and-validation" + } + ] + }, + { + "id": "signal-forms", + "parentNodeId": "forms", + "label": "comingSoon", + "title": "Signal Forms", + "previousNodeId": "control-value-accessor", + "description": "Signal Forms offer a reactive and fine-grained approach to form state and validation using Angular’s signal-based reactivity model.", + "resources": [] + }, + { + "id": "reactive-forms", + "parentNodeId": "forms", + "title": "Reactive Forms", + "previousNodeId": "signal-forms", + "label": "recommended", + "description": "Reactive forms provide a model-driven way to manage form controls in code, giving more explicit control over validation and dynamic form logic.", + "resources": [ + { + "name": "strongly-typed-reactive-forms-in-angular", + "type": "article", + "url": "strongly-typed-reactive-forms-in-angular" + }, + { + "name": "implementing-reusable-and-reactive-forms-in-angular", + "type": "article", + "url": "implementing-reusable-and-reactive-forms-in-angular" + }, + { + "name": "angular-forms-reactive-design-patterns-catalog", + "type": "article", + "url": "angular-forms-reactive-design-patterns-catalog" + }, + { + "name": "convert-into-strongly-typed-angular-forms-in-a-minute", + "type": "article", + "url": "convert-into-strongly-typed-angular-forms-in-a-minute" + }, + { + "name": "exploring-the-difference-between-disabling-a-form-control-through-reactive-forms-api-and-html-attributes", + "type": "article", + "url": "exploring-the-difference-between-disabling-a-form-control-through-reactive-forms-api-and-html-attributes" + }, + { + "name": "nested-forms-with-controlcontainer", + "type": "article", + "url": "nested-forms-with-controlcontainer" + }, + { + "name": "angular-forms-story-strong-types", + "type": "article", + "url": "angular-forms-story-strong-types" + } + ] + }, + { + "id": "validation", + "parentNodeId": "forms", + "title": "Validation", + "previousNodeId": "reactive-forms", + "description": "Form validation in Angular ensures user input meets defined rules, supporting both built-in validators and custom validation logic.", + "resources": [ + { + "name": "the-best-way-to-implement-custom-validators", + "type": "article", + "url": "the-best-way-to-implement-custom-validators" + }, + { + "name": "creating-elegant-reactive-forms-with-rxwebvalidators", + "type": "article", + "url": "creating-elegant-reactive-forms-with-rxwebvalidators" + } + ] + }, + { + "id": "reactivity", + "title": "Reactivity", + "previousNodeId": "forms", + "description": "Reactivity in Angular refers to the automatic synchronization of data between the application state and the UI, enabling efficient, responsive updates through tools like signals and RxJS.", + "resources": [ + { + "name": "finding-fine-grained-reactive-programming", + "type": "article", + "url": "finding-fine-grained-reactive-programming" + }, + { + "name": "exploring-the-state-of-reactivity-patterns-in-2020", + "type": "article", + "url": "exploring-the-state-of-reactivity-patterns-in-2020" + }, + { + "name": "declarative-reactive-data-and-action-streams-in-angular", + "type": "article", + "url": "declarative-reactive-data-and-action-streams-in-angular" + } + ] + }, + { + "id": "signals", + "parentNodeId": "reactivity", + "title": "Signals", + "previousNodeId": null, + "description": "Signals in Angular is a reactive primitive for managing and tracking state changes in a straightforward and predictable way, enabling fine-grained reactivity without relying on observables.", + "resources": [ + { + "name": "what-linkedsignal-is-and-how-to-use-it", + "type": "article", + "url": "what-linkedsignal-is-and-how-to-use-it" + }, + { + "name": "signals-in-angular-deep-dive-for-busy-developers", + "type": "article", + "url": "signals-in-angular-deep-dive-for-busy-developers" + }, + { + "name": "angular-signals-a-new-feature-in-angular-16", + "type": "article", + "url": "angular-signals-a-new-feature-in-angular-16" + }, + { + "name": "the-latest-in-angular-change-detection-zoneless-signals", + "type": "article", + "url": "the-latest-in-angular-change-detection-zoneless-signals" + }, + { + "name": "why-angular-signals-wont-replace-rxjs", + "type": "article", + "url": "why-angular-signals-wont-replace-rxjs" + }, + { + "name": "angular-signals-rxjs-interop-from-a-practical-example", + "type": "article", + "url": "angular-signals-rxjs-interop-from-a-practical-example" + } + ] + }, + { + "id": "rxjs", + "parentNodeId": "reactivity", + "title": "RXJS", + "previousNodeId": "signals", + "description": "RxJS is a powerful library for reactive programming in Angular, allowing developers to work with asynchronous data streams and events using observables, operators, and subscriptions.", + "resources": [ + { + "name": "rxjs-recipes-forkjoin-with-the-progress-of-completion-for-bulk-network-requests-in-angular", + "type": "article", + "url": "rxjs-recipes-forkjoin-with-the-progress-of-completion-for-bulk-network-requests-in-angular" + }, + { + "name": "rxjs-for-await-what", + "type": "article", + "url": "rxjs-for-await-what" + }, + { + "name": "rxjs-why-memory-leaks-occur-when-using-a-subject", + "type": "article", + "url": "rxjs-why-memory-leaks-occur-when-using-a-subject" + }, + { + "name": "rxjs-custom-operators", + "type": "article", + "url": "rxjs-custom-operators" + }, + { + "name": "create-a-taponce-custom-rxjs-operator", + "type": "article", + "url": "create-a-taponce-custom-rxjs-operator" + }, + { + "name": "telegraph-with-rxjs-the-power-of-reactive-systems", + "type": "article", + "url": "telegraph-with-rxjs-the-power-of-reactive-systems" + }, + { + "name": "the-state-of-rxjs-rxjs-7-and-beyond", + "type": "article", + "url": "the-state-of-rxjs-rxjs-7-and-beyond" + }, + { + "name": "rxjs7-whats-new", + "type": "article", + "url": "rxjs7-whats-new" + }, + { + "name": "subtle-difference-between-map-and-pluck-rxjs-operators-that-you-should-know", + "type": "article", + "url": "subtle-difference-between-map-and-pluck-rxjs-operators-that-you-should-know" + }, + { + "name": "rxjs-applying-asyncscheduler-as-an-argument-vs-with-observeon-operator", + "type": "article", + "url": "rxjs-applying-asyncscheduler-as-an-argument-vs-with-observeon-operator" + }, + { + "name": "reading-the-rxjs-6-sources-map-and-pipe", + "type": "article", + "url": "reading-the-rxjs-6-sources-map-and-pipe" + }, + { + "name": "rxjs-in-angular-when-to-subscribe-rarely", + "type": "article", + "url": "rxjs-in-angular-when-to-subscribe-rarely" + }, + { + "name": "how-to-read-the-rxjs-6-sources-part-1-understanding-of-and-subscriptions", + "type": "article", + "url": "how-to-read-the-rxjs-6-sources-part-1-understanding-of-and-subscriptions" + }, + { + "name": "rxjs-in-angular-part-i", + "type": "article", + "url": "rxjs-in-angular-part-i" + }, + { + "name": "rxjs-in-angular-part-ii", + "type": "article", + "url": "rxjs-in-angular-part-ii" + }, + { + "name": "rxjs-in-angular-part-iii", + "type": "article", + "url": "rxjs-in-angular-part-iii" + }, + { + "name": "fastest-way-to-cache-for-lazy-developers-angular-with-rxjs", + "type": "article", + "url": "fastest-way-to-cache-for-lazy-developers-angular-with-rxjs" + }, + { + "name": "rxjs-repeat-operator-beginner-necromancer-guide", + "type": "article", + "url": "rxjs-repeat-operator-beginner-necromancer-guide" + }, + { + "name": "how-to-debounce-an-input-while-skipping-the-first-entry", + "type": "article", + "url": "how-to-debounce-an-input-while-skipping-the-first-entry" + }, + { + "name": "throttling-notifications-from-multiple-users-with-rxjs", + "type": "article", + "url": "throttling-notifications-from-multiple-users-with-rxjs" + }, + { + "name": "power-of-rxjs-when-using-exponential-backoff", + "type": "article", + "url": "power-of-rxjs-when-using-exponential-backoff" + }, + { + "name": "rxjs-heads-up-topromise-is-being-deprecated", + "type": "article", + "url": "rxjs-heads-up-topromise-is-being-deprecated" + }, + { + "name": "the-simple-way-to-reload-data-using-rxjs", + "type": "article", + "url": "the-simple-way-to-reload-data-using-rxjs" + }, + { + "name": "rxjs-used-in-angular-knowledge-in-a-nutshell", + "type": "article", + "url": "rxjs-used-in-angular-knowledge-in-a-nutshell" + }, + { + "name": "build-your-own-observable-part-1-arrays", + "type": "article", + "url": "build-your-own-observable-part-1-arrays" + }, + { + "name": "build-your-own-observable-part-2-containers-and-intuition", + "type": "article", + "url": "build-your-own-observable-part-2-containers-and-intuition" + }, + { + "name": "building-your-own-observable-part-3-the-observer-pattern-and-creational-methods", + "type": "article", + "url": "building-your-own-observable-part-3-the-observer-pattern-and-creational-methods" + }, + { + "name": "build-your-own-observable-part-4-map-filter-take-and-all-that-jazz", + "type": "article", + "url": "build-your-own-observable-part-4-map-filter-take-and-all-that-jazz" + } + ] + }, + { + "id": "http", + "title": "HTTP", + "previousNodeId": "reactivity", + "description": "Angular's HttpClient provides a streamlined way to communicate with backend services via HTTP methods like GET, POST, PUT, and DELETE.", + "resources": [ + { + "name": "the-new-angular-httpclient-api", + "type": "article", + "url": "the-new-angular-httpclient-api" + }, + { + "name": "exploring-the-httpclientmodule-in-angular", + "type": "article", + "url": "exploring-the-httpclientmodule-in-angular" + }, + { + "name": "how-to-use-the-environment-for-specific-http-services", + "type": "article", + "url": "how-to-use-the-environment-for-specific-http-services" + }, + { + "name": "how-to-use-ts-decorators-to-add-caching-logic-to-api-calls", + "type": "article", + "url": "how-to-use-ts-decorators-to-add-caching-logic-to-api-calls" + } + ] + }, + { + "id": "interceptors", + "parentNodeId": "http", + "title": "interceptors", + "previousNodeId": null, + "description": "HTTP interceptors in Angular allow you to intercept and modify HTTP requests or responses globally, useful for tasks like adding auth tokens or handling errors.", + "resources": [ + { + "name": "how-to-implement-automatic-token-insertion-in-requests-using-http-interceptor-angular-tutorials", + "type": "article", + "url": "how-to-implement-automatic-token-insertion-in-requests-using-http-interceptor-angular-tutorials" + }, + { + "name": "how-to-split-http-interceptors-between-multiple-backends", + "type": "article", + "url": "how-to-split-http-interceptors-between-multiple-backends" + }, + { + "name": "insiders-guide-into-interceptors-and-httpclient-mechanics-in-angular", + "type": "article", + "url": "insiders-guide-into-interceptors-and-httpclient-mechanics-in-angular" + } + ] + }, + { + "id": "requests", + "parentNodeId": "http", + "title": "requests", + "previousNodeId": "interceptors", + "description": "In Angular, HTTP requests are the primary way to interact with RESTful APIs, enabling the app to fetch, send, and manipulate remote data in a structured and asynchronous manner.", + "resources": [ + { + "name": "parsing-and-mapping-api-response-using-zod-js", + "type": "article", + "url": "parsing-and-mapping-api-response-using-zod-js" + } + ] + }, + { + "id": "testing", + "title": "Testing", + "previousNodeId": "http", + "description": "Testing in Angular ensures code reliability and maintainability by verifying that components, services, and other parts of the app behave as expected.", + "resources": [ + { + "name": "effective-rxjs-marble-testing", + "type": "article", + "url": "effective-rxjs-marble-testing" + }, + { + "name": "angular-testing-with-headless-chrome", + "type": "article", + "url": "angular-testing-with-headless-chrome" + } + ] + }, + { + "id": "unit-tests", + "parentNodeId": "testing", + "title": "Unit Tests", + "previousNodeId": null, + "description": "Unit tests focus on testing individual pieces of logic, like functions or components, in isolation to validate their correctness.", + "resources": [ + { + "name": "angular-unit-testing-viewchild", + "type": "article", + "url": "angular-unit-testing-viewchild" + }, + { + "name": "create-your-angular-unit-test-spies-automagically", + "type": "article", + "url": "create-your-angular-unit-test-spies-automagically" + }, + { + "name": "spectator-when-testing-becomes-a-pleasure", + "type": "article", + "url": "spectator-when-testing-becomes-a-pleasure" + }, + { + "name": "ng-mocks-what-is-it-all-about", + "type": "article", + "url": "ng-mocks-what-is-it-all-about" + }, + { + "name": "learn-how-to-unit-test-the-deferrable-views", + "type": "article", + "url": "learn-how-to-unit-test-the-deferrable-views" + } + ] + }, + { + "id": "integration-tests", + "parentNodeId": "testing", + "title": "Integration Tests", + "previousNodeId": "unit-tests", + "description": "Integration tests verify how different parts of the application work together, ensuring combined components or services interact as intended.", + "resources": [ + { + "name": "write-better-automated-tests-with-cypress-in-angular", + "type": "article", + "url": "write-better-automated-tests-with-cypress-in-angular" + }, + { + "name": "how-cypress-makes-testing-fun", + "type": "article", + "url": "how-cypress-makes-testing-fun" + }, + { + "name": "visual-regression-testing-with-cypress-and-angular", + "type": "article", + "url": "visual-regression-testing-with-cypress-and-angular" + }, + { + "name": "cypress-introduction", + "type": "article", + "url": "cypress-introduction" + } + ] + }, + { + "id": "angular-cli", + "title": "Angular CLI", + "previousNodeId": "testing", + "description": "Angular CLI is a powerful command-line tool that simplifies Angular development by automating project setup, code generation, testing, and building processes.", + "resources": [ + { + "name": "angular-cli-flows-big-picture", + "type": "article", + "url": "angular-cli-flows-big-picture" + }, + { + "name": "angular-cli-builders", + "type": "article", + "url": "angular-cli-builders" + }, + { + "name": "angular-cli-camelcase-or-kebab-case", + "type": "article", + "url": "angular-cli-camelcase-or-kebab-case" + }, + { + "name": "angular-generators", + "type": "article", + "url": "angular-generators" + }, + { + "name": "hide-boilerplate-nx-files-in-vscode-webstorm", + "type": "article", + "url": "hide-boilerplate-nx-files-in-vscode-webstorm" + }, + { + "name": "how-to-stop-being-afraid-and-create-your-own-angular-cli-builder", + "type": "article", + "url": "how-to-stop-being-afraid-and-create-your-own-angular-cli-builder" + }, + { + "name": "angular-compilation-restrictions-overview", + "type": "article", + "url": "angular-compilation-restrictions-overview" + } + ] + }, + { + "id": "state-management", + "title": "State Management", + "previousNodeId": "angular-cli", + "description": "State management in Angular involves handling application data in a predictable way to ensure consistency across components.", + "resources": [ + { + "name": "ngrx-best-practices", + "type": "article", + "url": "ngrx-best-practices" + }, + { + "name": "ngrx-bad-practices", + "type": "article", + "url": "ngrx-bad-practices" + }, + { + "name": "ngrx-not-only-store", + "type": "article", + "url": "ngrx-not-only-store" + }, + { + "name": "how-to-manage-component-state-in-angular-using-ngrx-component-store", + "type": "article", + "url": "how-to-manage-component-state-in-angular-using-ngrx-component-store" + }, + { + "name": "how-i-got-rid-of-state-observables-in-angular", + "type": "article", + "url": "how-i-got-rid-of-state-observables-in-angular" + } + ] + }, + { + "id": "ngrx", + "parentNodeId": "state-management", + "title": "NGRX", + "previousNodeId": null, + "description": "NgRx is a Redux-inspired state management library for Angular that uses actions, reducers, and effects to manage complex application state in a reactive and scalable manner.", + "resources": [ + { + "name": "ngrx-tips-tricks-2", + "type": "article", + "url": "ngrx-tips-tricks-2" + }, + { + "name": "adding-ngrx-to-your-existing-applications", + "type": "article", + "url": "adding-ngrx-to-your-existing-applications" + }, + { + "name": "make-ngrx-hold-business-logic-dumb-components-smart-store", + "type": "article", + "url": "make-ngrx-hold-business-logic-dumb-components-smart-store" + }, + { + "name": "better-action-hygiene-with-events-in-ngrx", + "type": "article", + "url": "better-action-hygiene-with-events-in-ngrx" + }, + { + "name": "understanding-the-magic-behind-ngrx-effects", + "type": "article", + "url": "understanding-the-magic-behind-ngrx-effects" + }, + { + "name": "understanding-the-magic-behind-storemodule-of-ngrx-ngrx-store", + "type": "article", + "url": "understanding-the-magic-behind-storemodule-of-ngrx-ngrx-store" + }, + { + "name": "typesafe-code-with-immer-and-where-it-can-help-in-ngrx", + "type": "article", + "url": "typesafe-code-with-immer-and-where-it-can-help-in-ngrx" + }, + { + "name": "a-journey-into-ngrx-selectors", + "type": "article", + "url": "a-journey-into-ngrx-selectors" + }, + { + "name": "ngrx-component", + "type": "article", + "url": "ngrx-component" + }, + { + "name": "understanding-ngrx-component-store-selector-debouncing", + "type": "article", + "url": "understanding-ngrx-component-store-selector-debouncing" + }, + { + "name": "ngrx-use-effects-and-router-store-to-isolate-route-related-side-effects", + "type": "article", + "url": "ngrx-use-effects-and-router-store-to-isolate-route-related-side-effects" + }, + { + "name": "whats-new-in-ngrx-changes-overview-tips-and-tricks", + "type": "article", + "url": "whats-new-in-ngrx-changes-overview-tips-and-tricks" + }, + { + "name": "making-an-angular-project-mono-repo-with-ngrx-state-management-and-lazy-loading", + "type": "article", + "url": "making-an-angular-project-mono-repo-with-ngrx-state-management-and-lazy-loading" + }, + { + "name": "how-to-start-flying-with-angular-and-ngrx", + "type": "article", + "url": "how-to-start-flying-with-angular-and-ngrx" + }, + { + "name": "ngrx-how-and-where-to-handle-loading-and-error-states-of-ajax-calls", + "type": "article", + "url": "ngrx-how-and-where-to-handle-loading-and-error-states-of-ajax-calls" + } + ] + }, + { + "id": "signal-store", + "parentNodeId": "state-management", + "title": "NGRX Signal Store", + "previousNodeId": "ngrx", + "description": "Signal Store is a lightweight, reactive state management solution built around Angular signals, offering a simpler alternative to more complex libraries like NgRx.", + "resources": [ + { + "name": "breakthrough-in-state-management-discover-the-simplicity-of-signal-store-part-1", + "type": "article", + "url": "breakthrough-in-state-management-discover-the-simplicity-of-signal-store-part-1" + }, + { + "name": "signal-store-ngxs-elevating-flexibility-in-state-management", + "type": "article", + "url": "signal-store-ngxs-elevating-flexibility-in-state-management" + } + ] + }, + { + "id": "ngxs", + "parentNodeId": "state-management", + "title": "NGXS", + "previousNodeId": "signal-store", + "description": "NGXS is a state management library for Angular that provides a more concise, decorator-based approach to handling state, aiming for simplicity while maintaining structure.", + "resources": [ + { + "name": "firebase-ngxs-the-perfect-couple", + "type": "article", + "url": "firebase-ngxs-the-perfect-couple" + }, + { + "name": "all-you-need-to-know-to-jumpstart-with-ngxs", + "type": "article", + "url": "all-you-need-to-know-to-jumpstart-with-ngxs" + }, + { + "name": "signal-store-ngxs-elevating-flexibility-in-state-management", + "type": "article", + "url": "signal-store-ngxs-elevating-flexibility-in-state-management" + } + ] + }, + { + "id": "developer-tools", + "title": "Developer Tools", + "previousNodeId": "state-management", + "description": "Angular Developer Tools (DevTools) is a browser extension that helps inspect and debug Angular applications, offering insights into component trees, change detection cycles, and performance bottlenecks.", + "resources": [ + { + "name": "debugging-techniques-chrome-devtools", + "type": "article", + "url": "debugging-techniques-chrome-devtools" + }, + { + "name": "easier-angular-ivy-debugging-with-a-chrome-extension", + "type": "article", + "url": "easier-angular-ivy-debugging-with-a-chrome-extension" + }, + { + "name": "useful-chrome-devtools-techniques-when-debugging-change-detection-in-angular", + "type": "article", + "url": "useful-chrome-devtools-techniques-when-debugging-change-detection-in-angular" + }, + { + "name": "debugging-techniques-angular-devtools", + "type": "article", + "url": "debugging-techniques-angular-devtools" + }, + { + "name": "setting-up-efficient-workflows-with-eslint-prettier-and-typescript", + "type": "article", + "url": "setting-up-efficient-workflows-with-eslint-prettier-and-typescript" + } + ] + }, + { + "id": "internationalization", + "title": "Internationalization", + "previousNodeId": "developer-tools", + "description": "Internationalization (i18n) in Angular enables applications to support multiple languages and regional formats by providing built-in tools for translating text, formatting dates, numbers, and handling locale-specific content.", + "resources": [ + { + "name": "internationalization-how-to-open-an-application-to-the-world-part-1", + "type": "article", + "url": "internationalization-how-to-open-an-application-to-the-world-part-1" + }, + { + "name": "internationalization-how-to-open-an-application-to-the-world-part-2", + "type": "article", + "url": "internationalization-how-to-open-an-application-to-the-world-part-2" + }, + { + "name": "implementing-multi-language-angular-applications-rendered-on-a-server-ssr", + "type": "article", + "url": "implementing-multi-language-angular-applications-rendered-on-a-server-ssr" + } + ] + }, + { + "id": "performance", + "title": "Performance", + "previousNodeId": "internationalization", + "description": "Performance focuses on optimizing load times and runtime efficiency through techniques like code splitting, efficient change detection, and minimizing unnecessary rendering.", + "resources": [ + { + "name": "how-to-use-angulars-defer-block-to-improve-performance", + "type": "article", + "url": "how-to-use-angulars-defer-block-to-improve-performance" + }, + { + "name": "bundle-size-improvements-from-deferred-views-in-angular", + "type": "article", + "url": "bundle-size-improvements-from-deferred-views-in-angular" + }, + { + "name": "boost-your-applications-performance-with-ngoptimizedimage", + "type": "article", + "url": "boost-your-applications-performance-with-ngoptimizedimage" + }, + { + "name": "improve-page-performance-and-lcp-with-ngoptimizedimage", + "type": "article", + "url": "improve-page-performance-and-lcp-with-ngoptimizedimage" + }, + { + "name": "how-in-depth-knowledge-of-change-detection-in-angular-helped-me-improve-applications-performance", + "type": "article", + "url": "how-in-depth-knowledge-of-change-detection-in-angular-helped-me-improve-applications-performance" + }, + { + "name": "simple-angular-context-help-component-or-how-global-event-listener-can-affect-your-performance", + "type": "article", + "url": "simple-angular-context-help-component-or-how-global-event-listener-can-affect-your-performance" + }, + { + "name": "optimizing-events-handling-in-angular", + "type": "article", + "url": "optimizing-events-handling-in-angular" + }, + { + "name": "optimize-angular-bundle-size-in-4-steps", + "type": "article", + "url": "optimize-angular-bundle-size-in-4-steps" + }, + { + "name": "optimize-your-angular-bundle-size", + "type": "article", + "url": "optimize-your-angular-bundle-size" + }, + { + "name": "how-to-exclude-stylesheets-from-the-bundle-and-lazy-load-them-in-angular-angular-tutorials", + "type": "article", + "url": "how-to-exclude-stylesheets-from-the-bundle-and-lazy-load-them-in-angular-angular-tutorials" + } + ] + }, + { + "id": "ngoptimizeimage", + "parentNodeId": "performance", + "title": "ngOptimizeImage", + "previousNodeId": null, + "description": "NgOptimizeImage is a directive that automatically optimizes images for better performance by handling responsive sizing, lazy loading, and format selection.", + "resources": [ + { + "name": "boost-your-applications-performance-with-ngoptimizedimage", + "type": "article", + "url": "boost-your-applications-performance-with-ngoptimizedimage" + }, + { + "name": "improve-page-performance-and-lcp-with-ngoptimizedimage", + "type": "article", + "url": "improve-page-performance-and-lcp-with-ngoptimizedimage" + }, + { + "name": "the-who-what-when-where-why-and-how-of-image-optimization-in-angular", + "type": "article", + "url": "the-who-what-when-where-why-and-how-of-image-optimization-in-angular" + } + ] + }, + { + "id": "defer", + "parentNodeId": "performance", + "title": "@defer", + "previousNodeId": "ngoptimizeimage", + "description": "The @defer block allows parts of a component’s template to load lazily, improving initial render performance by delaying non-critical content.", + "resources": [ + { + "name": "learn-how-to-unit-test-the-deferrable-views", + "type": "article", + "url": "learn-how-to-unit-test-the-deferrable-views" + }, + { + "name": "how-to-use-angulars-defer-block-to-improve-performance", + "type": "article", + "url": "how-to-use-angulars-defer-block-to-improve-performance" + }, + { + "name": "bundle-size-improvements-from-deferred-views-in-angular", + "type": "article", + "url": "bundle-size-improvements-from-deferred-views-in-angular" + }, + { + "name": "deferred-components-vs-dynamic-components-in-angular", + "type": "article", + "url": "deferred-components-vs-dynamic-components-in-angular" + } + ] + }, + { + "id": "lazy-loading", + "parentNodeId": "performance", + "title": "Lazy Loading", + "previousNodeId": "defer", + "description": "Lazy loading in Angular loads features only when needed, reducing the initial bundle size and speeding up application startup.", + "resources": [ + { + "name": "angular-router-series-pillar-3-lazy-loading-aot-and-preloading", + "type": "article", + "url": "angular-router-series-pillar-3-lazy-loading-aot-and-preloading" + }, + { + "name": "lazy-loading-angular-modules-with-ivy", + "type": "article", + "url": "lazy-loading-angular-modules-with-ivy" + }, + { + "name": "asynchronous-modules-and-components-in-angular-ivy", + "type": "article", + "url": "asynchronous-modules-and-components-in-angular-ivy" + }, + { + "name": "lazy-loading-angular-components-from-non-angular-applications", + "type": "article", + "url": "lazy-loading-angular-components-from-non-angular-applications" + }, + { + "name": "angular-lazy-load-common-styles-specific-to-a-feature-module", + "type": "article", + "url": "angular-lazy-load-common-styles-specific-to-a-feature-module" + } + ] + }, + { + "id": "architecture-design-patterns", + "title": "Architecture / Design Patterns", + "previousNodeId": "performance", + "description": "Angular’s architecture follows a modular and component-based design, leveraging patterns like dependency injection, reactive programming, and separation of concerns to build scalable and maintainable applications.", + "resources": [ + { + "name": "ports-and-adapters-vs-hexagonal-architecture-is-it-the-same-pattern", + "type": "article", + "url": "ports-and-adapters-vs-hexagonal-architecture-is-it-the-same-pattern" + }, + { + "name": "angular-facade-pattern", + "type": "article", + "url": "angular-facade-pattern" + }, + { + "name": "designing-angular-architecture-container-presentation-pattern", + "type": "article", + "url": "designing-angular-architecture-container-presentation-pattern" + }, + { + "name": "view-state-selector-angular-design-pattern", + "type": "article", + "url": "view-state-selector-angular-design-pattern" + }, + { + "name": "designing-scalable-angular-applications", + "type": "article", + "url": "designing-scalable-angular-applications" + }, + { + "name": "angular-and-solid-principles", + "type": "article", + "url": "angular-and-solid-principles" + }, + { + "name": "stop-using-services-the-importance-of-defining-object-responsibilities-precisely", + "type": "article", + "url": "stop-using-services-the-importance-of-defining-object-responsibilities-precisely" + }, + { + "name": "angular-dependency-inversion-principle-2", + "type": "article", + "url": "angular-dependency-inversion-principle-2" + }, + { + "name": "angular-interface-segregation-principle-2", + "type": "article", + "url": "angular-interface-segregation-principle-2" + }, + { + "name": "angular-liskov-substitution-principle-2", + "type": "article", + "url": "angular-liskov-substitution-principle-2" + }, + { + "name": "angular-open-closed-principle-2", + "type": "article", + "url": "angular-open-closed-principle-2" + }, + { + "name": "angular-single-responsibility-principle-2", + "type": "article", + "url": "angular-single-responsibility-principle-2" + }, + { + "name": "building-an-extensible-dynamic-pluggable-enterprise-application-with-angular", + "type": "article", + "url": "building-an-extensible-dynamic-pluggable-enterprise-application-with-angular" + }, + { + "name": "implementing-shared-logic-for-crud-ui-components-in-angular", + "type": "article", + "url": "implementing-shared-logic-for-crud-ui-components-in-angular" + }, + { + "name": "scalable-modular-angular-application-with-nx", + "type": "article", + "url": "scalable-modular-angular-application-with-nx" + }, + { + "name": "building-a-type-agnostic-cache-using-generics-in-typescript", + "type": "article", + "url": "building-a-type-agnostic-cache-using-generics-in-typescript" + }, + { + "name": "overview-of-oop-patterns-implementation-in-javascript", + "type": "article", + "url": "overview-of-oop-patterns-implementation-in-javascript" + }, + { + "name": "demystifying-taiga-ui-root-component-portals-pattern-in-angular", + "type": "article", + "url": "demystifying-taiga-ui-root-component-portals-pattern-in-angular" + }, + { + "name": "the-controllers-of-component-concept-in-angular-part-ii", + "type": "article", + "url": "the-controllers-of-component-concept-in-angular-part-ii" + } + ] + }, + { + "id": "security", + "title": "Security", + "previousNodeId": "architecture-design-patterns", + "description": "Security in Angular involves protecting applications from common web vulnerabilities such as XSS and CSRF by using built-in safeguards like sanitization, strict typing, and secure HTTP practices.", + "resources": [ + { + "name": "localstorage-vs-cookies-all-you-need-to-know-about-storing-jwt-tokens-securely-in-the-front-end", + "type": "article", + "url": "localstorage-vs-cookies-all-you-need-to-know-about-storing-jwt-tokens-securely-in-the-front-end" + }, + { + "name": "can-we-fully-trust-html-sanitizers-and-how-to-work-without-them", + "type": "article", + "url": "can-we-fully-trust-html-sanitizers-and-how-to-work-without-them" + }, + { + "name": "implement-google-sign-inoauth-in-your-angular-app-in-under-15-minutes", + "type": "article", + "url": "implement-google-sign-inoauth-in-your-angular-app-in-under-15-minutes" + } + ] + }, + { + "id": "ssr", + "title": "SSR", + "previousNodeId": "security", + "description": "SSR renders application pages on the server before sending them to the client, improving initial load speed, SEO, and user experience, especially on slower devices or networks.", + "resources": [ + { + "name": "angular-universal-real-app-problems", + "type": "article", + "url": "angular-universal-real-app-problems" + }, + { + "name": "the-dark-side-of-server-side-rendering-part-1", + "type": "article", + "url": "the-dark-side-of-server-side-rendering-part-1" + }, + { + "name": "the-dark-side-of-server-side-rendering-part-2", + "type": "article", + "url": "the-dark-side-of-server-side-rendering-part-2" + }, + { + "name": "the-journey-to-isomorphic-rendering-performance", + "type": "article", + "url": "the-journey-to-isomorphic-rendering-performance" + }, + { + "name": "implementing-multi-language-angular-applications-rendered-on-a-server-ssr", + "type": "article", + "url": "implementing-multi-language-angular-applications-rendered-on-a-server-ssr" + } + ] + }, + { + "id": "accessibility", + "title": "Accessibility", + "previousNodeId": "ssr", + "description": "Accessibility ensures applications are usable by people with disabilities by following best practices like semantic HTML, ARIA roles, keyboard navigation, and screen reader support.", + "resources": [ + { + "name": "angular-a11y-11-tips-on-how-to-make-your-apps-more-accessible", + "type": "article", + "url": "angular-a11y-11-tips-on-how-to-make-your-apps-more-accessible" + }, + { + "name": "doing-a11y-easily-with-angular-cdk-keyboard-navigable-lists", + "type": "article", + "url": "doing-a11y-easily-with-angular-cdk-keyboard-navigable-lists" + }, + { + "name": "angular-for-everyone-how-to-adapt-applications-for-people-with-disabilities", + "type": "article", + "url": "angular-for-everyone-how-to-adapt-applications-for-people-with-disabilities" + } + ] + }, + { + "title": "Deployment & CI/CD", + "id": "deployment-&-ci/cd", + "previousNodeId": "accessibility", + "description": "Deployment involves packaging and delivering the app to production environments, while CI/CD automates building, testing, and deploying code changes to ensure fast, reliable updates.", + "resources": [ + { + "name": "build-your-angular-app-once-deploy-anywhere", + "type": "article", + "url": "build-your-angular-app-once-deploy-anywhere" + }, + { + "name": "effortless-angular-deployment-with-vercel", + "type": "article", + "url": "effortless-angular-deployment-with-vercel" + }, + { + "name": "craft-a-complete-gitlab-pipeline-for-angular-part-1", + "type": "article", + "url": "craft-a-complete-gitlab-pipeline-for-angular-part-1" + }, + { + "name": "craft-a-complete-gitlab-pipeline-for-angular-part-2", + "type": "article", + "url": "craft-a-complete-gitlab-pipeline-for-angular-part-2" + }, + { + "name": "the-angular-devops-series-deploying-to-firebase-with-circleci", + "type": "article", + "url": "the-angular-devops-series-deploying-to-firebase-with-circleci" + }, + { + "name": "deploy-an-angular-application-to-iis", + "type": "article", + "url": "deploy-an-angular-application-to-iis" + }, + { + "name": "how-to-deploy-a-run-time-micro-frontend-application-using-aws", + "type": "article", + "url": "how-to-deploy-a-run-time-micro-frontend-application-using-aws" + }, + { + "name": "automate-angular-application-deployment-via-aws-codepipeline", + "type": "article", + "url": "automate-angular-application-deployment-via-aws-codepipeline" + }, + { + "name": "how-to-automate-npm-package-publishing-with-azure-devops", + "type": "article", + "url": "how-to-automate-npm-package-publishing-with-azure-devops" + } + ] + }, + { + "title": "Bundling & Optimization", + "id": "bundling-&-optimization", + "previousNodeId": "deployment-&-ci/cd", + "description": "Bundling in Angular combines multiple files into fewer bundles to reduce load times, while optimization techniques like minification, tree-shaking, and ahead-of-time (AOT) compilation improve performance and reduce application size.", + "resources": [ + { + "name": "optimize-your-angular-bundle-size", + "type": "article", + "url": "optimize-your-angular-bundle-size" + }, + { + "name": "optimize-angular-bundle-size-in-4-steps", + "type": "article", + "url": "optimize-angular-bundle-size-in-4-steps" + }, + { + "name": "track-your-bundle-size-with-bundlemon", + "type": "article", + "url": "track-your-bundle-size-with-bundlemon" + }, + { + "name": "a-gentle-introduction-into-tree-shaking-in-angular-ivy", + "type": "article", + "url": "a-gentle-introduction-into-tree-shaking-in-angular-ivy" + }, + { + "name": "angular-tree-shaking-2", + "type": "article", + "url": "angular-tree-shaking-2" + }, + { + "name": "how-to-exclude-stylesheets-from-the-bundle-and-lazy-load-them-in-angular-angular-tutorials", + "type": "article", + "url": "how-to-exclude-stylesheets-from-the-bundle-and-lazy-load-them-in-angular-angular-tutorials" + }, + { + "name": "code-splitting-in-angular-or-how-to-share-components-between-lazy-modules", + "type": "article", + "url": "code-splitting-in-angular-or-how-to-share-components-between-lazy-modules" + }, + { + "name": "reduce-your-bundle-size-through-this-component-styling-technique", + "type": "article", + "url": "reduce-your-bundle-size-through-this-component-styling-technique" + } + ] + }, + { + "title": "Libraries & Packages", + "id": "libraries-&-packages", + "previousNodeId": "bundling-&-optimization", + "description": "Libraries and packages are reusable sets of code or features - often distributed via npm - that help developers add functionality, share components, or extend the framework efficiently.", + "resources": [ + { + "name": "what-makes-a-good-angular-library", + "type": "article", + "url": "what-makes-a-good-angular-library" + }, + { + "name": "the-angular-library-series-building-and-packaging", + "type": "article", + "url": "the-angular-library-series-building-and-packaging" + }, + { + "name": "the-angular-library-series-publishing", + "type": "article", + "url": "the-angular-library-series-publishing" + }, + { + "name": "creating-a-library-in-angular-6-using-angular-cli-and-ng-packagr", + "type": "article", + "url": "creating-a-library-in-angular-6-using-angular-cli-and-ng-packagr" + }, + { + "name": "complete-beginner-guide-to-publishing-an-angular-library-to-npm", + "type": "article", + "url": "complete-beginner-guide-to-publishing-an-angular-library-to-npm" + }, + { + "name": "create-your-standalone-angular-library-in-10-minutes", + "type": "article", + "url": "create-your-standalone-angular-library-in-10-minutes" + } + ] + }, + { + "title": "Micro Frontends", + "id": "micro-frontends", + "previousNodeId": "libraries-&-packages", + "description": "Micro frontends break a large Angular application into smaller, independently deployable pieces, allowing teams to develop, test, and deploy features in isolation for better scalability and maintainability", + "resources": [ + { + "name": "the-micro-frontend-chaos-and-how-to-solve-it", + "type": "article", + "url": "the-micro-frontend-chaos-and-how-to-solve-it" + }, + { + "name": "angular-micro-frontends-a-modern-approach-to-complex-app-development", + "type": "article", + "url": "angular-micro-frontends-a-modern-approach-to-complex-app-development" + }, + { + "name": "taking-micro-frontends-to-the-next-level", + "type": "article", + "url": "taking-micro-frontends-to-the-next-level" + }, + { + "name": "how-to-deploy-a-run-time-micro-frontend-application-using-aws", + "type": "article", + "url": "how-to-deploy-a-run-time-micro-frontend-application-using-aws" + } + ] + }, + { + "title": "Advanced Angular Features", + "id": "advanced-angular-features", + "previousNodeId": "micro-frontends", + "description": "Advanced Angular features include techniques like dynamic component loading or custom directives enable building more flexible and high-performance applications.", + "resources": [ + { + "name": "teleportation-in-angular", + "type": "article", + "url": "teleportation-in-angular" + }, + { + "name": "what-is-forwardref-in-angular-and-why-we-need-it", + "type": "article", + "url": "what-is-forwardref-in-angular-and-why-we-need-it" + }, + { + "name": "angular-tools-you-should-be-aware-of", + "type": "article", + "url": "angular-tools-you-should-be-aware-of" + }, + { + "name": "headless-angular-components", + "type": "article", + "url": "headless-angular-components" + }, + { + "name": "global-objects-in-angular", + "type": "article", + "url": "global-objects-in-angular" + }, + { + "name": "angular-extended-diagnostics-2", + "type": "article", + "url": "angular-extended-diagnostics-2" + }, + { + "name": "type-checking-templates-in-angular-view-engine-and-ivy", + "type": "article", + "url": "type-checking-templates-in-angular-view-engine-and-ivy" + }, + { + "name": "running-event-listeners-outside-of-the-ngzone", + "type": "article", + "url": "running-event-listeners-outside-of-the-ngzone" + }, + { + "name": "from-zone-js-to-zoneless-angular-and-back-how-it-all-works", + "type": "article", + "url": "from-zone-js-to-zoneless-angular-and-back-how-it-all-works" + }, + { + "name": "do-you-still-think-that-ngzone-zone-js-is-required-for-change-detection-in-angular", + "type": "article", + "url": "do-you-still-think-that-ngzone-zone-js-is-required-for-change-detection-in-angular" + } + ] + }, + { + "title": "Angular Versions Updates", + "id": "angular-versions-updates", + "previousNodeId": "advanced-angular-features", + "description": "Angular version updates bring new features, performance improvements, and bug fixes, while often providing migration tools and guides to help developers smoothly upgrade their applications.", + "resources": [ + { + "name": "angular-19-2-whats-new", + "type": "article", + "url": "angular-19-2-whats-new" + }, + { + "name": "angular-19-1", + "type": "article", + "url": "angular-19-1" + }, + { + "name": "angular-19-whats-new", + "type": "article", + "url": "angular-19-whats-new" + }, + { + "name": "angular-18-whats-new", + "type": "article", + "url": "angular-18-whats-new" + }, + { + "name": "angular-17-introduction-to-angular-renaissance", + "type": "article", + "url": "angular-17-introduction-to-angular-renaissance" + }, + { + "name": "angular-16-whats-new", + "type": "article", + "url": "angular-16-whats-new" + }, + { + "name": "angular-15-whats-new", + "type": "article", + "url": "angular-15-whats-new" + }, + { + "name": "angular-14-what-you-should-know", + "type": "article", + "url": "angular-14-what-you-should-know" + }, + { + "name": "angular-11-towards-the-type-safety", + "type": "article", + "url": "angular-11-towards-the-type-safety" + }, + { + "name": "whats-new-after-angular-8", + "type": "article", + "url": "whats-new-after-angular-8" + }, + { + "name": "brace-yourself-angular-8-is-coming", + "type": "article", + "url": "brace-yourself-angular-8-is-coming" + } + ] + }, + { + "title": "Data Visualization", + "id": "data-visualization", + "previousNodeId": "angular-versions-updates", + "description": "Data visualization in Angular involves integrating libraries and components to create interactive charts, graphs, and dashboards that help users understand complex data visually.", + "resources": [ + { + "name": "customization-with-ng2-charts-an-easy-way-to-visualize-data", + "type": "article", + "url": "customization-with-ng2-charts-an-easy-way-to-visualize-data" + }, + { + "name": "creating-a-sketchpad-with-angular-and-p5js", + "type": "article", + "url": "creating-a-sketchpad-with-angular-and-p5js" + }, + { + "name": "inside-ag-grid-techniques-to-make-the-fastest-javascript-datagrid-in-the-world", + "type": "article", + "url": "inside-ag-grid-techniques-to-make-the-fastest-javascript-datagrid-in-the-world" + } + ] + }, + { + "title": "Web Components", + "id": "web-components", + "previousNodeId": "internationalization", + "description": "Web Components are a set of standardized web platform APIs that allow Angular apps to create reusable, encapsulated custom elements which can be used across different frameworks and projects.", + "resources": [ + { + "name": "angular-web-components-a-complete-guide", + "type": "article", + "url": "angular-web-components-a-complete-guide" + }, + { + "name": "building-and-consuming-angular-elements-as-web-components", + "type": "article", + "url": "building-and-consuming-angular-elements-as-web-components" + }, + { + "name": "simplifying-web-components-usage-with-angular-elements", + "type": "article", + "url": "simplifying-web-components-usage-with-angular-elements" + }, + { + "name": "angular-elements-2", + "type": "article", + "url": "angular-elements-2" + } + ] + }, + { + "title": "Monorepo & Workspace", + "id": "monorepo-&-workspace", + "previousNodeId": "web-components", + "description": "A monorepo in Angular manages multiple related projects within a single repository. It provides tooling and structure to develop, build, and test these projects efficiently under one unified setup.", + "resources": [ + { + "name": "scalable-modular-angular-application-with-nx", + "type": "article", + "url": "scalable-modular-angular-application-with-nx" + }, + { + "name": "full-stack-apps-with-angular-and-nestjs-in-an-nx-monorepo", + "type": "article", + "url": "full-stack-apps-with-angular-and-nestjs-in-an-nx-monorepo" + }, + { + "name": "angular-workspace-no-application-for-you", + "type": "article", + "url": "angular-workspace-no-application-for-you" + }, + { + "name": "shell-library-patterns-with-nx-and-monorepo-architectures", + "type": "article", + "url": "shell-library-patterns-with-nx-and-monorepo-architectures" + }, + { + "name": "nx-angular-elements-case-study", + "type": "article", + "url": "nx-angular-elements-case-study" + }, + { + "name": "making-an-angular-project-mono-repo-with-ngrx-state-management-and-lazy-loading", + "type": "article", + "url": "making-an-angular-project-mono-repo-with-ngrx-state-management-and-lazy-loading" + } + ] + }, + { + "title": "Cross-Platform", + "id": "cross-platform", + "previousNodeId": "monorepo-&-workspace", + "description": "Cross-platform refers to building applications that can run seamlessly across different environments - such as web, mobile, and desktop - often using frameworks like Angular with Ionic or NativeScript.", + "resources": [ + { + "name": "angular-on-mobile-applications", + "type": "article", + "url": "angular-on-mobile-applications" + }, + { + "name": "angular-electron-2", + "type": "article", + "url": "angular-electron-2" + }, + { + "name": "angular-electron-part-2", + "type": "article", + "url": "angular-electron-part-2" + }, + { + "name": "building-web-desktop-and-mobile-apps-from-a-single-codebase-using-angular", + "type": "article", + "url": "building-web-desktop-and-mobile-apps-from-a-single-codebase-using-angular" + } + ] + }, + { + "title": "Rendering & DOM Manipulation", + "id": "rendering-&-dom-manipulation", + "previousNodeId": "cross-platform", + "description": "Understand how Angular's rendering engine works, including component lifecycle management and change detection mechanisms that efficiently update the DOM. Learn safe DOM manipulation techniques and best practices for interacting with elements while maintaining Angular's reactive data flow.", + "resources": [ + { + "name": "how-to-do-dom-manipulation-properly-in-angular", + "type": "article", + "url": "how-to-do-dom-manipulation-properly-in-angular" + }, + { + "name": "working-with-dom-in-angular-unexpected-consequences-and-optimization-techniques", + "type": "article", + "url": "working-with-dom-in-angular-unexpected-consequences-and-optimization-techniques" + }, + { + "name": "angular-platforms-in-depth-part-3-rendering-angular-applications-in-terminal", + "type": "article", + "url": "angular-platforms-in-depth-part-3-rendering-angular-applications-in-terminal" + }, + { + "name": "exploring-angular-dom-manipulation-techniques-using-viewcontainerref", + "type": "article", + "url": "exploring-angular-dom-manipulation-techniques-using-viewcontainerref" + } + ] + } +] diff --git a/libs/blog-contracts/roadmap/.eslintrc.json b/libs/blog-contracts/roadmap/.eslintrc.json new file mode 100644 index 00000000..70a4eb33 --- /dev/null +++ b/libs/blog-contracts/roadmap/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "al", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "al", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/blog-contracts/roadmap/README.md b/libs/blog-contracts/roadmap/README.md new file mode 100644 index 00000000..a908cb23 --- /dev/null +++ b/libs/blog-contracts/roadmap/README.md @@ -0,0 +1,7 @@ +# blog-contracts-roadmap + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test blog-contracts-roadmap` to execute the unit tests. diff --git a/libs/blog-contracts/roadmap/jest.config.ts b/libs/blog-contracts/roadmap/jest.config.ts new file mode 100644 index 00000000..dd224a23 --- /dev/null +++ b/libs/blog-contracts/roadmap/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'blog-contracts-roadmap', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/blog-contracts/roadmap', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/blog-contracts/roadmap/project.json b/libs/blog-contracts/roadmap/project.json new file mode 100644 index 00000000..83137d70 --- /dev/null +++ b/libs/blog-contracts/roadmap/project.json @@ -0,0 +1,20 @@ +{ + "name": "blog-contracts-roadmap", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog-contracts/roadmap/src", + "prefix": "al", + "projectType": "library", + "tags": ["scope:shared", "type:contract"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog-contracts/roadmap/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog-contracts/roadmap/src/index.ts b/libs/blog-contracts/roadmap/src/index.ts new file mode 100644 index 00000000..6d79ac2c --- /dev/null +++ b/libs/blog-contracts/roadmap/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/roadmap-node'; +export * from './lib/resource.interface'; diff --git a/libs/blog-contracts/roadmap/src/lib/angular-love-node.interface.ts b/libs/blog-contracts/roadmap/src/lib/angular-love-node.interface.ts new file mode 100644 index 00000000..1362a592 --- /dev/null +++ b/libs/blog-contracts/roadmap/src/lib/angular-love-node.interface.ts @@ -0,0 +1,12 @@ +import { BaseNodeDTO } from './base-node.interface'; +import { Creator } from './creator.interface'; + +export interface AngularLoveNodeDTO extends BaseNodeDTO { + nodeType: 'angular-love'; + additionalDescription: { + introduction: string; + toPrepareList: string[]; + ending: string; + }; + creators: Creator[]; +} diff --git a/libs/blog-contracts/roadmap/src/lib/base-node.interface.ts b/libs/blog-contracts/roadmap/src/lib/base-node.interface.ts new file mode 100644 index 00000000..548ed18e --- /dev/null +++ b/libs/blog-contracts/roadmap/src/lib/base-node.interface.ts @@ -0,0 +1,8 @@ +export interface BaseNodeDTO { + id: string; + title: string; + description: string; + nodeType: 'angular-love' | 'regular'; + previousNodeId?: string; + parentNodeId?: string; +} diff --git a/libs/blog-contracts/roadmap/src/lib/creator.interface.ts b/libs/blog-contracts/roadmap/src/lib/creator.interface.ts new file mode 100644 index 00000000..656bc8b9 --- /dev/null +++ b/libs/blog-contracts/roadmap/src/lib/creator.interface.ts @@ -0,0 +1,4 @@ +export interface Creator { + name: string; + slug: string; +} diff --git a/libs/blog-contracts/roadmap/src/lib/regular-node.type.ts b/libs/blog-contracts/roadmap/src/lib/regular-node.type.ts new file mode 100644 index 00000000..110c41f0 --- /dev/null +++ b/libs/blog-contracts/roadmap/src/lib/regular-node.type.ts @@ -0,0 +1,8 @@ +import { BaseNodeDTO } from './base-node.interface'; +import { Resource } from './resource.interface'; + +export interface RegularNodeDTO extends BaseNodeDTO { + nodeType: 'regular'; + label?: 'optional' | 'recommended' | 'comingSoon'; + resources: Resource[]; +} diff --git a/libs/blog-contracts/roadmap/src/lib/resource.interface.ts b/libs/blog-contracts/roadmap/src/lib/resource.interface.ts new file mode 100644 index 00000000..ebd897d3 --- /dev/null +++ b/libs/blog-contracts/roadmap/src/lib/resource.interface.ts @@ -0,0 +1,5 @@ +export interface Resource { + url: string; + name: string; + type: 'article' | 'video'; +} diff --git a/libs/blog-contracts/roadmap/src/lib/roadmap-node.ts b/libs/blog-contracts/roadmap/src/lib/roadmap-node.ts new file mode 100644 index 00000000..7bf95b02 --- /dev/null +++ b/libs/blog-contracts/roadmap/src/lib/roadmap-node.ts @@ -0,0 +1,4 @@ +import { AngularLoveNodeDTO } from './angular-love-node.interface'; +import { RegularNodeDTO } from './regular-node.type'; + +export type RoadmapNodeDTO = RegularNodeDTO; diff --git a/libs/blog-contracts/roadmap/src/test-setup.ts b/libs/blog-contracts/roadmap/src/test-setup.ts new file mode 100644 index 00000000..ea414013 --- /dev/null +++ b/libs/blog-contracts/roadmap/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/blog-contracts/roadmap/tsconfig.json b/libs/blog-contracts/roadmap/tsconfig.json new file mode 100644 index 00000000..fde35eab --- /dev/null +++ b/libs/blog-contracts/roadmap/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/blog-contracts/roadmap/tsconfig.lib.json b/libs/blog-contracts/roadmap/tsconfig.lib.json new file mode 100644 index 00000000..9b49be75 --- /dev/null +++ b/libs/blog-contracts/roadmap/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/libs/blog-contracts/roadmap/tsconfig.spec.json b/libs/blog-contracts/roadmap/tsconfig.spec.json new file mode 100644 index 00000000..f858ef78 --- /dev/null +++ b/libs/blog-contracts/roadmap/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/blog/layouts/ui-layouts/src/lib/layout/layout.component.html b/libs/blog/layouts/ui-layouts/src/lib/layout/layout.component.html index 11df8903..2fe2a7c5 100644 --- a/libs/blog/layouts/ui-layouts/src/lib/layout/layout.component.html +++ b/libs/blog/layouts/ui-layouts/src/lib/layout/layout.component.html @@ -1,3 +1,10 @@ -
+
diff --git a/libs/blog/layouts/ui-layouts/src/lib/layout/layout.component.ts b/libs/blog/layouts/ui-layouts/src/lib/layout/layout.component.ts index f929652c..adc4a407 100644 --- a/libs/blog/layouts/ui-layouts/src/lib/layout/layout.component.ts +++ b/libs/blog/layouts/ui-layouts/src/lib/layout/layout.component.ts @@ -1,4 +1,13 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + input, +} from '@angular/core'; + +export interface LayoutConfig { + fullLayout: boolean; +} @Component({ selector: 'al-layout', @@ -7,7 +16,25 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; styleUrls: ['./layout.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, host: { - class: 'min-h-screen grid grid-rows-[auto_1fr_auto]', + class: 'grid grid-rows-[auto_1fr_auto]', + '[class]': 'class()', }, }) -export class LayoutComponent {} +export class LayoutComponent { + readonly layoutConfig = input(); + + protected readonly class = computed(() => { + const layoutConfig = this.layoutConfig(); + if (layoutConfig?.fullLayout) { + return { + 'flex-1': true, + 'basis-0': true, + 'overflow-hidden': true, + 'min-h-full': true, + }; + } else + return { + 'min-h-screen': true, + }; + }); +} diff --git a/libs/blog/layouts/ui-navigation/src/lib/navigation/navigation.component.ts b/libs/blog/layouts/ui-navigation/src/lib/navigation/navigation.component.ts index 35b96b7b..0db2b4ad 100644 --- a/libs/blog/layouts/ui-navigation/src/lib/navigation/navigation.component.ts +++ b/libs/blog/layouts/ui-navigation/src/lib/navigation/navigation.component.ts @@ -50,6 +50,11 @@ export class NavigationComponent { link: ['become-author'], dataTestId: 'navigation-become-author', }, + { + translationPath: 'nav.roadmap', + link: ['roadmap'], + dataTestId: 'navigation-roadmap', + }, ]; protected navigated = output(); } diff --git a/libs/blog/roadmap/data-access/.eslintrc.json b/libs/blog/roadmap/data-access/.eslintrc.json new file mode 100644 index 00000000..5b79c406 --- /dev/null +++ b/libs/blog/roadmap/data-access/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "al", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "al", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/blog/roadmap/data-access/README.md b/libs/blog/roadmap/data-access/README.md new file mode 100644 index 00000000..fe4207f9 --- /dev/null +++ b/libs/blog/roadmap/data-access/README.md @@ -0,0 +1,7 @@ +# roadmap-data-access + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test roadmap-data-access` to execute the unit tests. diff --git a/libs/blog/roadmap/data-access/jest.config.ts b/libs/blog/roadmap/data-access/jest.config.ts new file mode 100644 index 00000000..1cdb2b1d --- /dev/null +++ b/libs/blog/roadmap/data-access/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'roadmap-data-access', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../../coverage/libs/blog/roadmap/data-access', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/blog/roadmap/data-access/project.json b/libs/blog/roadmap/data-access/project.json new file mode 100644 index 00000000..fc57f029 --- /dev/null +++ b/libs/blog/roadmap/data-access/project.json @@ -0,0 +1,20 @@ +{ + "name": "roadmap-data-access", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/roadmap/data-access/src", + "prefix": "al", + "projectType": "library", + "tags": ["scope:client", "type:data-access"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog/roadmap/data-access/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog/roadmap/data-access/src/index.ts b/libs/blog/roadmap/data-access/src/index.ts new file mode 100644 index 00000000..6520065d --- /dev/null +++ b/libs/blog/roadmap/data-access/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/infractructure/index'; +export * from './lib/state/index'; diff --git a/libs/blog/roadmap/data-access/src/lib/infractructure/index.ts b/libs/blog/roadmap/data-access/src/lib/infractructure/index.ts new file mode 100644 index 00000000..b53ec011 --- /dev/null +++ b/libs/blog/roadmap/data-access/src/lib/infractructure/index.ts @@ -0,0 +1 @@ +export * from './roadmap.service'; diff --git a/libs/blog/roadmap/data-access/src/lib/infractructure/roadmap.service.ts b/libs/blog/roadmap/data-access/src/lib/infractructure/roadmap.service.ts new file mode 100644 index 00000000..1841693a --- /dev/null +++ b/libs/blog/roadmap/data-access/src/lib/infractructure/roadmap.service.ts @@ -0,0 +1,15 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; + +import { RoadmapNodeDTO } from '@angular-love/blog/contracts/roadmap'; + +@Injectable({ providedIn: 'root' }) +export class RoadmapService { + private readonly _http = inject(HttpClient); + + getNodes() { + return this._http.get('assets/roadmap-tiles.json', { + responseType: 'json', + }); + } +} diff --git a/libs/blog/roadmap/data-access/src/lib/state/build-roadmap-layers-from-dto.ts b/libs/blog/roadmap/data-access/src/lib/state/build-roadmap-layers-from-dto.ts new file mode 100644 index 00000000..b2d06482 --- /dev/null +++ b/libs/blog/roadmap/data-access/src/lib/state/build-roadmap-layers-from-dto.ts @@ -0,0 +1,143 @@ +import { RoadmapNodeDTO } from '@angular-love/blog/contracts/roadmap'; +import { RoadmapLayer } from '@angular-love/blog/roadmap/ui-roadmap'; +import { + RoadmapClusterNode, + RoadmapNode, + RoadmapStandardNode, +} from '@angular-love/blog/roadmap/ui-roadmap-node'; + +export function buildRoadmapLayersFromDto( + roadmapNodesDto: RoadmapNodeDTO[] | undefined, + roadmapTitleLayer: RoadmapLayer, +): RoadmapLayer[] { + if (!roadmapNodesDto) { + return []; + } + + const allNodeDtosMap = roadmapNodesDto.reduce( + (acc, node) => ({ ...acc, [node.id]: node }), + {} as { [nodeId: string]: RoadmapNodeDTO }, + ); + const layerChildNodeIdsMap: { [parentNodeId: string]: string[] } = {}; + const clusterChildNodeIdsMap: { [clusterNodeId: string]: string[] } = {}; + const allNodesMap: { [nodeId: string]: RoadmapNode } = {}; + + // Build all the initial maps + roadmapNodesDto.forEach((nodeDto) => { + // Only primary nodes don't have a parent node id + if (!nodeDto.parentNodeId) { + allNodesMap[nodeDto.id] = createPrimaryNode(nodeDto); + + layerChildNodeIdsMap[nodeDto.id] ??= []; + return; + } + + // Node is a child of a cluster node - it's a secondary node + if (allNodeDtosMap[nodeDto.parentNodeId].parentNodeId) { + const parentClusterNodeDto = allNodeDtosMap[nodeDto.parentNodeId]; + + // Setup cluster nodes map as it's the only place where it's possible to do so + clusterChildNodeIdsMap[parentClusterNodeDto.id] ??= []; + clusterChildNodeIdsMap[parentClusterNodeDto.id].push(nodeDto.id); + + if (allNodesMap[nodeDto.parentNodeId]) { + // Promote an already stored node to a cluster node as it might have been created as a secondary node + allNodesMap[parentClusterNodeDto.id] = { + ...allNodesMap[nodeDto.parentNodeId], + nodeType: 'cluster', + clusteredNodes: [], + }; + } else { + allNodesMap[parentClusterNodeDto.id] = + createClusterNode(parentClusterNodeDto); + } + } else { + // Layer child node - either cluster or secondary + layerChildNodeIdsMap[nodeDto.parentNodeId] ??= []; + layerChildNodeIdsMap[nodeDto.parentNodeId].push(nodeDto.id); + } + + if (!allNodesMap[nodeDto.id]) { + // There is no way to tell if a node is a secondary or a cluster node, + // so we initially assume it's a secondary node + allNodesMap[nodeDto.id] = createSecondaryNode(nodeDto); + } + }); + + // Setup clusters + Object.entries(clusterChildNodeIdsMap).forEach( + ([clusterNodeId, childrenNodeIds]) => { + const clusterNode = allNodesMap[clusterNodeId] as RoadmapClusterNode; + + getOrderedNodeIdsList(childrenNodeIds, allNodeDtosMap).forEach( + (nodeId) => { + const clusterChildNode = allNodesMap[nodeId] as RoadmapStandardNode; + clusterNode.clusteredNodes.push(clusterChildNode); + }, + ); + }, + ); + + // Setup layers + const primaryNodeIds = Object.keys(layerChildNodeIdsMap); + const layers: RoadmapLayer[] = getOrderedNodeIdsList( + primaryNodeIds, + allNodeDtosMap, + ).map((primaryNodeId) => { + const parentNode = allNodesMap[primaryNodeId] as RoadmapStandardNode; + const childNodes = layerChildNodeIdsMap[primaryNodeId].map( + (childrenNodeId) => allNodesMap[childrenNodeId], + ); + return { parentNode, childNodes }; + }); + + return [roadmapTitleLayer, ...layers]; +} + +function createPrimaryNode({ + id, + title, + description, + resources, + label, +}: RoadmapNodeDTO): RoadmapStandardNode { + return { id, nodeType: 'primary', title, description, resources, label }; +} + +function createSecondaryNode({ + id, + title, + description, + resources, + label, +}: RoadmapNodeDTO): RoadmapStandardNode { + return { id, nodeType: 'secondary', title, description, resources, label }; +} + +function createClusterNode({ id, title }: RoadmapNodeDTO): RoadmapClusterNode { + return { id, nodeType: 'cluster', title, clusteredNodes: [] }; +} + +function getOrderedNodeIdsList( + nodeIds: string[], + allNodeDtosMap: { [nodeId: string]: RoadmapNodeDTO }, +): string[] { + const chainedNodeIdsMap: { + [previousNodeId: string | 'initialNode']: string; + } = {}; + + nodeIds.forEach((nodeId) => { + const nodeDto = allNodeDtosMap[nodeId]; + const previousNodeId = nodeDto.previousNodeId || 'initialNode'; + chainedNodeIdsMap[previousNodeId] = nodeId; + }); + + let nextNodeId = chainedNodeIdsMap['initialNode']; + const orderedNodeIds: string[] = []; + while (nextNodeId) { + orderedNodeIds.push(nextNodeId); + nextNodeId = chainedNodeIdsMap[nextNodeId]; + } + + return orderedNodeIds; +} diff --git a/libs/blog/roadmap/data-access/src/lib/state/index.ts b/libs/blog/roadmap/data-access/src/lib/state/index.ts new file mode 100644 index 00000000..1bfa6505 --- /dev/null +++ b/libs/blog/roadmap/data-access/src/lib/state/index.ts @@ -0,0 +1 @@ +export * from './roadmap-store'; diff --git a/libs/blog/roadmap/data-access/src/lib/state/roadmap-store.ts b/libs/blog/roadmap/data-access/src/lib/state/roadmap-store.ts new file mode 100644 index 00000000..0565942f --- /dev/null +++ b/libs/blog/roadmap/data-access/src/lib/state/roadmap-store.ts @@ -0,0 +1,98 @@ +import { computed, inject } from '@angular/core'; +import { tapResponse } from '@ngrx/operators'; +import { + patchState, + signalStore, + withComputed, + withMethods, + withState, +} from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { pipe, switchMap, tap } from 'rxjs'; + +import { RoadmapNodeDTO } from '@angular-love/blog/contracts/roadmap'; +import { RoadmapLayer } from '@angular-love/blog/roadmap/ui-roadmap'; + +import { RoadmapService } from '../infractructure'; + +import { buildRoadmapLayersFromDto } from './build-roadmap-layers-from-dto'; + +const roadmapTitleLayer: RoadmapLayer = { + parentNode: { + id: 'angular-love', + title: 'Angular.Love Roadmap Introduction', + nodeType: 'angular-love', + description: + 'The Angular Roadmap on Angular.Love is your go-to guide for learning Angular the right way. It breaks down key concepts, best practices, and useful resources in a clear, structured way. Plus, it’s packed with valuable materials and constantly updated to keep up with the latest trends.', + additionalDescription: { + introduction: + "Before diving into Angular, it's essential to have a solid understanding of the following concepts:", + toPrepareList: [ + 'HTML & CSS – Structure and styling of web pages, including Flexbox, Grid, and responsiveness.', + 'JavaScript (ES6+) – Core concepts like variables, functions, promises, and async/await.', + 'TypeScript – A typed version of JavaScript with interfaces, generics, and decorators.', + 'Node.js & npm – Running JavaScript outside the browser and managing packages.', + 'Git – Handling code versions, branches, and teamwork.', + 'APIs & HTTP – Making requests and working with JSON data.', + ], + ending: + 'Having a grasp of these topics will make your Angular learning journey much smoother and more effective.', + }, + creators: [ + { name: 'Miłosz Rutkowski', slug: 'milosz-rutkowski' }, + { name: 'Damian Brzeziński', slug: 'damian-brzezinski' }, + { name: 'Łukasz Myszkowski', slug: 'lukaszm' }, + { name: 'Dominik Kalinowski', slug: 'dominik-kalinowski' }, + { name: 'Dawid Gruszczyński', slug: 'dawid-gruszczynski' }, + ], + }, + childNodes: [], +}; + +type RoadmapState = { + loading: 'success' | 'error' | 'init' | 'loading'; + nodesDto: RoadmapNodeDTO[]; +}; + +const initialState: RoadmapState = { + loading: 'init', + nodesDto: [], +}; + +export const RoadmapStore = signalStore( + withState(initialState), + withComputed((store) => ({ + roadmapLayers: computed(() => + buildRoadmapLayersFromDto(store.nodesDto() ?? [], roadmapTitleLayer), + ), + })), + withMethods((store, roadmapService = inject(RoadmapService)) => ({ + resetToInit: () => { + patchState(store, { loading: 'init' }); + }, + getNodes: rxMethod( + pipe( + tap(() => { + patchState(store, { + loading: 'loading', + }); + }), + switchMap(() => + roadmapService.getNodes().pipe( + tapResponse({ + next: (nodesDto) => { + patchState(store, { + loading: 'success', + nodesDto, + }); + }, + error: () => { + patchState(store, { loading: 'error' }); + }, + }), + ), + ), + ), + ), + })), +); diff --git a/libs/blog/roadmap/data-access/src/test-setup.ts b/libs/blog/roadmap/data-access/src/test-setup.ts new file mode 100644 index 00000000..ea414013 --- /dev/null +++ b/libs/blog/roadmap/data-access/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/blog/roadmap/data-access/tsconfig.json b/libs/blog/roadmap/data-access/tsconfig.json new file mode 100644 index 00000000..52a0866e --- /dev/null +++ b/libs/blog/roadmap/data-access/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/blog/roadmap/data-access/tsconfig.lib.json b/libs/blog/roadmap/data-access/tsconfig.lib.json new file mode 100644 index 00000000..91273870 --- /dev/null +++ b/libs/blog/roadmap/data-access/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/libs/blog/roadmap/data-access/tsconfig.spec.json b/libs/blog/roadmap/data-access/tsconfig.spec.json new file mode 100644 index 00000000..6e5925e5 --- /dev/null +++ b/libs/blog/roadmap/data-access/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/blog/roadmap/feature-roadmap/.eslintrc.json b/libs/blog/roadmap/feature-roadmap/.eslintrc.json new file mode 100644 index 00000000..5b79c406 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "al", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "al", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/blog/roadmap/feature-roadmap/README.md b/libs/blog/roadmap/feature-roadmap/README.md new file mode 100644 index 00000000..a4c80cff --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/README.md @@ -0,0 +1,7 @@ +# blog-roadmap-feature-roadmap + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test blog-roadmap-feature-roadmap` to execute the unit tests. diff --git a/libs/blog/roadmap/feature-roadmap/jest.config.ts b/libs/blog/roadmap/feature-roadmap/jest.config.ts new file mode 100644 index 00000000..1c2ece76 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'blog-roadmap-feature-roadmap', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../../coverage/libs/blog/roadmap/feature-roadmap', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/blog/roadmap/feature-roadmap/project.json b/libs/blog/roadmap/feature-roadmap/project.json new file mode 100644 index 00000000..875f5d9e --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/project.json @@ -0,0 +1,20 @@ +{ + "name": "blog-roadmap-feature-roadmap", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/roadmap/feature-roadmap/src", + "prefix": "al", + "projectType": "library", + "tags": ["scope:client", "type:feature"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog/roadmap/feature-roadmap/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog/roadmap/feature-roadmap/src/index.ts b/libs/blog/roadmap/feature-roadmap/src/index.ts new file mode 100644 index 00000000..c25654c4 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/src/index.ts @@ -0,0 +1 @@ +export * from './lib/feature-roadmap.component'; diff --git a/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html b/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html new file mode 100644 index 00000000..2b468a83 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html @@ -0,0 +1,18 @@ +
+ +
+ @if (isPlatformBrowser) { + @for ( + layer of roadmapLayers(); + track layer.parentNode.id; + let last = $last + ) { + + } + } +
+
+ diff --git a/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss b/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss new file mode 100644 index 00000000..70b2f448 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss @@ -0,0 +1,14 @@ +:host { + animation: fadeIn 1s ease-in-out; + display: block; + position: relative; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts b/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts new file mode 100644 index 00000000..e0b2f2a8 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts @@ -0,0 +1,206 @@ +import { isPlatformBrowser } from '@angular/common'; +import { + afterRenderEffect, + ChangeDetectionStrategy, + Component, + ElementRef, + inject, + input, + PLATFORM_ID, + signal, + ViewChild, +} from '@angular/core'; +import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; +import { ActivatedRoute } from '@angular/router'; +import panzoom, { PanZoom, PanZoomOptions } from 'panzoom'; +import { map, tap } from 'rxjs'; + +import { + EventType, + RoadmapLayerComponent, + RoadmapPanControlsComponent, +} from '@angular-love/blog/roadmap/ui-roadmap'; +import { RoadmapBottomSheetNotifierService } from '@angular-love/blog/roadmap/ui-roadmap-node'; +import { RoadmapStore } from '@angular-love/roadmap-data-access'; + +import { RoadmapBottomsheetManagerService } from './roadmap-bottomsheet-menager.service'; + +const panZoomInitialConfig: PanZoomOptions = { + maxZoom: 2, + minZoom: 0.5, + zoomSpeed: 0.1, + initialZoom: 1, + smoothScroll: true, +}; + +@Component({ + selector: 'al-feature-roadmap', + imports: [RoadmapLayerComponent, RoadmapPanControlsComponent], + templateUrl: './feature-roadmap.component.html', + styleUrl: './feature-roadmap.component.scss', + host: { + class: 'block max-h-full overflow-hidden w-full relative', + }, + changeDetection: ChangeDetectionStrategy.OnPush, + providers: [RoadmapStore], +}) +export class FeatureRoadmapComponent { + @ViewChild('roadmapWrapper', { static: true }) + roadmapWrapper!: ElementRef; + + private readonly _roadmapStore = inject(RoadmapStore); + private readonly _platform = inject(PLATFORM_ID); + private readonly _panZoomInitialConfig = panZoomInitialConfig; + private readonly _scaleMultiplier = 0.5; + private readonly _route = inject(ActivatedRoute); + private readonly elementRef = inject>( + ElementRef, + ); + private readonly _roadmapBottomsheetManagerService = inject( + RoadmapBottomsheetManagerService, + ); + private readonly _roadmapBottomSheetNotifierService = inject( + RoadmapBottomSheetNotifierService, + ); + + private readonly _selectedNodeId = toSignal( + this._route.queryParams.pipe(map((params) => params['nodeId'])), + { initialValue: undefined }, + ); + + private _panZoomInstance = signal(undefined); + + private readonly _nodesDto = this._roadmapStore.nodesDto; + protected readonly roadmapLayers = this._roadmapStore.roadmapLayers; + protected readonly isPlatformBrowser = isPlatformBrowser(this._platform); + + language = input.required(); + + constructor() { + this._roadmapStore.getNodes(); + + this._roadmapBottomSheetNotifierService.nodeAsObservable + .pipe( + tap((node) => { + this.focusSelectedNode(node.id); + this._roadmapBottomsheetManagerService.open(node); + }), + takeUntilDestroyed(), + ) + .subscribe(); + afterRenderEffect(async () => { + if (!isPlatformBrowser(this._platform)) return; + if (this._nodesDto()?.length) { + this.initPanZoom(); + } + }); + + afterRenderEffect(() => { + if (!isPlatformBrowser(this._platform)) return; + + const selectedNodeId = this._selectedNodeId(); + if (selectedNodeId) this.clickSelectedNode(selectedNodeId); + }); + } + + clickSelectedNode(selectedNodeId: string) { + const selectedNode = this.elementRef.nativeElement.querySelector( + `[node-id="${selectedNodeId}"]`, + ) as HTMLElement | null; + + selectedNode?.dispatchEvent(new PointerEvent('pointerup')); + } + + resizeRoadmap(event: EventType): void { + const panZoomInstance = this._panZoomInstance(); + if (!panZoomInstance) return; + const transform = panZoomInstance.getTransform(); + const centerX = this.elementRef.nativeElement.clientWidth / 2; + const centerY = this.elementRef.nativeElement.clientHeight / 2; + + const currentScale = transform.scale; + let targetScale = currentScale; + + if (event === 'increment') { + targetScale = Math.min( + currentScale + this._scaleMultiplier, + this._panZoomInitialConfig.maxZoom ?? 2, + ); + } else if (event === 'decrement') { + targetScale = Math.max( + currentScale - this._scaleMultiplier, + this._panZoomInitialConfig.minZoom ?? 0.5, + ); + } else if (event === 'reset') { + panZoomInstance.moveTo(0, 0); + panZoomInstance.zoomAbs(0, 0, 1); + return; + } else if (event === 'zoom-reset') { + panZoomInstance.zoomAbs(centerX, centerY, 1); + return; + } + + const zoomFactor = targetScale / currentScale; + + panZoomInstance.smoothZoom(centerX, centerY, zoomFactor); + } + + private focusSelectedNode(nodeId: string): void { + const panZoomInstance = this._panZoomInstance(); + if (!panZoomInstance) return; + + const selectedNode = this.elementRef.nativeElement.querySelector( + `[node-id="${nodeId}"]`, + ) as HTMLElement | null; + + if (!selectedNode) return; + const nodeRect = selectedNode.getBoundingClientRect(); + const containerRect = this.elementRef.nativeElement.getBoundingClientRect(); + + const nodeCenterX = nodeRect.left + nodeRect.width / 2; + const nodeCenterY = nodeRect.top + nodeRect.height / 2; + + const containerCenterX = containerRect.left + containerRect.width / 2; + const containerCenterY = containerRect.top + containerRect.height / 2; + + const deltaX = nodeCenterX - containerCenterX; + const deltaY = nodeCenterY - containerCenterY; + + const transform = panZoomInstance.getTransform(); + + const targetX = transform.x - deltaX; + const targetY = transform.y - deltaY; + + panZoomInstance.smoothMoveTo(targetX, targetY); + } + + private initPanZoom() { + const roadmapWrapper = this.roadmapWrapper.nativeElement; + this._panZoomInstance.set( + panzoom(roadmapWrapper, this._panZoomInitialConfig), + ); + + const controlButtons = document.querySelectorAll( + 'al-roadmap-pan-controls button', + ); + + controlButtons.forEach((btn) => { + btn.addEventListener('pointerdown', () => { + this._panZoomInstance()?.pause(); + }); + + btn.addEventListener('click', () => { + setTimeout(() => this._panZoomInstance()?.resume(), 0); + }); + + btn.addEventListener( + 'wheel', + (e) => { + e.preventDefault(); + e.stopPropagation(); + }, + { passive: false }, + ); + }); + } +} diff --git a/libs/blog/roadmap/feature-roadmap/src/lib/roadmap-bottomsheet-menager.service.ts b/libs/blog/roadmap/feature-roadmap/src/lib/roadmap-bottomsheet-menager.service.ts new file mode 100644 index 00000000..59affcbd --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/src/lib/roadmap-bottomsheet-menager.service.ts @@ -0,0 +1,17 @@ +import { Dialog, DialogRef } from '@angular/cdk/dialog'; +import { inject, Injectable } from '@angular/core'; + +import { RoadmapStandardNode } from '@angular-love/blog/roadmap/ui-roadmap-node'; +import { RoadmapBottomsheetComponent } from '@angular-love/ui-roadmap-bottomsheet'; + +@Injectable({ providedIn: 'root' }) +export class RoadmapBottomsheetManagerService { + private dialog = inject(Dialog); + + open(node: RoadmapStandardNode) { + this.dialog.open(RoadmapBottomsheetComponent, { + data: node, + disableClose: false, + }); + } +} diff --git a/libs/blog/roadmap/feature-roadmap/src/test-setup.ts b/libs/blog/roadmap/feature-roadmap/src/test-setup.ts new file mode 100644 index 00000000..ea414013 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/blog/roadmap/feature-roadmap/tsconfig.json b/libs/blog/roadmap/feature-roadmap/tsconfig.json new file mode 100644 index 00000000..52a0866e --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/blog/roadmap/feature-roadmap/tsconfig.lib.json b/libs/blog/roadmap/feature-roadmap/tsconfig.lib.json new file mode 100644 index 00000000..91273870 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/libs/blog/roadmap/feature-roadmap/tsconfig.spec.json b/libs/blog/roadmap/feature-roadmap/tsconfig.spec.json new file mode 100644 index 00000000..6e5925e5 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/.eslintrc.json b/libs/blog/roadmap/ui-roadmap-bottomsheet/.eslintrc.json new file mode 100644 index 00000000..5b79c406 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "al", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "al", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/README.md b/libs/blog/roadmap/ui-roadmap-bottomsheet/README.md new file mode 100644 index 00000000..0790fa40 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/README.md @@ -0,0 +1,7 @@ +# ui-roadmap-bottomsheet + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ui-roadmap-bottomsheet` to execute the unit tests. diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/jest.config.ts b/libs/blog/roadmap/ui-roadmap-bottomsheet/jest.config.ts new file mode 100644 index 00000000..433de88f --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/jest.config.ts @@ -0,0 +1,22 @@ +export default { + displayName: 'ui-roadmap-bottomsheet', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: + '../../../../coverage/libs/blog/roadmap/ui-roadmap-bottomsheet', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/project.json b/libs/blog/roadmap/ui-roadmap-bottomsheet/project.json new file mode 100644 index 00000000..ffce6331 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/project.json @@ -0,0 +1,20 @@ +{ + "name": "ui-roadmap-bottomsheet", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/roadmap/ui-roadmap-bottomsheet/src", + "prefix": "al", + "projectType": "library", + "tags": ["scope:client", "type:ui"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog/roadmap/ui-roadmap-bottomsheet/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/index.ts b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/index.ts new file mode 100644 index 00000000..5e8daf45 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/index.ts @@ -0,0 +1 @@ +export * from './lib/roadmap-bottomsheet.component'; diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-additional-description/roadmap-bottomsheet-additional-description.component.ts b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-additional-description/roadmap-bottomsheet-additional-description.component.ts new file mode 100644 index 00000000..97c85a65 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-additional-description/roadmap-bottomsheet-additional-description.component.ts @@ -0,0 +1,35 @@ +import { Component, input } from '@angular/core'; + +import { AdditionalDescription } from '@angular-love/blog/roadmap/ui-roadmap-node'; + +import { RoadmapBottomsheetSubtitleComponent } from '../roadmap-bottomsheet-subtitle/roadmap-bottomsheet-subtitle.component'; + +@Component({ + imports: [RoadmapBottomsheetSubtitleComponent], + selector: 'al-roadmap-bottomsheet-additional-description', + template: ` +
+ +
+

+ {{ additionalDescription().introduction }} +

+
    + @for ( + listElement of additionalDescription().toPrepareList; + track $index + ) { +
  • {{ listElement }}
  • + } +
+

+ {{ additionalDescription().ending }} +

+
+
+ `, +}) +export class RoadmapBottomsheetAdditionalDescriptionComponent { + title = input.required(); + additionalDescription = input.required(); +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-creators/roadmap-bottomsheet-creators.component.ts b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-creators/roadmap-bottomsheet-creators.component.ts new file mode 100644 index 00000000..fbb08135 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-creators/roadmap-bottomsheet-creators.component.ts @@ -0,0 +1,28 @@ +import { Component, input } from '@angular/core'; + +import { Creator } from '@angular-love/blog/roadmap/ui-roadmap-node'; + +import { RoadmapBottomsheetSubtitleComponent } from '../roadmap-bottomsheet-subtitle/roadmap-bottomsheet-subtitle.component'; + +@Component({ + imports: [RoadmapBottomsheetSubtitleComponent], + selector: 'al-roadmap-bottomsheet-creators', + template: ` +
+ + +
+ `, +}) +export class RoadmapBottomsheetCreatorsComponent { + title = input.required(); + creators = input.required(); +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-description/roadmap-bottomsheet-description.component.ts b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-description/roadmap-bottomsheet-description.component.ts new file mode 100644 index 00000000..a292ff70 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-description/roadmap-bottomsheet-description.component.ts @@ -0,0 +1,20 @@ +import { Component, input } from '@angular/core'; + +import { RoadmapBottomsheetSubtitleComponent } from '../roadmap-bottomsheet-subtitle/roadmap-bottomsheet-subtitle.component'; + +@Component({ + imports: [RoadmapBottomsheetSubtitleComponent], + selector: 'al-roadmap-bottomsheet-description', + template: ` +
+ +
+

{{ description() }}

+
+
+ `, +}) +export class RoadmapBottomsheetDescriptionComponent { + title = input.required(); + description = input.required(); +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-footer/roadmap-bottomsheet-footer.component.ts b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-footer/roadmap-bottomsheet-footer.component.ts new file mode 100644 index 00000000..45971e89 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-footer/roadmap-bottomsheet-footer.component.ts @@ -0,0 +1,67 @@ +import { Component, computed, input } from '@angular/core'; +import { FastSvgComponent } from '@push-based/ngx-fast-svg'; + +import { IconType } from '@angular-love/blog/shared/ui-icon'; + +import { RoadmapBottomsheetSubtitleComponent } from '../roadmap-bottomsheet-subtitle/roadmap-bottomsheet-subtitle.component'; + +type ShareItem = { + href: string; + icon: IconType; + ariaLabel: string; +}; +@Component({ + imports: [RoadmapBottomsheetSubtitleComponent, FastSvgComponent], + selector: 'al-roadmap-bottomsheet-footer', + template: ` +
+ +
+ @for (item of items(); track item.href) { + + + + } +
+
+ `, +}) +export class RoadmapBottomsheetFooterComponent { + readonly title = input.required(); + readonly nodeId = input.required(); + readonly language = input.required(); + + readonly nodeUrl = computed(() => + this.language() === 'pl_PL' + ? `https://angular.love/pl/roadmap?nodeId=${this.nodeId()}` + : `https://angular.love/roadmap?nodeId=${this.nodeId()}`, + ); + + readonly items = computed(() => { + const url = this.nodeUrl(); + const text = encodeURIComponent(this.title()); + + return [ + { + icon: 'twitter-x', + href: `https://twitter.com/intent/tweet?text=${text}&url=${url}&hashtags=angularlove`, + ariaLabel: 'articleShareIcons.twitterAriaLabel', + }, + { + icon: 'linkedIn', + href: `https://www.linkedin.com/shareArticle?mini=true&url=${url}`, + ariaLabel: 'articleShareIcons.linkedInAriaLabel', + }, + { + icon: 'facebook', + href: `https://www.facebook.com/sharer/sharer.php?u=${url}`, + ariaLabel: 'articleShareIcons.facebookAriaLabel', + }, + ]; + }); +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-header/roadmap-bottomsheet-header.component.html b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-header/roadmap-bottomsheet-header.component.html new file mode 100644 index 00000000..62a21bcf --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-header/roadmap-bottomsheet-header.component.html @@ -0,0 +1,10 @@ +
+

{{ title() | titlecase }}

+
diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-header/roadmap-bottomsheet-header.component.scss b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-header/roadmap-bottomsheet-header.component.scss new file mode 100644 index 00000000..714391af --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-header/roadmap-bottomsheet-header.component.scss @@ -0,0 +1,4 @@ +:host { + --secondary-color: #66002b; + --gradient-color: #481cab; +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-header/roadmap-bottomsheet-header.component.ts b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-header/roadmap-bottomsheet-header.component.ts new file mode 100644 index 00000000..6fb00978 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-header/roadmap-bottomsheet-header.component.ts @@ -0,0 +1,16 @@ +import { NgClass, TitleCasePipe } from '@angular/common'; +import { Component, input } from '@angular/core'; + +import { RoadmapNodeDTO } from '@angular-love/blog/contracts/roadmap'; +import { RoadmapStandardNode } from '@angular-love/blog/roadmap/ui-roadmap-node'; + +@Component({ + selector: 'al-roadmap-bottomsheet-header', + imports: [TitleCasePipe, NgClass], + templateUrl: './roadmap-bottomsheet-header.component.html', + styleUrl: 'roadmap-bottomsheet-header.component.scss', +}) +export class RoadmapBottomsheetHeaderComponent { + nodeType = input.required(); + title = input.required(); +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-regular-content/roadmap-bottomsheet-regular-content.component.ts b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-regular-content/roadmap-bottomsheet-regular-content.component.ts new file mode 100644 index 00000000..801719ec --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-regular-content/roadmap-bottomsheet-regular-content.component.ts @@ -0,0 +1,28 @@ +import { Component, input } from '@angular/core'; + +import { Resource } from '@angular-love/blog/contracts/roadmap'; + +import { RoadmapBottomsheetSubtitleComponent } from '../roadmap-bottomsheet-subtitle/roadmap-bottomsheet-subtitle.component'; + +@Component({ + imports: [RoadmapBottomsheetSubtitleComponent], + selector: 'al-roadmap-bottomsheet-regular-content', + template: ` +
+ + +
+ `, +}) +export class RoadmapBottomsheetRegularContentComponent { + title = input.required(); + resources = input.required(); +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-subtitle/roadmap-bottomsheet-subtitle.component.ts b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-subtitle/roadmap-bottomsheet-subtitle.component.ts new file mode 100644 index 00000000..9a652587 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet-subtitle/roadmap-bottomsheet-subtitle.component.ts @@ -0,0 +1,15 @@ +import { Component, input } from '@angular/core'; + +@Component({ + selector: 'al-roadmap-bottomsheet-subtitle', + template: ` +

{{ title() }}

+
+ `, + host: { + class: 'flex items-center gap-4', + }, +}) +export class RoadmapBottomsheetSubtitleComponent { + title = input.required(); +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet.component.html b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet.component.html new file mode 100644 index 00000000..8e4b7e5b --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet.component.html @@ -0,0 +1,56 @@ +
+
+ + + + @if (angularLoveNodeDetails(); as angularLoveNodeDetails) { + + + } + + @if (regularNodeDetails(); as regularNodeDetails) { + @if (!regularNodeDetails.resources.length) { +
+ +
+ } @else { + + + } + } + +
+
diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet.component.ts b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet.component.ts new file mode 100644 index 00000000..9476b0bc --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/lib/roadmap-bottomsheet.component.ts @@ -0,0 +1,122 @@ +import { DIALOG_DATA, DialogRef } from '@angular/cdk/dialog'; +import { + Component, + computed, + effect, + ElementRef, + inject, + signal, + viewChild, +} from '@angular/core'; +import { Router } from '@angular/router'; + +import { + AngularLoveNode, + RoadmapRegularNode, + RoadmapStandardNode, +} from '@angular-love/blog/roadmap/ui-roadmap-node'; +import { ButtonComponent } from '@angular-love/blog/shared/ui-button'; + +import { RoadmapBottomsheetAdditionalDescriptionComponent } from './roadmap-bottomsheet-additional-description/roadmap-bottomsheet-additional-description.component'; +import { RoadmapBottomsheetCreatorsComponent } from './roadmap-bottomsheet-creators/roadmap-bottomsheet-creators.component'; +import { RoadmapBottomsheetDescriptionComponent } from './roadmap-bottomsheet-description/roadmap-bottomsheet-description.component'; +import { RoadmapBottomsheetFooterComponent } from './roadmap-bottomsheet-footer/roadmap-bottomsheet-footer.component'; +import { RoadmapBottomsheetHeaderComponent } from './roadmap-bottomsheet-header/roadmap-bottomsheet-header.component'; +import { RoadmapBottomsheetRegularContentComponent } from './roadmap-bottomsheet-regular-content/roadmap-bottomsheet-regular-content.component'; + +function isAngularNode(node: RoadmapStandardNode): node is AngularLoveNode { + return node.nodeType === 'angular-love'; +} + +function isRegularNode(node: RoadmapStandardNode): node is RoadmapRegularNode { + return node.nodeType !== 'angular-love'; +} + +@Component({ + imports: [ + RoadmapBottomsheetHeaderComponent, + RoadmapBottomsheetDescriptionComponent, + RoadmapBottomsheetRegularContentComponent, + RoadmapBottomsheetCreatorsComponent, + ButtonComponent, + RoadmapBottomsheetFooterComponent, + RoadmapBottomsheetAdditionalDescriptionComponent, + ], + selector: 'al-roadmap-bottomsheet', + templateUrl: './roadmap-bottomsheet.component.html', + styles: ` + .bottomsheet { + animation: fadeInBottom 0.4s ease-out forwards; + } + + @keyframes fadeInBottom { + from { + opacity: 0; + transform: translateY(100%); + } + to { + opacity: 1; + transform: translateY(0); + } + } + `, +}) +export class RoadmapBottomsheetComponent { + private _router = inject(Router); + private _dialogRef = inject(DialogRef); + + protected nodeDetails = inject(DIALOG_DATA); + protected readonly bottomSheetRef = viewChild('bottomsheet', { + read: ElementRef, + }); + + language = signal(''); + + protected readonly angularLoveNodeDetails = computed< + AngularLoveNode | undefined + >(() => { + return isAngularNode(this.nodeDetails) ? this.nodeDetails : undefined; + }); + + protected readonly regularNodeDetails = computed< + RoadmapRegularNode | undefined + >(() => { + return isRegularNode(this.nodeDetails) ? this.nodeDetails : undefined; + }); + + protected readonly regularNodeArticles = computed(() => { + const regularNodeDetails = this.regularNodeDetails(); + return ( + regularNodeDetails?.resources.filter( + (resource) => resource.type === 'article', + ) ?? [] + ); + }); + + protected readonly regularNodeVideos = computed(() => { + const regularNodeDetails = this.regularNodeDetails(); + return ( + regularNodeDetails?.resources.filter( + (resource) => resource.type === 'video', + ) ?? [] + ); + }); + + onClose() { + this._dialogRef.close(); + } + + navigateToAuthor() { + this._router.navigate(['/become-author']); + this._dialogRef.close(); + } + + constructor() { + effect(() => { + const el = this.bottomSheetRef(); + if (el) { + queueMicrotask(() => el.nativeElement.scrollTo({ top: 0 })); + } + }); + } +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/src/test-setup.ts b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/test-setup.ts new file mode 100644 index 00000000..ea414013 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/tsconfig.json b/libs/blog/roadmap/ui-roadmap-bottomsheet/tsconfig.json new file mode 100644 index 00000000..52a0866e --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/tsconfig.lib.json b/libs/blog/roadmap/ui-roadmap-bottomsheet/tsconfig.lib.json new file mode 100644 index 00000000..91273870 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/libs/blog/roadmap/ui-roadmap-bottomsheet/tsconfig.spec.json b/libs/blog/roadmap/ui-roadmap-bottomsheet/tsconfig.spec.json new file mode 100644 index 00000000..6e5925e5 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-bottomsheet/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/blog/roadmap/ui-roadmap-node/.eslintrc.json b/libs/blog/roadmap/ui-roadmap-node/.eslintrc.json new file mode 100644 index 00000000..5b79c406 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": ["../../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": [ + "plugin:@nx/angular", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "al", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "al", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/libs/blog/roadmap/ui-roadmap-node/README.md b/libs/blog/roadmap/ui-roadmap-node/README.md new file mode 100644 index 00000000..4cbd5465 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/README.md @@ -0,0 +1,7 @@ +# blog-roadmap-ui-roadmap-node + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test blog-roadmap-ui-roadmap-node` to execute the unit tests. diff --git a/libs/blog/roadmap/ui-roadmap-node/jest.config.ts b/libs/blog/roadmap/ui-roadmap-node/jest.config.ts new file mode 100644 index 00000000..0725b77a --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'blog-roadmap-ui-roadmap-node', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../../coverage/libs/blog/roadmap/ui-roadmap-node', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/blog/roadmap/ui-roadmap-node/project.json b/libs/blog/roadmap/ui-roadmap-node/project.json new file mode 100644 index 00000000..50fb97f0 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/project.json @@ -0,0 +1,20 @@ +{ + "name": "blog-roadmap-ui-roadmap-node", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/roadmap/ui-roadmap-node/src", + "prefix": "al", + "projectType": "library", + "tags": ["scope:client", "type:ui"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog/roadmap/ui-roadmap-node/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog/roadmap/ui-roadmap-node/src/index.ts b/libs/blog/roadmap/ui-roadmap-node/src/index.ts new file mode 100644 index 00000000..5feb11fb --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/index.ts @@ -0,0 +1,4 @@ +export * from './lib/types/roadmap-node'; +export * from './lib/services/roadmap-bottomsheet-notifier.service'; +export * from './lib/components/roadmap-cluster/roadmap-cluster.component'; +export * from './lib/components/roadmap-basic-node/roadmap-basic-node.component'; diff --git a/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-basic-node/roadmap-basic-node.component.scss b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-basic-node/roadmap-basic-node.component.scss new file mode 100644 index 00000000..b210995a --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-basic-node/roadmap-basic-node.component.scss @@ -0,0 +1,18 @@ +@use '../../style/roadmap-hover-border-gradient' as r; + +.node { + @include r.roadmap-hover-border-gradient; +} + +.label:hover + .node, +.node:hover { + @include r.roadmap-hover-border-gradient-hover; +} + +:host { + --primary-color: #b3004a; + --secondary-color: #66002b; + --gradient-color: #481cab; + --on-hover-border-1: #923cff; + --on-hover-border-2: #ff006a; +} diff --git a/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-basic-node/roadmap-basic-node.component.ts b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-basic-node/roadmap-basic-node.component.ts new file mode 100644 index 00000000..d2dfdbc3 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-basic-node/roadmap-basic-node.component.ts @@ -0,0 +1,91 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + input, + signal, +} from '@angular/core'; + +import { RoadmapBottomSheetNotifierService } from '../../services/roadmap-bottomsheet-notifier.service'; +import { RoadmapStandardNode } from '../../types/roadmap-node'; +import { RoadmapNodeLabelComponent } from '../roadmap-node-label/roadmap-node-label.component'; + +@Component({ + selector: 'al-roadmap-basic-node', + template: ` + @if (node().label; as label) { + + } + +
+
+ {{ node().title }} +
+
+ `, + styleUrl: 'roadmap-basic-node.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [RoadmapNodeLabelComponent], + host: { + class: 'relative', + }, +}) +export class RoadmapBasicNodeComponent { + private readonly difference = 5; + protected readonly _roadmapBottomSheetNotifierService = inject( + RoadmapBottomSheetNotifierService, + ); + protected readonly pointerDown = signal<{ x: number; y: number }>({ + x: 0, + y: 0, + }); + + readonly node = input.required(); + readonly variant = input.required<'primary' | 'secondary' | 'angular-love'>(); + + protected readonly labelClass = computed(() => { + const variant = this.variant(); + if (variant === 'primary') { + return `translate-x-1.5x`; + } else return 'translate-x-full'; + }); + + protected readonly tileClass = computed(() => { + const variant = this.variant(); + + switch (variant) { + case 'primary': + return 'm-[4px] bg-[--primary-color] text-[24px]'; + case 'secondary': + return 'm-[2px] bg-[--secondary-color] text-[20px]'; + case 'angular-love': + return 'm-[4px] bg-gradient-to-r from-[--secondary-color] to-[--gradient-color] text-[24px]'; + default: + return ''; + } + }); + + onPointerDown(event: PointerEvent) { + this.pointerDown.set({ x: event.clientX, y: event.clientY }); + } + + onPointerUp(event: PointerEvent) { + const dx = Math.abs(event.clientX - this.pointerDown().x); + const dy = Math.abs(event.clientY - this.pointerDown().y); + + console.log(dx, dy); + if (dx < this.difference && dy < this.difference) { + this._roadmapBottomSheetNotifierService.openBottomSheet(this.node()); + } + } +} diff --git a/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.scss b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.scss new file mode 100644 index 00000000..0113819c --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.scss @@ -0,0 +1,9 @@ +@use '../../style/roadmap-hover-border-gradient'; + +:host { + --primary-color: #b3004a; + --secondary-color: #66002b; + --gradient-color: #481cab; + --on-hover-border-1: #923cff; + --on-hover-border-2: #ff006a; +} diff --git a/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.ts b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.ts new file mode 100644 index 00000000..02606154 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.ts @@ -0,0 +1,52 @@ +import { + ChangeDetectionStrategy, + Component, + inject, + input, + output, +} from '@angular/core'; + +import { RoadmapBottomSheetNotifierService } from '../../services/roadmap-bottomsheet-notifier.service'; +import { RoadmapClusterNode } from '../../types/roadmap-node'; + +@Component({ + selector: 'al-roadmap-cluster', + template: ` +
+
{{ cluster().title }}
+
+ +
+ @for (clusterNode of cluster().clusteredNodes; track clusterNode.id) { +
+
+
{{ clusterNode.title }}
+
+
+ } +
+ `, + host: { + class: + 'block bg-gradient-to-br from-[#100F15] to-[#3B0019] rounded-lg text-center border-2 border-[#FDF5FD]', + }, + styleUrl: 'roadmap-cluster.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RoadmapClusterComponent { + protected readonly _roadmapBottomSheetNotifierService = inject( + RoadmapBottomSheetNotifierService, + ); + readonly cluster = input.required(); +} diff --git a/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-node-label/roadmap-node-label.component.scss b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-node-label/roadmap-node-label.component.scss new file mode 100644 index 00000000..1e8ed87d --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-node-label/roadmap-node-label.component.scss @@ -0,0 +1,11 @@ +@use '../../style/roadmap-hover-border-gradient'; + +:host { + --primary-color: #b3004a; + --secondary-color: #66002b; + --gradient-color: #481cab; + --on-hover-border-1: #923cff; + --on-hover-border-2: #ff006a; + --comingSoon: #008b38; + --optional: #c79701; +} diff --git a/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-node-label/roadmap-node-label.component.ts b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-node-label/roadmap-node-label.component.ts new file mode 100644 index 00000000..710e264d --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-node-label/roadmap-node-label.component.ts @@ -0,0 +1,39 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + input, +} from '@angular/core'; + +import { Label } from '../../types/roadmap-node'; + +@Component({ + selector: 'al-roadmap-node-label', + template: ` +
+ {{ label() }} +
+ `, + styleUrl: 'roadmap-node-label.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RoadmapNodeLabelComponent { + readonly label = input.required