Initialized repository for chat Kindness fund website
Co-authored-by: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com>
|
|
@ -0,0 +1,30 @@
|
|||
# Kindness fund website
|
||||
|
||||
*Automatically synced with your [v0.app](https://v0.app) deployments*
|
||||
|
||||
[](https://vercel.com/jeff-emmetts-projects/v0-kindness-fund-website)
|
||||
[](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
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<html lang="en" className="dark">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen bg-background text-foreground`}
|
||||
>
|
||||
{children}
|
||||
<Analytics />
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="min-h-screen flex flex-col font-sans selection:bg-primary/30">
|
||||
<StreamCanvas />
|
||||
<Header />
|
||||
|
||||
<main className="flex-1 flex flex-col relative z-10 pt-16">
|
||||
{/* Hero Section */}
|
||||
<section className="relative py-20 md:py-32 px-4 overflow-hidden">
|
||||
<div className="container mx-auto flex flex-col items-center text-center space-y-8">
|
||||
<div className="inline-flex items-center rounded-full border border-primary/30 bg-primary/10 px-4 py-1.5 text-sm text-primary shadow-[0_0_20px_rgba(0,243,255,0.3)] backdrop-blur-sm animate-in fade-in slide-in-from-bottom-4 duration-1000">
|
||||
<Zap className="mr-2 h-4 w-4 fill-current" />
|
||||
<span className="font-medium">Live Fund Streaming Active</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold tracking-tighter max-w-5xl animate-in fade-in slide-in-from-bottom-8 duration-1000 delay-100">
|
||||
Turn Kindness into <br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary via-secondary to-accent animate-pulse">
|
||||
Infinite Value
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-xl md:text-2xl text-muted-foreground max-w-2xl leading-relaxed animate-in fade-in slide-in-from-bottom-8 duration-1000 delay-200">
|
||||
Submit your good deeds. The community directs the flow of funds to what matters most. Real-time rewards
|
||||
for real-world impact.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 w-full justify-center pt-8 animate-in fade-in slide-in-from-bottom-8 duration-1000 delay-300">
|
||||
<Button
|
||||
size="lg"
|
||||
className="text-lg h-14 px-8 rounded-full bg-primary hover:bg-primary/90 text-background font-bold shadow-[0_0_30px_rgba(0,243,255,0.4)] transition-all hover:scale-105"
|
||||
>
|
||||
Submit Act of Kindness
|
||||
<Heart className="ml-2 h-5 w-5 fill-current" />
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="text-lg h-14 px-8 rounded-full border-white/20 bg-white/5 hover:bg-white/10 hover:border-white/40 backdrop-blur-sm transition-all"
|
||||
>
|
||||
View Live Streams
|
||||
<ArrowRight className="ml-2 h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Stats Grid */}
|
||||
<section className="py-12 container mx-auto px-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{[
|
||||
{ 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) => (
|
||||
<div
|
||||
key={i}
|
||||
className="glass p-6 rounded-2xl flex items-center space-x-4 hover:bg-white/10 transition-colors cursor-default"
|
||||
>
|
||||
<div className={`p-3 rounded-xl bg-white/5 ${stat.color}`}>
|
||||
<stat.icon className="h-8 w-8" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground font-medium uppercase tracking-wider">{stat.label}</p>
|
||||
<p className="text-3xl font-bold tracking-tight">{stat.value}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Submission Form Section */}
|
||||
<section id="submit" className="py-24 relative">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div>
|
||||
<h2 className="text-4xl md:text-5xl font-bold mb-6 leading-tight">
|
||||
Share Your <br />
|
||||
<span className="text-primary">Light</span> with the World
|
||||
</h2>
|
||||
<p className="text-xl text-muted-foreground mb-8 leading-relaxed">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{ 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) => (
|
||||
<div key={i} className="flex items-start space-x-4">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-primary mt-2.5" />
|
||||
<div>
|
||||
<h4 className="font-bold text-lg">{item.title}</h4>
|
||||
<p className="text-muted-foreground">{item.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
{/* Background blob for form */}
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-primary/20 to-secondary/20 rounded-3xl blur-3xl -z-10 transform rotate-3 scale-105" />
|
||||
<SubmissionForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Dashboard Section */}
|
||||
<section id="dashboard" className="py-12 bg-muted/30 border-t relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(0,243,255,0.05),transparent_70%)]" />
|
||||
|
||||
<div className="container px-4 md:px-6 relative z-10">
|
||||
<div className="text-center mb-12 space-y-4">
|
||||
<h2 className="text-4xl font-bold tracking-tight">The Flow of Kindness</h2>
|
||||
<p className="text-muted-foreground text-lg max-w-2xl mx-auto">
|
||||
Control the stream. Adjust the sliders to direct real-time value to the acts that resonate with you.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<AllocationDashboard />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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<string, string> = {
|
||||
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<string, string> = {
|
||||
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 (
|
||||
<div className="w-full bg-background/50 backdrop-blur-xl rounded-3xl border border-white/10 overflow-hidden flex flex-col min-h-[800px] relative">
|
||||
{/* Header / Source */}
|
||||
<div className="relative z-20 p-8 border-b border-white/5 bg-black/20 text-center">
|
||||
<div className="inline-flex flex-col items-center justify-center">
|
||||
<div className="w-16 h-16 rounded-full bg-primary/20 flex items-center justify-center mb-4 ring-4 ring-primary/10 animate-pulse">
|
||||
<Droplets className="w-8 h-8 text-primary" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">Community Stream Source</h2>
|
||||
<div className="flex items-center space-x-2 mt-2 text-muted-foreground">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>
|
||||
Total Active Flow: <span className="text-primary font-mono font-bold">${totalFlow.toLocaleString()}</span>{" "}
|
||||
/ hr
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area with Visuals */}
|
||||
<div className="flex-1 relative p-8">
|
||||
{/* Visual Layer */}
|
||||
<div className="absolute inset-0 top-0 pointer-events-none">
|
||||
<FlowVisual
|
||||
allocations={acts.map((act) => ({
|
||||
id: act.id,
|
||||
amount: act.allocation,
|
||||
color: categoryHex[act.category] || "#ffffff",
|
||||
}))}
|
||||
containerHeight={300} // Visual height of the flow area
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Spacing for the visual flow lines */}
|
||||
<div className="h-[250px] w-full flex items-center justify-center pointer-events-none">
|
||||
<div className="text-center opacity-30">
|
||||
<p className="text-sm tracking-[0.5em] uppercase">Flow Direction</p>
|
||||
<div className="h-16 w-[1px] bg-gradient-to-b from-white/50 to-transparent mx-auto mt-4" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Acts Grid (Destinations) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 relative z-10">
|
||||
{acts.map((act) => (
|
||||
<div
|
||||
key={act.id}
|
||||
className="group relative flex flex-col bg-card/50 border border-white/5 hover:border-primary/30 rounded-2xl p-5 transition-all duration-300 hover:-translate-y-1 hover:shadow-lg hover:shadow-primary/5"
|
||||
>
|
||||
{/* Connection Point (Top) */}
|
||||
<div className="absolute -top-1.5 left-1/2 -translate-x-1/2 w-3 h-3 rounded-full bg-background border-2 border-white/10 group-hover:border-primary transition-colors" />
|
||||
|
||||
{/* Card Header */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-bold px-2 py-1 rounded-full bg-white/5 border border-white/5",
|
||||
`text-[${categoryHex[act.category]}]`, // Dynamic color hint
|
||||
)}
|
||||
style={{ color: categoryHex[act.category] }}
|
||||
>
|
||||
{act.category}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<h3 className="font-bold text-lg leading-tight mb-2 min-h-[3rem]">{act.title}</h3>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2 mb-4 flex-1">{act.description}</p>
|
||||
|
||||
{/* Allocation Control */}
|
||||
<div className="mt-auto space-y-3 bg-black/20 p-3 rounded-xl">
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-muted-foreground">Flow Rate</span>
|
||||
<span className="font-mono font-bold text-primary">${act.allocation}/hr</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[act.allocation]}
|
||||
max={1500}
|
||||
step={10}
|
||||
onValueChange={(val) => handleAllocationChange(act.id, val)}
|
||||
className="[&>.range]:bg-primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<JSX.Element[]>([])
|
||||
const containerRef = useRef<HTMLDivElement>(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 (
|
||||
<g key={alloc.id}>
|
||||
{/* Glow Effect Path */}
|
||||
<path
|
||||
d={`M ${sourceX} ${sourceY} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${targetX} ${targetY}`}
|
||||
fill="none"
|
||||
stroke={alloc.color}
|
||||
strokeWidth={strokeWidth + 8}
|
||||
strokeOpacity="0.1"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
{/* Main Flow Path */}
|
||||
<path
|
||||
d={`M ${sourceX} ${sourceY} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${targetX} ${targetY}`}
|
||||
fill="none"
|
||||
stroke={alloc.color}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
className="opacity-80"
|
||||
>
|
||||
<animate
|
||||
attributeName="stroke-dasharray"
|
||||
from={`0, ${strokeWidth * 20}`}
|
||||
to={`${strokeWidth * 20}, 0`}
|
||||
dur={`${30000 / (alloc.amount + 100)}s`}
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
{/* Moving Particle */}
|
||||
<circle r={strokeWidth / 2 + 2} fill="white">
|
||||
<animateMotion
|
||||
dur={`${20000 / (alloc.amount + 100)}s`}
|
||||
repeatCount="indefinite"
|
||||
path={`M ${sourceX} ${sourceY} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${targetX} ${targetY}`}
|
||||
/>
|
||||
</circle>
|
||||
</g>
|
||||
)
|
||||
})
|
||||
|
||||
setPaths(newPaths)
|
||||
}
|
||||
|
||||
updatePaths()
|
||||
window.addEventListener("resize", updatePaths)
|
||||
return () => window.removeEventListener("resize", updatePaths)
|
||||
}, [allocations, containerHeight])
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="absolute inset-0 pointer-events-none z-0">
|
||||
<svg width="100%" height="100%" className="overflow-visible">
|
||||
<defs>
|
||||
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="5" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
||||
</filter>
|
||||
</defs>
|
||||
{paths}
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<header className="fixed top-0 w-full z-50 glass border-b border-white/10">
|
||||
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center space-x-2 group">
|
||||
<div className="relative">
|
||||
<Droplets className="h-6 w-6 text-primary animate-pulse" />
|
||||
<div className="absolute inset-0 bg-primary/50 blur-md rounded-full opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
<span className="font-bold text-xl tracking-tight">
|
||||
dokindthings<span className="text-primary">.fund</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden md:flex items-center space-x-6">
|
||||
<Link
|
||||
href="#submit"
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Submit Kindness
|
||||
</Link>
|
||||
<Link
|
||||
href="#dashboard"
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
Live Stream
|
||||
</Link>
|
||||
<Link
|
||||
href="#about"
|
||||
className="text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
How it Works
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="hidden sm:flex border-primary/20 hover:bg-primary/10 hover:text-primary bg-transparent"
|
||||
>
|
||||
Connect Wallet
|
||||
</Button>
|
||||
<Button size="sm" className="bg-gradient-to-r from-primary to-secondary hover:opacity-90 border-0">
|
||||
<Sparkles className="mr-2 h-4 w-4" />
|
||||
Start Flow
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import Link from "next/link"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { HeartHandshake } from "lucide-react"
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur-xs supports-backdrop-filter:bg-background/60">
|
||||
<div className="container flex h-16 items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<HeartHandshake className="h-6 w-6 text-primary" />
|
||||
<span className="text-xl font-bold tracking-tight text-primary">
|
||||
dokindthings<span className="text-secondary">.fund</span>
|
||||
</span>
|
||||
</div>
|
||||
<nav className="hidden md:flex items-center gap-6 text-sm font-medium text-muted-foreground">
|
||||
<Link href="#" className="hover:text-foreground transition-colors">
|
||||
The Stream
|
||||
</Link>
|
||||
<Link href="#" className="hover:text-foreground transition-colors">
|
||||
Impact
|
||||
</Link>
|
||||
<Link href="#" className="hover:text-foreground transition-colors">
|
||||
Community
|
||||
</Link>
|
||||
</nav>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="hidden sm:block text-sm text-right">
|
||||
<p className="text-muted-foreground">Current Stream</p>
|
||||
<p className="font-bold text-primary">$10,000.00</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="rounded-full bg-gradient-to-r from-primary to-secondary hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Connect Wallet
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
export function StreamCanvas() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(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 <canvas ref={canvasRef} className="fixed inset-0 -z-10 bg-[#0a0a0a]" />
|
||||
}
|
||||
|
|
@ -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<string>("")
|
||||
|
||||
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 (
|
||||
<div className="glass p-8 rounded-2xl text-center space-y-6 animate-in zoom-in duration-500 border-primary/30 shadow-[0_0_50px_rgba(0,243,255,0.2)]">
|
||||
<div className="mx-auto w-20 h-20 bg-primary/20 rounded-full flex items-center justify-center mb-4">
|
||||
<Sparkles className="w-10 h-10 text-primary animate-pulse" />
|
||||
</div>
|
||||
<h3 className="text-2xl font-bold text-foreground">Released to the Stream!</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Your act of kindness is now flowing through the community. Watch as others allocate value to it.
|
||||
</p>
|
||||
<Button onClick={() => window.location.reload()} variant="outline" className="mt-4">
|
||||
Submit Another Act
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="glass p-8 rounded-2xl border-white/10 relative overflow-hidden group hover:border-primary/30 transition-colors duration-500">
|
||||
{/* Decorational glow */}
|
||||
<div className="absolute -top-20 -right-20 w-40 h-40 bg-primary/20 rounded-full blur-3xl group-hover:bg-primary/30 transition-all duration-700" />
|
||||
|
||||
<div className="relative z-10 mb-8">
|
||||
<h2 className="text-3xl font-bold tracking-tight mb-2">Submit Kindness</h2>
|
||||
<p className="text-muted-foreground">Share what you've done. Let the community value it.</p>
|
||||
</div>
|
||||
|
||||
<form action={formAction} className="space-y-6 relative z-10">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="title">What did you do?</Label>
|
||||
<Input
|
||||
id="title"
|
||||
name="title"
|
||||
placeholder="e.g., Cleaned up the local park..."
|
||||
required
|
||||
className="bg-white/5 border-white/10 focus:border-primary/50 focus:ring-primary/20 transition-all"
|
||||
/>
|
||||
{state?.errors?.title && <p className="text-sm text-destructive">{state.errors.title}</p>}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Select name="category" required onValueChange={setActiveCategory}>
|
||||
<SelectTrigger className="bg-white/5 border-white/10 focus:border-primary/50 focus:ring-primary/20 transition-all">
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{categories.map((cat) => (
|
||||
<SelectItem key={cat.value} value={cat.value}>
|
||||
<div className="flex items-center">
|
||||
<div className={`w-2 h-2 rounded-full mr-2 ${cat.color}`} />
|
||||
{cat.label}
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{state?.errors?.category && <p className="text-sm text-destructive">{state.errors.category}</p>}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="impact">Who did it help?</Label>
|
||||
<Input
|
||||
id="impact"
|
||||
name="impact"
|
||||
placeholder="e.g., 50 families in the neighborhood"
|
||||
required
|
||||
className="bg-white/5 border-white/10 focus:border-primary/50 focus:ring-primary/20 transition-all"
|
||||
/>
|
||||
{state?.errors?.impact && <p className="text-sm text-destructive">{state.errors.impact}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">The Story</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
placeholder="Tell us the details of your act..."
|
||||
required
|
||||
className="min-h-[120px] bg-white/5 border-white/10 focus:border-primary/50 focus:ring-primary/20 transition-all"
|
||||
/>
|
||||
{state?.errors?.description && <p className="text-sm text-destructive">{state.errors.description}</p>}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
className="w-full h-12 text-lg font-bold bg-gradient-to-r from-primary to-secondary hover:opacity-90 transition-all shadow-lg shadow-primary/20"
|
||||
disabled={isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
||||
Releasing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Release into Stream
|
||||
<Send className="ml-2 h-5 w-5" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ThemeProvider as NextThemesProvider,
|
||||
type ThemeProviderProps,
|
||||
} from 'next-themes'
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
outline:
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost:
|
||||
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-sm': 'size-8',
|
||||
'icon-lg': 'size-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SelectPrimitive from '@radix-ui/react-select'
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = 'default',
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: 'sm' | 'default'
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = 'popper',
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn('text-muted-foreground px-2 py-1.5 text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SliderPrimitive from '@radix-ui/react-slider'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Slider({
|
||||
className,
|
||||
defaultValue,
|
||||
value,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SliderPrimitive.Root>) {
|
||||
const _values = React.useMemo(
|
||||
() =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
: Array.isArray(defaultValue)
|
||||
? defaultValue
|
||||
: [min, max],
|
||||
[value, defaultValue, min, max],
|
||||
)
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
data-slot="slider"
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
min={min}
|
||||
max={max}
|
||||
className={cn(
|
||||
'relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
data-slot="slider-track"
|
||||
className={
|
||||
'bg-muted relative grow overflow-hidden rounded-full data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5'
|
||||
}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
data-slot="slider-range"
|
||||
className={
|
||||
'bg-primary absolute data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full'
|
||||
}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{Array.from({ length: _values.length }, (_, index) => (
|
||||
<SliderPrimitive.Thumb
|
||||
data-slot="slider-thumb"
|
||||
key={index}
|
||||
className="border-primary ring-ring/50 block size-4 shrink-0 rounded-full border bg-white shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Slider }
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
'use client'
|
||||
|
||||
import { useTheme } from 'next-themes'
|
||||
import { Toaster as Sonner, ToasterProps } from 'sonner'
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = 'system' } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps['theme']}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
'--normal-bg': 'var(--popover)',
|
||||
'--normal-text': 'var(--popover-foreground)',
|
||||
'--normal-border': 'var(--border)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
export interface Act {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
author: string
|
||||
currentAllocation: number // Percentage or amount
|
||||
color?: string
|
||||
}
|
||||
|
||||
export const TOTAL_POOL = 10000 // $10,000 total stream
|
||||
|
||||
export const initialActs: Act[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "Community Garden Cleanup",
|
||||
description:
|
||||
"Organized a weekend cleanup for the local community garden, planting 50 new flowers and removing 20 bags of trash.",
|
||||
author: "Sarah J.",
|
||||
currentAllocation: 2500,
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Senior Tech Support",
|
||||
description:
|
||||
"Spent 3 weekends teaching seniors at the local library how to video call their families and use tablets.",
|
||||
author: "Mike T.",
|
||||
currentAllocation: 1500,
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Homeless Shelter Meals",
|
||||
description: "Cooked and delivered 50 hot meals to the downtown shelter on a rainy Tuesday night.",
|
||||
author: "Elena R.",
|
||||
currentAllocation: 3000,
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "Stray Cat Rescue",
|
||||
description:
|
||||
"Rescued a litter of 4 kittens found under a porch, got them vet care, and found them all forever homes.",
|
||||
author: "Davide B.",
|
||||
currentAllocation: 1000,
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
title: "Free Math Tutoring",
|
||||
description:
|
||||
"Provided 20 hours of free math tutoring to underprivileged high school students preparing for finals.",
|
||||
author: "Jenny W.",
|
||||
currentAllocation: 2000,
|
||||
color: "hsl(var(--chart-5))",
|
||||
},
|
||||
]
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import type { Act } from "./types"
|
||||
|
||||
export const initialActs: Act[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "Community Garden Cleanup",
|
||||
description: "Cleared 500lbs of trash and planted native wildflowers in the downtown empty lot.",
|
||||
impact: "Local residents and pollinators",
|
||||
category: "Environment",
|
||||
createdAt: new Date(),
|
||||
allocation: 450,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "After-school Coding Club",
|
||||
description: "Providing free python lessons to 20 middle school students twice a week.",
|
||||
impact: "20 students + families",
|
||||
category: "Education",
|
||||
createdAt: new Date(),
|
||||
allocation: 890,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Senior Grocery Delivery",
|
||||
description: "Delivered weekly groceries to 15 homebound seniors during the winter storm.",
|
||||
impact: "15 seniors",
|
||||
category: "Community",
|
||||
createdAt: new Date(),
|
||||
allocation: 320,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "Free Mental Health Workshop",
|
||||
description: "Hosted a weekend workshop on anxiety management techniques open to all.",
|
||||
impact: "45 attendees",
|
||||
category: "Health",
|
||||
createdAt: new Date(),
|
||||
allocation: 600,
|
||||
},
|
||||
]
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export interface Act {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
impact: string
|
||||
category: "Community" | "Environment" | "Education" | "Health" | "Other"
|
||||
createdAt: Date
|
||||
allocation: number // The amount of "flow" allocated
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"name": "my-v0-project",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"lint": "eslint .",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@radix-ui/react-accordion": "1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "1.1.4",
|
||||
"@radix-ui/react-aspect-ratio": "1.1.1",
|
||||
"@radix-ui/react-avatar": "1.1.2",
|
||||
"@radix-ui/react-checkbox": "1.1.3",
|
||||
"@radix-ui/react-collapsible": "1.1.2",
|
||||
"@radix-ui/react-context-menu": "2.2.4",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||
"@radix-ui/react-hover-card": "1.1.4",
|
||||
"@radix-ui/react-label": "2.1.1",
|
||||
"@radix-ui/react-menubar": "1.1.4",
|
||||
"@radix-ui/react-navigation-menu": "1.2.3",
|
||||
"@radix-ui/react-popover": "1.1.4",
|
||||
"@radix-ui/react-progress": "1.1.1",
|
||||
"@radix-ui/react-radio-group": "1.2.2",
|
||||
"@radix-ui/react-scroll-area": "1.2.2",
|
||||
"@radix-ui/react-select": "2.1.4",
|
||||
"@radix-ui/react-separator": "1.1.1",
|
||||
"@radix-ui/react-slider": "1.2.2",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-switch": "1.1.2",
|
||||
"@radix-ui/react-tabs": "1.1.2",
|
||||
"@radix-ui/react-toast": "1.2.4",
|
||||
"@radix-ui/react-toggle": "1.1.1",
|
||||
"@radix-ui/react-toggle-group": "1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.6",
|
||||
"@vercel/analytics": "latest",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"date-fns": "4.1.0",
|
||||
"embla-carousel-react": "8.5.1",
|
||||
"input-otp": "1.4.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"next": "16.0.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "latest",
|
||||
"react-day-picker": "9.8.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "2.15.4",
|
||||
"sonner": "latest",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.9",
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"postcss": "^8.5",
|
||||
"tailwindcss": "^4.1.9",
|
||||
"tw-animate-css": "1.3.3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 585 B |
|
After Width: | Height: | Size: 566 B |
|
|
@ -0,0 +1,26 @@
|
|||
<svg width="180" height="180" viewBox="0 0 180 180" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<style>
|
||||
@media (prefers-color-scheme: light) {
|
||||
.background { fill: black; }
|
||||
.foreground { fill: white; }
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.background { fill: white; }
|
||||
.foreground { fill: black; }
|
||||
}
|
||||
</style>
|
||||
<g clip-path="url(#clip0_7960_43945)">
|
||||
<rect class="background" width="180" height="180" rx="37" />
|
||||
<g style="transform: scale(95%); transform-origin: center">
|
||||
<path class="foreground"
|
||||
d="M101.141 53H136.632C151.023 53 162.689 64.6662 162.689 79.0573V112.904H148.112V79.0573C148.112 78.7105 148.098 78.3662 148.072 78.0251L112.581 112.898C112.701 112.902 112.821 112.904 112.941 112.904H148.112V126.672H112.941C98.5504 126.672 86.5638 114.891 86.5638 100.5V66.7434H101.141V100.5C101.141 101.15 101.191 101.792 101.289 102.422L137.56 66.7816C137.255 66.7563 136.945 66.7434 136.632 66.7434H101.141V53Z" />
|
||||
<path class="foreground"
|
||||
d="M65.2926 124.136L14 66.7372H34.6355L64.7495 100.436V66.7372H80.1365V118.47C80.1365 126.278 70.4953 129.958 65.2926 124.136Z" />
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_7960_43945">
|
||||
<rect width="180" height="180" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 568 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1,50 @@
|
|||
-- Dropping existing tables to ensure clean slate for new schema structure
|
||||
DROP TABLE IF EXISTS allocations;
|
||||
DROP TABLE IF EXISTS acts_of_kindness;
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS stream_config;
|
||||
DROP TYPE IF EXISTS act_category;
|
||||
|
||||
-- Updated schema to match new application requirements
|
||||
|
||||
-- Enable UUID extension
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Categories Enum
|
||||
CREATE TYPE act_category AS ENUM ('Community', 'Environment', 'Education', 'Health', 'Other');
|
||||
|
||||
-- Acts of Kindness Table
|
||||
CREATE TABLE IF NOT EXISTS acts (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
impact TEXT NOT NULL,
|
||||
category act_category NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
user_wallet_address TEXT, -- Optional: if wallet connection is implemented
|
||||
status TEXT DEFAULT 'active', -- active, archived, flagged
|
||||
|
||||
-- Metrics
|
||||
total_allocation NUMERIC DEFAULT 0, -- Total funds allocated
|
||||
stream_velocity NUMERIC DEFAULT 0 -- Current rate of funding
|
||||
);
|
||||
|
||||
-- Allocations (Votes/Streams)
|
||||
CREATE TABLE IF NOT EXISTS allocations (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
act_id UUID REFERENCES acts(id) ON DELETE CASCADE,
|
||||
allocator_wallet_address TEXT, -- Who allocated
|
||||
amount NUMERIC NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create index for faster querying of active acts
|
||||
CREATE INDEX idx_acts_created_at ON acts(created_at DESC);
|
||||
CREATE INDEX idx_acts_category ON acts(category);
|
||||
|
||||
-- Added seed data for the demo
|
||||
INSERT INTO acts (title, description, impact, category, total_allocation) VALUES
|
||||
('Community Garden Cleanup', 'Cleared 500lbs of trash and planted native wildflowers in the downtown empty lot.', 'Local residents and pollinators', 'Environment', 450),
|
||||
('After-school Coding Club', 'Providing free python lessons to 20 middle school students twice a week.', '20 students + families', 'Education', 890),
|
||||
('Senior Grocery Delivery', 'Delivered weekly groceries to 15 homebound seniors during the winter storm.', '15 seniors', 'Community', 320),
|
||||
('Free Mental Health Workshop', 'Hosted a weekend workshop on anxiety management techniques open to all.', '45 attendees', 'Health', 600);
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: 'Geist', 'Geist Fallback';
|
||||
--font-mono: 'Geist Mono', 'Geist Mono Fallback';
|
||||
--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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"target": "ES6",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||