+
diff --git a/Client/src/app/components/secondary-page/secondary-page.component.ts b/Client/src/app/components/secondary-page/secondary-page.component.ts
index 9f2d400..ac89748 100644
--- a/Client/src/app/components/secondary-page/secondary-page.component.ts
+++ b/Client/src/app/components/secondary-page/secondary-page.component.ts
@@ -7,9 +7,11 @@ import { Component, Input, OnInit } from '@angular/core';
})
export class SecondaryPageComponent implements OnInit {
@Input()
- public hideSideBarOnMobile: boolean;
+ public hideSideBarOnMobile: boolean = true;
@Input()
- public fixedSideBar: boolean;
+ public fixedSideBar: boolean = true;
+ @Input()
+ public hideAlways: boolean = true;
constructor(){
diff --git a/Client/src/app/components/vbs-page/vbs-page.component.css b/Client/src/app/components/vbs-page/vbs-page.component.css
new file mode 100644
index 0000000..de10736
--- /dev/null
+++ b/Client/src/app/components/vbs-page/vbs-page.component.css
@@ -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;
+}
\ No newline at end of file
diff --git a/Client/src/app/components/vbs-page/vbs-page.component.html b/Client/src/app/components/vbs-page/vbs-page.component.html
new file mode 100644
index 0000000..3f9fa19
--- /dev/null
+++ b/Client/src/app/components/vbs-page/vbs-page.component.html
@@ -0,0 +1,216 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/Client/src/app/components/vbs-page/vbs-page.component.ts b/Client/src/app/components/vbs-page/vbs-page.component.ts
new file mode 100644
index 0000000..4e5b90c
--- /dev/null
+++ b/Client/src/app/components/vbs-page/vbs-page.component.ts
@@ -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;
+}
\ No newline at end of file
diff --git a/Client/src/app/constants/urls.ts b/Client/src/app/constants/urls.ts
index ebb2acb..f72fbf1 100644
--- a/Client/src/app/constants/urls.ts
+++ b/Client/src/app/constants/urls.ts
@@ -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_VALIDATE_TOKEN = '';
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";
diff --git a/Client/src/app/services/email.service.ts b/Client/src/app/services/email.service.ts
index 1f0582f..6643f83 100644
--- a/Client/src/app/services/email.service.ts
+++ b/Client/src/app/services/email.service.ts
@@ -3,7 +3,7 @@ import { catchError } from 'rxjs/operators';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
-import { EMAIL_URL } from '../constants/urls';
+import { EMAIL_URL, VBS_REGISTRATION_URL } from '../constants/urls';
@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) {
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
diff --git a/Server/src/routes/api/api.js b/Server/src/routes/api/api.js
index 10a210b..20e7140 100644
--- a/Server/src/routes/api/api.js
+++ b/Server/src/routes/api/api.js
@@ -19,6 +19,8 @@ router.use("/events", require("./events"));
router.use("/login", require("./login"));
router.use("/email", require("./email"));
router.use("/transactions", require("./transactions"));
+router.use("/vbs", require("./vbs"));
+
router.use('/share',require('./share'));
diff --git a/Server/src/routes/api/vbs.js b/Server/src/routes/api/vbs.js
new file mode 100644
index 0000000..dcd195a
--- /dev/null
+++ b/Server/src/routes/api/vbs.js
@@ -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 => `
+
+ |
+ ${field.label}
+ |
+
+ ${field.value}
+ |
+
+ `).join('');
+
+ return `
+
+ ${section.title}
+
+
+ `;
+ }).join('');
+
+ return `
+
+
+
+
+
+
New Registration Submission
+
+
+
+
+
+
+
+
+
+
+
+ New Registration Submitted
+
+ |
+
+
+
+
+ |
+
+ A new form submission has been received.
+
+
+ ${htmlContent}
+
+ |
+
+
+
+
+ |
+ This email was automatically generated and sent from the registration system.
+ |
+
+
+
+ |
+
+
+
+
+ `;
+}
+
+module.exports = router;
\ No newline at end of file