Back to skills
extension
Category: Development & EngineeringNo API key required

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.

personAuthor: jakexiaohubgithub

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 operations
  • sikhaid-components - Component patterns
  • sikhaid-styling - Form styling and design
  • sikhaid-payment - Payment form specifics