Commit 38c41597 authored by Trần Anh Phú's avatar Trần Anh Phú

Initial commit

parent 9aea5fe6
Pipeline #30596 canceled with stages
......@@ -20,20 +20,24 @@
"outputPath": "dist/meu-template-csr",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/assets",
{
"glob": "**/*",
"input": "public"
"input": "src/assets/",
"ignore": ["**/*.scss"],
"output": "/assets/"
},
{
"glob": "**/*",
"input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
"output": "/assets/"
}
],
"styles": [
"src/styles.scss"
],
"styles": ["src/styles.scss"],
"scripts": []
},
"configurations": {
......@@ -78,10 +82,7 @@
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
......@@ -90,13 +91,14 @@
"input": "public"
}
],
"styles": [
"src/styles.scss"
],
"styles": ["src/styles.scss"],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": "eb0735f0-7ed8-4a66-96fb-3723bd4a5d66"
}
}
......@@ -5,19 +5,29 @@
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"build:development": "node --max-old-space-size=8000 ./node_modules/@angular/cli/bin/ng build --configuration=development",
"build:production": "node --max-old-space-size=8000 ./node_modules/@angular/cli/bin/ng build --configuration=production",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"engines": {
"node": "20.15.1"
},
"private": true,
"dependencies": {
"@angular/animations": "^18.0.0",
"@angular/cdk": "^18.2.8",
"@angular/common": "^18.0.0",
"@angular/compiler": "^18.0.0",
"@angular/core": "^18.0.0",
"@angular/forms": "^18.0.0",
"@angular/platform-browser": "^18.0.0",
"@angular/platform-browser-dynamic": "^18.0.0",
"@angular/platform-server": "^18.2.8",
"@angular/router": "^18.0.0",
"@ngrx/signals": "^18.1.0",
"express": "^4.21.1",
"ng-zorro-antd": "^18.1.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.3"
......@@ -27,12 +37,15 @@
"@angular/cli": "^18.0.3",
"@angular/compiler-cli": "^18.0.0",
"@types/jasmine": "~5.1.0",
"autoprefixer": "^10.4.20",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"typescript": "~5.4.2"
}
}
import { Route } from '@angular/router';
import { AdminLayoutComponent } from './layout/feature/ui/layout.component';
const ADMIN_ROUTES: Route[] = [
{
path: '',
component: AdminLayoutComponent,
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'configuration',
},
],
},
];
export default ADMIN_ROUTES;
import { RouterLink, RouterModule, RouterOutlet } from '@angular/router';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { NzLayoutModule } from 'ng-zorro-antd/layout';
import { CommonModule } from '@angular/common';
@Component({
selector: 'admin-layout',
standalone: true,
imports: [
RouterOutlet,
NzLayoutModule,
RouterLink,
RouterModule,
CommonModule,
],
template: ` <nz-content>
<div>
<router-outlet></router-outlet>
</div>
</nz-content>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AdminLayoutComponent implements OnInit {
constructor() {}
ngOnInit(): void {}
}
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NzCarouselModule } from 'ng-zorro-antd/carousel';
@Component({
selector: 'meu-home',
standalone: true,
imports: [NzCarouselModule],
templateUrl: './home.component.html',
styleUrl: './home.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HomeComponent {}
import { Route } from '@angular/router';
const HOME_ROUTES: Route[] = [
{
path: '',
loadComponent: () =>
import('./feature/home.component').then((m) => m.HomeComponent),
},
];
export default HOME_ROUTES;
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class AdminGuard implements CanActivate {
constructor() {}
canActivate(): boolean {
return true;
}
}
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor() {}
canActivate(): boolean {
return true;
}
}
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'meu-login',
standalone: true,
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LoginComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
import { Route } from '@angular/router';
const LOGIN_ROUTES: Route[] = [
{
path: '',
loadComponent: () =>
import('./feature/login.component').then((m) => m.LoginComponent),
},
];
export default LOGIN_ROUTES;
import { shellConfig } from './shell.config';
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering()],
};
export const config = mergeApplicationConfig(shellConfig, serverConfig);
import {
ApplicationConfig,
provideZoneChangeDetection,
importProvidersFrom,
} from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideNzIcons } from 'ng-zorro-antd/icon';
import { en_US, provideNzI18n } from 'ng-zorro-antd/i18n';
import { registerLocaleData } from '@angular/common';
import en from '@angular/common/locales/en';
import { FormsModule } from '@angular/forms';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { interceptors } from '../../shared/utils/interceptor/interceptor';
import { shellRoutes } from './shell.routes';
registerLocaleData(en);
export const shellConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(shellRoutes, withComponentInputBinding()),
provideAnimations(),
provideHttpClient(withInterceptors([interceptors])),
provideNzI18n(en_US),
importProvidersFrom(FormsModule),
provideAnimationsAsync(),
provideHttpClient(),
],
};
import { Routes } from '@angular/router';
import { LayoutComponent } from '../ui/layout.component';
import { LoginComponent } from '../../+login/feature/login.component';
import { AuthGuard } from '../../+login/data-access/guard/auth.guard';
import { AdminGuard } from '../../+login/data-access/guard/admin.guard';
export const shellRoutes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{
path: '',
pathMatch: 'full',
redirectTo: 'home',
},
{
path: 'home',
canActivate: [], //not have guard yet, set up later
loadChildren: () => import('../../+home/home.routes'),
},
],
},
{
path: 'admin',
children: [
{
path: '',
canActivate: [AdminGuard],
loadChildren: () => import('../../+admin/admin.routes'),
},
],
},
{
path: 'login',
component: LoginComponent,
canActivate: [AuthGuard],
},
];
<footer>
<h1>
It is a footer
</h1>
</footer>
\ No newline at end of file
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
@Component({
selector: 'meu-footer',
standalone: true,
imports: [CommonModule],
templateUrl: './footer.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FooterComponent implements OnInit {
ngOnInit(): void {}
}
<header>
<h1>
It is a header
</h1>
</header>
\ No newline at end of file
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
@Component({
selector: 'meu-header',
standalone: true,
imports: [CommonModule],
templateUrl: './header.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderComponent implements OnInit {
ngOnInit(): void {}
}
import { CommonModule, DatePipe } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
OnInit,
signal,
} from '@angular/core';
import { RouterModule } from '@angular/router';
import { NzButtonModule } from 'ng-zorro-antd/button';
import { NzIconModule } from 'ng-zorro-antd/icon';
import { NzToolTipModule } from 'ng-zorro-antd/tooltip';
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { HeaderComponent } from './components/header/feature/header.component';
import { FooterComponent } from './components/footer/feature/footer.component';
@Component({
selector: 'meu-layout',
standalone: true,
imports: [
RouterModule,
DatePipe,
HeaderComponent,
FooterComponent,
NzButtonModule,
NzIconModule,
CommonModule,
NzToolTipModule,
],
template: `
<div class="tw-container">
<meu-header></meu-header>
<div class="tw-relative tw-min-h-[92dvh]">
<router-outlet></router-outlet>
</div>
<meu-footer></meu-footer>
</div>
`,
animations: [],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LayoutComponent implements OnInit {
isSmallScreen = signal(false);
constructor(private breakpointObserver: BreakpointObserver) {}
ngOnInit() {
this.breakpointObserver.observe([Breakpoints.Handset]).subscribe((res) => {
this.isSmallScreen.set(res.matches);
});
if (
typeof window !== 'undefined' &&
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
const root = document.documentElement;
root.style.setProperty('--color-theme', '31 31 31');
root.style.setProperty('--color-paper', '255 255 255');
}
}
}
This diff is collapsed.
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'Meu-template-CSR' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('Meu-template-CSR');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, Meu-template-CSR');
});
});
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterOutlet, CommonModule],
template: `<router-outlet />`,
})
export class AppComponent {
title = 'Meu-template-CSR';
......
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
};
import { Routes } from '@angular/router';
export const routes: Routes = [];
// Allow these characters: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+:Ññíáö;, and white space
export const userNameRegex = '^[0-9a-zA-Z_.+:Ññíáö;, @-]{1,50}$';
export interface ResponseResult<T> {
message: string;
responseData: T | null;
success: boolean;
status: number | null;
violations: T | null;
path: string;
timestamp: number;
}
export interface Rows<T> {
rows: T[];
page: number;
take: number;
itemCount: number;
pageCount: number;
hasPreviousPage: boolean;
hasNextPage: boolean;
}
import { AbstractControl, ValidationErrors } from '@angular/forms';
import { userNameRegex } from '../../data-access/const/user-name-regex.const';
export function validateUsername(
usernameControl: AbstractControl
): ValidationErrors | null {
const value = usernameControl.value;
const userName = new RegExp(userNameRegex);
if (value && !userName.test(value)) {
return {
userNamePattern: true,
};
}
return null;
}
import {
HttpContextToken,
HttpErrorResponse,
HttpEvent,
HttpEventType,
HttpHandlerFn,
HttpInterceptorFn,
HttpRequest,
HttpResponse,
HttpStatusCode,
} from '@angular/common/http';
import { environment } from '../../../../environments/environment.development';
import { catchError, EMPTY, Observable, tap, throwError } from 'rxjs';
import { ResponseResult } from '../../data-access/interface/response.type';
export const SHOW_LOADING = new HttpContextToken<boolean>(() => true);
export const SHOW_API_MESSAGE = new HttpContextToken<boolean>(() => true);
export const SHOW_API_ERROR_MESSAGE = new HttpContextToken<boolean>(() => true);
export const interceptors: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
const showLoading = req.context.get(SHOW_LOADING);
const showApiMessage = req.context.get(SHOW_API_MESSAGE);
const showApiErrorMessage = req.context.get(SHOW_API_ERROR_MESSAGE);
const token = localStorage.getItem('token'); //custom
req = req.clone({
url: !req.url.includes('http') ? environment.API_DOMAIN + req.url : req.url,
setHeaders: {
Authorization: `Bearer ${token ? token : ''}`,
StandardTimeZoneName: `${
Intl.DateTimeFormat().resolvedOptions().timeZone
}`,
},
});
runIf(showLoading, () => {});
return next(req).pipe(
tap({
next: (response) => {
if (response.type === HttpEventType.Response) {
if (response && response.body) {
const apiType = checkApiType(response);
if (apiType === 'success') {
const resp = response.body as ResponseResult<unknown>;
runIf(
showApiMessage && !!resp.message && req.method !== 'GET',
() => {}
);
} else if (apiType === 'error') {
const resp = response.body as ResponseResult<unknown>;
runIf(!resp.success && !!resp.message, () => {});
}
}
}
},
finalize: () => runIf(showLoading, () => {}),
}),
catchError(
(error: HttpErrorResponse, _: Observable<HttpEvent<unknown>>) => {
const apiError = error.error as ResponseResult<unknown> | Blob | null;
if (apiError instanceof Blob) {
apiError.text().then((val) => {
const errors = JSON.parse(val);
runIf(showApiErrorMessage, () => {
// console.log('error: ', val);
});
});
return EMPTY;
}
if (error.status === HttpStatusCode.InternalServerError) {
// console.log('error: InternalServerError');
return EMPTY;
}
// sandbox api
if (error.error === '404 Page Not Found' && showApiErrorMessage) {
// console.log('error: ', error.error);
return EMPTY;
}
// if (error.status === HttpStatusCode.Unauthorized) {
// // authStore.signOut();
// // console.log('error: Unauthorized');
// return EMPTY;
// }
if (error.status === HttpStatusCode.Forbidden) {
//authStore.signOut();
// console.log('error: Forbidden');
return EMPTY;
}
if (!apiError?.message) {
// console.log('error: ???');
return EMPTY;
}
return throwError(() => apiError);
}
)
);
};
function runIf(isShowMessage: boolean, func: () => void) {
if (isShowMessage) {
func();
}
}
function checkApiType(
httpResponse: HttpResponse<unknown>
): 'success' | 'error' {
return Object.hasOwn(httpResponse.body as object, 'res')
? 'error'
: 'success';
}
//input
.ant-input,
.ant-input-affix-wrapper {
@apply tw-text-lg tw-rounded-lg tw-border-grey tw-p-3 tw-w-full tw-flex;
}
.ant-input-focused,
.ant-input:focus {
@apply tw-border-primary tw-shadow-[0_0_0_2px_rgb(153,20,47,0.35)];
}
.ant-input:hover {
@apply tw-border-primary;
}
@layer utilities {
.text-error {
color: #ff4d4f;
}
.required::after {
content: " (*) ";
color: red;
}
.bg-warning {
background-color: orange;
color: white;
border: rgb(218, 141, 0);
}
}
@import "base";
@import "components";
@import "utilities";
export const environment = {
API_DOMAIN: 'http://localhost:5051/api/v1.0/', //custom
};
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/+shell/feature/shell.config.server';
const bootstrap = () => bootstrapApplication(AppComponent, config);
export default bootstrap;
/* You can add global styles to this file, and also import other style files */
//tailwind
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "custom/tailwind";
@import "custom/ant";
//ng-zorro
@import "node_modules/ng-zorro-antd/ng-zorro-antd.min.css";
@layer base {
:root {
//default
--color-primary: 153 20 47;
--color-secondary: 138 21 44;
--color-paper: 0 0 0;
}
}
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
/** @type {import('tailwindcss').Config} */
/** generates a map of sizes from min to max e.g. {'1px':'1px',..., '60px':'60px'} */
function genPxSizeMap(min, max) {
const sizes = {};
for (let size = min; size <= max; size++) {
const sizeToAdd = `${size}px`;
sizes[sizeToAdd] = sizeToAdd;
}
return sizes;
}
module.exports = {
prefix: "tw-",
important: true,
darkMode: "selector",
content: ["./src/**/*.{html,ts,scss}"],
theme: {
container: {
center: true,
screens: {
sm: "100%",
md: "100%",
lg: "100%",
xl: "100%",
"2xl": "100%",
},
},
colors: {
white: "#ffffff",
black: "#000000",
},
fontFamily: {
"roboto-black": ["Roboto-Black", "sans-serif"],
"roboto-bold": ["Roboto-Bold", "sans-serif"],
"roboto-light": ["Roboto-Light", "sans-serif"],
"roboto-medium": ["Roboto-Medium", "sans-serif"],
"roboto-regular": ["Roboto-Regular", "sans-serif"],
"roboto-thin": ["Roboto-Thin", "sans-serif"],
},
screens: {
// default: 0 <-> 479 // portrait mobile // fluid width
sm: "480px", // landscape mobile // fixed width // same design as portrait mobile
md: "600px", // tablet // fixed width
lg: "1014px", // laptop // fixed width
xl: "1336px", // desktop // fixed width
},
extend: {
fontSize: genPxSizeMap(1, 150),
spacing: genPxSizeMap(1, 300),
borderRadius: genPxSizeMap(1, 150),
},
},
plugins: [],
};
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment