feat: initial project prototype (#2)
Some checks failed
Node.js CI / Lint and Test (push) Has been cancelled

### Explanation

_No response_

### Issue

_No response_

### Attestations

- [x] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [x] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [x] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [x] I have pinned the dependencies to a specific patch version.

### Style

- [x] I have run the linter and resolved any errors.
- [x] My pull request uses an appropriate title, matching the conventional commit standards.
- [x] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [x] All new and existing tests pass locally with my changes.
- [x] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

Major - My pull request introduces a breaking change.

Reviewed-on: #2
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit is contained in:
Naomi Carrigan 2025-02-17 14:03:32 -08:00 committed by Naomi Carrigan
parent b43bfd612b
commit e8f5078aa3
120 changed files with 15657 additions and 0 deletions

44
.gitea/workflows/ci.yml Normal file
View File

@ -0,0 +1,44 @@
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: Build internal package
run: cd packages/types && pnpm build
- name: Install again
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

3
.gitignore vendored Normal file
View File

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

10
.vscode/settings.json vendored Normal file
View 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
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.

76
client/angular.json Normal file
View File

@ -0,0 +1,76 @@
{
"$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.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"
}
}
}
}
}

22
client/eslint.config.js Normal file
View File

@ -0,0 +1,22 @@
import NaomisConfig from "@nhcarrigan/eslint-config";
export default [
...NaomisConfig,
{
rules: {
"no-console": "off",
"new-cap": "off",
"@typescript-eslint/naming-convention": "off",
"jsdoc/require-jsdoc": "off",
"jsdoc/require-param": "off",
"jsdoc/require-returns": "off",
"@typescript-eslint/no-useless-constructor": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/consistent-type-assertions": "off",
"@typescript-eslint/no-extraneous-class": "off",
"stylistic/no-multi-spaces": "off",
"unicorn/filename-case": "off",
"@typescript-eslint/consistent-type-imports": "off"
},
},
]

41
client/package.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "client",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "ng dev",
"lint": "eslint src --max-warnings 0",
"build": "ng build",
"test": "echo \"Error: no test specified\" && exit 0"
},
"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",
"@repo/types": "../packages/types",
"@types/jasmine": "~5.1.0",
"eslint": "9.20.1",
"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"
}
}

View File

@ -0,0 +1,190 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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 = "https://forms-api.nhcarrigan.com";
public constructor() {}
public async validateToken(token: string | null): Promise<boolean> {
if (token === null) {
return false;
}
const ipRequest = await fetch("https://api.ipify.org?format=json");
const ipResult = await ipRequest.json() as { ip: string };
const { ip } = ipResult;
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() as { valid: boolean };
return response.valid;
}
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() as SuccessResponse | ErrorResponse;
return response;
}
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() as SuccessResponse | ErrorResponse;
return response;
}
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() as SuccessResponse | ErrorResponse;
return response;
}
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() as SuccessResponse | ErrorResponse;
return response;
}
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() as SuccessResponse | ErrorResponse;
return response;
}
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() as SuccessResponse | ErrorResponse;
return response;
}
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() as SuccessResponse | ErrorResponse;
return response;
}
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() as DataResponse | ErrorResponse;
return response;
}
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() as SuccessResponse | ErrorResponse;
return response;
}
}

View File

View File

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

View File

@ -0,0 +1,18 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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 {
public title = "NHCarrigan Forms";
}

View File

@ -0,0 +1,17 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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,30 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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,17 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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,16 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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,111 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { ReactiveFormsModule, FormControl } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ApiService } from "../../api.service";
import { ConsentComponent } from "../../consent/consent.component";
import { ErrorComponent } from "../../error/error.component";
import { CheckboxComponent } from "../../inputs/checkbox/checkbox.component";
import { MultiLineComponent }
from "../../inputs/multi-line/multi-line.component";
import { SelectMenuComponent }
from "../../inputs/select-menu/select-menu.component";
import { SingleLineComponent }
from "../../inputs/single-line/single-line.component";
import { SuccessComponent } from "../../success/success.component";
import { UserinfoComponent } from "../../userinfo/userinfo.component";
import type { Platform } from "@repo/types/prod/unions/platform";
import type { Sanction } from "@repo/types/prod/unions/sanction";
@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);
public constructor(private readonly apiService: ApiService) {}
// eslint-disable-next-line complexity -- stfu
public submit(event: MouseEvent): void {
this.error = "";
const { form } = event.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (valid !== true) {
return;
}
this.loading = true;
this.apiService.
submitAppeal({
appealReason: this.appealReason.value ?? undefined,
behaviourImprove: this.behaviourImprove.value ?? undefined,
behaviourViolation: this.behaviourViolation.value ?? undefined,
caseNumber: Number.parseInt(this.caseNumber.value ?? "0", 10),
consent: this.consent.value ?? false,
email: this.email.value ?? undefined,
firstName: this.firstName.value ?? undefined,
lastName: this.lastName.value ?? undefined,
platformUsername: this.platformUsername.value ?? undefined,
sanctionFair: this.sanctionFair.value ?? undefined,
sanctionPlatform: this.sanctionPlatform.value ?? undefined,
sanctionReason: this.sanctionReason.value ?? undefined,
sanctionType:
(this.sanctionType.value?.split(/\s+/)[0] as Sanction | undefined)
?? undefined,
understandBinding: this.understandBinding.value ?? undefined,
}).
then((response) => {
if ("error" in response) {
this.error = response.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,81 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ApiService } from "../../api.service";
import { ConsentComponent } from "../../consent/consent.component";
import { ErrorComponent } from "../../error/error.component";
import { MultiLineComponent }
from "../../inputs/multi-line/multi-line.component";
import { SuccessComponent } from "../../success/success.component";
import { UserinfoComponent } from "../../userinfo/userinfo.component";
@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);
public constructor(private readonly apiService: ApiService) {}
public submit(event: MouseEvent): void {
this.error = "";
const { form } = event.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (valid !== true) {
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((response) => {
if ("error" in response) {
this.error = response.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,81 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ApiService } from "../../api.service";
import { ConsentComponent } from "../../consent/consent.component";
import { ErrorComponent } from "../../error/error.component";
import { MultiLineComponent }
from "../../inputs/multi-line/multi-line.component";
import { SuccessComponent } from "../../success/success.component";
import { UserinfoComponent } from "../../userinfo/userinfo.component";
@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);
public constructor(private readonly apiService: ApiService) {}
public submit(event: MouseEvent): void {
this.error = "";
const { form } = event.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (valid !== true) {
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((response) => {
if ("error" in response) {
this.error = response.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,101 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ApiService } from "../../api.service";
import { ConsentComponent } from "../../consent/consent.component";
import { ErrorComponent } from "../../error/error.component";
import { CheckboxComponent } from "../../inputs/checkbox/checkbox.component";
import { MultiLineComponent }
from "../../inputs/multi-line/multi-line.component";
import { SingleLineComponent }
from "../../inputs/single-line/single-line.component";
import { SuccessComponent } from "../../success/success.component";
import { UserinfoComponent } from "../../userinfo/userinfo.component";
@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);
public constructor(private readonly apiService: ApiService) {}
// eslint-disable-next-line complexity -- stfu
public submit(event: MouseEvent): void {
this.error = "";
const { form } = event.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (valid !== true) {
return;
}
this.loading = true;
this.apiService.
submitEvent({
companyName: this.company.value ?? undefined,
consent: this.consent.value ?? false,
email: this.email.value ?? undefined,
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((response) => {
if ("error" in response) {
this.error = response.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,94 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ApiService } from "../../api.service";
import { ConsentComponent } from "../../consent/consent.component";
import { ErrorComponent } from "../../error/error.component";
import { CheckboxComponent } from "../../inputs/checkbox/checkbox.component";
import { MultiLineComponent }
from "../../inputs/multi-line/multi-line.component";
import { SelectMenuComponent }
from "../../inputs/select-menu/select-menu.component";
import { SuccessComponent } from "../../success/success.component";
import { UserinfoComponent } from "../../userinfo/userinfo.component";
import type { SessionLength } from "@repo/types/prod/unions/session";
@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);
public constructor(private readonly apiService: ApiService) {}
// eslint-disable-next-line complexity -- stfu
public submit(event: MouseEvent): void {
this.error = "";
const { form } = event.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (valid !== true) {
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((response) => {
if ("error" in response) {
this.error = response.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,88 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ApiService } from "../../api.service";
import { ConsentComponent } from "../../consent/consent.component";
import { ErrorComponent } from "../../error/error.component";
import { CheckboxComponent } from "../../inputs/checkbox/checkbox.component";
import { MultiLineComponent }
from "../../inputs/multi-line/multi-line.component";
import { SuccessComponent } from "../../success/success.component";
import { UserinfoComponent } from "../../userinfo/userinfo.component";
@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);
public constructor(private readonly apiService: ApiService) {}
// eslint-disable-next-line complexity -- stfu
public submit(event: MouseEvent): void {
this.error = "";
const { form } = event.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (valid !== true) {
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((response) => {
if ("error" in response) {
this.error = response.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,108 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ApiService } from "../../api.service";
import { ConsentComponent } from "../../consent/consent.component";
import { ErrorComponent } from "../../error/error.component";
import { CheckboxComponent } from "../../inputs/checkbox/checkbox.component";
import { MultiLineComponent }
from "../../inputs/multi-line/multi-line.component";
import { SelectMenuComponent }
from "../../inputs/select-menu/select-menu.component";
import { SingleLineComponent }
from "../../inputs/single-line/single-line.component";
import { SuccessComponent } from "../../success/success.component";
import { UserinfoComponent } from "../../userinfo/userinfo.component";
import type { Platform } from "@repo/types/prod/unions/platform";
@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);
public constructor(private readonly apiService: ApiService) {}
// eslint-disable-next-line complexity -- stfu
public submit(event: MouseEvent): void {
this.error = "";
const { form } = event.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (valid !== true) {
return;
}
this.loading = true;
this.apiService.
submitStaff({
consent: this.consent.value ?? false,
currentBehaviour: this.currentBehaviour.value ?? undefined,
difficultSituation: this.difficultSituation.value ?? undefined,
email: this.email.value ?? undefined,
firstName: this.firstName.value ?? undefined,
handlingTrauma: this.handlingTrauma.value ?? undefined,
internalConflict: this.internalConflict.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((response) => {
if ("error" in response) {
this.error = response.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,17 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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,19 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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,19 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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,20 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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,21 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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,151 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* global localStorage -- this runs in the browser */
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { FormControl, ReactiveFormsModule } from "@angular/forms";
import { RouterModule } from "@angular/router";
import { ApiService } from "../api.service";
import { ErrorComponent } from "../error/error.component";
import { SingleLineComponent }
from "../inputs/single-line/single-line.component";
@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" = "";
public constructor(private readonly apiService: ApiService) {
const storedToken = localStorage.getItem("token");
if (storedToken === null) {
return;
}
this.apiService.validateToken(storedToken).
then((valid) => {
if (valid) {
this.valid = true;
this.token.setValue(storedToken);
} else {
this.error = "The token found in local storage is invalid.";
this.valid = false;
}
}).
catch(() => {
// eslint-disable-next-line stylistic/max-len -- Long string
this.error = "An error occurred while validating the token found in local storage.";
this.valid = false;
});
}
public submit(event: MouseEvent): void {
this.error = "";
const { form } = event.target as HTMLButtonElement;
const valid = form?.reportValidity();
if (valid !== true) {
return;
}
this.apiService.
validateToken(this.token.value).
then((tokenValid) => {
if (tokenValid) {
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;
});
}
public markReviewed(id: string): void {
const view = this.view as
| "appeals"
| "commissions"
| "contacts"
| "events"
| "meetings"
| "mentorships"
| "staff";
void this.apiService.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- We know token is not null
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;
});
}
});
}
public setView(
view:
| "appeals"
| "commissions"
| "contacts"
| "events"
| "meetings"
| "mentorships"
| "staff",
): void {
this.view = view;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- We know token is not null
void 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: 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,17 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
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,23 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Component, input } from "@angular/core";
import { SingleLineComponent }
from "../inputs/single-line/single-line.component";
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>

15
client/src/main.ts Normal file
View File

@ -0,0 +1,15 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";
import { appConfig } from "./app/app.config";
bootstrapApplication(AppComponent, appConfig).
// eslint-disable-next-line unicorn/prefer-top-level-await -- This is the entry point of the application.
catch((error: unknown) => {
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;
}

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

23
package.json Normal file
View 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": "turbo test"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"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 0"
},
"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,
};

View File

@ -0,0 +1,8 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface ReviewRequest {
submissionId: string;
}

View File

@ -0,0 +1,8 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface DataResponse {
data: unknown;
}

View File

@ -0,0 +1,8 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface ErrorResponse {
error: string;
}

View File

@ -0,0 +1,8 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export interface SuccessResponse {
success: true;
}

View File

@ -0,0 +1,7 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export type Platform = "Forum" | "IRC" | "Matrix" | "Gitea" | "Fediverse";

View File

@ -0,0 +1,7 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export type Sanction = "reminder" | "warning" | "temporary" | "permanent";

View File

@ -0,0 +1,7 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
export type SessionLength = 15 | 30 | 60;

View File

@ -0,0 +1,8 @@
{
"extends": "@nhcarrigan/typescript-config",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./prod",
"declaration": true,
},
}

12010
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,4 @@
packages:
- "client"
- "server"
- "packages/types"

105
prisma/schema.prisma Normal file
View File

@ -0,0 +1,105 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("MONGO_URI")
}
model Appeals {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
firstName String
lastName String
understandBinding Boolean
sanctionType String
caseNumber Int
sanctionPlatform String
platformUsername String
sanctionReason String
sanctionFair String
behaviourViolation String
appealReason String
behaviourImprove String
createdAt DateTime @default(now())
}
model Contacts {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
firstName String
lastName String
companyName String
request String
createdAt DateTime @default(now())
}
model Commissions {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
firstName String
lastName String
companyName String
request String
createdAt DateTime @default(now())
}
model Staff {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
firstName String
lastName String
understandVolunteer Boolean
platform String
platformUsername String
whyJoin String
currentBehaviour String
priorExperience String
internalConflict String
handlingTrauma String
difficultSituation String
leadershipSituation String
createdAt DateTime @default(now())
}
model Events {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
firstName String
lastName String
companyName String
eventDescription String
eventTopic String
eventLocation String
eventDate String
eventBudget String
travelCovered Boolean
lodgingCovered Boolean
foodCovered Boolean
createdAt DateTime @default(now())
}
model Meetings {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
firstName String
lastName String
companyName String
sessionLength Int
sessionGoal String
paymentUnderstanding Boolean
createdAt DateTime @default(now())
}
model Mentorships {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
firstName String
lastName String
companyName String
mentorshipGoal String
currentFocus String
paymentUnderstanding Boolean
createdAt DateTime @default(now())
}

18
server/eslint.config.js Normal file
View File

@ -0,0 +1,18 @@
import NaomisConfig from "@nhcarrigan/eslint-config";
export default [
...NaomisConfig,
{
files: ["src/routes/*.ts", "src/hooks/*.ts"],
rules: {
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-misused-promises": "off"
},
},
{
files: ["src/handlers/**/*.ts"],
rules: {
"@typescript-eslint/naming-convention": "off",
},
},
];

33
server/package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "forms-api",
"version": "0.0.0",
"description": "The API for all of our web forms.",
"main": "index.js",
"type": "module",
"scripts": {
"build": "tsc",
"lint": "eslint src --max-warnings 0",
"start": "op run --env-file=prod.env --no-masking -- node prod/index.js",
"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",
"@repo/types": "../packages/types",
"@types/node": "22.13.4",
"@types/nodemailer": "6.4.17",
"eslint": "9.20.1",
"prisma": "6.3.1",
"typescript": "5.7.3"
},
"dependencies": {
"@fastify/cors": "10.0.2",
"@nhcarrigan/logger": "1.0.0",
"@prisma/client": "6.3.1",
"fastify": "5.2.1",
"nodemailer": "6.10.0"
}
}

4
server/prod.env Normal file
View File

@ -0,0 +1,4 @@
MONGO_URI="op://Environment Variables - Naomi/Forms API/mongo_uri"
EMAIL_PASS="op://Environment Variables - Naomi/Forms API/email_password"
API_TOKEN="op://Environment Variables - Naomi/Forms API/api_token"
LOG_TOKEN="op://Environment Variables - Naomi/Alert Server/api_auth"

View File

@ -0,0 +1,38 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { listUnreviewedSubmissions } from "../../modules/genericDataQueries.js";
import { logger } from "../../utils/logger.js";
import type { DatabasePath } from "../../interfaces/databasePath.js";
import type { PrismaClient } from "@prisma/client";
import type { FastifyReply } from "fastify";
/**
* Queries the database for a specific submission type (based on
* the route parameter) and returns all unreviewed submissions.).
* @param database - The Prisma client.
* @param route - The type of data to list.
* @param response - The Fastify reply object.
*/
export const listHandler = async(
database: PrismaClient,
route: DatabasePath,
response: FastifyReply,
): Promise<void> => {
try {
const data = await listUnreviewedSubmissions(database, route);
await response.code(200).send({
data: data.sort((a, b) => {
return a.createdAt.getTime() - b.createdAt.getTime();
}),
});
} catch (error) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we bein' lazy.
await logger.error(`/list/${route}`, error as Error);
await response.status(500).send({ error: error instanceof Error
? error.message
: "Internal Server Error" });
}
};

View File

@ -0,0 +1,56 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import {
checkSubmissionExists,
markSubmissionReviewed,
} from "../../modules/genericDataQueries.js";
import { logger } from "../../utils/logger.js";
import type { DatabasePath } from "../../interfaces/databasePath.js";
import type { PrismaClient } from "@prisma/client";
import type {
ErrorResponse,
ReviewRequest,
SuccessResponse,
} from "@repo/types";
import type { FastifyReply, FastifyRequest } from "fastify";
/**
* Queries the database for a specific submission type (based on
* the route parameter) and returns all unreviewed submissions.).
* @param database - The Prisma client.
* @param route - The type of data to list.
* @param data - The fastify data.
* @param data.request - The request body.
* @param data.response - The Fastify reply object.
*/
export const reviewHandler = async(
database: PrismaClient,
route: DatabasePath,
{
request,
response,
}: {
request: FastifyRequest<{ Body: ReviewRequest }>;
response: FastifyReply<{ Reply: SuccessResponse | ErrorResponse }>;
},
): Promise<void> => {
try {
const { submissionId } = request.body;
const exists = await checkSubmissionExists(database, route, submissionId);
if (!exists) {
response.code(404).send({ error: `${route} submission not found.` });
return;
}
await markSubmissionReviewed(database, route, submissionId);
await response.code(200).send({ success: true });
} catch (error) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we bein' lazy.
await logger.error(`/review/${route}`, error as Error);
await response.status(500).send({ error: error instanceof Error
? error.message
: "Internal Server Error" });
}
};

View File

@ -0,0 +1,62 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { validateBody } from "../../modules/validateBody.js";
import { logger } from "../../utils/logger.js";
import { sendMail } from "../../utils/mailer.js";
import type { PrismaClient } from "@prisma/client";
import type { Appeal, ErrorResponse, SuccessResponse } from "@repo/types";
import type { FastifyReply, FastifyRequest } from "fastify";
/**
*Handles appeal form submissions.
* @param database - The Prisma database client.
* @param request - The request object.
* @param response - The Fastify reply utility.
*/
export const submitAppealHandler = async(
database: PrismaClient,
request: FastifyRequest<{ Body: Appeal }>,
response: FastifyReply<{ Reply: SuccessResponse | ErrorResponse }>,
): Promise<void> => {
try {
const isInvalid = validateBody(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We're passing a narrower type and TS hates that?
request.body as unknown as Record<string, unknown>,
"appeals",
);
if (isInvalid !== null) {
await response.status(400).send({ error: isInvalid });
return;
}
const exists = await database.appeals.findUnique({
where: {
email: request.body.email,
},
});
if (exists !== null) {
await response.status(429).send({
error:
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"You have already submitted an appeal. Please wait for it to be reviewed.",
});
return;
}
const data = { ...request.body };
// @ts-expect-error -- We're deleting a property here.
delete data.consent;
await database.appeals.create({
data,
});
await sendMail("appeal", data);
await response.send({ success: true });
} catch (error) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we bein' lazy.
await logger.error("/submit/appeals", error as Error);
await response.status(500).send({ error: error instanceof Error
? error.message
: "Internal Server Error" });
}
};

View File

@ -0,0 +1,64 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { validateBody } from "../../modules/validateBody.js";
import { logger } from "../../utils/logger.js";
import { sendMail } from "../../utils/mailer.js";
import type { PrismaClient } from "@prisma/client";
import type { Commission, ErrorResponse, SuccessResponse } from "@repo/types";
import type { FastifyReply, FastifyRequest } from "fastify";
/**
*Handles commission form submissions.
* @param database - The Prisma client.
* @param request - The request object.
* @param response - The Fastify reply utility.
*/
export const submitCommissionHandler = async(
database: PrismaClient,
request: FastifyRequest<{ Body: Commission }>,
response: FastifyReply<{ Reply: SuccessResponse | ErrorResponse }>,
): Promise<void> => {
try {
const isInvalid = validateBody(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We're passing a narrower type and TS hates that?
request.body as unknown as Record<string, unknown>,
"commissions",
);
if (isInvalid !== null) {
await response.status(400).send({ error: isInvalid });
return;
}
const exists = await database.commissions.findUnique({
where: {
email: request.body.email,
},
});
if (exists !== null) {
await response.
status(429).
send({
error:
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"You have already submitted a commission request. Please wait for it to be reviewed.",
});
return;
}
const data = { ...request.body };
// @ts-expect-error -- We're deleting a property here.
delete data.consent;
await database.commissions.create({
data,
});
await sendMail("commissions", data);
await response.send({ success: true });
} catch (error) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we bein' lazy.
await logger.error("/submit/commissions", error as Error);
await response.status(500).send({ error: error instanceof Error
? error.message
: "Internal Server Error" });
}
};

View File

@ -0,0 +1,64 @@
/**
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { validateBody } from "../../modules/validateBody.js";
import { logger } from "../../utils/logger.js";
import { sendMail } from "../../utils/mailer.js";
import type { PrismaClient } from "@prisma/client";
import type { Contact, ErrorResponse, SuccessResponse } from "@repo/types";
import type { FastifyReply, FastifyRequest } from "fastify";
/**
*Handles contact form submissions.
* @param database - The Prisma database client.
* @param request - The request object.
* @param response - The Fastify reply utility.
*/
export const submitContactHandler = async(
database: PrismaClient,
request: FastifyRequest<{ Body: Contact }>,
response: FastifyReply<{ Reply: SuccessResponse | ErrorResponse }>,
): Promise<void> => {
try {
const isInvalid = validateBody(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We're passing a narrower type and TS hates that?
request.body as unknown as Record<string, unknown>,
"contacts",
);
if (isInvalid !== null) {
await response.status(400).send({ error: isInvalid });
return;
}
const exists = await database.contacts.findUnique({
where: {
email: request.body.email,
},
});
if (exists !== null) {
await response.
status(429).
send({
error:
// eslint-disable-next-line stylistic/max-len -- This is a long string.
"You have already submitted a contact request. Please wait for it to be reviewed.",
});
return;
}
const data = { ...request.body };
// @ts-expect-error -- We're deleting a property here.
delete data.consent;
await database.contacts.create({
data,
});
await sendMail("contact", data);
await response.send({ success: true });
} catch (error) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- we bein' lazy.
await logger.error("/submit/contacts", error as Error);
await response.status(500).send({ error: error instanceof Error
? error.message
: "Internal Server Error" });
}
};

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