feat: add application forms for adopt, foster, volunteer, and surrender
Each form checks orgConfig.applications to verify the type is enabled, validates required fields, inserts into the applications table, and sends email notification via sendApplicationNotification. Adoption form supports optional pet pre-selection via ?pet=slug query param. Surrender form auto-creates a pet record from submitted data. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { applications } from '$lib/server/schema';
|
||||
import { sendApplicationNotification } from '$lib/server/email';
|
||||
import { loginLimiter } from '$lib/server/rate-limit';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
const parentData = await parent();
|
||||
|
||||
if (!parentData.orgConfig.applications.fosterEnabled) {
|
||||
throw redirect(302, '/');
|
||||
}
|
||||
|
||||
return parentData;
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
default: async ({ request, getClientAddress, locals }) => {
|
||||
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? getClientAddress();
|
||||
if (!loginLimiter.check(`apply-foster:${ip}`)) {
|
||||
return fail(429, { error: 'Too many submissions. Please try again later.' });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
|
||||
const firstName = formData.get('firstName')?.toString().trim() ?? '';
|
||||
const lastName = formData.get('lastName')?.toString().trim() ?? '';
|
||||
const applicantName = `${firstName} ${lastName}`.trim();
|
||||
const email = formData.get('email')?.toString().trim() ?? '';
|
||||
const phone = formData.get('phone')?.toString().trim() ?? '';
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
const fieldNames = [
|
||||
'firstName', 'lastName',
|
||||
'address1', 'address2', 'city', 'state', 'zip',
|
||||
'ownRent', 'landlordInfo',
|
||||
'householdMembers', 'currentAnimals', 'otherPropertyPets', 'previousPets',
|
||||
'petsVaccinated', 'yardFenced', 'fenceHeight',
|
||||
'whereDuringDay', 'whereSleepNight', 'hoursAlone', 'activityLevel',
|
||||
'whyFoster', 'howHeard', 'agreeLeash',
|
||||
'integratedBefore', 'twoWeekShutdown',
|
||||
'felony', 'felonyDetails', 'animalNeglect',
|
||||
'reference1Name', 'reference1Phone', 'reference1Relationship',
|
||||
'reference2Name', 'reference2Phone', 'reference2Relationship',
|
||||
'reference3Name', 'reference3Phone', 'reference3Relationship',
|
||||
'vetName', 'vetClinic', 'vetPhone',
|
||||
'landlordName', 'landlordPhone',
|
||||
'acknowledge', 'signature'
|
||||
];
|
||||
|
||||
for (const name of fieldNames) {
|
||||
data[name] = formData.get(name)?.toString() ?? '';
|
||||
}
|
||||
|
||||
data.petType = formData.getAll('petType').map((v) => v.toString());
|
||||
data.traits = formData.getAll('traits').map((v) => v.toString());
|
||||
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
data[`agreement${i}`] = formData.get(`agreement${i}`)?.toString() ?? '';
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
if (!firstName) errors.push('First name is required.');
|
||||
if (!lastName) errors.push('Last name is required.');
|
||||
if (!email) errors.push('Email is required.');
|
||||
if (!phone) errors.push('Phone number is required.');
|
||||
if (!data.address1) errors.push('Address is required.');
|
||||
if (!data.city) errors.push('City is required.');
|
||||
if (!data.state) errors.push('State is required.');
|
||||
if (!data.zip) errors.push('ZIP code is required.');
|
||||
if (!data.ownRent) errors.push('Own/rent is required.');
|
||||
if (!data.householdMembers) errors.push('Household members is required.');
|
||||
if (!data.currentAnimals) errors.push('Current animals is required.');
|
||||
if (!data.previousPets) errors.push('Previous pets is required.');
|
||||
if (!data.petsVaccinated) errors.push('Vaccination status is required.');
|
||||
if (!data.yardFenced) errors.push('Yard fencing status is required.');
|
||||
if (!data.whereDuringDay) errors.push('Where animal stays during the day is required.');
|
||||
if (!data.whereSleepNight) errors.push('Where animal sleeps at night is required.');
|
||||
if (!data.hoursAlone) errors.push('Hours alone is required.');
|
||||
if (!data.activityLevel) errors.push('Activity level is required.');
|
||||
if (!data.whyFoster) errors.push('Why you want to foster is required.');
|
||||
if (!data.howHeard) errors.push('How you heard about us is required.');
|
||||
if (!data.agreeLeash) errors.push('Leash agreement is required.');
|
||||
if (!data.integratedBefore) errors.push('Integration experience is required.');
|
||||
if (!data.twoWeekShutdown) errors.push('2-week shutdown response is required.');
|
||||
if (!data.felony) errors.push('Felony question is required.');
|
||||
if (!data.animalNeglect) errors.push('Animal neglect question is required.');
|
||||
if (!data.reference1Name || !data.reference1Phone) errors.push('Reference 1 is required.');
|
||||
if (!data.reference2Name || !data.reference2Phone) errors.push('Reference 2 is required.');
|
||||
if (!data.reference3Name || !data.reference3Phone) errors.push('Reference 3 is required.');
|
||||
if (!data.vetName || !data.vetClinic || !data.vetPhone) errors.push('Veterinarian information is required.');
|
||||
if (!data.landlordName || !data.landlordPhone) errors.push('Landlord information is required (put N/A if you own).');
|
||||
if (data.acknowledge !== 'yes') errors.push('You must acknowledge the foster agreement.');
|
||||
if (!data.signature) errors.push('Signature is required.');
|
||||
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
if (!data[`agreement${i}`]) {
|
||||
errors.push(`Agreement item ${i} is required.`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return fail(400, {
|
||||
error: errors.join(' '),
|
||||
firstName,
|
||||
lastName,
|
||||
applicantName,
|
||||
email,
|
||||
phone,
|
||||
...data
|
||||
});
|
||||
}
|
||||
|
||||
await db.insert(applications).values({
|
||||
type: 'foster',
|
||||
petId: null,
|
||||
applicantName,
|
||||
applicantEmail: email,
|
||||
applicantPhone: phone || null,
|
||||
formData: data
|
||||
});
|
||||
|
||||
const orgConfig = locals.orgConfig;
|
||||
if (orgConfig.orgInfo.email) {
|
||||
try {
|
||||
sendApplicationNotification(
|
||||
orgConfig.orgInfo.name,
|
||||
orgConfig.orgInfo.email,
|
||||
applicantName,
|
||||
'foster'
|
||||
);
|
||||
} catch {
|
||||
// Email failure should not break form submission
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user