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..1e5fd149 --- /dev/null +++ b/apps/blog/src/assets/roadmap-tiles.json @@ -0,0 +1,842 @@ +[ + { + "id": "components", + "title": "Components", + "articleSlugs": [ + "heres-what-you-should-know-when-creating-flexible-and-reusable-components-in-angular", + "compliant-components-declarative-approach-in-angular", + "building-a-custom-stepper-using-angular-cdk" + ], + "previousNodeId": null + }, + { + "id": "styling", + "title": "Styling", + "articleSlugs": [ + "angular-material-theming-application-with-material-3", + "theming-angular-app-its-libraries", + "angular-styles-masterclass-2", + "reduce-your-bundle-size-through-this-component-styling-technique", + "lets-implement-a-theme-switch-like-the-angular-material-site", + "switch-themes-like-a-fox-based-on-ambient-light-in-your-angular-apps", + "techniques-to-style-component-host-element-in-angular" + ], + "parentNodeId": "components" + }, + { + "id": "sass", + "parentNodeId": "styling", + "title": "Sass", + "articleSlugs": [ + "migrate-from-css-to-scss-stylesheets-for-an-existing-angular-project" + ], + "previousNodeId": null + }, + { + "id": "angular-material", + "parentNodeId": "styling", + "title": "Angular Material", + "articleSlugs": [ + "angular-material-theming-application-with-material-3", + "custom-theme-for-angular-material-components-series-part-1-create-a-theme", + "custom-theme-for-angular-material-components-series-part-2-understand-theme", + "custom-theme-for-angular-material-components-series-part-3-apply-theme", + "faster-perceived-response-time-with-angular-material-to-tackle-need-for-speed", + "stop-using-shared-material-module" + ], + "previousNodeId": "sass" + }, + { + "id": "view-encapsulation", + "parentNodeId": "styling", + "title": "View Encapsulation", + "articleSlugs": [ + "techniques-to-style-component-host-element-in-angular", + "angular-css-modules" + ], + "previousNodeId": "angular-material" + }, + { + "id": "lifecycle", + "parentNodeId": "components", + "title": "Lifecycle", + "articleSlugs": [ + "the-essential-difference-between-constructor-and-ngoninit-in-angular", + "component-initialization-without-ngoninit-with-async-pipes-for-observables-and-ngonchanges", + "complete-guide-angular-lifecycle-hooks", + "get-to-know-the-destroyref", + "get-to-know-the-afterrendereffect", + "takeuntildestroy-in-angular-v16" + ], + "previousNodeId": "styling" + }, + { + "id": "animations", + "parentNodeId": "components", + "title": "Animations", + "articleSlugs": [ + "in-depth-guide-into-animations-in-angular", + "controlling-angular-animations-programmatically", + "add-support-for-reduced-motion-in-angular-animations" + ], + "previousNodeId": "lifecycle" + }, + { + "id": "change-detection", + "parentNodeId": "components", + "title": "Change Detection", + "articleSlugs": [ + "change-detection-big-picture-unidirectional-data-flow", + "change-detection-big-picture-rendering-cycle", + "change-detection-big-picture-operations", + "change-detection-big-picture-overview", + "change-detection-and-component-trees-in-angular-applications", + "angular-ivy-change-detection-execution-are-you-prepared", + "do-you-really-know-what-unidirectional-data-flow-means-in-angular", + "what-every-front-end-developer-should-know-about-change-detection-in-angular-and-react", + "a-gentle-introduction-into-change-detection-in-angular", + "the-difference-between-ngdocheck-and-asyncpipe-in-onpush-components", + "deep-dive-into-the-onpush-change-detection-strategy-in-angular", + "the-latest-in-angular-change-detection-zoneless-signals", + "optimization-techniques-onpush-strategy", + "optimization-techniques-reusing-views", + "from-zone-js-to-zoneless-angular-and-back-how-it-all-works", + "everything-you-need-to-know-about-change-detection-in-angular", + "do-you-still-think-that-ngzone-zone-js-is-required-for-change-detection-in-angular" + ], + "previousNodeId": "animations" + }, + { + "id": "component-interactions", + "parentNodeId": "components", + "title": "Component Interactions (input/output)", + "articleSlugs": [ + "making-hostbinding-work-with-observables", + "how-to-cancel-a-component-event-from-output-properties-in-angular", + "router-data-as-components-inputs-in-angular-v16", + "required-inputs-in-angular-v16" + ], + "previousNodeId": "change-detection" + }, + { + "id": "dynamic-components", + "parentNodeId": "components", + "title": "Dynamic Components", + "articleSlugs": [ + "dynamic-components-what-they-are-part-ii", + "here-is-what-you-need-to-know-about-dynamic-components-in-angular", + "dynamically-loading-components-with-angular-cli", + "rendering-dynamic-components-by-selector-name-in-ivy", + "deferred-components-vs-dynamic-components-in-angular" + ], + "previousNodeId": "component-interactions" + }, + { + "id": "templates", + "title": "Templates", + "parentNodeId": "components", + "articleSlugs": [ + "using-angular-in-the-right-way-template-syntax", + "angular-template-let-variable-hot-or-not" + ], + "previousNodeId": "dynamic-components" + }, + { + "id": "data-binding", + "parentNodeId": "templates", + "title": "Data Binding", + "articleSlugs": [ + "bindon-lesser-known-angular-template-features", + "the-mechanics-of-property-bindings-update-in-angular" + ], + "previousNodeId": null + }, + { + "id": "control-flow", + "parentNodeId": "templates", + "title": "Control Flow", + "articleSlugs": [ + "diving-into-the-new-angular-control-flow-internals", + "new-syntax-for-control-flow-in-angular", + "build-a-pokemon-gallery-with-new-control-flow-in-angular-17" + ], + "previousNodeId": "data-binding" + }, + { + "id": "content-projection", + "parentNodeId": "templates", + "title": "Content Projection", + "articleSlugs": ["ngtemplateoutlet-the-secret-to-customisation"], + "previousNodeId": "control-flow" + }, + { + "id": "modules", + "title": "Modules", + "articleSlugs": [ + "angular-workspace-no-application-for-you", + "avoiding-common-confusions-with-modules-in-angular", + "asynchronous-modules-and-components-in-angular-ivy" + ], + "previousNodeId": "components" + }, + { + "id": "pipes", + "title": "Pipes", + "articleSlugs": [ + "new-possibilities-with-angulars-push-pipe-part-1", + "new-possibilities-with-angulars-push-pipe-part-2", + "the-essential-difference-between-pure-and-impure-pipes-in-angular-and-why-that-matters", + "how-pure-and-impure-pipes-work-in-angular-ivy" + ], + "previousNodeId": "modules" + }, + { + "id": "directives", + "title": "Directives", + "articleSlugs": [ + "angular-self-saving-dropdowns-yet-another-directive", + "create-a-directive-for-free-dragging-in-angular" + ], + "previousNodeId": "pipes" + }, + { + "id": "attribute-directives", + "parentNodeId": "directives", + "title": "Attribute Directives", + "articleSlugs": ["angular-augmenting-native-elements"], + "previousNodeId": null + }, + { + "id": "structural-directives", + "parentNodeId": "directives", + "title": "Structural Directives", + "articleSlugs": [ + "doing-a11y-easily-with-angular-cdk-keyboard-navigable-lists" + ], + "previousNodeId": "attribute-directives" + }, + { + "id": "ng-template-ng-container", + "parentNodeId": "directives", + "title": "ng-template, ng-container", + "articleSlugs": ["ngtemplateoutlet-the-secret-to-customisation"], + "previousNodeId": "structural-directives" + }, + { + "id": "directive-composition", + "parentNodeId": "directives", + "title": "Directive Composition", + "articleSlugs": ["work-smart-not-hard-use-directive-composition-api"], + "previousNodeId": "ng-template-ng-container" + }, + { + "id": "routing", + "title": "Routing", + "articleSlugs": [ + "angular-router-everything-you-need-to-know-about", + "how-to-reuse-common-layouts-in-angular-using-router", + "improved-navigation-in-angular-7-with-switchmap", + "angular-scroll-position-restoration", + "router-data-as-components-inputs-in-angular-v16" + ], + "previousNodeId": "directives" + }, + { + "id": "configuration", + "parentNodeId": "routing", + "title": "Configuration", + "articleSlugs": [ + "external-configurations-in-angular", + "dynamic-configuration-leveraging-app-initializer" + ], + "previousNodeId": null + }, + { + "id": "guards-resolvers", + "parentNodeId": "routing", + "title": "Guards, Resolvers", + "articleSlugs": [], + "previousNodeId": "configuration" + }, + { + "id": "routerlink", + "parentNodeId": "routing", + "title": "routerLink, routerLinkActive, ...", + "articleSlugs": [], + "previousNodeId": "guards-resolvers" + }, + { + "id": "router-outlets", + "parentNodeId": "routing", + "title": "Router Outlets", + "articleSlugs": ["angular-router-series-secondary-outlets-primer"], + "previousNodeId": "routerlink" + }, + { + "id": "dependency-injection", + "title": "Dependency Injection", + "articleSlugs": [ + "dependency-injection-in-angular-everything-you-need-to-know", + "make-the-most-of-angular-di-private-providers-concept", + "a-deep-dive-into-injectable-and-providedin-in-ivy", + "angular-di-getting-to-know-the-ivy-nodeinjector", + "a-curious-case-of-the-host-decorator-and-element-injectors-in-angular", + "what-you-always-wanted-to-know-about-angular-dependency-injection-tree", + "how-to-follow-the-dependency-inversion-principle-in-nestjs-and-angular", + "leveraging-dependency-injection-to-reduce-duplicated-code-in-angular", + "how-to-avoid-angular-injectable-instances-duplication", + "what-is-forwardref-in-angular-and-why-we-need-it" + ], + "previousNodeId": "routing" + }, + { + "id": "forms", + "title": "Forms", + "articleSlugs": [ + "a-thorough-exploration-of-angular-forms", + "angular-forms-useful-tips", + "angular-forms-why-is-ngmodelchange-late-when-updating-ngmodel-value", + "the-updateon-option-in-angular-forms" + ], + "previousNodeId": "dependency-injection" + }, + { + "id": "template-driven-forms", + "parentNodeId": "forms", + "title": "Template-Driven Forms", + "articleSlugs": [], + "previousNodeId": null + }, + { + "id": "control-value-accessor", + "parentNodeId": "forms", + "title": "Control Value Accessor", + "articleSlugs": [ + "never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms", + "how-to-use-controlvalueaccessor-to-enhance-date-input-with-automatic-conversion-and-validation" + ], + "previousNodeId": "template-driven-forms" + }, + { + "id": "signal-forms", + "parentNodeId": "forms", + "title": "Signal Forms", + "articleSlugs": [], + "previousNodeId": "control-value-accessor" + }, + { + "id": "reactive-forms", + "parentNodeId": "forms", + "title": "Reactive Forms", + "articleSlugs": [ + "strongly-typed-reactive-forms-in-angular", + "implementing-reusable-and-reactive-forms-in-angular", + "angular-forms-reactive-design-patterns-catalog", + "convert-into-strongly-typed-angular-forms-in-a-minute", + "exploring-the-difference-between-disabling-a-form-control-through-reactive-forms-api-and-html-attributes", + "nested-forms-with-controlcontainer", + "angular-forms-story-strong-types" + ], + "previousNodeId": "signal-forms" + }, + { + "id": "validation", + "parentNodeId": "forms", + "title": "Validation", + "articleSlugs": [ + "the-best-way-to-implement-custom-validators", + "creating-elegant-reactive-forms-with-rxwebvalidators" + ], + "previousNodeId": "reactive-forms" + }, + { + "id": "reactivity", + "title": "Reactivity", + "articleSlugs": [ + "finding-fine-grained-reactive-programming", + "exploring-the-state-of-reactivity-patterns-in-2020", + "solidjs-reactivity-to-rendering", + "declarative-reactive-data-and-action-streams-in-angular" + ], + "previousNodeId": "forms" + }, + { + "id": "signals", + "parentNodeId": "reactivity", + "title": "Signals", + "articleSlugs": [ + "what-linkedsignal-is-and-how-to-use-it", + "signals-in-angular-deep-dive-for-busy-developers", + "angular-signals-a-new-feature-in-angular-16", + "the-latest-in-angular-change-detection-zoneless-signals", + "why-angular-signals-wont-replace-rxjs", + "angular-signals-rxjs-interop-from-a-practical-example", + "the-angular-viewmodel-of-a-component-as-an-observable" + ], + "previousNodeId": null + }, + { + "id": "rxjs", + "parentNodeId": "reactivity", + "title": "RXJS", + "articleSlugs": [ + "rxjs-recipes-forkjoin-with-the-progress-of-completion-for-bulk-network-requests-in-angular", + "rxjs-for-await-what", + "rxjs-why-memory-leaks-occur-when-using-a-subject", + "rxjs-custom-operators", + "create-a-taponce-custom-rxjs-operator", + "telegraph-with-rxjs-the-power-of-reactive-systems", + "the-state-of-rxjs-rxjs-7-and-beyond", + "rxjs7-whats-new", + "subtle-difference-between-map-and-pluck-rxjs-operators-that-you-should-know", + "rxjs-applying-asyncscheduler-as-an-argument-vs-with-observeon-operator", + "reading-the-rxjs-6-sources-map-and-pipe", + "rxjs-in-angular-when-to-subscribe-rarely", + "how-to-read-the-rxjs-6-sources-part-1-understanding-of-and-subscriptions", + "rxjs-in-angular-part-i", + "rxjs-in-angular-part-ii", + "rxjs-in-angular-part-iii", + "fastest-way-to-cache-for-lazy-developers-angular-with-rxjs", + "rxjs-repeat-operator-beginner-necromancer-guide", + "how-to-debounce-an-input-while-skipping-the-first-entry", + "throttling-notifications-from-multiple-users-with-rxjs", + "power-of-rxjs-when-using-exponential-backoff", + "rxjs-heads-up-topromise-is-being-deprecated", + "the-simple-way-to-reload-data-using-rxjs", + "rxjs-used-in-angular-knowledge-in-a-nutshell", + "build-your-own-observable-part-1-arrays", + "build-your-own-observable-part-2-containers-and-intuition", + "building-your-own-observable-part-3-the-observer-pattern-and-creational-methods", + "build-your-own-observable-part-4-map-filter-take-and-all-that-jazz" + ], + "previousNodeId": "signals" + }, + { + "id": "http", + "title": "HTTP", + "articleSlugs": [ + "the-new-angular-httpclient-api", + "exploring-the-httpclientmodule-in-angular", + "how-to-use-the-environment-for-specific-http-services", + "how-to-use-ts-decorators-to-add-caching-logic-to-api-calls" + ], + "previousNodeId": "reactivity" + }, + { + "id": "interceptors", + "parentNodeId": "http", + "title": "interceptors", + "articleSlugs": [ + "how-to-implement-automatic-token-insertion-in-requests-using-http-interceptor-angular-tutorials", + "how-to-split-http-interceptors-between-multiple-backends", + "insiders-guide-into-interceptors-and-httpclient-mechanics-in-angular" + ], + "previousNodeId": null + }, + { + "id": "requests", + "parentNodeId": "http", + "title": "requests", + "articleSlugs": [ + "parsing-and-mapping-api-response-using-zod-js", + "how-to-read-azure-dev-ops-logs-from-node-js-using-rest-api", + "building-an-api-with-firebase", + "building-a-backendless-application-with-angular-appwrite" + ], + "previousNodeId": "interceptors" + }, + { + "id": "testing", + "title": "Testing", + "articleSlugs": [ + "catch-angular-template-errors-like-a-pro-or-how-i-create-angular-demo", + "effective-rxjs-marble-testing", + "angular-testing-with-headless-chrome" + ], + "previousNodeId": "http" + }, + { + "id": "unit-tests", + "parentNodeId": "testing", + "title": "Unit Tests", + "articleSlugs": [ + "angular-unit-testing-viewchild", + "create-your-angular-unit-test-spies-automagically", + "spectator-when-testing-becomes-a-pleasure", + "ng-mocks-what-is-it-all-about", + "learn-how-to-unit-test-the-deferrable-views", + "announcing-stryker-4-0-mutation-switching" + ], + "previousNodeId": null + }, + { + "id": "integration-tests", + "parentNodeId": "testing", + "title": "Integration Tests", + "articleSlugs": [ + "write-better-automated-tests-with-cypress-in-angular", + "how-cypress-makes-testing-fun", + "visual-regression-testing-with-cypress-and-angular", + "cypress-introduction" + ], + "previousNodeId": "unit-tests" + }, + { + "id": "angular-cli", + "title": "Angular CLI", + "articleSlugs": [ + "angular-cli-flows-big-picture", + "angular-cli-builders", + "angular-cli-camelcase-or-kebab-case", + "angular-generators", + "hide-boilerplate-nx-files-in-vscode-webstorm", + "how-to-stop-being-afraid-and-create-your-own-angular-cli-builder", + "angular-compilation-restrictions-overview" + ], + "previousNodeId": "testing" + }, + { + "id": "state-management", + "title": "State Management", + "articleSlugs": [ + "ngrx-best-practices", + "ngrx-bad-practices", + "ngrx-not-only-store", + "how-to-manage-component-state-in-angular-using-ngrx-component-store", + "how-i-got-rid-of-state-observables-in-angular", + "an-overview-of-state-management-solutions-for-react-and-nextjs", + "state-machines-in-javascript-with-xstate" + ], + "previousNodeId": "angular-cli" + }, + { + "id": "ngrx", + "parentNodeId": "state-management", + "title": "NGRX", + "articleSlugs": [ + "ngrx-tips-tricks-2", + "adding-ngrx-to-your-existing-applications", + "make-ngrx-hold-business-logic-dumb-components-smart-store", + "better-action-hygiene-with-events-in-ngrx", + "understanding-the-magic-behind-ngrx-effects", + "understanding-the-magic-behind-storemodule-of-ngrx-ngrx-store", + "typesafe-code-with-immer-and-where-it-can-help-in-ngrx", + "a-journey-into-ngrx-selectors", + "ngrx-component", + "understanding-ngrx-component-store-selector-debouncing", + "ngrx-use-effects-and-router-store-to-isolate-route-related-side-effects", + "whats-new-in-ngrx-changes-overview-tips-and-tricks", + "making-an-angular-project-mono-repo-with-ngrx-state-management-and-lazy-loading", + "how-to-start-flying-with-angular-and-ngrx", + "ngrx-how-and-where-to-handle-loading-and-error-states-of-ajax-calls" + ], + "previousNodeId": null + }, + { + "id": "signal-store", + "parentNodeId": "state-management", + "title": "NGRX Signal Store", + "articleSlugs": [ + "breakthrough-in-state-management-discover-the-simplicity-of-signal-store-part-1", + "signal-store-ngxs-elevating-flexibility-in-state-management" + ], + "previousNodeId": "ngrx" + }, + { + "id": "ngxs", + "parentNodeId": "state-management", + "title": "NGXS", + "articleSlugs": [ + "firebase-ngxs-the-perfect-couple", + "all-you-need-to-know-to-jumpstart-with-ngxs", + "signal-store-ngxs-elevating-flexibility-in-state-management" + ], + "previousNodeId": "signal-store" + }, + { + "id": "developer-tools", + "title": "Developer Tools", + "articleSlugs": [ + "debugging-techniques-chrome-devtools", + "easier-angular-ivy-debugging-with-a-chrome-extension", + "useful-chrome-devtools-techniques-when-debugging-change-detection-in-angular", + "debugging-techniques-angular-devtools", + "setting-up-efficient-workflows-with-eslint-prettier-and-typescript" + ], + "previousNodeId": "state-management" + }, + { + "id": "internationalization", + "title": "Internationalization", + "articleSlugs": [ + "internationalization-how-to-open-an-application-to-the-world-part-1", + "internationalization-how-to-open-an-application-to-the-world-part-2", + "implementing-multi-language-angular-applications-rendered-on-a-server-ssr" + ], + "previousNodeId": "developer-tools" + }, + { + "id": "performance", + "title": "Performance", + "articleSlugs": [ + "how-to-use-angulars-defer-block-to-improve-performance", + "bundle-size-improvements-from-deferred-views-in-angular", + "boost-your-applications-performance-with-ngoptimizedimage", + "improve-page-performance-and-lcp-with-ngoptimizedimage", + "how-in-depth-knowledge-of-change-detection-in-angular-helped-me-improve-applications-performance", + "simple-angular-context-help-component-or-how-global-event-listener-can-affect-your-performance", + "optimizing-events-handling-in-angular", + "optimize-angular-bundle-size-in-4-steps", + "optimize-your-angular-bundle-size", + "how-to-exclude-stylesheets-from-the-bundle-and-lazy-load-them-in-angular-angular-tutorials" + ], + "previousNodeId": "internationalization" + }, + { + "id": "ngoptimizeimage", + "parentNodeId": "performance", + "title": "ngOptimizeImage", + "articleSlugs": [ + "boost-your-applications-performance-with-ngoptimizedimage", + "improve-page-performance-and-lcp-with-ngoptimizedimage", + "the-who-what-when-where-why-and-how-of-image-optimization-in-angular" + ], + "previousNodeId": null + }, + { + "id": "defer", + "parentNodeId": "performance", + "title": "@defer", + "articleSlugs": [ + "learn-how-to-unit-test-the-deferrable-views", + "how-to-use-angulars-defer-block-to-improve-performance", + "bundle-size-improvements-from-deferred-views-in-angular", + "deferred-components-vs-dynamic-components-in-angular" + ], + "previousNodeId": "ngoptimizeimage" + }, + { + "id": "lazy-loading", + "parentNodeId": "performance", + "title": "Lazy Loading", + "articleSlugs": [ + "angular-router-series-pillar-3-lazy-loading-aot-and-preloading", + "lazy-loading-angular-modules-with-ivy", + "asynchronous-modules-and-components-in-angular-ivy", + "lazy-loading-angular-components-from-non-angular-applications", + "angular-lazy-load-common-styles-specific-to-a-feature-module" + ], + "previousNodeId": "defer" + }, + { + "id": "architecture-design-patterns", + "title": "Architecture / Design Patterns", + "articleSlugs": [ + "ports-and-adapters-vs-hexagonal-architecture-is-it-the-same-pattern", + "angular-facade-pattern", + "designing-angular-architecture-container-presentation-pattern", + "view-state-selector-angular-design-pattern", + "designing-scalable-angular-applications", + "angular-and-solid-principles", + "stop-using-services-the-importance-of-defining-object-responsibilities-precisely", + "angular-dependency-inversion-principle-2", + "angular-interface-segregation-principle-2", + "angular-liskov-substitution-principle-2", + "angular-open-closed-principle-2", + "angular-single-responsibility-principle-2", + "building-an-extensible-dynamic-pluggable-enterprise-application-with-angular", + "implementing-shared-logic-for-crud-ui-components-in-angular", + "scalable-modular-angular-application-with-nx", + "building-a-type-agnostic-cache-using-generics-in-typescript", + "overview-of-oop-patterns-implementation-in-javascript", + "demystifying-taiga-ui-root-component-portals-pattern-in-angular", + "the-controllers-of-component-concept-in-angular-part-ii" + ], + "previousNodeId": "performance" + }, + { + "id": "security", + "title": "Security", + "articleSlugs": [ + "localstorage-vs-cookies-all-you-need-to-know-about-storing-jwt-tokens-securely-in-the-front-end", + "can-we-fully-trust-html-sanitizers-and-how-to-work-without-them", + "implement-google-sign-inoauth-in-your-angular-app-in-under-15-minutes" + ], + "previousNodeId": "architecture-design-patterns" + }, + { + "id": "ssr", + "title": "SSR", + "articleSlugs": [ + "angular-universal-real-app-problems", + "the-dark-side-of-server-side-rendering-part-1", + "the-dark-side-of-server-side-rendering-part-2", + "the-journey-to-isomorphic-rendering-performance", + "implementing-multi-language-angular-applications-rendered-on-a-server-ssr", + "angular-analog-and-vite", + "analog-a-meta-framework-for-angular", + "effortless-angular-deployment-with-vercel" + ], + "previousNodeId": "security" + }, + { + "id": "accessibility", + "title": "Accessibility", + "articleSlugs": [ + "angular-a11y-11-tips-on-how-to-make-your-apps-more-accessible", + "doing-a11y-easily-with-angular-cdk-keyboard-navigable-lists", + "angular-for-everyone-how-to-adapt-applications-for-people-with-disabilities" + ], + "previousNodeId": "ssr" + }, + { + "title": "Deployment & CI/CD", + "id": "deployment-&-ci/cd", + "articleSlugs": [ + "build-your-angular-app-once-deploy-anywhere", + "effortless-angular-deployment-with-vercel", + "craft-a-complete-gitlab-pipeline-for-angular-part-1", + "craft-a-complete-gitlab-pipeline-for-angular-part-2", + "the-angular-devops-series-deploying-to-firebase-with-circleci", + "deploy-an-angular-application-to-iis", + "how-to-deploy-a-run-time-micro-frontend-application-using-aws", + "automate-angular-application-deployment-via-aws-codepipeline", + "how-to-automate-npm-package-publishing-with-azure-devops" + ], + "previousNodeId": "accessibility" + }, + { + "title": "Bundling & Optimization", + "id": "bundling-&-optimization", + "articleSlugs": [ + "optimize-your-angular-bundle-size", + "optimize-angular-bundle-size-in-4-steps", + "the-simple-way-to-reload-data-using-rxjs", + "track-your-bundle-size-with-bundlemon", + "a-gentle-introduction-into-tree-shaking-in-angular-ivy", + "angular-tree-shaking-2", + "how-to-exclude-stylesheets-from-the-bundle-and-lazy-load-them-in-angular-angular-tutorials", + "code-splitting-in-angular-or-how-to-share-components-between-lazy-modules", + "reduce-your-bundle-size-through-this-component-styling-technique" + ], + "previousNodeId": "deployment-&-ci/cd" + }, + { + "title": "Libraries & Packages", + "id": "libraries-&-packages", + "articleSlugs": [ + "what-makes-a-good-angular-library", + "the-angular-library-series-building-and-packaging", + "the-angular-library-series-publishing", + "creating-a-library-in-angular-6-using-angular-cli-and-ng-packagr", + "complete-beginner-guide-to-publishing-an-angular-library-to-npm", + "create-your-standalone-angular-library-in-10-minutes", + "stop-using-shared-material-module", + "simplifying-web-components-usage-with-angular-elements", + "building-and-consuming-angular-elements-as-web-components" + ], + "previousNodeId": "bundling-&-optimization" + }, + { + "title": "Micro Frontends", + "id": "micro-frontends", + "articleSlugs": [ + "the-micro-frontend-chaos-and-how-to-solve-it", + "angular-micro-frontends-a-modern-approach-to-complex-app-development", + "taking-micro-frontends-to-the-next-level", + "how-to-deploy-a-run-time-micro-frontend-application-using-aws" + ], + "previousNodeId": "libraries-&-packages" + }, + { + "title": "Advanced Angular Features", + "id": "advanced-angular-features", + "articleSlugs": [ + "teleportation-in-angular", + "what-is-forwardref-in-angular-and-why-we-need-it", + "angular-tools-you-should-be-aware-of", + "headless-angular-components", + "global-objects-in-angular", + "angular-extended-diagnostics-2", + "type-checking-templates-in-angular-view-engine-and-ivy", + "running-event-listeners-outside-of-the-ngzone", + "from-zone-js-to-zoneless-angular-and-back-how-it-all-works", + "do-you-still-think-that-ngzone-zone-js-is-required-for-change-detection-in-angular" + ], + "previousNodeId": "micro-frontends" + }, + { + "title": "Angular Versions Updates", + "id": "angular-versions-updates", + "articleSlugs": [ + "angular-19-2-whats-new", + "angular-19-1", + "angular-19-whats-new", + "angular-18-whats-new", + "angular-17-introduction-to-angular-renaissance", + "angular-16-whats-new", + "angular-15-whats-new", + "angular-14-what-you-should-know", + "angular-11-towards-the-type-safety", + "whats-new-after-angular-8", + "brace-yourself-angular-8-is-coming" + ], + "previousNodeId": "advanced-angular-features" + }, + { + "title": "Data Visualization", + "id": "data-visualization", + "articleSlugs": [ + "customization-with-ng2-charts-an-easy-way-to-visualize-data", + "creating-a-sketchpad-with-angular-and-p5js", + "inside-ag-grid-techniques-to-make-the-fastest-javascript-datagrid-in-the-world" + ], + "previousNodeId": "angular-versions-updates" + }, + { + "title": "Web Components", + "id": "web-components", + "articleSlugs": [ + "angular-web-components-a-complete-guide", + "building-and-consuming-angular-elements-as-web-components", + "simplifying-web-components-usage-with-angular-elements", + "angular-elements-2" + ], + "previousNodeId": "internationalization" + }, + { + "title": "Monorepo & Workspace", + "id": "monorepo-&-workspace", + "articleSlugs": [ + "scalable-modular-angular-application-with-nx", + "full-stack-apps-with-angular-and-nestjs-in-an-nx-monorepo", + "angular-workspace-no-application-for-you", + "shell-library-patterns-with-nx-and-monorepo-architectures", + "nx-angular-elements-case-study", + "making-an-angular-project-mono-repo-with-ngrx-state-management-and-lazy-loading" + ], + "previousNodeId": "web-components" + }, + { + "title": "Cross-Platform", + "id": "cross-platform", + "articleSlugs": [ + "angular-on-mobile-applications", + "angular-electron-2", + "angular-electron-part-2", + "building-web-desktop-and-mobile-apps-from-a-single-codebase-using-angular" + ], + "previousNodeId": "monorepo-&-workspace" + }, + { + "title": "Rendering & DOM Manipulation", + "id": "rendering-&-dom-manipulation", + "articleSlugs": [ + "how-to-do-dom-manipulation-properly-in-angular", + "working-with-dom-in-angular-unexpected-consequences-and-optimization-techniques", + "angular-platforms-in-depth-part-3-rendering-angular-applications-in-terminal", + "exploring-angular-dom-manipulation-techniques-using-viewcontainerref" + ], + "previousNodeId": "cross-platform" + } +] 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..a5ff24a2 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.html @@ -0,0 +1,24 @@ + + + +
+ @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..e4813704 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.scss @@ -0,0 +1,13 @@ +// TODO - still needed? +.roadmap-container { + animation: fadeIn 1s ease-in-out; +} + +@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..f3eb12a4 --- /dev/null +++ b/libs/blog/roadmap/feature-roadmap/src/lib/feature-roadmap.component.ts @@ -0,0 +1,211 @@ +import { isPlatformBrowser } from '@angular/common'; +import { HttpClient } from '@angular/common/http'; +import { + AfterViewInit, + ChangeDetectionStrategy, + Component, + computed, + ElementRef, + inject, + PLATFORM_ID, + viewChild, +} from '@angular/core'; +import { rxResource } from '@angular/core/rxjs-interop'; + +import { + EventType, + RoadmapLayer, + RoadmapLayerComponent, + RoadmapSvgControlsComponent, +} from '@angular-love/blog/roadmap/ui-roadmap'; +import { + RoadmapClusterNode, + RoadmapNode, + RoadmapStandardNode, +} from '@angular-love/blog/roadmap/ui-roadmap-node'; + +export interface RoadmapNodeDTO { + id: string; + previousNodeId?: string; + parentNodeId?: string; + title: string; +} + +const svgPanZoomInitialConfig = { + fit: false, + center: false, + minZoom: 0.5, + maxZoom: 2.5, + zoomScaleSensitivity: 0.1, +}; + +@Component({ + selector: 'al-feature-roadmap', + imports: [RoadmapLayerComponent, RoadmapSvgControlsComponent], + templateUrl: './feature-roadmap.component.html', + styleUrl: './feature-roadmap.component.scss', + host: { + class: 'block h-full w-full relative', + }, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class FeatureRoadmapComponent implements AfterViewInit { + private readonly _platform = inject(PLATFORM_ID); + private _svgPanZoom!: SvgPanZoom.Instance; + private readonly _svgRoadmap = + viewChild.required>('roadmap'); + + private readonly _http = inject(HttpClient); + private readonly nodesDto = rxResource({ + loader: () => + this._http.get('assets/roadmap-tiles.json', { + responseType: 'json', + }), + }).value.asReadonly(); + + protected readonly roadmapLayers = computed(() => + this.buildRoadmapLayers(this.nodesDto()), + ); + + async ngAfterViewInit() { + if (isPlatformBrowser(this._platform)) { + await this.initSvgPanZoom(); + } + } + + resizeRoadmap(event: EventType): void { + if (event === 'reset') this._svgPanZoom.reset(); + if (event === 'decrement') this._svgPanZoom.zoomOut(); + if (event === 'increment') this._svgPanZoom.zoomIn(); + if (event === 'zoom-reset') this._svgPanZoom.resetZoom(); + } + + // TODO - maybe extract to util function and rewrite this to be more readable + private buildRoadmapLayers( + roadmapNodesDto: RoadmapNodeDTO[] | undefined, + ): RoadmapLayer[] { + if (!roadmapNodesDto) { + return []; + } + + const nodeDtoMap = roadmapNodesDto.reduce( + (acc, node) => ({ ...acc, [node.id]: node }), + {} as { [nodeId: string]: RoadmapNodeDTO }, + ); + const layerMap: { [parentNodeId: string]: string[] } = {}; + const clusterMap: { [clusterNodeId: string]: string[] } = {}; + const nodeMap: { [nodeId: string]: RoadmapNode } = {}; + + roadmapNodesDto.forEach((nodeDto) => { + if (nodeDto.parentNodeId) { + if (nodeDtoMap[nodeDto.parentNodeId].parentNodeId) { + const parentClusterNodeDto = nodeDtoMap[nodeDto.parentNodeId]; + + clusterMap[parentClusterNodeDto.id] = [ + ...(clusterMap[parentClusterNodeDto.id] ?? []), + nodeDto.id, + ]; + + if (nodeMap[nodeDto.parentNodeId]) { + nodeMap[parentClusterNodeDto.id].nodeType = 'cluster'; + } else { + nodeMap[parentClusterNodeDto.id] = { + id: parentClusterNodeDto.id, + nodeType: 'cluster', + title: parentClusterNodeDto.title, + }; + } + } else { + layerMap[nodeDto.parentNodeId] = [ + ...(layerMap[nodeDto.parentNodeId] ?? []), + nodeDto.id, + ]; + } + if (!nodeMap[nodeDto.id]) { + nodeMap[nodeDto.id] = { + id: nodeDto.id, + nodeType: 'secondary', + title: nodeDto.title, + }; + } + } else { + nodeMap[nodeDto.id] = { + id: nodeDto.id, + nodeType: 'primary', + title: nodeDto.title, + }; + if (!layerMap[nodeDto.id]) { + layerMap[nodeDto.id] = []; + } + } + }); + + // setup clusters + Object.entries(clusterMap).forEach(([clusterNodeId, childrenNodeIds]) => { + const previousClusterNodeIdToNodeIdMap = childrenNodeIds.reduce( + (acc, primaryNodeId) => ({ + ...acc, + [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']: + primaryNodeId, + }), + {} as { [previousNodeId: string | 'initialNode']: string }, + ); + + const clusterNode = nodeMap[clusterNodeId] as RoadmapClusterNode; + clusterNode.clusteredNodes = []; + let nextNodeId = previousClusterNodeIdToNodeIdMap['initialNode']; + while (nextNodeId) { + clusterNode.clusteredNodes.push( + nodeMap[nextNodeId] as RoadmapStandardNode, + ); + nextNodeId = previousClusterNodeIdToNodeIdMap[nextNodeId]; + } + }); + + // setup layers + const previousLayerNodeIdToNodeIdMap = Object.keys(layerMap).reduce( + (acc, primaryNodeId) => ({ + ...acc, + [nodeDtoMap[primaryNodeId].previousNodeId || 'initialNode']: + primaryNodeId, + }), + {} as { [previousNodeId: string | 'initialNode']: string }, + ); + + const layers: RoadmapLayer[] = []; + let nextParentNodeId = previousLayerNodeIdToNodeIdMap['initialNode']; + while (nextParentNodeId) { + layers.push({ + parentNode: nodeMap[nextParentNodeId] as RoadmapStandardNode, + childNodes: layerMap[nextParentNodeId].map( + (childrenNodeId) => nodeMap[childrenNodeId], + ), + }); + nextParentNodeId = previousLayerNodeIdToNodeIdMap[nextParentNodeId]; + } + + return [ + { + parentNode: { + id: '1', + title: 'Angular.Love Roadmap Introduction', + nodeType: 'angular-love', + }, + childNodes: [], + }, + ...layers, + ]; + } + + private async initSvgPanZoom() { + const svgPanZoomModule = await import('svg-pan-zoom'); + const svgPanZoom: SvgPanZoom.Instance = + 'default' in svgPanZoomModule + ? (svgPanZoomModule.default as SvgPanZoom.Instance) + : svgPanZoomModule; + + this._svgPanZoom = svgPanZoom(this._svgRoadmap().nativeElement, { + ...svgPanZoomInitialConfig, + }); + } +} 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-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..084266a6 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/index.ts @@ -0,0 +1,6 @@ +export * from './lib/types/roadmap-node'; + +export * from './lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component'; +export * from './lib/components/roadmap-cluster/roadmap-cluster.component'; +export * from './lib/components/roadmap-primary-node/roadmap-primary-node.component'; +export * from './lib/components/roadmap-secondary-node/roadmap-secondary-node.component'; diff --git a/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.scss b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.scss new file mode 100644 index 00000000..0113819c --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.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-angular-love-node/roadmap-angular-love-node.component.ts b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.ts new file mode 100644 index 00000000..171e281a --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-angular-love-node/roadmap-angular-love-node.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { RoadmapStandardNode } from '../../types/roadmap-node'; + +@Component({ + selector: 'al-roadmap-angular-love-node', + template: ` +
+
+
{{ node().title }}
+
+
+ `, + styleUrl: 'roadmap-angular-love-node.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RoadmapAngularLoveNodeComponent { + readonly node = input.required(); +} 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..1dd61ac1 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-cluster/roadmap-cluster.component.ts @@ -0,0 +1,37 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +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 { + readonly cluster = input.required(); +} diff --git a/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.scss b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.scss new file mode 100644 index 00000000..0113819c --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.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-primary-node/roadmap-primary-node.component.ts b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.ts new file mode 100644 index 00000000..a553e0c3 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-primary-node/roadmap-primary-node.component.ts @@ -0,0 +1,27 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { RoadmapNode } from '../../types/roadmap-node'; + +@Component({ + selector: 'al-roadmap-primary-node', + template: ` +
+
+ {{ node().title }} +
+
+ `, + host: { + class: + 'roadmap-hover-border-gradient relative w-fit text-nowrap rounded-lg bg-[#FDF5FD] text-[#FDF5FD]', + }, + styleUrl: 'roadmap-primary-node.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RoadmapPrimaryNodeComponent { + readonly node = input.required(); +} diff --git a/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.scss b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.scss new file mode 100644 index 00000000..0113819c --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.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-secondary-node/roadmap-secondary-node.component.ts b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.ts new file mode 100644 index 00000000..58e7210d --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/components/roadmap-secondary-node/roadmap-secondary-node.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { RoadmapNode } from '../../types/roadmap-node'; + +@Component({ + selector: 'al-roadmap-secondary-node', + template: ` +
+
+ {{ node().title }} +
+
+ `, + styleUrl: 'roadmap-secondary-node.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RoadmapSecondaryNodeComponent { + readonly node = input.required(); +} diff --git a/libs/blog/roadmap/ui-roadmap-node/src/lib/style/_roadmap-hover-border-gradient.scss b/libs/blog/roadmap/ui-roadmap-node/src/lib/style/_roadmap-hover-border-gradient.scss new file mode 100644 index 00000000..d80ef765 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/style/_roadmap-hover-border-gradient.scss @@ -0,0 +1,32 @@ +.roadmap-hover-border-gradient { + position: relative; + overflow: hidden; + + &::before { + content: ''; + display: block; + position: absolute; + width: 2000px; + height: 2000px; + top: calc(50% - 1000px); + left: calc(50% - 1000px); + background: conic-gradient(#ff006a, #923cff, #ff006a); + opacity: 0; + transition: opacity 0.2s ease-in-out; + z-index: 1; + } + + &:hover::before { + opacity: 1; + animation: rotate 1s linear infinite; + } +} + +@keyframes rotate { + from { + transform: rotate(0); + } + to { + transform: rotate(360deg); + } +} diff --git a/libs/blog/roadmap/ui-roadmap-node/src/lib/types/roadmap-node.ts b/libs/blog/roadmap/ui-roadmap-node/src/lib/types/roadmap-node.ts new file mode 100644 index 00000000..715ec03c --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/src/lib/types/roadmap-node.ts @@ -0,0 +1,15 @@ +interface RoadmapNodeBase { + id: string; + title: string; +} + +export interface RoadmapStandardNode extends RoadmapNodeBase { + nodeType: 'primary' | 'secondary' | 'angular-love'; +} + +export interface RoadmapClusterNode extends RoadmapNodeBase { + nodeType: 'cluster'; + clusteredNodes?: RoadmapStandardNode[]; +} + +export type RoadmapNode = RoadmapStandardNode | RoadmapClusterNode; diff --git a/libs/blog/roadmap/ui-roadmap-node/src/test-setup.ts b/libs/blog/roadmap/ui-roadmap-node/src/test-setup.ts new file mode 100644 index 00000000..ea414013 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/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-node/tsconfig.json b/libs/blog/roadmap/ui-roadmap-node/tsconfig.json new file mode 100644 index 00000000..52a0866e --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/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-node/tsconfig.lib.json b/libs/blog/roadmap/ui-roadmap-node/tsconfig.lib.json new file mode 100644 index 00000000..91273870 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/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-node/tsconfig.spec.json b/libs/blog/roadmap/ui-roadmap-node/tsconfig.spec.json new file mode 100644 index 00000000..6e5925e5 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap-node/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/.eslintrc.json b/libs/blog/roadmap/ui-roadmap/.eslintrc.json new file mode 100644 index 00000000..5b79c406 --- /dev/null +++ b/libs/blog/roadmap/ui-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/ui-roadmap/README.md b/libs/blog/roadmap/ui-roadmap/README.md new file mode 100644 index 00000000..729efe0c --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/README.md @@ -0,0 +1,7 @@ +# blog-roadmap-ui-roadmap + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test blog-roadmap-ui-roadmap` to execute the unit tests. diff --git a/libs/blog/roadmap/ui-roadmap/jest.config.ts b/libs/blog/roadmap/ui-roadmap/jest.config.ts new file mode 100644 index 00000000..5e094e8f --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'blog-roadmap-ui-roadmap', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../../coverage/libs/blog/roadmap/ui-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/ui-roadmap/project.json b/libs/blog/roadmap/ui-roadmap/project.json new file mode 100644 index 00000000..15a2af6d --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/project.json @@ -0,0 +1,20 @@ +{ + "name": "blog-roadmap-ui-roadmap", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/roadmap/ui-roadmap/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/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog/roadmap/ui-roadmap/src/index.ts b/libs/blog/roadmap/ui-roadmap/src/index.ts new file mode 100644 index 00000000..d056de92 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/components/roadmap-layer/roadmap-layer.component'; +export * from './lib/components/roadmap-svg-controls/roadmap-svg-controls.component'; diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/connected-node/connected-node.component.html b/libs/blog/roadmap/ui-roadmap/src/lib/components/connected-node/connected-node.component.html new file mode 100644 index 00000000..dfd10379 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/connected-node/connected-node.component.html @@ -0,0 +1,5 @@ + + +
+ +
diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/connected-node/connected-node.component.ts b/libs/blog/roadmap/ui-roadmap/src/lib/components/connected-node/connected-node.component.ts new file mode 100644 index 00000000..4ef9b0f8 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/connected-node/connected-node.component.ts @@ -0,0 +1,34 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + input, +} from '@angular/core'; + +import { + NodeConnectorLineComponent, + NodeConnectorType, +} from '../node-connector-line/node-connector-line.component'; + +@Component({ + selector: 'al-connected-node', + templateUrl: 'connected-node.component.html', + imports: [NodeConnectorLineComponent], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ConnectedNodeComponent { + readonly isLeftSideFirst = input(false); + readonly isRightSideLast = input(false); + + protected readonly connectorType = computed(() => { + if (this.isLeftSideFirst()) { + return 'left-end'; + } + + if (this.isRightSideLast()) { + return 'right-end'; + } + + return 'intermediate'; + }); +} diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/node-connector-line/node-connector-line.component.scss b/libs/blog/roadmap/ui-roadmap/src/lib/components/node-connector-line/node-connector-line.component.scss new file mode 100644 index 00000000..171f2be3 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/node-connector-line/node-connector-line.component.scss @@ -0,0 +1,71 @@ +:host { + $connector-container-height: 50px; + $connector-border-width: 4px; + $end-connector-border-radius: 20px; + $connector-line-border-style: $connector-border-width solid #fdf5fd; + + display: block; + width: 100%; + height: $connector-container-height; + box-sizing: border-box; + position: relative; + + &:after, + &:before { + content: ' '; + position: absolute; + bottom: 0; + height: $connector-container-height; + z-index: 1; + } + + &.left-end { + // Smooth rounded left end + &:after { + right: 0; + width: 50%; + border-top: $connector-line-border-style; + border-left: $connector-line-border-style; + border-top-left-radius: $end-connector-border-radius; + } + + &:before { + display: none; + } + } + + &.right-end { + // Smooth rounded right end + &:before { + left: 0; + width: 50%; + border-top: $connector-line-border-style; + border-right: $connector-line-border-style; + border-top-right-radius: $end-connector-border-radius; + } + + &:after { + display: none; + } + } + + &.line, + &.intermediate { + // Simple line + &:before { + left: 0; + width: 100%; + border-top: $connector-line-border-style; + } + } + + &.intermediate { + // Stack the after element into a T-shape + &:after { + left: 0; + width: 50%; + height: $connector-container-height - 0.5 * $connector-border-width; + border-right: $connector-line-border-style; + } + } +} diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/node-connector-line/node-connector-line.component.ts b/libs/blog/roadmap/ui-roadmap/src/lib/components/node-connector-line/node-connector-line.component.ts new file mode 100644 index 00000000..2ef54a97 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/node-connector-line/node-connector-line.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +export type NodeConnectorType = + | 'left-end' + | 'intermediate' + | 'line' + | 'right-end'; + +@Component({ + selector: 'al-node-connector-line', + template: '', + host: { + '[class.left-end]': 'type() === "left-end"', + '[class.intermediate]': 'type() === "intermediate"', + '[class.line]': 'type() === "line"', + '[class.right-end]': 'type() === "right-end"', + }, + styleUrl: 'node-connector-line.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NodeConnectorLineComponent { + readonly type = input('line'); +} diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/left-slice.pipe.ts b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/left-slice.pipe.ts new file mode 100644 index 00000000..412e8109 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/left-slice.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'leftSlice', +}) +export class LeftSlicePipe implements PipeTransform { + transform(value: T[]): T[] { + const halfLength = Math.ceil(value.length / 2); + return value.slice(0, halfLength); + } +} diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/right-slice.pipe.ts b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/right-slice.pipe.ts new file mode 100644 index 00000000..b0054589 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/right-slice.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'rightSlice', +}) +export class RightSlicePipe implements PipeTransform { + transform(value: T[]): T[] { + const halfLength = Math.ceil(value.length / 2); + return value.slice(halfLength); + } +} diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.html b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.html new file mode 100644 index 00000000..33cb11b2 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.html @@ -0,0 +1,51 @@ + +@if (layer().parentNode.nodeType === 'primary') { + +} @else if (layer().parentNode.nodeType === 'angular-love') { + +} + +@if (showLayerConnector()) { + +} + + +@if ((layer().childNodes || []).length) { +
+ +
+ @for ( + node of layer().childNodes | leftSlice; + track node.id; + let first = $first + ) { + + @if (node.nodeType === 'secondary') { + + } @else if (node.nodeType === 'cluster') { + + } + + } +
+ + + + +
+ @for ( + node of layer().childNodes | rightSlice; + track node.id; + let last = $last + ) { + + @if (node.nodeType === 'secondary') { + + } @else if (node.nodeType === 'cluster') { + + } + + } +
+
+} diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.scss b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.scss new file mode 100644 index 00000000..28eba7f8 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.scss @@ -0,0 +1,4 @@ +.layer-nodes { + display: grid; + grid-template-columns: minmax(0, 1fr) 100px minmax(0, 1fr); +} diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.ts b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.ts new file mode 100644 index 00000000..4a6c46af --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-layer/roadmap-layer.component.ts @@ -0,0 +1,48 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { + RoadmapAngularLoveNodeComponent, + RoadmapClusterComponent, + RoadmapNode, + RoadmapPrimaryNodeComponent, + RoadmapSecondaryNodeComponent, + RoadmapStandardNode, +} from '@angular-love/blog/roadmap/ui-roadmap-node'; + +import { ConnectedNodeComponent } from '../connected-node/connected-node.component'; +import { NodeConnectorLineComponent } from '../node-connector-line/node-connector-line.component'; +import { VerticalConnectorArrowComponent } from '../vertical-connector-arrow/vertical-connector-arrow.component'; + +import { LeftSlicePipe } from './left-slice.pipe'; +import { RightSlicePipe } from './right-slice.pipe'; + +export interface RoadmapLayer { + parentNode: RoadmapStandardNode; + childNodes: RoadmapNode[]; +} + +@Component({ + selector: 'al-roadmap-layer', + templateUrl: 'roadmap-layer.component.html', + styleUrl: 'roadmap-layer.component.scss', + imports: [ + LeftSlicePipe, + RightSlicePipe, + RoadmapClusterComponent, + RoadmapPrimaryNodeComponent, + RoadmapAngularLoveNodeComponent, + RoadmapSecondaryNodeComponent, + ConnectedNodeComponent, + VerticalConnectorArrowComponent, + NodeConnectorLineComponent, + ], + host: { + class: 'flex w-full flex-col items-center relative gap-12 pb-16', + }, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RoadmapLayerComponent { + readonly layer = input.required(); + + readonly showLayerConnector = input(false); +} diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-svg-controls/roadmap-svg-controls.component.ts b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-svg-controls/roadmap-svg-controls.component.ts new file mode 100644 index 00000000..211f0fb5 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/roadmap-svg-controls/roadmap-svg-controls.component.ts @@ -0,0 +1,55 @@ +import { ChangeDetectionStrategy, Component, output } from '@angular/core'; +import { FastSvgComponent } from '@push-based/ngx-fast-svg'; + +export type EventType = 'increment' | 'decrement' | 'reset' | 'zoom-reset'; + +interface Control { + size: string; + name: string; + event: EventType; +} + +@Component({ + selector: 'al-roadmap-svg-controls', + imports: [FastSvgComponent], + template: ` + @for (control of controls; track $index) { + + } + `, + host: { + class: 'flex h-fit w-fit flex-col items-center gap-8', + }, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class RoadmapSvgControlsComponent { + readonly resizeRoadmap = output(); + + protected readonly controls: Control[] = [ + { + event: 'increment', + size: '24', + name: 'zoom-in', + }, + { + event: 'reset', + size: '24', + name: 'circle-center', + }, + { + event: 'zoom-reset', + size: '24', + name: 'zoom-reset', + }, + { + event: 'decrement', + size: '24', + name: 'zoom-out', + }, + ]; +} diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/vertical-connector-arrow/vertical-connector-arrow.component.scss b/libs/blog/roadmap/ui-roadmap/src/lib/components/vertical-connector-arrow/vertical-connector-arrow.component.scss new file mode 100644 index 00000000..665411d3 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/vertical-connector-arrow/vertical-connector-arrow.component.scss @@ -0,0 +1,12 @@ +.line { + border-left: 6px solid #fdf5fd; +} + +.arrow-down { + width: 0; + height: 0; + border-left: 16px solid transparent; + border-right: 16px solid transparent; + + border-top: 30px solid #fdf5fd; +} diff --git a/libs/blog/roadmap/ui-roadmap/src/lib/components/vertical-connector-arrow/vertical-connector-arrow.component.ts b/libs/blog/roadmap/ui-roadmap/src/lib/components/vertical-connector-arrow/vertical-connector-arrow.component.ts new file mode 100644 index 00000000..c34eb534 --- /dev/null +++ b/libs/blog/roadmap/ui-roadmap/src/lib/components/vertical-connector-arrow/vertical-connector-arrow.component.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'al-vertical-connector-arrow', + template: ` +
+
+ `, + host: { + class: 'flex flex-col items-center h-full', + }, + styleUrl: 'vertical-connector-arrow.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VerticalConnectorArrowComponent {} diff --git a/libs/blog/roadmap/ui-roadmap/src/test-setup.ts b/libs/blog/roadmap/ui-roadmap/src/test-setup.ts new file mode 100644 index 00000000..ea414013 --- /dev/null +++ b/libs/blog/roadmap/ui-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/ui-roadmap/tsconfig.json b/libs/blog/roadmap/ui-roadmap/tsconfig.json new file mode 100644 index 00000000..52a0866e --- /dev/null +++ b/libs/blog/roadmap/ui-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/ui-roadmap/tsconfig.lib.json b/libs/blog/roadmap/ui-roadmap/tsconfig.lib.json new file mode 100644 index 00000000..91273870 --- /dev/null +++ b/libs/blog/roadmap/ui-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/ui-roadmap/tsconfig.spec.json b/libs/blog/roadmap/ui-roadmap/tsconfig.spec.json new file mode 100644 index 00000000..6e5925e5 --- /dev/null +++ b/libs/blog/roadmap/ui-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/shell/feature-shell-web/src/lib/blog-shell.routes.ts b/libs/blog/shell/feature-shell-web/src/lib/blog-shell.routes.ts index eafafde9..68b854bb 100644 --- a/libs/blog/shell/feature-shell-web/src/lib/blog-shell.routes.ts +++ b/libs/blog/shell/feature-shell-web/src/lib/blog-shell.routes.ts @@ -21,6 +21,13 @@ export const blogShellRoutes: Route[] = [ ]; export const commonRoutes: Route[] = [ + { + path: 'roadmap', + loadComponent: async () => + await import('./roadmap-shell.component').then( + (m) => m.RoadmapShellComponent, + ), + }, { path: '', component: RootShellComponent, diff --git a/libs/blog/shell/feature-shell-web/src/lib/roadmap-shell.component.ts b/libs/blog/shell/feature-shell-web/src/lib/roadmap-shell.component.ts new file mode 100644 index 00000000..420747a9 --- /dev/null +++ b/libs/blog/shell/feature-shell-web/src/lib/roadmap-shell.component.ts @@ -0,0 +1,89 @@ +import { ViewportScroller } from '@angular/common'; +import { Component, computed, effect, inject } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { Router } from '@angular/router'; +import { TranslocoService } from '@jsverse/transloco'; +import { startWith } from 'rxjs'; + +import { AdBannerStore } from '@angular-love/blog/ad-banner/data-access'; +import { AlLocalizeService } from '@angular-love/blog/i18n/util'; +import { + FooterComponent, + HeaderComponent, +} from '@angular-love/blog/layouts/ui-layouts'; +import { FeatureRoadmapComponent } from '@angular-love/blog/roadmap/feature-roadmap'; +import { SearchComponent } from '@angular-love/blog/search/feature-search'; +import { AdImageBanner } from '@angular-love/blog/shared/ad-banner'; + +@Component({ + selector: 'al-root-shell', + template: ` + + + + + + `, + imports: [ + HeaderComponent, + FooterComponent, + SearchComponent, + FeatureRoadmapComponent, + ], + host: { + class: 'flex flex-col min-h-screen', + }, +}) +export class RoadmapShellComponent { + protected readonly sliderStore = inject(AdBannerStore); + protected readonly slides = computed(() => + this.sliderStore.slider()?.slides.map((slide) => ({ + url: slide.url, + alt: slide.alt, + action: { + type: 'url', + url: slide.navigateTo, + }, + })), + ); + protected readonly msPerSlide = computed( + () => this.sliderStore.slider()?.slideDisplayTimeMs, + ); + + readonly translocoService = inject(TranslocoService); + + // todo: temporary solution to keep in mind how banner influence the layout + protected readonly adBannerVisible = computed(() => false); + + readonly language = toSignal( + this.translocoService.langChanges$.pipe( + startWith(this.translocoService.getActiveLang()), + ), + { + initialValue: 'en', + }, + ); + + private readonly _router = inject(Router); + private readonly _localizeService = inject(AlLocalizeService); + + onLanguageChange(lang: string) { + this._router.navigateByUrl( + this._localizeService.localizeExplicitPath(this._router.url, lang), + ); + } + + constructor(viewport: ViewportScroller) { + // todo: temporary solution to keep in mind how banner influence the layout + effect(() => { + this.adBannerVisible() + ? viewport.setOffset([0, 160]) + : viewport.setOffset([0, 80]); + }); + this.sliderStore.getData(); + } +} diff --git a/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts b/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts index 48ae1611..a9aac4ec 100644 --- a/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts +++ b/libs/blog/shell/feature-shell-web/src/lib/root-shell.component.ts @@ -52,6 +52,9 @@ import { NgClass, AlBannerCarouselComponent, ], + host: { + class: 'flex flex-col min-h-screen', + }, }) export class RootShellComponent { protected readonly sliderStore = inject(AdBannerStore); diff --git a/nx.json b/nx.json index fdb9f601..4d807ac3 100644 --- a/nx.json +++ b/nx.json @@ -85,7 +85,8 @@ }, "@nx/angular:component": { "style": "scss", - "prefix": "al" + "prefix": "al", + "changeDetection": "OnPush" } }, "workspaceLayout": { diff --git a/package.json b/package.json index 12d4fe3c..af45a3d1 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "sanitize-html": "^2.13.0", "shiki": "^2.4.2", "stylelint": "^16.3.1", + "svg-pan-zoom": "^3.6.2", "tailwind-merge": "^2.3.0", "tslib": "^2.6.1", "valibot": "1.0.0-rc.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce81d7a3..b0fd8424 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: stylelint: specifier: ^16.3.1 version: 16.9.0(typescript@5.7.3) + svg-pan-zoom: + specifier: ^3.6.2 + version: 3.6.2 tailwind-merge: specifier: ^2.3.0 version: 2.5.3 @@ -9382,6 +9385,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-pan-zoom@3.6.2: + resolution: {integrity: sha512-JwnvRWfVKw/Xzfe6jriFyfey/lWJLq4bUh2jwoR5ChWQuQoOH8FEh1l/bEp46iHHKHEJWIyFJETbazraxNWECg==} + svg-tags@1.0.0: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} @@ -21448,6 +21454,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-pan-zoom@3.6.2: {} + svg-tags@1.0.0: {} svgo@3.3.2: diff --git a/tsconfig.base.json b/tsconfig.base.json index 5e13d455..5c7e2b2b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -119,6 +119,15 @@ "@angular-love/blog/partners/ui-partners": [ "libs/blog/partners/ui-partners/src/index.ts" ], + "@angular-love/blog/roadmap/feature-roadmap": [ + "libs/blog/roadmap/feature-roadmap/src/index.ts" + ], + "@angular-love/blog/roadmap/ui-roadmap": [ + "libs/blog/roadmap/ui-roadmap/src/index.ts" + ], + "@angular-love/blog/roadmap/ui-roadmap-node": [ + "libs/blog/roadmap/ui-roadmap-node/src/index.ts" + ], "@angular-love/blog/search/data-access": [ "libs/blog/search/data-access/src/index.ts" ],