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,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<string, unknown> = {};
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,631 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
|
let { data, form: formData }: { data: any; form: Record<string, any> | null } = $props();
|
||||||
|
const form = $derived(formData);
|
||||||
|
|
||||||
|
let ownRent = $state('');
|
||||||
|
let yardFenced = $state('');
|
||||||
|
let felony = $state('');
|
||||||
|
let animalNeglect = $state('');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (form) {
|
||||||
|
ownRent = form.ownRent ?? '';
|
||||||
|
yardFenced = form.yardFenced ?? '';
|
||||||
|
felony = form.felony ?? '';
|
||||||
|
animalNeglect = form.animalNeglect ?? '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Signature pad
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
let isDrawing = false;
|
||||||
|
let signatureData = $state('');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.strokeStyle = '#1a1a1a';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
});
|
||||||
|
|
||||||
|
function getCtx() {
|
||||||
|
return canvas?.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPos(e: MouseEvent | TouchEvent) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
if ('touches' in e) {
|
||||||
|
return { x: e.touches[0].clientX - rect.left, y: e.touches[0].clientY - rect.top };
|
||||||
|
}
|
||||||
|
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDraw(e: MouseEvent | TouchEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
isDrawing = true;
|
||||||
|
const ctx = getCtx();
|
||||||
|
if (!ctx) return;
|
||||||
|
const pos = getPos(e);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pos.x, pos.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(e: MouseEvent | TouchEvent) {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const ctx = getCtx();
|
||||||
|
if (!ctx) return;
|
||||||
|
const pos = getPos(e);
|
||||||
|
ctx.lineTo(pos.x, pos.y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function endDraw() {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
isDrawing = false;
|
||||||
|
signatureData = canvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSignature() {
|
||||||
|
const ctx = getCtx();
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
signatureData = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputClass = 'w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500';
|
||||||
|
const labelClass = 'block text-sm font-medium text-gray-700 mb-1';
|
||||||
|
const radioGroupClass = 'flex items-center gap-6 mt-1';
|
||||||
|
const radioClass = 'h-4 w-4 border-gray-300 text-teal-600 focus:ring-teal-500/30';
|
||||||
|
const checkboxClass = 'mt-0.5 h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500/30';
|
||||||
|
|
||||||
|
const dogTypeOptions = [
|
||||||
|
'Male', 'Female', 'Puppy', 'Young', 'Adult', 'Senior',
|
||||||
|
'Small Breed', 'Medium Breed', 'Large Breed', 'X-Large Breed'
|
||||||
|
];
|
||||||
|
|
||||||
|
const traitOptions = [
|
||||||
|
'Very Active (needing walks daily)',
|
||||||
|
'Mildly Active (yard to run in, walks throughout the week)',
|
||||||
|
'Lazy (couch-potato)',
|
||||||
|
'Laid Back',
|
||||||
|
'Kid Friendly',
|
||||||
|
'Dog Friendly',
|
||||||
|
'Cat Friendly',
|
||||||
|
'No Fence Required'
|
||||||
|
];
|
||||||
|
|
||||||
|
const agreements = [
|
||||||
|
'I understand that there is an adoption fee and that it helps cover veterinary care, spay/neuter, vaccinations, and other rescue expenses.',
|
||||||
|
'I understand that the rescue warrants that it has NO knowledge of any vicious or destructive tendencies of this pet. I agree to assume full responsibility and liability for the actions of this pet once the adoption is finalized.',
|
||||||
|
'I agree to provide proper veterinary care, including annual vaccinations, heartworm prevention, flea/tick prevention, and prompt medical attention when needed.',
|
||||||
|
'I understand that the pet must remain spayed/neutered. If the pet is not yet altered at the time of adoption, I agree to have the procedure completed as directed.',
|
||||||
|
'I agree to keep the pet as an indoor pet with outdoor access only under supervision or in a securely fenced area. I agree to keep pets leashed at all times when outside of a fenced area.',
|
||||||
|
'I agree to return the pet to the rescue if I am no longer able to care for it. I will NOT rehome, sell, give away, or surrender the pet to any other person, shelter, or rescue without prior approval.',
|
||||||
|
'I understand the rescue is not liable for any illness, disease, injury, or damage a person or pet may contract or sustain from the adopted pet.',
|
||||||
|
'Should any damages occur against my existing pet(s) from the adopted pet, I understand that I am responsible for any resulting veterinary costs.',
|
||||||
|
'I agree to provide the pet with a nutritious diet, fresh water, shelter, regular exercise, and socialization appropriate for the pet\'s needs.',
|
||||||
|
'I understand that the rescue reserves the right to conduct follow-up home visits and welfare checks after the adoption is finalized.',
|
||||||
|
'I agree to contact the rescue immediately if the pet is lost, stolen, or escapes.',
|
||||||
|
'I agree to comply with all local, state, and federal laws regarding pet ownership, including licensing and leash laws.',
|
||||||
|
'I understand that misrepresentation on this application may result in denial of the application or revocation of the adoption.',
|
||||||
|
'I understand that completing this application does not guarantee approval. The rescue reserves the right to deny any application.',
|
||||||
|
'A representative of the rescue has permission to inspect my home and property at a mutually convenient time prior to and/or after the adoption.',
|
||||||
|
'If it is determined by either party that the terms of this agreement cannot be fulfilled, I agree to return the pet immediately.'
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Adoption Application — {data.orgConfig?.orgInfo?.name ?? 'Pet Rescue'}</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Apply to adopt a rescue pet. Complete our adoption application to start the process."
|
||||||
|
/>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section class="py-12 md:py-20">
|
||||||
|
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center mb-10">
|
||||||
|
<h1 class="text-3xl md:text-4xl font-bold text-gray-900">Adoption Application</h1>
|
||||||
|
<p class="mt-3 text-gray-600 text-lg">
|
||||||
|
Thank you for considering adoption! Please fill out this application completely.
|
||||||
|
We will review it and get back to you as soon as possible.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if form?.success}
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm p-8 md:p-12 text-center">
|
||||||
|
<div class="w-16 h-16 mx-auto mb-6 bg-teal-50 rounded-full flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-8 h-8 text-teal-600">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 mb-3">Application Submitted!</h2>
|
||||||
|
<p class="text-gray-600 mb-6">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/pets"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 bg-teal-600 text-white font-semibold rounded-full hover:bg-teal-700 transition-colors"
|
||||||
|
>
|
||||||
|
Browse More Pets
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<form method="POST" use:enhance class="bg-white rounded-2xl shadow-sm divide-y divide-gray-100">
|
||||||
|
{#if form?.error}
|
||||||
|
<div class="px-6 md:px-8 pt-6">
|
||||||
|
<div class="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 text-sm">
|
||||||
|
{form.error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Pet of Interest -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Pet of Interest</h2>
|
||||||
|
{#if data.pet}
|
||||||
|
<input type="hidden" name="petId" value={data.pet.id} />
|
||||||
|
<input type="hidden" name="petOfInterest" value={data.pet.name} />
|
||||||
|
<p class="text-gray-600">
|
||||||
|
Applying for: <span class="font-semibold text-gray-900">{data.pet.name}</span>
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div>
|
||||||
|
<label for="petOfInterest" class={labelClass}>
|
||||||
|
Which pet are you interested in adopting?
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="petOfInterest"
|
||||||
|
name="petOfInterest"
|
||||||
|
value={form?.petOfInterest ?? ''}
|
||||||
|
placeholder="Enter the name of the pet you're interested in (if known)"
|
||||||
|
class={inputClass}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contact Information -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Contact Information</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="firstName" class={labelClass}>
|
||||||
|
First Name <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="firstName" name="firstName" value={form?.firstName ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="lastName" class={labelClass}>
|
||||||
|
Last Name <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="lastName" name="lastName" value={form?.lastName ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="address1" class={labelClass}>
|
||||||
|
Address 1 <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="address1" name="address1" value={form?.address1 ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="address2" class={labelClass}>Address 2</label>
|
||||||
|
<input type="text" id="address2" name="address2" value={form?.address2 ?? ''} class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="city" class={labelClass}>
|
||||||
|
City <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="city" name="city" value={form?.city ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="state" class={labelClass}>
|
||||||
|
State <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="state" name="state" value={form?.state ?? ''} required maxlength="2" placeholder="OH" class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="zip" class={labelClass}>
|
||||||
|
Zip Code <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="zip" name="zip" value={form?.zip ?? ''} required maxlength="10" class={inputClass} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="phone" class={labelClass}>
|
||||||
|
Phone <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="tel" id="phone" name="phone" value={form?.phone ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="email" class={labelClass}>
|
||||||
|
Email Address <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="email" id="email" name="email" value={form?.email ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<span class={labelClass}>Do you own your home or rent? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="ownRent" value="own" required checked={ownRent === 'own'} onchange={() => ownRent = 'own'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Own</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="ownRent" value="rent" checked={ownRent === 'rent'} onchange={() => ownRent = 'rent'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Rent</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if ownRent === 'rent'}
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="landlordInfo" class={labelClass}>
|
||||||
|
If you rent, please provide your landlord's name and phone number.
|
||||||
|
</label>
|
||||||
|
<input type="text" id="landlordInfo" name="landlordInfo" value={form?.landlordInfo ?? ''} placeholder="Landlord name and phone number" class={inputClass} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Household & Home -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Your Family & Home</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="householdMembers" class={labelClass}>
|
||||||
|
Please list the names and ages of the people who live in your home. <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="householdMembers" name="householdMembers" rows="3" required class={inputClass}>{form?.householdMembers ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="currentAnimals" class={labelClass}>
|
||||||
|
Please list the current animals you have in your home (breed, gender, age). <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="currentAnimals" name="currentAnimals" rows="3" required class={inputClass}>{form?.currentAnimals ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="otherPropertyPets" class={labelClass}>
|
||||||
|
Any other pets on your property not listed above?
|
||||||
|
</label>
|
||||||
|
<textarea id="otherPropertyPets" name="otherPropertyPets" rows="2" class={inputClass}>{form?.otherPropertyPets ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="previousPets" class={labelClass}>
|
||||||
|
Please list pets you had in the last 3 years not listed above and what happened to them. <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="previousPets" name="previousPets" rows="3" required class={inputClass}>{form?.previousPets ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Are your animals spayed/neutered and up to date on vaccines? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="petsVaccinated" value="yes" required checked={form?.petsVaccinated === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="petsVaccinated" value="no" checked={form?.petsVaccinated === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Is your yard completely fenced in? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="yardFenced" value="yes" required checked={yardFenced === 'yes'} onchange={() => yardFenced = 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="yardFenced" value="no" checked={yardFenced === 'no'} onchange={() => yardFenced = 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if yardFenced === 'yes'}
|
||||||
|
<div>
|
||||||
|
<label for="fenceHeight" class={labelClass}>What height of fence do you have?</label>
|
||||||
|
<input type="text" id="fenceHeight" name="fenceHeight" value={form?.fenceHeight ?? ''} placeholder="e.g. 4 feet, 6 feet" class={inputClass} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<label for="whereDuringDay" class={labelClass}>
|
||||||
|
Where would the animal stay during the day? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="whereDuringDay" name="whereDuringDay" rows="3" required class={inputClass}>{form?.whereDuringDay ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="whereSleepNight" class={labelClass}>
|
||||||
|
Where would the animal sleep at night? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="whereSleepNight" name="whereSleepNight" rows="2" required class={inputClass}>{form?.whereSleepNight ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="hoursAlone" class={labelClass}>
|
||||||
|
How many hours a day will the animal be home alone? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="hoursAlone" name="hoursAlone" value={form?.hoursAlone ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>How would you describe your level of activity at home? <span class="text-red-500">*</span></span>
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-6 mt-1">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="activityLevel" value="busy" required checked={form?.activityLevel === 'busy'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Busy/Noisy</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="activityLevel" value="moderate" checked={form?.activityLevel === 'moderate'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Moderate Comings/Goings</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="activityLevel" value="quiet" checked={form?.activityLevel === 'quiet'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Quiet with Occasional Guests</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Information -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Additional Information</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="whyAdopt" class={labelClass}>
|
||||||
|
Why do you want to adopt? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="whyAdopt" name="whyAdopt" rows="3" required class={inputClass}>{form?.whyAdopt ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="howHeard" class={labelClass}>
|
||||||
|
How did you hear about us and what prompted you to apply? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="howHeard" name="howHeard" rows="3" required class={inputClass}>{form?.howHeard ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>What type of pet are you interested in? <span class="text-red-500">*</span></span>
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 mt-2">
|
||||||
|
{#each dogTypeOptions as option}
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" name="dogType" value={option} checked={form?.dogType?.includes?.(option)} class={checkboxClass} />
|
||||||
|
<span class="text-sm text-gray-700">{option}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>What traits should your adopted pet have?</span>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2">
|
||||||
|
{#each traitOptions as option}
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" name="traits" value={option} checked={form?.traits?.includes?.(option)} class={checkboxClass} />
|
||||||
|
<span class="text-sm text-gray-700">{option}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Do you agree to keep pets leashed at all times when outside of a fenced area? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="agreeLeash" value="yes" required checked={form?.agreeLeash === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="agreeLeash" value="no" checked={form?.agreeLeash === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Have you integrated a new pet into your home before? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="integratedBefore" value="yes" required checked={form?.integratedBefore === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="integratedBefore" value="no" checked={form?.integratedBefore === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Are you comfortable doing a "2 week shut down" with your new pet? <span class="text-red-500">*</span></span>
|
||||||
|
<p class="text-xs text-gray-500 mb-1">A 2-week shut down is a decompression period where you limit the new pet's environment and stimulation to help them adjust.</p>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="twoWeekShutdown" value="yes" required checked={form?.twoWeekShutdown === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="twoWeekShutdown" value="no" checked={form?.twoWeekShutdown === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Have you ever been charged with or convicted of a felony? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="felony" value="yes" required checked={felony === 'yes'} onchange={() => felony = 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="felony" value="no" checked={felony === 'no'} onchange={() => felony = 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if felony === 'yes'}
|
||||||
|
<div>
|
||||||
|
<label for="felonyDetails" class={labelClass}>
|
||||||
|
Please provide information about the charge(s) and dates.
|
||||||
|
</label>
|
||||||
|
<textarea id="felonyDetails" name="felonyDetails" rows="3" class={inputClass}>{form?.felonyDetails ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Have you ever been charged with neglect, abandonment, or mistreatment of an animal? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="animalNeglect" value="yes" required checked={animalNeglect === 'yes'} onchange={() => animalNeglect = 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="animalNeglect" value="no" checked={animalNeglect === 'no'} onchange={() => animalNeglect = 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- References -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">References</h2>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">Please provide three personal references (non-relatives).</p>
|
||||||
|
<div class="space-y-6">
|
||||||
|
{#each [1, 2, 3] as num}
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-900 mb-3">Personal Reference {num} <span class="text-red-500">*</span></h3>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="reference{num}Name" class={labelClass}>Name</label>
|
||||||
|
<input type="text" id="reference{num}Name" name="reference{num}Name" value={form?.[`reference${num}Name`] ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="reference{num}Phone" class={labelClass}>Phone</label>
|
||||||
|
<input type="tel" id="reference{num}Phone" name="reference{num}Phone" value={form?.[`reference${num}Phone`] ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="reference{num}Relationship" class={labelClass}>Relationship</label>
|
||||||
|
<input type="text" id="reference{num}Relationship" name="reference{num}Relationship" value={form?.[`reference${num}Relationship`] ?? ''} class={inputClass} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-900 mb-3">Veterinarian <span class="text-red-500">*</span></h3>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="vetName" class={labelClass}>Veterinarian Name</label>
|
||||||
|
<input type="text" id="vetName" name="vetName" value={form?.vetName ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="vetClinic" class={labelClass}>Veterinarian Clinic</label>
|
||||||
|
<input type="text" id="vetClinic" name="vetClinic" value={form?.vetClinic ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="vetPhone" class={labelClass}>Vet Phone</label>
|
||||||
|
<input type="tel" id="vetPhone" name="vetPhone" value={form?.vetPhone ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-900 mb-3">Landlord</h3>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="landlordName" class={labelClass}>Landlord Name (Put N/A if Own Home)</label>
|
||||||
|
<input type="text" id="landlordName" name="landlordName" value={form?.landlordName ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="landlordPhone" class={labelClass}>Landlord Phone Number</label>
|
||||||
|
<input type="tel" id="landlordPhone" name="landlordPhone" value={form?.landlordPhone ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Adoption Agreement -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Adoption Agreement</h2>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">Please read and respond to each item below.</p>
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each agreements as text, i}
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-gray-700 mb-2">{text}</p>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="agreement{i + 1}" value="yes" required checked={form?.[`agreement${i + 1}`] === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="agreement{i + 1}" value="no" checked={form?.[`agreement${i + 1}`] === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-gray-700 mb-2">I acknowledge that my responses above indicate that I have read and understood each item in this adoption agreement.</p>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="acknowledge" value="yes" required checked={form?.acknowledge === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes, I acknowledge</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="acknowledge" value="no" checked={form?.acknowledge === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No, I do not acknowledge</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Signature -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Signature</h2>
|
||||||
|
<p class="text-sm text-gray-600 mb-3">
|
||||||
|
My signature indicates that I have read and filled out this application truthfully and completely. Please sign below using your mouse or finger.
|
||||||
|
</p>
|
||||||
|
<input type="hidden" name="signature" value={signatureData} />
|
||||||
|
<div class="border-2 border-gray-200 rounded-lg overflow-hidden bg-white">
|
||||||
|
<canvas
|
||||||
|
bind:this={canvas}
|
||||||
|
width="600"
|
||||||
|
height="150"
|
||||||
|
class="w-full cursor-crosshair touch-none"
|
||||||
|
onmousedown={startDraw}
|
||||||
|
onmousemove={draw}
|
||||||
|
onmouseup={endDraw}
|
||||||
|
onmouseleave={endDraw}
|
||||||
|
ontouchstart={startDraw}
|
||||||
|
ontouchmove={draw}
|
||||||
|
ontouchend={endDraw}
|
||||||
|
></canvas>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={clearSignature}
|
||||||
|
class="mt-2 text-sm text-teal-600 hover:text-teal-700 transition-colors"
|
||||||
|
>
|
||||||
|
Clear Signature
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full sm:w-auto px-8 py-3 bg-teal-600 text-white font-semibold rounded-full hover:bg-teal-700 transition-colors focus:outline-none focus:ring-2 focus:ring-teal-500/50 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Submit Adoption Application
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,582 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
|
let { data, form }: { data: any; form: Record<string, any> | null } = $props();
|
||||||
|
|
||||||
|
let ownRent = $state(form?.ownRent ?? '');
|
||||||
|
let yardFenced = $state(form?.yardFenced ?? '');
|
||||||
|
let felony = $state(form?.felony ?? '');
|
||||||
|
let animalNeglect = $state(form?.animalNeglect ?? '');
|
||||||
|
|
||||||
|
// Signature pad
|
||||||
|
let canvas: HTMLCanvasElement;
|
||||||
|
let isDrawing = false;
|
||||||
|
let signatureData = $state('');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (!canvas) return;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.strokeStyle = '#1a1a1a';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
});
|
||||||
|
|
||||||
|
function getCtx() {
|
||||||
|
return canvas?.getContext('2d');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPos(e: MouseEvent | TouchEvent) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
if ('touches' in e) {
|
||||||
|
return { x: e.touches[0].clientX - rect.left, y: e.touches[0].clientY - rect.top };
|
||||||
|
}
|
||||||
|
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDraw(e: MouseEvent | TouchEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
isDrawing = true;
|
||||||
|
const ctx = getCtx();
|
||||||
|
if (!ctx) return;
|
||||||
|
const pos = getPos(e);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pos.x, pos.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(e: MouseEvent | TouchEvent) {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const ctx = getCtx();
|
||||||
|
if (!ctx) return;
|
||||||
|
const pos = getPos(e);
|
||||||
|
ctx.lineTo(pos.x, pos.y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function endDraw() {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
isDrawing = false;
|
||||||
|
signatureData = canvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSignature() {
|
||||||
|
const ctx = getCtx();
|
||||||
|
if (!ctx) return;
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
signatureData = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputClass = 'w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500';
|
||||||
|
const labelClass = 'block text-sm font-medium text-gray-700 mb-1';
|
||||||
|
const radioGroupClass = 'flex items-center gap-6 mt-1';
|
||||||
|
const radioClass = 'h-4 w-4 border-gray-300 text-teal-600 focus:ring-teal-500/30';
|
||||||
|
const checkboxClass = 'mt-0.5 h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500/30';
|
||||||
|
|
||||||
|
const petTypeOptions = [
|
||||||
|
'Male', 'Female', 'Puppy', 'Young', 'Adult', 'Senior', 'Hospice',
|
||||||
|
'Kitten', 'Cat', 'Whelping Mother',
|
||||||
|
'Small Breed', 'Medium Breed', 'Large Breed', 'X-Large Breed'
|
||||||
|
];
|
||||||
|
|
||||||
|
const traitOptions = [
|
||||||
|
'Very Active (needing walks daily)',
|
||||||
|
'Mildly Active (yard to run in, walks throughout the week)',
|
||||||
|
'Lazy (couch-potato)',
|
||||||
|
'Laid Back',
|
||||||
|
'Kid Friendly',
|
||||||
|
'Dog Friendly',
|
||||||
|
'Cat Friendly',
|
||||||
|
'No Fence Required'
|
||||||
|
];
|
||||||
|
|
||||||
|
const agreements = [
|
||||||
|
'I agree to foster the pet(s) for the benefit of the pet(s) and not for any personal gain.',
|
||||||
|
'If I wish to adopt the pet(s) I am fostering, I understand I am required to pay the adoption fee.',
|
||||||
|
'I understand there will be no charge to the rescue from me for boarding/caring for the pet(s).',
|
||||||
|
'I understand that the rescue warrants that it has NO knowledge of any vicious or destructive tendencies of this pet. I agree to assume full responsibility.',
|
||||||
|
'I understand it is recommended that the pet(s) be isolated from my personal pets for a minimum of 2 weeks.',
|
||||||
|
'I understand the rescue is not liable for any illness or disease a person or pet may contract from the foster pet(s).',
|
||||||
|
'Should any damages occur against my pet, from the pet(s) which I am providing foster care to, I understand that I am responsible for any resulting veterinary costs.',
|
||||||
|
'Should any damages occur against the pet(s) which I am providing foster care to, caused by my pet, I understand that I am responsible for any resulting veterinary costs.',
|
||||||
|
'While responsible for the care of the pet(s) in a reasonable and loving manner, I am NOT liable for any veterinary bills.',
|
||||||
|
'If the pet(s) in my care dies, I will contact the rescue immediately.',
|
||||||
|
'The rescue will provide the pet(s) with the appropriate vaccinations and boosters.',
|
||||||
|
'I UNDERSTAND THE RESCUE MUST BE CONTACTED PRIOR TO ANY VETERINARY VISITS UNLESS IT IS AN ABSOLUTE EMERGENCY.',
|
||||||
|
'I understand in rare circumstances if the rescue is not able to provide veterinary care, I may need to transport the pet(s) to an approved vet.',
|
||||||
|
'The rescue will provide all necessary materials to foster this/these pet(s) (crate, food, leash, etc.).',
|
||||||
|
'All material borrowed from the rescue must be returned upon completion of fostering.',
|
||||||
|
'I agree to inform any individuals who are interested in one or more of the pet(s) that they must fill out an adoption application through the rescue.',
|
||||||
|
'I agree to immediately return all pet(s) fostered to the rescue when requested.',
|
||||||
|
'A representative of the rescue has permission to inspect my home and property at a mutually convenient time.',
|
||||||
|
'If it is determined by either party that the terms of this agreement cannot be fulfilled, I agree to return the pet(s) immediately.',
|
||||||
|
'If I have neglected to initial any item in this contract, it is still assumed that I have read and agree to abide by the stated term.'
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Foster Application — {data.orgConfig?.orgInfo?.name ?? 'Pet Rescue'}</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Apply to become a foster home. Help save lives by providing a temporary safe haven."
|
||||||
|
/>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section class="py-12 md:py-20">
|
||||||
|
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center mb-10">
|
||||||
|
<h1 class="text-3xl md:text-4xl font-bold text-gray-900">Foster Application</h1>
|
||||||
|
<p class="mt-3 text-gray-600 text-lg">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if form?.success}
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm p-8 md:p-12 text-center">
|
||||||
|
<div class="w-16 h-16 mx-auto mb-6 bg-teal-50 rounded-full flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-8 h-8 text-teal-600">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 mb-3">Application Submitted!</h2>
|
||||||
|
<p class="text-gray-600 mb-6">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/pets"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 bg-teal-600 text-white font-semibold rounded-full hover:bg-teal-700 transition-colors"
|
||||||
|
>
|
||||||
|
Meet Our Pets
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<form method="POST" use:enhance class="bg-white rounded-2xl shadow-sm divide-y divide-gray-100">
|
||||||
|
{#if form?.error}
|
||||||
|
<div class="px-6 md:px-8 pt-6">
|
||||||
|
<div class="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 text-sm">
|
||||||
|
{form.error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Contact Information -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Contact Information</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="firstName" class={labelClass}>First Name <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="firstName" name="firstName" value={form?.firstName ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="lastName" class={labelClass}>Last Name <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="lastName" name="lastName" value={form?.lastName ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="address1" class={labelClass}>Address 1 <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="address1" name="address1" value={form?.address1 ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="address2" class={labelClass}>Address 2</label>
|
||||||
|
<input type="text" id="address2" name="address2" value={form?.address2 ?? ''} class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="city" class={labelClass}>City <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="city" name="city" value={form?.city ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="state" class={labelClass}>State <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="state" name="state" value={form?.state ?? ''} required maxlength="2" placeholder="OH" class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="zip" class={labelClass}>Zip Code <span class="text-red-500">*</span></label>
|
||||||
|
<input type="text" id="zip" name="zip" value={form?.zip ?? ''} required maxlength="10" class={inputClass} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="phone" class={labelClass}>Phone <span class="text-red-500">*</span></label>
|
||||||
|
<input type="tel" id="phone" name="phone" value={form?.phone ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="email" class={labelClass}>Email Address <span class="text-red-500">*</span></label>
|
||||||
|
<input type="email" id="email" name="email" value={form?.email ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<span class={labelClass}>Do you own your home or rent? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="ownRent" value="own" required checked={ownRent === 'own'} onchange={() => ownRent = 'own'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Own</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="ownRent" value="rent" checked={ownRent === 'rent'} onchange={() => ownRent = 'rent'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Rent</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if ownRent === 'rent'}
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="landlordInfo" class={labelClass}>
|
||||||
|
If you rent, please provide your landlord's name and phone number.
|
||||||
|
</label>
|
||||||
|
<input type="text" id="landlordInfo" name="landlordInfo" value={form?.landlordInfo ?? ''} placeholder="Landlord name and phone number" class={inputClass} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Household & Home -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Your Family & Home</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="householdMembers" class={labelClass}>
|
||||||
|
Please list the names and ages of the people who live in your home. <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="householdMembers" name="householdMembers" rows="3" required class={inputClass}>{form?.householdMembers ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="currentAnimals" class={labelClass}>
|
||||||
|
Please list the current animals you have in your home (breed, gender, age). <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="currentAnimals" name="currentAnimals" rows="3" required class={inputClass}>{form?.currentAnimals ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="otherPropertyPets" class={labelClass}>Any other pets on your property not listed above?</label>
|
||||||
|
<textarea id="otherPropertyPets" name="otherPropertyPets" rows="2" class={inputClass}>{form?.otherPropertyPets ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="previousPets" class={labelClass}>
|
||||||
|
Please list pets you had in the last 3 years not listed above and what happened to them. <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="previousPets" name="previousPets" rows="3" required class={inputClass}>{form?.previousPets ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Are your animals spayed/neutered and up to date on vaccines? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="petsVaccinated" value="yes" required checked={form?.petsVaccinated === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="petsVaccinated" value="no" checked={form?.petsVaccinated === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Is your yard completely fenced in? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="yardFenced" value="yes" required checked={yardFenced === 'yes'} onchange={() => yardFenced = 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="yardFenced" value="no" checked={yardFenced === 'no'} onchange={() => yardFenced = 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if yardFenced === 'yes'}
|
||||||
|
<div>
|
||||||
|
<label for="fenceHeight" class={labelClass}>What height of fence do you have?</label>
|
||||||
|
<input type="text" id="fenceHeight" name="fenceHeight" value={form?.fenceHeight ?? ''} placeholder="e.g. 4 feet, 6 feet" class={inputClass} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<label for="whereDuringDay" class={labelClass}>
|
||||||
|
Where would the animal stay during the day? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="whereDuringDay" name="whereDuringDay" rows="3" required class={inputClass}>{form?.whereDuringDay ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="whereSleepNight" class={labelClass}>
|
||||||
|
Where would the animal sleep at night? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="whereSleepNight" name="whereSleepNight" rows="2" required class={inputClass}>{form?.whereSleepNight ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="hoursAlone" class={labelClass}>
|
||||||
|
How many hours a day will your foster animal be home alone? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="hoursAlone" name="hoursAlone" value={form?.hoursAlone ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>How would you describe your level of activity at home? <span class="text-red-500">*</span></span>
|
||||||
|
<div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-6 mt-1">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="activityLevel" value="busy" required checked={form?.activityLevel === 'busy'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Busy/Noisy</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="activityLevel" value="moderate" checked={form?.activityLevel === 'moderate'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Moderate Comings/Goings</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="activityLevel" value="quiet" checked={form?.activityLevel === 'quiet'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Quiet with Occasional Guests</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Information -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Additional Information</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="whyFoster" class={labelClass}>
|
||||||
|
Why do you want to foster? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="whyFoster" name="whyFoster" rows="3" required class={inputClass}>{form?.whyFoster ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="howHeard" class={labelClass}>
|
||||||
|
How did you hear about us and what prompted you to apply? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="howHeard" name="howHeard" rows="3" required class={inputClass}>{form?.howHeard ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>What type of pet are you interested in fostering? <span class="text-red-500">*</span></span>
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2 mt-2">
|
||||||
|
{#each petTypeOptions as option}
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" name="petType" value={option} checked={form?.petType?.includes?.(option)} class={checkboxClass} />
|
||||||
|
<span class="text-sm text-gray-700">{option}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>What traits should your foster pet have?</span>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 mt-2">
|
||||||
|
{#each traitOptions as option}
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" name="traits" value={option} checked={form?.traits?.includes?.(option)} class={checkboxClass} />
|
||||||
|
<span class="text-sm text-gray-700">{option}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Do you agree to keep pets leashed at all times when outside of a fenced area? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="agreeLeash" value="yes" required checked={form?.agreeLeash === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="agreeLeash" value="no" checked={form?.agreeLeash === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Have you integrated a new pet into your home before? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="integratedBefore" value="yes" required checked={form?.integratedBefore === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="integratedBefore" value="no" checked={form?.integratedBefore === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Are you comfortable doing a "2 week shut down" with your new foster pet? <span class="text-red-500">*</span></span>
|
||||||
|
<p class="text-xs text-gray-500 mb-1">A 2-week shut down is a decompression period where you limit the new pet's environment and stimulation to help them adjust.</p>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="twoWeekShutdown" value="yes" required checked={form?.twoWeekShutdown === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="twoWeekShutdown" value="no" checked={form?.twoWeekShutdown === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Have you ever been charged with or convicted of a felony? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="felony" value="yes" required checked={felony === 'yes'} onchange={() => felony = 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="felony" value="no" checked={felony === 'no'} onchange={() => felony = 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if felony === 'yes'}
|
||||||
|
<div>
|
||||||
|
<label for="felonyDetails" class={labelClass}>
|
||||||
|
Please provide information about the charge(s) and dates.
|
||||||
|
</label>
|
||||||
|
<textarea id="felonyDetails" name="felonyDetails" rows="3" class={inputClass}>{form?.felonyDetails ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<span class={labelClass}>Have you ever been charged with neglect, abandonment, or mistreatment of an animal? <span class="text-red-500">*</span></span>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="animalNeglect" value="yes" required checked={animalNeglect === 'yes'} onchange={() => animalNeglect = 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="animalNeglect" value="no" checked={animalNeglect === 'no'} onchange={() => animalNeglect = 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- References -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">References</h2>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">Please provide three personal references (non-relatives).</p>
|
||||||
|
<div class="space-y-6">
|
||||||
|
{#each [1, 2, 3] as num}
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-900 mb-3">Personal Reference {num} <span class="text-red-500">*</span></h3>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="reference{num}Name" class={labelClass}>Name</label>
|
||||||
|
<input type="text" id="reference{num}Name" name="reference{num}Name" value={form?.[`reference${num}Name`] ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="reference{num}Phone" class={labelClass}>Phone</label>
|
||||||
|
<input type="tel" id="reference{num}Phone" name="reference{num}Phone" value={form?.[`reference${num}Phone`] ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="reference{num}Relationship" class={labelClass}>Relationship</label>
|
||||||
|
<input type="text" id="reference{num}Relationship" name="reference{num}Relationship" value={form?.[`reference${num}Relationship`] ?? ''} class={inputClass} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-900 mb-3">Veterinarian <span class="text-red-500">*</span></h3>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="vetName" class={labelClass}>Veterinarian Name</label>
|
||||||
|
<input type="text" id="vetName" name="vetName" value={form?.vetName ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="vetClinic" class={labelClass}>Veterinarian Clinic</label>
|
||||||
|
<input type="text" id="vetClinic" name="vetClinic" value={form?.vetClinic ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="vetPhone" class={labelClass}>Vet Phone</label>
|
||||||
|
<input type="tel" id="vetPhone" name="vetPhone" value={form?.vetPhone ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-900 mb-3">Landlord</h3>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="landlordName" class={labelClass}>Landlord Name (Put N/A if Own Home)</label>
|
||||||
|
<input type="text" id="landlordName" name="landlordName" value={form?.landlordName ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="landlordPhone" class={labelClass}>Landlord Phone Number</label>
|
||||||
|
<input type="tel" id="landlordPhone" name="landlordPhone" value={form?.landlordPhone ?? ''} required class={inputClass} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Foster Agreement -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Foster Agreement</h2>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">Please read and respond to each item below.</p>
|
||||||
|
<div class="space-y-4">
|
||||||
|
{#each agreements as text, i}
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-gray-700 mb-2">{text}</p>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="agreement{i + 1}" value="yes" required checked={form?.[`agreement${i + 1}`] === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="agreement{i + 1}" value="no" checked={form?.[`agreement${i + 1}`] === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<div class="border border-gray-200 rounded-lg p-4">
|
||||||
|
<p class="text-sm text-gray-700 mb-2">I acknowledge that my responses above indicate that I have read and understood each item in this foster agreement.</p>
|
||||||
|
<div class={radioGroupClass}>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="acknowledge" value="yes" required checked={form?.acknowledge === 'yes'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">Yes, I acknowledge</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="radio" name="acknowledge" value="no" checked={form?.acknowledge === 'no'} class={radioClass} />
|
||||||
|
<span class="text-sm text-gray-700">No, I do not acknowledge</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Signature -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Signature</h2>
|
||||||
|
<p class="text-sm text-gray-600 mb-3">
|
||||||
|
My signature indicates that I have read and filled out this application truthfully and completely. Please sign below using your mouse or finger.
|
||||||
|
</p>
|
||||||
|
<input type="hidden" name="signature" value={signatureData} />
|
||||||
|
<div class="border-2 border-gray-200 rounded-lg overflow-hidden bg-white">
|
||||||
|
<canvas
|
||||||
|
bind:this={canvas}
|
||||||
|
width="600"
|
||||||
|
height="150"
|
||||||
|
class="w-full cursor-crosshair touch-none"
|
||||||
|
onmousedown={startDraw}
|
||||||
|
onmousemove={draw}
|
||||||
|
onmouseup={endDraw}
|
||||||
|
onmouseleave={endDraw}
|
||||||
|
ontouchstart={startDraw}
|
||||||
|
ontouchmove={draw}
|
||||||
|
ontouchend={endDraw}
|
||||||
|
></canvas>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={clearSignature}
|
||||||
|
class="mt-2 text-sm text-teal-600 hover:text-teal-700 transition-colors"
|
||||||
|
>
|
||||||
|
Clear Signature
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full sm:w-auto px-8 py-3 bg-teal-600 text-white font-semibold rounded-full hover:bg-teal-700 transition-colors focus:outline-none focus:ring-2 focus:ring-teal-500/50 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Submit Foster Application
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -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<string, unknown> = {};
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,323 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
|
let { data, form }: { data: any; form: Record<string, any> | null } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Surrender a Pet — {data.orgConfig?.orgInfo?.name ?? 'Pet Rescue'}</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="If you need to surrender your pet, we may be able to help. Please fill out this form so we can learn about your pet."
|
||||||
|
/>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section class="py-12 md:py-20">
|
||||||
|
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center mb-10">
|
||||||
|
<h1 class="text-3xl md:text-4xl font-bold text-gray-900">Surrender a Pet</h1>
|
||||||
|
<p class="mt-3 text-gray-600 text-lg">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if form?.success}
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm p-8 md:p-12 text-center">
|
||||||
|
<div class="w-16 h-16 mx-auto mb-6 bg-teal-50 rounded-full flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-8 h-8 text-teal-600">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 mb-3">Form Submitted</h2>
|
||||||
|
<p class="text-gray-600 mb-6">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 bg-teal-600 text-white font-semibold rounded-full hover:bg-teal-700 transition-colors"
|
||||||
|
>
|
||||||
|
Back to Home
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<form method="POST" use:enhance class="bg-white rounded-2xl shadow-sm divide-y divide-gray-100">
|
||||||
|
{#if form?.error}
|
||||||
|
<div class="px-6 md:px-8 pt-6">
|
||||||
|
<div class="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 text-sm">
|
||||||
|
{form.error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Owner Information -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Your Information</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="applicantName" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Full Name <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="applicantName" name="applicantName" value={form?.applicantName ?? ''} required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Email <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="email" id="email" name="email" value={form?.email ?? ''} required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Phone <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="tel" id="phone" name="phone" value={form?.phone ?? ''} required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="street" class="block text-sm font-medium text-gray-700 mb-1">Street Address</label>
|
||||||
|
<input type="text" id="street" name="street" value={form?.street ?? ''}
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="city" class="block text-sm font-medium text-gray-700 mb-1">City</label>
|
||||||
|
<input type="text" id="city" name="city" value={form?.city ?? ''}
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="state" class="block text-sm font-medium text-gray-700 mb-1">State</label>
|
||||||
|
<input type="text" id="state" name="state" value={form?.state ?? ''} maxlength="2" placeholder="OH"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="zip" class="block text-sm font-medium text-gray-700 mb-1">ZIP</label>
|
||||||
|
<input type="text" id="zip" name="zip" value={form?.zip ?? ''} maxlength="10"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pet Information -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Pet Information</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="petName" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Pet's Name <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="petName" name="petName" value={form?.petName ?? ''} required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="species" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Species <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select id="species" name="species" required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500">
|
||||||
|
<option value="" disabled selected={!form?.species}>Select...</option>
|
||||||
|
<option value="dog" selected={form?.species === 'dog'}>Dog</option>
|
||||||
|
<option value="cat" selected={form?.species === 'cat'}>Cat</option>
|
||||||
|
<option value="other" selected={form?.species === 'other'}>Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="petBreed" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Breed <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="petBreed" name="petBreed" value={form?.petBreed ?? ''} required
|
||||||
|
placeholder="e.g. Lab mix, Domestic Shorthair"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="petAge" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Age <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input type="text" id="petAge" name="petAge" value={form?.petAge ?? ''} required
|
||||||
|
placeholder="e.g. 3 years, 6 months"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="petSex" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Sex <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select id="petSex" name="petSex" required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500">
|
||||||
|
<option value="" disabled selected={!form?.petSex}>Select...</option>
|
||||||
|
<option value="male" selected={form?.petSex === 'male'}>Male</option>
|
||||||
|
<option value="female" selected={form?.petSex === 'female'}>Female</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="petWeight" class="block text-sm font-medium text-gray-700 mb-1">Weight (approx.)</label>
|
||||||
|
<input type="text" id="petWeight" name="petWeight" value={form?.petWeight ?? ''}
|
||||||
|
placeholder="e.g. 45 lbs"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="spayedNeutered" class="block text-sm font-medium text-gray-700 mb-1">Spayed/Neutered?</label>
|
||||||
|
<select id="spayedNeutered" name="spayedNeutered"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500">
|
||||||
|
<option value="" selected={!form?.spayedNeutered}>Select...</option>
|
||||||
|
<option value="yes" selected={form?.spayedNeutered === 'yes'}>Yes</option>
|
||||||
|
<option value="no" selected={form?.spayedNeutered === 'no'}>No</option>
|
||||||
|
<option value="unknown" selected={form?.spayedNeutered === 'unknown'}>Unknown</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="microchipped" class="block text-sm font-medium text-gray-700 mb-1">Microchipped?</label>
|
||||||
|
<select id="microchipped" name="microchipped"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500">
|
||||||
|
<option value="" selected={!form?.microchipped}>Select...</option>
|
||||||
|
<option value="yes" selected={form?.microchipped === 'yes'}>Yes</option>
|
||||||
|
<option value="no" selected={form?.microchipped === 'no'}>No</option>
|
||||||
|
<option value="unknown" selected={form?.microchipped === 'unknown'}>Unknown</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Medical History -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Medical History</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="vaccinationsCurrent" class="block text-sm font-medium text-gray-700 mb-1">Vaccinations current?</label>
|
||||||
|
<select id="vaccinationsCurrent" name="vaccinationsCurrent"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500">
|
||||||
|
<option value="" selected={!form?.vaccinationsCurrent}>Select...</option>
|
||||||
|
<option value="yes" selected={form?.vaccinationsCurrent === 'yes'}>Yes</option>
|
||||||
|
<option value="no" selected={form?.vaccinationsCurrent === 'no'}>No</option>
|
||||||
|
<option value="unknown" selected={form?.vaccinationsCurrent === 'unknown'}>Unknown</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="vetName" class="block text-sm font-medium text-gray-700 mb-1">Veterinarian Name/Clinic</label>
|
||||||
|
<input type="text" id="vetName" name="vetName" value={form?.vetName ?? ''}
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500" />
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="medicalConditions" class="block text-sm font-medium text-gray-700 mb-1">Known Medical Conditions</label>
|
||||||
|
<textarea id="medicalConditions" name="medicalConditions" rows="2"
|
||||||
|
placeholder="Allergies, injuries, chronic conditions, etc."
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
>{form?.medicalConditions ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="medications" class="block text-sm font-medium text-gray-700 mb-1">Current Medications</label>
|
||||||
|
<textarea id="medications" name="medications" rows="2"
|
||||||
|
placeholder="List any medications the pet is currently taking"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
>{form?.medications ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Behavior -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Behavior</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
{#each [
|
||||||
|
{ id: 'goodWithKids', label: 'Good with kids?' },
|
||||||
|
{ id: 'goodWithDogs', label: 'Good with dogs?' },
|
||||||
|
{ id: 'goodWithCats', label: 'Good with cats?' }
|
||||||
|
] as field}
|
||||||
|
<div>
|
||||||
|
<label for={field.id} class="block text-sm font-medium text-gray-700 mb-1">{field.label}</label>
|
||||||
|
<select id={field.id} name={field.id}
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500">
|
||||||
|
<option value="" selected={!form?.[field.id]}>Select...</option>
|
||||||
|
<option value="yes" selected={form?.[field.id] === 'yes'}>Yes</option>
|
||||||
|
<option value="no" selected={form?.[field.id] === 'no'}>No</option>
|
||||||
|
<option value="unknown" selected={form?.[field.id] === 'unknown'}>Unknown</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div>
|
||||||
|
<label for="houseTrained" class="block text-sm font-medium text-gray-700 mb-1">House trained?</label>
|
||||||
|
<select id="houseTrained" name="houseTrained"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500">
|
||||||
|
<option value="" selected={!form?.houseTrained}>Select...</option>
|
||||||
|
<option value="yes" selected={form?.houseTrained === 'yes'}>Yes</option>
|
||||||
|
<option value="no" selected={form?.houseTrained === 'no'}>No</option>
|
||||||
|
<option value="partially" selected={form?.houseTrained === 'partially'}>Partially</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="crateTrained" class="block text-sm font-medium text-gray-700 mb-1">Crate trained?</label>
|
||||||
|
<select id="crateTrained" name="crateTrained"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500">
|
||||||
|
<option value="" selected={!form?.crateTrained}>Select...</option>
|
||||||
|
<option value="yes" selected={form?.crateTrained === 'yes'}>Yes</option>
|
||||||
|
<option value="no" selected={form?.crateTrained === 'no'}>No</option>
|
||||||
|
<option value="unknown" selected={form?.crateTrained === 'unknown'}>Unknown</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="leashTrained" class="block text-sm font-medium text-gray-700 mb-1">Leash trained?</label>
|
||||||
|
<select id="leashTrained" name="leashTrained"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500">
|
||||||
|
<option value="" selected={!form?.leashTrained}>Select...</option>
|
||||||
|
<option value="yes" selected={form?.leashTrained === 'yes'}>Yes</option>
|
||||||
|
<option value="no" selected={form?.leashTrained === 'no'}>No</option>
|
||||||
|
<option value="partially" selected={form?.leashTrained === 'partially'}>Partially</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="aggressionOrAnxiety" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Any aggression, anxiety, or behavioral concerns?
|
||||||
|
</label>
|
||||||
|
<textarea id="aggressionOrAnxiety" name="aggressionOrAnxiety" rows="3"
|
||||||
|
placeholder="Please describe any behavioral issues honestly — this helps us find the right placement."
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
>{form?.aggressionOrAnxiety ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reason for Surrender -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Reason for Surrender</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="reasonForSurrender" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Why are you surrendering this pet? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea id="reasonForSurrender" name="reasonForSurrender" rows="4" required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
>{form?.reasonForSurrender ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="timeline" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
How soon do you need to rehome? <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select id="timeline" name="timeline" required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500">
|
||||||
|
<option value="" disabled selected={!form?.timeline}>Select...</option>
|
||||||
|
<option value="immediate" selected={form?.timeline === 'immediate'}>Immediate</option>
|
||||||
|
<option value="1_week" selected={form?.timeline === '1_week'}>Within 1 week</option>
|
||||||
|
<option value="2_weeks" selected={form?.timeline === '2_weeks'}>Within 2 weeks</option>
|
||||||
|
<option value="1_month" selected={form?.timeline === '1_month'}>Within 1 month</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full sm:w-auto px-8 py-3 bg-teal-600 text-white font-semibold rounded-full hover:bg-teal-700 transition-colors focus:outline-none focus:ring-2 focus:ring-teal-500/50 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Submit Surrender Request
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -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<string, unknown> = {};
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
|
let { data, form }: { data: any; form: Record<string, any> | null } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Volunteer Application — {data.orgConfig?.orgInfo?.name ?? 'Pet Rescue'}</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Join our volunteer team. Help with events, transport, photography, and more."
|
||||||
|
/>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<section class="py-12 md:py-20">
|
||||||
|
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="text-center mb-10">
|
||||||
|
<h1 class="text-3xl md:text-4xl font-bold text-gray-900">Volunteer Application</h1>
|
||||||
|
<p class="mt-3 text-gray-600 text-lg">
|
||||||
|
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!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if form?.success}
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm p-8 md:p-12 text-center">
|
||||||
|
<div class="w-16 h-16 mx-auto mb-6 bg-teal-50 rounded-full flex items-center justify-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-8 h-8 text-teal-600">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 mb-3">Thank You for Volunteering!</h2>
|
||||||
|
<p class="text-gray-600 mb-6">
|
||||||
|
We appreciate your willingness to help! Our volunteer manager will review your
|
||||||
|
application and be in touch soon with next steps.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/"
|
||||||
|
class="inline-flex items-center gap-2 px-6 py-3 bg-teal-600 text-white font-semibold rounded-full hover:bg-teal-700 transition-colors"
|
||||||
|
>
|
||||||
|
Back to Home
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<form method="POST" use:enhance class="bg-white rounded-2xl shadow-sm divide-y divide-gray-100">
|
||||||
|
{#if form?.error}
|
||||||
|
<div class="px-6 md:px-8 pt-6">
|
||||||
|
<div class="bg-red-50 border border-red-200 text-red-700 rounded-lg px-4 py-3 text-sm">
|
||||||
|
{form.error}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Personal Information -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Personal Information</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label for="applicantName" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Full Name <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="applicantName"
|
||||||
|
name="applicantName"
|
||||||
|
value={form?.applicantName ?? ''}
|
||||||
|
required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Email <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
value={form?.email ?? ''}
|
||||||
|
required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Phone <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
id="phone"
|
||||||
|
name="phone"
|
||||||
|
value={form?.phone ?? ''}
|
||||||
|
required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Availability -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Availability</h2>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="availabilityDays" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
When are you available?
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="availabilityDays"
|
||||||
|
name="availabilityDays"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
>
|
||||||
|
<option value="" selected={!form?.availabilityDays}>Select...</option>
|
||||||
|
<option value="weekdays" selected={form?.availabilityDays === 'weekdays'}>Weekdays</option>
|
||||||
|
<option value="weekends" selected={form?.availabilityDays === 'weekends'}>Weekends</option>
|
||||||
|
<option value="both" selected={form?.availabilityDays === 'both'}>Both weekdays and weekends</option>
|
||||||
|
<option value="flexible" selected={form?.availabilityDays === 'flexible'}>Flexible</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="hoursPerWeek" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Hours per week
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="hoursPerWeek"
|
||||||
|
name="hoursPerWeek"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
>
|
||||||
|
<option value="" selected={!form?.hoursPerWeek}>Select...</option>
|
||||||
|
<option value="1-3" selected={form?.hoursPerWeek === '1-3'}>1-3 hours</option>
|
||||||
|
<option value="4-8" selected={form?.hoursPerWeek === '4-8'}>4-8 hours</option>
|
||||||
|
<option value="8+" selected={form?.hoursPerWeek === '8+'}>8+ hours</option>
|
||||||
|
<option value="events_only" selected={form?.hoursPerWeek === 'events_only'}>Events only</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Interests -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Areas of Interest</h2>
|
||||||
|
<p class="text-sm text-gray-600 mb-4">Check all that apply.</p>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{#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}
|
||||||
|
<label class="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name={interest.name}
|
||||||
|
checked={form?.interests?.includes?.(interest.name)}
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-teal-600 focus:ring-teal-500/30"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-700">{interest.label}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Experience -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Experience</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="dogExperience" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Experience with animals
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="dogExperience"
|
||||||
|
name="dogExperience"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Tell us about your experience with animals"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
>{form?.dogExperience ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="rescueExperience" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Experience with rescue or nonprofit organizations
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="rescueExperience"
|
||||||
|
name="rescueExperience"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Any previous volunteer or nonprofit work"
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
>{form?.rescueExperience ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Why Interested -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<h2 class="text-lg font-bold text-gray-900 mb-4">Why Are You Interested?</h2>
|
||||||
|
<div>
|
||||||
|
<label for="whyInterested" class="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Tell us why you want to volunteer with us <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="whyInterested"
|
||||||
|
name="whyInterested"
|
||||||
|
rows="4"
|
||||||
|
required
|
||||||
|
class="w-full rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-800 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500/30 focus:border-teal-500"
|
||||||
|
>{form?.whyInterested ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit -->
|
||||||
|
<div class="px-6 md:px-8 py-6">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full sm:w-auto px-8 py-3 bg-teal-600 text-white font-semibold rounded-full hover:bg-teal-700 transition-colors focus:outline-none focus:ring-2 focus:ring-teal-500/50 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
Submit Volunteer Application
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
Reference in New Issue
Block a user