feat: initial project prototype #2

Merged
naomi merged 14 commits from feat/init into main 2025-02-17 14:03:33 -08:00
142 changed files with 12012 additions and 96 deletions
Showing only changes of commit 2f08f1ed18 - Show all commits

1
.gitignore vendored
View File

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

17
client/.editorconfig Normal file
View 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
View 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
View 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
View 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": []
}
}
}
}
}
}

View File

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

40
client/package.json Normal file
View 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"
}
}

View File

@ -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();
});
});

View File

@ -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;
}
}

View File

View File

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

View File

@ -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");
});
});

View File

@ -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";
}

View File

@ -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) ],
};

View File

@ -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" },
];

View File

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

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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>();
}

View File

@ -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);
}

View File

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

View File

@ -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();
});
});

View File

@ -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>();
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
});
}
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
});
}
}

View File

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

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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 {
}

View File

@ -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;
}

View File

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

View File

@ -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();
});
});

View File

@ -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);
}

View File

@ -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;
}

View File

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

View File

@ -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();
});
});

View File

@ -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>();
}

View File

@ -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";
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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>();
}

View File

@ -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;
}

View File

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

View File

@ -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();
});
});

View File

@ -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>();
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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;
});
}
});
}
}

View File

@ -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);
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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 {
}

View File

@ -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>

View File

@ -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();
});
});

View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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"
]
}

View File

@ -1,22 +1,23 @@
{
"name": "forms-api",
"name": "forms",
"version": "0.0.0",
"description": "The API for all of our web forms.",
"description": "Application for managing and submitting our various forms!",
"main": "index.js",
"packageManager": "pnpm@10.0.0",
"scripts": {
"build": "tsc",
"lint": "eslint src --max-warnings 0",
"start": "op run --env-file=prod.env --no-masking -- node prod/index.js",
"build": "turbo build",
"start": "turbo start",
"lint": "turbo lint",
"test": "echo \"Error: no test specified\" && exit 0"
},
"keywords": [],
"author": "",
"license": "ISC",
"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"
"prisma": "6.3.1",
"turbo": "2.4.2"
},
"dependencies": {
"@prisma/client": "6.3.1"
}
}

View File

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

View File

@ -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"
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,24 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { Platform } from "../unions/platform.js";
export interface Staff {
consent: boolean;
email: string;
firstName: string;
lastName: string;
understandVolunteer: boolean;
platform: Platform;
platformUsername: string;
whyJoin: string;
currentBehaviour: string;
priorExperience: string;
internalConflict: string;
handlingTrauma: string;
difficultSituation: string;
leadershipSituation: string;
}

View File

@ -0,0 +1,31 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import type { Appeal } from "./forms/appeal.js";
import type { Commission } from "./forms/commission.js";
import type { Contact } from "./forms/contact.js";
import type { Event } from "./forms/event.js";
import type { Meeting } from "./forms/meeting.js";
import type { Mentorship } from "./forms/mentorship.js";
import type { Staff } from "./forms/staff.js";
import type { ReviewRequest } from "./requests/review.js";
import type { DataResponse } from "./responses/data.js";
import type { ErrorResponse } from "./responses/error.js";
import type { SuccessResponse } from "./responses/success.js";
export type {
Appeal,
Commission,
Contact,
Event,
Meeting,
Mentorship,
Staff,
ReviewRequest,
DataResponse,
ErrorResponse,
SuccessResponse,
};

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