Agent skill
sikhaid-forms
Use when creating or modifying forms in SikhAid project. Covers form patterns, validation logic, submission workflows, Firestore integration, error handling, and success feedback.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/sikhaid-forms
SKILL.md
SikhAid Form Patterns
Form Types in Project
1. Contact Form
Location: src/routes/contact/+page.svelte
Purpose: General inquiries and contact requests
Firestore Collection: contact_submissions
2. Volunteer Form
Location: src/routes/volunteering/+page.svelte
Purpose: Volunteer registration and applications
Firestore Collection: volunteer_submissions
3. CSR Partnership Form
Location: src/routes/csr/+page.svelte
Purpose: Corporate Social Responsibility partnership inquiries
Firestore Collection: csr_submissions
4. Donation/Payment Form
Location: src/lib/components/PaymentForm.svelte
Purpose: Donation processing with Razorpay
Integration: Razorpay payment gateway
Standard Form Pattern
Complete Form Structure
<script lang="ts">
import { addContactSubmission } from '$lib/stores/contact';
import { addContactToFirestore } from '$lib/firestore';
// 1. Form data state
let formData = {
name: '',
email: '',
phone: '',
subject: '',
message: ''
};
// 2. UI state
let isSubmitting = false;
let successMessage = '';
let errorMessage = '';
// 3. Field errors
let errors = {
name: '',
email: '',
phone: ''
};
// 4. Validation functions
function validateName(): boolean {
if (formData.name.trim().length < 2) {
errors.name = 'Name must be at least 2 characters';
return false;
}
errors.name = '';
return true;
}
function validateEmail(): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
errors.email = 'Please enter a valid email';
return false;
}
errors.email = '';
return true;
}
function validatePhone(): boolean {
const phoneRegex = /^[6-9]\d{9}$/;
if (!phoneRegex.test(formData.phone)) {
errors.phone = 'Please enter a valid 10-digit mobile number';
return false;
}
errors.phone = '';
return true;
}
function validateForm(): boolean {
const nameValid = validateName();
const emailValid = validateEmail();
const phoneValid = validatePhone();
return nameValid && emailValid && phoneValid;
}
// 5. Submit handler
async function handleSubmit() {
// Clear previous messages
successMessage = '';
errorMessage = '';
// Validate
if (!validateForm()) {
errorMessage = 'Please fix the errors above';
return;
}
// Start submission
isSubmitting = true;
try {
// Create submission object
const submission = {
...formData,
timestamp: Date.now()
};
// Update store
addContactSubmission(submission);
// Save to Firestore
const docId = await addContactToFirestore(submission);
console.log('✅ Submission saved:', docId);
// Success feedback
successMessage = 'Thank you! We will get back to you soon.';
// Reset form
formData = {
name: '',
email: '',
phone: '',
subject: '',
message: ''
};
} catch (error) {
console.error('❌ Submission error:', error);
errorMessage = 'Submission failed. Please try again.';
} finally {
isSubmitting = false;
}
}
</script>
<!-- 6. Form markup -->
<form on:submit|preventDefault={handleSubmit} class="max-w-lg mx-auto">
<!-- Name field -->
<div class="mb-4">
<label for="name" class="block text-gray-700 font-semibold mb-2">
Full Name *
</label>
<input
id="name"
type="text"
bind:value={formData.name}
on:blur={validateName}
class="w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-custom"
class:border-red-500={errors.name}
required
/>
{#if errors.name}
<p class="text-red-500 text-sm mt-1">{errors.name}</p>
{/if}
</div>
<!-- Email field -->
<div class="mb-4">
<label for="email" class="block text-gray-700 font-semibold mb-2">
Email *
</label>
<input
id="email"
type="email"
bind:value={formData.email}
on:blur={validateEmail}
class="w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-custom"
class:border-red-500={errors.email}
required
/>
{#if errors.email}
<p class="text-red-500 text-sm mt-1">{errors.email}</p>
{/if}
</div>
<!-- Phone field -->
<div class="mb-4">
<label for="phone" class="block text-gray-700 font-semibold mb-2">
Phone *
</label>
<input
id="phone"
type="tel"
bind:value={formData.phone}
on:blur={validatePhone}
maxlength="10"
class="w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-custom"
class:border-red-500={errors.phone}
required
/>
{#if errors.phone}
<p class="text-red-500 text-sm mt-1">{errors.phone}</p>
{/if}
</div>
<!-- Subject field -->
<div class="mb-4">
<label for="subject" class="block text-gray-700 font-semibold mb-2">
Subject *
</label>
<input
id="subject"
type="text"
bind:value={formData.subject}
class="w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-custom"
required
/>
</div>
<!-- Message field -->
<div class="mb-6">
<label for="message" class="block text-gray-700 font-semibold mb-2">
Message *
</label>
<textarea
id="message"
bind:value={formData.message}
rows="5"
class="w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-custom resize-none"
required
></textarea>
</div>
<!-- Success message -->
{#if successMessage}
<div class="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg">
{successMessage}
</div>
{/if}
<!-- Error message -->
{#if errorMessage}
<div class="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg">
{errorMessage}
</div>
{/if}
<!-- Submit button -->
<button
type="submit"
disabled={isSubmitting}
class="w-full bg-orange-custom hover:bg-orange-dark text-white font-semibold py-3 px-6 rounded-lg transition-colors duration-300"
class:opacity-50={isSubmitting}
class:cursor-not-allowed={isSubmitting}
>
{#if isSubmitting}
Submitting...
{:else}
Submit
{/if}
</button>
</form>
Validation Patterns
Email Validation
function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return false;
}
return true;
}
// With error message
function validateEmailWithError(email: string): { valid: boolean; error: string } {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email.trim()) {
return { valid: false, error: 'Email is required' };
}
if (!emailRegex.test(email)) {
return { valid: false, error: 'Please enter a valid email address' };
}
return { valid: true, error: '' };
}
Phone Validation (Indian Mobile)
function validatePhone(phone: string): boolean {
// Indian mobile: 10 digits, starts with 6-9
const phoneRegex = /^[6-9]\d{9}$/;
return phoneRegex.test(phone);
}
// With detailed errors
function validatePhoneWithError(phone: string): { valid: boolean; error: string } {
if (!phone.trim()) {
return { valid: false, error: 'Phone number is required' };
}
if (!/^\d+$/.test(phone)) {
return { valid: false, error: 'Phone must contain only digits' };
}
if (phone.length !== 10) {
return { valid: false, error: 'Phone must be exactly 10 digits' };
}
if (!/^[6-9]/.test(phone)) {
return { valid: false, error: 'Phone must start with 6, 7, 8, or 9' };
}
return { valid: true, error: '' };
}
Name Validation
function validateName(name: string): boolean {
return name.trim().length >= 2;
}
// With error message
function validateNameWithError(name: string): { valid: boolean; error: string } {
const trimmed = name.trim();
if (!trimmed) {
return { valid: false, error: 'Name is required' };
}
if (trimmed.length < 2) {
return { valid: false, error: 'Name must be at least 2 characters' };
}
if (trimmed.length > 100) {
return { valid: false, error: 'Name is too long (max 100 characters)' };
}
return { valid: true, error: '' };
}
Required Field Validation
function validateRequired(value: string, fieldName: string): { valid: boolean; error: string } {
if (!value.trim()) {
return { valid: false, error: `${fieldName} is required` };
}
return { valid: true, error: '' };
}
Dropdown/Select Validation
function validateSelect(value: string, fieldName: string): { valid: boolean; error: string } {
if (!value || value === '') {
return { valid: false, error: `Please select a ${fieldName}` };
}
return { valid: true, error: '' };
}
Multi-Select Validation
function validateMultiSelect(values: string[], fieldName: string): { valid: boolean; error: string } {
if (values.length === 0) {
return { valid: false, error: `Please select at least one ${fieldName}` };
}
return { valid: true, error: '' };
}
Form Input Components
Text Input with Validation
<div class="mb-4">
<label for={fieldId} class="block text-gray-700 font-semibold mb-2">
{label} {#if required}*{/if}
</label>
<input
id={fieldId}
type="text"
bind:value={value}
on:blur={validateField}
class="w-full px-4 py-3 border border-gray-300 rounded-lg
focus:outline-none focus:ring-2 focus:ring-orange-custom focus:border-transparent
transition-all"
class:border-red-500={error}
class:focus:ring-red-500={error}
placeholder={placeholder}
required={required}
/>
{#if error}
<p class="text-red-500 text-sm mt-1">{error}</p>
{/if}
</div>
Email Input
<div class="mb-4">
<label for="email" class="block text-gray-700 font-semibold mb-2">
Email Address *
</label>
<input
id="email"
type="email"
bind:value={formData.email}
on:blur={validateEmail}
class="w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-custom"
class:border-red-500={errors.email}
placeholder="your@email.com"
required
/>
{#if errors.email}
<p class="text-red-500 text-sm mt-1">{errors.email}</p>
{/if}
</div>
Phone Input
<div class="mb-4">
<label for="phone" class="block text-gray-700 font-semibold mb-2">
Mobile Number *
</label>
<div class="flex">
<span class="inline-flex items-center px-4 bg-gray-100 border border-r-0 border-gray-300 rounded-l-lg text-gray-600">
+91
</span>
<input
id="phone"
type="tel"
bind:value={formData.phone}
on:blur={validatePhone}
on:input={(e) => {
// Only allow digits
e.target.value = e.target.value.replace(/\D/g, '');
}}
maxlength="10"
class="flex-1 px-4 py-3 border border-gray-300 rounded-r-lg focus:outline-none focus:ring-2 focus:ring-orange-custom"
class:border-red-500={errors.phone}
placeholder="9876543210"
required
/>
</div>
{#if errors.phone}
<p class="text-red-500 text-sm mt-1">{errors.phone}</p>
{/if}
</div>
Textarea
<div class="mb-6">
<label for="message" class="block text-gray-700 font-semibold mb-2">
Message *
</label>
<textarea
id="message"
bind:value={formData.message}
rows="5"
class="w-full px-4 py-3 border border-gray-300 rounded-lg
focus:outline-none focus:ring-2 focus:ring-orange-custom focus:border-transparent
resize-none"
placeholder="Tell us more..."
required
></textarea>
<p class="text-sm text-gray-500 mt-1">
{formData.message.length} / 500 characters
</p>
</div>
Select Dropdown
<div class="mb-4">
<label for="opportunity" class="block text-gray-700 font-semibold mb-2">
Volunteer Opportunity *
</label>
<select
id="opportunity"
bind:value={formData.opportunity}
class="w-full px-4 py-3 border border-gray-300 rounded-lg
focus:outline-none focus:ring-2 focus:ring-orange-custom
bg-white"
required
>
<option value="">Select an opportunity</option>
<option value="teaching">Teaching</option>
<option value="food-distribution">Food Distribution</option>
<option value="healthcare">Healthcare Support</option>
<option value="community">Community Outreach</option>
</select>
</div>
Radio Buttons
<div class="mb-4">
<label class="block text-gray-700 font-semibold mb-3">
Gender *
</label>
<div class="flex gap-4">
<label class="inline-flex items-center cursor-pointer">
<input
type="radio"
bind:group={formData.gender}
value="Male"
class="w-4 h-4 text-orange-custom focus:ring-orange-custom"
required
/>
<span class="ml-2">Male</span>
</label>
<label class="inline-flex items-center cursor-pointer">
<input
type="radio"
bind:group={formData.gender}
value="Female"
class="w-4 h-4 text-orange-custom focus:ring-orange-custom"
/>
<span class="ml-2">Female</span>
</label>
<label class="inline-flex items-center cursor-pointer">
<input
type="radio"
bind:group={formData.gender}
value="Other"
class="w-4 h-4 text-orange-custom focus:ring-orange-custom"
/>
<span class="ml-2">Other</span>
</label>
</div>
</div>
Checkboxes (Multi-Select)
<div class="mb-4">
<label class="block text-gray-700 font-semibold mb-3">
Areas of Interest *
</label>
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
{#each interestOptions as option}
<label class="inline-flex items-center cursor-pointer">
<input
type="checkbox"
bind:group={formData.interests}
value={option}
class="w-4 h-4 text-orange-custom rounded focus:ring-orange-custom"
/>
<span class="ml-2">{option}</span>
</label>
{/each}
</div>
{#if errors.interests}
<p class="text-red-500 text-sm mt-1">{errors.interests}</p>
{/if}
</div>
Date Input
<div class="mb-4">
<label for="startDate" class="block text-gray-700 font-semibold mb-2">
Preferred Start Date *
</label>
<input
id="startDate"
type="date"
bind:value={formData.startDate}
min={new Date().toISOString().split('T')[0]}
class="w-full px-4 py-3 border border-gray-300 rounded-lg
focus:outline-none focus:ring-2 focus:ring-orange-custom"
required
/>
</div>
Form Submission Workflow
Standard Submission Flow
<script lang="ts">
async function handleSubmit() {
// 1. Clear previous messages
successMessage = '';
errorMessage = '';
// 2. Validate all fields
if (!validateForm()) {
errorMessage = 'Please fix the errors above';
return;
}
// 3. Set loading state
isSubmitting = true;
try {
// 4. Create submission object with timestamp
const submission = {
...formData,
timestamp: Date.now()
};
// 5. Update local store (optional, for immediate UI feedback)
addSubmission(submission);
// 6. Save to Firestore
const docId = await addToFirestore(submission);
console.log('✅ Saved with ID:', docId);
// 7. Show success message
successMessage = 'Thank you! Your submission has been received.';
// 8. Reset form (optional)
resetForm();
// 9. Optional: Redirect after delay
setTimeout(() => {
goto('/thank-you');
}, 2000);
} catch (error) {
// 10. Handle errors
console.error('❌ Submission error:', error);
errorMessage = 'Something went wrong. Please try again.';
} finally {
// 11. Clear loading state
isSubmitting = false;
}
}
function resetForm() {
formData = { /* reset to initial values */ };
errors = { /* clear all errors */ };
}
</script>
Success & Error Messages
Success Message Component
{#if successMessage}
<div class="mb-4 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg flex items-start animate-fade-in">
<Icon icon="mdi:check-circle" class="text-2xl mr-3 flex-shrink-0" />
<div>
<p class="font-semibold">Success!</p>
<p class="text-sm">{successMessage}</p>
</div>
</div>
{/if}
Error Message Component
{#if errorMessage}
<div class="mb-4 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start animate-fade-in">
<Icon icon="mdi:alert-circle" class="text-2xl mr-3 flex-shrink-0" />
<div>
<p class="font-semibold">Error</p>
<p class="text-sm">{errorMessage}</p>
</div>
</div>
{/if}
Inline Field Errors
{#if errors.fieldName}
<p class="text-red-500 text-sm mt-1 flex items-center">
<Icon icon="mdi:alert-circle-outline" class="mr-1" />
{errors.fieldName}
</p>
{/if}
Submit Button States
Standard Submit Button
<button
type="submit"
disabled={isSubmitting}
class="w-full bg-orange-custom hover:bg-orange-dark text-white font-semibold
py-3 px-6 rounded-lg transition-all duration-300
shadow-md hover:shadow-lg"
class:opacity-50={isSubmitting}
class:cursor-not-allowed={isSubmitting}
>
{#if isSubmitting}
<Icon icon="mdi:loading" class="inline-block animate-spin mr-2" />
Submitting...
{:else}
Submit
{/if}
</button>
Button with Success State
<button
type="submit"
disabled={isSubmitting || isSuccess}
class="w-full font-semibold py-3 px-6 rounded-lg transition-all duration-300"
class:bg-orange-custom={!isSuccess}
class:hover:bg-orange-dark={!isSuccess}
class:bg-green-500={isSuccess}
class:opacity-50={isSubmitting}
>
{#if isSubmitting}
<Icon icon="mdi:loading" class="inline-block animate-spin mr-2" />
Submitting...
{:else if isSuccess}
<Icon icon="mdi:check" class="inline-block mr-2" />
Submitted!
{:else}
Submit
{/if}
</button>
Form Accessibility
Accessible Form Markup
<form on:submit|preventDefault={handleSubmit} aria-label="Contact form">
<div class="mb-4">
<label for="name" class="block text-gray-700 font-semibold mb-2">
Full Name *
<span class="sr-only">required</span>
</label>
<input
id="name"
type="text"
bind:value={formData.name}
aria-required="true"
aria-invalid={!!errors.name}
aria-describedby={errors.name ? 'name-error' : undefined}
class="w-full px-4 py-3 border rounded-lg"
required
/>
{#if errors.name}
<p id="name-error" class="text-red-500 text-sm mt-1" role="alert">
{errors.name}
</p>
{/if}
</div>
</form>
When to Use This Skill
- Creating new forms
- Adding form validation
- Implementing submission workflows
- Handling form errors
- Integrating with Firestore
- Improving form UX
- Adding accessibility features
Related Skills
sikhaid-data- Stores and Firestore operationssikhaid-components- Component patternssikhaid-styling- Form styling and designsikhaid-payment- Payment form specifics
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?