blog-blueprint 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/BlueprintTemplate/.editorconfig +17 -0
  2. package/BlueprintTemplate/.gitlab-ci.yml +34 -0
  3. package/BlueprintTemplate/DESIGN.txt +37 -0
  4. package/BlueprintTemplate/README.md +78 -0
  5. package/BlueprintTemplate/angular.json +101 -0
  6. package/BlueprintTemplate/gitignore +42 -0
  7. package/BlueprintTemplate/package.json +58 -0
  8. package/BlueprintTemplate/public/assets/lessons/aws/aws-overview.md +139 -0
  9. package/BlueprintTemplate/public/assets/lessons/core/aws-aurora-guide.md +181 -0
  10. package/BlueprintTemplate/public/assets/lessons/core/aws-dynamodb-guide.md +139 -0
  11. package/BlueprintTemplate/public/assets/lessons/core/aws-ec2-guide.md +152 -0
  12. package/BlueprintTemplate/public/assets/lessons/core/aws-eventbridge-guide.md +152 -0
  13. package/BlueprintTemplate/public/assets/lessons/core/aws-iam-guide.md +132 -0
  14. package/BlueprintTemplate/public/assets/lessons/core/aws-lambda-guide.md +129 -0
  15. package/BlueprintTemplate/public/assets/lessons/core/aws-rds-guide.md +193 -0
  16. package/BlueprintTemplate/public/assets/lessons/core/aws-s3-guide.md +158 -0
  17. package/BlueprintTemplate/public/assets/lessons/core/aws-sns-guide.md +163 -0
  18. package/BlueprintTemplate/public/assets/lessons/core/aws-sqs-guide.md +173 -0
  19. package/BlueprintTemplate/public/assets/lessons/core/aws-vpc-guide.md +145 -0
  20. package/BlueprintTemplate/public/assets/lessons/groups/aws-application-integration-overview.md +194 -0
  21. package/BlueprintTemplate/public/assets/lessons/groups/aws-compute-architecture-overview.md +187 -0
  22. package/BlueprintTemplate/public/assets/lessons/groups/aws-database-architecture.md +104 -0
  23. package/BlueprintTemplate/public/assets/lessons/groups/aws-networking-architecture.md +97 -0
  24. package/BlueprintTemplate/public/assets/lessons/groups/aws-security-architecture.md +88 -0
  25. package/BlueprintTemplate/public/assets/lessons/groups/aws-storage-architecture.md +116 -0
  26. package/BlueprintTemplate/public/assets/lessons/index.json +202 -0
  27. package/BlueprintTemplate/public/assets/lessons/theory/aws-caching-strategy.md +61 -0
  28. package/BlueprintTemplate/public/assets/lessons/theory/aws-disaster-recovery-strategies.md +117 -0
  29. package/BlueprintTemplate/public/assets/lessons/theory/aws-event-driven-architecture.md +77 -0
  30. package/BlueprintTemplate/public/assets/lessons/theory/aws-ha-ft-scalability.md +98 -0
  31. package/BlueprintTemplate/public/assets/lessons/theory/aws-hybrid-cloud.md +82 -0
  32. package/BlueprintTemplate/public/assets/lessons/theory/aws-microservices-vs-monolithic.md +77 -0
  33. package/BlueprintTemplate/public/assets/lessons/theory/aws-well-architected-framework.md +174 -0
  34. package/BlueprintTemplate/public/favicon.ico +0 -0
  35. package/BlueprintTemplate/public/robots.txt +23 -0
  36. package/BlueprintTemplate/public/sitemap.xml +11 -0
  37. package/BlueprintTemplate/src/app/app.config.server.ts +12 -0
  38. package/BlueprintTemplate/src/app/app.config.ts +16 -0
  39. package/BlueprintTemplate/src/app/app.html +36 -0
  40. package/BlueprintTemplate/src/app/app.routes.server.ts +8 -0
  41. package/BlueprintTemplate/src/app/app.routes.ts +11 -0
  42. package/BlueprintTemplate/src/app/app.scss +71 -0
  43. package/BlueprintTemplate/src/app/app.ts +50 -0
  44. package/BlueprintTemplate/src/app/core/models/lesson.model.ts +8 -0
  45. package/BlueprintTemplate/src/app/core/services/lesson.service.ts +46 -0
  46. package/BlueprintTemplate/src/app/core/services/seo.service.ts +68 -0
  47. package/BlueprintTemplate/src/app/features/about/about.html +14 -0
  48. package/BlueprintTemplate/src/app/features/about/about.scss +4 -0
  49. package/BlueprintTemplate/src/app/features/about/about.ts +23 -0
  50. package/BlueprintTemplate/src/app/features/lesson-detail/lesson-detail.html +4 -0
  51. package/BlueprintTemplate/src/app/features/lesson-detail/lesson-detail.scss +0 -0
  52. package/BlueprintTemplate/src/app/features/lesson-detail/lesson-detail.ts +55 -0
  53. package/BlueprintTemplate/src/app/features/lesson-list/lesson-list.html +13 -0
  54. package/BlueprintTemplate/src/app/features/lesson-list/lesson-list.scss +0 -0
  55. package/BlueprintTemplate/src/app/features/lesson-list/lesson-list.ts +59 -0
  56. package/BlueprintTemplate/src/app/shared/facades/event-abstract.facade.ts +18 -0
  57. package/BlueprintTemplate/src/app/shared/facades/pages.facade.ts +55 -0
  58. package/BlueprintTemplate/src/app/shared/utils/common.utils.ts +3 -0
  59. package/BlueprintTemplate/src/index.html +35 -0
  60. package/BlueprintTemplate/src/main.server.ts.bak +7 -0
  61. package/BlueprintTemplate/src/main.ts +6 -0
  62. package/BlueprintTemplate/src/server.ts.bak +68 -0
  63. package/BlueprintTemplate/src/styles.scss +36 -0
  64. package/BlueprintTemplate/tsconfig.app.json +17 -0
  65. package/BlueprintTemplate/tsconfig.json +34 -0
  66. package/README.md +22 -0
  67. package/create.js +92 -0
  68. package/package.json +21 -0
@@ -0,0 +1,16 @@
1
+ import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
2
+ import { provideRouter } from '@angular/router';
3
+ import { provideMarkdown } from 'ngx-markdown';
4
+ import { routes } from './app.routes';
5
+ import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
6
+ import { provideHttpClient } from '@angular/common/http';
7
+
8
+ export const appConfig: ApplicationConfig = {
9
+ providers: [
10
+ provideBrowserGlobalErrorListeners(),
11
+ provideZoneChangeDetection({ eventCoalescing: true }),
12
+ provideRouter(routes), provideClientHydration(withEventReplay()),
13
+ provideMarkdown(),
14
+ provideHttpClient()
15
+ ]
16
+ };
@@ -0,0 +1,36 @@
1
+ <app-menu
2
+ [menuItems]="menuItems"
3
+ [logoUrl]="logoUrl"
4
+ [logoRouter]="logoRouter"
5
+ [direction]="menuDirection">
6
+ </app-menu>
7
+
8
+ <div class="main-container">
9
+
10
+ <!-- Loading -->
11
+ <div *ngIf="(pageStatus$ | async) === 'loading'" class="loading-wrapper main-page">
12
+ <app-loading></app-loading>
13
+ </div>
14
+
15
+ <!-- Ready -->
16
+ <div *ngIf="(pageStatus$ | async) === 'ready'" class="content main-page">
17
+ <router-outlet></router-outlet>
18
+ </div>
19
+
20
+ <!-- Error -->
21
+ <div *ngIf="(pageStatus$ | async) === 'error'" class="error-page main-page">
22
+ <div class="page-content">
23
+ Error: {{ (errorDetails$ | async)?.message }}
24
+ </div>
25
+ </div>
26
+
27
+ <app-footer
28
+ productName="AWS Edu"
29
+ tagline="Build practical cloud & software knowledge."
30
+ companyName="TruongAn"
31
+ [quickLinks]="footerLinks"
32
+ [contact]="footerContact">
33
+ </app-footer>
34
+
35
+ <app-scroll-to-top></app-scroll-to-top>
36
+ </div>
@@ -0,0 +1,8 @@
1
+ import { RenderMode, ServerRoute } from '@angular/ssr';
2
+
3
+ export const serverRoutes: ServerRoute[] = [
4
+ {
5
+ path: '**',
6
+ renderMode: RenderMode.Prerender
7
+ }
8
+ ];
@@ -0,0 +1,11 @@
1
+ import { Routes } from '@angular/router';
2
+ import { About } from './features/about/about';
3
+ import { LessonDetail } from './features/lesson-detail/lesson-detail';
4
+ import { LessonList } from './features/lesson-list/lesson-list';
5
+
6
+ export const routes: Routes = [
7
+ { path: '', component: LessonList },
8
+ { path: 'about', component: About },
9
+ { path: 'lessons/:id', component: LessonDetail },
10
+ { path: '**', redirectTo: '' }
11
+ ];
@@ -0,0 +1,71 @@
1
+ :host {
2
+ display: flex;
3
+ width: 100%;
4
+ flex-direction: column;
5
+ margin: 0;
6
+ height: 100vh; /* Full viewport height */
7
+ }
8
+
9
+ app-menu {
10
+ position: sticky; /* Make the menu stick to the top without overlaying content */
11
+ top: 0;
12
+ left: 0;
13
+ width: 100%; /* Full width */
14
+ background-color: #fff;
15
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* Optional: shadow for separation */
16
+ z-index: 2; /* Ensure the menu stays above other content */
17
+ }
18
+
19
+ .main-container {
20
+ display: flex;
21
+ flex-direction: column;
22
+ flex-grow: 1;
23
+ overflow-y: auto; /* Allow scrolling for the main content */
24
+ }
25
+
26
+ .content,
27
+ .loading-wrapper,
28
+ .error-page {
29
+ flex: 1;
30
+ display: flex;
31
+ flex-direction: column;
32
+ width: 100%;
33
+ }
34
+
35
+ app-footer {
36
+ flex-shrink: 0; /* Prevent footer from shrinking */
37
+ }
38
+
39
+ .loading-wrapper {
40
+ display: flex;
41
+ flex-direction: column;
42
+ width: 100%;
43
+ overflow-y: auto; /* Scroll vertically when content overflows */
44
+ overflow-x: hidden; /* Hide horizontal scrollbar */
45
+
46
+ /* Custom Scrollbar for WebKit-based Browsers (Chrome, Safari) */
47
+ &::-webkit-scrollbar {
48
+ width: 6px; /* Thinner scrollbar */
49
+ }
50
+
51
+ &::-webkit-scrollbar-track {
52
+ background: #f1f1f1; /* Light gray background for the track */
53
+ border-radius: 10px; /* Rounded corners for the track */
54
+ box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.1); /* Subtle shadow for a polished look */
55
+ }
56
+
57
+ &::-webkit-scrollbar-thumb {
58
+ background: #888; /* Neutral gray color for the thumb */
59
+ border-radius: 10px; /* Rounded corners for the thumb */
60
+ border: 2px solid #f1f1f1; /* Creates a gap between the thumb and track */
61
+ transition: background 0.3s; /* Smooth transition for hover effect */
62
+ }
63
+
64
+ &::-webkit-scrollbar-thumb:hover {
65
+ background: #555; /* Darker gray for the thumb on hover */
66
+ }
67
+
68
+ /* For Firefox (thinner scrollbar) */
69
+ scrollbar-width: thin; /* Makes the scrollbar thin in Firefox */
70
+ scrollbar-color: #888 #f1f1f1; /* Sets thumb and track colors in Firefox */
71
+ }
@@ -0,0 +1,50 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import { Component, signal } from '@angular/core';
3
+ import { RouterOutlet } from '@angular/router';
4
+ import { FooterComponent, FooterLink, LoadingComponent, MenuComponent, MenuDirection, ScrollToTopComponent } from 'angular-dumb-lib';
5
+ import { Observable } from 'rxjs';
6
+ import { PageError, PagesFacade, PageStatus } from './shared/facades/pages.facade';
7
+
8
+ @Component({
9
+ selector: 'app-root',
10
+ imports: [
11
+ RouterOutlet,
12
+ MenuComponent,
13
+ ScrollToTopComponent,
14
+ FooterComponent,
15
+ CommonModule,
16
+ LoadingComponent
17
+ ],
18
+ templateUrl: './app.html',
19
+ styleUrl: './app.scss'
20
+ })
21
+ export class App {
22
+ pageStatus$: Observable<PageStatus>;
23
+ errorDetails$: Observable<PageError | null>;
24
+
25
+ public menuDirection: MenuDirection = MenuDirection.horizontal;
26
+ public logoUrl: string = 'favicon.ico';
27
+ public logoRouter: string = '/';
28
+ public menuItems = [
29
+ { label: 'Lessons', value: 'lessons', route: '/' },
30
+ { label: 'About', value: 'about', route: '/about' }
31
+ ];
32
+
33
+ constructor(public pagesFacade: PagesFacade) {
34
+ this.pageStatus$ = pagesFacade.pageStatus;
35
+ this.errorDetails$ = pagesFacade.errorDetails;
36
+ }
37
+
38
+ protected readonly title = signal('aws-learning');
39
+ footerLinks: FooterLink[] = [
40
+ { label: 'Lessons', url: '/lessons' },
41
+ { label: 'About', url: '/about' },
42
+ ];
43
+
44
+ footerContact = {
45
+ email: 'contact@truongan.vn',
46
+ phone: '+84901234567',
47
+ facebook: 'https://facebook.com/truongan',
48
+ website: 'https://truongan.vn',
49
+ };
50
+ }
@@ -0,0 +1,8 @@
1
+ export interface Lesson {
2
+ id: string;
3
+ title: string;
4
+ category: string;
5
+ service: string;
6
+ level: string;
7
+ file: string;
8
+ }
@@ -0,0 +1,46 @@
1
+ import { HttpClient } from '@angular/common/http';
2
+ import { Injectable } from '@angular/core';
3
+ import { Lesson } from '../models/lesson.model';
4
+ import { Observable, map } from 'rxjs';
5
+
6
+ @Injectable({ providedIn: 'root' })
7
+ export class LessonService {
8
+ private basePath = 'assets/lessons';
9
+
10
+ constructor(private http: HttpClient) {}
11
+
12
+ getLessons(): Observable<Lesson[]> {
13
+ return this.http.get<Lesson[]>(`${this.basePath}/index.json`);
14
+ }
15
+
16
+ getLessonById(id: string): Observable<Lesson | undefined> {
17
+ return this.getLessons().pipe(
18
+ map(list => list.find(x => x.id === id))
19
+ );
20
+ }
21
+
22
+ getLessonContent(path: string): Observable<string> {
23
+ return this.http.get(`${this.basePath}/${path}`, { responseType: 'text' });
24
+ }
25
+
26
+ loadMarkdown(lesson: any) {
27
+ const path = `assets/lessons/${lesson.category}/${lesson.file}`;
28
+ return this.http.get(path, { responseType: 'text' });
29
+ }
30
+
31
+ // Lấy lessons và nhóm theo category
32
+ getLessonsGroupedByCategory(): Observable<{ [category: string]: Lesson[] }> {
33
+ return this.getLessons().pipe(
34
+ map(lessons =>
35
+ lessons.reduce((groups, lesson) => {
36
+ const category = lesson.category || 'Uncategorized';
37
+ if (!groups[category]) {
38
+ groups[category] = [];
39
+ }
40
+ groups[category].push(lesson);
41
+ return groups;
42
+ }, {} as { [category: string]: Lesson[] })
43
+ )
44
+ );
45
+ }
46
+ }
@@ -0,0 +1,68 @@
1
+ // src/app/core/seo/seo.service.ts
2
+ import { Injectable } from '@angular/core';
3
+ import { Meta, Title } from '@angular/platform-browser';
4
+
5
+ export interface SeoConfig {
6
+ title?: string;
7
+ description?: string;
8
+ keywords?: string;
9
+ canonical?: string;
10
+ image?: string;
11
+ }
12
+
13
+ @Injectable({ providedIn: 'root' })
14
+ export class SeoService {
15
+
16
+ constructor(
17
+ private titleService: Title,
18
+ private metaService: Meta
19
+ ) {}
20
+
21
+ update(config: SeoConfig) {
22
+ if (config.title) {
23
+ this.titleService.setTitle(config.title);
24
+ this.metaService.updateTag({ property: 'og:title', content: config.title });
25
+ }
26
+
27
+ if (config.description) {
28
+ this.metaService.updateTag({
29
+ name: 'description',
30
+ content: config.description
31
+ });
32
+ this.metaService.updateTag({
33
+ property: 'og:description',
34
+ content: config.description
35
+ });
36
+ }
37
+
38
+ if (config.keywords) {
39
+ this.metaService.updateTag({
40
+ name: 'keywords',
41
+ content: config.keywords
42
+ });
43
+ }
44
+
45
+ if (config.image) {
46
+ this.metaService.updateTag({
47
+ property: 'og:image',
48
+ content: config.image
49
+ });
50
+ }
51
+
52
+ if (config.canonical) {
53
+ this.setCanonical(config.canonical);
54
+ }
55
+ }
56
+
57
+ private setCanonical(url: string) {
58
+ let link: HTMLLinkElement =
59
+ document.querySelector("link[rel='canonical']") || document.createElement('link');
60
+
61
+ link.setAttribute('rel', 'canonical');
62
+ link.setAttribute('href', url);
63
+
64
+ if (!link.parentNode) {
65
+ document.head.appendChild(link);
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,14 @@
1
+ <div class="page-content">
2
+ <h1>About This Site</h1>
3
+
4
+ <p>
5
+ Chào mừng bạn đến với trang web của tôi!
6
+ Đây là nơi tôi chia sẻ kiến thức, dự án và các nội dung hữu ích.
7
+ Nếu bạn thấy trang này hữu ích, bạn có thể ủng hộ tôi một ly cà phê ☕.
8
+ </p>
9
+
10
+ <div class="buy-coffee">
11
+ <p>Scan QR để ủng hộ:</p>
12
+ <img src="https://truongdx8-private-info.s3.ap-southeast-1.amazonaws.com/QR.jpg" alt="Buy me a coffee QR code" />
13
+ </div>
14
+ </div>
@@ -0,0 +1,4 @@
1
+ .buy-coffee img {
2
+ width: 300px; /* hoặc % như 50% */
3
+ object-fit: contain; /* giữ ảnh không bị méo */
4
+ }
@@ -0,0 +1,23 @@
1
+ import { Component } from '@angular/core';
2
+ import { SeoService } from '../../core/services/seo.service';
3
+
4
+ @Component({
5
+ selector: 'app-about',
6
+ imports: [],
7
+ templateUrl: './about.html',
8
+ styleUrl: './about.scss',
9
+ })
10
+ export class About {
11
+ constructor(
12
+ private seo: SeoService
13
+ ) {}
14
+
15
+ ngOnInit() {
16
+ this.seo.update({
17
+ title: 'Giới thiệu | AWS Edu',
18
+ description: 'AWS Edu là website chia sẻ kiến thức AWS, Cloud, DevOps và kinh nghiệm thực tế cho developer.',
19
+ canonical: 'https://awsedu.io.vn/about',
20
+ image: 'https://awsedu.io.vn/assets/og/about.png'
21
+ });
22
+ }
23
+ }
@@ -0,0 +1,4 @@
1
+ <div class="page-content">
2
+ <app-button [config]="backHomeConfig" (clickEvent)="backHome()"></app-button>
3
+ <markdown [data]="content"></markdown>
4
+ </div>
@@ -0,0 +1,55 @@
1
+ import { CommonModule } from '@angular/common';
2
+ import { Component } from '@angular/core';
3
+ import { ActivatedRoute, Router } from '@angular/router';
4
+ import { LessonService } from '../../core/services/lesson.service';
5
+ import { switchMap, catchError, of } from 'rxjs';
6
+ import { MarkdownModule } from 'ngx-markdown';
7
+ import { ButtonComponent, IButtonConfig } from 'angular-dumb-lib';
8
+ import { PageError, PagesFacade } from '../../shared/facades/pages.facade';
9
+
10
+ @Component({
11
+ selector: 'app-lesson-detail',
12
+ imports: [CommonModule, MarkdownModule, ButtonComponent],
13
+ templateUrl: './lesson-detail.html',
14
+ styleUrl: './lesson-detail.scss',
15
+ })
16
+ export class LessonDetail {
17
+ content = '';
18
+ backHomeConfig: IButtonConfig = {
19
+ iconValue: 'arrow_left',
20
+ }
21
+
22
+ constructor(
23
+ private router: Router,
24
+ private route: ActivatedRoute,
25
+ private lessonService: LessonService,
26
+ public pagesFacade: PagesFacade
27
+ ) {}
28
+
29
+ ngOnInit(): void {
30
+ // mark page as loading
31
+ // this.pagesFacade.markAsLoading();
32
+ const id = this.route.snapshot.paramMap.get('id')!;
33
+
34
+ this.lessonService.getLessonById(id).pipe(
35
+ switchMap(lesson => this.lessonService.loadMarkdown(lesson)),
36
+ catchError(err => {
37
+ const error: PageError = {
38
+ message: 'Failed to load lesson content',
39
+ details: err
40
+ };
41
+ this.pagesFacade.markAsError(error);
42
+ return of(null); // stop the stream
43
+ })
44
+ ).subscribe(md => {
45
+ if (md !== null) {
46
+ this.content = md;
47
+ this.pagesFacade.markAsReady();
48
+ }
49
+ });
50
+ }
51
+
52
+ backHome() {
53
+ this.router.navigate(['/']);
54
+ }
55
+ }
@@ -0,0 +1,13 @@
1
+ <div class="page-content">
2
+ <h1>Lessons</h1>
3
+ <div *ngFor="let category of lessonsByCategory | keyvalue">
4
+ <h3>{{ category.key | titlecase }}</h3>
5
+ <ul>
6
+ <li *ngFor="let lesson of category.value">
7
+ <a (click)="goToLesson(lesson.id)">
8
+ {{ lesson.title }}
9
+ </a>
10
+ </li>
11
+ </ul>
12
+ </div>
13
+ </div>
@@ -0,0 +1,59 @@
1
+ import { Component } from '@angular/core';
2
+ import { Lesson } from '../../core/models/lesson.model';
3
+ import { LessonService } from '../../core/services/lesson.service';
4
+ import { Router } from '@angular/router';
5
+ import { CommonModule } from '@angular/common';
6
+ import { PagesFacade } from '../../shared/facades/pages.facade';
7
+ import { catchError, of } from 'rxjs';
8
+ import { SeoService } from '../../core/services/seo.service';
9
+
10
+ @Component({
11
+ selector: 'app-lesson-list',
12
+ imports: [CommonModule],
13
+ templateUrl: './lesson-list.html',
14
+ styleUrl: './lesson-list.scss',
15
+ })
16
+ export class LessonList {
17
+ lessonsByCategory: { [category: string]: Lesson[] } = {};
18
+
19
+ constructor(
20
+ private lessonService: LessonService,
21
+ private router: Router,
22
+ public pagesFacade: PagesFacade,
23
+ private seo: SeoService
24
+ ) {}
25
+
26
+ ngOnInit(): void {
27
+ this.seo.update({
28
+ title: 'AWS Edu – Học AWS & Cloud cho Developer',
29
+ description: 'Nền tảng học AWS, Cloud, DevOps từ cơ bản đến thực chiến dành cho developer.',
30
+ canonical: 'https://awsedu.io.vn/',
31
+ });
32
+
33
+ // mark page as loading
34
+ this.pagesFacade.markAsLoading();
35
+ this.lessonService.getLessonsGroupedByCategory()
36
+ .pipe(
37
+ catchError(err => {
38
+ // mark page as error with message
39
+ this.pagesFacade.markAsError({
40
+ message: 'Failed to load lessons',
41
+ details: err
42
+ });
43
+ return of(null); // return empty observable to stop the stream
44
+ })
45
+ )
46
+ .subscribe(data => {
47
+ if (data) {
48
+ this.lessonsByCategory = data;
49
+ // mark page as ready
50
+ this.pagesFacade.markAsReady();
51
+ }
52
+ }
53
+ );
54
+ }
55
+
56
+ goToLesson(id: string): void {
57
+ this.router.navigate(['/lessons', id]);
58
+ }
59
+ }
@@ -0,0 +1,18 @@
1
+ import { Injectable } from "@angular/core";
2
+ import { isDefined } from "../utils/common.utils";
3
+
4
+ @Injectable()
5
+ export abstract class EventAbstractFacade<T> {
6
+ private readonly _eventsDictionary = new Map<string, (...args: T[] | unknown[]) => void>();
7
+
8
+ addEventListener(name: string, handler: (...argument: T[] | unknown[]) => void): void {
9
+ this._eventsDictionary.set(name, handler);
10
+ }
11
+
12
+ dispatch(name: string, ...args: T[] | unknown[]): void {
13
+ const handler = this._eventsDictionary.get(name);
14
+ if(isDefined(handler)) {
15
+ handler(...args);
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,55 @@
1
+ import { Injectable } from "@angular/core";
2
+ import { EventAbstractFacade } from "./event-abstract.facade";
3
+ import { IModalEvent } from "angular-dumb-lib";
4
+ import { BehaviorSubject, Observable } from "rxjs";
5
+
6
+ export interface ILoadingEvent {
7
+ text: string;
8
+ position?: 'top' | 'bottom';
9
+ }
10
+
11
+ export interface PageError {
12
+ message: string; // user-friendly message
13
+ code?: number; // optional error code
14
+ details?: any; // optional raw error or debug info
15
+ }
16
+
17
+ export type PageEventDetails = IModalEvent | ILoadingEvent | unknown;
18
+
19
+ export type PageStatus = 'loading' | 'ready' | 'error';
20
+
21
+ export interface IPageEvent {
22
+ type?: 'start' | 'end';
23
+ details?: PageEventDetails;
24
+ }
25
+
26
+ @Injectable({
27
+ providedIn: 'root'
28
+ })
29
+ export class PagesFacade extends EventAbstractFacade<IPageEvent> {
30
+ private readonly _pageStatus = new BehaviorSubject<PageStatus>('ready');
31
+ private readonly _errorDetails = new BehaviorSubject<PageError | null>(null);
32
+
33
+ get pageStatus(): Observable<PageStatus> {
34
+ return this._pageStatus.asObservable();
35
+ }
36
+
37
+ get errorDetails(): Observable<PageError | null> {
38
+ return this._errorDetails.asObservable();
39
+ }
40
+
41
+ markAsReady(): void {
42
+ this._pageStatus.next('ready');
43
+ this._errorDetails.next(null); // clear previous error
44
+ }
45
+
46
+ markAsLoading(): void {
47
+ this._pageStatus.next('loading');
48
+ this._errorDetails.next(null); // clear previous error
49
+ }
50
+
51
+ markAsError(details: PageError): void {
52
+ this._pageStatus.next('error');
53
+ this._errorDetails.next(details);
54
+ }
55
+ }
@@ -0,0 +1,3 @@
1
+ export function isDefined<T>(obj: T | null | undefined): obj is T {
2
+ return obj !== undefined && obj !== null;
3
+ }
@@ -0,0 +1,35 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>AWS Edu</title>
6
+ <base href="/">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+
9
+ <!-- SEO cơ bản -->
10
+ <meta name="description" content="AWS Edu – Cổng học tập và thực hành AWS cho người mới bắt đầu và nâng cao.">
11
+ <meta name="robots" content="index, follow">
12
+
13
+ <!-- Open Graph (chia sẻ lên mạng xã hội đẹp hơn) -->
14
+ <meta property="og:title" content="AWS Edu – Học tập và Thực hành AWS">
15
+ <meta property="og:description" content="Học AWS từ cơ bản đến nâng cao với AWS Edu. Tài liệu, hướng dẫn, bài tập thực hành.">
16
+ <meta property="og:type" content="website">
17
+ <!-- <meta property="og:url" content="https://yourdomain.com/">
18
+ <meta property="og:image" content="https://yourdomain.com/og-image.png"> -->
19
+
20
+ <!-- Google tag (gtag.js) -->
21
+ <script async src="https://www.googletagmanager.com/gtag/js?id=G-72HLJXKSY8"></script>
22
+ <script>
23
+ window.dataLayer = window.dataLayer || [];
24
+ function gtag(){dataLayer.push(arguments);}
25
+ gtag('js', new Date());
26
+ gtag('config', 'G-72HLJXKSY8');
27
+ </script>
28
+
29
+ <link rel="icon" href="/favicon.ico" />
30
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
31
+ </head>
32
+ <body>
33
+ <app-root></app-root>
34
+ </body>
35
+ </html>
@@ -0,0 +1,7 @@
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { App } from './app/app';
3
+ import { config } from './app/app.config.server';
4
+
5
+ const bootstrap = () => bootstrapApplication(App, config);
6
+
7
+ export default bootstrap;
@@ -0,0 +1,6 @@
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { appConfig } from './app/app.config';
3
+ import { App } from './app/app';
4
+
5
+ bootstrapApplication(App, appConfig)
6
+ .catch((err) => console.error(err));