> ## Documentation Index
> Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Success Stories

> User testimonials that inspire action and reduce stigma around benefit programs

# Success Stories

> Real stories from real people who discovered benefits they qualified for, building trust and reducing stigma.

***

## Why Success Stories Matter

Many people don't apply for benefits because:

* They assume "those programs aren't for people like me"
* They feel ashamed about needing help
* They don't believe the programs actually work

Success stories counter these barriers by showing:

* Diverse people benefiting (not just stereotypes)
* Concrete impact (dollar amounts, life changes)
* That the process is manageable

```mermaid theme={null}
flowchart LR
    subgraph Barrier
        Stigma[Stigma]
        Doubt[Doubt]
        Fear[Fear]
    end

    subgraph Story
        Real[Real Person]
        Impact[Real Impact]
        Process[Real Process]
    end

    subgraph Outcome
        Trust[Trust]
        Action[Take Action]
    end

    Stigma --> Real --> Trust
    Doubt --> Impact --> Trust
    Fear --> Process --> Trust
    Trust --> Action
```

***

## Data Model

```typescript theme={null}
interface SuccessStory {
  id: string;

  // Author
  authorId: string | null;  // null for truly anonymous
  authorName: string;       // Display name or "Anonymous"
  isAnonymous: boolean;

  // Content
  title: string;           // "How SNAP helped my family..."
  story: string;           // Full narrative (min 100 chars)
  quote: string | null;    // Pull quote for cards

  // Association
  programId: string | null;
  programName: string | null;

  // Impact metrics
  benefitReceivedCents: number | null;
  timeToApprovalDays: number | null;

  // Moderation
  status: "pending" | "approved" | "rejected" | "featured";
  moderatedBy: string | null;
  moderatedAt: Date | null;
  rejectionReason: string | null;

  // Engagement
  viewCount: number;
  helpfulCount: number;

  // Media
  imageUrl: string | null;
  videoUrl: string | null;

  // Timestamps
  createdAt: Date;
  updatedAt: Date;
}
```

***

## Database Schema

```sql theme={null}
CREATE TABLE success_stories (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  -- Author (can be anonymous)
  author_id TEXT REFERENCES profiles(id) ON DELETE SET NULL,
  author_name TEXT NOT NULL,
  is_anonymous BOOLEAN DEFAULT false,

  -- Story content
  title TEXT NOT NULL,
  story TEXT NOT NULL,
  quote TEXT,

  -- Related program
  program_id UUID REFERENCES programs(id) ON DELETE SET NULL,

  -- Impact metrics
  benefit_received_cents INTEGER,
  time_to_approval_days INTEGER,

  -- Moderation
  status TEXT DEFAULT 'pending' CHECK (status IN (
    'pending',     -- Awaiting review
    'approved',    -- Published
    'rejected',    -- Not suitable
    'featured'     -- Highlighted on homepage
  )),
  moderated_by TEXT REFERENCES profiles(id) ON DELETE SET NULL,
  moderated_at TIMESTAMPTZ,
  rejection_reason TEXT,

  -- Engagement
  view_count INTEGER DEFAULT 0,
  helpful_count INTEGER DEFAULT 0,

  -- Media
  image_url TEXT,
  video_url TEXT,

  -- Timestamps
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Indexes
CREATE INDEX idx_success_stories_status
  ON success_stories(status, created_at DESC)
  WHERE status IN ('approved', 'featured');

CREATE INDEX idx_success_stories_program
  ON success_stories(program_id, status)
  WHERE status IN ('approved', 'featured');

-- Deduplicated voting
CREATE TABLE story_helpful_votes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  story_id UUID NOT NULL REFERENCES success_stories(id) ON DELETE CASCADE,
  user_id TEXT REFERENCES profiles(id) ON DELETE SET NULL,
  session_id TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW(),

  UNIQUE(story_id, user_id),
  UNIQUE(story_id, session_id)
);
```

***

## Story Submission Flow

Three-step wizard for submitting stories.

```tsx theme={null}
// app/(dashboard)/share-story/page.tsx
export default function ShareStoryPage() {
  const [step, setStep] = useState<"form" | "review" | "success">("form");
  const [formData, setFormData] = useState<StoryFormData>({
    title: "",
    story: "",
    quote: "",
    programId: "",
    benefitAmount: "",
    isAnonymous: false,
    displayName: "",
  });

  const isFormValid =
    formData.title.trim() &&
    formData.story.trim() &&
    formData.story.length >= 100 &&
    (formData.isAnonymous || formData.displayName.trim());

  if (step === "success") {
    return <SuccessScreen />;
  }

  if (step === "review") {
    return (
      <ReviewScreen
        formData={formData}
        onEdit={() => setStep("form")}
        onSubmit={async () => {
          await submitStory(formData);
          setStep("success");
        }}
      />
    );
  }

  return (
    <div className="max-w-2xl mx-auto">
      {/* Hero */}
      <div className="text-center mb-8">
        <div className="w-16 h-16 rounded-full bg-amber-100 mx-auto mb-4 flex items-center justify-center">
          <Sparkles className="h-8 w-8 text-amber-600" />
        </div>
        <h1 className="text-2xl font-bold">Share Your Success Story</h1>
        <p className="text-stone-600 mt-2">
          Your experience could help someone else discover benefits they qualify for
        </p>
      </div>

      {/* Form */}
      <Card>
        <CardContent className="space-y-6 p-6">
          {/* Title */}
          <div>
            <Label>Story Title *</Label>
            <Input
              placeholder="e.g., How SNAP helped my family during a difficult time"
              value={formData.title}
              onChange={e => setFormData(f => ({ ...f, title: e.target.value }))}
            />
          </div>

          {/* Story */}
          <div>
            <Label>Your Story *</Label>
            <p className="text-xs text-stone-500 mb-1.5">
              Minimum 100 characters. Share what happened, how you found the
              program, and how it helped.
            </p>
            <Textarea
              rows={8}
              value={formData.story}
              onChange={e => setFormData(f => ({ ...f, story: e.target.value }))}
            />
            <p className="text-xs text-stone-500 mt-1">
              {formData.story.length} / 100 characters minimum
            </p>
          </div>

          {/* Pull quote */}
          <div>
            <Label>Pull Quote (Optional)</Label>
            <p className="text-xs text-stone-500 mb-1.5">
              A short, impactful quote we can feature in cards
            </p>
            <Textarea
              rows={2}
              placeholder="e.g., This program changed my family's life..."
              value={formData.quote}
              onChange={e => setFormData(f => ({ ...f, quote: e.target.value }))}
            />
          </div>

          {/* Program selection */}
          <div>
            <Label>Related Program (Optional)</Label>
            <Select
              value={formData.programId}
              onValueChange={v => setFormData(f => ({ ...f, programId: v }))}
            >
              <SelectTrigger>
                <SelectValue placeholder="Select a program..." />
              </SelectTrigger>
              <SelectContent>
                {PROGRAMS.map(p => (
                  <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>

          {/* Benefit amount */}
          <div>
            <Label>Benefit Amount Received (Optional)</Label>
            <div className="relative">
              <span className="absolute left-3 top-1/2 -translate-y-1/2 text-stone-500">
                $
              </span>
              <Input
                type="number"
                className="pl-7"
                value={formData.benefitAmount}
                onChange={e => setFormData(f => ({ ...f, benefitAmount: e.target.value }))}
              />
            </div>
            <p className="text-xs text-stone-500 mt-1">
              Monthly benefit or one-time assistance received
            </p>
          </div>

          {/* Anonymous toggle */}
          <div className="flex items-center justify-between p-4 bg-stone-50 rounded-lg">
            <div>
              <p className="font-medium">Share Anonymously</p>
              <p className="text-sm text-stone-500">
                Your name won&apos;t be displayed
              </p>
            </div>
            <Switch
              checked={formData.isAnonymous}
              onCheckedChange={v => setFormData(f => ({ ...f, isAnonymous: v }))}
            />
          </div>

          {/* Display name */}
          {!formData.isAnonymous && (
            <div>
              <Label>Display Name *</Label>
              <Input
                placeholder="e.g., Sarah J."
                value={formData.displayName}
                onChange={e => setFormData(f => ({ ...f, displayName: e.target.value }))}
              />
              <p className="text-xs text-stone-500 mt-1">
                You can use first name and last initial
              </p>
            </div>
          )}

          {/* Guidelines */}
          <div className="bg-blue-50 rounded-lg p-4">
            <h4 className="font-medium text-blue-900 mb-2 flex items-center gap-2">
              <Heart className="h-4 w-4" />
              Story Guidelines
            </h4>
            <ul className="text-sm text-blue-800 space-y-1">
              <li>Be honest and share your real experience</li>
              <li>Avoid sensitive info (SSN, account numbers)</li>
              <li>Keep it respectful and appropriate</li>
              <li>Stories are reviewed before publishing</li>
            </ul>
          </div>

          {/* Submit */}
          <Button
            className="w-full"
            size="lg"
            disabled={!isFormValid}
            onClick={() => setStep("review")}
          >
            Preview & Submit
          </Button>
        </CardContent>
      </Card>
    </div>
  );
}
```

***

## Story Card Component

Three variants for different display contexts.

```tsx theme={null}
// components/success-stories/story-card.tsx
interface StoryCardProps {
  story: SuccessStory;
  variant?: "default" | "compact" | "featured";
  onMarkHelpful?: (storyId: string) => void;
  hasVoted?: boolean;
}

export function StoryCard({
  story,
  variant = "default",
  onMarkHelpful,
  hasVoted = false,
}: StoryCardProps) {
  // Compact variant for carousels
  if (variant === "compact") {
    return (
      <Card className="h-full hover:shadow-md transition-shadow">
        <CardContent className="p-5">
          {story.quote && (
            <div className="relative mb-4">
              <Quote className="absolute -top-1 -left-1 h-6 w-6 text-stone-200" />
              <p className="text-stone-700 italic pl-6 line-clamp-3">
                &ldquo;{story.quote}&rdquo;
              </p>
            </div>
          )}

          <div className="flex items-center gap-3">
            <div className="w-8 h-8 rounded-full bg-stone-200 flex items-center justify-center">
              {getInitials(story)}
            </div>
            <div>
              <p className="text-sm font-medium">{getDisplayName(story)}</p>
              {story.programName && (
                <p className="text-xs text-stone-500">{story.programName}</p>
              )}
            </div>
          </div>

          {story.benefitReceivedCents && (
            <Badge className="mt-3 bg-emerald-100 text-emerald-700">
              <DollarSign className="h-3 w-3 mr-1" />
              {formatBenefit(story.benefitReceivedCents)} received
            </Badge>
          )}
        </CardContent>
      </Card>
    );
  }

  // Featured variant for homepage
  if (variant === "featured") {
    return (
      <Card className="overflow-hidden bg-gradient-to-br from-stone-50 to-white">
        <CardContent className="p-6 md:p-8">
          <div className="flex flex-col md:flex-row gap-6">
            {story.imageUrl && (
              <div className="w-full md:w-48 h-48 rounded-xl overflow-hidden shrink-0">
                <img
                  src={story.imageUrl}
                  alt={`${getDisplayName(story)}'s story`}
                  className="w-full h-full object-cover"
                />
              </div>
            )}

            <div className="flex-1">
              <div className="relative mb-4">
                <Quote className="absolute -top-2 -left-2 h-8 w-8 text-emerald-200" />
                <p className="text-lg md:text-xl italic pl-8">
                  &ldquo;{story.quote || story.story.slice(0, 150)}...&rdquo;
                </p>
              </div>

              <div className="flex items-center gap-3 mb-4">
                <div className="w-10 h-10 rounded-full bg-emerald-100 flex items-center justify-center text-emerald-700 font-medium">
                  {getInitials(story)}
                </div>
                <div>
                  <p className="font-semibold">{getDisplayName(story)}</p>
                  {story.programName && (
                    <p className="text-sm text-stone-500">{story.programName}</p>
                  )}
                </div>
              </div>

              <div className="flex flex-wrap gap-4 mb-4">
                {story.benefitReceivedCents && (
                  <Badge className="bg-emerald-100 text-emerald-700">
                    {formatBenefit(story.benefitReceivedCents)} in benefits
                  </Badge>
                )}
                <Badge variant="secondary">
                  <ThumbsUp className="h-3 w-3 mr-1" />
                  {story.helpfulCount} found helpful
                </Badge>
              </div>

              <Link
                href={`/stories/${story.id}`}
                className="inline-flex items-center gap-1 text-sm font-medium text-emerald-600"
              >
                Read full story
                <ArrowRight className="h-4 w-4" />
              </Link>
            </div>
          </div>
        </CardContent>
      </Card>
    );
  }

  // Default variant
  return (
    <Card className="h-full hover:shadow-md transition-shadow">
      <CardContent className="p-6">
        {/* Header */}
        <div className="flex items-start justify-between gap-4 mb-4">
          <div className="flex items-center gap-3">
            <div className="w-10 h-10 rounded-full bg-stone-200 flex items-center justify-center">
              {story.isAnonymous ? (
                <User className="h-5 w-5" />
              ) : (
                getInitials(story)
              )}
            </div>
            <div>
              <p className="font-medium">{getDisplayName(story)}</p>
              <p className="text-xs text-stone-500">
                {formatDate(story.createdAt)}
              </p>
            </div>
          </div>
          {story.programName && (
            <Badge variant="secondary">{story.programName}</Badge>
          )}
        </div>

        {/* Title & Content */}
        <h3 className="font-semibold mb-2">{story.title}</h3>

        {story.quote ? (
          <div className="relative mb-4">
            <Quote className="absolute -top-1 -left-1 h-5 w-5 text-stone-200" />
            <p className="text-stone-600 italic pl-5 line-clamp-3">
              &ldquo;{story.quote}&rdquo;
            </p>
          </div>
        ) : (
          <p className="text-stone-600 line-clamp-3 mb-4">{story.story}</p>
        )}

        {/* Benefit */}
        {story.benefitReceivedCents && (
          <div className="flex items-center gap-2 mb-4 text-emerald-700 bg-emerald-50 rounded-lg px-3 py-2">
            <DollarSign className="h-4 w-4" />
            <span className="text-sm font-medium">
              Received {formatBenefit(story.benefitReceivedCents)} in benefits
            </span>
          </div>
        )}

        {/* Footer */}
        <div className="flex items-center justify-between pt-4 border-t">
          <Button
            variant={hasVoted ? "secondary" : "outline"}
            size="sm"
            onClick={() => onMarkHelpful?.(story.id)}
            disabled={hasVoted}
          >
            <ThumbsUp className={cn("h-4 w-4 mr-1.5", hasVoted && "fill-current")} />
            {hasVoted ? "Thanks!" : "Helpful"} ({story.helpfulCount})
          </Button>

          <Link
            href={`/stories/${story.id}`}
            className="text-sm font-medium text-stone-600 hover:text-stone-900"
          >
            Read more
          </Link>
        </div>
      </CardContent>
    </Card>
  );
}
```

***

## Stories Carousel

Horizontal scrolling carousel for program pages.

```tsx theme={null}
// components/success-stories/program-stories.tsx
export function ProgramStories({
  stories,
  programName,
  title,
  showViewAll = true,
  viewAllHref = "/stories",
  onMarkHelpful,
}: ProgramStoriesProps) {
  const scrollContainerRef = useRef<HTMLDivElement>(null);
  const [canScrollLeft, setCanScrollLeft] = useState(false);
  const [canScrollRight, setCanScrollRight] = useState(false);

  const checkScroll = useCallback(() => {
    const container = scrollContainerRef.current;
    if (!container) return;

    setCanScrollLeft(container.scrollLeft > 0);
    setCanScrollRight(
      container.scrollLeft < container.scrollWidth - container.clientWidth - 10
    );
  }, []);

  const scroll = (direction: "left" | "right") => {
    const container = scrollContainerRef.current;
    if (!container) return;

    container.scrollBy({
      left: direction === "left" ? -300 : 300,
      behavior: "smooth",
    });
  };

  if (stories.length === 0) return null;

  return (
    <section className="py-8">
      <div className="flex items-center justify-between mb-6">
        <div>
          <h2 className="text-xl font-bold flex items-center gap-2">
            <Sparkles className="h-5 w-5 text-amber-500" />
            {title || `Success Stories from ${programName}`}
          </h2>
          <p className="text-stone-500 mt-1">
            Real experiences from people who received benefits
          </p>
        </div>
        {showViewAll && (
          <Link href={viewAllHref}>
            <Button variant="outline">View All Stories</Button>
          </Link>
        )}
      </div>

      <div className="relative">
        {/* Scroll buttons */}
        {canScrollLeft && (
          <button
            onClick={() => scroll("left")}
            className="absolute left-0 top-1/2 -translate-y-1/2 z-10 w-10 h-10 bg-white border rounded-full shadow-md flex items-center justify-center -ml-5"
          >
            <ChevronLeft className="h-5 w-5" />
          </button>
        )}
        {canScrollRight && (
          <button
            onClick={() => scroll("right")}
            className="absolute right-0 top-1/2 -translate-y-1/2 z-10 w-10 h-10 bg-white border rounded-full shadow-md flex items-center justify-center -mr-5"
          >
            <ChevronRight className="h-5 w-5" />
          </button>
        )}

        {/* Stories container */}
        <div
          ref={scrollContainerRef}
          onScroll={checkScroll}
          className="flex gap-4 overflow-x-auto scrollbar-hide pb-4 -mx-4 px-4"
        >
          {stories.map(story => (
            <div key={story.id} className="flex-shrink-0 w-72 md:w-80">
              <StoryCard
                story={story}
                variant="compact"
                onMarkHelpful={onMarkHelpful}
              />
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}
```

***

## Moderation Workflow

Stories go through moderation before publishing.

```mermaid theme={null}
stateDiagram-v2
    [*] --> Pending: User submits

    Pending --> Approved: Moderator approves
    Pending --> Rejected: Moderator rejects
    Pending --> Featured: Moderator features

    Approved --> Featured: Promote
    Featured --> Approved: Demote

    Rejected --> [*]: End
```

### Moderation Functions

```typescript theme={null}
// lib/dal/repositories/success-stories.repository.ts
export async function approveStory(
  storyId: string,
  moderatorId: string
): Promise<void> {
  await supabaseAdmin
    .from("success_stories")
    .update({
      status: "approved",
      moderated_by: moderatorId,
      moderated_at: new Date().toISOString(),
    })
    .eq("id", storyId);
}

export async function rejectStory(
  storyId: string,
  moderatorId: string,
  reason: string
): Promise<void> {
  await supabaseAdmin
    .from("success_stories")
    .update({
      status: "rejected",
      moderated_by: moderatorId,
      moderated_at: new Date().toISOString(),
      rejection_reason: reason,
    })
    .eq("id", storyId);
}

export async function featureStory(
  storyId: string,
  moderatorId: string
): Promise<void> {
  await supabaseAdmin
    .from("success_stories")
    .update({
      status: "featured",
      moderated_by: moderatorId,
      moderated_at: new Date().toISOString(),
    })
    .eq("id", storyId);
}
```

***

## Helpful Voting

Deduplicated voting with database function.

```sql theme={null}
CREATE OR REPLACE FUNCTION mark_story_helpful(
  p_story_id UUID,
  p_user_id TEXT DEFAULT NULL,
  p_session_id TEXT DEFAULT NULL
)
RETURNS BOOLEAN AS $$
DECLARE
  v_inserted BOOLEAN;
BEGIN
  -- Try to insert vote (will fail silently if duplicate)
  INSERT INTO story_helpful_votes (story_id, user_id, session_id)
  VALUES (p_story_id, p_user_id, p_session_id)
  ON CONFLICT DO NOTHING;

  GET DIAGNOSTICS v_inserted = ROW_COUNT;

  IF v_inserted THEN
    -- Increment helpful count
    UPDATE success_stories
    SET helpful_count = helpful_count + 1
    WHERE id = p_story_id
    AND status IN ('approved', 'featured');
  END IF;

  RETURN v_inserted > 0;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Notifications" icon="bell" href="/pathfinder/features/notifications">
    Notify users about success stories
  </Card>

  <Card title="Case Manager" icon="users" href="/pathfinder/features/case-manager">
    Share client success stories
  </Card>
</CardGroup>
