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.
- package/BlueprintTemplate/.editorconfig +17 -0
- package/BlueprintTemplate/.gitlab-ci.yml +34 -0
- package/BlueprintTemplate/DESIGN.txt +37 -0
- package/BlueprintTemplate/README.md +78 -0
- package/BlueprintTemplate/angular.json +101 -0
- package/BlueprintTemplate/gitignore +42 -0
- package/BlueprintTemplate/package.json +58 -0
- package/BlueprintTemplate/public/assets/lessons/aws/aws-overview.md +139 -0
- package/BlueprintTemplate/public/assets/lessons/core/aws-aurora-guide.md +181 -0
- package/BlueprintTemplate/public/assets/lessons/core/aws-dynamodb-guide.md +139 -0
- package/BlueprintTemplate/public/assets/lessons/core/aws-ec2-guide.md +152 -0
- package/BlueprintTemplate/public/assets/lessons/core/aws-eventbridge-guide.md +152 -0
- package/BlueprintTemplate/public/assets/lessons/core/aws-iam-guide.md +132 -0
- package/BlueprintTemplate/public/assets/lessons/core/aws-lambda-guide.md +129 -0
- package/BlueprintTemplate/public/assets/lessons/core/aws-rds-guide.md +193 -0
- package/BlueprintTemplate/public/assets/lessons/core/aws-s3-guide.md +158 -0
- package/BlueprintTemplate/public/assets/lessons/core/aws-sns-guide.md +163 -0
- package/BlueprintTemplate/public/assets/lessons/core/aws-sqs-guide.md +173 -0
- package/BlueprintTemplate/public/assets/lessons/core/aws-vpc-guide.md +145 -0
- package/BlueprintTemplate/public/assets/lessons/groups/aws-application-integration-overview.md +194 -0
- package/BlueprintTemplate/public/assets/lessons/groups/aws-compute-architecture-overview.md +187 -0
- package/BlueprintTemplate/public/assets/lessons/groups/aws-database-architecture.md +104 -0
- package/BlueprintTemplate/public/assets/lessons/groups/aws-networking-architecture.md +97 -0
- package/BlueprintTemplate/public/assets/lessons/groups/aws-security-architecture.md +88 -0
- package/BlueprintTemplate/public/assets/lessons/groups/aws-storage-architecture.md +116 -0
- package/BlueprintTemplate/public/assets/lessons/index.json +202 -0
- package/BlueprintTemplate/public/assets/lessons/theory/aws-caching-strategy.md +61 -0
- package/BlueprintTemplate/public/assets/lessons/theory/aws-disaster-recovery-strategies.md +117 -0
- package/BlueprintTemplate/public/assets/lessons/theory/aws-event-driven-architecture.md +77 -0
- package/BlueprintTemplate/public/assets/lessons/theory/aws-ha-ft-scalability.md +98 -0
- package/BlueprintTemplate/public/assets/lessons/theory/aws-hybrid-cloud.md +82 -0
- package/BlueprintTemplate/public/assets/lessons/theory/aws-microservices-vs-monolithic.md +77 -0
- package/BlueprintTemplate/public/assets/lessons/theory/aws-well-architected-framework.md +174 -0
- package/BlueprintTemplate/public/favicon.ico +0 -0
- package/BlueprintTemplate/public/robots.txt +23 -0
- package/BlueprintTemplate/public/sitemap.xml +11 -0
- package/BlueprintTemplate/src/app/app.config.server.ts +12 -0
- package/BlueprintTemplate/src/app/app.config.ts +16 -0
- package/BlueprintTemplate/src/app/app.html +36 -0
- package/BlueprintTemplate/src/app/app.routes.server.ts +8 -0
- package/BlueprintTemplate/src/app/app.routes.ts +11 -0
- package/BlueprintTemplate/src/app/app.scss +71 -0
- package/BlueprintTemplate/src/app/app.ts +50 -0
- package/BlueprintTemplate/src/app/core/models/lesson.model.ts +8 -0
- package/BlueprintTemplate/src/app/core/services/lesson.service.ts +46 -0
- package/BlueprintTemplate/src/app/core/services/seo.service.ts +68 -0
- package/BlueprintTemplate/src/app/features/about/about.html +14 -0
- package/BlueprintTemplate/src/app/features/about/about.scss +4 -0
- package/BlueprintTemplate/src/app/features/about/about.ts +23 -0
- package/BlueprintTemplate/src/app/features/lesson-detail/lesson-detail.html +4 -0
- package/BlueprintTemplate/src/app/features/lesson-detail/lesson-detail.scss +0 -0
- package/BlueprintTemplate/src/app/features/lesson-detail/lesson-detail.ts +55 -0
- package/BlueprintTemplate/src/app/features/lesson-list/lesson-list.html +13 -0
- package/BlueprintTemplate/src/app/features/lesson-list/lesson-list.scss +0 -0
- package/BlueprintTemplate/src/app/features/lesson-list/lesson-list.ts +59 -0
- package/BlueprintTemplate/src/app/shared/facades/event-abstract.facade.ts +18 -0
- package/BlueprintTemplate/src/app/shared/facades/pages.facade.ts +55 -0
- package/BlueprintTemplate/src/app/shared/utils/common.utils.ts +3 -0
- package/BlueprintTemplate/src/index.html +35 -0
- package/BlueprintTemplate/src/main.server.ts.bak +7 -0
- package/BlueprintTemplate/src/main.ts +6 -0
- package/BlueprintTemplate/src/server.ts.bak +68 -0
- package/BlueprintTemplate/src/styles.scss +36 -0
- package/BlueprintTemplate/tsconfig.app.json +17 -0
- package/BlueprintTemplate/tsconfig.json +34 -0
- package/README.md +22 -0
- package/create.js +92 -0
- 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,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,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,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
|
+
}
|
|
File without changes
|
|
@@ -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>
|
|
File without changes
|
|
@@ -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,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>
|