vbs registration
continuous-integration/drone Build is failing Details

master
Hannah 2026-07-13 23:45:25 -06:00
parent 4739e0d6e1
commit a8abe3f3d8
14 changed files with 805 additions and 15 deletions

View File

@ -22,6 +22,7 @@ import { ImageComponent } from './components/image/image.component';
import { VideoComponent } from './components/video/video.component'; import { VideoComponent } from './components/video/video.component';
import { FCCPermitPageComponent } from './components/fcc-permit-page/fcc-permit-page.component'; import { FCCPermitPageComponent } from './components/fcc-permit-page/fcc-permit-page.component';
import { BalloonComponent } from './components/balloon/balloon.component'; import { BalloonComponent } from './components/balloon/balloon.component';
import { VbsPageComponent } from './components/vbs-page/vbs-page.component';
const routes = const routes =
[ [
@ -120,6 +121,10 @@ const routes =
{ {
path: 'balloon', path: 'balloon',
component: BalloonComponent component: BalloonComponent
},
{
path: 'vbs',
component: VbsPageComponent
} }
] ]

View File

@ -18,7 +18,10 @@ import { MatButtonModule,
MatOptionModule, MatOptionModule,
MatRadioModule, MatRadioModule,
MatAutocompleteModule, MatAutocompleteModule,
MatCheckboxModule} from '@angular/material'; MatCheckboxModule,
MatDatepickerModule,
MatNativeDateModule,
MatCardModule} from '@angular/material';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import 'hammerjs'; import 'hammerjs';
@ -86,7 +89,7 @@ import { VideoComponent } from './components/video/video.component';
import { YoutubeListService } from './components/youtube-list/youtube-list-service'; import { YoutubeListService } from './components/youtube-list/youtube-list-service';
import { VideoItemComponent } from './components/video-item/video-item.component'; import { VideoItemComponent } from './components/video-item/video-item.component';
import { YoutubePopupComponent } from './components/popups/youtube-popup/youtube-popup.component'; import { YoutubePopupComponent } from './components/popups/youtube-popup/youtube-popup.component';
import { VbsPageComponent } from './components/vbs-page/vbs-page.component';
@ -143,7 +146,8 @@ import { YoutubePopupComponent } from './components/popups/youtube-popup/youtube
YoutubeListComponent, YoutubeListComponent,
VideoComponent, VideoComponent,
VideoItemComponent, VideoItemComponent,
YoutubePopupComponent YoutubePopupComponent,
VbsPageComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -162,7 +166,10 @@ import { YoutubePopupComponent } from './components/popups/youtube-popup/youtube
MatDialogModule, MatDialogModule,
MatAutocompleteModule, MatAutocompleteModule,
MatRadioModule, MatRadioModule,
MatCheckboxModule MatCheckboxModule,
MatCardModule,
MatDatepickerModule,
MatNativeDateModule
], ],
providers: [LoginService,PrintService,UserService,GoogleAnalyticsService,SermonService,TransactionService,EventService,EmailService,MissionarySupportService,YoutubeListService,WindowRefService], providers: [LoginService,PrintService,UserService,GoogleAnalyticsService,SermonService,TransactionService,EventService,EmailService,MissionarySupportService,YoutubeListService,WindowRefService],
entryComponents: [AddSermonPopupComponent, entryComponents: [AddSermonPopupComponent,

View File

@ -41,8 +41,17 @@
<div id="content-wrapper"> <div id="content-wrapper">
<div style="text-align: center; position: relative; margin-bottom: 15px; " *ngIf="showVBS"> <div style="text-align: center; position: relative; margin-bottom: 15px; " *ngIf="showVBS">
<div style="position: absolute; width: 100%; height: calc(65% + 15px); top: 35%; background-color: white;"></div> <div style="position: absolute; width: 100%; height: calc(65% + 15px); top: 35%; background-color: rgb(240,240,240);"></div>
<img src="assets/images/home-images/tiny/VBS_small.jpg" style="width:75%; max-width: 350px; position: relative; z-index: 1; box-shadow: 0 0 20px pink;" > <a routerLink="/vbs" style="display: block; width:75%; max-width: 350px; position: relative; z-index: 1; margin-left: auto; margin-right: auto;">
<img src="assets/images/home-images/tiny/VBS_small.jpg" style="width:100%; height: auto; display: block; box-shadow: 0 0 20px gray;" >
<div style="margin-top: 30px; font-size: 30px;">
Click To Register!
</div>
</a>
</div>
<div style="height: 2px; background-color: lightgray;">
&nbsp;
</div> </div>
<div class="row tint" *ngIf="showVGC" > <div class="row tint" *ngIf="showVGC" >

View File

@ -16,7 +16,7 @@ export class HomeComponent {
backgroundTop: string = "0px"; backgroundTop: string = "0px";
public get showVGC() : boolean { public get showVGC() : boolean {
return false;
let maxDate = new Date(2024,7,15,11); // August 15th 2018 -- Set the month one month behind since JavaScript dates are 0 based let maxDate = new Date(2024,7,15,11); // August 15th 2018 -- Set the month one month behind since JavaScript dates are 0 based
let now = new Date(); let now = new Date();
if (now.getFullYear() > maxDate.getFullYear()) return false; if (now.getFullYear() > maxDate.getFullYear()) return false;
@ -63,7 +63,8 @@ export class HomeComponent {
return true; return true;
} }
public get showVBS(): boolean { public get showVBS(): boolean {
let maxDate = new Date(2019,7,22); // August 22nd 2018 -- Set the month one month behind since JavaScript dates are 0 based return false;
let maxDate = new Date(2026,7,13); // August 22nd 2018 -- Set the month one month behind since JavaScript dates are 0 based
let now = new Date(); let now = new Date();
if (now.getFullYear() > maxDate.getFullYear()) return false; if (now.getFullYear() > maxDate.getFullYear()) return false;
if (now.getFullYear() == maxDate.getFullYear()) { if (now.getFullYear() == maxDate.getFullYear()) {

View File

@ -12,6 +12,10 @@
padding: 20px 10px 10px 10px; padding: 20px 10px 10px 10px;
} }
.width100{
width:100%;
}
.side-bar{ .side-bar{
width: 30%; width: 30%;
display: table-cell; display: table-cell;
@ -27,6 +31,10 @@
vertical-align: top; vertical-align: top;
} }
.display-none {
display: none;
}
@media(max-width:800px){ @media(max-width:800px){
.side-bar{ .side-bar{
display: block; display: block;

View File

@ -1,9 +1,9 @@
<div id="content-wrapper"> <div id="content-wrapper">
<div id="content"> <div id="content">
<div class="main-content align-top"> <div class="main-content align-top" [class.width100]="hideAlways">
<ng-content select="[mainContent]"></ng-content> <ng-content select="[mainContent]"></ng-content>
</div><!-- </div><!--
--><div class="side-bar align-top" [class.hide-on-mobile]="hideSideBarOnMobile" [class.fixed-side-bar]="fixedSideBar"> --><div class="side-bar align-top" [class.hide-on-mobile]="hideSideBarOnMobile" [class.display-none]="true" [class.fixed-side-bar]="fixedSideBar">
<ng-content select="[sideBar]"></ng-content> <ng-content select="[sideBar]"></ng-content>
</div> </div>
</div> </div>

View File

@ -7,9 +7,11 @@ import { Component, Input, OnInit } from '@angular/core';
}) })
export class SecondaryPageComponent implements OnInit { export class SecondaryPageComponent implements OnInit {
@Input() @Input()
public hideSideBarOnMobile: boolean; public hideSideBarOnMobile: boolean = true;
@Input() @Input()
public fixedSideBar: boolean; public fixedSideBar: boolean = true;
@Input()
public hideAlways: boolean = true;
constructor(){ constructor(){

View File

@ -0,0 +1,100 @@
.w-50 {
width: 50%;
}
.w-100 {
width: 100%;
}
.section-header{
font-size: 1.2em;
font-weight: bold;
border-bottom: 1px solid gray;
margin-bottom: 10px;
}
.center {
text-align: center;
}
.form-row-1-col {
margin-left: 10px;
margin-right: 10px;
}
.form-row-2-col {
margin-left: 10px;
margin-right: 10px;
gap: 10px;
display: flex;
}
@media(max-width:600px){
.form-row-2-col {
flex-direction: column;
}
}
.form-container {
width: 800px;
margin-left: auto;
margin-right: auto;
}
@media(max-width:800px){
.form-container {
width: auto;
margin-left: 10px;
margin-right: 10px;
}
}
.error {
color: red;
}
mat-label {
font-weight: bold;
overflow-wrap: break-word;
}
label {
display: flex;
flex-direction: column;
font-size: 1.15rem;
margin-top: 35px;
color: black;
font-weight: 400;
font-family: Arial, Helvetica, sans-serif
}
ol > li {
margin-left: 20px;
}
mat-label {
font-size: 1.15rem;
color: black;
font-weight: 400;
}
label.sub {
margin-top: 0px;
font-size: .85rem;
}
mat-radio-group {
display: flex;
flex-direction: row;
gap: 15px;
}
mat-radio-button {
display: flex;
flex-direction: row;
margin: 15px 0;
}
.hidden {
display: none;
}

View File

@ -0,0 +1,216 @@
<secondary-page-component [hideAlways]="true" >
<div mainContent>
<div class="form-container">
<h1 class="center" style="background-color: lightblue; padding:10px;">
Vacation Bible School Registration
</h1>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div *ngIf="studentStep">
<h2 class="center" style="margin:25px;">Student Information</h2>
<div class="form-row-2-col">
<mat-form-field class="w-100" [class.background-error]="hasErrors('firstName')">
<mat-label>First Name</mat-label>
<input matInput type="text" formControlName="firstName" required>
<mat-error>Please enter student first name</mat-error>
</mat-form-field>
<mat-form-field class="w-100" [class.background-error]="hasErrors('lastName')">
<mat-label>Last Name</mat-label>
<input matInput type="text" formControlName="lastName" required>
<mat-error>Please enter student last name</mat-error>
</mat-form-field>
</div>
<div class="form-row-2-col">
<mat-form-field class="w-100" [class.background-error]="hasErrors('grade')">
<mat-label>Grade</mat-label>
<input matInput type="number" formControlName="grade" required>
<mat-error>Please enter student grade</mat-error>
</mat-form-field>
<mat-form-field class="w-100" [class.background-error]="hasErrors('dob')">
<mat-label>Birthday</mat-label>
<input matInput [matDatepicker]="picker" (click)="picker.open()" formControlName="dob" required readonly>
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker touchUi #picker></mat-datepicker>
<mat-error>Please enter student birthday</mat-error>
</mat-form-field>
</div>
<div class="form-row-1-col">
<div>
<label id="studentHasAllergiesLabel">Does the student have any allergies?</label>
<mat-radio-group aria-label="Student has allergies" formControlName="hasAllergies" aria-labelledby="studentHasAllergiesLabel">
<mat-radio-button class="radio-button" value="true">Yes</mat-radio-button>
<mat-radio-button class="radio-button" value="false">No</mat-radio-button>
</mat-radio-group>
</div>
<mat-form-field *ngIf="form.get('hasAllergies').value === 'true'" class="w-100" [class.background-error]="hasErrors('allergies')">
<mat-label>Allergies</mat-label>
<textarea matInput type="text" formControlName="allergies"></textarea>
<mat-error>Please enter student allergies</mat-error>
</mat-form-field>
</div>
<div class="form-row-1-col">
<div>
<label id="studentHasDietaryRestrictionsLabel">Does the student have any dietary restrictions?</label>
<mat-radio-group aria-label="Student has allergies" formControlName="hasDietaryRestrictions" aria-labelledby="studentHasDietaryRestrictionsLabel">
<mat-radio-button class="radio-button" value="true">Yes</mat-radio-button>
<mat-radio-button class="radio-button" value="false">No</mat-radio-button>
</mat-radio-group>
</div>
<mat-form-field *ngIf="form.get('hasDietaryRestrictions').value === 'true'" class="w-100" [class.background-error]="hasErrors('dietaryRestrictions')">
<mat-label>Dietary Restrictions</mat-label>
<textarea matInput type="text" formControlName="dietaryRestrictions"></textarea>
<mat-error>Please enter student dietary restrictions</mat-error>
</mat-form-field>
</div>
<div class="form-row-1-col">
<div>
<label id="studentAttendedInPastLabel">Has the student attended VBS in the past?</label>
<mat-radio-group aria-label="Student Attended In Past" formControlName="attendedInPast" aria-labelledby="studentAttendedInPastLabel">
<mat-radio-button class="radio-button" value="true">Yes</mat-radio-button>
<mat-radio-button class="radio-button" value="false">No</mat-radio-button>
</mat-radio-group>
</div>
</div>
<div class="form-row-1-col">
<div>
<label id="studentRidesBus">Does the student currently ride the bus or van?</label>
<mat-radio-group aria-label="Student Rides The Bus or Van" formControlName="ridesBus" aria-labelledby="studentRidesBus">
<mat-radio-button class="radio-button" value="true">Yes</mat-radio-button>
<mat-radio-button class="radio-button" value="false">No</mat-radio-button>
</mat-radio-group>
</div>
</div>
<div style="display: flex; justify-content: flex-end; margin-bottom:20px;">
<button [disabled]="!studentComplete()" mat-raised-button color="primary" (click)="gotoParentStep()">Next</button>
</div>
</div>
<div *ngIf="parentStep">
<h2 class="center" style="margin:25px;">Parent/Guardian Information</h2>
<div class="form-row-2-col">
<mat-form-field class="w-100" [class.background-error]="hasErrors('parentFirstName')">
<mat-label>First Name</mat-label>
<input matInput type="text" formControlName="parentFirstName" required>
<mat-error>Please enter parent/guardian name</mat-error>
</mat-form-field>
<mat-form-field class="w-100" [class.background-error]="hasErrors('parentLastName')">
<mat-label>Last Name</mat-label>
<input matInput type="text" formControlName="parentLastName" required>
<mat-error>Please enter parent/guardian name</mat-error>
</mat-form-field>
</div>
<div class="form-row-1-col">
<mat-form-field class="w-100" [class.background-error]="hasErrors('address1')">
<mat-label>Address Line 1</mat-label>
<input matInput type="text" formControlName="address1" required>
<mat-error>Please enter parent/guardian address</mat-error>
</mat-form-field>
<mat-form-field class="w-100" [class.background-error]="hasErrors('address2')">
<mat-label>Address Line 2</mat-label>
<input matInput type="text" formControlName="address2">
</mat-form-field>
</div>
<div class="form-row-2-col">
<mat-form-field class="w-100" [class.background-error]="hasErrors('city')">
<mat-label>City</mat-label>
<input matInput type="text" formControlName="city" required>
<mat-error>Please enter parent/guardian city</mat-error>
</mat-form-field>
<mat-form-field class="w-100" [class.background-error]="hasErrors('state')">
<mat-label>State *</mat-label>
<mat-select formControlName="state">
<!-- Empty option to allow clearing the choice -->
<mat-option [value]="null">-- Select a State --</mat-option>
<!-- Dynamically loop over the array mapping code to value and name to label -->
<mat-option *ngFor="let state of stateList" [value]="state.code">
{{ state.name }}
</mat-option>
</mat-select>
<mat-error>Please enter parent/guardian state</mat-error>
</mat-form-field>
<mat-form-field class="w-100" [class.background-error]="hasErrors('zip')">
<mat-label>Zip</mat-label>
<input matInput type="text" formControlName="zip" required>
<mat-error>Please enter parent/guardian zip</mat-error>
</mat-form-field>
</div>
<div class="form-row-2-col">
<mat-form-field class="w-100" [class.background-error]="hasErrors('parentHomePhone')">
<mat-label>Home Phone</mat-label>
<input matInput type="tel" formControlName="parentHomePhone">
</mat-form-field>
<mat-form-field class="w-100" [class.background-error]="hasErrors('parentMobilePhone')">
<mat-label>Mobile Phone</mat-label>
<input matInput type="tel" formControlName="parentMobilePhone" required>
<mat-error>Please enter parent/guardian mobile phone (must be 10 digits)</mat-error>
</mat-form-field>
</div>
<div class="form-row-2-col">
<mat-form-field class="w-100" [class.background-error]="hasErrors('parentWorkPhone')">
<mat-label>Work Phone</mat-label>
<input matInput type="tel" formControlName="parentWorkPhone">
</mat-form-field>
<mat-form-field class="w-100" [class.background-error]="hasErrors('parentEmail')">
<mat-label>Email</mat-label>
<input matInput type="email" formControlName="parentEmail" required>
<mat-error>Please enter parent/guardian email</mat-error>
</mat-form-field>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
<button mat-raised-button color="primary" (click)="gotoStudentStep()">Back</button>
<button [disabled]="!parentComplete()" mat-raised-button color="primary" (click)="gotoEmergencyStep()">Next</button>
</div>
</div>
<div *ngIf="emergencyStep">
<h2 class="center" style="margin:25px;">Emergency Contact Information</h2>
<div class="form-row-1-col">
<mat-checkbox matPrefix formControlName="emergencySameAsParent">Same as Parent/Guardian</mat-checkbox>
</div>
<div class="form-row-2-col">
<mat-form-field class="w-100" [class.background-error]="hasErrors('emergencyFirstName')">
<mat-label>First Name</mat-label>
<input matInput type="text" formControlName="emergencyFirstName">
</mat-form-field>
<mat-form-field class="w-100" [class.background-error]="hasErrors('emergencyLastName')">
<mat-label>Last Name</mat-label>
<input matInput type="text" formControlName="emergencyLastName">
</mat-form-field>
<mat-form-field class="w-100" [class.background-error]="hasErrors('emergencyPhone')">
<mat-label>Phone</mat-label>
<input matInput type="tel" formControlName="emergencyPhone">
</mat-form-field>
</div>
<input type="hidden" formControlName="hp">
<div style="display: flex; justify-content: space-between; margin-bottom: 20px;">
<button [disabled]="submitting" mat-raised-button color="primary" (click)="gotoParentStep()">Back</button>
<button [disabled]="!emergencyComplete() || submitting" mat-raised-button color="primary" (click)="submit()">Submit</button>
</div>
</div>
<div *ngIf="completeStep">
<h2 class="center" style="margin:25px;">Complete!</h2>
<h4 class="center">{{form.get('firstName').value}} has been registered!</h4>
<div class="center" style="margin-top:20px;">
<button mat-raised-button color="primary" (click)="registerAnotherStudent()">Register Another Student</button>
</div>
</div>
</form>
</div>
</div>
</secondary-page-component>

View File

@ -0,0 +1,253 @@
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators, FormArray, FormControl, ValidatorFn, ValidationErrors } from '@angular/forms';
import { MissionarySupportService } from 'src/app/services/missionary-support-service';
import { MatDialog, MatDialogConfig } from '@angular/material';
import { OkPopupComponent } from '../popups/ok-popup/ok-popup.component';
import { EmailService } from 'src/app/services/email.service';
@Component({
selector: 'app-vbs-page',
templateUrl: './vbs-page.component.html',
styleUrls: ['./vbs-page.component.css']
})
export class VbsPageComponent implements OnInit {
form: FormGroup;
submitButtonText: string = 'Submit';
submitButtonDisabled: boolean = false;
errorMessages: string[] = [];
public students: Student[] = [];
public studentStep: boolean = true;
public parentStep: boolean = false;
public emergencyStep: boolean = false;
public completeStep: boolean = false;
public submitting: boolean = false;
constructor(private formBuilder: FormBuilder, private matDialog: MatDialog, private emailService: EmailService, private missionarySupportService: MissionarySupportService) {
//this.setupForm();
this.form = this.formBuilder.group({
firstName: ['', [Validators.required]],
lastName: ['', [Validators.required]],
grade: ['', [Validators.required]],
dob: [null, [Validators.required]],
hasAllergies: [null, [Validators.required]],
allergies: ['', [Validators.required]],
hasDietaryRestrictions: [null, [Validators.required]],
dietaryRestrictions: ['', [Validators.required]],
attendedInPast: [null, [Validators.required]],
ridesBus: [null, [Validators.required]],
parentFirstName: ['', [Validators.required]],
parentLastName: ['', [Validators.required]],
parentHomePhone: ['', []],
parentMobilePhone: ['', [Validators.required]],
parentWorkPhone: ['', []],
parentEmail: ['', [Validators.required]],
address1: ['', [Validators.required]],
address2: ['', [Validators.required]],
city: ['', [Validators.required]],
state: ['', [Validators.required]],
zip: ['', [Validators.required]],
emergencySameAsParent: ['', []],
emergencyFirstName: ['', []],
emergencyLastName: ['', []],
emergencyPhone: ['', []],
hp: ['.', [Validators.required]]
});
}
ngOnInit(): void {
}
hasErrors(formControlName: string): boolean {
let res = this.form.get(formControlName) && typeof this.form.get(formControlName).errors !== 'undefined';
if (res === true) {
res = this.form.get(formControlName).dirty || (this.form.get(formControlName).touched && this.form.get(formControlName).valid === false);
}
return res;
}
studentComplete(): boolean {
var firstName = this.form.get('firstName');
var lastName = this.form.get('lastName');
var grade = this.form.get('grade');
var dob = this.form.get('dob');
var hasAllergies = this.form.get('hasAllergies');
var hasDietaryRestrictions = this.form.get('hasDietaryRestrictions');
var allergies = this.form.get('allergies');
var dietaryRestrictions = this.form.get('dietaryRestrictions');
var attendedInPast = this.form.get('attendedInPast');
var ridesBus = this.form.get('ridesBus');
var hasFirst = firstName && firstName.value && firstName.value.length > 2;
var hasLast = lastName && lastName.value && lastName.value.length > 2;
var hasGrade = grade && grade.value && grade.value > 0;
var hasDob = dob && dob.value != null;
var hasAllergiesComplete = hasAllergies && hasAllergies.value != null;
var hasDietaryRestrictionsComplete = hasDietaryRestrictions && hasDietaryRestrictions.value != null;
var allergiesComplete = allergies && allergies.value && allergies.value.length > 2;
var dietaryRestrictionsComplete = dietaryRestrictions && dietaryRestrictions.value && dietaryRestrictions.value.length > 2;
var hasAttendedInPast = attendedInPast && attendedInPast.value != null;
var hasRidesBus = ridesBus && ridesBus.value != null;
var validAllergies = hasAllergiesComplete && ((hasAllergies.value === 'true' && allergiesComplete) || (hasAllergies.value === 'false'));
var validDietaryRestrictions = hasDietaryRestrictionsComplete && ((hasDietaryRestrictions.value === 'true' && dietaryRestrictionsComplete) || (hasDietaryRestrictions.value === 'false'));
return hasFirst && hasLast && hasGrade && hasDob && validAllergies && validDietaryRestrictions && hasRidesBus && hasAttendedInPast;
}
parentComplete(): boolean {
var first = this.form.get('parentFirstName');
var last = this.form.get('parentLastName');
var mobilePhone = this.form.get('parentMobilePhone');
var email = this.form.get('parentEmail');
var address1 = this.form.get('address1');
var city = this.form.get('city');
var state = this.form.get('state');
var zip = this.form.get('zip');
var hasFirst = first && first.value && first.value.length > 1;
var hasLast = last && last.value && last.value.length > 1;
var hasMobilePhone = mobilePhone && mobilePhone.value && mobilePhone.value.length > 9;
var hasEmail = email && email.value && this.isValidEmail(email.value);
var hasAddress1 = address1 && address1.value && address1.value.length > 2;
var hasCity = city && city.value && city.value.length > 2;
var hasState = state && state.value && state.value.length > 0;
var hasZip = zip && zip.value && zip.value.length > 2;
return hasFirst && hasLast && hasMobilePhone && hasEmail && hasAddress1 && hasCity && hasState && hasZip;
}
emergencyComplete(): boolean {
var same = this.form.get('emergencySameAsParent');
var first = this.form.get('emergencyFirstName');
var last = this.form.get('emergencyLastName');
var phone = this.form.get('emergencyPhone');
var isSame = same && same.value && same.value === true;
var hasFirst = first && first.value && first.value.length > 2;
var hasLast = last && last.value && last.value.length > 2;
var hasPhone = phone && phone.value && phone.value.length > 9;
if (isSame) {
return true;
}
return hasFirst && hasLast && hasPhone;
}
isValidEmail(email: string) {
// The official regex standard used by most frontend validation engines
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
}
gotoStudentStep() {
this.studentStep = true;
this.parentStep = false;
this.emergencyStep = false;
this.completeStep = false;
}
gotoParentStep() {
this.studentStep = false;
this.parentStep = true;
this.emergencyStep = false;
this.completeStep = false;
}
gotoEmergencyStep() {
this.studentStep = false;
this.parentStep = false;
this.emergencyStep = true;
this.completeStep = false;
}
gotoCompleteStep() {
this.studentStep = false;
this.parentStep = false;
this.emergencyStep = false;
this.completeStep = true;
this.submitting = false;
}
submit() {
if (this.submitting === true) {
return;
}
this.submitting = true;
this.emailService.sendVbsEmail(this.form.value)
.subscribe(
success => {this.gotoCompleteStep()},
error => {this.submitError();});
}
private submitError(){
this.submitting = false;
console.error("error");
let opts = new MatDialogConfig;
opts.data = { title:'Error','message':'Error submitting registration form. Please try again or contact us at (406) 494 - 5028.' };
let popup = this.matDialog.open(OkPopupComponent,opts);
this.submitButtonText = "Submit";
this.submitButtonDisabled = false;
}
registerAnotherStudent() {
this.completeStep = false;
this.studentStep = true;
this.form.get('firstName').reset();
this.form.get('lastName').reset();
this.form.get('grade').reset();
this.form.get('dob').reset();
this.form.get('hasAllergies').reset();
this.form.get('allergies').reset();
this.form.get('hasDietaryRestrictions').reset();
this.form.get('dietaryRestrictions').reset();
this.form.get('attendedInPast').reset();
this.form.get('ridesBus').reset();
}
public stateList = [
{ code: 'AL', name: 'Alabama' }, { code: 'AK', name: 'Alaska' }, { code: 'AZ', name: 'Arizona' },
{ code: 'AR', name: 'Arkansas' }, { code: 'CA', name: 'California' }, { code: 'CO', name: 'Colorado' },
{ code: 'CT', name: 'Connecticut' }, { code: 'DE', name: 'Delaware' }, { code: 'FL', name: 'Florida' },
{ code: 'GA', name: 'Georgia' }, { code: 'HI', name: 'Hawaii' }, { code: 'ID', name: 'Idaho' },
{ code: 'IL', name: 'Illinois' }, { code: 'IN', name: 'Indiana' }, { code: 'IA', name: 'Iowa' },
{ code: 'KS', name: 'Kansas' }, { code: 'KY', name: 'Kentucky' }, { code: 'LA', name: 'Louisiana' },
{ code: 'ME', name: 'Maine' }, { code: 'MD', name: 'Maryland' }, { code: 'MA', name: 'Massachusetts' },
{ code: 'MI', name: 'Michigan' }, { code: 'MN', name: 'Minnesota' }, { code: 'MS', name: 'Mississippi' },
{ code: 'MO', name: 'Missouri' }, { code: 'MT', name: 'Montana' }, { code: 'NE', name: 'Nebraska' },
{ code: 'NV', name: 'Nevada' }, { code: 'NH', name: 'New Hampshire' }, { code: 'NJ', name: 'New Jersey' },
{ code: 'NM', name: 'New Mexico' }, { code: 'NY', name: 'New York' }, { code: 'NC', name: 'North Carolina' },
{ code: 'ND', name: 'North Dakota' }, { code: 'OH', name: 'Ohio' }, { code: 'OK', name: 'Oklahoma' },
{ code: 'OR', name: 'Oregon' }, { code: 'PA', name: 'Pennsylvania' }, { code: 'RI', name: 'Rhode Island' },
{ code: 'SC', name: 'South Carolina' }, { code: 'SD', name: 'South Dakota' }, { code: 'TN', name: 'Tennessee' },
{ code: 'TX', name: 'Texas' }, { code: 'UT', name: 'Utah' }, { code: 'VT', name: 'Vermont' },
{ code: 'VA', name: 'Virginia' }, { code: 'WA', name: 'Washington' }, { code: 'WV', name: 'West Virginia' },
{ code: 'WI', name: 'Wisconsin' }, { code: 'WY', name: 'Wyoming' }
];
}
class Student {
public firstName: string;
public lastName: string;
public grade: number;
public birthday: string;
public allergies: string;
public dietaryRestrictions: string;
public attendedInPast: boolean;
public ridesBus: boolean;
public guardian: Guardian
public emergencyContact: EmergencyContact;
}
class Guardian {
public firstName: string;
public lastName: string;
}
class EmergencyContact {
public firstName: string;
public lastName: string;
}

View File

@ -20,4 +20,5 @@ export const USER_GET_ALL_URL = environment.baseUrl + "/api2/users/a";
export const LOGIN_URL = environment.baseUrl + '/api2/login'; export const LOGIN_URL = environment.baseUrl + '/api2/login';
export const LOGIN_VALIDATE_TOKEN = ''; export const LOGIN_VALIDATE_TOKEN = '';
export const EMAIL_URL = environment.baseUrl + "/api2/email"; export const EMAIL_URL = environment.baseUrl + "/api2/email";
export const VBS_REGISTRATION_URL = environment.baseUrl + "/api2/vbs";
export const RANDOM_VERSE_URL = "//www.kingjamesbibleonline.org/popular-bible-verses-widget.php"; export const RANDOM_VERSE_URL = "//www.kingjamesbibleonline.org/popular-bible-verses-widget.php";

View File

@ -3,7 +3,7 @@ import { catchError } from 'rxjs/operators';
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { EMAIL_URL } from '../constants/urls'; import { EMAIL_URL, VBS_REGISTRATION_URL } from '../constants/urls';
@Injectable() @Injectable()
@ -32,6 +32,11 @@ export class EmailService {
} }
sendVbsEmail(formData) {
return this.httpClient.post(VBS_REGISTRATION_URL, formData, {withCredentials:true})
.pipe(catchError(this.handleError));
}
private handleError(error: HttpErrorResponse) { private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) { if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly. // A client-side or network error occurred. Handle it accordingly.

View File

@ -19,6 +19,8 @@ router.use("/events", require("./events"));
router.use("/login", require("./login")); router.use("/login", require("./login"));
router.use("/email", require("./email")); router.use("/email", require("./email"));
router.use("/transactions", require("./transactions")); router.use("/transactions", require("./transactions"));
router.use("/vbs", require("./vbs"));
router.use('/share',require('./share')); router.use('/share',require('./share'));

View File

@ -0,0 +1,181 @@
var express = require('express');
var router = express.Router();
const nodemailer = require('nodemailer');
let transporter = nodemailer.createTransport({
host: 'smtp.ionos.com',
port: 587,
secure: false,
auth:{
user: 'mail@ofbbutte.com',
pass: '@2014OfbPwd'
}
});
router.get("/",function(req,res){
res.status(200).json({"message":"Hello World"});
return;
});
router.post("/", function(req,res){
console.log(req.body);
//This is the honeypot field
//If it has something in it then we know it was filled out by a bot
if (!req.body.hp || req.body.hp != '.'){
res.status(200).json({"status":200,"message":"Success!"});
return;
}
let mailOptions = {
from: 'donotreply@ofbbutte.com',
to: 'djabsher@gmail.com',
subject: 'OFB - VBS Registration' + req.body.firstName,
html: generateRegistrationEmailHTML(req.body)
};
transporter.sendMail(mailOptions,(error, info) =>{
if (error){
console.log(error);
res.status(400).json({"status":400,"message":"There was an error","error":error.response});
} else {
res.status(200).json({"status":200,"message":"Success"});
}
});
});
/**
* Generates a sectioned HTML email string for the registration form.
* Displays all fields even if they are empty.
* @param {Object} data - The value object from this.form.value
* @returns {string} Ready-to-send HTML string
*/
function generateRegistrationEmailHTML(data) {
// Helper to cleanly format boolean true/false fields
const formatBool = (val) => {
if (val === true || val === 'true') return 'Yes';
if (val === false || val === 'false') return 'No';
return val;
};
// Helper to ensure we don't render literal 'null' or 'undefined' text
const sanitizeValue = (val) => {
if (val === null || val === undefined) return '';
return String(val).trim();
};
// Define the layout sections and human-readable labels
const sections = [
{
title: "Student Information",
fields: [
{ label: "Student Name", value: `${sanitizeValue(data.firstName)} ${sanitizeValue(data.lastName)}`.trim() },
{ label: "Grade", value: sanitizeValue(data.grade) },
{ label: "Date of Birth", value: sanitizeValue(data.dob) },
{ label: "Has Allergies?", value: sanitizeValue(formatBool(data.hasAllergies)) },
{ label: "Allergies", value: sanitizeValue(data.allergies) },
{ label: "Has Dietary Restrictions?", value: sanitizeValue(formatBool(data.hasDietaryRestrictions)) },
{ label: "Dietary Restrictions", value: sanitizeValue(data.dietaryRestrictions) },
{ label: "Attended in Past?", value: sanitizeValue(formatBool(data.attendedInPast)) },
{ label: "Rides Bus?", value: sanitizeValue(formatBool(data.ridesBus)) }
]
},
{
title: "Parent / Guardian Contact",
fields: [
{ label: "Parent Name", value: `${sanitizeValue(data.parentFirstName)} ${sanitizeValue(data.parentLastName)}`.trim() },
{ label: "Email", value: sanitizeValue(data.parentEmail) },
{ label: "Mobile Phone", value: sanitizeValue(data.parentMobilePhone) },
{ label: "Home Phone", value: sanitizeValue(data.parentHomePhone) },
{ label: "Work Phone", value: sanitizeValue(data.parentWorkPhone) },
{ label: "Address", value: `${sanitizeValue(data.address1)}${data.address2 ? ', ' + sanitizeValue(data.address2) : ''}, ${sanitizeValue(data.city)}, ${sanitizeValue(data.state)} ${sanitizeValue(data.zip)}`.trim().replace(/^,|,$/g, '') }
]
},
{
title: "Emergency Contact",
fields: [
{ label: "Same as Parent?", value: sanitizeValue(formatBool(data.emergencySameAsParent)) },
{ label: "Emergency Name", value: `${sanitizeValue(data.emergencyFirstName)} ${sanitizeValue(data.emergencyLastName)}`.trim() },
{ label: "Emergency Phone", value: sanitizeValue(data.emergencyPhone) }
]
}
];
// Map the sections array into HTML tables (No filtering out fields!)
const htmlContent = sections.map(section => {
const rows = section.fields.map(field => `
<tr>
<td style="padding: 10px 12px; border-bottom: 1px solid #edf2f7; font-weight: 600; color: #4a5568; width: 35%; font-size: 14px;">
${field.label}
</td>
<td style="padding: 10px 12px; border-bottom: 1px solid #edf2f7; color: #2d3748; font-size: 14px;">
${field.value}
</td>
</tr>
`).join('');
return `
<h3 style="color: #4f46e5; font-size: 16px; margin-top: 24px; margin-bottom: 10px; border-bottom: 2px solid #e2e8f0; padding-bottom: 6px; font-weight: 600;">
${section.title}
</h3>
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="border-collapse: collapse; margin-bottom: 15px;">
<tbody>
${rows}
</tbody>
</table>
`;
}).join('');
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Registration Submission</title>
</head>
<body style="margin: 0; padding: 0; background-color: #f7fafc; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="background-color: #f7fafc; padding: 24px 0;">
<tr>
<td align="center">
<table border="0" cellpadding="0" cellspacing="0" width="100%" style="max-width: 650px; background-color: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.05); border: 1px solid #e2e8f0;">
<!-- Header Banner -->
<tr>
<td style="background-color: #4f46e5; padding: 24px; text-align: center;">
<h1 style="color: #ffffff; margin: 0; font-size: 20px; font-weight: 600; letter-spacing: 0.5px;">
New Registration Submitted
</h1>
</td>
</tr>
<!-- Content Body -->
<tr>
<td style="padding: 24px 32px;">
<p style="margin-top: 0; margin-bottom: 16px; font-size: 15px; color: #718096; line-height: 1.5;">
A new form submission has been received.
</p>
${htmlContent}
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #f8fafc; padding: 20px; text-align: center; font-size: 12px; color: #a0aec0; border-top: 1px solid #edf2f7;">
This email was automatically generated and sent from the registration system.
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
`;
}
module.exports = router;