返回 Skill 列表
extension
分类: 开发与工程无需 API Key

sikhaid-data

在SikhAid项目中处理数据、状态管理、Firebase或Firestore时使用。涵盖Svelte存储、数据流模式、Firebase初始化、Firestore CRUD操作和数据类型。

person作者: jakexiaohubgithub

SikhAid Data & State Management

State Management Architecture

Svelte Stores

Pattern: Reactive stores for client-side state management

Store Types Used:

  • writable() - Read/write stores for component state
  • No derived stores currently
  • No custom stores (plain writable stores)

Store Locations:

src/lib/stores/
├── donation.ts       # Donation amount state
├── contact.ts        # Contact form submissions
├── volunteering.ts   # Volunteer form submissions
└── csr.ts            # CSR partnership submissions

Store Patterns

1. Donation Store

File: src/lib/stores/donation.ts

Purpose: Manage selected donation amount across components

import { writable } from 'svelte/store';

export const selectedAmount = writable<number>(0);

export function setDonationAmount(amount: number) {
  selectedAmount.set(amount);

  // Side effect: scroll to payment form
  setTimeout(() => {
    const paymentSection = document.getElementById('payment-form');
    paymentSection?.scrollIntoView({ behavior: 'smooth' });
  }, 100);
}

Usage:

<script lang="ts">
  import { selectedAmount, setDonationAmount } from '$lib/stores/donation';

  // Read value with $ prefix
  $: amount = $selectedAmount;

  // Write value
  function selectAmount(value: number) {
    setDonationAmount(value);
  }
</script>

<div>Selected: ₹{$selectedAmount}</div>
<button on:click={() => selectAmount(1000)}>₹1,000</button>

2. Contact Submissions Store

File: src/lib/stores/contact.ts

Purpose: Store contact form submissions (in-memory and Firestore)

import { writable } from 'svelte/store';

export interface ContactSubmission {
  name: string;
  email: string;
  phone: string;
  subject: string;
  message: string;
  timestamp: number;
}

export const contactSubmissions = writable<ContactSubmission[]>([]);

export function addContactSubmission(submission: ContactSubmission) {
  contactSubmissions.update(submissions => {
    return [...submissions, submission];
  });
}

3. Volunteering Store

File: src/lib/stores/volunteering.ts

Purpose: Store volunteer applications

export interface VolunteerSubmission {
  fullName: string;
  email: string;
  mobile: string;
  gender: string;
  address: string;
  opportunity: string;
  durationMonths: number;
  durationYears: number;
  startDate: string;
  about: string;
  timestamp: number;
}

export const volunteerSubmissions = writable<VolunteerSubmission[]>([]);

4. CSR Store

File: src/lib/stores/csr.ts

Purpose: Store CSR partnership inquiries

export interface CSRSubmission {
  companyName: string;
  contactPerson: string;
  email: string;
  phone: string;
  companySize: string;
  interests: string[];
  message: string;
  timestamp: number;
}

export const csrSubmissions = writable<CSRSubmission[]>([]);

Data Flow Pattern

Standard Form Submission Flow

1. User fills form
   ↓
2. Form validation
   ↓
3. Create submission object with timestamp
   ↓
4. Update Svelte store (addSubmission)
   ↓
5. Write to Firestore (addToFirestore)
   ↓
6. Show success/error feedback
   ↓
7. Reset form (optional)

Example Flow Implementation

<script lang="ts">
  import { addContactSubmission } from '$lib/stores/contact';
  import { addContactToFirestore } from '$lib/firestore';

  let formData = { name: '', email: '', phone: '', subject: '', message: '' };
  let isSubmitting = false;
  let successMessage = '';
  let errorMessage = '';

  async function handleSubmit() {
    // 1. Validate
    if (!validateForm()) return;

    isSubmitting = true;
    errorMessage = '';

    try {
      // 2. Create submission object
      const submission = {
        ...formData,
        timestamp: Date.now()
      };

      // 3. Update store
      addContactSubmission(submission);

      // 4. Write to Firestore
      await addContactToFirestore(submission);

      // 5. Success feedback
      successMessage = 'Submission received!';
      formData = { name: '', email: '', phone: '', subject: '', message: '' };
    } catch (error) {
      // 6. Error feedback
      errorMessage = 'Submission failed. Please try again.';
      console.error('❌ Error:', error);
    } finally {
      isSubmitting = false;
    }
  }
</script>

Firebase Integration

Firebase Initialization

File: src/lib/firebase.ts

import { initializeApp, type FirebaseApp } from 'firebase/app';
import { getFirestore, type Firestore } from 'firebase/firestore';
import { getAuth, type Auth } from 'firebase/auth';

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID
};

let app: FirebaseApp | null = null;
let db: Firestore | null = null;
let auth: Auth | null = null;

// Browser-only initialization
if (typeof window !== 'undefined') {
  try {
    app = initializeApp(firebaseConfig);
    db = getFirestore(app);
    auth = getAuth(app);
    console.log('✅ Firebase initialized successfully');
  } catch (error) {
    console.error('❌ Firebase initialization error:', error);
  }
}

export { app, db, auth };

Key Points:

  • Browser-only initialization (typeof window !== 'undefined')
  • Environment variables for configuration
  • Error handling with console logging
  • Exports app, db, and auth instances

Firestore Operations

Collections Structure

Firestore Database
├── contact_submissions/
│   └── [auto-generated-id]/
│       ├── name: string
│       ├── email: string
│       ├── phone: string
│       ├── subject: string
│       ├── message: string
│       ├── status: 'new' | 'read' | 'resolved'
│       └── timestamp: Firestore Timestamp
│
├── volunteer_submissions/
│   └── [auto-generated-id]/
│       ├── fullName: string
│       ├── email: string
│       ├── mobile: string
│       ├── opportunity: string
│       ├── status: 'new' | 'read' | 'resolved'
│       └── ...
│
└── csr_submissions/
    └── [auto-generated-id]/
        ├── companyName: string
        ├── email: string
        ├── status: 'new' | 'read' | 'resolved'
        └── ...

Firestore Functions

File: src/lib/firestore.ts

Add Contact Submission

import { collection, addDoc, Timestamp } from 'firebase/firestore';
import { db } from './firebase';
import type { ContactSubmission } from './stores/contact';

export async function addContactToFirestore(
  submission: ContactSubmission
): Promise<string> {
  if (!db) throw new Error('Firestore not initialized');

  try {
    const docRef = await addDoc(collection(db, 'contact_submissions'), {
      ...submission,
      status: 'new',
      timestamp: Timestamp.fromMillis(submission.timestamp)
    });

    console.log('✅ Contact added to Firestore:', docRef.id);
    return docRef.id;
  } catch (error) {
    console.error('❌ Error adding contact:', error);
    throw error;
  }
}

Get All Submissions

import { collection, getDocs, query, orderBy } from 'firebase/firestore';

export interface FirestoreContactSubmission extends ContactSubmission {
  id?: string;
  status: 'new' | 'read' | 'resolved';
  firestoreTimestamp?: Timestamp;
}

export async function getContactSubmissions(): Promise<FirestoreContactSubmission[]> {
  if (!db) throw new Error('Firestore not initialized');

  try {
    const q = query(
      collection(db, 'contact_submissions'),
      orderBy('timestamp', 'desc')
    );

    const querySnapshot = await getDocs(q);
    const submissions: FirestoreContactSubmission[] = [];

    querySnapshot.forEach((doc) => {
      submissions.push({
        id: doc.id,
        ...doc.data() as FirestoreContactSubmission
      });
    });

    console.log(`📊 Fetched ${submissions.length} contact submissions`);
    return submissions;
  } catch (error) {
    console.error('❌ Error fetching submissions:', error);
    throw error;
  }
}

Update Submission Status

import { doc, updateDoc } from 'firebase/firestore';

export async function updateSubmissionStatus(
  collectionName: string,
  documentId: string,
  status: 'new' | 'read' | 'resolved'
): Promise<void> {
  if (!db) throw new Error('Firestore not initialized');

  try {
    const docRef = doc(db, collectionName, documentId);
    await updateDoc(docRef, { status });

    console.log(`✅ Updated ${documentId} status to ${status}`);
  } catch (error) {
    console.error('❌ Error updating status:', error);
    throw error;
  }
}

Add Volunteer Submission

export async function addVolunteerToFirestore(
  submission: VolunteerSubmission
): Promise<string> {
  if (!db) throw new Error('Firestore not initialized');

  try {
    const docRef = await addDoc(collection(db, 'volunteer_submissions'), {
      ...submission,
      status: 'new',
      timestamp: Timestamp.fromMillis(submission.timestamp)
    });

    console.log('✅ Volunteer added:', docRef.id);
    return docRef.id;
  } catch (error) {
    console.error('❌ Error adding volunteer:', error);
    throw error;
  }
}

Get Volunteer Submissions

export async function getVolunteerSubmissions(): Promise<FirestoreVolunteerSubmission[]> {
  if (!db) throw new Error('Firestore not initialized');

  try {
    const q = query(
      collection(db, 'volunteer_submissions'),
      orderBy('timestamp', 'desc')
    );

    const querySnapshot = await getDocs(q);
    const submissions: FirestoreVolunteerSubmission[] = [];

    querySnapshot.forEach((doc) => {
      submissions.push({
        id: doc.id,
        ...doc.data() as FirestoreVolunteerSubmission
      });
    });

    return submissions;
  } catch (error) {
    console.error('❌ Error fetching volunteers:', error);
    throw error;
  }
}

CSR Functions (Similar Pattern)

  • addCSRToFirestore() - Add CSR submission
  • getCSRSubmissions() - Get all CSR submissions
  • Both follow same pattern as contact/volunteer

Static Data Management

Campaigns Data

File: src/lib/data/campaigns.js

Structure:

export const campaigns = [
  {
    slug: 'langar-aid',
    title: 'Langar Aid',
    subtitle: 'Free Meals for All',
    shortDescription: 'Serving nutritious meals...',
    fullDescription: 'Detailed description...',
    image: '/images/campaign.jpg',
    category: 'hunger-relief',
    status: 'ongoing', // 'ongoing' | 'completed' | 'seasonal'
    impactStats: [
      { label: 'Meals Served', value: '100,000+', icon: 'mdi:food' }
    ],
    howItWorks: [
      { step: 1, title: 'Step Title', description: 'Step description' }
    ],
    gallery: [
      { src: '/images/gallery1.jpg', alt: 'Description' }
    ]
  },
  // More campaigns...
];

Usage:

<script lang="ts">
  import { campaigns } from '$lib/data/campaigns';
  import type { Campaign } from '$lib/types/campaign';

  // Filter campaigns
  $: ongoingCampaigns = campaigns.filter(c => c.status === 'ongoing');

  // Find by slug
  $: campaign = campaigns.find(c => c.slug === 'langar-aid');
</script>

{#each ongoingCampaigns as campaign}
  <div>{campaign.title}</div>
{/each}

Blog Data

File: src/lib/data/blogs.js

Structure:

export const blogs = [
  {
    slug: 'post-slug',
    title: 'Blog Post Title',
    excerpt: 'Short excerpt...',
    content: 'Full blog content...',
    author: 'Author Name',
    date: '2024-01-15',
    image: '/images/blog.jpg',
    category: 'community'
  },
  // More blog posts...
];

TypeScript Types & Interfaces

Campaign Types

File: src/lib/types/campaign.ts

export interface Campaign {
  slug: string;
  title: string;
  subtitle: string;
  shortDescription: string;
  fullDescription: string;
  image: string;
  category: string;
  status: 'ongoing' | 'completed' | 'seasonal';
  impactStats: ImpactStat[];
  howItWorks: HowItWorksStep[];
  gallery: GalleryImage[];
}

export interface ImpactStat {
  label: string;
  value: string;
  icon: string;
}

export interface HowItWorksStep {
  step: number;
  title: string;
  description: string;
}

export interface GalleryImage {
  src: string;
  alt: string;
}

Store Types (Defined in store files)

// Contact
export interface ContactSubmission {
  name: string;
  email: string;
  phone: string;
  subject: string;
  message: string;
  timestamp: number;
}

// Volunteer
export interface VolunteerSubmission {
  fullName: string;
  email: string;
  mobile: string;
  gender: string;
  address: string;
  opportunity: string;
  durationMonths: number;
  durationYears: number;
  startDate: string;
  about: string;
  timestamp: number;
}

// CSR
export interface CSRSubmission {
  companyName: string;
  contactPerson: string;
  email: string;
  phone: string;
  companySize: string;
  interests: string[];
  message: string;
  timestamp: number;
}

Firestore Extended Types

import type { Timestamp } from 'firebase/firestore';

export interface FirestoreContactSubmission extends ContactSubmission {
  id?: string;
  status: 'new' | 'read' | 'resolved';
  firestoreTimestamp?: Timestamp;
}

export interface FirestoreVolunteerSubmission extends VolunteerSubmission {
  id?: string;
  status: 'new' | 'read' | 'resolved';
  firestoreTimestamp?: Timestamp;
}

export interface FirestoreCSRSubmission extends CSRSubmission {
  id?: string;
  status: 'new' | 'read' | 'resolved';
  firestoreTimestamp?: Timestamp;
}

Data Loading in Components

Loading Firestore Data (Admin Panel)

<script lang="ts">
  import { onMount } from 'svelte';
  import {
    getContactSubmissions,
    type FirestoreContactSubmission
  } from '$lib/firestore';

  let submissions: FirestoreContactSubmission[] = [];
  let isLoading = true;
  let error = '';

  onMount(async () => {
    try {
      submissions = await getContactSubmissions();
      isLoading = false;
    } catch (err) {
      error = 'Failed to load submissions';
      isLoading = false;
    }
  });
</script>

{#if isLoading}
  <div>Loading...</div>
{:else if error}
  <div class="text-red-500">{error}</div>
{:else}
  {#each submissions as submission}
    <div>{submission.name}</div>
  {/each}
{/if}

Using Static Data

<script lang="ts">
  import { campaigns } from '$lib/data/campaigns';

  // No onMount needed - data is already available
  $: featuredCampaigns = campaigns.filter(c => c.featured);
</script>

{#each featuredCampaigns as campaign}
  <div>{campaign.title}</div>
{/each}

Store Subscription Patterns

Auto-subscribing with $ Prefix

<script lang="ts">
  import { selectedAmount } from '$lib/stores/donation';

  // Automatic subscription
  $: amount = $selectedAmount;
</script>

<div>Amount: ₹{$selectedAmount}</div>

Manual Subscribe/Unsubscribe

<script lang="ts">
  import { onDestroy } from 'svelte';
  import { contactSubmissions } from '$lib/stores/contact';

  let submissions = [];

  const unsubscribe = contactSubmissions.subscribe(value => {
    submissions = value;
  });

  onDestroy(() => {
    unsubscribe();
  });
</script>

Common Data Patterns

Filtering Arrays

<script lang="ts">
  import { campaigns } from '$lib/data/campaigns';

  let selectedCategory = 'all';

  $: filteredCampaigns = selectedCategory === 'all'
    ? campaigns
    : campaigns.filter(c => c.category === selectedCategory);
</script>

Searching

<script lang="ts">
  let searchQuery = '';
  let campaigns = [...];

  $: searchResults = campaigns.filter(campaign =>
    campaign.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
    campaign.shortDescription.toLowerCase().includes(searchQuery.toLowerCase())
  );
</script>

Sorting

<script lang="ts">
  let submissions = [...];
  let sortBy = 'timestamp'; // 'timestamp' | 'name' | 'status'

  $: sortedSubmissions = [...submissions].sort((a, b) => {
    if (sortBy === 'timestamp') {
      return b.timestamp - a.timestamp; // Newest first
    }
    if (sortBy === 'name') {
      return a.name.localeCompare(b.name);
    }
    return 0;
  });
</script>

When to Use This Skill

  • Working with Svelte stores
  • Integrating Firebase/Firestore
  • Managing form submissions
  • Loading or filtering data
  • Creating new data types
  • Implementing CRUD operations
  • Understanding data flow in the app

Related Skills

  • sikhaid-forms - Form submission patterns
  • sikhaid-components - Component state management
  • sikhaid-overview - Firebase configuration