diff --git a/angular.json b/angular.json index f03481d..bfc30d1 100644 --- a/angular.json +++ b/angular.json @@ -17,7 +17,7 @@ "build": { "builder": "@angular-devkit/build-angular:browser", "options": { - "outputPath": "dist/personal-web", + "outputPath": "dist/personal-web/browser", "index": "src/index.html", "main": "src/main.ts", "polyfills": ["zone.js"], @@ -101,6 +101,65 @@ "options": { "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] } + }, + "server": { + "builder": "@angular-devkit/build-angular:server", + "options": { + "outputPath": "dist/personal-web/server", + "main": "server.ts", + "tsConfig": "tsconfig.server.json", + "inlineStyleLanguage": "scss" + }, + "configurations": { + "pro": { + "outputHashing": "media" + }, + "dev": { + "buildOptimizer": false, + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.dev.ts" + } + ], + "optimization": false, + "sourceMap": true, + "extractLicenses": false, + "vendorChunk": true + } + }, + "defaultConfiguration": "pro" + }, + "serve-ssr": { + "builder": "@angular-devkit/build-angular:ssr-dev-server", + "configurations": { + "dev": { + "browserTarget": "personal-web:build:dev", + "serverTarget": "personal-web:server:dev" + }, + "pro": { + "browserTarget": "personal-web:build:pro", + "serverTarget": "personal-web:server:pro" + } + }, + "defaultConfiguration": "dev" + }, + "prerender": { + "builder": "@angular-devkit/build-angular:prerender", + "options": { + "routes": ["/"] + }, + "configurations": { + "pro": { + "browserTarget": "personal-web:build:pro", + "serverTarget": "personal-web:server:pro" + }, + "dev": { + "browserTarget": "personal-web:build:dev", + "serverTarget": "personal-web:server:dev" + } + }, + "defaultConfiguration": "pro" } } } diff --git a/package.json b/package.json index ec3a285..27a1680 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,11 @@ "watch": "ng build --watch --configuration dev", "test": "ng test", "lint": "eslint .", - "prepare": "husky install" + "prepare": "husky install", + "dev:ssr": "ng run personal-web:serve-ssr --port 4201", + "serve:ssr": "node dist/personal-web/server/main.js --port 4201", + "build:ssr": "ng build && ng run personal-web:server", + "prerender": "ng run personal-web:prerender" }, "private": true, "dependencies": { diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..8f389c3 --- /dev/null +++ b/server.ts @@ -0,0 +1,69 @@ +import 'zone.js/node'; + +import { APP_BASE_HREF } from '@angular/common'; +import { CommonEngine } from '@angular/ssr'; +import * as express from 'express'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import AppServerModule from './src/main.server'; + +// The Express app is exported so that it can be used by serverless Functions. +export function app(): express.Express { + const server = express(); + const distFolder = join(process.cwd(), 'dist/personal-web/browser'); + const indexHtml = existsSync(join(distFolder, 'index.original.html')) + ? join(distFolder, 'index.original.html') + : join(distFolder, 'index.html'); + + const commonEngine = new CommonEngine(); + + server.set('view engine', 'html'); + server.set('views', distFolder); + + // Example Express Rest API endpoints + // server.get('/api/**', (req, res) => { }); + // Serve static files from /browser + server.get('*.*', express.static(distFolder, { + maxAge: '1y' + })); + + // All regular routes use the Angular engine + server.get('*', (req, res, next) => { + const { protocol, originalUrl, baseUrl, headers } = req; + + commonEngine + .render({ + bootstrap: AppServerModule, + documentFilePath: indexHtml, + url: `${protocol}://${headers.host}${originalUrl}`, + publicPath: distFolder, + providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }], + }) + .then((html) => res.send(html)) + .catch((err) => next(err)); + }); + + return server; +} + +function run(): void { + const port = process.env['PORT'] || 4000; + + // Start up the Node server + const server = app(); + server.listen(port, () => { + console.log(`Node Express server listening on http://localhost:${port}`); + }); +} + +// Webpack will replace 'require' with '__webpack_require__' +// '__non_webpack_require__' is a proxy to Node 'require' +// The below code is to ensure that the server is run only when not requiring the bundle. +declare const __non_webpack_require__: NodeRequire; +const mainModule = __non_webpack_require__.main; +const moduleFilename = mainModule && mainModule.filename || ''; +if (moduleFilename === __filename || moduleFilename.includes('iisnode')) { + run(); +} + +export default AppServerModule; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 245face..f9f901e 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -42,6 +42,11 @@ export class AppComponent implements OnInit, AfterViewInit { this.store.dispatch(publicLanguageActions.loadOneByAcronym({ acronym: language.acronym })); } }); + } else { + this.store.dispatch(publicLanguageActions.loadAll({})); + this.languages$.pipe(take(2)).subscribe(() => { + this.store.dispatch(publicLanguageActions.loadOneByAcronym({ acronym: 'es' })); + }); } } diff --git a/src/app/app.module.server.ts b/src/app/app.module.server.ts new file mode 100644 index 0000000..03cacd8 --- /dev/null +++ b/src/app/app.module.server.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; + +import { AppComponent } from './app.component'; +import { AppModule } from './app.module'; + +@NgModule({ + imports: [AppModule, ServerModule], + bootstrap: [AppComponent], +}) +export class AppServerModule {} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e49a33a..c1d5d44 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,7 +1,7 @@ -import { HttpClient, HttpClientModule } from '@angular/common/http'; +import { HttpClient, HttpClientModule, provideHttpClient, withFetch } from '@angular/common/http'; import { NgModule, isDevMode } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserModule } from '@angular/platform-browser'; +import { BrowserModule, provideClientHydration } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { EffectsModule } from '@ngrx/effects'; @@ -29,7 +29,13 @@ export function HttpLoaderFactory(http: HttpClient) { } @NgModule({ declarations: [AppComponent, LoginComponent, FormResetPasswordComponent, ResetPasswordComponent], - providers: [MessageService, ConfirmationService, PrimeNGConfig], + providers: [ + MessageService, + ConfirmationService, + PrimeNGConfig, + provideHttpClient(withFetch()), + provideClientHydration(), + ], bootstrap: [AppComponent], imports: [ ToastModule, diff --git a/src/app/shared/components/input-translations/input-translations.component.scss b/src/app/shared/components/input-translations/input-translations.component.scss index 0469254..0b9ca51 100644 --- a/src/app/shared/components/input-translations/input-translations.component.scss +++ b/src/app/shared/components/input-translations/input-translations.component.scss @@ -7,6 +7,8 @@ display: flex; justify-content: center; align-items: center; + height: 1.2rem; + overflow: hidden; } .name { diff --git a/src/app/shared/components/language-select/language-select.component.html b/src/app/shared/components/language-select/language-select.component.html index 7fcc512..b9026df 100644 --- a/src/app/shared/components/language-select/language-select.component.html +++ b/src/app/shared/components/language-select/language-select.component.html @@ -20,22 +20,21 @@ } @case ('dropdown') { - -
+ +
- +
- {{ language.acronym | uppercase }} | - {{ language.nativeName }} - | {{ language.name }} + {{ languageSelected.acronym | uppercase }} | + {{ languageSelected.nativeName }} + | {{ languageSelected.name }}
diff --git a/src/app/shared/components/language-select/language-select.component.ts b/src/app/shared/components/language-select/language-select.component.ts index 6f71d14..772e231 100644 --- a/src/app/shared/components/language-select/language-select.component.ts +++ b/src/app/shared/components/language-select/language-select.component.ts @@ -6,6 +6,7 @@ import { OnInit, ViewEncapsulation, inject, + signal, } from '@angular/core'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; @@ -36,7 +37,9 @@ export class LanguageSelectComponent implements OnInit { mode: 'simple' | 'complete' = 'simple'; languages: Language[] = []; - language!: Language; + languagesSignal = signal([]); + language: Language | undefined; + languageSignal = signal(undefined); ngOnInit(): void { this.store @@ -46,23 +49,29 @@ export class LanguageSelectComponent implements OnInit { this.languages = languages .filter((language) => (language.active ? true : false)) .sort((a, b) => a.nativeName.localeCompare(b.nativeName)); + this.languagesSignal.set(this.languages); }); this.store.select(publicLanguageReducer.getOne).subscribe((language) => { if (!language) return; this.language = language; + this.languageSignal.set(this.language); this.translateSrv.use(language.acronym); this.translateSrv .get('calendar') .pipe(take(1)) .subscribe((res) => this.config.setTranslation(res)); - this.ref.detectChanges(); }); } onLanguageChange(event: DropdownChangeEvent | SelectButtonChangeEvent) { this.language = event.value; + if (!this.language) { + return; + } + this.languageSignal.set(this.language); + this.store.dispatch(publicLanguageActions.loadOneSuccess({ payload: this.language })); } } diff --git a/src/app/shared/guards/auth.guard.ts b/src/app/shared/guards/auth.guard.ts index d5d305c..28f5373 100644 --- a/src/app/shared/guards/auth.guard.ts +++ b/src/app/shared/guards/auth.guard.ts @@ -1,4 +1,6 @@ -import { Injectable } from '@angular/core'; +/* eslint-disable @typescript-eslint/ban-types */ +import { isPlatformBrowser } from '@angular/common'; +import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; import { Router, UrlTree } from '@angular/router'; import { Observable } from 'rxjs'; import { filter, map, take } from 'rxjs/operators'; @@ -8,9 +10,17 @@ import { AuthService } from '../services/auth.service'; providedIn: 'root', }) export class AuthGuard { - constructor(private auth: AuthService, private router: Router) {} + constructor( + private auth: AuthService, + private router: Router, + @Inject(PLATFORM_ID) private platformId: Object, + ) {} canActivate(): Observable { + if (!isPlatformBrowser(this.platformId)) { + this.router.navigate(['login']); + } + return this.auth.getCurrentUser().pipe( filter((val) => val !== null), take(1), diff --git a/src/app/shared/state/languages/public-language.reducer.ts b/src/app/shared/state/languages/public-language.reducer.ts index 7ebab23..068ca40 100644 --- a/src/app/shared/state/languages/public-language.reducer.ts +++ b/src/app/shared/state/languages/public-language.reducer.ts @@ -1,90 +1,13 @@ import { ReducerTypes, on } from '@ngrx/store'; import { Language } from 'src/app/backoffice/tables/language/models/language.model'; import { CommonReducer } from 'src/app/shared/state/common/common.reducer'; -import { v4 as uuidv4 } from 'uuid'; import { ActionStatus, ActionType } from '../common/common-state'; import { Naming, NumberMode } from '../common/common.names'; import { publicLanguageActions } from './public-language.actions'; import { publicLanguageNames } from './public-language.names'; import { PublicLanguageState } from './public-language.state'; -const englishId = uuidv4(); export const initialState = new PublicLanguageState(); -initialState.selectedId = englishId; -initialState.entities = [ - { - id: uuidv4(), - acronym: 'de', - nativeName: '', - name: '', - active: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: englishId, - acronym: 'en', - nativeName: '', - name: '', - active: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: uuidv4(), - acronym: 'es', - nativeName: '', - name: '', - active: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: uuidv4(), - acronym: 'fr', - nativeName: '', - name: '', - active: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: uuidv4(), - acronym: 'it', - nativeName: '', - name: '', - active: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: uuidv4(), - acronym: 'nl', - nativeName: '', - name: '', - active: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: uuidv4(), - acronym: 'no', - nativeName: '', - name: '', - active: true, - createdAt: new Date(), - updatedAt: new Date(), - }, - { - id: uuidv4(), - acronym: 'pt', - nativeName: '', - name: '', - active: true, - createdAt: new Date(), - updatedAt: new Date(), - }, -]; // eslint-disable-next-line @typescript-eslint/no-explicit-any const otherReducers: ReducerTypes[] = [ diff --git a/src/main.server.ts b/src/main.server.ts new file mode 100644 index 0000000..dfb6fdb --- /dev/null +++ b/src/main.server.ts @@ -0,0 +1 @@ +export { AppServerModule as default } from './app/app.module.server'; diff --git a/tsconfig.server.json b/tsconfig.server.json new file mode 100644 index 0000000..e2ebe5a --- /dev/null +++ b/tsconfig.server.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.app.json", + "compilerOptions": { + "outDir": "./out-tsc/server", + "types": [ + "node" + ] + }, + "files": [ + "src/main.server.ts", + "server.ts" + ] +}