diff --git a/src/routes/(public)/apply/adopt/+page.server.ts b/src/routes/(public)/apply/adopt/+page.server.ts new file mode 100644 index 0000000..d93ab88 --- /dev/null +++ b/src/routes/(public)/apply/adopt/+page.server.ts @@ -0,0 +1,173 @@ +import type { PageServerLoad, Actions } from './$types'; +import { fail, redirect } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { applications, pets } from '$lib/server/schema'; +import { eq } from 'drizzle-orm'; +import { sendApplicationNotification } from '$lib/server/email'; +import { loginLimiter } from '$lib/server/rate-limit'; + +export const load: PageServerLoad = async ({ parent, url }) => { + const parentData = await parent(); + + if (!parentData.orgConfig.applications.adoptionEnabled) { + throw redirect(302, '/'); + } + + const petSlug = url.searchParams.get('pet'); + let pet: { id: number; name: string; slug: string } | null = null; + + if (petSlug) { + const result = await db + .select({ id: pets.id, name: pets.name, slug: pets.slug }) + .from(pets) + .where(eq(pets.slug, petSlug)) + .limit(1); + + if (result.length > 0) { + pet = result[0]; + } + } + + return { ...parentData, pet }; +}; + +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-adopt:${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 petIdRaw = formData.get('petId')?.toString() ?? ''; + const petId = petIdRaw ? parseInt(petIdRaw, 10) : null; + + const data: Record = {}; + const fieldNames = [ + 'petOfInterest', 'firstName', 'lastName', + 'address1', 'address2', 'city', 'state', 'zip', + 'ownRent', 'landlordInfo', + 'householdMembers', 'currentAnimals', 'otherPropertyPets', 'previousPets', + 'petsVaccinated', 'yardFenced', 'fenceHeight', + 'whereDuringDay', 'whereSleepNight', 'hoursAlone', 'activityLevel', + 'whyAdopt', '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.dogType = formData.getAll('dogType').map((v) => v.toString()); + data.traits = formData.getAll('traits').map((v) => v.toString()); + + for (let i = 1; i <= 16; 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.whyAdopt) errors.push('Why you want to adopt 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 adoption agreement.'); + if (!data.signature) errors.push('Signature is required.'); + + for (let i = 1; i <= 16; 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: 'adoption', + petId: petId && !isNaN(petId) ? petId : null, + applicantName, + applicantEmail: email, + applicantPhone: phone || null, + formData: data + }); + + const orgConfig = locals.orgConfig; + if (orgConfig.orgInfo.email) { + let petName: string | undefined; + if (petId && !isNaN(petId)) { + try { + const [petRecord] = await db + .select({ name: pets.name }) + .from(pets) + .where(eq(pets.id, petId)) + .limit(1); + petName = petRecord?.name; + } catch { + // ignore + } + } + try { + sendApplicationNotification( + orgConfig.orgInfo.name, + orgConfig.orgInfo.email, + applicantName, + 'adoption', + petName + ); + } catch { + // Email failure should not break form submission + } + } + + return { success: true }; + } +}; diff --git a/src/routes/(public)/apply/adopt/+page.svelte b/src/routes/(public)/apply/adopt/+page.svelte new file mode 100644 index 0000000..ddf1a06 --- /dev/null +++ b/src/routes/(public)/apply/adopt/+page.svelte @@ -0,0 +1,631 @@ + + + + Adoption Application — {data.orgConfig?.orgInfo?.name ?? 'Pet Rescue'} + + + +
+
+
+

Adoption Application

+

+ Thank you for considering adoption! Please fill out this application completely. + We will review it and get back to you as soon as possible. +

+
+ + {#if form?.success} +
+
+ + + +
+

Application Submitted!

+

+ Thank you for your adoption application. Our team will review it and reach out to you + within a few business days. If you have any questions in the meantime, please don't + hesitate to contact us. +

+ + Browse More Pets + +
+ {:else} +
+ {#if form?.error} +
+
+ {form.error} +
+
+ {/if} + + +
+

Pet of Interest

+ {#if data.pet} + + +

+ Applying for: {data.pet.name} +

+ {:else} +
+ + +
+ {/if} +
+ + +
+

Contact Information

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ Do you own your home or rent? * +
+ + +
+
+ {#if ownRent === 'rent'} +
+ + +
+ {/if} +
+
+ + +
+

Your Family & Home

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Are your animals spayed/neutered and up to date on vaccines? * +
+ + +
+
+
+ Is your yard completely fenced in? * +
+ + +
+
+ {#if yardFenced === 'yes'} +
+ + +
+ {/if} +
+ + +
+
+ + +
+
+ + +
+
+ How would you describe your level of activity at home? * +
+ + + +
+
+
+
+ + +
+

Additional Information

+
+
+ + +
+
+ + +
+
+ What type of pet are you interested in? * +
+ {#each dogTypeOptions as option} + + {/each} +
+
+
+ What traits should your adopted pet have? +
+ {#each traitOptions as option} + + {/each} +
+
+
+ Do you agree to keep pets leashed at all times when outside of a fenced area? * +
+ + +
+
+
+ Have you integrated a new pet into your home before? * +
+ + +
+
+
+ Are you comfortable doing a "2 week shut down" with your new pet? * +

A 2-week shut down is a decompression period where you limit the new pet's environment and stimulation to help them adjust.

+
+ + +
+
+
+ Have you ever been charged with or convicted of a felony? * +
+ + +
+
+ {#if felony === 'yes'} +
+ + +
+ {/if} +
+ Have you ever been charged with neglect, abandonment, or mistreatment of an animal? * +
+ + +
+
+
+
+ + +
+

References

+

Please provide three personal references (non-relatives).

+
+ {#each [1, 2, 3] as num} +
+

Personal Reference {num} *

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ {/each} + +
+

Veterinarian *

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Landlord

+
+
+ + +
+
+ + +
+
+
+
+
+ + +
+

Adoption Agreement

+

Please read and respond to each item below.

+
+ {#each agreements as text, i} +
+

{text}

+
+ + +
+
+ {/each} + +
+

I acknowledge that my responses above indicate that I have read and understood each item in this adoption agreement.

+
+ + +
+
+
+
+ + +
+

Signature

+

+ My signature indicates that I have read and filled out this application truthfully and completely. Please sign below using your mouse or finger. +

+ +
+ +
+ +
+ + +
+ +
+
+ {/if} +
+
diff --git a/src/routes/(public)/apply/foster/+page.server.ts b/src/routes/(public)/apply/foster/+page.server.ts new file mode 100644 index 0000000..2bd1984 --- /dev/null +++ b/src/routes/(public)/apply/foster/+page.server.ts @@ -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 = {}; + 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 }; + } +}; diff --git a/src/routes/(public)/apply/foster/+page.svelte b/src/routes/(public)/apply/foster/+page.svelte new file mode 100644 index 0000000..5d9268e --- /dev/null +++ b/src/routes/(public)/apply/foster/+page.svelte @@ -0,0 +1,582 @@ + + + + Foster Application — {data.orgConfig?.orgInfo?.name ?? 'Pet Rescue'} + + + +
+
+
+

Foster Application

+

+ Fostering saves lives! We provide all supplies and cover vet expenses. + Please fill out this application completely and we will get back to you as soon as possible. +

+
+ + {#if form?.success} +
+
+ + + +
+

Application Submitted!

+

+ Thank you for your interest in fostering! Our foster coordinator will review your + application and reach out to you soon. In the meantime, feel free to browse our + pets who need foster homes. +

+ + Meet Our Pets + +
+ {:else} +
+ {#if form?.error} +
+
+ {form.error} +
+
+ {/if} + + +
+

Contact Information

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ Do you own your home or rent? * +
+ + +
+
+ {#if ownRent === 'rent'} +
+ + +
+ {/if} +
+
+ + +
+

Your Family & Home

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ Are your animals spayed/neutered and up to date on vaccines? * +
+ + +
+
+
+ Is your yard completely fenced in? * +
+ + +
+
+ {#if yardFenced === 'yes'} +
+ + +
+ {/if} +
+ + +
+
+ + +
+
+ + +
+
+ How would you describe your level of activity at home? * +
+ + + +
+
+
+
+ + +
+

Additional Information

+
+
+ + +
+
+ + +
+
+ What type of pet are you interested in fostering? * +
+ {#each petTypeOptions as option} + + {/each} +
+
+
+ What traits should your foster pet have? +
+ {#each traitOptions as option} + + {/each} +
+
+
+ Do you agree to keep pets leashed at all times when outside of a fenced area? * +
+ + +
+
+
+ Have you integrated a new pet into your home before? * +
+ + +
+
+
+ Are you comfortable doing a "2 week shut down" with your new foster pet? * +

A 2-week shut down is a decompression period where you limit the new pet's environment and stimulation to help them adjust.

+
+ + +
+
+
+ Have you ever been charged with or convicted of a felony? * +
+ + +
+
+ {#if felony === 'yes'} +
+ + +
+ {/if} +
+ Have you ever been charged with neglect, abandonment, or mistreatment of an animal? * +
+ + +
+
+
+
+ + +
+

References

+

Please provide three personal references (non-relatives).

+
+ {#each [1, 2, 3] as num} +
+

Personal Reference {num} *

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ {/each} + +
+

Veterinarian *

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Landlord

+
+
+ + +
+
+ + +
+
+
+
+
+ + +
+

Foster Agreement

+

Please read and respond to each item below.

+
+ {#each agreements as text, i} +
+

{text}

+
+ + +
+
+ {/each} + +
+

I acknowledge that my responses above indicate that I have read and understood each item in this foster agreement.

+
+ + +
+
+
+
+ + +
+

Signature

+

+ My signature indicates that I have read and filled out this application truthfully and completely. Please sign below using your mouse or finger. +

+ +
+ +
+ +
+ + +
+ +
+
+ {/if} +
+
diff --git a/src/routes/(public)/apply/surrender/+page.server.ts b/src/routes/(public)/apply/surrender/+page.server.ts new file mode 100644 index 0000000..fa73003 --- /dev/null +++ b/src/routes/(public)/apply/surrender/+page.server.ts @@ -0,0 +1,158 @@ +import type { PageServerLoad, Actions } from './$types'; +import { fail, redirect } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { applications, pets } from '$lib/server/schema'; +import { eq } from 'drizzle-orm'; +import { sendApplicationNotification } from '$lib/server/email'; +import { loginLimiter } from '$lib/server/rate-limit'; +import { slugify } from '$lib/slugify'; + +export const load: PageServerLoad = async ({ parent }) => { + const parentData = await parent(); + + if (!parentData.orgConfig.applications.surrenderEnabled) { + 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-surrender:${ip}`)) { + return fail(429, { error: 'Too many submissions. Please try again later.' }); + } + + const formData = await request.formData(); + + const applicantName = formData.get('applicantName')?.toString().trim() ?? ''; + const email = formData.get('email')?.toString().trim() ?? ''; + const phone = formData.get('phone')?.toString().trim() ?? ''; + + const data: Record = {}; + const fieldNames = [ + 'street', 'city', 'state', 'zip', + 'petName', 'species', 'petBreed', 'petAge', 'petSex', 'petWeight', + 'spayedNeutered', 'microchipped', + 'vaccinationsCurrent', 'medicalConditions', 'medications', 'vetName', + 'goodWithKids', 'goodWithDogs', 'goodWithCats', + 'houseTrained', 'crateTrained', 'leashTrained', + 'aggressionOrAnxiety', + 'reasonForSurrender', 'timeline' + ]; + + for (const name of fieldNames) { + data[name] = formData.get(name)?.toString() ?? ''; + } + + const errors: string[] = []; + if (!applicantName) errors.push('Your name is required.'); + if (!email) errors.push('Email is required.'); + if (!phone) errors.push('Phone number is required.'); + if (!data.petName) errors.push('Pet name is required.'); + if (!data.species) errors.push('Species is required.'); + if (!data.petBreed) errors.push('Breed is required.'); + if (!data.petAge) errors.push('Pet age is required.'); + if (!data.petSex) errors.push('Pet sex is required.'); + if (!data.reasonForSurrender) errors.push('Reason for surrender is required.'); + if (!data.timeline) errors.push('Timeline is required.'); + + if (errors.length > 0) { + return fail(400, { + error: errors.join(' '), + applicantName, + email, + phone, + ...data + }); + } + + // Insert the surrender application first + const [appResult] = await db.insert(applications).values({ + type: 'surrender', + petId: null, + applicantName, + applicantEmail: email, + applicantPhone: phone || null, + formData: data + }).$returningId(); + + // Auto-create a pet record + const petName = String(data.petName).trim(); + let slug = slugify(petName); + + const [existing] = await db + .select({ id: pets.id }) + .from(pets) + .where(eq(pets.slug, slug)) + .limit(1); + if (existing) { + slug = `${slug}-${Date.now()}`; + } + + const rawSex = String(data.petSex).toLowerCase().trim(); + const sex: 'male' | 'female' | 'unknown' = + rawSex === 'male' || rawSex === 'm' ? 'male' : + rawSex === 'female' || rawSex === 'f' ? 'female' : + 'unknown'; + + const rawSpecies = String(data.species).toLowerCase().trim(); + const species: 'dog' | 'cat' | 'other' = + rawSpecies === 'dog' ? 'dog' : + rawSpecies === 'cat' ? 'cat' : + 'other'; + + const notesParts: string[] = []; + if (data.medicalConditions) notesParts.push(`Medical conditions: ${data.medicalConditions}`); + if (data.medications) notesParts.push(`Medications: ${data.medications}`); + if (data.vaccinationsCurrent) notesParts.push(`Vaccinations current: ${data.vaccinationsCurrent}`); + if (data.spayedNeutered) notesParts.push(`Spayed/Neutered: ${data.spayedNeutered}`); + if (data.aggressionOrAnxiety) notesParts.push(`Aggression/Anxiety: ${data.aggressionOrAnxiety}`); + if (data.goodWithKids) notesParts.push(`Good with kids: ${data.goodWithKids}`); + if (data.goodWithDogs) notesParts.push(`Good with dogs: ${data.goodWithDogs}`); + if (data.goodWithCats) notesParts.push(`Good with cats: ${data.goodWithCats}`); + if (data.houseTrained) notesParts.push(`House trained: ${data.houseTrained}`); + if (data.crateTrained) notesParts.push(`Crate trained: ${data.crateTrained}`); + if (data.leashTrained) notesParts.push(`Leash trained: ${data.leashTrained}`); + if (data.reasonForSurrender) notesParts.push(`Reason for surrender: ${data.reasonForSurrender}`); + if (data.vetName) notesParts.push(`Previous vet: ${data.vetName}`); + const medicalNotes = notesParts.length > 0 ? notesParts.join('\n') : null; + + const [petResult] = await db.insert(pets).values({ + name: petName, + species, + breed: String(data.petBreed || '') || null, + age: String(data.petAge || '') || null, + sex, + weight: String(data.petWeight || '') || null, + status: 'pending', + slug, + intakeDate: new Date(), + adopterName: applicantName, + medicalNotes + }).$returningId(); + + // Link the pet to the application + await db + .update(applications) + .set({ petId: petResult.id }) + .where(eq(applications.id, appResult.id)); + + const orgConfig = locals.orgConfig; + if (orgConfig.orgInfo.email) { + try { + sendApplicationNotification( + orgConfig.orgInfo.name, + orgConfig.orgInfo.email, + applicantName, + 'surrender' + ); + } catch { + // Email failure should not break form submission + } + } + + return { success: true }; + } +}; diff --git a/src/routes/(public)/apply/surrender/+page.svelte b/src/routes/(public)/apply/surrender/+page.svelte new file mode 100644 index 0000000..bcedc88 --- /dev/null +++ b/src/routes/(public)/apply/surrender/+page.svelte @@ -0,0 +1,323 @@ + + + + Surrender a Pet — {data.orgConfig?.orgInfo?.name ?? 'Pet Rescue'} + + + +
+
+
+

Surrender a Pet

+

+ We understand that sometimes circumstances change and you may no longer be able to + care for your pet. Please complete this form so we can learn about your pet and + determine if we can help. +

+
+ + {#if form?.success} +
+
+ + + +
+

Form Submitted

+

+ Thank you for reaching out. We know this is a difficult decision. Our team will + review your submission and contact you as soon as possible. Please note that due + to limited capacity, we may not be able to accept every surrender request. +

+ + Back to Home + +
+ {:else} +
+ {#if form?.error} +
+
+ {form.error} +
+
+ {/if} + + +
+

Your Information

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+

Pet Information

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Medical History

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Behavior

+
+ {#each [ + { id: 'goodWithKids', label: 'Good with kids?' }, + { id: 'goodWithDogs', label: 'Good with dogs?' }, + { id: 'goodWithCats', label: 'Good with cats?' } + ] as field} +
+ + +
+ {/each} +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+

Reason for Surrender

+
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ {/if} +
+
diff --git a/src/routes/(public)/apply/volunteer/+page.server.ts b/src/routes/(public)/apply/volunteer/+page.server.ts new file mode 100644 index 0000000..5374fa7 --- /dev/null +++ b/src/routes/(public)/apply/volunteer/+page.server.ts @@ -0,0 +1,96 @@ +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.volunteerEnabled) { + 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-volunteer:${ip}`)) { + return fail(429, { error: 'Too many submissions. Please try again later.' }); + } + + const formData = await request.formData(); + + const applicantName = formData.get('applicantName')?.toString().trim() ?? ''; + const email = formData.get('email')?.toString().trim() ?? ''; + const phone = formData.get('phone')?.toString().trim() ?? ''; + + const data: Record = {}; + + const fieldNames = [ + 'availabilityDays', 'hoursPerWeek', + 'dogExperience', 'rescueExperience', 'whyInterested' + ]; + + for (const name of fieldNames) { + data[name] = formData.get(name)?.toString() ?? ''; + } + + const interestOptions = [ + 'eventHelp', 'transport', 'photography', 'socialMedia', + 'adminDataEntry', 'fundraising', 'fosterMentoring' + ]; + const interests: string[] = []; + for (const opt of interestOptions) { + if (formData.get(opt)?.toString() === 'on') { + interests.push(opt); + } + } + data.interests = interests; + + const errors: string[] = []; + if (!applicantName) errors.push('Full name is required.'); + if (!email) errors.push('Email is required.'); + if (!phone) errors.push('Phone number is required.'); + if (!data.whyInterested) errors.push('Please tell us why you are interested in volunteering.'); + + if (errors.length > 0) { + return fail(400, { + error: errors.join(' '), + applicantName, + email, + phone, + ...data, + interests + }); + } + + await db.insert(applications).values({ + type: 'volunteer', + 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, + 'volunteer' + ); + } catch { + // Email failure should not break form submission + } + } + + return { success: true }; + } +}; diff --git a/src/routes/(public)/apply/volunteer/+page.svelte b/src/routes/(public)/apply/volunteer/+page.svelte new file mode 100644 index 0000000..04ef7a5 --- /dev/null +++ b/src/routes/(public)/apply/volunteer/+page.svelte @@ -0,0 +1,226 @@ + + + + Volunteer Application — {data.orgConfig?.orgInfo?.name ?? 'Pet Rescue'} + + + +
+
+
+

Volunteer Application

+

+ Our volunteers are the backbone of our rescue. Whether you have a few hours a week + or can help at events, we would love to have you on our team! +

+
+ + {#if form?.success} +
+
+ + + +
+

Thank You for Volunteering!

+

+ We appreciate your willingness to help! Our volunteer manager will review your + application and be in touch soon with next steps. +

+ + Back to Home + +
+ {:else} +
+ {#if form?.error} +
+
+ {form.error} +
+
+ {/if} + + +
+

Personal Information

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+

Availability

+
+
+ + +
+
+ + +
+
+
+ + +
+

Areas of Interest

+

Check all that apply.

+
+ {#each [ + { name: 'eventHelp', label: 'Event help' }, + { name: 'transport', label: 'Transport (vet visits, meetups)' }, + { name: 'photography', label: 'Photography' }, + { name: 'socialMedia', label: 'Social media' }, + { name: 'adminDataEntry', label: 'Admin / data entry' }, + { name: 'fundraising', label: 'Fundraising' }, + { name: 'fosterMentoring', label: 'Foster mentoring' } + ] as interest} + + {/each} +
+
+ + +
+

Experience

+
+
+ + +
+
+ + +
+
+
+ + +
+

Why Are You Interested?

+
+ + +
+
+ + +
+ +
+
+ {/if} +
+