5 Commits
0.0.0 ... 1.0.0

Author SHA1 Message Date
72e27eaedc Mobile fixes
All checks were successful
Build Website / Build NPM Project (push) Successful in 1m35s
Build Website / Tag Version (push) Successful in 47s
Build Website / Build Container (push) Successful in 3m2s
2026-06-05 22:54:31 -04:00
16ddd1c8a3 Updated site to use momentum for contacting, registration, gallery and calendar, as well as some other updates to the site content
All checks were successful
Build Website / Build NPM Project (push) Successful in 1m42s
Build Website / Tag Version (push) Successful in 1m2s
Build Website / Build Container (push) Successful in 2m50s
2026-06-05 19:19:27 -04:00
3e4efc2fd2 Lock contact form on success
Some checks failed
Build Website / Build NPM Project (push) Successful in 1m31s
Build Website / Tag Version (push) Successful in 1m12s
Build Website / Build Container (push) Failing after 3m1s
2026-06-05 10:15:28 -04:00
1a845b3f2e Removed stupid angular budgets
All checks were successful
Build Website / Build NPM Project (push) Successful in 1m52s
Build Website / Tag Version (push) Successful in 1m6s
Build Website / Build Container (push) Successful in 3m45s
2026-06-04 19:04:15 -04:00
38e018d034 - Better banner
Some checks failed
Build Website / Build Container (push) Failing after 1m39s
Build Website / Tag Version (push) Has been cancelled
Build Website / Build NPM Project (push) Has been cancelled
- Removed old communication methods
- Connected email form
2026-06-04 18:59:10 -04:00
43 changed files with 1049 additions and 389 deletions

View File

@@ -33,18 +33,7 @@
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"budgets": [],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",

14
package-lock.json generated
View File

@@ -18,7 +18,7 @@
"@angular/platform-browser": "^14.2.0",
"@angular/platform-browser-dynamic": "^14.2.0",
"@angular/router": "^14.2.0",
"@ztimson/momentum": "^1.1.11",
"@ztimson/momentum": "^1.2.1",
"bootstrap": "^5.2.1",
"jquery": "^3.6.1",
"ngx-google-analytics": "^14.0.1",
@@ -3342,9 +3342,9 @@
"dev": true
},
"node_modules/@ztimson/momentum": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@ztimson/momentum/-/momentum-1.1.11.tgz",
"integrity": "sha512-ArFIOJj0mCbvR/P6XBlOjvICytf3ggPClbMmDDsZNtLMGXIZ+HqNjt58LDuEpLr3a8pe0mXEZ3jGkIAv2no8mA==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@ztimson/momentum/-/momentum-1.2.1.tgz",
"integrity": "sha512-O3Z06SfMin6zXlW0jZjgS16qwcyPtKfJ8hRzV6SuFlN9Il8y7HepUylnV8Hd5RTdWXeztBmo+pNTD/RjPJ37wQ==",
"dependencies": {
"@ztimson/utils": "0.29.1"
},
@@ -14098,9 +14098,9 @@
"dev": true
},
"@ztimson/momentum": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@ztimson/momentum/-/momentum-1.1.11.tgz",
"integrity": "sha512-ArFIOJj0mCbvR/P6XBlOjvICytf3ggPClbMmDDsZNtLMGXIZ+HqNjt58LDuEpLr3a8pe0mXEZ3jGkIAv2no8mA==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@ztimson/momentum/-/momentum-1.2.1.tgz",
"integrity": "sha512-O3Z06SfMin6zXlW0jZjgS16qwcyPtKfJ8hRzV6SuFlN9Il8y7HepUylnV8Hd5RTdWXeztBmo+pNTD/RjPJ37wQ==",
"requires": {
"@ztimson/utils": "0.29.1"
}

View File

@@ -1,6 +1,6 @@
{
"name": "legio-xxx",
"version": "0.0.0",
"version": "1.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
@@ -21,7 +21,7 @@
"@angular/platform-browser": "^14.2.0",
"@angular/platform-browser-dynamic": "^14.2.0",
"@angular/router": "^14.2.0",
"@ztimson/momentum": "^1.1.11",
"@ztimson/momentum": "^1.2.1",
"bootstrap": "^5.2.1",
"jquery": "^3.6.1",
"ngx-google-analytics": "^14.0.1",

View File

@@ -1,5 +1,5 @@
import {NgModule} from '@angular/core';
import {ReactiveFormsModule} from '@angular/forms';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {BrowserModule} from '@angular/platform-browser';
import {NgxGoogleAnalyticsModule} from 'ngx-google-analytics';
import {environment} from '../environments/environment';
@@ -14,6 +14,7 @@ import {NavbarComponent} from './components/navbar/navbar.component';
import {PlaceholderComponent} from './components/placeholder/placeholder.component';
import {AppComponent} from './containers/app/app.component';
import {BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {InViewDirective} from './directives/in-view.directive';
import {MaterialModule} from './material.module';
import {PrelaodService} from './services/prelaod.service';
import {FourOFourComponent} from './views/404/404.component';
@@ -24,13 +25,14 @@ import {CalendarComponent} from './views/events/calendar/calendar.component';
import {HibernaComponent} from './views/events/hiberna/hiberna.component';
import {GalleryComponent} from './views/gallery/gallery.component';
import {HomeComponent} from './views/home/home.component';
import {DiyComponent} from './views/reenact/diy/diy.component';
import {DrillComponent} from './views/reenact/drill/drill.component';
import {EquipmentComponent} from './views/reenact/equipment/equipment.component';
import {GettingStartedComponent} from './views/reenact/getting-started/getting-started.component';
import {MaintenanceComponent} from './views/reenact/maintinance/maintenance.component';
import {RulesComponent} from './views/reenact/rules/rules.component';
import {BuyComponent} from './views/reenact/buy/buy.component';
import {ResourcesComponent} from './views/resources/resources.component';
import {RegisterComponent} from './views/register/register.component';
import {UploaderComponent} from './components/uploader/uploader.component';
export const APP_COMPONENTS: any[] = [
AboutComponent,
@@ -38,12 +40,11 @@ export const APP_COMPONENTS: any[] = [
AgoniumComponent,
AppComponent,
BannerComponent,
BuyComponent,
CalendarComponent,
CarouselComponent,
ContactComponent,
DiyComponent,
DrillComponent,
EquipmentComponent,
FooterComponent,
FourOFourComponent,
GalleryComponent,
@@ -51,18 +52,22 @@ export const APP_COMPONENTS: any[] = [
HibernaComponent,
HomeComponent,
ImageViewerComponent,
InViewDirective,
LogoComponent,
MaintenanceComponent,
NavbarComponent,
PlaceholderComponent,
RegisterComponent,
ResourcesComponent,
RulesComponent,
UploaderComponent,
]
export const APP_IMPORTS: any[] = [
AppRouting,
BrowserAnimationsModule,
BrowserModule,
FormsModule,
ReactiveFormsModule,
MaterialModule
]

View File

@@ -8,20 +8,19 @@ import {CalendarComponent} from './views/events/calendar/calendar.component';
import {HibernaComponent} from './views/events/hiberna/hiberna.component';
import {GalleryComponent} from './views/gallery/gallery.component';
import {HomeComponent} from './views/home/home.component';
import {DiyComponent} from './views/reenact/diy/diy.component';
import {DrillComponent} from './views/reenact/drill/drill.component';
import {EquipmentComponent} from './views/reenact/equipment/equipment.component';
import {GettingStartedComponent} from './views/reenact/getting-started/getting-started.component';
import {MaintenanceComponent} from './views/reenact/maintinance/maintenance.component';
import {RulesComponent} from './views/reenact/rules/rules.component';
import {BuyComponent} from './views/reenact/buy/buy.component';
import {RegisterComponent} from './views/register/register.component';
import {ResourcesComponent} from './views/resources/resources.component';
const routes: Routes = [
{path: '', pathMatch: 'full', component: HomeComponent},
{path: 'about', component: AboutComponent, data: {title: 'About'}},
{path: 'buy', component: BuyComponent, data: {title: 'Buy'}},
{path: 'drill', component: DrillComponent, data: {title: 'Drill Commands'}},
{path: 'diy', component: DiyComponent, data: {title: 'Build Equipment'}},
{path: 'equipment', component: EquipmentComponent, data: {title: 'Equipment'}},
{path: 'events/agonium', component: AgoniumComponent, data: {title: 'Agonium'}},
{path: 'events/castra-aestiva', component: AestivaComponent, data: {title: 'Castra Aestiva'}},
{path: 'events/castra-hiberna', component: HibernaComponent, data: {title: 'Castra Hiberna'}},
@@ -30,6 +29,7 @@ const routes: Routes = [
{path: 'getting-started', component: GettingStartedComponent, data: {title: 'Getting Started'}},
{path: 'info/resources', component: ResourcesComponent, data: {title: 'Resources'}},
{path: 'maintenance', component: MaintenanceComponent, data: {title: 'Maintenance'}},
{path: 'register', component: RegisterComponent, data: {title: 'Register'}},
{path: 'rules', component: RulesComponent, data: {title: 'Rules & Regulations'}},
{path: '**', component: FourOFourComponent, data: {title: '404'}}
];

View File

@@ -12,11 +12,11 @@
<mat-icon *ngIf="manual">play_arrow</mat-icon>
</div>
<div *ngIf="!manual" class="banner-seal d-flex flex-column align-items-center justify-content-center">
<img src="/assets/img/favicon.svg" class="mt-5" alt="SPQR" height="250" width="250" style="filter: brightness(100%) drop-shadow(2px 4px 6px black);">
<img src="/assets/img/favicon.svg" class="mt-5" alt="SPQR" height="250" width="250">
<div>
<a class="text-white" routerLink="" fragment="about">
<i class="fa fa-angle-double-down fa-4x" style="filter: drop-shadow(2px 4px 6px black);"></i>
</a>
<button type="button" class="banner-scroll text-white" (click)="scrollToAbout()" aria-label="Scroll to about section">
<i class="fa fa-angle-double-down fa-4x"></i>
</button>
</div>
</div>
</div>

View File

@@ -3,48 +3,192 @@
overflow: hidden;
width: 100%;
height: 100%;
min-height: 420px;
background: #111;
isolation: isolate;
}
.banner-container::before {
content: '';
position: absolute;
inset: 0;
z-index: 1;
background:
linear-gradient(90deg, rgba(0, 0, 0, 0.32), rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.32)),
linear-gradient(180deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.34));
pointer-events: none;
}
.banner-container::after {
content: '';
position: absolute;
inset: auto 0 0;
z-index: 2;
height: 28%;
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.38));
pointer-events: none;
}
.banner-background {
width: 100%;
height: 100%;
filter: blur(10px);
-webkit-filter: blur(10px);
object-fit: cover;
transform: scale(1.04);
filter: blur(8px) brightness(90%) saturate(112%);
-webkit-filter: blur(8px) brightness(90%) saturate(112%);
}
.banner-image {
position: absolute;
z-index: 1;
height: 90%;
width: auto;
max-width: 92%;
top: 5%;
left: 50%;
transform: translateX(-50%);
object-fit: contain;
border-radius: 14px;
box-shadow:
0 18px 44px rgba(0, 0, 0, 0.36),
0 0 0 1px rgba(255, 255, 255, 0.14);
filter: contrast(102%) saturate(104%);
}
.banner-next,
.banner-previous,
.banner-pause {
z-index: 4;
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
color: #fff;
cursor: pointer;
border-radius: 999px;
background: rgba(0, 0, 0, 0.34);
border: 1px solid rgba(255, 255, 255, 0.24);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
box-shadow: 0 10px 22px rgba(0, 0, 0, 0.24);
transition:
background 160ms ease,
border-color 160ms ease,
box-shadow 160ms ease,
color 160ms ease;
}
.banner-next:hover,
.banner-previous:hover,
.banner-pause:hover {
background: rgba(75, 15, 15, 0.55);
border-color: rgba(240, 195, 106, 0.52);
color: #f0c36a;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.34);
}
.banner-next mat-icon,
.banner-previous mat-icon,
.banner-pause mat-icon {
font-size: 30px;
width: 30px;
height: 30px;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.42));
}
.banner-next {
position: absolute;
top: 50%;
right: 26px;
transform: translate(-50%, -50%);
transform: translateY(-50%);
}
.banner-previous {
position: absolute;
top: 50%;
left: 50px;
transform: translate(-50%, -50%);
left: 26px;
transform: translateY(-50%);
}
.banner-seal {
position: absolute;
z-index: 3;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
pointer-events: none;
}
.banner-seal img {
max-width: min(250px, 42vw);
height: auto;
opacity: 0.95;
filter: brightness(108%) drop-shadow(0 10px 16px rgba(0, 0, 0, 0.65)) !important;
}
.banner-seal a,
.banner-scroll {
pointer-events: auto;
display: inline-flex;
margin-top: 18px;
color: #fff;
opacity: 0.9;
transition:
opacity 160ms ease,
color 160ms ease;
}
.banner-scroll {
padding: 0;
border: 0;
background: transparent;
cursor: pointer;
filter: drop-shadow(2px 4px 6px black);
}
.banner-seal a:hover,
.banner-scroll:hover {
color: #f0c36a !important;
opacity: 1;
}
.banner-pause {
position: absolute;
bottom: 25px;
left: 50%;
transform: translate(-50%, -50%);
transform: translateX(-50%);
width: 46px;
height: 46px;
}
@media (max-width: 768px) {
.banner-container {
min-height: 340px;
}
.banner-image {
height: 78%;
max-width: 96%;
border-radius: 12px;
}
.banner-next,
.banner-previous {
width: 44px;
height: 44px;
}
.banner-next {
right: 12px;
}
.banner-previous {
left: 12px;
}
.banner-pause {
bottom: 16px;
}
}

View File

@@ -41,7 +41,7 @@ export class BannerComponent implements AfterViewInit, OnDestroy, OnInit {
this.sub = interval(this.speed)
.subscribe( i => {
if(this.manual) return;
this.selected = i % this.images.length
this.selected = i % this.images.length
});
}
@@ -66,4 +66,11 @@ export class BannerComponent implements AfterViewInit, OnDestroy, OnInit {
this.selected--;
if(this.selected < 0) this.selected = this.images.length - 1;
}
scrollToAbout() {
document.getElementById('about')?.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
}

View File

@@ -1,4 +1,4 @@
import {AfterViewInit, Component, Input, OnDestroy, OnInit} from '@angular/core';
import {AfterViewInit, Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core';
import {Photo} from '../models/photo';
@Component({
@@ -18,6 +18,7 @@ export class CarouselComponent implements OnDestroy, OnInit, AfterViewInit {
@Input() disableAutoplay = false;
@Input() index = ~~(Math.random() * this.photos.length);
@Input() height = '100%';
@Output() indexChange = new EventEmitter<number>();
ngOnInit() {
if(this.disableAutoplay) this.pause = true;
@@ -41,11 +42,13 @@ export class CarouselComponent implements OnDestroy, OnInit, AfterViewInit {
this.pause = pause;
this.index++;
if(this.index >= this.photos.length) this.index = 0;
this.indexChange.emit(this.index);
}
previous(pause = true) {
this.loading = true;
this.pause = pause;
this.index = this.index > 0 ? this.index - 1 : this.photos.length - 1;
this.indexChange.emit(this.index);
}
}

View File

@@ -1,9 +1,18 @@
<div class="d-flex flex-column-reverse flex-md-row justify-content-center cap-width">
<div style="flex: 2 0 0;">
<div *ngIf="error" class="alert alert-danger py-2">
Coming Soon: This feature is under development
{{error}}
</div>
<form [formGroup]="form">
<div *ngIf="success" class="alert alert-success py-2">
Your message was sent successfully.
</div>
<form [formGroup]="form" (ngSubmit)="submitEmail()">
<div>
<mat-form-field appearance="fill" class="w-100">
<mat-label>Name</mat-label>
<input matInput formControlName="name">
</mat-form-field>
</div>
<div>
<mat-form-field appearance="fill" class="w-100">
<mat-label>Email</mat-label>
@@ -25,13 +34,10 @@
<textarea matInput rows="10" formControlName="body"></textarea>
</mat-form-field>
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex justify-content-end align-items-center">
<div>
<mat-checkbox color="primary">Send me a copy</mat-checkbox>
</div>
<div>
<button mat-stroked-button class="me-3" (click)="reset()">Reset</button>
<button mat-raised-button color="primary" (click)="error = true">Send</button>
<button mat-stroked-button type="button" class="me-3" (click)="reset()">Reset</button>
<button mat-raised-button type="submit" color="primary" [disabled]="success">Send</button>
</div>
</div>
</form>
@@ -41,17 +47,24 @@
<div class="d-none d-md-block mx-4 border-end border-dark" style="height: 100%; width: 1px;"></div>
</div>
<div class="d-flex flex-column text-center text-md-start align-self-center" style="width: min(100%, 250px)">
<div *ngIf="form.controls['subject'].value != 'Castra'">
<h3>Robert Sacco</h3>
<h4 class="mb-0">Legio XXX President</h4>
<h5>Portrays: Aquilifer Primus Marius Maximus</h5>
<a href="mailto:primuspiluslxxx@gmail.com" target="_blank">primuspiluslxxx@gmail.com</a>
</div>
<div *ngIf="form.controls['subject'].value == 'Castra'">
<h3>Tom Ross</h3>
<h4 class="mb-0">Legio XXX <em>Patronus</em> (Patron)</h4>
<h5>Portrays: Titus Quartinius Saturnalus</h5>
<a href="mailto:tomlongwoods@gmail.com" target="_blank">tomlongwoods@gmail.com</a>
<div class="d-flex flex-column text-center text-md-start align-self-center" style="width: min(100%, 250px)">
<div
class="mb-3"
[ngClass]="form.controls['subject'].value != 'Castra' ? '' : 'opacity-50'"
>
<h3>Robert Sacco</h3>
<h4 class="mb-0">Legio XXX President</h4>
<h5>Portrays: Aquilifer Primus Marius Maximus</h5>
<a href="mailto:primuspiluslxxx@gmail.com" target="_blank">primuspiluslxxx@gmail.com</a>
</div>
<div
[ngClass]="form.controls['subject'].value == 'Castra' ? '' : 'opacity-50'"
>
<h3>Tom Ross</h3>
<h4 class="mb-0">Legio XXX <em>Patronus</em> (Patron)</h4>
<h5>Portrays: Titus Quartinius Saturnalus</h5>
<a href="mailto:tomlongwoods@gmail.com" target="_blank">tomlongwoods@gmail.com</a>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import {Component} from '@angular/core';
import {FormBuilder, FormGroup} from '@angular/forms';
import {MomentumService} from '../../services/momentum.service';
@Component({
selector: 'app-contact',
@@ -7,18 +8,34 @@ import {FormBuilder, FormGroup} from '@angular/forms';
})
export class ContactComponent {
public error = false;
public success = false;
public form!: FormGroup;
constructor(private fb: FormBuilder) {
constructor(private fb: FormBuilder, private momentum: MomentumService) {
this.form = fb.group({
name: '',
email: '',
subject: '',
body: '',
});
}
public async submitEmail(): Promise<void> {
this.error = false;
this.success = false;
try {
await this.momentum.api.data.create(MomentumService.SCHEMA['contact'], this.form.value);
this.success = true;
this.form.disable();
} catch (error: any) {
this.error = error.message;
}
}
reset() {
this.error = false;
this.success = false;
this.form.reset();
}
}

View File

@@ -1,24 +1,16 @@
<footer>
<div class="social text-center py-3" style="background: #990000">
<h2 class="mb-4">Follow us on social media</h2>
<div class="d-flex justify-content-around mx-auto transparent-link" style="max-width: 300px">
<a href="https://discord.gg/wW458KYR79" target="_blank">
<i class="fa-brands fa-discord fa-2xl"></i>
</a>
<a href="https://facebook.com" target="_blank" aria-label="Facebook">
<i class="fa-brands fa-facebook fa-2xl"></i>
</a>
<a href="https://instagram.com" target="_blank" aria-label="Instagram">
<i class="fa-brands fa-instagram fa-2xl"></i>
</a>
<a href="https://tiktok.com" target="_blank" aria-label="TikTok">
<i class="fa-brands fa-tiktok fa-2xl"></i>
</a>
<a href="https://youtube.com" target="_blank" aria-label="Youtube">
<i class="fa-brands fa-youtube fa-2xl"></i>
</a>
<div class="text-center d-flex flex-row justify-content-center align-items-center gap-3">
<hr class="flex-grow-1 m-0 opacity-75">
<div class="d-flex flex-column align-items-end">
<h2 class="mb-0">Ready to Enlist?</h2>
<p class="mb-0">Take your place in our ranks...</p>
</div>
<div class="d-flex align-items-center">
<a [routerLink]="['/register']" class="btn btn-light btn-lg">JOIN NOW</a>
</div>
<hr class="flex-grow-1 m-0 opacity-75">
</div>
<h3 class="mt-4 mb-0">so we can invade your feed ⚔️</h3>
</div>
<div class="bg-dark text-center text-sm-start">
<div class="d-flex flex-column flex-sm-row flex-wrap justify-content-center container p-3 pb-0">
@@ -45,7 +37,7 @@
<div class="py-3 text-center">
<p class="copyright m-0">
Copyright &copy; Legio XXX 2024 | All Rights Reserved<br>
Created by <a href="https://zakscode.com" target="_blank">Zak Timson</a>
Created by <a href="https://zakscode.com" target="_blank">Zak Timson</a> | Built with <a href="https://momentum.zakscode.com" target="_blank">Momentum</a>
</p>
</div>
</footer>

View File

@@ -1,5 +1,6 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {MomentumService} from '../../services/momentum.service';
import {Photo} from '../models/photo';
export interface ImageViewerOptions {
@@ -10,12 +11,13 @@ export interface ImageViewerOptions {
@Component({
selector: 'xxx-viewer',
styles: [`
.close {
.close, .delete {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 100000;
}
.close { right: 1rem; }
.delete { right: 4rem; }
::ng-deep .cdk-overlay-pane {
max-width: 100% !important;
}
@@ -24,18 +26,32 @@ export interface ImageViewerOptions {
}
`],
template: `
<button class="close" mat-icon-button aria-label="close dialog" mat-dialog-close>
<button class="close" mat-icon-button mat-dialog-close>
<mat-icon>close</mat-icon>
</button>
<xxx-carousel [photos]="photos" [index]="index" [disableAutoplay]="true"></xxx-carousel>
<button *ngIf="momentum.admin | async" class="delete me-3" mat-icon-button (click)="delete()">
<mat-icon>delete</mat-icon>
</button>
<xxx-carousel [photos]="photos" [index]="index" [disableAutoplay]="true" (indexChange)="index = $event"></xxx-carousel>
`
})
export class ImageViewerComponent {
index!: number;
photos!: Photo[];
constructor(public ref: MatDialogRef<ImageViewerComponent>, @Inject(MAT_DIALOG_DATA) data: ImageViewerOptions) {
constructor(
public ref: MatDialogRef<ImageViewerComponent>,
public momentum: MomentumService,
@Inject(MAT_DIALOG_DATA) data: ImageViewerOptions
) {
this.index = data.index || 0;
this.photos = data.photos || [];
}
async delete() {
if (!confirm('Delete this photo?')) return;
const photo = this.photos[this.index];
await this.momentum.api.storage.delete(photo.path);
this.ref.close({deleted: photo});
}
}

View File

@@ -1,4 +1,5 @@
export interface Photo {
alt: string;
src: string;
path: string;
}

View File

@@ -18,19 +18,43 @@
<mat-menu #menu="matMenu">
<ng-container *ngFor="let section of group.children; let first = first">
<mat-divider *ngIf="!first"></mat-divider>
<button *ngFor="let item of section" mat-menu-item [routerLink]="item.url" [fragment]="item.fragment">
<button *ngFor="let item of section" mat-menu-item (click)="openItem(item)">
{{item.label}}
</button>
</ng-container>
<!-- Auth items, Members menu only -->
<ng-container *ngIf="group.label === 'Members'">
<mat-divider></mat-divider>
<ng-container *ngIf="momentum.isLoggedIn | async; else guestItems">
<button mat-menu-item (click)="momentum.api.auth.logout()">Logout</button>
</ng-container>
<ng-template #guestItems>
<button mat-menu-item (click)="momentum.api.auth.handleLogin()">Login</button>
<button mat-menu-item (click)="openItem({label:'Register', url:'/register'})">Register</button>
</ng-template>
</ng-container>
</mat-menu>
</ng-container>
<button mat-button [matMenuTriggerFor]="menu2" class="navbar-button">
Members <mat-icon>expand_more</mat-icon>
</button>
<mat-menu #menu2="matMenu">
<ng-container *ngIf="momentum.isLoggedIn | async; else guestItems">
<button *ngIf="momentum.admin | async" mat-menu-item (click)="openAdmin()">Admin</button>
<button mat-menu-item (click)="momentum.api.auth.logout()">Logout</button>
</ng-container>
<ng-template #guestItems>
<button mat-menu-item (click)="momentum.api.auth.handleLogin()">Login</button>
<button mat-menu-item (click)="openItem({label:'Register', url:'/register'})">Register</button>
</ng-template>
</mat-menu>
</div>
<!-- Mobile NavBar -->
<!-- Mobile NavBar-->
<button *ngIf="hamburger" mat-icon-button class="text-start" [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let item of links.topLevel" [routerLink]="item.url" [fragment]="item.fragment">
<button mat-menu-item *ngFor="let item of links.topLevel" (click)="openItem(item)">
{{item.label}}
</button>
<ng-container *ngFor="let group of links.other">
@@ -44,6 +68,19 @@
</ng-container>
</mat-menu>
</ng-container>
<button mat-menu-item [matMenuTriggerFor]="menu3" class="navbar-button">
Members
</button>
<mat-menu #menu3="matMenu">
<ng-container *ngIf="momentum.isLoggedIn | async; else guestItems">
<button *ngIf="momentum.admin | async" mat-menu-item (click)="openAdmin()">Admin</button>
<button mat-menu-item (click)="momentum.api.auth.logout()">Logout</button>
</ng-container>
<ng-template #guestItems>
<button mat-menu-item (click)="momentum.api.auth.handleLogin()">Login</button>
<button mat-menu-item (click)="openItem({label:'Register', url:'/register'})">Register</button>
</ng-template>
</mat-menu>
</mat-menu>
</mat-toolbar-row>
</mat-toolbar>

View File

@@ -1,8 +1,8 @@
import {AfterViewInit, Component, EventEmitter, Input, OnDestroy, Output} from '@angular/core';
import {ActivatedRoute, NavigationEnd, NavigationStart, Router} from '@angular/router';
import {ActivatedRoute, NavigationStart, Router} from '@angular/router';
import {combineLatest, filter, Subscription} from 'rxjs';
import {NAVIGATION} from '../../misc/navigation';
import {BreakpointService} from '../../services/breakpoint.service';
import {NAVIGATION, NavigationItem} from '../../misc/navigation';
import {MomentumService} from '../../services/momentum.service';
@Component({
selector: 'xxx-navbar',
@@ -21,7 +21,7 @@ export class NavbarComponent implements AfterViewInit, OnDestroy {
@Output() hamburgerClick = new EventEmitter<void>();
constructor(private route: ActivatedRoute, private router: Router, public breakpoint: BreakpointService) { }
constructor(private route: ActivatedRoute, private router: Router, public momentum: MomentumService) { }
ngAfterViewInit() {
this.sub = combineLatest([this.router.events.pipe(filter((e: any) => e.navigationTrigger != 'popstate' || e instanceof NavigationStart)), this.route.fragment]).subscribe(([url, frag]) => {
@@ -34,6 +34,19 @@ export class NavbarComponent implements AfterViewInit, OnDestroy {
if(this.sub) this.sub.unsubscribe();
}
openAdmin() {
location.href = '/ui';
}
openItem(item: NavigationItem) {
// Full url
if(item.url.startsWith('http'))
location.href = item.url;
// Relative
else
this.router.navigate([item.url], {fragment: item.fragment});
}
scroll(id: string) {
const el = document.getElementById(id);
if(el) el.scrollIntoView({behavior: 'smooth'});

View File

@@ -0,0 +1,16 @@
<h2 mat-dialog-title>Upload Photos</h2>
<mat-dialog-content>
<p class="text-muted">Select the year these photos were taken then choose your files.</p>
<mat-form-field appearance="outline" class="w-100">
<mat-label>Year</mat-label>
<mat-select [(ngModel)]="year">
<mat-option *ngFor="let y of years" [value]="y">{{y}}</mat-option>
</mat-select>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-raised-button color="primary" (click)="upload()" [disabled]="uploading">
<mat-icon>upload</mat-icon> Choose Files
</button>
</mat-dialog-actions>

View File

@@ -0,0 +1,21 @@
import {Component} from '@angular/core';
import {MatDialogRef} from '@angular/material/dialog';
import {MomentumService} from '../../services/momentum.service';
@Component({
selector: 'xxx-uploader',
templateUrl: './uploader.component.html',
})
export class UploaderComponent {
year: number = new Date().getFullYear();
years: number[] = Array.from({length: this.year - 2006}, (_, i) => 2007 + i).reverse();
uploading = false;
constructor(private momentum: MomentumService, private ref: MatDialogRef<UploaderComponent>) {}
async upload() {
this.uploading = true;
await this.momentum.api.storage.upload(`Photos/Submissions/${this.year}`, undefined, {multiple: true, accept: 'image/*'});
this.ref.close();
}
}

View File

@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
import {NavigationEnd, Router} from '@angular/router';
import {combineLatest, filter, Subscription} from 'rxjs';
import {BreakpointService} from '../../services/breakpoint.service';
import {MomentumService} from '../../services/momentum.service';
@Component({
selector: 'app-root',
@@ -14,7 +15,7 @@ export class AppComponent {
mobile = false;
open = false;
constructor(private breakpoint: BreakpointService, private router: Router) {
constructor(private breakpoint: BreakpointService, private momentum: MomentumService, private router: Router) {
this.sub = combineLatest([
router.events.pipe(filter(event => event instanceof NavigationEnd)),
breakpoint.isMobile$
@@ -24,6 +25,10 @@ export class AppComponent {
})
}
ngOnInit(): void {
this.momentum.api.client.inject();
}
ngOnDestroy() {
if(this.sub) this.sub.unsubscribe();
}

View File

@@ -0,0 +1,24 @@
import {Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output} from '@angular/core';
@Directive({selector: '[inView]'})
export class InViewDirective implements OnInit, OnDestroy {
@Output() inView = new EventEmitter<void>();
private observer!: IntersectionObserver;
constructor(private el: ElementRef) {}
ngOnInit() {
this.observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
this.inView.emit();
this.observer.disconnect();
}
}, {rootMargin: '200px'});
this.observer.observe(this.el.nativeElement);
}
ngOnDestroy() {
this.observer.disconnect();
}
}

View File

@@ -1,25 +1,31 @@
import {NgModule} from '@angular/core';
import {MatButtonModule} from '@angular/material/button';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatNativeDateModule} from '@angular/material/core';
import {MatDatepickerModule} from '@angular/material/datepicker';
import {MatDialogModule} from '@angular/material/dialog';
import {MatDividerModule} from '@angular/material/divider';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatMenuModule} from '@angular/material/menu';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatSelectModule} from '@angular/material/select';
import {MatToolbarModule} from '@angular/material/toolbar';
export const MATERIAL_MODULES = [
MatButtonModule,
MatCheckboxModule,
MatDatepickerModule,
MatDialogModule,
MatDividerModule,
MatIconModule,
MatInputModule,
MatFormFieldModule,
MatMenuModule,
MatNativeDateModule,
MatSelectModule,
MatProgressSpinnerModule,
MatToolbarModule
];

View File

@@ -14,13 +14,6 @@ export const NAVIGATION: NavigationGroup = [
{label: 'About', url: '/about'},
{label: 'Contact', url: '/', fragment: 'contact'},
{label: 'Gallery', url: '/gallery'},
{label: 'Resources', url: '/info/resources'},
]]},
{label: 'Events', children: [[
{label: 'Castra Aestiva', url: '/events/castra-aestiva'},
{label: 'Castra Hiberna', url: '/events/castra-hiberna'},
], [
{label: 'Calendar', url: '/events/calendar'},
]]},
// {label: 'Learn', children: [[
// {label: 'Trajan', url: '/info/trajan'},
@@ -35,14 +28,21 @@ export const NAVIGATION: NavigationGroup = [
// {label: 'Glossary', url: '/info/glossary'},
// {label: 'Resources', url: '/info/resources'},
// ]]},
{label: 'Reenact', children: [[
{label: 'Reenact', children: [
[
{label: 'Getting Started', url: '/getting-started'},
{label: 'Rules & Regulations', url: '/rules'},
{label: 'Drill Commands', url: '/drill'},
], [
{label: 'Buy Equipment', url: '/buy'},
{label: 'Build Equipment', url: '/diy'},
{label: 'Equipment', url: '/equipment'},
{label: 'Maintenance', url: '/maintenance'},
{label: 'Resources', url: '/info/resources'},
{label: 'Rules & Regulations', url: '/rules'},
]
]},
{label: 'Events', children: [[
{label: 'Calendar', url: '/events/calendar'},
], [
{label: 'Castra Aestiva', url: '/events/castra-aestiva'},
{label: 'Castra Hiberna', url: '/events/castra-hiberna'},
]
]},
]

View File

@@ -1,5 +1,5 @@
import {Injectable} from '@angular/core';
import {Momentum, type User} from '@ztimson/momentum';
import {Momentum} from '@ztimson/momentum';
import {BehaviorSubject} from 'rxjs';
import {from, map} from 'rxjs';
import {filter} from 'rxjs/operators';
@@ -18,7 +18,7 @@ declare global {
@Injectable({providedIn: 'root'})
export class MomentumService {
static SCHEMA: {[key: string]: string} = {
// TODO: Add paths
contact: 'Contact',
}
api!: Momentum;
@@ -29,19 +29,23 @@ export class MomentumService {
// @ts-ignore
user = new BehaviorSubject<User | null | undefined>(undefined); // Undefined at init, null when logged out, object when logged in.
admin = from(this.user).pipe(filter((u: any) => u !== undefined), map((u: User | null) => u?.groups.includes('admin')));
admin = from(this.user).pipe(filter(u => u !== undefined), map(u => this.api.permissions.has('admin')));
isLoggedIn = from(this.user).pipe(filter(u => u !== undefined), map(Boolean));
constructor() {
this.api = new Momentum("https://legio-30.org", {
this.api = window['momentum'] = new Momentum("https://legio-30.org", {
app: "Website",
analytics: "prompt",
logLevel: "ERROR",
persist: true,
});
this.api.auth.on('login', () => this.user.next(this.api.auth.user));
this.api.auth.on('logout', () => location.reload());
this.api.client.on('install', () => this.installable.next(this.api.client.canInstall));
this.installable.next(this.api.client.canInstall);
this.api.auth.readSession();
this.api.settings.sync((event, value) => {
this.title.next(value['title']);
this.settings.next(value);

View File

@@ -35,7 +35,11 @@
<li>Canadian Museum of History (formerly the Museum of Civilization), Gatineau PQ (2005)</li>
<li>Royal Ontario Museum, Toronto ON (2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2023, 2024)</li>
<li>RCR Museum, Wolseley Barracks, London ON (2019)</li>
<li>Ontario Regiment RCAC Museum, Whitby, ON (2023, 2024)</li>
<li>Ontario Regiment RCAC Museum, Whitby, ON</li>
<ul>
<li>Aquino Weekend (2023, 2025)</li>
<li>The Evolution of Soldiers and Vehicles (2024)</li>
</ul>
</ul>
</div>
<div class="mx-3">
@@ -61,7 +65,8 @@
<div class="mx-3">
<h4 class="mb-2">Re-enactments</h4>
<ul class="mt-0">
<li>Fort Malden, Amherstburg ON (2004, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2015, 2016, 2024)</li>
<li>Hastings Medieval Festival, Hastings, ON (2025)</li>
<li>Fort Malden, Amherstburg ON (2004, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2015, 2016, 2024, 2025, 2026)</li>
<li>Roman Market Days (2004)</li>
<li>NE Route March, Erie Canal (2005)</li>
<li>Roman Days (2005)</li>
@@ -87,14 +92,14 @@
</ul>
<p class="mb-2">It continues to host our annual events:</p>
<ul class="mt-0">
<li>Castra Aestiva (2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2022, 2023, 2024, 2025)</li>
<li>Castra Hiberna (2017, 2018, 2021, 2022, 2023)</li>
<li>Castra Aestiva (2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2022, 2023, 2024, 2025, 2026)</li>
<li>Castra Hiberna (2017, 2018, 2021, 2022, 2023, 2024, 2025)</li>
</ul>
<h2>Special Thanks</h2>
<p>Tom Ross - Legio XXX's <em>patronus</em> (patron) providing re-enactment facilities</p>
<p>Robert Sacco - <em>Aquilifer</em> (Eagle bearer) & Legio XXX's president</p>
<p>David Blain - <em>Tribunus angusticlavius</em> (staff officer) & subject matter expert</p>
<p>Robert Norton - <em>Armicustos</em> (Quartermaster) who provides equipment, repairs & <a routerLink="/diy">manuals</a></p>
<p>Robert Norton - <em>Armicustos</em> (Quartermaster) who provides equipment, repairs & <a routerLink="/equipment">manuals</a></p>
</div>
</div>
</div>

View File

@@ -2,12 +2,105 @@
<div class="cap-width py-5 px-3">
<div class="d-flex align-items-end justify-content-between mb-2">
<h1 class="mb-0">Calendar</h1>
<a class="d-none d-print-none d-sm-flex justify-content-center text-muted" href="javascript:window.print()">
<mat-icon class="me-1">print</mat-icon>
Print
</a>
<div class="d-flex gap-3 align-items-center">
<ng-container *ngIf="momentum.admin | async">
<button class="d-print-none" mat-raised-button color="primary" (click)="openCreate()">
<mat-icon>add</mat-icon> Add Event
</button>
</ng-container>
<a class="d-none d-print-none d-sm-flex justify-content-center text-muted" href="javascript:window.print()">
<mat-icon class="me-1">print</mat-icon>
Print
</a>
</div>
</div>
<mat-divider class="mb-4"></mat-divider>
<iframe src="https://calendar.google.com/calendar/embed?height=600&wkst=1&bgcolor=%23ffffff&ctz=America%2FToronto&showTitle=0&showNav=1&showPrint=1&showTabs=0&showCalendars=0&src=Y18wYTIxNTM3ZDgwMWMzNTQ0MTUwYzk5YTUwNjI5Yjc5MjM0MjYyYjY0YzYyNzZiZTQ3OWJlMzA4OWU0MmM3NTQ5QGdyb3VwLmNhbGVuZGFyLmdvb2dsZS5jb20&color=%23D50000" style="border-width:0" width="100%" height="600" frameborder="0" scrolling="no"></iframe>
<div class="d-flex align-items-stretch mb-5 p-3" style="background:#f6eed9">
<div style="width:3px;background:currentColor;opacity:.4" class="me-3 flex-shrink-0"></div>
<div class="py-1">
<div class="text-muted small text-uppercase" style="letter-spacing:.15em">Every Month</div>
<div class="fw-bold fs-5">Monthly Online Meetup</div>
<div class="text-muted small mb-2">Join us online for our regular group gathering</div>
<a class="d-print-none" mat-stroked-button href="/register">Register Now</a>
</div>
</div>
<ng-container *ngIf="!loading; else spinner">
<ng-container *ngIf="events.length">
<div *ngFor="let group of groupedEvents" class="mb-5">
<div class="text-uppercase small fw-bold mb-3" style="letter-spacing:.2em">{{group.month}}</div>
<!-- Added print-border class and edit button -->
<div *ngFor="let event of group.events" class="d-flex align-items-stretch mb-3 p-3 event-card" style="background:#f6eed9">
<div class="text-muted small text-end flex-shrink-0 pe-3" style="min-width:3.5rem">
<div class="fw-bold" style="font-size:1.1rem;line-height:1">{{event.date | date:'d'}}</div>
<div>{{event.date | date:'EEE'}}</div>
<div *ngIf="event.endDate" class="mt-1" style="border-top:1px solid currentColor;opacity:.4;padding-top:.25rem">
<div class="fw-bold" style="font-size:1.1rem;line-height:1">{{event.endDate | date:'d'}}</div>
<div>{{event.endDate | date:'EEE'}}</div>
</div>
</div>
<div style="width:2px;background:currentColor;opacity:.15" class="flex-shrink-0 me-3"></div>
<div class="flex-grow-1">
<div class="fw-bold">{{event.title}}</div>
<div class="text-muted small" *ngIf="event.location">{{event.location}}</div>
<div class="text-muted small" *ngIf="!event.endDate">{{event.date | date:'h:mm a'}}</div>
<div class="text-muted small" *ngIf="event.endDate">
{{event.date | date:'MMM d, h:mm a'}} {{event.endDate | date:'MMM d, h:mm a'}}
</div>
<div class="text-muted small fst-italic mt-1" *ngIf="event.description">{{event.description}}</div>
</div>
<!-- Edit button, screen only -->
<ng-container *ngIf="momentum.admin | async">
<button class="d-print-none align-self-start ms-2" mat-icon-button (click)="openEdit(event)">
<mat-icon>edit</mat-icon>
</button>
<button class="d-print-none align-self-start" mat-icon-button (click)="deleteEvent(event)">
<mat-icon>delete</mat-icon>
</button>
</ng-container>
</div>
</div>
</ng-container>
</ng-container>
<ng-template #spinner>
<div class="text-center py-5"><mat-spinner class="mx-auto"></mat-spinner></div>
</ng-template>
</div>
</div>
<!-- Create/Edit Dialog -->
<ng-template #createDialog>
<h2 mat-dialog-title>{{editingId ? 'Edit Event' : 'Add Event'}}</h2>
<mat-dialog-content>
<mat-form-field class="w-100 mb-2">
<mat-label>Title</mat-label>
<input matInput [(ngModel)]="form.title" required />
</mat-form-field>
<mat-form-field class="w-100 mb-2">
<mat-label>Date & Time</mat-label>
<input matInput type="datetime-local" [(ngModel)]="form.date" required />
</mat-form-field>
<mat-form-field class="w-100 mb-2">
<mat-label>End Date & Time (optional)</mat-label>
<input matInput type="datetime-local" [(ngModel)]="form.endDate" />
</mat-form-field>
<mat-form-field class="w-100 mb-2">
<mat-label>Location</mat-label>
<input matInput [(ngModel)]="form.location" />
</mat-form-field>
<mat-form-field class="w-100">
<mat-label>Description</mat-label>
<textarea matInput [(ngModel)]="form.description" rows="3"></textarea>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Cancel</button>
<button mat-raised-button color="primary" (click)="saveEvent()" [disabled]="saving">
{{saving ? 'Saving...' : 'Save'}}
</button>
</mat-dialog-actions>
</ng-template>

View File

@@ -1,7 +1,90 @@
import {Component} from '@angular/core';
import {Component, OnInit, TemplateRef, ViewChild} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {MomentumService} from '../../../services/momentum.service';
type CalEvent = {
_id?: string;
title: string;
date: string;
endDate?: string;
location?: string;
description?: string;
};
type EventGroup = {
month: string;
events: CalEvent[];
};
@Component({
selector: 'xxx-calendar',
templateUrl: './calendar.component.html'
})
export class CalendarComponent { }
export class CalendarComponent implements OnInit {
@ViewChild('createDialog') createDialog!: TemplateRef<any>;
events: CalEvent[] = [];
groupedEvents: EventGroup[] = [];
loading = true;
saving = false;
form: Partial<CalEvent> = {};
editingId: string | null = null;
constructor(private dialog: MatDialog, public momentum: MomentumService) {}
async ngOnInit() {
await this.loadEvents();
}
async deleteEvent(event: CalEvent) {
if (!event._id || !confirm(`Delete "${event.title}"?`)) return;
await this.momentum.api.data.delete('Events', <any>event._id);
await this.loadEvents();
}
async loadEvents() {
this.loading = true;
const raw = await this.momentum.api.data.read<CalEvent>('Events') as CalEvent[];
const now = new Date();
this.events = raw
.filter(e => new Date(e.date) >= now)
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
this.groupedEvents = this.groupByMonth(this.events);
this.loading = false;
}
groupByMonth(events: CalEvent[]): EventGroup[] {
const map = new Map<string, CalEvent[]>();
for (const e of events) {
const key = new Date(e.date).toLocaleDateString('en-US', {month: 'long', year: 'numeric'});
if (!map.has(key)) map.set(key, []);
map.get(key)!.push(e);
}
return Array.from(map.entries()).map(([month, events]) => ({month, events}));
}
openCreate() {
this.editingId = null;
this.form = {};
this.dialog.open(this.createDialog, {width: '480px'});
}
openEdit(event: CalEvent) {
this.editingId = event._id ?? null;
this.form = {...event};
this.dialog.open(this.createDialog, {width: '480px'});
}
async saveEvent() {
if (!this.form.title || !this.form.date) return;
this.saving = true;
if (this.editingId) {
await this.momentum.api.data.update<CalEvent>('Events', <any>{_id: this.editingId, ...this.form});
} else {
await this.momentum.api.data.create<CalEvent>('Events', <any>this.form);
}
this.dialog.closeAll();
this.saving = false;
await this.loadEvents();
}
}

View File

@@ -1,8 +1,22 @@
<div class="invert p-5">
<div *ngFor="let album of photos" class="mb-5">
<h1 class="mb-0">{{album.album}}</h1>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="display-4 mb-0">Gallery</h1>
<ng-container *ngIf="momentum.isLoggedIn | async">
<button mat-raised-button color="primary" [matMenuTriggerFor]="uploadMenu">
<mat-icon>upload</mat-icon> Upload Photos
</button>
<mat-menu #uploadMenu="matMenu">
<button mat-menu-item *ngFor="let y of uploadYears" (click)="upload(y)">{{y}}</button>
</mat-menu>
</ng-container>
</div>
<div *ngFor="let album of photos" class="mb-5" (inView)="loadAlbum(album)">
<h1 class="display-6 mb-0">{{album.album}}</h1>
<mat-divider class="mb-3"></mat-divider>
<div *ngFor="let photo of album.photos; let i = index" class="d-inline-block me-3 mb-3">
<div *ngFor="let photo of album.photos" class="d-inline-block me-3 mb-3">
<xxx-placeholder [src]="photo.src" [alt]="photo.alt" height="150px" (click)="open(photo)"></xxx-placeholder>
</div>
</div>

View File

@@ -2,3 +2,17 @@ xxx-placeholder:hover ::ng-deep img {
transform: scale(1.1);
cursor: pointer;
}
.photo-wrap {
.delete-btn {
display: none;
position: absolute;
top: 4px;
right: 4px;
transform: scale(0.75);
}
&:hover .delete-btn {
display: block;
}
}

View File

@@ -1,196 +1,74 @@
import {Component} from '@angular/core';
import {Component, OnInit} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {MomentumService} from '../../services/momentum.service';
import {ImageViewerComponent} from '../../components/image-viewer/image-viewer.component';
import {Photo} from '../../components/models/photo';
type Album = {album: string, photos: Photo[], loaded: boolean};
@Component({
selector: 'xxx-gallery',
templateUrl: './gallery.component.html',
styleUrls: ['./gallery.component.scss'],
})
export class GalleryComponent {
photos: {album: string, photos: Photo[]}[] = [
{album: '2023', photos: [
{alt: '', src: '/assets/img/gallery/2023/001.jpg'},
{alt: '', src: '/assets/img/gallery/2023/002.jpg'},
{alt: '', src: '/assets/img/gallery/2023/003.jpg'},
{alt: '', src: '/assets/img/gallery/2023/004.jpg'},
{alt: '', src: '/assets/img/gallery/2023/005.jpg'},
{alt: '', src: '/assets/img/gallery/2023/006.jpg'},
{alt: '', src: '/assets/img/gallery/2023/007.jpg'},
{alt: '', src: '/assets/img/gallery/2023/008.jpg'},
{alt: '', src: '/assets/img/gallery/2023/009.jpg'},
{alt: '', src: '/assets/img/gallery/2023/010.jpg'},
{alt: '', src: '/assets/img/gallery/2023/011.jpg'},
]}, {album: '2022', photos: [
{alt: '', src: '/assets/img/gallery/2022/001.jpg'},
{alt: '', src: '/assets/img/gallery/2022/002.jpg'},
{alt: '', src: '/assets/img/gallery/2022/003.jpg'},
{alt: '', src: '/assets/img/gallery/2022/004.jpg'},
{alt: '', src: '/assets/img/gallery/2022/005.jpg'},
{alt: '', src: '/assets/img/gallery/2022/006.jpg'},
{alt: '', src: '/assets/img/gallery/2022/007.jpg'},
{alt: '', src: '/assets/img/gallery/2022/008.jpg'},
{alt: '', src: '/assets/img/gallery/2022/009.jpg'},
{alt: '', src: '/assets/img/gallery/2022/010.jpg'},
{alt: '', src: '/assets/img/gallery/2022/011.jpg'},
{alt: '', src: '/assets/img/gallery/2022/012.jpg'},
{alt: '', src: '/assets/img/gallery/2022/013.jpg'},
{alt: '', src: '/assets/img/gallery/2022/014.jpg'},
{alt: '', src: '/assets/img/gallery/2022/015.jpg'},
{alt: '', src: '/assets/img/gallery/2022/016.jpg'},
{alt: '', src: '/assets/img/gallery/2022/017.jpg'},
]}, {album: '2021', photos: [
{alt: '', src: '/assets/img/gallery/2021/001.jpg'},
{alt: '', src: '/assets/img/gallery/2021/002.jpg'},
{alt: '', src: '/assets/img/gallery/2021/003.jpg'},
{alt: '', src: '/assets/img/gallery/2021/004.jpg'},
{alt: '', src: '/assets/img/gallery/2021/005.jpg'},
]}, {album: '2019', photos: [
{alt: '', src: '/assets/img/gallery/2019/001.jpg'},
{alt: '', src: '/assets/img/gallery/2019/002.jpg'},
{alt: '', src: '/assets/img/gallery/2019/003.jpg'},
{alt: '', src: '/assets/img/gallery/2019/004.jpg'},
]}, {album: '2018', photos: [
{alt: '', src: '/assets/img/gallery/2018/001.jpg'},
{alt: '', src: '/assets/img/gallery/2018/002.jpg'},
{alt: '', src: '/assets/img/gallery/2018/003.jpg'},
{alt: '', src: '/assets/img/gallery/2018/004.jpg'},
{alt: '', src: '/assets/img/gallery/2018/005.jpg'},
{alt: '', src: '/assets/img/gallery/2018/006.jpg'},
{alt: '', src: '/assets/img/gallery/2018/007.jpg'},
{alt: '', src: '/assets/img/gallery/2018/008.jpg'},
]}, {album: '2017', photos: [
{alt: '', src: '/assets/img/gallery/2017/001.jpg'},
{alt: '', src: '/assets/img/gallery/2017/002.jpg'},
{alt: '', src: '/assets/img/gallery/2017/003.jpg'},
{alt: '', src: '/assets/img/gallery/2017/004.jpg'},
{alt: '', src: '/assets/img/gallery/2017/005.jpg'},
{alt: '', src: '/assets/img/gallery/2017/006.jpg'},
{alt: '', src: '/assets/img/gallery/2017/007.jpg'},
]}, {album: '2016', photos: [
{alt: '', src: '/assets/img/gallery/2016/001.jpg'},
{alt: '', src: '/assets/img/gallery/2016/002.jpg'},
{alt: '', src: '/assets/img/gallery/2016/003.jpg'},
{alt: '', src: '/assets/img/gallery/2016/004.jpg'},
{alt: '', src: '/assets/img/gallery/2016/005.jpg'},
{alt: '', src: '/assets/img/gallery/2016/006.jpg'},
{alt: '', src: '/assets/img/gallery/2016/007.jpg'},
{alt: '', src: '/assets/img/gallery/2016/008.jpg'},
{alt: '', src: '/assets/img/gallery/2016/009.jpg'},
{alt: '', src: '/assets/img/gallery/2016/010.jpg'},
{alt: '', src: '/assets/img/gallery/2016/011.jpg'},
{alt: '', src: '/assets/img/gallery/2016/012.jpg'},
{alt: '', src: '/assets/img/gallery/2016/013.jpg'},
{alt: '', src: '/assets/img/gallery/2016/014.jpg'},
{alt: '', src: '/assets/img/gallery/2016/015.jpg'},
{alt: '', src: '/assets/img/gallery/2016/016.jpg'},
{alt: '', src: '/assets/img/gallery/2016/017.jpg'},
{alt: '', src: '/assets/img/gallery/2016/018.jpg'},
{alt: '', src: '/assets/img/gallery/2016/019.jpg'},
]}, {album: '2014', photos: [
{alt: '', src: '/assets/img/gallery/2014/001.jpg'},
{alt: '', src: '/assets/img/gallery/2014/002.jpg'},
{alt: '', src: '/assets/img/gallery/2014/003.jpg'},
{alt: '', src: '/assets/img/gallery/2014/004.jpg'},
{alt: '', src: '/assets/img/gallery/2014/005.jpg'},
{alt: '', src: '/assets/img/gallery/2014/006.jpg'},
{alt: '', src: '/assets/img/gallery/2014/007.jpg'},
{alt: '', src: '/assets/img/gallery/2014/008.jpg'},
{alt: '', src: '/assets/img/gallery/2014/009.jpg'},
{alt: '', src: '/assets/img/gallery/2014/010.jpg'},
]}, {album: '2013', photos: [
{alt: '', src: '/assets/img/gallery/2013/001.jpg'},
{alt: '', src: '/assets/img/gallery/2013/002.jpg'},
{alt: '', src: '/assets/img/gallery/2013/003.jpg'},
{alt: '', src: '/assets/img/gallery/2013/004.jpg'},
{alt: '', src: '/assets/img/gallery/2013/005.jpg'},
{alt: '', src: '/assets/img/gallery/2013/006.jpg'},
]}, {album: '2012', photos: [
{alt: '', src: '/assets/img/gallery/2012/001.jpg'},
{alt: '', src: '/assets/img/gallery/2012/002.jpg'},
{alt: '', src: '/assets/img/gallery/2012/003.jpg'},
{alt: '', src: '/assets/img/gallery/2012/004.jpg'},
{alt: '', src: '/assets/img/gallery/2012/005.jpg'},
{alt: '', src: '/assets/img/gallery/2012/006.jpg'},
{alt: '', src: '/assets/img/gallery/2012/007.jpg'},
{alt: '', src: '/assets/img/gallery/2012/008.jpg'},
{alt: '', src: '/assets/img/gallery/2012/009.jpg'},
{alt: '', src: '/assets/img/gallery/2012/010.jpg'},
{alt: '', src: '/assets/img/gallery/2012/011.jpg'},
{alt: '', src: '/assets/img/gallery/2012/012.jpg'},
{alt: '', src: '/assets/img/gallery/2012/013.jpg'},
{alt: '', src: '/assets/img/gallery/2012/014.jpg'},
{alt: '', src: '/assets/img/gallery/2012/015.jpg'},
{alt: '', src: '/assets/img/gallery/2012/016.jpg'},
]}, {album: '2011', photos: [
{alt: '', src: '/assets/img/gallery/2011/001.jpg'},
{alt: '', src: '/assets/img/gallery/2011/002.jpg'},
{alt: '', src: '/assets/img/gallery/2011/003.jpg'},
{alt: '', src: '/assets/img/gallery/2011/004.jpg'},
{alt: '', src: '/assets/img/gallery/2011/005.jpg'},
{alt: '', src: '/assets/img/gallery/2011/006.jpg'},
]}, {album: '2010', photos: [
{alt: '', src: '/assets/img/gallery/2010/001.jpg'},
{alt: '', src: '/assets/img/gallery/2010/002.jpg'},
{alt: '', src: '/assets/img/gallery/2010/003.jpg'},
{alt: '', src: '/assets/img/gallery/2010/004.jpg'},
{alt: '', src: '/assets/img/gallery/2010/005.jpg'},
{alt: '', src: '/assets/img/gallery/2010/006.jpg'},
{alt: '', src: '/assets/img/gallery/2010/007.jpg'},
{alt: '', src: '/assets/img/gallery/2010/008.jpg'},
{alt: '', src: '/assets/img/gallery/2010/009.jpg'},
{alt: '', src: '/assets/img/gallery/2010/010.jpg'},
{alt: '', src: '/assets/img/gallery/2010/011.jpg'},
{alt: '', src: '/assets/img/gallery/2010/012.jpg'},
{alt: '', src: '/assets/img/gallery/2010/013.jpg'},
]}, {album: '2009', photos: [
{alt: '', src: '/assets/img/gallery/2009/001.jpg'},
{alt: '', src: '/assets/img/gallery/2009/002.jpg'},
{alt: '', src: '/assets/img/gallery/2009/003.jpg'},
{alt: '', src: '/assets/img/gallery/2009/004.jpg'},
{alt: '', src: '/assets/img/gallery/2009/005.jpg'},
{alt: '', src: '/assets/img/gallery/2009/006.jpg'},
{alt: '', src: '/assets/img/gallery/2009/007.jpg'},
{alt: '', src: '/assets/img/gallery/2009/008.jpg'},
{alt: '', src: '/assets/img/gallery/2009/009.jpg'},
{alt: '', src: '/assets/img/gallery/2009/010.jpg'},
{alt: '', src: '/assets/img/gallery/2009/011.jpg'},
{alt: '', src: '/assets/img/gallery/2009/012.jpg'},
]}, {album: '2008', photos: [
{alt: '', src: '/assets/img/gallery/2008/001.jpg'},
{alt: '', src: '/assets/img/gallery/2008/002.jpg'},
{alt: '', src: '/assets/img/gallery/2008/003.jpg'},
{alt: '', src: '/assets/img/gallery/2008/004.jpg'},
{alt: '', src: '/assets/img/gallery/2008/005.jpg'},
{alt: '', src: '/assets/img/gallery/2008/006.jpg'},
{alt: '', src: '/assets/img/gallery/2008/007.jpg'},
{alt: '', src: '/assets/img/gallery/2008/008.jpg'},
]}, {album: '2007', photos: [
{alt: '', src: '/assets/img/gallery/2007/001.jpg'},
{alt: '', src: '/assets/img/gallery/2007/002.jpg'},
{alt: '', src: '/assets/img/gallery/2007/003.jpg'},
{alt: '', src: '/assets/img/gallery/2007/004.jpg'},
{alt: '', src: '/assets/img/gallery/2007/005.jpg'},
{alt: '', src: '/assets/img/gallery/2007/006.jpg'},
]}];
export class GalleryComponent implements OnInit {
photos: Album[] = [];
constructor(private dialog: MatDialog) {}
constructor(private dialog: MatDialog, public momentum: MomentumService) {}
get flatten() {
return this.photos.reduce((acc: any[], album) => {
return [...acc, ...album.photos];
}, []);
uploadYears = Array.from({length: new Date().getFullYear() - 2006}, (_, i) => 2007 + i).reverse();
async upload(year: number) {
await this.momentum.api.storage.upload(`Photos/Gallery/${year}`, undefined, {multiple: true, accept: 'image/*'});
await this.loadAlbums();
}
open(photo: any) {
async ngOnInit() {
await this.loadAlbums();
}
async loadAlbums() {
const files = await this.momentum.api.storage.all('Photos/Gallery');
const albumMap = new Map<string, null>();
for (const file of files.filter((f: any) => f.mime !== 'directory')) {
const album = file.path.split('/')[3];
albumMap.set(album, null);
}
this.photos = [...albumMap.keys()]
.sort((a, b) => +b - +a)
.map(album => ({album, photos: [], loaded: false}));
}
async loadAlbum(album: Album) {
if (album.loaded) return;
album.loaded = true;
const files = await this.momentum.api.storage.all(`Photos/Gallery/${album.album}`);
album.photos = files
.filter((f: any) => f.mime !== 'directory')
.map((file: any) => ({
alt: file.name,
src: this.momentum.api.storage.open(file.path, false) as string,
path: file.path
}));
}
get flatten() {
return this.photos.reduce((acc: Photo[], album) => [...acc, ...album.photos], []);
}
open(photo: Photo) {
const flat = this.flatten;
const index = flat.findIndex(p => p.src == photo.src);
const index = flat.findIndex(p => p.src === photo.src);
this.dialog.open(ImageViewerComponent, {
width: '100%',
height: '100%',
autoFocus: false,
data: {index, photos: flat}
}).afterClosed().subscribe(result => {
if (result?.deleted) this.loadAlbums();
});
}
}

View File

@@ -2,6 +2,7 @@
<header id="banner" style="height: calc(100vh - 64px)">
<xxx-banner></xxx-banner>
</header>
<!-- ABout -->
<section id="about" class="d-flex flex-column flex-md-row align-items-center justify-content-center" style="height: 100vh">
<div class="d-none d-md-inline flex-grow-1 text-end" style="flex-basis: 0">
@@ -20,34 +21,54 @@
that recreate the lives of soldiers found in Trajan's legions during the 1st - 2nd Century AD
</p>
</div>
<a routerLink="/about">More</a>
<a routerLink="/about" class="d-block mt-4" style="font-size: 2em;">More >></a>
</div>
<div class="flex-md-grow-1" style="flex-basis: 0">
<img class="mt-5" src="/assets/img/standard.png" alt="Legio XXX Standard" height="250px" width="auto">
</div>
</section>
<!-- Discord -->
<section class="d-flex" style="background-color: #990000">
<div class="d-flex flex-grow-1">
<div class="h-100 w-100" style="background: #7289d9"></div>
<img class="d-block d-md-none" src="/assets/img/discord.png" style="height: 100px; transform: translateX(-1px);">
<img class="d-none d-md-block" src="/assets/img/discord.png" style="height: 200px; transform: translateX(-1px);">
</div>
<div class="d-flex justify-content-start align-items-center flex-grow-1 p-3">
<!--<section class="d-flex" style="background-color: #990000">-->
<!-- <div class="d-flex flex-grow-1">-->
<!-- <div class="h-100 w-100" style="background: #7289d9"></div>-->
<!-- <img class="d-block d-md-none" src="/assets/img/discord.png" style="height: 100px; transform: translateX(-1px);">-->
<!-- <img class="d-none d-md-block" src="/assets/img/discord.png" style="height: 200px; transform: translateX(-1px);">-->
<!-- </div>-->
<!-- <div class="d-flex justify-content-start align-items-center flex-grow-1 p-3">-->
<!-- <div class="d-block text-center">-->
<!-- <h1 class="d-block m-0 mb-md-3 transparent-link">-->
<!-- <a href="https://discord.gg/wW458KYR79" target="_blank">Join us on Discord</a>-->
<!-- </h1>-->
<!-- <div class="d-none d-md-inline">-->
<!-- <p>-->
<!-- Ask us questions, get involved-->
<!-- <br><br>-->
<!-- and celebrate the glory of Rome with us!-->
<!-- </p>-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!--</section>-->
<!-- Meetings -->
<section class="d-flex justify-content-center py-4" style="background-color: #990000">
<div class="d-flex justify-content-start align-items-center">
<div class="d-block text-center">
<h1 class="d-block m-0 mb-md-3 transparent-link">
<a href="https://discord.gg/wW458KYR79" target="_blank">Join us on Discord</a>
<a routerLink="/gallery" style="font-weight: bold; text-decoration: none;">⚔️ Witness the Glory: Explore our Gallery</a>
</h1>
<div class="d-none d-md-inline">
<p>
Ask us questions, get involved
Experience the history and spirit of the legion through our photos.
<br><br>
and celebrate the glory of Rome with us!
See our gear, events, and traditions in action!
</p>
</div>
</div>
</div>
</section>
<!-- Links -->
<section id="resources" class="d-flex flex-column flex-md-row align-items-center justify-content-center invert" style="min-height: 50vh; max-height: 100vh; background: url('/assets/img/texture.png') repeat;">
<div class="py-5 container">
@@ -59,7 +80,7 @@
<div>
<h3 class="mb-1">Legio XXX</h3>
<p class="m-0">Learn about Legio XXX, who we are & what we do</p>
<a routerLink="/about">About Us</a> / <a routerLink="/gallery">Gallery</a> / <a routerLink="/" fragment="contact">Contact</a>
<a routerLink="/about">About Us</a> / <a routerLink="/" fragment="contact">Contact</a> / <a routerLink="/gallery">Gallery</a>
</div>
</div>
<div class="d-flex p-3 pe-4 rounded-1 bg-light m-3 align-items-center border" style="min-width: 45%; flex: 1 0 0;">
@@ -89,19 +110,20 @@
<div>
<h3 class="mb-1">Equipment</h3>
<p class="m-0">Learn how to assemble & maintain your kit</p>
<a routerLink="/buy">Buy Equipment</a> / <a routerLink="/diy">Build Equipment</a> / <a routerLink="/maintenance">Maintenance</a>
<a routerLink="/equipment">Acquire Equipment</a> / <a routerLink="/maintenance">Maintenance</a>
</div>
</div>
</div>
</div>
</section>
<!-- Recruitment -->
<section id="recruitment" class="d-flex flex-column flex-md-row align-items-center justify-content-center invert" style="padding: 10rem 0">
<div class="flex-grow-1 mt-5 mt-md-0 p-5 pb-0 text-center text-md-end" style="flex-basis: 0">
<img src="/assets/img/recruitment.png" alt="Recruitment poster" style="max-width: min(90%, 400px)">
</div>
<div class="d-flex flex-grow-1 justify-content-start align-items-center" style="flex-basis: 0">
<div class="d-block text-center">
<div class="d-block text-center d-flex flex-column align-items-center">
<h2 class="my-3">Enlist Today!</h2>
<p>
Interested in any <a routerLink="/events/calendar">events</a>?
@@ -112,13 +134,18 @@
<br><br>
Family participation is welcome!
</p>
<div class="d-flex align-items-center">
<a [routerLink]="['/register']" class="btn btn-light btn-lg" style="background: var(--theme-primary); color: var(--theme-primary-contrast)">JOIN NOW</a>
</div>
</div>
</div>
</section>
<!-- Page Break-->
<section style="background: #fff">
<div style="height: 300px; background: 0 url('/assets/img/formation.png') repeat-x; background-size: auto 300px;"></div>
</section>
<!-- Contact -->
<section id="contact" class="d-flex flex-column align-items-center justify-content-center" style="padding: 10rem 0">
<div class="container">

View File

@@ -1,18 +0,0 @@
<div class="invert">
<div class="cap-width py-5 px-3">
<div class="mb-3">
<div class="d-flex align-items-end justify-content-between mb-2">
<h1 class="mb-0">Buy Equipment</h1>
</div>
<mat-divider></mat-divider>
</div>
<div>
<ul>
<li><a href="https://armae.com/en" target="_blank">Armæ</a></li>
<li><a href="https://www.by-the-sword.com/">By The Sword</a></li>
<li><a href="https://www.facebook.com/FabricaCacti" target="_blank">Fabrica Cacti</a></li>
<li><a href="http://www.lawrensnest.com">La Wren's Nests</a></li>
</ul>
</div>
</div>
</div>

View File

@@ -1,7 +0,0 @@
import {Component} from '@angular/core';
@Component({
selector: 'xxx-buy',
templateUrl: './buy.component.html'
})
export class BuyComponent { }

View File

@@ -1,21 +0,0 @@
<div class="invert">
<div class="cap-width py-5 px-3">
<div class="mb-3">
<div class="d-flex align-items-end justify-content-between mb-2">
<h1 class="mb-0">Build Equipment</h1>
</div>
<mat-divider></mat-divider>
</div>
<div>
<ul>
<li><em>Balteus</em> (Belt): <a href="/assets/manuals/Beltus%20Part%201%20-%20Robert%20Norton.pdf" target="_blank"> Part 1</a>, <a href="/assets/manuals/Beltus%20Part%202%20-%20Robert%20Norton.pdf" target="_blank">Part 2</a></li>
<li><a href="/assets/manuals/Calcei%20-%20Lee%20Holeva.pdf" target="_blank"><em>Calcei</em> (Boots)</a></li>
<li><a href="/assets/manuals/Caligae%20-%20Robert%20Norton.pdf" target="_blank"><em>Caligae</em> (Sandals)</a></li>
<li><a href="/assets/manuals/Helmet%20Liner%20-%20Robert%20Norton.pdf" target="_blank">Helmet Liner</a></li>
<li><a href="/assets/manuals/Loculus%20-%20Robert%20Norton.pdf" target="_blank"><em>Loculus</em> (Bag)</a></li>
<li><em>Lorica Hamata</em> (Chain-mail): <a href="/assets/manuals/Lorica%20Hamata%20Part%201.pdf" target="_blank"> Part 1</a>, <a href="/assets/manuals/Lorica%20Hamata%20Part%202.pdf" target="_blank">Part 2</a></li>
<li><a href="/assets/manuals/Scutum.pdf" target="_blank"><em>Scutum</em> (Shield)</a></li>
</ul>
</div>
</div>
</div>

View File

@@ -105,7 +105,7 @@
<td>Turn 180° to the right, pivoting on the right heal & left toes.</td>
</tr>
<tr>
<td><em>Ad scuta transformate</em></td>
<td><em>Ad scutum transformate</em></td>
<td>About Face to the Left</td>
<td>Turn 180° to the left, pivoting on the left heal & right toes.</td>
</tr>
@@ -153,12 +153,12 @@
<tr>
<td><em>Aciem formate</em></td>
<td>Battle line</td>
<td>Soldiers form a line with their <em>pilum</em> at the ready. This is not a shield wall, 6' spacing should be maintained between each column.</td>
<td>Soldiers form a line with their <em>pilum</em> at the ready. Maintain 6' spacing between each column.</td>
</tr>
<tr>
<td><em>Cuneum formate</em></td>
<td>Wedge</td>
<td>Centerio is placed at the tip of the wedge with soldiers trailing off his side.</td>
<td>The centurio is placed at the tip of the wedge with soldiers trailing off his side.</td>
</tr>
<tr>
<td><em>Orbem formate</em></td>
@@ -172,7 +172,7 @@
</tr>
<tr>
<td><em>Testudinem formate</em></td>
<td><em>Testudo</em> (Tortis)</td>
<td><em>Testudo</em> (Tortoise)</td>
<td>First rank raises shields to eye height, the following ranks lift their shields onto their heads overlapping the rank in-front. Formation advances together using a chant to stay in step ("Ro-ma, Ro-ma" or "Dex-Sin, Dex-Sin").</td>
</tr>
<tr>
@@ -242,7 +242,7 @@
<tr>
<td><em>Ballistam</em></td>
<td>Bolt thrower</td>
<td>Following command is for <em>scropios</em> / <em>balistas</em>.</td>
<td>Following command is for <em>scropio</em> / <em>balista</em>.</td>
</tr>
<tr>
<td><em>Catapultam</em></td>
@@ -304,13 +304,13 @@
</tr>
<tr>
<td><em>Sin[ister]/Gladios</em></td>
<td>Right/Sword [hand]</td>
<td>Legio XXX prefers to use <em>gladios</em> instead of <em>sin</em>.</td>
<td>Left/Shield [hand]</td>
<td>Legio XXX prefers to use <em>scuta</em> instead of <em>dex</em>.</td>
</tr>
<tr>
<td><em>Dex[ter]/Scuta</em></td>
<td>Left/Shield [hand]</td>
<td>Legio XXX prefers to use <em>scuta</em> instead of <em>dex</em>.</td>
<td>Right/Sword [hand]</td>
<td>Legio XXX prefers to use <em>gladios</em> instead of <em>sin</em>.</td>
</tr>
<tr>
<td><em>Clamate</em></td>

View File

@@ -0,0 +1,40 @@
<div class="invert">
<div class="cap-width py-5 px-3">
<div class="row">
<!-- Buy Equipment Card -->
<div class="col-12 col-md-6 mb-4">
<div class="p-4 border rounded shadow-sm">
<div class="mb-3">
<h1 class="mb-0">Buy Equipment</h1>
<mat-divider class="mt-2"></mat-divider>
</div>
<ul class="mt-3">
<li><a href="https://armae.com/en" target="_blank">Arma</a></li>
<li><a href="https://www.by-the-sword.com/">By The Sword</a></li>
<li><a href="https://www.facebook.com/FabricaCacti" target="_blank">Fabrica Cacti</a></li>
<li><a href="http://www.lawrensnest.com">La Wren's Nests</a></li>
</ul>
</div>
</div>
<!-- Build Equipment Card -->
<div class="col-12 col-md-6">
<div class="p-4 border rounded shadow-sm">
<div class="mb-3">
<h1 class="mb-0">Build Equipment</h1>
<mat-divider class="mt-2"></mat-divider>
</div>
<ul class="mt-3">
<li><em>Balteus</em> (Belt): <a href="/assets/manuals/Beltus%20Part%201%20-%20Robert%20Norton.pdf" target="_blank"> Part 1</a>, <a href="/assets/manuals/Beltus%20Part%202%20-%20Robert%20Norton.pdf" target="_blank">Part 2</a></li>
<li><a href="/assets/manuals/Calcei%20-%20Lee%20Holeva.pdf" target="_blank"><em>Calcei</em> (Boots)</a></li>
<li><a href="/assets/manuals/Caligae%20-%20Robert%20Norton.pdf" target="_blank"><em>Caligae</em> (Sandals)</a></li>
<li><a href="/assets/manuals/Helmet%20Liner%20-%20Robert%20Norton.pdf" target="_blank">Helmet Liner</a></li>
<li><a href="/assets/manuals/Loculus%20-%20Robert%20Norton.pdf" target="_blank"><em>Loculus</em> (Bag)</a></li>
<li><em>Lorica Hamata</em> (Chain-mail): <a href="/assets/manuals/Lorica%20Hamata%20Part%201.pdf" target="_blank"> Part 1</a>, <a href="/assets/manuals/Lorica%20Hamata%20Part%202.pdf" target="_blank">Part 2</a></li>
<li><a href="/assets/manuals/Scutum.pdf" target="_blank"><em>Scutum</em> (Shield)</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>

View File

@@ -2,6 +2,6 @@ import {Component} from '@angular/core';
@Component({
selector: 'xxx-diy',
templateUrl: './diy.component.html'
templateUrl: './equipment.component.html'
})
export class DiyComponent { }
export class EquipmentComponent { }

View File

@@ -24,7 +24,7 @@
<h2 class="mb-2">Enlisting</h2>
<p>
If reenacting is something you would seriously like to do, great! Interest in the history of Rome is the primary requirement and you are in the right place. This guide will walk you through everything
you need to know to get started. Acquiring a full kit may take some time & cost a couple thousand dollars (USD/CAD) however we provide <a routerLink="/diy">manuals</a> to build equipment yourself
you need to know to get started. Acquiring a full kit may take some time & cost a couple thousand dollars (USD/CAD) however we provide <a routerLink="/equipment">manuals</a> to build equipment yourself
to save on costs. With enough notice we may be able to loan any equipment you are missing for an event, please let us know.
</p>
<p>
@@ -35,6 +35,11 @@
<div class="d-flex flex-column flex-md-row justify-content-md-between">
<div class="me-md-3">
<ul class="mt-0">
<li>Enlist with your local reenactment group:
<ul>
<li><strong>Southern-Ontario</strong> - <a routerLink="/register">Enlist with Legio XXX</a></li>
<li><em>Elsewhere</em> - <a href="https://www.romanempire.net/roman_reenactment_groups.html" target="_blank">Find your local recruiter</a></li>
</ul>
<li>Familiarize yourself with our <a routerLink="/rules">Rules & Regulations</a></li>
<li>Pick out a Roman name:</li>
<ul>
@@ -42,7 +47,7 @@
<li>Can be fictional or non-fictional</li>
<li>Avoid famous people/names</li>
</ul>
<li><a routerLink="/vendors">Purchase</a>, <a routerLink="/diy">build</a> or arrange for loaner equipment by reaching out. Bold items are considered the minimum camp attire:</li>
<li><a routerLink="/equipment">Purchase, build</a> or arrange for equipment by reaching out. Bold items are minimum camp attire:</li>
<ul>
<li><span class="fw-bold"><em>Tunica</em> (Tunic)</span> - A large shirt/dress held at the waist by a cord or cloth belt</li>
<li><span class="fw-bold"><em>Caligae</em> (Sandals)</span> - Modern leather sandals will do in a pinch</li>
@@ -56,7 +61,7 @@
<li><em>Subarmalis</em> (Padded Vest) - Worn under your armour for comfort and protection</li>
<li><em>Lorica</em> (Armour) - Segmata, hamata or plumata/squamta</li>
</ul>
<li>Submit an <a href="/assets/docs/Application.pdf" target="_blank">application</a> to Robert Sacco: <a href="mailto:primuspiluslxxx@gmail.com" target="_blank">primuspiluslxxx@gmail.com</a></li>
<li><a routerLink="/events/calendar">Go to your first event!</a></li>
</ul>
</div>
<div class="d-flex d-print-none align-items-center justify-content-center pt-4">

View File

@@ -0,0 +1,129 @@
<div class="invert">
<div class="banner d-print-none"></div>
<div class="cap-width py-5 px-3">
<div>
<img src="/assets/img/header.png" alt="Legio XXX" class="mx-auto d-block" style="width: 100%; height: auto;">
<hr>
</div>
<div *ngIf="success" class="alert alert-success">
✅ Your registration has been submitted! We will be in touch soon.
</div>
<div *ngIf="error" class="alert alert-danger">
⚠️ Something went wrong, please try again or contact us directly.
</div>
<form #form="ngForm" (ngSubmit)="submit(form.value)" [class.locked]="success">
<h3 class="fw-bold mb-2">Member Information</h3>
<h4 class="bg-black p-2 text-center text-white fw-bold">Personal Information</h4>
<div class="row g-3 mb-3">
<div class="field col-4">
<label>Last Name <span class="required">*</span></label>
<input ngModel name="lastName" required [disabled]="success">
</div>
<div class="field col-4">
<label>First Name <span class="required">*</span></label>
<input ngModel name="firstName" required [disabled]="success">
</div>
<div class="field col-4">
<label>Date of Birth <span class="required">*</span></label>
<input ngModel name="dateOfBirth" type="date" required [disabled]="success">
</div>
</div>
<br><br><br>
<div class="row g-3 mb-3">
<div class="field col-8">
<label>Street Address</label>
<input ngModel name="streetAddress" [disabled]="success">
</div>
<div class="field col-4">
<label>Apartment/Unit #</label>
<input ngModel name="unit" [disabled]="success">
</div>
</div>
<div class="row g-3 mb-3">
<div class="field col-4">
<label>City</label>
<input ngModel name="city" [disabled]="success">
</div>
<div class="field col-4">
<label>Province/State</label>
<input ngModel name="province" [disabled]="success">
</div>
<div class="field col-4">
<label>Postal Code</label>
<input ngModel name="postalCode" [disabled]="success">
</div>
</div>
<br><br><br>
<div class="row g-3 mb-3">
<div class="field col-6">
<label>Home Phone <span class="required">*</span></label>
<input ngModel name="homePhone" type="tel" [disabled]="success" required>
</div>
<div class="field col-6">
<label>Alternate Phone</label>
<input ngModel name="alternatePhone" type="tel" [disabled]="success">
</div>
</div>
<div class="field mb-3">
<label>Email <span class="required">*</span></label>
<input ngModel name="email" type="email" required [disabled]="success">
</div>
<h4 class="bg-black p-2 text-center text-white fw-bold">Reenacting</h4>
<div class="field mb-3">
<label>Chosen Roman Name</label>
<input ngModel name="romanName" [disabled]="success">
</div>
<div class="row g-3 mb-3">
<div class="field col-6">
<label>First Choice of Position</label>
<input ngModel name="positionFirst" [disabled]="success">
</div>
<div class="field col-6">
<label>Second Choice of Position</label>
<input ngModel name="positionSecond" [disabled]="success">
</div>
</div>
<div class="field mb-3">
<label>Have you ever re-enacted?</label>
<input ngModel name="previousReenacting" [disabled]="success">
</div>
<br><br><br>
<div class="field mb-3">
<label>Special Skills</label>
<textarea ngModel name="specialSkills" rows="3" [disabled]="success"></textarea>
</div>
<div class="field mb-3">
<label>Physical Limitations</label>
<textarea ngModel name="physicalLimitations" rows="3" [disabled]="success"></textarea>
</div>
<div class="field mb-3">
<label>Comments</label>
<textarea ngModel name="comments" rows="5" [disabled]="success"></textarea>
</div>
<div class="d-flex justify-content-end">
<button type="submit" [disabled]="form.invalid || loading || success">
{{ loading ? 'Submitting...' : 'Submit Registration' }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,100 @@
import {Component} from '@angular/core';
@Component({
selector: 'xxx-register',
templateUrl: './register.component.html',
styles: [`
.extra-spacing li { margin-bottom: 1em }
form { color: #000; }
hr { border-color: rgba(0,0,0,0.2); }
h3, h4 { color: #000; }
.alert {
padding: 12px 16px;
margin-bottom: 1em;
font-weight: 600;
border-radius: 4px;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-danger {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.required { color: red; }
.field {
display: flex;
flex-direction: column;
}
.field label {
font-size: 0.75em;
font-weight: 600;
color: #333;
margin-bottom: 2px;
}
.field input,
.field textarea {
border: none;
border-bottom: 2px solid #333;
background: transparent;
color: #000;
font-size: 1em;
padding: 4px 2px;
outline: none;
width: 100%;
font-family: inherit;
}
.field input:focus,
.field textarea:focus {
border-bottom-color: #000;
}
.field input:disabled,
.field textarea:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.field textarea {
resize: vertical;
}
button[type=submit] {
background: #000;
color: #fff;
border: none;
padding: 10px 28px;
font-size: 1em;
cursor: pointer;
}
button[type=submit]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`]
})
export class RegisterComponent {
loading = false;
success = false;
error = false;
async submit(value: any) {
this.loading = true;
this.error = false;
try {
await window.momentum.data.create('Applications', value);
this.success = true;
} catch {
this.error = true;
} finally {
this.loading = false;
}
}
}

View File

@@ -15,13 +15,16 @@
<div>
<h2 class="mb-2">Websites</h2>
<ul class="mt-0">
<li><a href="https://www.erminestreetguard.co.uk" target="_blank">The Ermine Street Guard (https://erminestreetguard.co.uk)</a></li>
<li><a href="https://www.jstor.org/journal/jromanstudies">The Journal of Roman Studies (https://jstor.org)</a></li>
<li><a href="https://www.larp.com/legioxx/hndbk.html" target="_blank">Legio XX Handbook (https://larp.com/legioxx)</a></li>
<li><a href="https://principialegionis.org" target="_blank">Principia Legionis (https://principialegionis.org)</a></li>
<li><a href="https://www.erminestreetguard.co.uk" target="_blank">The Ermine Street Guard (https://www.erminestreetguard.co.uk)</a></li>
<li><a href="https://www.romanempire.net">Roman Empire Groups (https://romanempire.net)</a></li>
<li><a href="https://www.romansociety.org">The Roman Society (https://romansociety.org)</a></li>
</ul>
</div>
<div>
<h2 class="mb-2">Modern</h2>
<h2 class="mb-2">Modern Literature</h2>
<h3 class="mb-2">Life</h3>
<ul>
<li>Adkins, Lesley and Adkins, Roy A. <strong>Handbook to Life in Ancient Rome</strong>: New York: Oxford University Press, 1994</li>
@@ -58,7 +61,7 @@
</ul>
</div>
<div>
<h2 class="mb-2">Historical</h2>
<h2 class="mb-2">Historical Literature</h2>
<h3 class="mb-2">Accounts</h3>
<ul class="mt-0 extra-spacing">
<li>

View File

@@ -3,27 +3,19 @@
<html lang="en">
<head>
<base href="/">
<title>LEGIO · XXX</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="Zak Timson">
<meta property="og:title" content="LEGIO · XXX">
<meta property="og:site_name" content="LEGIO · XXX">
<meta property="og:type" content="article"/>
<meta property="og:url" content="https://legioxxx.zakscode.com">
<meta property="og:image" content="https://legioxxx.zakscode.com/assets/img/standard.png">
<meta name="twitter:card" content="https://legioxxx.zakscode.com/assets/img/standard.png">
<meta name="twitter:image:alt" content="Alt text for image">
<meta name="description" content="Legio XXX is a North American Roman reenactment group established in 2004. It's members represent a cross between living history enthusiasts and 'edutainers' that recreate the lives of soldiers found in Trajan's leagions during the 1st - 2nd Century AD">
<meta property="og:description" content="Legio XXX is a North American Roman reenactment group established in 2004. It's members represent a cross between living history enthusiasts and 'edutainers' that recreate the lives of soldiers found in Trajan's leagions during the 1st - 2nd Century AD">
<title>LEGIO · XXX</title>
<!-- Momentum:meta -->
<link href="assets/img/favicon.svg" rel="icon" type="image/svg">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://use.fontawesome.com/releases/v6.1.1/css/all.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Momentum:theme -->
</head>
<body class="mat-typography" style="background: #000">

View File

@@ -111,3 +111,10 @@ ol {
a, a:visited { color: #fff; }
a:hover, a:visited:hover { color: rgba(255, 255, 255, 0.8); }
}
@media print {
.event-card {
border: 1px solid #ccc !important;
border-radius: 4px;
}
}

View File

@@ -4,6 +4,8 @@
"compilerOptions": {
"baseUrl": "./",
"outDir": "./out-tsc/app",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
@@ -13,6 +15,7 @@
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,