feat: initial project prototype (#2)
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: nhcarrigan/forms#2
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #2.
This commit is contained in:
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
+190
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
+3
View File
@@ -0,0 +1,3 @@
<main>
<router-outlet></router-outlet>
</main>
+18
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";
}
+17
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),
],
};
+30
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" },
];
@@ -0,0 +1,7 @@
p, label {
font-size: 0.7rem;
}
strong {
color: red;
}
@@ -0,0 +1,16 @@
<p>
nhcarrigan is committed to protecting and respecting your privacy, and we will
only use your personal information to administer your account and to provide
the products and services you requested from us.
</p>
<p>
We are required to collect your consent to email you. If we don't, we could
not send you a response to your form submission. We will ONLY use your email
address to respond to the form submission, and your data will be automatically
deleted once we have responded to your submission.
</p>
<input id="consent" name="consent" type="checkbox" required [formControl]="control()" />
<label for="consent">
I agree to receive email communication from NHCarrigan
<strong>for the sole purpose</strong> of responding to this form submission.
</label>
@@ -0,0 +1,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>();
}
+8
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);
}
@@ -0,0 +1,3 @@
<div>
{{ error() }}
</div>
+16
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>();
}
@@ -0,0 +1,25 @@
<h1>Appeal a Sanction</h1>
<p>This form allows you to appeal a sanction levied against you on one of our platforms.</p>
<p>Need to find your sanction info? <a href="https://moderation.nhcarrigan.com/" target="_blank" rel="noreferrer">Check our logs</a>.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-checkbox [control]="understandBinding" label="I understand that the decision made by the appeals team is final and binding. If my appeal is denied, I agree that I cannot appeal again."></app-checkbox>
<div class="two-col">
<app-select-menu label="What sanction was levied against you?" options="reminder,warning,temporary removal,permanent removal" [control]="sanctionType"></app-select-menu>
<app-single-line label="What is your moderation case number?" type="number" [control]="caseNumber"></app-single-line>
</div>
<div class="two-col">
<app-select-menu label="What platform were you sanctioned on?" options="Forum,IRC,Matrix,Gitea,Fediverse" [control]="sanctionPlatform"></app-select-menu>
<app-single-line label="What is your username on that platform?" type="text" [control]="platformUsername"></app-single-line>
</div>
<app-multi-line label="Why were you sanctioned? Use your own words. Do NOT copy the sanction reason you were provided by our team." [control]="sanctionReason"></app-multi-line>
<app-multi-line label="Do you feel the sanction was fair? Why or why not?" [control]="sanctionFair"></app-multi-line>
<app-multi-line label="How did your behaviour violate our Code of Conduct, Community Guidelines, or other policies?" [control]="behaviourViolation"></app-multi-line>
<app-multi-line label="Why do you feel this sanction should be revoked?" [control]="appealReason"></app-multi-line>
<app-multi-line label="How will you improve your conduct to prevent future sanctions?" [control]="behaviourImprove"></app-multi-line>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>
@@ -0,0 +1,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;
});
}
}
@@ -0,0 +1,11 @@
<h1>Commission Us!</h1>
<p>Want us to do some work for you? Fill out this form!</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-multi-line label="Explain your business needs and how we can meet those needs for you. Be as descriptive as possible." [control]="request"></app-multi-line>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>
@@ -0,0 +1,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;
});
}
}
@@ -0,0 +1,12 @@
<h1>Contact Us!</h1>
<p>This form is provided as a fallback contact method in the event our self-hosted platforms are unavailable.</p>
<p>Responses to this form are very low priority.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-multi-line label="What can we help you with today?" [control]="request"></app-multi-line>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>
@@ -0,0 +1,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;
});
}
}
@@ -0,0 +1,18 @@
<h1>Event Requests</h1>
<p>This form allows you to submit a request for us to give a talk at your event or conference.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-multi-line label="What is your event about?" [control]="eventDescription"></app-multi-line>
<app-multi-line label="What would you like us to speak about at your event?" [control]="eventTopic"></app-multi-line>
<app-single-line label="Where is your event located? Provide an address if in-person, a URL if remote." type="text" [control]="eventLocation"></app-single-line>
<app-single-line label="When is your event?" type="text" [control]="eventDate"></app-single-line>
<app-single-line label="What is your budget to pay us for attending?" type="text" [control]="eventBudget"></app-single-line>
<app-checkbox [control]="travelCovered" label="We will cover travel expenses for NHCarrigan to attend."></app-checkbox>
<app-checkbox [control]="lodgingCovered" label="We will cover lodging expenses for NHCarrigan to attend."></app-checkbox>
<app-checkbox [control]="foodCovered" label="We will cover food expenses for NHCarrigan to attend."></app-checkbox>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>
@@ -0,0 +1,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;
});
}
}
@@ -0,0 +1,14 @@
<h1>Request a Meeting</h1>
<p>This form allows you to request a 1:1 meeting.</p>
<p>Sessions are billed at a pro-rated amount equivalent to $200 / hour.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-select-menu [control]="sessionLength" label="How long of a session would you like (in minutes)?" options="15,30,60"></app-select-menu>
<app-multi-line label="What do you wish to achieve during this session?" [control]="sessionGoal"></app-multi-line>
<app-checkbox [control]="paymentUnderstanding" label="I understand that I will be invoiced for the session, and that payment must be submitted prior to the scheduled time or our session will be cancelled."></app-checkbox>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>
@@ -0,0 +1,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;
});
}
}
@@ -0,0 +1,13 @@
<h1>Mentorship Application</h1>
<p>This form allows you to apply to join our mentorship programme.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-multi-line label="What do you want to focus on during your time in our programme?" [control]="mentorshipGoal"></app-multi-line>
<app-multi-line label="Where are you currently at in your learning journey?" [control]="currentFocus"></app-multi-line>
<app-checkbox [control]="paymentUnderstanding" label="I understand that the programme cost is $200 per month, due on the first of the month. I also understand that failure to pay on the due date will result in my removal from the programme."></app-checkbox>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>
@@ -0,0 +1,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;
});
}
}
@@ -0,0 +1,23 @@
<h1>Staff Application</h1>
<p>This form allows you to apply to join our staff team.</p>
<p><a href="https://docs.nhcarrigan.com/staff/apply/" target="_blank" rel="noreferrer">Read this first</a>.</p>
<app-error *ngIf="error" error="{{ error }}"></app-error>
<app-success *ngIf="success"></app-success>
<form *ngIf="!loading">
<app-userinfo [firstNameControl]="firstName" [lastNameControl]="lastName" [emailControl]="email" [companyControl]="company"></app-userinfo>
<app-checkbox [control]="understandVolunteer" label="I understand that my membership on the staff team is on a voluntary basis, and I expect no compensation for my time or effort."></app-checkbox>
<div class="two-col">
<app-select-menu [control]="platform" label="Which platform do you wish to moderate?" options="Forum,IRC,Matrix,Gitea,Fediverse"></app-select-menu>
<app-single-line label="What is your username on that platform?" type="text" [control]="platformUsername"></app-single-line>
</div>
<app-multi-line label="Why do you wish to join our team?" [control]="whyJoin"></app-multi-line>
<app-multi-line label="Describe how your current behaviour in our communities embodies our Code of Conduct, Community Guidelines, and other Policies." [control]="currentBehaviour"></app-multi-line>
<app-multi-line label="What prior moderation experience do you have, if any?" [control]="priorExperience"></app-multi-line>
<app-multi-line label="You and another member of our team are in disagreement with how to handle a situation, and are unable to reach a consensus. What do you do?" [control]="internalConflict"></app-multi-line>
<app-multi-line label="A member of the community has posted material which violates our rules and may be traumatic to other members. How do you address the situation to ensure the well-being of our community?" [control]="handlingTrauma"></app-multi-line>
<app-multi-line label="Explain a time where you were in a difficult situation, and you had to be the one to resolve it. How did you do so, and why did you take that approach?" [control]="difficultSituation"></app-multi-line>
<app-multi-line label="Explain a situation in which you had to display strong leadership. How did you do so, and why did you take that approach?" [control]="leadershipSituation"></app-multi-line>
<app-consent [control]="consent"></app-consent>
<button type="button" (click)="submit($event)">Submit</button>
</form>
<a routerLink="/">Back to home</a>
@@ -0,0 +1,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;
});
}
}
+4
View File
@@ -0,0 +1,4 @@
a[routerLink] {
display: block;
text-decoration: underline;
}
+31
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>
+17
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 {
}
@@ -0,0 +1,10 @@
div {
display: grid;
grid-template-columns: auto auto;
align-items: start;
margin: 0.5rem 0;
}
label {
font-size: 0.7rem;
line-height: 1rem;
}
@@ -0,0 +1,4 @@
<div>
<input type="checkbox" id="{{label()}}-input" [required]="required()" [formControl]="control()" />
<label for="{{label()}}-input">{{ label() }}</label>
</div>
@@ -0,0 +1,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);
}
@@ -0,0 +1,20 @@
div {
width: 100%;
}
label {
display: block;
font-size: 0.75rem;
text-align: left;
}
textarea {
width: 100%;
background: var(--foreground);
color: var(--background);
border: 1px solid white;
border-radius: 10px;
padding: 0.25rem;
font-family: "OpenDyslexic";
resize: vertical;
}
@@ -0,0 +1,4 @@
<div>
<label for="{{label()}}-input">{{ label() }}:</label>
<textarea id="{{label()}}-input" required [formControl]="control()"></textarea>
</div>
@@ -0,0 +1,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>();
}
@@ -0,0 +1,19 @@
div {
width: 100%;
}
label {
display: block;
font-size: 0.75rem;
text-align: left;
}
select {
width: 100%;
background: var(--foreground);
color: var(--background);
border: 1px solid white;
border-radius: 10px;
padding: 0.25rem;
font-family: "OpenDyslexic";
}
@@ -0,0 +1,7 @@
<div>
<label for="{{label()}}-input">{{ label() }}:</label>
<select id="{{label()}}-input" [formControl]="control()" required>
<option value="" disabled selected>Select an option</option>
<option *ngFor="let option of options().split(',')" [value]="option">{{ option }}</option>
</select>
</div>
@@ -0,0 +1,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>();
}
@@ -0,0 +1,18 @@
div {
width: 100%;
}
label {
display: block;
font-size: 0.75rem;
text-align: left;
}
input {
width: 100%;
background: var(--foreground);
color: var(--background);
border: 1px solid white;
border-radius: 10px;
padding: 0.25rem;
}
@@ -0,0 +1,4 @@
<div>
<label for="{{label()}}-input">{{ label() }}:</label>
<input [type]="type()" id="{{label()}}-input" required [formControl]="control()" />
</div>
@@ -0,0 +1,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>();
}
@@ -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>
+151
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;
});
}
});
}
}
@@ -0,0 +1,8 @@
div {
width: 100%;
padding: 0.5rem;
font-size: 1.3rem;
background: rgba(100, 255, 100, 0.5);
color: rgb(0, 100, 0);
border: 1px solid rgb(0, 100, 0);
}
@@ -0,0 +1,3 @@
<div>
<p>Your submission has been received! Please keep an eye on your email - we will reach out soon.</p>
</div>
@@ -0,0 +1,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 {
}
@@ -0,0 +1,8 @@
<div class="two-col">
<app-single-line label="First Name" type="text" [control]="firstNameControl()"></app-single-line>
<app-single-line label="Last Name" type="text" [control]="lastNameControl()"></app-single-line>
</div>
<div class="two-col">
<app-single-line label="Email" type="email" [control]="emailControl()"></app-single-line>
<app-single-line label="Company Name" type="text" [control]="companyControl()"></app-single-line>
</div>
@@ -0,0 +1,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
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
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
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;
}