diff --git a/apps/blog/src/assets/al-ebook.png b/apps/blog/src/assets/al-ebook.png new file mode 100644 index 000000000..4c0b26de4 Binary files /dev/null and b/apps/blog/src/assets/al-ebook.png differ diff --git a/apps/blog/src/assets/i18n/en.json b/apps/blog/src/assets/i18n/en.json index e6570d6ea..87a055057 100644 --- a/apps/blog/src/assets/i18n/en.json +++ b/apps/blog/src/assets/i18n/en.json @@ -7,6 +7,8 @@ "seeAll": "See all the articles", "latest": "Latest Articles", "recommended": "Recommended Articles", + "talks": "Angular talks", + "videoPlayer": "YouTube video player", "angularNews": "Angular News", "guides": "Guides", "angularInDepth": "Angular In Depth", @@ -312,5 +314,19 @@ "section1": "We’ll transfer your article to the CMS and schedule its publication.", "section2": "We’ll also promote it on social media to maximize its reach." } + }, + "presentations": { + "listTitle": "Meetup presentations", + "watchRecording": "WATCH RECORDING", + "authoredBy": "by {{author}}", + "workshopsTitle": "Angular Workshops", + "workshopsDescription": "Whether you want to level up your individual skills or train your entire team, our workshops are 100% hands-on practice with an expert, directly translating into the quality of your code and work speed with Mateusz Stefańczyk (Google Developer Expert).", + "workshopsButton": "CHECK OPEN WORKSHOPS" + }, + "ebook": { + "title": "FREE EBOOK", + "subTitle": "The Angular Evolution Guide: ng20+ Summary for CTOs", + "description": "Download to discover how the latest Angular version improves performance, DX, UX and overall app efficiency, and how it affects your business.", + "downloadButton": "DOWNLOAD NOW" } } diff --git a/apps/blog/src/assets/i18n/pl.json b/apps/blog/src/assets/i18n/pl.json index 61acd50bf..8aa9fd42d 100644 --- a/apps/blog/src/assets/i18n/pl.json +++ b/apps/blog/src/assets/i18n/pl.json @@ -7,6 +7,8 @@ "seeAll": "Zobacz wszystkie", "latest": "Najnowsze Artykuły", "recommended": "Polecane Artykuły", + "talks": "Prezentacje", + "videoPlayer": "Odtwarzacz wideo YouTube", "angularNews": "Wiadomości ze świata Angulara", "guides": "Poradniki", "angularInDepth": "Angular In Depth", @@ -315,5 +317,19 @@ "section1": "Teraz my zajmiemy się przeniesieniem twojego artykułu do CMS’a i zaplanujemy jego opublikowanie", "section2": "Zajmiemy się również jego promowaniem w mediach społecznościowych, aby jak najwięcej osób mogło skorzystać z wiedzy, którą w nim zawarłeś!" } + }, + "presentations": { + "listTitle": "Prezentacje Meetup", + "watchRecording": "OBEJRZYJ NAGRANIE", + "authoredBy": "autor: {{author}}", + "workshopsTitle": "Warsztaty Angular", + "workshopsDescription": "Niezależnie od tego, czy chcesz podnieść swoje umiejętności, czy przeszkolić cały zespół, nasze warsztaty to 100% praktyki z ekspertem, co bezpośrednio przekłada się na jakość twojego kodu i szybkość pracy z Mateuszem Stefańczykiem (Google Developer Expert).", + "workshopsButton": "SPRAWDŹ OTWARTE WARSZTATY" + }, + "ebook": { + "title": "BEZPŁATNY EBOOK", + "subTitle": "Przewodnik po ewolucji Angulara: Podsumowanie ng20+ dla CTO", + "description": "Pobierz, aby odkryć, jak najnowsza wersja Angulara poprawia wydajność, DX, UX i ogólną efektywność aplikacji oraz jak wpływa na Twoją firmę.", + "downloadButton": "POBIERZ TERAZ" } } diff --git a/apps/blog/src/assets/icons/video.svg b/apps/blog/src/assets/icons/video.svg new file mode 100644 index 000000000..6a762348d --- /dev/null +++ b/apps/blog/src/assets/icons/video.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/blog-contracts/presentations/.eslintrc.json b/libs/blog-contracts/presentations/.eslintrc.json new file mode 100644 index 000000000..3456be9b9 --- /dev/null +++ b/libs/blog-contracts/presentations/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/blog-contracts/presentations/README.md b/libs/blog-contracts/presentations/README.md new file mode 100644 index 000000000..c5d38cb77 --- /dev/null +++ b/libs/blog-contracts/presentations/README.md @@ -0,0 +1,7 @@ +# presentations + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test presentations` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/blog-contracts/presentations/jest.config.ts b/libs/blog-contracts/presentations/jest.config.ts new file mode 100644 index 000000000..2374703c1 --- /dev/null +++ b/libs/blog-contracts/presentations/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'presentations', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/blog-contracts/presentations', +}; diff --git a/libs/blog-contracts/presentations/project.json b/libs/blog-contracts/presentations/project.json new file mode 100644 index 000000000..0ff6ef994 --- /dev/null +++ b/libs/blog-contracts/presentations/project.json @@ -0,0 +1,19 @@ +{ + "name": "presentations", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog-contracts/presentations/src", + "projectType": "library", + "tags": ["scope:shared", "type:contract"], + "targets": { + "lint": { + "executor": "@nx/eslint:lint" + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog-contracts/presentations/jest.config.ts" + } + } + } +} diff --git a/libs/blog-contracts/presentations/src/index.ts b/libs/blog-contracts/presentations/src/index.ts new file mode 100644 index 000000000..2da7b99b3 --- /dev/null +++ b/libs/blog-contracts/presentations/src/index.ts @@ -0,0 +1 @@ +export * from './lib/presentations'; diff --git a/libs/blog-contracts/presentations/src/lib/presentations.ts b/libs/blog-contracts/presentations/src/lib/presentations.ts new file mode 100644 index 000000000..5b4a7efd9 --- /dev/null +++ b/libs/blog-contracts/presentations/src/lib/presentations.ts @@ -0,0 +1,9 @@ +import { ArticlePreview } from '@angular-love/contracts/articles'; + +export type PresentationPreview = Omit< + ArticlePreview, + 'readingTime' | 'difficulty' +> & { + eventName: string; + presentationUrl: string; +}; diff --git a/libs/blog-contracts/presentations/tsconfig.json b/libs/blog-contracts/presentations/tsconfig.json new file mode 100644 index 000000000..25f7201d8 --- /dev/null +++ b/libs/blog-contracts/presentations/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/blog-contracts/presentations/tsconfig.lib.json b/libs/blog-contracts/presentations/tsconfig.lib.json new file mode 100644 index 000000000..e583571ea --- /dev/null +++ b/libs/blog-contracts/presentations/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/libs/blog-contracts/presentations/tsconfig.spec.json b/libs/blog-contracts/presentations/tsconfig.spec.json new file mode 100644 index 000000000..69a251f32 --- /dev/null +++ b/libs/blog-contracts/presentations/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/blog/articles/feature-list/src/lib/category-section-container/category-section-container.component.html b/libs/blog/articles/feature-list/src/lib/category-section-container/category-section-container.component.html index 52154c656..3b43ebbbe 100644 --- a/libs/blog/articles/feature-list/src/lib/category-section-container/category-section-container.component.html +++ b/libs/blog/articles/feature-list/src/lib/category-section-container/category-section-container.component.html @@ -5,7 +5,7 @@ hasCategoryPage() ? { displayName: t('homePage.seeAll'), - href: category() + href: category(), } : null " @@ -22,7 +22,7 @@ } } @else { - @for (skeleton of skeleonLoaders(); track $index) { + @for (skeleton of skeletonLoaders(); track $index) { @if (cardType() === 'regular') { } @else { diff --git a/libs/blog/articles/feature-list/src/lib/category-section-container/category-section-container.component.ts b/libs/blog/articles/feature-list/src/lib/category-section-container/category-section-container.component.ts index ed78905c6..c14c5d59b 100644 --- a/libs/blog/articles/feature-list/src/lib/category-section-container/category-section-container.component.ts +++ b/libs/blog/articles/feature-list/src/lib/category-section-container/category-section-container.component.ts @@ -50,7 +50,7 @@ export class CategorySectionContainerComponent { readonly cardType = input('regular'); readonly take = input(6); readonly hasCategoryPage = input(true); - readonly skeleonLoaders = computed(() => [...Array(this.take()).keys()]); + readonly skeletonLoaders = computed(() => [...Array(this.take()).keys()]); protected displayName = computed(() => displayNameDict[this.category()]); diff --git a/libs/blog/articles/feature-shell/src/lib/routes.ts b/libs/blog/articles/feature-shell/src/lib/routes.ts index 57be4a635..d70bf85d6 100644 --- a/libs/blog/articles/feature-shell/src/lib/routes.ts +++ b/libs/blog/articles/feature-shell/src/lib/routes.ts @@ -19,6 +19,21 @@ export const articleRoutes: Routes = [ id: 'angular-news', }, }, + { + path: 'presentations', + loadComponent: async () => + (await import('@angular-love/blog/presentations/presentations-page')) + .PresentationsPageComponent, + }, + { + path: 'presentation/:presentationSlug', + loadComponent: async () => + ( + await import( + '@angular-love/blog/presentations/presentation-details-page' + ) + ).PresentationDetailsPageComponent, + }, { path: 'guides', loadComponent: async () => diff --git a/libs/blog/home/feature-home/src/lib/home-page/home-page.component.html b/libs/blog/home/feature-home/src/lib/home-page/home-page.component.html index b6d095d66..ecfa963bb 100644 --- a/libs/blog/home/feature-home/src/lib/home-page/home-page.component.html +++ b/libs/blog/home/feature-home/src/lib/home-page/home-page.component.html @@ -2,4 +2,4 @@ - + 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 9fc43a5ad..e931bab48 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,5 +1,5 @@
/src/test-setup.ts'], + coverageDirectory: '../../../../coverage/libs/blog/presentations/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/presentations/data-access/project.json b/libs/blog/presentations/data-access/project.json new file mode 100644 index 000000000..92c8069e3 --- /dev/null +++ b/libs/blog/presentations/data-access/project.json @@ -0,0 +1,20 @@ +{ + "name": "blog-presentations-data-access", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/presentations/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/presentations/data-access/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog/presentations/data-access/src/index.ts b/libs/blog/presentations/data-access/src/index.ts new file mode 100644 index 000000000..f15f7b013 --- /dev/null +++ b/libs/blog/presentations/data-access/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/state/presentations-list.store'; +export * from './lib/state/presentation-detail.store'; diff --git a/libs/blog/presentations/data-access/src/lib/dto/presentations.query.ts b/libs/blog/presentations/data-access/src/lib/dto/presentations.query.ts new file mode 100644 index 000000000..f7d917bdc --- /dev/null +++ b/libs/blog/presentations/data-access/src/lib/dto/presentations.query.ts @@ -0,0 +1,3 @@ +export type PresentationsQuery = { + take?: number; +} | null; diff --git a/libs/blog/presentations/data-access/src/lib/infrastructure/presentations.service.ts b/libs/blog/presentations/data-access/src/lib/infrastructure/presentations.service.ts new file mode 100644 index 000000000..91c537bc4 --- /dev/null +++ b/libs/blog/presentations/data-access/src/lib/infrastructure/presentations.service.ts @@ -0,0 +1,138 @@ +import { HttpClient } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; + +import { PresentationPreview } from '@angular-love/blog-contracts/presentations'; +import { ArrayResponse } from '@angular-love/blog-contracts/shared'; +import { ConfigService } from '@angular-love/shared/config'; + +import { PresentationsQuery } from '../dto/presentations.query'; + +const MOCK_PRESENTATIONS = [ + { + slug: 'from-chaos-to-clarity-mastering-state-management', + title: 'From Chaos to Clarity: Mastering State Management in Angular', + excerpt: + 'Discover practical patterns for managing complex application state without losing your mind.', + featuredImageUrl: null, + eventName: 'Angular Camp', + presentationUrl: 'https://www.youtube.com/@angularlove/videos', + publishDate: '2025-03-15', + author: { + name: 'Sarah Chen', + slug: 'sarah-chen', + avatarUrl: 'assets/mock-avatar.png', + }, + }, + { + slug: 'zero-to-hero-building-accessible-angular-apps', + title: 'Zero to Hero: Building Accessible Angular Applications', + excerpt: + 'Transform your Angular apps into inclusive experiences that everyone can use and enjoy.', + featuredImageUrl: null, + eventName: 'Angular Camp', + presentationUrl: 'https://www.youtube.com/@angularlove/videos', + publishDate: '2025-03-14', + author: { + name: 'Marcus Thompson', + slug: 'marcus-thompson', + avatarUrl: 'assets/mock-avatar.png', + }, + }, + { + slug: 'performance-wizardry-making-angular-blazingly-fast', + title: 'Performance Wizardry: Making Angular Blazingly Fast', + excerpt: + 'Learn the secrets behind lightning-fast Angular applications that delight users.', + featuredImageUrl: null, + eventName: 'Angular Camp', + presentationUrl: 'https://www.youtube.com/@angularlove/videos', + publishDate: '2025-03-13', + author: { + name: 'Priya Sharma', + slug: 'priya-sharma', + avatarUrl: 'assets/mock-avatar.png', + }, + }, + { + slug: 'reactive-revolution-rxjs-patterns-that-actually-work', + title: 'Reactive Revolution: RxJS Patterns That Actually Work', + excerpt: + 'Cut through the complexity and master RxJS with real-world patterns you can use today.', + featuredImageUrl: null, + eventName: 'Angular Camp', + presentationUrl: 'https://www.youtube.com/@angularlove/videos', + publishDate: '2025-03-12', + author: { + name: 'Lukas Müller', + slug: 'lukas-muller', + avatarUrl: 'assets/mock-avatar.png', + }, + }, + { + slug: 'component-architecture-building-scalable-design-systems', + title: 'Component Architecture: Building Scalable Design Systems', + excerpt: + 'Create maintainable, reusable components that grow with your application and team.', + featuredImageUrl: null, + eventName: 'Angular Camp', + presentationUrl: 'https://www.youtube.com/@angularlove/videos', + publishDate: '2025-03-11', + author: { + name: 'Elena Rodriguez', + slug: 'elena-rodriguez', + avatarUrl: 'assets/mock-avatar.png', + }, + }, + { + slug: 'debugging-like-a-detective-advanced-angular-techniques', + title: 'Debugging Like a Detective: Advanced Angular Techniques', + excerpt: + 'Uncover hidden bugs and solve mysterious issues with professional debugging strategies.', + featuredImageUrl: null, + eventName: 'Angular Camp', + presentationUrl: 'https://www.youtube.com/@angularlove/videos', + publishDate: '2025-03-10', + author: { + name: 'Takeshi Nakamura', + slug: 'takeshi-nakamura', + avatarUrl: 'assets/mock-avatar.png', + }, + }, +]; + +@Injectable({ providedIn: 'root' }) +export class PresentationsService { + private readonly _apiBaseUrl = inject(ConfigService).get('apiBaseUrl'); + private readonly _http = inject(HttpClient); + + getPresentationsList( + query: PresentationsQuery, + ): Observable> { + return of({ + data: MOCK_PRESENTATIONS, + total: MOCK_PRESENTATIONS.length, + }); + // TODO - add real API + // return this._http.get>( + // `${this._apiBaseUrl}/presentations`, + // { params: query || {} }, + // ); + } + + getPresentationDetail(slug: string): Observable { + // Find the presentation preview by slug + // This is a mock implementation; replace with real API call + const preview = MOCK_PRESENTATIONS.find((p) => p.slug === slug); + + if (!preview) { + throw new Error(`Presentation with slug "${slug}" not found`); + } + + return of(preview); + // TODO - add real API + // return this._http.get( + // `${this._apiBaseUrl}/presentations/${slug}`, + // ); + } +} diff --git a/libs/blog/presentations/data-access/src/lib/infrastructure/videos.service.ts b/libs/blog/presentations/data-access/src/lib/infrastructure/videos.service.ts new file mode 100644 index 000000000..e69de29bb diff --git a/libs/blog/presentations/data-access/src/lib/state/presentation-detail.store.ts b/libs/blog/presentations/data-access/src/lib/state/presentation-detail.store.ts new file mode 100644 index 000000000..4b92069ed --- /dev/null +++ b/libs/blog/presentations/data-access/src/lib/state/presentation-detail.store.ts @@ -0,0 +1,60 @@ +import { inject } from '@angular/core'; +import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { concatMap, pipe, tap } from 'rxjs'; + +import { PresentationPreview } from '@angular-love/blog-contracts/presentations'; +import { withLangState } from '@angular-love/blog/i18n/data-access'; +import { + LoadingState, + withCallState, +} from '@angular-love/shared/utils-signal-store'; + +import { PresentationsService } from '../infrastructure/presentations.service'; + +type PresentationDetailState = { + presentation: PresentationPreview | null; + slug: string | null; +}; + +const initialState: PresentationDetailState = { + presentation: null, + slug: null, +}; + +export const PresentationDetailStore = signalStore( + { providedIn: 'root' }, + withState(initialState), + withLangState(), + withCallState('fetch presentation detail'), + withMethods((store) => { + const presentationsService = inject(PresentationsService); + return { + fetchPresentationDetail: rxMethod( + pipe( + tap((slug) => + patchState(store, { + slug: slug, + fetchPresentationDetailCallState: LoadingState.LOADING, + }), + ), + concatMap((slug) => + presentationsService.getPresentationDetail(slug).pipe( + tap({ + next: (data) => + patchState(store, { + presentation: data, + fetchPresentationDetailCallState: LoadingState.LOADED, + }), + error: (error) => + patchState(store, { + fetchPresentationDetailCallState: { error }, + }), + }), + ), + ), + ), + ), + }; + }), +); diff --git a/libs/blog/presentations/data-access/src/lib/state/presentations-list.store.ts b/libs/blog/presentations/data-access/src/lib/state/presentations-list.store.ts new file mode 100644 index 000000000..f6ffb60de --- /dev/null +++ b/libs/blog/presentations/data-access/src/lib/state/presentations-list.store.ts @@ -0,0 +1,64 @@ +import { inject } from '@angular/core'; +import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { concatMap, pipe, tap } from 'rxjs'; + +import { PresentationPreview } from '@angular-love/blog-contracts/presentations'; +import { withLangState } from '@angular-love/blog/i18n/data-access'; +import { + LoadingState, + withCallState, +} from '@angular-love/shared/utils-signal-store'; + +import { PresentationsQuery } from '../dto/presentations.query'; +import { PresentationsService } from '../infrastructure/presentations.service'; + +type PresentationsListState = { + presentations: PresentationPreview[] | null; + query: PresentationsQuery; + total: number; +}; + +const initialState: PresentationsListState = { + presentations: null, + query: null, + total: 0, +}; + +export const PresentationsListStore = signalStore( + { providedIn: 'root' }, + withState(initialState), + withLangState(), + withCallState('fetch presentations list'), + withMethods((store) => { + const presentationsService = inject(PresentationsService); + return { + fetchPresentationsList: rxMethod( + pipe( + tap((query) => + patchState(store, { + query: query, + fetchPresentationsListCallState: LoadingState.LOADING, + }), + ), + concatMap((query) => + presentationsService.getPresentationsList({ ...query }).pipe( + tap({ + next: ({ data, total }) => + patchState(store, { + presentations: data, + fetchPresentationsListCallState: LoadingState.LOADED, + total, + }), + error: (error) => + patchState(store, { + fetchPresentationsListCallState: { error }, + }), + }), + ), + ), + ), + ), + }; + }), +); diff --git a/libs/blog/presentations/data-access/src/lib/state/videos-list.store.ts b/libs/blog/presentations/data-access/src/lib/state/videos-list.store.ts new file mode 100644 index 000000000..e69de29bb diff --git a/libs/blog/presentations/data-access/src/test-setup.ts b/libs/blog/presentations/data-access/src/test-setup.ts new file mode 100644 index 000000000..d2c50cd70 --- /dev/null +++ b/libs/blog/presentations/data-access/src/test-setup.ts @@ -0,0 +1,9 @@ +import 'jest-preset-angular/setup-jest'; + +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +}; diff --git a/libs/blog/presentations/data-access/tsconfig.json b/libs/blog/presentations/data-access/tsconfig.json new file mode 100644 index 000000000..52a0866e0 --- /dev/null +++ b/libs/blog/presentations/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/presentations/data-access/tsconfig.lib.json b/libs/blog/presentations/data-access/tsconfig.lib.json new file mode 100644 index 000000000..912738705 --- /dev/null +++ b/libs/blog/presentations/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/presentations/data-access/tsconfig.spec.json b/libs/blog/presentations/data-access/tsconfig.spec.json new file mode 100644 index 000000000..6e5925e5c --- /dev/null +++ b/libs/blog/presentations/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/presentations/feature-list/src/index.ts b/libs/blog/presentations/feature-list/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/libs/blog/presentations/feature-list/src/lib/presentations-list-container.component.html b/libs/blog/presentations/feature-list/src/lib/presentations-list-container.component.html new file mode 100644 index 000000000..e69de29bb diff --git a/libs/blog/presentations/feature-list/src/lib/videos-list-container.component.ts b/libs/blog/presentations/feature-list/src/lib/videos-list-container.component.ts new file mode 100644 index 000000000..e69de29bb diff --git a/libs/blog/presentations/presentation-details-page/.eslintrc.json b/libs/blog/presentations/presentation-details-page/.eslintrc.json new file mode 100644 index 000000000..5b79c406a --- /dev/null +++ b/libs/blog/presentations/presentation-details-page/.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/presentations/presentation-details-page/README.md b/libs/blog/presentations/presentation-details-page/README.md new file mode 100644 index 000000000..409f98ab1 --- /dev/null +++ b/libs/blog/presentations/presentation-details-page/README.md @@ -0,0 +1,7 @@ +# presentation-details-page + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test presentation-details-page` to execute the unit tests. diff --git a/libs/blog/presentations/presentation-details-page/jest.config.ts b/libs/blog/presentations/presentation-details-page/jest.config.ts new file mode 100644 index 000000000..5aa2ce492 --- /dev/null +++ b/libs/blog/presentations/presentation-details-page/jest.config.ts @@ -0,0 +1,22 @@ +export default { + displayName: 'presentation-details-page', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: + '../../../../coverage/libs/blog/presentations/presentation-details-page', + 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/presentations/presentation-details-page/project.json b/libs/blog/presentations/presentation-details-page/project.json new file mode 100644 index 000000000..443a73e48 --- /dev/null +++ b/libs/blog/presentations/presentation-details-page/project.json @@ -0,0 +1,20 @@ +{ + "name": "presentation-details-page", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/presentations/presentation-details-page/src", + "prefix": "al", + "projectType": "library", + "tags": ["scope:client", "type:feature"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog/presentations/presentation-details-page/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog/presentations/presentation-details-page/src/index.ts b/libs/blog/presentations/presentation-details-page/src/index.ts new file mode 100644 index 000000000..9a018b390 --- /dev/null +++ b/libs/blog/presentations/presentation-details-page/src/index.ts @@ -0,0 +1 @@ +export * from './lib/presentation-details-page/presentation-details-page.component'; diff --git a/libs/blog/presentations/presentation-details-page/src/lib/presentation-details-page/presentation-details-page.component.html b/libs/blog/presentations/presentation-details-page/src/lib/presentation-details-page/presentation-details-page.component.html new file mode 100644 index 000000000..47ef2302c --- /dev/null +++ b/libs/blog/presentations/presentation-details-page/src/lib/presentation-details-page/presentation-details-page.component.html @@ -0,0 +1,50 @@ +
+ @if (presentation(); as presentation) { + + +
+

{{ presentation.title }}

+
{{ presentation.eventName }}
+
{{ t('authoredBy', { author: presentation.author.name }) }}
+
+ + {{ 'Watch recording' }} + + + + + } @else { + Something went wrong... + } +
diff --git a/libs/blog/presentations/presentation-details-page/src/lib/presentation-details-page/presentation-details-page.component.scss b/libs/blog/presentations/presentation-details-page/src/lib/presentation-details-page/presentation-details-page.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/libs/blog/presentations/presentation-details-page/src/lib/presentation-details-page/presentation-details-page.component.ts b/libs/blog/presentations/presentation-details-page/src/lib/presentation-details-page/presentation-details-page.component.ts new file mode 100644 index 000000000..630404298 --- /dev/null +++ b/libs/blog/presentations/presentation-details-page/src/lib/presentation-details-page/presentation-details-page.component.ts @@ -0,0 +1,47 @@ +import { NgOptimizedImage } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + input, +} from '@angular/core'; +import { TranslocoDirective } from '@jsverse/transloco'; + +import { NewsletterComponent } from '@angular-love/blog/newsletter'; +import { PresentationDetailStore } from '@angular-love/blog/presentations/data-access'; +import { ButtonComponent } from '@angular-love/blog/shared/ui-button'; +import { EbookComponent } from '@angular-love/blog/shared/ui-ebook'; + +@Component({ + selector: 'al-presentation-details-page', + imports: [ + NgOptimizedImage, + ButtonComponent, + EbookComponent, + NewsletterComponent, + TranslocoDirective, + ], + templateUrl: './presentation-details-page.component.html', + styleUrl: './presentation-details-page.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PresentationDetailsPageComponent { + readonly presentationSlug = input.required(); + + protected readonly presentationDetailStore = inject(PresentationDetailStore); + + readonly isFetchPresentationDetailLoading = + this.presentationDetailStore.isFetchPresentationDetailLoading; + + readonly presentation = this.presentationDetailStore.presentation; + + readonly isFetchPresentationDetailError = + this.presentationDetailStore.isFetchPresentationDetailError; + + constructor() { + this.presentationDetailStore.fetchPresentationDetail( + computed(() => this.presentationSlug()), + ); + } +} diff --git a/libs/blog/presentations/presentation-details-page/src/test-setup.ts b/libs/blog/presentations/presentation-details-page/src/test-setup.ts new file mode 100644 index 000000000..ea414013f --- /dev/null +++ b/libs/blog/presentations/presentation-details-page/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/presentations/presentation-details-page/tsconfig.json b/libs/blog/presentations/presentation-details-page/tsconfig.json new file mode 100644 index 000000000..52a0866e0 --- /dev/null +++ b/libs/blog/presentations/presentation-details-page/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/presentations/presentation-details-page/tsconfig.lib.json b/libs/blog/presentations/presentation-details-page/tsconfig.lib.json new file mode 100644 index 000000000..912738705 --- /dev/null +++ b/libs/blog/presentations/presentation-details-page/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/presentations/presentation-details-page/tsconfig.spec.json b/libs/blog/presentations/presentation-details-page/tsconfig.spec.json new file mode 100644 index 000000000..6e5925e5c --- /dev/null +++ b/libs/blog/presentations/presentation-details-page/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/presentations/presentations-page/.eslintrc.json b/libs/blog/presentations/presentations-page/.eslintrc.json new file mode 100644 index 000000000..5b79c406a --- /dev/null +++ b/libs/blog/presentations/presentations-page/.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/presentations/presentations-page/README.md b/libs/blog/presentations/presentations-page/README.md new file mode 100644 index 000000000..c0c03a460 --- /dev/null +++ b/libs/blog/presentations/presentations-page/README.md @@ -0,0 +1,7 @@ +# presentations-page + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test presentations-page` to execute the unit tests. diff --git a/libs/blog/presentations/presentations-page/jest.config.ts b/libs/blog/presentations/presentations-page/jest.config.ts new file mode 100644 index 000000000..89ee4f09d --- /dev/null +++ b/libs/blog/presentations/presentations-page/jest.config.ts @@ -0,0 +1,22 @@ +export default { + displayName: 'presentations-page', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: + '../../../../coverage/libs/blog/presentations/presentations-page', + 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/presentations/presentations-page/project.json b/libs/blog/presentations/presentations-page/project.json new file mode 100644 index 000000000..3cd105ec1 --- /dev/null +++ b/libs/blog/presentations/presentations-page/project.json @@ -0,0 +1,20 @@ +{ + "name": "presentations-page", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/presentations/presentations-page/src", + "prefix": "al", + "projectType": "library", + "tags": ["scope:client", "type:feature"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog/presentations/presentations-page/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog/presentations/presentations-page/src/index.ts b/libs/blog/presentations/presentations-page/src/index.ts new file mode 100644 index 000000000..68c750a18 --- /dev/null +++ b/libs/blog/presentations/presentations-page/src/index.ts @@ -0,0 +1 @@ +export * from './lib/presentations-page/presentations-page.component'; diff --git a/libs/blog/presentations/presentations-page/src/lib/presentations-page/presentations-page.component.html b/libs/blog/presentations/presentations-page/src/lib/presentations-page/presentations-page.component.html new file mode 100644 index 000000000..c91bebdee --- /dev/null +++ b/libs/blog/presentations/presentations-page/src/lib/presentations-page/presentations-page.component.html @@ -0,0 +1,38 @@ +

+ {{ t('listTitle') }} +

+ +
+ @if (!presentationsStore.isFetchPresentationsListLoading()) { + @for ( + presentation of presentationsStore.presentations(); + track presentation.slug + ) { + + } + } @else { + + } +
+ + +
+ +
diff --git a/libs/blog/presentations/presentations-page/src/lib/presentations-page/presentations-page.component.scss b/libs/blog/presentations/presentations-page/src/lib/presentations-page/presentations-page.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/libs/blog/presentations/presentations-page/src/lib/presentations-page/presentations-page.component.spec.ts b/libs/blog/presentations/presentations-page/src/lib/presentations-page/presentations-page.component.spec.ts new file mode 100644 index 000000000..24738fa37 --- /dev/null +++ b/libs/blog/presentations/presentations-page/src/lib/presentations-page/presentations-page.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PresentationsPageComponent } from './presentations-page.component'; + +describe('PresentationsPageComponent', () => { + let component: PresentationsPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PresentationsPageComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PresentationsPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/blog/presentations/presentations-page/src/lib/presentations-page/presentations-page.component.ts b/libs/blog/presentations/presentations-page/src/lib/presentations-page/presentations-page.component.ts new file mode 100644 index 000000000..4a9c1b208 --- /dev/null +++ b/libs/blog/presentations/presentations-page/src/lib/presentations-page/presentations-page.component.ts @@ -0,0 +1,65 @@ +import { isPlatformBrowser } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + input, + PLATFORM_ID, + signal, +} from '@angular/core'; +import { TranslocoDirective } from '@jsverse/transloco'; + +import { PresentationsListStore } from '@angular-love/blog/presentations/data-access'; +import { + PresentationCardComponent, + PresentationCardSkeletonComponent, +} from '@angular-love/blog/presentations/ui-presentation-card'; +import { EbookComponent } from '@angular-love/blog/shared/ui-ebook'; +import { + PageChangeEvent, + PaginationComponent, + QueryPaginationDirective, +} from '@angular-love/blog/shared/ui-pagination'; +import { RepeatDirective } from '@angular-love/utils'; + +@Component({ + selector: 'al-presentations-page', + imports: [ + PaginationComponent, + QueryPaginationDirective, + PresentationCardComponent, + PresentationCardSkeletonComponent, + RepeatDirective, + EbookComponent, + TranslocoDirective, + ], + templateUrl: './presentations-page.component.html', + styleUrl: './presentations-page.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'flex flex-col h-full w-full', + }, +}) +export class PresentationsPageComponent { + readonly title = input.required(); + + readonly pagination = signal({ take: 12, skip: 0 }); + + protected readonly presentationsStore = inject(PresentationsListStore); + + private readonly _platform = inject(PLATFORM_ID); + + constructor() { + const query = computed(() => ({ + ...this.pagination(), + })); + + this.presentationsStore.fetchPresentationsList(query); + } + + protected pageChange(event: PageChangeEvent) { + isPlatformBrowser(this._platform) && window.scrollTo(0, 0); + this.pagination.set(event); + } +} diff --git a/libs/blog/presentations/presentations-page/src/test-setup.ts b/libs/blog/presentations/presentations-page/src/test-setup.ts new file mode 100644 index 000000000..ea414013f --- /dev/null +++ b/libs/blog/presentations/presentations-page/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/presentations/presentations-page/tsconfig.json b/libs/blog/presentations/presentations-page/tsconfig.json new file mode 100644 index 000000000..52a0866e0 --- /dev/null +++ b/libs/blog/presentations/presentations-page/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/presentations/presentations-page/tsconfig.lib.json b/libs/blog/presentations/presentations-page/tsconfig.lib.json new file mode 100644 index 000000000..912738705 --- /dev/null +++ b/libs/blog/presentations/presentations-page/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/presentations/presentations-page/tsconfig.spec.json b/libs/blog/presentations/presentations-page/tsconfig.spec.json new file mode 100644 index 000000000..6e5925e5c --- /dev/null +++ b/libs/blog/presentations/presentations-page/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/presentations/ui-presentation-card/.eslintrc.json b/libs/blog/presentations/ui-presentation-card/.eslintrc.json new file mode 100644 index 000000000..5b79c406a --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/.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/presentations/ui-presentation-card/README.md b/libs/blog/presentations/ui-presentation-card/README.md new file mode 100644 index 000000000..91cc07eb6 --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/README.md @@ -0,0 +1,7 @@ +# blog-presentation-ui-presentation-card + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test blog-presentations-ui-presentation-card` to execute the unit tests. diff --git a/libs/blog/presentations/ui-presentation-card/jest.config.ts b/libs/blog/presentations/ui-presentation-card/jest.config.ts new file mode 100644 index 000000000..289e27a84 --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/jest.config.ts @@ -0,0 +1,22 @@ +export default { + displayName: 'blog-presentations-ui-presentation-card', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: + '../../../../coverage/libs/blog/presentations/ui-presentation-card', + 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/presentations/ui-presentation-card/project.json b/libs/blog/presentations/ui-presentation-card/project.json new file mode 100644 index 000000000..903a09f6c --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/project.json @@ -0,0 +1,20 @@ +{ + "name": "blog-presentations-ui-presentation-card", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/presentations/ui-presentation-card/src", + "prefix": "al", + "projectType": "library", + "tags": ["scope:client", "type:ui"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog/videos/ui-video-card/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog/presentations/ui-presentation-card/src/index.ts b/libs/blog/presentations/ui-presentation-card/src/index.ts new file mode 100644 index 000000000..35067e647 --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/components/presentation-card/presentation-card.component'; +export * from './lib/components/presentation-card/presentation-card-skeleton.component'; diff --git a/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card-skeleton.component.ts b/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card-skeleton.component.ts new file mode 100644 index 000000000..aac836eaa --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card-skeleton.component.ts @@ -0,0 +1,50 @@ +import { Component } from '@angular/core'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; + +@Component({ + selector: 'al-presentation-card-skeleton', + imports: [NgxSkeletonLoaderModule], + template: ` +
+ + + +
+ +
+ + +
+ + + + + + + + + + +
+ + +
+
+
+ `, +}) +export class PresentationCardSkeletonComponent {} diff --git a/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card.component.html b/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card.component.html new file mode 100644 index 000000000..da56165dc --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card.component.html @@ -0,0 +1,46 @@ + diff --git a/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card.component.ts b/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card.component.ts new file mode 100644 index 000000000..a1341f809 --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card.component.ts @@ -0,0 +1,24 @@ +import { NgOptimizedImage } from '@angular/common'; +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { FastSvgComponent } from '@push-based/ngx-fast-svg'; + +import { PresentationPreview } from '@angular-love/blog-contracts/presentations'; +import { AlLocalizePipe } from '@angular-love/blog/i18n/util'; +import { AvatarComponent } from '@angular-love/blog/shared/ui-avatar'; + +@Component({ + selector: 'al-presentation-card', + templateUrl: './presentation-card.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + AvatarComponent, + RouterLink, + NgOptimizedImage, + FastSvgComponent, + AlLocalizePipe, + ], +}) +export class PresentationCardComponent { + readonly presentation = input.required(); +} diff --git a/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card.stories.ts b/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card.stories.ts new file mode 100644 index 000000000..ddf2cd09f --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/presentation-card.stories.ts @@ -0,0 +1,57 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; + +import { PresentationPreview } from '@angular-love/blog-contracts/presentations'; + +import { PresentationCardSkeletonComponent } from './presentation-card-skeleton.component'; +import { PresentationCardComponent } from './presentation-card.component'; + +const presentation: PresentationPreview = { + slug: 'lazy-but-fast-how-taking-it-slow-can-speed-up-your-app', + title: 'Lazy, but Fast: How Taking It Slow Can Speed Up Your App!', + excerpt: + 'Learn how lazy loading can actually improve your application performance.', + featuredImageUrl: 'https://via.placeholder.com/405x228', + publishDate: '2025-03-15', + author: { + name: 'Jarosław Żołnowski', + slug: 'jaroslaw-zolnowski', + avatarUrl: 'https://via.placeholder.com/150', + }, +}; + +const meta: Meta = { + component: PresentationCardComponent, + title: 'Presentations / PresentationCard', + decorators: [ + moduleMetadata({ + imports: [PresentationCardSkeletonComponent], + }), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + presentation: presentation, + }, + render: (args) => ({ + props: args, + template: ` +
+ +
+ `, + }), +}; + +export const Skeleton: StoryObj = { + render: () => ({ + template: ` +
+ +
+ `, + }), +}; diff --git a/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/video-card.component.ts b/libs/blog/presentations/ui-presentation-card/src/lib/components/presentation-card/video-card.component.ts new file mode 100644 index 000000000..e69de29bb diff --git a/libs/blog/presentations/ui-presentation-card/src/test-setup.ts b/libs/blog/presentations/ui-presentation-card/src/test-setup.ts new file mode 100644 index 000000000..d2c50cd70 --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/src/test-setup.ts @@ -0,0 +1,9 @@ +import 'jest-preset-angular/setup-jest'; + +// @ts-expect-error https://thymikee.github.io/jest-preset-angular/docs/getting-started/test-environment +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +}; diff --git a/libs/blog/presentations/ui-presentation-card/tsconfig.json b/libs/blog/presentations/ui-presentation-card/tsconfig.json new file mode 100644 index 000000000..52a0866e0 --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/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/presentations/ui-presentation-card/tsconfig.lib.json b/libs/blog/presentations/ui-presentation-card/tsconfig.lib.json new file mode 100644 index 000000000..912738705 --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/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/presentations/ui-presentation-card/tsconfig.spec.json b/libs/blog/presentations/ui-presentation-card/tsconfig.spec.json new file mode 100644 index 000000000..6e5925e5c --- /dev/null +++ b/libs/blog/presentations/ui-presentation-card/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/shared/ui-ebook/.eslintrc.json b/libs/blog/shared/ui-ebook/.eslintrc.json new file mode 100644 index 000000000..5b79c406a --- /dev/null +++ b/libs/blog/shared/ui-ebook/.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/shared/ui-ebook/README.md b/libs/blog/shared/ui-ebook/README.md new file mode 100644 index 000000000..b947d5540 --- /dev/null +++ b/libs/blog/shared/ui-ebook/README.md @@ -0,0 +1,7 @@ +# ui-ebook + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test ui-ebook` to execute the unit tests. diff --git a/libs/blog/shared/ui-ebook/jest.config.ts b/libs/blog/shared/ui-ebook/jest.config.ts new file mode 100644 index 000000000..a627cc6f8 --- /dev/null +++ b/libs/blog/shared/ui-ebook/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'ui-ebook', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../../coverage/libs/blog/shared/ui-ebook', + 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/shared/ui-ebook/project.json b/libs/blog/shared/ui-ebook/project.json new file mode 100644 index 000000000..eff222569 --- /dev/null +++ b/libs/blog/shared/ui-ebook/project.json @@ -0,0 +1,20 @@ +{ + "name": "ui-ebook", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/blog/shared/ui-ebook/src", + "prefix": "al", + "projectType": "library", + "tags": ["scope:client", "type:ui"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/blog/shared/ui-ebook/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/blog/shared/ui-ebook/src/index.ts b/libs/blog/shared/ui-ebook/src/index.ts new file mode 100644 index 000000000..853ccec8c --- /dev/null +++ b/libs/blog/shared/ui-ebook/src/index.ts @@ -0,0 +1 @@ +export * from './lib/ui-ebook/ui-ebook.component'; diff --git a/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.html b/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.html new file mode 100644 index 000000000..9f5091e4c --- /dev/null +++ b/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.html @@ -0,0 +1,23 @@ + +
+ +
+

{{ t('title') }}

+
+ {{ t('subTitle') }} +
+

+ {{ t('description') }} +

+ + {{ t('downloadButton') }} + +
+
+
diff --git a/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.scss b/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.scss new file mode 100644 index 000000000..a279da4c9 --- /dev/null +++ b/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.scss @@ -0,0 +1,14 @@ +.presentations-page__ebook { + display: grid; + align-items: center; + justify-content: center; + + @screen md { + grid-template-columns: repeat(2, auto); + } + + &-description { + max-width: 40ch; + margin-bottom: 0.5rem; + } +} diff --git a/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.spec.ts b/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.spec.ts new file mode 100644 index 000000000..d515b5639 --- /dev/null +++ b/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UiEbookComponent } from './ui-ebook.component'; + +describe('UiEbookComponent', () => { + let component: UiEbookComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UiEbookComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(UiEbookComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.ts b/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.ts new file mode 100644 index 000000000..b7188bdc0 --- /dev/null +++ b/libs/blog/shared/ui-ebook/src/lib/ui-ebook/ui-ebook.component.ts @@ -0,0 +1,24 @@ +import { NgOptimizedImage } from '@angular/common'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { TranslocoDirective } from '@jsverse/transloco'; + +import { ButtonComponent } from '@angular-love/blog/shared/ui-button'; +import { + CardComponent, + GradientCardDirective, +} from '@angular-love/blog/shared/ui-card'; + +@Component({ + selector: 'al-ebook', + imports: [ + CardComponent, + GradientCardDirective, + ButtonComponent, + NgOptimizedImage, + TranslocoDirective, + ], + templateUrl: './ui-ebook.component.html', + styleUrl: './ui-ebook.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EbookComponent {} diff --git a/libs/blog/shared/ui-ebook/src/test-setup.ts b/libs/blog/shared/ui-ebook/src/test-setup.ts new file mode 100644 index 000000000..ea414013f --- /dev/null +++ b/libs/blog/shared/ui-ebook/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/shared/ui-ebook/tsconfig.json b/libs/blog/shared/ui-ebook/tsconfig.json new file mode 100644 index 000000000..52a0866e0 --- /dev/null +++ b/libs/blog/shared/ui-ebook/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/shared/ui-ebook/tsconfig.lib.json b/libs/blog/shared/ui-ebook/tsconfig.lib.json new file mode 100644 index 000000000..912738705 --- /dev/null +++ b/libs/blog/shared/ui-ebook/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/shared/ui-ebook/tsconfig.spec.json b/libs/blog/shared/ui-ebook/tsconfig.spec.json new file mode 100644 index 000000000..6e5925e5c --- /dev/null +++ b/libs/blog/shared/ui-ebook/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/shared/ui-icon/src/lib/icon/icon.component.ts b/libs/blog/shared/ui-icon/src/lib/icon/icon.component.ts index 0ed088f2b..d17f65fad 100644 --- a/libs/blog/shared/ui-icon/src/lib/icon/icon.component.ts +++ b/libs/blog/shared/ui-icon/src/lib/icon/icon.component.ts @@ -20,7 +20,8 @@ export type IconType = | 'twitter-x' | 'youtube' | 'sun' - | 'moon'; + | 'moon' + | 'video'; @Component({ selector: 'al-icon', diff --git a/tsconfig.base.json b/tsconfig.base.json index 92fdaea58..75d8a97fd 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -36,6 +36,9 @@ "@angular-love/blog-bff/shared/util-middleware": [ "libs/blog-bff/shared/util-middleware/src/index.ts" ], + "@angular-love/blog-contracts/presentations": [ + "libs/blog-contracts/presentations/src/index.ts" + ], "@angular-love/blog-contracts/shared": [ "libs/blog-contracts/shared/src/index.ts" ], @@ -94,6 +97,9 @@ "@angular-love/blog/contracts/banners": [ "libs/blog-contracts/banners/src/index.ts" ], + "@angular-love/blog/contracts/presentations": [ + "libs/blog-contracts/presentations/src/index.ts" + ], "@angular-love/blog/contracts/roadmap": [ "libs/blog-contracts/roadmap/src/index.ts" ], @@ -122,6 +128,18 @@ "@angular-love/blog/partners/ui-partners": [ "libs/blog/partners/ui-partners/src/index.ts" ], + "@angular-love/blog/presentations/data-access": [ + "libs/blog/presentations/data-access/src/index.ts" + ], + "@angular-love/blog/presentations/presentation-details-page": [ + "libs/blog/presentations/presentation-details-page/src/index.ts" + ], + "@angular-love/blog/presentations/presentations-page": [ + "libs/blog/presentations/presentations-page/src/index.ts" + ], + "@angular-love/blog/presentations/ui-presentation-card": [ + "libs/blog/presentations/ui-presentation-card/src/index.ts" + ], "@angular-love/blog/roadmap/feature-roadmap": [ "libs/blog/roadmap/feature-roadmap/src/index.ts" ], @@ -161,6 +179,9 @@ "@angular-love/blog/shared/ui-dynamic-text-clamp": [ "libs/blog/shared/ui-dynamic-text-clamp/src/index.ts" ], + "@angular-love/blog/shared/ui-ebook": [ + "libs/blog/shared/ui-ebook/src/index.ts" + ], "@angular-love/blog/shared/ui-icon": [ "libs/blog/shared/ui-icon/src/index.ts" ],