From 324a08e6267d4f759916196dad3f2f31ee2e49c6 Mon Sep 17 00:00:00 2001 From: v0 Date: Sun, 23 Nov 2025 01:27:35 +0000 Subject: [PATCH] Initialized repository for chat Kindness fund website Co-authored-by: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> --- README.md | 30 + app/actions.ts | 42 + app/globals.css | 137 ++ app/layout.tsx | 40 + app/page.tsx | 137 ++ components.json | 21 + components/allocation-dashboard.tsx | 133 ++ components/flow-visual.tsx | 104 + components/header.tsx | 56 + components/site-header.tsx | 42 + components/stream-canvas.tsx | 109 + components/submission-form.tsx | 134 ++ components/theme-provider.tsx | 11 + components/ui/button.tsx | 60 + components/ui/input.tsx | 21 + components/ui/label.tsx | 24 + components/ui/select.tsx | 185 ++ components/ui/slider.tsx | 63 + components/ui/sonner.tsx | 25 + components/ui/textarea.tsx | 18 + lib/data.ts | 57 + lib/mock-data.ts | 40 + lib/types.ts | 9 + lib/utils.ts | 6 + next.config.mjs | 12 + package.json | 73 + pnpm-lock.yaml | 3233 +++++++++++++++++++++++++++ postcss.config.mjs | 8 + public/apple-icon.png | Bin 0 -> 2626 bytes public/icon-dark-32x32.png | Bin 0 -> 585 bytes public/icon-light-32x32.png | Bin 0 -> 566 bytes public/icon.svg | 26 + public/placeholder-logo.png | Bin 0 -> 568 bytes public/placeholder-logo.svg | 1 + public/placeholder-user.jpg | Bin 0 -> 1635 bytes public/placeholder.jpg | Bin 0 -> 1064 bytes public/placeholder.svg | 1 + scripts/schema.sql | 50 + styles/globals.css | 125 ++ tsconfig.json | 27 + 40 files changed, 5060 insertions(+) create mode 100644 README.md create mode 100644 app/actions.ts create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 components.json create mode 100644 components/allocation-dashboard.tsx create mode 100644 components/flow-visual.tsx create mode 100644 components/header.tsx create mode 100644 components/site-header.tsx create mode 100644 components/stream-canvas.tsx create mode 100644 components/submission-form.tsx create mode 100644 components/theme-provider.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 lib/data.ts create mode 100644 lib/mock-data.ts create mode 100644 lib/types.ts create mode 100644 lib/utils.ts create mode 100644 next.config.mjs create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 postcss.config.mjs create mode 100644 public/apple-icon.png create mode 100644 public/icon-dark-32x32.png create mode 100644 public/icon-light-32x32.png create mode 100644 public/icon.svg create mode 100644 public/placeholder-logo.png create mode 100644 public/placeholder-logo.svg create mode 100644 public/placeholder-user.jpg create mode 100644 public/placeholder.jpg create mode 100644 public/placeholder.svg create mode 100644 scripts/schema.sql create mode 100644 styles/globals.css create mode 100644 tsconfig.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..ab44978 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Kindness fund website + +*Automatically synced with your [v0.app](https://v0.app) deployments* + +[![Deployed on Vercel](https://img.shields.io/badge/Deployed%20on-Vercel-black?style=for-the-badge&logo=vercel)](https://vercel.com/jeff-emmetts-projects/v0-kindness-fund-website) +[![Built with v0](https://img.shields.io/badge/Built%20with-v0.app-black?style=for-the-badge)](https://v0.app/chat/tPQAQ061Hxi) + +## Overview + +This repository will stay in sync with your deployed chats on [v0.app](https://v0.app). +Any changes you make to your deployed app will be automatically pushed to this repository from [v0.app](https://v0.app). + +## Deployment + +Your project is live at: + +**[https://vercel.com/jeff-emmetts-projects/v0-kindness-fund-website](https://vercel.com/jeff-emmetts-projects/v0-kindness-fund-website)** + +## Build your app + +Continue building your app on: + +**[https://v0.app/chat/tPQAQ061Hxi](https://v0.app/chat/tPQAQ061Hxi)** + +## How It Works + +1. Create and modify your project using [v0.app](https://v0.app) +2. Deploy your chats from the v0 interface +3. Changes are automatically pushed to this repository +4. Vercel deploys the latest version from this repository \ No newline at end of file diff --git a/app/actions.ts b/app/actions.ts new file mode 100644 index 0000000..be631e4 --- /dev/null +++ b/app/actions.ts @@ -0,0 +1,42 @@ +"use server" + +import { z } from "zod" +import { revalidatePath } from "next/cache" + +// Schema for validation +const actSchema = z.object({ + title: z.string().min(5, "Title must be at least 5 characters"), + description: z.string().min(20, "Please provide more detail in the description"), + impact: z.string().min(10, "Describe the impact in more detail"), + category: z.enum(["Community", "Environment", "Education", "Health", "Other"]), +}) + +export async function submitAct(prevState: any, formData: FormData) { + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 1000)) + + const validatedFields = actSchema.safeParse({ + title: formData.get("title"), + description: formData.get("description"), + impact: formData.get("impact"), + category: formData.get("category"), + }) + + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + message: "Please check your entries.", + } + } + + // In a real app, we would insert into the DB here + // await db.insert(acts).values(validatedFields.data) + + console.log("Act submitted:", validatedFields.data) + + revalidatePath("/") + return { + message: "Act released into the stream!", + success: true, + } +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..9634704 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,137 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + /* Neon Cyan - Stream */ + --primary: oklch(0.8 0.15 220); + --primary-foreground: oklch(0.1 0.02 240); + + /* Neon Magenta - Kindness */ + --secondary: oklch(0.7 0.25 330); + --secondary-foreground: oklch(0.985 0 0); + + /* Neon Lime - Growth/Allocation */ + --accent: oklch(0.85 0.25 140); + --accent-foreground: oklch(0.1 0.02 240); + + /* Deep Dark Background */ + --background: oklch(0.1 0.02 240); + --foreground: oklch(0.985 0 0); + + --card: oklch(0.15 0.02 240); + --card-foreground: oklch(0.985 0 0); + + --popover: oklch(0.15 0.02 240); + --popover-foreground: oklch(0.985 0 0); + + --muted: oklch(0.2 0.02 240); + --muted-foreground: oklch(0.7 0 0); + + --destructive: oklch(0.6 0.25 20); + --destructive-foreground: oklch(0.985 0 0); + + --border: oklch(0.25 0.02 240); + --input: oklch(0.25 0.02 240); + --ring: oklch(0.8 0.15 220); + + --radius: 0.75rem; + + /* Charts - Neon Palette */ + --chart-1: oklch(0.8 0.15 220); /* Cyan */ + --chart-2: oklch(0.7 0.25 330); /* Magenta */ + --chart-3: oklch(0.85 0.25 140); /* Lime */ + --chart-4: oklch(0.75 0.2 280); /* Purple */ + --chart-5: oklch(0.8 0.2 60); /* Orange */ + + --sidebar: oklch(0.15 0.02 240); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.8 0.15 220); + --sidebar-primary-foreground: oklch(0.1 0.02 240); + --sidebar-accent: oklch(0.2 0.02 240); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.25 0.02 240); + --sidebar-ring: oklch(0.8 0.15 220); +} + +.dark { + /* Dark mode is default/same */ + --background: oklch(0.1 0.02 240); + --foreground: oklch(0.985 0 0); + /* ... existing dark variables ... */ + /* Using same neon palette for dark mode */ + --primary: oklch(0.8 0.15 220); + --primary-foreground: oklch(0.1 0.02 240); + --secondary: oklch(0.7 0.25 330); + --secondary-foreground: oklch(0.985 0 0); + --accent: oklch(0.85 0.25 140); + --accent-foreground: oklch(0.1 0.02 240); + + --card: oklch(0.15 0.02 240); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.15 0.02 240); + --popover-foreground: oklch(0.985 0 0); + --muted: oklch(0.2 0.02 240); + --muted-foreground: oklch(0.7 0 0); + --border: oklch(0.25 0.02 240); + --input: oklch(0.25 0.02 240); +} + +@theme inline { + /* optional: --font-sans, --font-serif, --font-mono if they are applied in the layout.tsx */ + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +/* Added utility for glassmorphism */ +@utility glass { + @apply bg-white/5 backdrop-blur-md border border-white/10; +} + +@utility text-glow { + text-shadow: 0 0 10px var(--color-primary); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..3a2fd9b --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,40 @@ +import type React from "react" +import type { Metadata } from "next" +import { Geist, Geist_Mono } from "next/font/google" +import { Analytics } from "@vercel/analytics/next" +import { Toaster } from "@/components/ui/sonner" +import "./globals.css" + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}) + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}) + +export const metadata: Metadata = { + title: "dokindthings.fund", + description: "Community allocated streams for acts of kindness", + generator: 'v0.app' +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..d919155 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,137 @@ +import { Header } from "@/components/header" +import { StreamCanvas } from "@/components/stream-canvas" +import { Button } from "@/components/ui/button" +import { ArrowRight, Heart, Share2, Zap } from "lucide-react" +import { SubmissionForm } from "@/components/submission-form" +import { AllocationDashboard } from "@/components/allocation-dashboard" + +export default function Home() { + return ( +
+ +
+ +
+ {/* Hero Section */} +
+
+
+ + Live Fund Streaming Active +
+ +

+ Turn Kindness into
+ + Infinite Value + +

+ +

+ Submit your good deeds. The community directs the flow of funds to what matters most. Real-time rewards + for real-world impact. +

+ +
+ + +
+
+
+ + {/* Stats Grid */} +
+
+ {[ + { label: "Total Value Streamed", value: "$1,240,500", icon: Zap, color: "text-primary" }, + { label: "Acts Rewarded", value: "15,420", icon: Heart, color: "text-secondary" }, + { label: "Community Allocators", value: "8,930", icon: Share2, color: "text-accent" }, + ].map((stat, i) => ( +
+
+ +
+
+

{stat.label}

+

{stat.value}

+
+
+ ))} +
+
+ + {/* Submission Form Section */} +
+
+
+
+

+ Share Your
+ Light with the World +

+

+ Every act of kindness creates a ripple. When you share your story, you allow the community to + recognize that value and amplify it through direct funding streams. +

+ +
+ {[ + { title: "Transparency", desc: "All allocations are visible on the public stream." }, + { title: "Direct Impact", desc: "Funds flow directly to the acts deemed most valuable." }, + { title: "Community Governed", desc: "The collective decides where the stream flows." }, + ].map((item, i) => ( +
+
+
+

{item.title}

+

{item.desc}

+
+
+ ))} +
+
+ +
+ {/* Background blob for form */} +
+ +
+
+
+
+ + {/* Dashboard Section */} +
+
+ +
+
+

The Flow of Kindness

+

+ Control the stream. Adjust the sliders to direct real-time value to the acts that resonate with you. +

+
+ + +
+
+
+
+ ) +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..4ee62ee --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/allocation-dashboard.tsx b/components/allocation-dashboard.tsx new file mode 100644 index 0000000..2016779 --- /dev/null +++ b/components/allocation-dashboard.tsx @@ -0,0 +1,133 @@ +"use client" + +import { useState } from "react" +import { initialActs } from "@/lib/mock-data" +import { FlowVisual } from "./flow-visual" +import { Slider } from "@/components/ui/slider" +import { Button } from "@/components/ui/button" +import { ArrowUpRight, Droplets, TrendingUp } from "lucide-react" +import { cn } from "@/lib/utils" + +const categoryColors: Record = { + Environment: "var(--chart-3)", // Lime + Education: "var(--chart-5)", // Orange + Community: "var(--chart-1)", // Cyan + Health: "var(--chart-2)", // Magenta + Other: "var(--chart-4)", // Purple +} + +const categoryHex: Record = { + Environment: "#84cc16", // Lime-500 + Education: "#f97316", // Orange-500 + Community: "#06b6d4", // Cyan-500 + Health: "#ec4899", // Pink-500 + Other: "#a855f7", // Purple-500 +} + +export function AllocationDashboard() { + const [acts, setActs] = useState(initialActs) + const [totalFlow, setTotalFlow] = useState(acts.reduce((acc, act) => acc + act.allocation, 0)) + + const handleAllocationChange = (id: string, newValue: number[]) => { + const value = newValue[0] + setActs((prev) => prev.map((act) => (act.id === id ? { ...act, allocation: value } : act))) + // Recalculate total (in a real app, this might be capped) + setTotalFlow(acts.reduce((acc, act) => acc + (act.id === id ? value : act.allocation), 0)) + } + + return ( +
+ {/* Header / Source */} +
+
+
+ +
+

Community Stream Source

+
+ + + Total Active Flow: ${totalFlow.toLocaleString()}{" "} + / hr + +
+
+
+ + {/* Main Content Area with Visuals */} +
+ {/* Visual Layer */} +
+ ({ + id: act.id, + amount: act.allocation, + color: categoryHex[act.category] || "#ffffff", + }))} + containerHeight={300} // Visual height of the flow area + /> +
+ + {/* Spacing for the visual flow lines */} +
+
+

Flow Direction

+
+
+
+ + {/* Acts Grid (Destinations) */} +
+ {acts.map((act) => ( +
+ {/* Connection Point (Top) */} +
+ + {/* Card Header */} +
+ + {act.category} + + +
+ + {/* Content */} +

{act.title}

+

{act.description}

+ + {/* Allocation Control */} +
+
+ Flow Rate + ${act.allocation}/hr +
+ handleAllocationChange(act.id, val)} + className="[&>.range]:bg-primary" + /> +
+
+ ))} +
+
+
+ ) +} diff --git a/components/flow-visual.tsx b/components/flow-visual.tsx new file mode 100644 index 0000000..2feb28c --- /dev/null +++ b/components/flow-visual.tsx @@ -0,0 +1,104 @@ +"use client" + +import { useEffect, useRef, useState } from "react" +import type { JSX } from "react/jsx-runtime" // Import JSX to fix the undeclared variable error + +interface FlowVisualProps { + allocations: { id: string; amount: number; color: string }[] + containerHeight: number +} + +export function FlowVisual({ allocations, containerHeight }: FlowVisualProps) { + const [paths, setPaths] = useState([]) + const containerRef = useRef(null) + + // Calculate paths whenever allocations change or window resizes + useEffect(() => { + const updatePaths = () => { + if (!containerRef.current) return + + const containerWidth = containerRef.current.clientWidth + const sourceX = containerWidth / 2 + const sourceY = 0 // Top center + + const newPaths = allocations.map((alloc, index) => { + // Calculate target position based on index and total items + // We assume the target cards are distributed evenly at the bottom + // This is a visual approximation to match the grid layout + const totalItems = allocations.length + const sectionWidth = containerWidth / totalItems + const targetX = sectionWidth * index + sectionWidth / 2 + const targetY = containerHeight - 20 // Bottom, slightly offset + + // Bezier curve control points + const cp1x = sourceX + const cp1y = containerHeight * 0.5 + const cp2x = targetX + const cp2y = containerHeight * 0.5 + + // Stroke width based on allocation amount (normalized) + const maxAllocation = Math.max(...allocations.map((a) => a.amount)) + const strokeWidth = Math.max(2, (alloc.amount / maxAllocation) * 20) + + return ( + + {/* Glow Effect Path */} + + {/* Main Flow Path */} + + + + {/* Moving Particle */} + + + + + ) + }) + + setPaths(newPaths) + } + + updatePaths() + window.addEventListener("resize", updatePaths) + return () => window.removeEventListener("resize", updatePaths) + }, [allocations, containerHeight]) + + return ( +
+ + + + + + + + {paths} + +
+ ) +} diff --git a/components/header.tsx b/components/header.tsx new file mode 100644 index 0000000..95f599f --- /dev/null +++ b/components/header.tsx @@ -0,0 +1,56 @@ +import Link from "next/link" +import { Sparkles, Droplets } from "lucide-react" +import { Button } from "@/components/ui/button" + +export function Header() { + return ( +
+
+ +
+ +
+
+ + dokindthings.fund + + + + + +
+ + +
+
+
+ ) +} diff --git a/components/site-header.tsx b/components/site-header.tsx new file mode 100644 index 0000000..eead9a4 --- /dev/null +++ b/components/site-header.tsx @@ -0,0 +1,42 @@ +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { HeartHandshake } from "lucide-react" + +export function SiteHeader() { + return ( +
+
+
+ + + dokindthings.fund + +
+ +
+
+

Current Stream

+

$10,000.00

+
+ +
+
+
+ ) +} diff --git a/components/stream-canvas.tsx b/components/stream-canvas.tsx new file mode 100644 index 0000000..588caaa --- /dev/null +++ b/components/stream-canvas.tsx @@ -0,0 +1,109 @@ +"use client" + +import { useEffect, useRef } from "react" + +export function StreamCanvas() { + const canvasRef = useRef(null) + + useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + const ctx = canvas.getContext("2d") + if (!ctx) return + + let animationFrameId: number + let particles: Particle[] = [] + const particleCount = 100 + + class Particle { + x: number + y: number + vx: number + vy: number + size: number + color: string + + constructor(w: number, h: number) { + this.x = Math.random() * w + this.y = Math.random() * h + this.vx = Math.random() * 2 + 0.5 + this.vy = Math.sin(this.x * 0.01) * 0.5 + this.size = Math.random() * 3 + 1 + const colors = ["#00f3ff", "#ff00ff", "#ccff00"] // Cyan, Magenta, Lime + this.color = colors[Math.floor(Math.random() * colors.length)] + } + + update(w: number, h: number) { + this.x += this.vx + this.y += Math.sin(this.x * 0.005 + Date.now() * 0.001) * 0.5 + + if (this.x > w) { + this.x = 0 + this.y = Math.random() * h + } + } + + draw(ctx: CanvasRenderingContext2D) { + ctx.fillStyle = this.color + ctx.beginPath() + ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2) + ctx.fill() + + // Glow effect + ctx.shadowBlur = 10 + ctx.shadowColor = this.color + } + } + + const init = () => { + canvas.width = window.innerWidth + canvas.height = window.innerHeight + particles = [] + for (let i = 0; i < particleCount; i++) { + particles.push(new Particle(canvas.width, canvas.height)) + } + } + + const animate = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height) + + // Draw connecting lines for "stream" effect + ctx.lineWidth = 1 + + for (let i = 0; i < particles.length; i++) { + const p = particles[i] + p.update(canvas.width, canvas.height) + p.draw(ctx) + + // Connect nearby particles + for (let j = i + 1; j < particles.length; j++) { + const p2 = particles[j] + const dx = p.x - p2.x + const dy = p.y - p2.y + const dist = Math.sqrt(dx * dx + dy * dy) + + if (dist < 100) { + ctx.beginPath() + ctx.strokeStyle = `rgba(255, 255, 255, ${0.1 - dist / 1000})` + ctx.moveTo(p.x, p.y) + ctx.lineTo(p2.x, p2.y) + ctx.stroke() + } + } + } + + animationFrameId = requestAnimationFrame(animate) + } + + init() + window.addEventListener("resize", init) + animate() + + return () => { + window.removeEventListener("resize", init) + cancelAnimationFrame(animationFrameId) + } + }, []) + + return +} diff --git a/components/submission-form.tsx b/components/submission-form.tsx new file mode 100644 index 0000000..dca3d83 --- /dev/null +++ b/components/submission-form.tsx @@ -0,0 +1,134 @@ +"use client" + +import { useActionState, useState } from "react" +import { submitAct } from "@/app/actions" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Label } from "@/components/ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Loader2, Send, Sparkles } from "lucide-react" +import { toast } from "sonner" + +const categories = [ + { value: "Community", label: "Community Support", color: "bg-blue-500" }, + { value: "Environment", label: "Environmental Care", color: "bg-green-500" }, + { value: "Education", label: "Teaching & Learning", color: "bg-yellow-500" }, + { value: "Health", label: "Health & Wellness", color: "bg-red-500" }, + { value: "Other", label: "Other Kindness", color: "bg-purple-500" }, +] + +export function SubmissionForm() { + const [state, formAction, isPending] = useActionState(submitAct, null) + const [activeCategory, setActiveCategory] = useState("") + + if (state?.success) { + toast.success("Your act has been released into the stream!") + // Reset form state logic would go here, but for now we show a success card + return ( +
+
+ +
+

Released to the Stream!

+

+ Your act of kindness is now flowing through the community. Watch as others allocate value to it. +

+ +
+ ) + } + + return ( +
+ {/* Decorational glow */} +
+ +
+

Submit Kindness

+

Share what you've done. Let the community value it.

+
+ +
+
+ + + {state?.errors?.title &&

{state.errors.title}

} +
+ +
+
+ + + {state?.errors?.category &&

{state.errors.category}

} +
+ +
+ + + {state?.errors?.impact &&

{state.errors.impact}

} +
+
+ +
+ +