Skip to content

Commit

Permalink
feat(App): 🔍 first SSR configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
juamber-rgs committed Jan 28, 2024
1 parent 1c26741 commit 606e5a1
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 94 deletions.
61 changes: 60 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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"
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
69 changes: 69 additions & 0 deletions server.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }));
});
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/app/app.module.server.ts
Original file line number Diff line number Diff line change
@@ -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 {}
12 changes: 9 additions & 3 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
display: flex;
justify-content: center;
align-items: center;
height: 1.2rem;
overflow: hidden;
}

.name {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,21 @@
}
@case ('dropdown') {
<p-dropdown
[style]="{ height: '100%', minWidth: '10em' }"
[options]="languages"
[options]="languagesSignal()"
[ngModel]="language"
[autoDisplayFirst]="false"
(onChange)="onLanguageChange($event)"
ariaLabel="Dropdown to select the language"
>
<ng-template pTemplate="selectedItem">
<div class="language-item language-item-value" *ngIf="language">
<ng-template let-languageSelected pTemplate="selectedItem">
<div class="language-item language-item-value" *ngIf="languageSelected">
<div class="flag">
<span class="" [class]="'flag flag-' + language.acronym"></span>
<span class="" [class]="'flag flag-' + languageSelected.acronym"></span>
</div>
<div class="name">
<ng-container *ngIf="mode === 'complete'"> {{ language.acronym | uppercase }} | </ng-container>
{{ language.nativeName }}
<ng-container *ngIf="mode === 'complete'"> | {{ language.name }} </ng-container>
<ng-container *ngIf="mode === 'complete'"> {{ languageSelected.acronym | uppercase }} | </ng-container>
{{ languageSelected.nativeName }}
<ng-container *ngIf="mode === 'complete'"> | {{ languageSelected.name }} </ng-container>
</div>
</div>
</ng-template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OnInit,
ViewEncapsulation,
inject,
signal,
} from '@angular/core';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
Expand Down Expand Up @@ -36,7 +37,9 @@ export class LanguageSelectComponent implements OnInit {
mode: 'simple' | 'complete' = 'simple';

languages: Language[] = [];
language!: Language;
languagesSignal = signal<Language[]>([]);
language: Language | undefined;
languageSignal = signal<Language | undefined>(undefined);

ngOnInit(): void {
this.store
Expand All @@ -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 }));
}
}
14 changes: 12 additions & 2 deletions src/app/shared/guards/auth.guard.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<boolean | UrlTree> {
if (!isPlatformBrowser(this.platformId)) {
this.router.navigate(['login']);
}

return this.auth.getCurrentUser().pipe(
filter((val) => val !== null),
take(1),
Expand Down
Loading

0 comments on commit 606e5a1

Please sign in to comment.