Added a bunch of services

This commit is contained in:
Zakary Timson 2022-11-10 11:06:14 -05:00
parent 1fc86d3614
commit fde487fc66
20 changed files with 268 additions and 38 deletions

View File

@ -17,12 +17,14 @@ COPY . .
RUN if [ ! -d "dist" ] && [ ! -d "node_modules" ]; then npm install; fi RUN if [ ! -d "dist" ] && [ ! -d "node_modules" ]; then npm install; fi
# Build # Build
RUN BUILD_MODE=$([ "$NODE_ENV" = "prod" ] && echo "dynmaic-prod" || echo "dynamic") && \ RUN BUILD_MODE=$([ "$NODE_ENV" = "prod" ] && echo "prod" || echo "dev") && \
if [ ! -d "dist" ]; then npm run "build:$BUILD_MODE"; fi if [ ! -d "dist" ]; then npm run "build:$BUILD_MODE"; fi
# Use Nginx to serve # Use Nginx to serve
FROM nginx:1.20-alpine FROM nginx:1.20-alpine
COPY --from=build /app/dist /usr/share/nginx/html COPY --from=build /app/dist /usr/share/nginx/html
COPY docker/robots.txt /usr/share/nginx/html/robots.txt COPY docker/config/robots.txt /usr/share/nginx/html/robots.txt
COPY docker/nginx.conf /etc/nginx/nginx.conf COPY docker/config/nginx.conf /etc/nginx/nginx.conf
COPY docker/scripts/setup-environment.sh /docker-entrypoint.d/setup-environment.sh
RUN chmod +x /docker-entrypoint.d/setup-environment.sh
EXPOSE 80 EXPOSE 80

View File

@ -0,0 +1,3 @@
#!/usr/bin/env sh
if [ -n "$ANALYTICS" ]; then sed -i -e "s/:[[:space:]]\?['\"]{{ANALYTICS}}['\"]/:'$ANALYTICS'/g" /usr/share/nginx/html/main*.js; fi

21
package-lock.json generated
View File

@ -20,6 +20,7 @@
"@angular/router": "^14.2.0", "@angular/router": "^14.2.0",
"bootstrap": "^5.2.1", "bootstrap": "^5.2.1",
"jquery": "^3.6.1", "jquery": "^3.6.1",
"ngx-google-analytics": "^14.0.1",
"rxjs": "~7.5.0", "rxjs": "~7.5.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"webstorage-decorators": "^4.2.0", "webstorage-decorators": "^4.2.0",
@ -7891,6 +7892,18 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true "dev": true
}, },
"node_modules/ngx-google-analytics": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/ngx-google-analytics/-/ngx-google-analytics-14.0.1.tgz",
"integrity": "sha512-PfOtnshSyq15EKevKlFW9IRgH+dTtPG4Q9HJYksuRNYDzjce0eqK3Bf6hz0tAZdyqbzTCyx5g+NgWBfpqQfb2w==",
"dependencies": {
"tslib": "^2.4.0"
},
"peerDependencies": {
"@angular/common": ">=12.0.0",
"@angular/core": ">=12.0.0"
}
},
"node_modules/nice-napi": { "node_modules/nice-napi": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",
@ -17380,6 +17393,14 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true "dev": true
}, },
"ngx-google-analytics": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/ngx-google-analytics/-/ngx-google-analytics-14.0.1.tgz",
"integrity": "sha512-PfOtnshSyq15EKevKlFW9IRgH+dTtPG4Q9HJYksuRNYDzjce0eqK3Bf6hz0tAZdyqbzTCyx5g+NgWBfpqQfb2w==",
"requires": {
"tslib": "^2.4.0"
}
},
"nice-napi": { "nice-napi": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",

View File

@ -23,6 +23,7 @@
"@angular/router": "^14.2.0", "@angular/router": "^14.2.0",
"bootstrap": "^5.2.1", "bootstrap": "^5.2.1",
"jquery": "^3.6.1", "jquery": "^3.6.1",
"ngx-google-analytics": "^14.0.1",
"rxjs": "~7.5.0", "rxjs": "~7.5.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"webstorage-decorators": "^4.2.0", "webstorage-decorators": "^4.2.0",

View File

@ -1,12 +1,16 @@
import { NgModule } from '@angular/core'; import {NgModule} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import {BrowserModule} from '@angular/platform-browser';
import { AppRouting } from './app.routing'; import {NgxGoogleAnalyticsModule} from 'ngx-google-analytics';
import {environment} from '../environments/environment';
import {AppRouting} from './app.routing';
import {BannerComponent} from './components/banner/banner.component'; import {BannerComponent} from './components/banner/banner.component';
import {FooterComponent} from './components/footer/footer.component'; import {FooterComponent} from './components/footer/footer.component';
import {LogoComponent} from './components/logo/logo.component';
import {NavbarComponent} from './components/navbar/navbar.component'; import {NavbarComponent} from './components/navbar/navbar.component';
import { AppComponent } from './containers/app/app.component'; import {AppComponent} from './containers/app/app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import {BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {MaterialModule} from './material.module'; import {MaterialModule} from './material.module';
import {PrelaodService} from './services/prelaod.service';
import {FourOFourComponent} from './views/404/404.component'; import {FourOFourComponent} from './views/404/404.component';
import {AboutComponent} from './views/about/about.component'; import {AboutComponent} from './views/about/about.component';
import {AestivaComponent} from './views/events/aestiva/aestiva.component'; import {AestivaComponent} from './views/events/aestiva/aestiva.component';
@ -18,32 +22,40 @@ import {DrillComponent} from './views/reenact/drill/drill.component';
import {GettingStartedComponent} from './views/reenact/getting-started/getting-started.component'; import {GettingStartedComponent} from './views/reenact/getting-started/getting-started.component';
import {RulesComponent} from './views/reenact/rules/rules.component'; import {RulesComponent} from './views/reenact/rules/rules.component';
export const APP_COMPONENTS = [ export const APP_COMPONENTS: any[] = [
AboutComponent, AboutComponent,
AestivaComponent, AestivaComponent,
AppComponent, AppComponent,
BannerComponent, BannerComponent,
CalendarComponent, CalendarComponent,
DrillComponent, DrillComponent,
GettingStartedComponent,
HibernaComponent,
FooterComponent, FooterComponent,
FourOFourComponent, FourOFourComponent,
GalleryComponent, GalleryComponent,
GettingStartedComponent,
HibernaComponent,
HomeComponent, HomeComponent,
LogoComponent,
NavbarComponent, NavbarComponent,
RulesComponent RulesComponent
] ]
@NgModule({ export const APP_IMPORTS: any[] = [
declarations: APP_COMPONENTS,
imports: [
BrowserModule,
AppRouting, AppRouting,
BrowserAnimationsModule, BrowserAnimationsModule,
BrowserModule,
MaterialModule MaterialModule
], ]
if(environment.analytics && (<any>environment.analytics) != '{{ANALYTICS}}')
APP_IMPORTS.push(NgxGoogleAnalyticsModule.forRoot(<any>environment.analytics));
@NgModule({
declarations: APP_COMPONENTS,
imports: APP_IMPORTS,
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { } export class AppModule {
constructor(preload: PrelaodService) { }
}

View File

@ -13,15 +13,15 @@ import {RulesComponent} from './views/reenact/rules/rules.component';
const routes: Routes = [ const routes: Routes = [
{path: '', pathMatch: 'full', component: HomeComponent}, {path: '', pathMatch: 'full', component: HomeComponent},
{path: 'about', component: AboutComponent}, {path: 'about', component: AboutComponent, data: {title: 'About'}},
{path: 'drill', component: DrillComponent}, {path: 'drill', component: DrillComponent, data: {title: 'Drill Commands'}},
{path: 'events/aestiva', component: AestivaComponent}, {path: 'events/aestiva', component: AestivaComponent, data: {title: 'Castra Aestiva'}},
{path: 'events/hiberna', component: HibernaComponent}, {path: 'events/hiberna', component: HibernaComponent, data: {title: 'Castra Hiberna'}},
{path: 'events/calendar', component: CalendarComponent}, {path: 'events/calendar', component: CalendarComponent, data: {title: 'Calendar'}},
{path: 'gallery', component: GalleryComponent}, {path: 'gallery', component: GalleryComponent, data: {title: 'Gallery'}},
{path: 'getting-started', component: GettingStartedComponent}, {path: 'getting-started', component: GettingStartedComponent, data: {title: 'Getting Started'}},
{path: 'rules', component: RulesComponent}, {path: 'rules', component: RulesComponent, data: {title: 'Rules & Regulations'}},
{path: '**', component: FourOFourComponent} {path: '**', component: FourOFourComponent, data: {title: '404'}}
]; ];
@NgModule({ @NgModule({

View File

@ -0,0 +1,9 @@
<div class="logo">
<div class="d-flex" aria-label="Legio XXX">
<div>L</div>
<div [@slide]="expand ? 'expand' : 'shrink'" style="overflow: hidden">EGIO · </div>
<div [@margin]="expand ? 'expand' : 'shrink'" [style.marginLeft]="expand ? '0.25rem' : 0">XXX</div>
<div *ngIf="loading">{{dots | async}}</div>
</div>
<div *ngIf="loadingText" class="logo-footer text-center">{{loadingText}}</div>
</div>

View File

@ -0,0 +1,26 @@
//.logo {
// .expandable {
// width: 0;
// }
//
// &.expanded {
// .expandable {
// width: auto;
// }
// }
//
// .logo-segment {
// font-family: Arial, sans-serif !important;
// overflow: hidden;
// padding: 0.3em 0;
// width: auto;
// }
//
// .logo-dots {
// width: 30px;
// }
//
// .logo-footer {
// transform: translate(-15px, -8px);
// }
//}

View File

@ -0,0 +1,35 @@
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
import {animate, state, style, transition, trigger} from '@angular/animations';
import {Observable, timer} from 'rxjs';
import {map} from 'rxjs/operators';
@Component({
selector: 'xxx-logo',
templateUrl: './logo.component.html',
animations: [
trigger('slide', [
state('expand', style({width: '*'})),
state('shrink', style({width: '0px'})),
transition('* => *', [animate('0.5s')])
]),
trigger('margin', [
state('expand', style({marginLeft: '0.25rem'})),
state('shrink', style({marginLeft: '0'})),
transition('* => *', [animate('0.5s')])
])
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LogoComponent {
@Input() expand = true;
@Input() loading = false;
@Input() loadingText = '';
dots: Observable<string>;
constructor() {
this.dots = timer(0, 1000).pipe(map(i => {
return Array(i % 4).fill('.').join('');
}));
}
}

View File

@ -3,7 +3,7 @@
<div> <div>
<a class="navbar-brand d-flex align-items-center" routerLink="/" fragment="banner" (click)="scroll('banner')"> <a class="navbar-brand d-flex align-items-center" routerLink="/" fragment="banner" (click)="scroll('banner')">
<img src="assets/img/eagle.png" alt="SPQR" height="45px" width="45px"> <img src="assets/img/eagle.png" alt="SPQR" height="45px" width="45px">
<div class="px-2">LEGIO · XXX</div> <xxx-logo class="px-2" [expand]="true"></xxx-logo>
</a> </a>
</div> </div>
<div class="flex-grow-1"></div> <div class="flex-grow-1"></div>

View File

@ -2,6 +2,7 @@ import {AfterViewInit, Component, EventEmitter, Input, OnDestroy, Output} from '
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router'; import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
import {combineLatest, filter, Subscription} from 'rxjs'; import {combineLatest, filter, Subscription} from 'rxjs';
import {NAVIGATION} from '../../misc/navigation'; import {NAVIGATION} from '../../misc/navigation';
import {BreakpointService} from '../../services/breakpoint.service';
@Component({ @Component({
selector: 'xxx-navbar', selector: 'xxx-navbar',
@ -20,7 +21,7 @@ export class NavbarComponent implements AfterViewInit, OnDestroy {
@Output() hamburgerClick = new EventEmitter<void>(); @Output() hamburgerClick = new EventEmitter<void>();
constructor(private route: ActivatedRoute, private router: Router) { } constructor(private route: ActivatedRoute, private router: Router, public breakpoint: BreakpointService) { }
ngAfterViewInit() { ngAfterViewInit() {
this.sub = combineLatest([this.router.events.pipe(filter(e => e instanceof NavigationEnd)), this.route.fragment]).subscribe(([url, frag]) => { this.sub = combineLatest([this.router.events.pipe(filter(e => e instanceof NavigationEnd)), this.route.fragment]).subscribe(([url, frag]) => {

View File

@ -1,7 +1,7 @@
import {BreakpointObserver} from '@angular/cdk/layout';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router'; import {NavigationEnd, Router} from '@angular/router';
import {filter} from 'rxjs'; import {combineLatest, filter, Subscription} from 'rxjs';
import {BreakpointService} from '../../services/breakpoint.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -9,15 +9,22 @@ import {filter} from 'rxjs';
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent { export class AppComponent {
private sub?: Subscription;
mobile = false; mobile = false;
open = false; open = false;
constructor(private router: Router, route: ActivatedRoute, breakpointObserver: BreakpointObserver) { constructor(private breakpoint: BreakpointService, private router: Router) {
router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => this.open = false); this.sub = combineLatest([
breakpointObserver.observe(['(max-width: 750px)']).subscribe(result => { router.events.pipe(filter(event => event instanceof NavigationEnd)),
this.mobile = result.matches; breakpoint.isMobile$
]).subscribe(([event, mobile]) => {
this.mobile = mobile;
this.open = !this.mobile; this.open = !this.mobile;
}) })
} }
ngOnDestroy() {
if(this.sub) this.sub.unsubscribe();
}
} }

View File

@ -0,0 +1,33 @@
import {Injectable, OnDestroy, Optional} from '@angular/core';
import {NavigationEnd, Router} from '@angular/router';
import {GoogleAnalyticsService} from 'ngx-google-analytics';
import {combineLatest, Subscription} from 'rxjs';
import {filter} from 'rxjs/operators';
import {TitleService} from './title.service';
@Injectable({providedIn: 'root'})
export class AnalyticsService implements OnDestroy {
private sub?: Subscription;
constructor(@Optional() private analyticsService: GoogleAnalyticsService,
private router: Router,
private title: TitleService
) {
if(this.analyticsService) {
combineLatest([
this.router.events.pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd))
]).subscribe(([navigation]) => {
this.analyticsService.pageView(navigation.urlAfterRedirects, this.title.title);
});
}
}
log(action: string, category?: string, label?: string, value?: any, interaction?: boolean) {
if(!this.analyticsService) return;
return this.analyticsService.event(action, category, label, value, interaction);
}
ngOnDestroy() {
if(this.sub) this.sub.unsubscribe();
}
}

View File

@ -0,0 +1,15 @@
import {Injectable} from '@angular/core';
import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout';
import {map, tap} from 'rxjs/operators';
@Injectable({providedIn: 'root'})
export class BreakpointService {
private _isMobile?: boolean;
get isMobile() { return !!this.isMobile$; }
isMobile$ = this.breakpointObserver.observe([Breakpoints.XSmall]).pipe(
map(e => e.matches),
tap(e => this._isMobile = e)
);
constructor(private breakpointObserver: BreakpointObserver) { }
}

View File

@ -0,0 +1,12 @@
import {Injectable} from '@angular/core';
import {AnalyticsService} from './analytics.service';
import {BreakpointService} from './breakpoint.service';
import {TitleService} from './title.service';
@Injectable({providedIn: 'root'})
export class PrelaodService {
constructor(private analytics: AnalyticsService,
private breakpoint: BreakpointService,
private title: TitleService
) { }
}

View File

@ -0,0 +1,51 @@
import {Injectable, OnDestroy} from '@angular/core';
import {Title} from '@angular/platform-browser';
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
import {filter} from 'rxjs/operators';
import {Observable, Subscription} from 'rxjs';
@Injectable({providedIn: 'root'})
export class TitleService implements OnDestroy {
private readonly orgTitle: string = 'LEGIO · XXX';
private routeSub?: Subscription;
private titleSub?: Subscription;
get title(): string {
const title = this._title.getTitle();
if(title.includes(this.orgTitle) && title.length > this.orgTitle.length)
return title.substring(this.orgTitle.length);
return this._title.getTitle();
}
set title(title: string | null) {
if(!title) this._title.setTitle(this.orgTitle);
else this._title.setTitle(`${this.orgTitle} | ${title}`);
}
constructor(private router: Router,
private route: ActivatedRoute,
private _title: Title
) {
// this.orgTitle = this.title; // Hardcoding because HMR breaks this
this.routeSub = this.router.events
.pipe(filter(e => e instanceof NavigationEnd))
.subscribe(() => this.getTitleFromRoute());
this.getTitleFromRoute();
}
private async getTitleFromRoute() {
if(this.titleSub) this.titleSub.unsubscribe();
let route = this.route, title = route.snapshot.data['title'];
while (route.firstChild) {
route = route.firstChild;
if(route.snapshot.data['title']) title = route.snapshot.data['title'];
}
if(title instanceof Observable) { this.titleSub = title.subscribe(t => this.title = t); }
else { this.title = title; }
}
ngOnDestroy() {
if(this.routeSub) this.routeSub.unsubscribe();
if(this.titleSub) this.titleSub.unsubscribe();
}
}

View File

@ -1,3 +1,4 @@
export const environment = { export const environment = {
analytics: 'G-7HLT4FQY9V',
production: true production: true
}; };

View File

@ -1,3 +1,4 @@
export const environment = { export const environment = {
analytics: false,
production: false production: false
}; };