Compare commits

...

2 Commits

Author SHA1 Message Date
2f08f1ed18
feat: we have a functional prototype 2025-02-17 02:43:15 -08:00
107f54d269
feat: load in configs 2025-02-16 15:54:42 -08:00
141 changed files with 15990 additions and 0 deletions
.gitea/workflows
.gitignore
.vscode
client
.editorconfig.gitignoreREADME.mdangular.jsoneslint.config.jspackage.json
src
app
api.service.spec.tsapi.service.tsapp.component.cssapp.component.htmlapp.component.spec.tsapp.component.tsapp.config.tsapp.routes.ts
consent
error
forms
home
inputs
review
success
userinfo
index.htmlmain.tsstyles.css
tsconfig.app.jsontsconfig.jsontsconfig.spec.json
package.json
packages/types

38
.gitea/workflows/ci.yml Normal file

@ -0,0 +1,38 @@
name: Node.js CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
name: Lint and Test
steps:
- name: Checkout Source Files
uses: actions/checkout@v4
- name: Use Node.js v22
uses: actions/setup-node@v4
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Install Dependencies
run: pnpm install
- name: Lint Source Files
run: pnpm run lint
- name: Verify Build
run: pnpm run build
- name: Run Tests
run: pnpm run test:ci

3
.gitignore vendored Normal file

@ -0,0 +1,3 @@
node_modules
prod
.turbo

10
.vscode/settings.json vendored Normal file

@ -0,0 +1,10 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": ["typescript"],
"sonarlint.connectedMode.project": {
"connectionId": "nhcarrigan",
"projectKey": "nhcarrigan_forms-api"
}
}

17
client/.editorconfig Normal file

@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
client/.gitignore vendored Normal file

@ -0,0 +1,42 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

59
client/README.md Normal file

@ -0,0 +1,59 @@
# Client
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.1.7.
## Development server
To start a local development server, run:
```bash
ng serve
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the project run:
```bash
ng build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

96
client/angular.json Normal file

@ -0,0 +1,96 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"client": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/client",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "client:build:production"
},
"development": {
"buildTarget": "client:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
],
"scripts": []
}
}
}
}
}
}

5
client/eslint.config.js Normal file

@ -0,0 +1,5 @@
import NaomisConfig from "@nhcarrigan/eslint-config";
export default [
...NaomisConfig,
]

40
client/package.json Normal file

@ -0,0 +1,40 @@
{
"name": "client",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "ng dev",
"lint": "eslint src --max-warnings 0",
"build": "ng build",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^19.1.0",
"@angular/common": "^19.1.0",
"@angular/compiler": "^19.1.0",
"@angular/core": "^19.1.0",
"@angular/forms": "^19.1.0",
"@angular/platform-browser": "^19.1.0",
"@angular/platform-browser-dynamic": "^19.1.0",
"@angular/router": "^19.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.1.7",
"@angular/cli": "^19.1.7",
"@angular/compiler-cli": "^19.1.0",
"@nhcarrigan/eslint-config": "5.2.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.7.2",
"@repo/types": "../packages/types"
}
}

@ -0,0 +1,15 @@
import { TestBed } from "@angular/core/testing";
import { ApiService } from "./api.service";
describe("ApiService", () => {
let service: ApiService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ApiService);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
});

@ -0,0 +1,177 @@
import { Injectable } from "@angular/core";
import type { Appeal, Commission, Contact, ErrorResponse, Event, Meeting, Mentorship, SuccessResponse, Staff, DataResponse } from "@repo/types";
/**
*
*/
@Injectable({
providedIn: "root",
})
export class ApiService {
public url = "http://localhost:1234";
/**
*
*/
constructor() { }
/**
* @param token
*/
public async validateToken(token: string | null): Promise<boolean> {
if (!token) {
return false;
}
const ipRequest = await fetch("https://api.ipify.org?format=json");
const ipRes = await ipRequest.json();
const { ip } = ipRes;
const request = await fetch(`${this.url}/validate-token`, {
body: JSON.stringify({ ip, token }),
headers: {
"Content-type": "application/json",
},
method: "POST",
});
const response = await request.json();
return response.valid;
}
/**
* @param appeal
*/
public async submitAppeal(appeal: Partial<Appeal>): Promise<SuccessResponse | ErrorResponse> {
const request = await fetch(`${this.url}/submit/appeals`, {
body: JSON.stringify(appeal),
headers: {
"Content-type": "application/json",
},
method: "POST",
});
const response = await request.json();
return response;
}
/**
* @param commission
*/
public async submitCommission(commission: Partial<Commission>): Promise<SuccessResponse | ErrorResponse> {
const request = await fetch(`${this.url}/submit/commissions`, {
body: JSON.stringify(commission),
headers: {
"Content-type": "application/json",
},
method: "POST",
});
const response = await request.json();
return response;
}
/**
* @param contact
*/
public async submitContact(contact: Partial<Contact>): Promise<SuccessResponse | ErrorResponse> {
const request = await fetch(`${this.url}/submit/contacts`, {
body: JSON.stringify(contact),
headers: {
"Content-type": "application/json",
},
method: "POST",
});
const response = await request.json();
return response;
}
/**
* @param event
*/
public async submitEvent(event: Partial<Event>): Promise<SuccessResponse | ErrorResponse> {
const request = await fetch(`${this.url}/submit/events`, {
body: JSON.stringify(event),
headers: {
"Content-type": "application/json",
},
method: "POST",
});
const response = await request.json();
return response;
}
/**
* @param meeting
*/
public async submitMeeting(meeting: Partial<Meeting>): Promise<SuccessResponse | ErrorResponse> {
const request = await fetch(`${this.url}/submit/meetings`, {
body: JSON.stringify(meeting),
headers: {
"Content-type": "application/json",
},
method: "POST",
});
const response = await request.json();
return response;
}
/**
* @param mentorship
*/
public async submitMentorship(mentorship: Partial<Mentorship>): Promise<SuccessResponse | ErrorResponse> {
const request = await fetch(`${this.url}/submit/mentorships`, {
body: JSON.stringify(mentorship),
headers: {
"Content-type": "application/json",
},
method: "POST",
});
const response = await request.json();
return response;
}
/**
* @param staff
*/
public async submitStaff(staff: Partial<Staff>): Promise<SuccessResponse | ErrorResponse> {
const request = await fetch(`${this.url}/submit/staff`, {
body: JSON.stringify(staff),
headers: {
"Content-type": "application/json",
},
method: "POST",
});
const response = await request.json();
return response;
}
/**
* @param type
* @param token
*/
public async getData(type: "appeals" | "commissions" | "contacts" | "events" | "meetings" | "mentorships" | "staff", token: string): Promise<DataResponse | ErrorResponse> {
const request = await fetch(`${this.url}/list/${type}`, {
headers: {
"Authorization": token,
"Content-type": "application/json",
},
method: "GET",
});
const response = await request.json();
return response;
}
/**
* @param type
* @param id
* @param token
*/
public async markReviewed(type: "appeals" | "commissions" | "contacts" | "events" | "meetings" | "mentorships" | "staff", id: string, token: string): Promise<SuccessResponse | ErrorResponse> {
const request = await fetch(`${this.url}/review/${type}`, {
body: JSON.stringify({ submissionId: id }),
headers: {
"Authorization": token,
"Content-type": "application/json",
},
method: "PUT",
});
const response = await request.json();
return response;
}
}

@ -0,0 +1,3 @@
<main>
<router-outlet></router-outlet>
</main>

@ -0,0 +1,29 @@
import { TestBed } from "@angular/core/testing";
import { AppComponent } from "./app.component";
describe("AppComponent", () => {
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ AppComponent ],
}).compileComponents();
});
it("should create the app", () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'client' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual("client");
});
it("should render title", () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector("h1")?.textContent).toContain("Hello, client");
});
});

@ -0,0 +1,15 @@
import { Component } from "@angular/core";
import { RouterOutlet } from "@angular/router";
/**
*
*/
@Component({
imports: [ RouterOutlet ],
selector: 'app-root',
styleUrl: './app.component.css',
templateUrl: "./app.component.html",
})
export class AppComponent {
title = "NHCarrigan Forms";
}

@ -0,0 +1,7 @@
import { type ApplicationConfig, provideZoneChangeDetection } from "@angular/core";
import { provideRouter } from "@angular/router";
import { routes } from "./app.routes";
export const appConfig: ApplicationConfig = {
providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes) ],
};

@ -0,0 +1,22 @@
import { AppealComponent } from "./forms/appeal/appeal.component.js";
import { CommissionComponent } from "./forms/commission/commission.component.js";
import { ContactComponent } from "./forms/contact/contact.component.js";
import { EventComponent } from "./forms/event/event.component.js";
import { MeetingComponent } from "./forms/meeting/meeting.component.js";
import { MentorshipComponent } from "./forms/mentorship/mentorship.component.js";
import { StaffComponent } from "./forms/staff/staff.component.js";
import { HomeComponent } from "./home/home.component.js";
import { ReviewComponent } from "./review/review.component.js";
import type { Routes } from "@angular/router";
export const routes: Routes = [
{ component: HomeComponent, path: "", pathMatch: "full" },
{ component: AppealComponent, path: "appeal" },
{ component: CommissionComponent, path: "commission" },
{ component: ContactComponent, path: "contact" },
{ component: EventComponent, path: "event" },
{ component: MeetingComponent, path: "meeting" },
{ component: MentorshipComponent, path: "mentorship" },
{ component: StaffComponent, path: "staff" },
{ component: ReviewComponent, path: "review" },
];

@ -0,0 +1,7 @@
p, label {
font-size: 0.7rem;
}
strong {
color: red;
}

@ -0,0 +1,16 @@
<p>
nhcarrigan is committed to protecting and respecting your privacy, and we will
only use your personal information to administer your account and to provide
the products and services you requested from us.
</p>
<p>
We are required to collect your consent to email you. If we don't, we could
not send you a response to your form submission. We will ONLY use your email
address to respond to the form submission, and your data will be automatically
deleted once we have responded to your submission.
</p>
<input id="consent" name="consent" type="checkbox" required [formControl]="control()" />
<label for="consent">
I agree to receive email communication from NHCarrigan
<strong>for the sole purpose</strong> of responding to this form submission.
</label>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { ConsentComponent } from "./consent.component";
describe("ConsentComponent", () => {
let component: ConsentComponent;
let fixture: ComponentFixture<ConsentComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ ConsentComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(ConsentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,15 @@
import { Component, input } from "@angular/core";
import { type FormControl, ReactiveFormsModule } from "@angular/forms";
/**
*
*/
@Component({
imports: [ ReactiveFormsModule ],
selector: 'app-consent',
styleUrl: './consent.component.css',
templateUrl: "./consent.component.html",
})
export class ConsentComponent {
public control = input.required<FormControl>();
}

@ -0,0 +1,8 @@
div {
width: 100%;
padding: 0.5rem;
font-size: 1.3rem;
background: rgba(255, 100, 100, 0.5);
color: rgb(100, 0, 0);
border: 1px solid rgb(100, 0, 0);
}

@ -0,0 +1,3 @@
<div>
{{ error() }}
</div>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { ErrorComponent } from "./error.component";
describe("ErrorComponent", () => {
let component: ErrorComponent;
let fixture: ComponentFixture<ErrorComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ ErrorComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(ErrorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,14 @@
import { Component, input } from "@angular/core";
/**
*
*/
@Component({
imports: [],
selector: 'app-error',
styleUrl: './error.component.css',
templateUrl: "./error.component.html",
})
export class ErrorComponent {
public error = input.required<string>();
}

@ -0,0 +1,25 @@
<h1>Appeal a Sanction</h1>
<p>This form allows you to appeal a sanction levied against you on one of our platforms.</p>
<p>Need to find your sanction info? <a href="https://moderation.nhcarrigan.com/" target="_blank" rel="noreferrer">Check our logs</a>.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-checkbox [control]="understandBinding" label="I understand that the decision made by the appeals team is final and binding. If my appeal is denied, I agree that I cannot appeal again."></app-checkbox>
<div class="two-col">
<app-select-menu label="What sanction was levied against you?" options="reminder,warning,temporary removal,permanent removal" [control]="sanctionType"></app-select-menu>
<app-single-line label="What is your moderation case number?" type="number" [control]="caseNumber"></app-single-line>
</div>
<div class="two-col">
<app-select-menu label="What platform were you sanctioned on?" options="Forum,IRC,Matrix,Gitea,Fediverse" [control]="sanctionPlatform"></app-select-menu>
<app-single-line label="What is your username on that platform?" type="text" [control]="platformUsername"></app-single-line>
</div>
<app-multi-line label="Why were you sanctioned? Use your own words. Do NOT copy the sanction reason you were provided by our team." [control]="sanctionReason"></app-multi-line>
<app-multi-line label="Do you feel the sanction was fair? Why or why not?" [control]="sanctionFair"></app-multi-line>
<app-multi-line label="How did your behaviour violate our Code of Conduct, Community Guidelines, or other policies?" [control]="behaviourViolation"></app-multi-line>
<app-multi-line label="Why do you feel this sanction should be revoked?" [control]="appealReason"></app-multi-line>
<app-multi-line label="How will you improve your conduct to prevent future sanctions?" [control]="behaviourImprove"></app-multi-line>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { AppealComponent } from "./appeal.component";
describe("AppealComponent", () => {
let component: AppealComponent;
let fixture: ComponentFixture<AppealComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ AppealComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(AppealComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,111 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { ReactiveFormsModule, FormControl } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ConsentComponent } from "../../consent/consent.component.js";
import { ErrorComponent } from "../../error/error.component.js";
import { CheckboxComponent } from "../../inputs/checkbox/checkbox.component.js";
import { MultiLineComponent } from "../../inputs/multi-line/multi-line.component.js";
import { SelectMenuComponent } from "../../inputs/select-menu/select-menu.component.js";
import { SingleLineComponent } from "../../inputs/single-line/single-line.component.js";
import { SuccessComponent } from "../../success/success.component.js";
import { UserinfoComponent } from "../../userinfo/userinfo.component.js";
import type { ApiService } from "../../api.service.js";
import type { Platform } from "@repo/types/prod/unions/platform.js";
import type { Sanction } from "@repo/types/prod/unions/sanction.js";
/**
*
*/
@Component({
imports: [
RouterModule,
CommonModule,
ConsentComponent,
UserinfoComponent,
ErrorComponent,
ReactiveFormsModule,
CheckboxComponent,
SingleLineComponent,
SelectMenuComponent,
MultiLineComponent,
SuccessComponent,
],
selector: 'appeal-form',
styleUrl: './appeal.component.css',
templateUrl: "./appeal.component.html",
})
export class AppealComponent {
public loading = false;
public error = "";
public success = false;
public firstName = new FormControl("");
public lastName = new FormControl("");
public email = new FormControl("");
public company = new FormControl("");
public understandBinding = new FormControl(false);
public sanctionType = new FormControl("reminder");
public caseNumber = new FormControl("");
public sanctionPlatform = new FormControl<Platform>("Forum");
public platformUsername = new FormControl("");
public sanctionReason = new FormControl("");
public sanctionFair = new FormControl("");
public behaviourViolation = new FormControl("");
public appealReason = new FormControl("");
public behaviourImprove = new FormControl("");
public consent = new FormControl(false);
/**
* @param apiService
*/
constructor(private readonly apiService: ApiService) {}
/**
* @param e
*/
public submit(e: MouseEvent): void {
this.error = "";
const { form } = e.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (!valid) {
return;
}
this.loading = true;
this.apiService.
submitAppeal({
caseNumber: Number.parseInt(this.caseNumber.value ?? "0", 10),
email: this.email.value ?? undefined,
appealReason: this.appealReason.value ?? undefined,
firstName: this.firstName.value ?? undefined,
behaviourImprove: this.behaviourImprove.value ?? undefined,
lastName: this.lastName.value ?? undefined,
behaviourViolation: this.behaviourViolation.value ?? undefined,
platformUsername: this.platformUsername.value ?? undefined,
consent: this.consent.value ?? false,
sanctionFair: this.sanctionFair.value ?? undefined,
sanctionPlatform: this.sanctionPlatform.value ?? undefined,
sanctionType:
(this.sanctionType.value?.split(/\s+/)[0] as Sanction | undefined)
?? undefined,
sanctionReason: this.sanctionReason.value ?? undefined,
understandBinding: this.understandBinding.value ?? undefined,
}).
then((res) => {
if ("error" in res) {
this.error = res.error;
this.loading = false;
} else {
this.error = "";
this.success = true;
}
}).
catch(() => {
this.error = "An error occurred while submitting the form.";
this.loading = false;
});
}
}

@ -0,0 +1,11 @@
<h1>Commission Us!</h1>
<p>Want us to do some work for you? Fill out this form!</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-multi-line label="Explain your business needs and how we can meet those needs for you. Be as descriptive as possible." [control]="request"></app-multi-line>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { CommissionComponent } from "./commission.component";
describe("CommissionComponent", () => {
let component: CommissionComponent;
let fixture: ComponentFixture<CommissionComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ CommissionComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(CommissionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,84 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ConsentComponent } from "../../consent/consent.component.js";
import { ErrorComponent } from "../../error/error.component.js";
import { MultiLineComponent } from "../../inputs/multi-line/multi-line.component.js";
import { SuccessComponent } from "../../success/success.component.js";
import { UserinfoComponent } from "../../userinfo/userinfo.component.js";
import type { ApiService } from "../../api.service.js";
/**
*
*/
@Component({
imports: [
RouterModule,
CommonModule,
ConsentComponent,
UserinfoComponent,
ErrorComponent,
ReactiveFormsModule,
MultiLineComponent,
SuccessComponent,
],
selector: 'commission-form',
styleUrl: './commission.component.css',
templateUrl: "./commission.component.html",
})
export class CommissionComponent {
public loading = false;
public error = "";
public success = false;
public firstName = new FormControl("");
public lastName = new FormControl("");
public email = new FormControl("");
public company = new FormControl("");
public request = new FormControl("");
public consent = new FormControl(false);
/**
* @param apiService
*/
constructor(private readonly apiService: ApiService) {}
/**
* @param e
*/
public submit(e: MouseEvent): void {
this.error = "";
const { form } = e.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (!valid) {
return;
}
this.loading = true;
this.apiService.
submitCommission({
companyName: this.company.value ?? undefined,
consent: this.consent.value ?? false,
email: this.email.value ?? undefined,
firstName: this.firstName.value ?? undefined,
lastName: this.lastName.value ?? undefined,
request: this.request.value ?? undefined,
}).
then((res) => {
if ("error" in res) {
this.error = res.error;
this.loading = false;
} else {
this.error = "";
this.success = true;
}
}).
catch(() => {
this.error = "An error occurred while submitting the form.";
this.loading = false;
});
}
}

@ -0,0 +1,12 @@
<h1>Contact Us!</h1>
<p>This form is provided as a fallback contact method in the event our self-hosted platforms are unavailable.</p>
<p>Responses to this form are very low priority.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-multi-line label="What can we help you with today?" [control]="request"></app-multi-line>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { ContactComponent } from "./contact.component";
describe("ContactComponent", () => {
let component: ContactComponent;
let fixture: ComponentFixture<ContactComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ ContactComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(ContactComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,84 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ConsentComponent } from "../../consent/consent.component.js";
import { ErrorComponent } from "../../error/error.component.js";
import { MultiLineComponent } from "../../inputs/multi-line/multi-line.component.js";
import { SuccessComponent } from "../../success/success.component.js";
import { UserinfoComponent } from "../../userinfo/userinfo.component.js";
import type { ApiService } from "../../api.service.js";
/**
*
*/
@Component({
imports: [
RouterModule,
CommonModule,
ConsentComponent,
UserinfoComponent,
ErrorComponent,
ReactiveFormsModule,
MultiLineComponent,
SuccessComponent,
],
selector: 'contact-form',
styleUrl: './contact.component.css',
templateUrl: "./contact.component.html",
})
export class ContactComponent {
public loading = false;
public error = "";
public success = false;
public firstName = new FormControl("");
public lastName = new FormControl("");
public email = new FormControl("");
public company = new FormControl("");
public request = new FormControl("");
public consent = new FormControl(false);
/**
* @param apiService
*/
constructor(private readonly apiService: ApiService) {}
/**
* @param e
*/
public submit(e: MouseEvent): void {
this.error = "";
const { form } = e.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (!valid) {
return;
}
this.loading = true;
this.apiService.
submitContact({
companyName: this.company.value ?? undefined,
consent: this.consent.value ?? false,
email: this.email.value ?? undefined,
firstName: this.firstName.value ?? undefined,
lastName: this.lastName.value ?? undefined,
request: this.request.value ?? undefined,
}).
then((res) => {
if ("error" in res) {
this.error = res.error;
this.loading = false;
} else {
this.error = "";
this.success = true;
}
}).
catch(() => {
this.error = "An error occurred while submitting the form.";
this.loading = false;
});
}
}

@ -0,0 +1,18 @@
<h1>Event Requests</h1>
<p>This form allows you to submit a request for us to give a talk at your event or conference.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-multi-line label="What is your event about?" [control]="eventDescription"></app-multi-line>
<app-multi-line label="What would you like us to speak about at your event?" [control]="eventTopic"></app-multi-line>
<app-single-line label="Where is your event located? Provide an address if in-person, a URL if remote." type="text" [control]="eventLocation"></app-single-line>
<app-single-line label="When is your event?" type="text" [control]="eventDate"></app-single-line>
<app-single-line label="What is your budget to pay us for attending?" type="text" [control]="eventBudget"></app-single-line>
<app-checkbox [control]="travelCovered" label="We will cover travel expenses for NHCarrigan to attend."></app-checkbox>
<app-checkbox [control]="lodgingCovered" label="We will cover lodging expenses for NHCarrigan to attend."></app-checkbox>
<app-checkbox [control]="foodCovered" label="We will cover food expenses for NHCarrigan to attend."></app-checkbox>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { EventComponent } from "./event.component";
describe("EventComponent", () => {
let component: EventComponent;
let fixture: ComponentFixture<EventComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ EventComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(EventComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,102 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ConsentComponent } from "../../consent/consent.component.js";
import { ErrorComponent } from "../../error/error.component.js";
import { CheckboxComponent } from "../../inputs/checkbox/checkbox.component.js";
import { MultiLineComponent } from "../../inputs/multi-line/multi-line.component.js";
import { SingleLineComponent } from "../../inputs/single-line/single-line.component.js";
import { SuccessComponent } from "../../success/success.component.js";
import { UserinfoComponent } from "../../userinfo/userinfo.component.js";
import type { ApiService } from "../../api.service.js";
/**
*
*/
@Component({
imports: [
RouterModule,
CommonModule,
ConsentComponent,
UserinfoComponent,
ErrorComponent,
ReactiveFormsModule,
CheckboxComponent,
SingleLineComponent,
MultiLineComponent,
SuccessComponent,
],
selector: 'event-form',
styleUrl: './event.component.css',
templateUrl: "./event.component.html",
})
export class EventComponent {
public loading = false;
public error = "";
public success = false;
public firstName = new FormControl("");
public lastName = new FormControl("");
public email = new FormControl("");
public company = new FormControl("");
public eventDescription = new FormControl("");
public eventTopic = new FormControl("");
public eventLocation = new FormControl("");
public eventDate = new FormControl("");
public eventBudget = new FormControl("");
public travelCovered = new FormControl(false);
public lodgingCovered = new FormControl(false);
public foodCovered = new FormControl(false);
public consent = new FormControl(false);
/**
* @param apiService
*/
constructor(private readonly apiService: ApiService) {}
/**
* @param e
*/
public submit(e: MouseEvent): void {
this.error = "";
const { form } = e.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (!valid) {
return;
}
this.loading = true;
this.apiService.
submitEvent({
companyName: this.company.value ?? undefined,
email: this.email.value ?? undefined,
consent: this.consent.value ?? false,
eventBudget: this.eventBudget.value ?? undefined,
eventDate: this.eventDate.value ?? undefined,
eventDescription: this.eventDescription.value ?? undefined,
eventLocation: this.eventLocation.value ?? undefined,
eventTopic: this.eventTopic.value ?? undefined,
firstName: this.firstName.value ?? undefined,
foodCovered: this.foodCovered.value ?? false,
lastName: this.lastName.value ?? undefined,
lodgingCovered: this.lodgingCovered.value ?? false,
travelCovered: this.travelCovered.value ?? false,
}).
then((res) => {
if ("error" in res) {
this.error = res.error;
this.loading = false;
} else {
this.error = "";
this.success = true;
}
}).
catch(() => {
this.error = "An error occurred while submitting the form.";
this.loading = false;
});
}
}

@ -0,0 +1,14 @@
<h1>Request a Meeting</h1>
<p>This form allows you to request a 1:1 meeting.</p>
<p>Sessions are billed at a pro-rated amount equivalent to $200 / hour.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-select-menu [control]="sessionLength" label="How long of a session would you like (in minutes)?" options="15,30,60"></app-select-menu>
<app-multi-line label="What do you wish to achieve during this session?" [control]="sessionGoal"></app-multi-line>
<app-checkbox [control]="paymentUnderstanding" label="I understand that I will be invoiced for the session, and that payment must be submitted prior to the scheduled time or our session will be cancelled."></app-checkbox>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { MeetingComponent } from "./meeting.component";
describe("MeetingComponent", () => {
let component: MeetingComponent;
let fixture: ComponentFixture<MeetingComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ MeetingComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(MeetingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,93 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ConsentComponent } from "../../consent/consent.component.js";
import { ErrorComponent } from "../../error/error.component.js";
import { CheckboxComponent } from "../../inputs/checkbox/checkbox.component.js";
import { MultiLineComponent } from "../../inputs/multi-line/multi-line.component.js";
import { SelectMenuComponent } from "../../inputs/select-menu/select-menu.component.js";
import { SuccessComponent } from "../../success/success.component.js";
import { UserinfoComponent } from "../../userinfo/userinfo.component.js";
import type { ApiService } from "../../api.service.js";
import type { SessionLength } from "@repo/types/prod/unions/session.js";
/**
*
*/
@Component({
imports: [
RouterModule,
CommonModule,
ConsentComponent,
UserinfoComponent,
ErrorComponent,
ReactiveFormsModule,
CheckboxComponent,
SelectMenuComponent,
MultiLineComponent,
SuccessComponent,
],
selector: 'meeting-form',
styleUrl: './meeting.component.css',
templateUrl: "./meeting.component.html",
})
export class MeetingComponent {
public loading = false;
public error = "";
public success = false;
public firstName = new FormControl("");
public lastName = new FormControl("");
public email = new FormControl("");
public company = new FormControl("");
public sessionLength = new FormControl("");
public sessionGoal = new FormControl("");
public paymentUnderstanding = new FormControl(false);
public consent = new FormControl(false);
/**
* @param apiService
*/
constructor(private readonly apiService: ApiService) {}
/**
* @param e
*/
public submit(e: MouseEvent): void {
this.error = "";
const { form } = e.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (!valid) {
return;
}
this.loading = true;
this.apiService.
submitMeeting({
companyName: this.company.value ?? undefined,
consent: this.consent.value ?? false,
email: this.email.value ?? undefined,
firstName: this.firstName.value ?? undefined,
lastName: this.lastName.value ?? undefined,
paymentUnderstanding: this.paymentUnderstanding.value ?? undefined,
sessionGoal: this.sessionGoal.value ?? undefined,
sessionLength: Number.parseInt(this.sessionLength.value ?? "15", 10) as SessionLength,
}).
then((res) => {
if ("error" in res) {
this.error = res.error;
this.loading = false;
} else {
this.error = "";
this.success = true;
}
}).
catch(() => {
this.error = "An error occurred while submitting the form.";
this.loading = false;
});
}
}

@ -0,0 +1,13 @@
<h1>Mentorship Application</h1>
<p>This form allows you to apply to join our mentorship programme.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-multi-line label="What do you want to focus on during your time in our programme?" [control]="mentorshipGoal"></app-multi-line>
<app-multi-line label="Where are you currently at in your learning journey?" [control]="currentFocus"></app-multi-line>
<app-checkbox [control]="paymentUnderstanding" label="I understand that the programme cost is $200 per month, due on the first of the month. I also understand that failure to pay on the due date will result in my removal from the programme."></app-checkbox>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { MentorshipComponent } from "./mentorship.component";
describe("MentorshipComponent", () => {
let component: MentorshipComponent;
let fixture: ComponentFixture<MentorshipComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ MentorshipComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(MentorshipComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,90 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ConsentComponent } from "../../consent/consent.component.js";
import { ErrorComponent } from "../../error/error.component.js";
import { CheckboxComponent } from "../../inputs/checkbox/checkbox.component.js";
import { MultiLineComponent } from "../../inputs/multi-line/multi-line.component.js";
import { SuccessComponent } from "../../success/success.component.js";
import { UserinfoComponent } from "../../userinfo/userinfo.component.js";
import type { ApiService } from "../../api.service.js";
/**
*
*/
@Component({
imports: [
RouterModule,
CommonModule,
ConsentComponent,
UserinfoComponent,
ErrorComponent,
ReactiveFormsModule,
CheckboxComponent,
MultiLineComponent,
SuccessComponent,
],
selector: 'mentorship-form',
styleUrl: './mentorship.component.css',
templateUrl: "./mentorship.component.html",
})
export class MentorshipComponent {
public loading = false;
public error = "";
public success = false;
public firstName = new FormControl("");
public lastName = new FormControl("");
public email = new FormControl("");
public company = new FormControl("");
public mentorshipGoal = new FormControl("");
public currentFocus = new FormControl("");
public paymentUnderstanding = new FormControl(false);
public consent = new FormControl(false);
/**
* @param apiService
*/
constructor(private readonly apiService: ApiService) {}
/**
* @param e
*/
public submit(e: MouseEvent): void {
this.error = "";
const { form } = e.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (!valid) {
return;
}
this.loading = true;
this.apiService.
submitMentorship({
companyName: this.company.value ?? undefined,
consent: this.consent.value ?? false,
currentFocus: this.currentFocus.value ?? undefined,
email: this.email.value ?? undefined,
firstName: this.firstName.value ?? undefined,
lastName: this.lastName.value ?? undefined,
mentorshipGoal: this.mentorshipGoal.value ?? undefined,
paymentUnderstanding: this.paymentUnderstanding.value ?? false,
}).
then((res) => {
if ("error" in res) {
this.error = res.error;
this.loading = false;
} else {
this.error = "";
this.success = true;
}
}).
catch(() => {
this.error = "An error occurred while submitting the form.";
this.loading = false;
});
}
}

@ -0,0 +1,23 @@
<h1>Staff Application</h1>
<p>This form allows you to apply to join our staff team.</p>
<p><a href="https://docs.nhcarrigan.com/staff/apply/" target="_blank" rel="noreferrer">Read this first</a>.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-checkbox [control]="understandVolunteer" label="I understand that my membership on the staff team is on a voluntary basis, and I expect no compensation for my time or effort."></app-checkbox>
<div class="two-col">
<app-select-menu [control]="platform" label="Which platform do you wish to moderate?" options="Forum,IRC,Matrix,Gitea,Fediverse"></app-select-menu>
<app-single-line label="What is your username on that platform?" type="text" [control]="platformUsername"></app-single-line>
</div>
<app-multi-line label="Why do you wish to join our team?" [control]="whyJoin"></app-multi-line>
<app-multi-line label="Describe how your current behaviour in our communities embodies our Code of Conduct, Community Guidelines, and other Policies." [control]="currentBehaviour"></app-multi-line>
<app-multi-line label="What prior moderation experience do you have, if any?" [control]="priorExperience"></app-multi-line>
<app-multi-line label="You and another member of our team are in disagreement with how to handle a situation, and are unable to reach a consensus. What do you do?" [control]="internalConflict"></app-multi-line>
<app-multi-line label="A member of the community has posted material which violates our rules and may be traumatic to other members. How do you address the situation to ensure the well-being of our community?" [control]="handlingTrauma"></app-multi-line>
<app-multi-line label="Explain a time where you were in a difficult situation, and you had to be the one to resolve it. How did you do so, and why did you take that approach?" [control]="difficultSituation"></app-multi-line>
<app-multi-line label="Explain a situation in which you had to display strong leadership. How did you do so, and why did you take that approach?" [control]="leadershipSituation"></app-multi-line>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { StaffComponent } from "./staff.component";
describe("StaffComponent", () => {
let component: StaffComponent;
let fixture: ComponentFixture<StaffComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ StaffComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(StaffComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,108 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import type { Platform } from "@repo/types/prod/unions/platform.js";
import type { ApiService } from "../../api.service.js";
import { ConsentComponent } from "../../consent/consent.component.js";
import { ErrorComponent } from "../../error/error.component.js";
import { CheckboxComponent } from "../../inputs/checkbox/checkbox.component.js";
import { MultiLineComponent } from "../../inputs/multi-line/multi-line.component.js";
import { SelectMenuComponent } from "../../inputs/select-menu/select-menu.component.js";
import { SingleLineComponent } from "../../inputs/single-line/single-line.component.js";
import { SuccessComponent } from "../../success/success.component.js";
import { UserinfoComponent } from "../../userinfo/userinfo.component.js";
/**
*
*/
@Component({
imports: [
RouterModule,
CommonModule,
ConsentComponent,
UserinfoComponent,
ErrorComponent,
ReactiveFormsModule,
CheckboxComponent,
SingleLineComponent,
SelectMenuComponent,
MultiLineComponent,
SuccessComponent,
],
selector: 'staff-form',
styleUrl: './staff.component.css',
templateUrl: "./staff.component.html",
})
export class StaffComponent {
public loading = false;
public error = "";
public success = false;
public firstName = new FormControl("");
public lastName = new FormControl("");
public email = new FormControl("");
public company = new FormControl("");
public understandVolunteer = new FormControl(false);
public platform = new FormControl<Platform>("Forum");
public platformUsername = new FormControl("");
public whyJoin = new FormControl("");
public currentBehaviour = new FormControl("");
public priorExperience = new FormControl("");
public internalConflict = new FormControl("");
public handlingTrauma = new FormControl("");
public difficultSituation = new FormControl("");
public leadershipSituation = new FormControl("");
public consent = new FormControl(false);
/**
* @param apiService
*/
constructor(private readonly apiService: ApiService) {}
/**
* @param e
*/
public submit(e: MouseEvent): void {
this.error = "";
const { form } = e.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (!valid) {
return;
}
this.loading = true;
this.apiService.
submitStaff({
currentBehaviour: this.currentBehaviour.value ?? undefined,
email: this.email.value ?? undefined,
difficultSituation: this.difficultSituation.value ?? undefined,
firstName: this.firstName.value ?? undefined,
consent: this.consent.value ?? false,
internalConflict: this.internalConflict.value ?? undefined,
handlingTrauma: this.handlingTrauma.value ?? undefined,
lastName: this.lastName.value ?? undefined,
leadershipSituation: this.leadershipSituation.value ?? undefined,
platform: this.platform.value ?? undefined,
platformUsername: this.platformUsername.value ?? undefined,
priorExperience: this.priorExperience.value ?? undefined,
understandVolunteer: this.understandVolunteer.value ?? false,
whyJoin: this.whyJoin.value ?? undefined,
}).
then((res) => {
if ("error" in res) {
this.error = res.error;
this.loading = false;
} else {
this.error = "";
this.success = true;
}
}).
catch(() => {
this.error = "An error occurred while submitting the form.";
this.loading = false;
});
}
}

@ -0,0 +1,4 @@
a[routerLink] {
display: block;
text-decoration: underline;
}

@ -0,0 +1,31 @@
<h1>Forms</h1>
<p>This is just a landing page for our various forms.</p>
<p>
If you aren't sure what to do here, you probably don't need to be here at all.
</p>
<p>
Or if you ARE supposed to be here, but need a refresher, check our
<a href="https://docs.nhcarrigan.com" target="_blank" rel="noreferrer"
>documentation</a
>.
</p>
<a routerLink="/appeal"><i class="fa-solid fa-gavel"></i> Appeal a Sanction</a>
<a routerLink="/contact">
<i class="fa-solid fa-address-card" aria-hidden="true"></i> Contact us!
</a>
<a routerLink="/commission">
<i class="fa-solid fa-money-check-dollar" aria-hidden="true"></i> Commission
Us
</a>
<a routerLink="/staff">
<i class="fa-solid fa-user-shield" aria-hidden="true"></i> Join Our Team
</a>
<a routerLink="/event">
<i class="fa-solid fa-calendar-days" aria-hidden="true"></i> Event Requests
</a>
<a routerLink="/meeting">
<i class="fa-solid fa-handshake" aria-hidden="true"></i> Book a 1:1
</a>
<a routerLink="/mentorship">
<i class="fa-solid fa-brain" aria-hidden="true"></i> Mentorship Programme
</a>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { HomeComponent } from "./home.component";
describe("HomeComponent", () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ HomeComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,15 @@
import { Component } from "@angular/core";
import { RouterModule } from "@angular/router";
/**
*
*/
@Component({
imports: [ RouterModule ],
selector: 'app-home',
styleUrl: './home.component.css',
templateUrl: "./home.component.html",
})
export class HomeComponent {
}

@ -0,0 +1,10 @@
div {
display: grid;
grid-template-columns: auto auto;
align-items: start;
margin: 0.5rem 0;
}
label {
font-size: 0.7rem;
line-height: 1rem;
}

@ -0,0 +1,4 @@
<div>
<input type="checkbox" id="{{label()}}-input" [required]="required()" [formControl]="control()" />
<label for="{{label()}}-input">{{ label() }}</label>
</div>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { CheckboxComponent } from "./checkbox.component";
describe("CheckboxComponent", () => {
let component: CheckboxComponent;
let fixture: ComponentFixture<CheckboxComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ CheckboxComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(CheckboxComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,17 @@
import { Component, input } from "@angular/core";
import { ReactiveFormsModule, type FormControl } from "@angular/forms";
/**
*
*/
@Component({
imports: [ ReactiveFormsModule ],
selector: 'app-checkbox',
styleUrl: './checkbox.component.css',
templateUrl: "./checkbox.component.html",
})
export class CheckboxComponent {
public label = input.required<string>();
public control = input.required<FormControl>();
public required = input<boolean>(true);
}

@ -0,0 +1,20 @@
div {
width: 100%;
}
label {
display: block;
font-size: 0.75rem;
text-align: left;
}
textarea {
width: 100%;
background: var(--foreground);
color: var(--background);
border: 1px solid white;
border-radius: 10px;
padding: 0.25rem;
font-family: "OpenDyslexic";
resize: vertical;
}

@ -0,0 +1,4 @@
<div>
<label for="{{label()}}-input">{{ label() }}:</label>
<textarea id="{{label()}}-input" required [formControl]="control()"></textarea>
</div>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { MultiLineComponent } from "./multi-line.component";
describe("MultiLineComponent", () => {
let component: MultiLineComponent;
let fixture: ComponentFixture<MultiLineComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ MultiLineComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(MultiLineComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,17 @@
import { CommonModule } from "@angular/common";
import { Component, input } from "@angular/core";
import { ReactiveFormsModule, type FormControl } from "@angular/forms";
/**
*
*/
@Component({
imports: [ CommonModule, ReactiveFormsModule ],
selector: 'app-multi-line',
styleUrl: './multi-line.component.css',
templateUrl: "./multi-line.component.html",
})
export class MultiLineComponent {
public label = input.required<string>();
public control = input.required<FormControl>();
}

@ -0,0 +1,19 @@
div {
width: 100%;
}
label {
display: block;
font-size: 0.75rem;
text-align: left;
}
select {
width: 100%;
background: var(--foreground);
color: var(--background);
border: 1px solid white;
border-radius: 10px;
padding: 0.25rem;
font-family: "OpenDyslexic";
}

@ -0,0 +1,7 @@
<div>
<label for="{{label()}}-input">{{ label() }}:</label>
<select id="{{label()}}-input" [formControl]="control()" required>
<option value="" disabled selected>Select an option</option>
<option *ngFor="let option of options().split(',')" [value]="option">{{ option }}</option>
</select>
</div>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { SelectMenuComponent } from "./select-menu.component";
describe("SelectMenuComponent", () => {
let component: SelectMenuComponent;
let fixture: ComponentFixture<SelectMenuComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ SelectMenuComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(SelectMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,18 @@
import { CommonModule } from "@angular/common";
import { Component, input } from "@angular/core";
import { ReactiveFormsModule, type FormControl } from "@angular/forms";
/**
*
*/
@Component({
imports: [ CommonModule, ReactiveFormsModule ],
selector: 'app-select-menu',
styleUrl: './select-menu.component.css',
templateUrl: "./select-menu.component.html",
})
export class SelectMenuComponent {
public label = input.required<string>();
public options = input.required<string>();
public control = input.required<FormControl>();
}

@ -0,0 +1,18 @@
div {
width: 100%;
}
label {
display: block;
font-size: 0.75rem;
text-align: left;
}
input {
width: 100%;
background: var(--foreground);
color: var(--background);
border: 1px solid white;
border-radius: 10px;
padding: 0.25rem;
}

@ -0,0 +1,4 @@
<div>
<label for="{{label()}}-input">{{ label() }}:</label>
<input [type]="type()" id="{{label()}}-input" required [formControl]="control()" />
</div>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { SingleLineComponent } from "./single-line.component";
describe("SingleLineComponent", () => {
let component: SingleLineComponent;
let fixture: ComponentFixture<SingleLineComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ SingleLineComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(SingleLineComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,18 @@
import { CommonModule } from "@angular/common";
import { Component, input } from "@angular/core";
import { type FormControl, ReactiveFormsModule } from "@angular/forms";
/**
*
*/
@Component({
imports: [ CommonModule, ReactiveFormsModule ],
selector: 'app-single-line',
styleUrl: './single-line.component.css',
templateUrl: "./single-line.component.html",
})
export class SingleLineComponent {
public type = input.required<"text" | "email" | "number">();
public label = input.required<string>();
public control = input.required<FormControl>();
}

@ -0,0 +1,71 @@
<h1>Review Submissions</h1>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<form *ngIf="!valid">
<app-single-line
label="API Key"
[control]="token"
type="text"
></app-single-line>
<button type="button" (click)="submit($event)">Validate</button>
</form>
<div *ngIf="valid">
<button
[disabled]="view === 'appeals'"
type="button"
(click)="setView('appeals')"
>
Sanction Appeals
</button>
<button
[disabled]="view === 'commissions'"
type="button"
(click)="setView('commissions')"
>
Commission Requests
</button>
<button
[disabled]="view === 'contacts'"
type="button"
(click)="setView('contacts')"
>
Contact Requests
</button>
<button
[disabled]="view === 'events'"
type="button"
(click)="setView('events')"
>
Event Proposals
</button>
<button
[disabled]="view === 'meetings'"
type="button"
(click)="setView('meetings')"
>
Meeting Requests
</button>
<button
[disabled]="view === 'mentorships'"
type="button"
(click)="setView('mentorships')"
>
Mentorship Applications
</button>
<button
[disabled]="view === 'staff'"
type="button"
(click)="setView('staff')"
>
Staff Applications
</button>
<h2>{{ view }}</h2>
<div *ngFor="let datum of data">
<h3>{{ datum.email }}</h3>
<div *ngFor="let obj of datum.info">
<p><strong>{{ obj.key }}</strong>: {{ obj.value }}</p>
</div>
<button type="button" (click)="markReviewed(datum.id)">
Mark as Reviewed
</button>
</div>
</div>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { ReviewComponent } from "./review.component";
describe("ReviewComponent", () => {
let component: ReviewComponent;
let fixture: ComponentFixture<ReviewComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ ReviewComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(ReviewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,145 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ErrorComponent } from "../error/error.component.js";
import { SingleLineComponent } from "../inputs/single-line/single-line.component.js";
import type { ApiService } from "../api.service.js";
/**
*
*/
@Component({
imports: [
CommonModule,
ReactiveFormsModule,
RouterModule,
SingleLineComponent,
ErrorComponent,
],
selector: 'app-review',
styleUrl: './review.component.css',
templateUrl: "./review.component.html",
})
export class ReviewComponent {
public error = "";
public token = new FormControl("");
public valid = false;
public data: Array<{ email: string; id: string; info: Array<{ key: string; value: unknown }> }> = [];
public view:
| ""
| "appeals"
| "commissions"
| "contacts"
| "events"
| "meetings"
| "mentorships"
| "staff" = "";
/**
* @param apiService
*/
constructor(private readonly apiService: ApiService) {
const storedToken = localStorage.getItem("token");
if (!storedToken) {
return;
}
this.token.setValue(storedToken);
this.valid = true;
/*
* This.apiService
* .validateToken(storedToken)
* .then((valid) => {
* if (valid) {
* this.valid = true;
* } else {
* this.error = 'The token found in local storage is invalid.';
* this.valid = false;
* }
* })
* .catch(() => {
* this.error = 'An error occurred while validating your stored token.';
* this.valid = false;
* });
*/
}
/**
* @param e
*/
public submit(e: MouseEvent): void {
this.error = "";
const { form } = e.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (!valid) {
return;
}
this.apiService.
validateToken(this.token.value).
then((valid) => {
if (valid) {
this.valid = true;
localStorage.setItem("token", this.token.value as string);
} else {
this.error = "The token you entered is invalid.";
this.valid = false;
}
}).
catch(() => {
this.error = "An error occurred while submitting the form.";
this.valid = false;
});
}
/**
* @param id
*/
public markReviewed(id: string): void {
const view = this.view as "appeals" | "commissions" | "contacts" | "events" | "meetings" | "mentorships" | "staff";
this.apiService.markReviewed(view, id, this.token.value!).then((data) => {
if ("error" in data) {
this.error = data.error;
} else {
this.data = this.data.filter((item) => {
return item.id !== id;
});
}
});
}
/**
* @param view
*/
public setView(
view:
| "appeals"
| "commissions"
| "contacts"
| "events"
| "meetings"
| "mentorships"
| "staff",
): void {
this.view = view;
this.apiService.getData(view, this.token.value!).then((data) => {
if ("error" in data) {
this.error = data.error;
} else {
this.data = (data.data as Array<
{ email: string; id: string } & Record<string, unknown>
>).map((item) => {
const object: { email: string; id: string; info: Array<{ key: string; value: unknown }> } = { email: item.email, id: item.id, info: [] };
for (const key in item) {
if (![ "email", "id" ].includes(key)) {
object.info.push({ key, value: item[key] });
}
}
return object;
});
}
});
}
}

@ -0,0 +1,8 @@
div {
width: 100%;
padding: 0.5rem;
font-size: 1.3rem;
background: rgba(100, 255, 100, 0.5);
color: rgb(0, 100, 0);
border: 1px solid rgb(0, 100, 0);
}

@ -0,0 +1,3 @@
<div>
<p>Your submission has been received! Please keep an eye on your email - we will reach out soon.</p>
</div>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { SuccessComponent } from "./success.component";
describe("SuccessComponent", () => {
let component: SuccessComponent;
let fixture: ComponentFixture<SuccessComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ SuccessComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(SuccessComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,14 @@
import { Component } from "@angular/core";
/**
*
*/
@Component({
imports: [],
selector: 'app-success',
styleUrl: './success.component.css',
templateUrl: "./success.component.html",
})
export class SuccessComponent {
}

@ -0,0 +1,8 @@
<div class="two-col">
<app-single-line label="First Name" type="text" [control]="firstNameControl()"></app-single-line>
<app-single-line label="Last Name" type="text" [control]="lastNameControl()"></app-single-line>
</div>
<div class="two-col">
<app-single-line label="Email" type="email" [control]="emailControl()"></app-single-line>
<app-single-line label="Company Name" type="text" [control]="companyControl()"></app-single-line>
</div>

@ -0,0 +1,22 @@
import { type ComponentFixture, TestBed } from "@angular/core/testing";
import { UserinfoComponent } from "./userinfo.component";
describe("UserinfoComponent", () => {
let component: UserinfoComponent;
let fixture: ComponentFixture<UserinfoComponent>;
beforeEach(async() => {
await TestBed.configureTestingModule({
imports: [ UserinfoComponent ],
}).
compileComponents();
fixture = TestBed.createComponent(UserinfoComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,19 @@
import { Component, input } from "@angular/core";
import { SingleLineComponent } from "../inputs/single-line/single-line.component.js";
import type { FormControl } from "@angular/forms";
/**
*
*/
@Component({
imports: [ SingleLineComponent ],
selector: 'app-userinfo',
styleUrl: './userinfo.component.css',
templateUrl: "./userinfo.component.html",
})
export class UserinfoComponent {
public firstNameControl = input.required<FormControl>();
public lastNameControl = input.required<FormControl>();
public emailControl = input.required<FormControl>();
public companyControl = input.required<FormControl>();
}

15
client/src/index.html Normal file

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>NHCarrigan Forms</title>
<meta name="description" content="Landing page for the various forms someone might need to fill out when engaging with us.">
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
<script src="https://cdn.nhcarrigan.com/headers/index.js"></script>
</html>

8
client/src/main.ts Normal file

@ -0,0 +1,8 @@
import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
import { appConfig } from "./app/app.config";
bootstrapApplication(AppComponent, appConfig).
catch((error) => {
console.error(error);
});

13
client/src/styles.css Normal file

@ -0,0 +1,13 @@
/* You can add global styles to this file, and also import other style files */
form {
max-width: 500px;
width: 95%;
margin: 0 auto;
}
.two-col {
display: grid;
grid-template-columns: 1fr 1fr;
justify-content: center;
gap: 10px;
}

15
client/tsconfig.app.json Normal file

@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

27
client/tsconfig.json Normal file

@ -0,0 +1,27 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

15
client/tsconfig.spec.json Normal file

@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

23
package.json Normal file

@ -0,0 +1,23 @@
{
"name": "forms",
"version": "0.0.0",
"description": "Application for managing and submitting our various forms!",
"main": "index.js",
"packageManager": "pnpm@10.0.0",
"scripts": {
"build": "turbo build",
"start": "turbo start",
"lint": "turbo lint",
"test": "echo \"Error: no test specified\" && exit 0"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"prisma": "6.3.1",
"turbo": "2.4.2"
},
"dependencies": {
"@prisma/client": "6.3.1"
}
}

@ -0,0 +1,5 @@
import NaomisConfig from "@nhcarrigan/eslint-config";
export default [
...NaomisConfig,
]

@ -0,0 +1,23 @@
{
"name": "@repo/types",
"version": "0.0.0",
"private": true,
"description": "Type-definitions shared between client and server.",
"type": "module",
"main": "prod/index.js",
"scripts": {
"build": "tsc",
"lint": "eslint src --max-warnings 0",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "See License in LICENSE.md",
"devDependencies": {
"@nhcarrigan/eslint-config": "5.2.0",
"@nhcarrigan/typescript-config": "4.0.0",
"@types/node": "22.13.4",
"eslint": "9.20.1",
"typescript": "5.7.3"
}
}

@ -0,0 +1,25 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { Platform } from "../unions/platform.js";
import type { Sanction } from "../unions/sanction.js";
export interface Appeal {
consent: boolean;
email: string;
firstName: string;
lastName: string;
understandBinding: boolean;
sanctionType: Sanction;
caseNumber: number;
sanctionPlatform: Platform;
platformUsername: string;
sanctionReason: string;
sanctionFair: string;
behaviourViolation: string;
appealReason: string;
behaviourImprove: string;
}

@ -0,0 +1,14 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface Commission {
consent: boolean;
email: string;
firstName: string;
lastName: string;
companyName: string;
request: string;
}

@ -0,0 +1,14 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface Contact {
consent: boolean;
email: string;
firstName: string;
lastName: string;
companyName: string;
request: string;
}

@ -0,0 +1,21 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface Event {
consent: boolean;
email: string;
firstName: string;
lastName: string;
companyName: string;
eventDescription: string;
eventTopic: string;
eventLocation: string;
eventDate: string;
eventBudget: string;
travelCovered: boolean;
lodgingCovered: boolean;
foodCovered: boolean;
}

@ -0,0 +1,18 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { SessionLength } from "../unions/session.js";
export interface Meeting {
consent: boolean;
email: string;
firstName: string;
lastName: string;
companyName: string;
sessionLength: SessionLength;
sessionGoal: string;
paymentUnderstanding: boolean;
}

@ -0,0 +1,16 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface Mentorship {
consent: boolean;
email: string;
firstName: string;
lastName: string;
companyName: string;
mentorshipGoal: string;
currentFocus: string;
paymentUnderstanding: boolean;
}

Some files were not shown because too many files have changed in this diff Show More