Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
775 changes: 775 additions & 0 deletions frontend/package-lock.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions frontend/src/app/dashboard/contributions/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default function ContributionsPage() {
return (
<div className="space-y-8">
<header>
<h1 className="text-3xl font-bold tracking-tight text-white">
Contributions
</h1>
<p className="mt-2 text-slate-400">
Track your contribution history across pools.
</p>
</header>
</div>
);
}
13 changes: 13 additions & 0 deletions frontend/src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DashboardShell } from "@/components/dashboard/DashboardShell";

export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="-mt-28 min-h-screen">
<DashboardShell>{children}</DashboardShell>
</div>
);
}
45 changes: 45 additions & 0 deletions frontend/src/app/dashboard/my-pools/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Link from "next/link";

export default function MyPoolsPage() {
return (
<div className="space-y-8">
<header className="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight text-white">
My Pools
</h1>
<p className="mt-2 text-slate-400">
View and manage your donation pools.
</p>
</div>
<Link
href="/dashboard/pools/create"
className="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-emerald-500 to-cyan-500 px-5 py-2.5 text-sm font-semibold text-white shadow-lg shadow-emerald-500/30 transition-all duration-200 hover:brightness-110 hover:shadow-emerald-500/50 active:scale-95"
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
Create Pool
</Link>
</header>

<section className="rounded-xl border border-slate-800/80 bg-slate-900/50 p-8 text-center backdrop-blur-sm">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-slate-800 text-slate-500">
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
</svg>
</div>
<h3 className="mt-4 text-base font-semibold text-white">No pools yet</h3>
<p className="mt-1 text-sm text-slate-500">
Get started by creating your first donation pool.
</p>
<Link
href="/dashboard/pools/create"
className="mt-5 inline-flex items-center gap-2 rounded-lg border border-slate-700 bg-slate-800 px-4 py-2 text-sm font-medium text-slate-300 transition hover:border-emerald-500/40 hover:text-emerald-400"
>
Create your first pool →
</Link>
</section>
</div>
);
}
21 changes: 21 additions & 0 deletions frontend/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export default function DashboardOverviewPage() {
return (
<div className="space-y-8">
<header>
<h1 className="text-3xl font-bold tracking-tight text-white">
Overview
</h1>
<p className="mt-2 text-slate-400">
Welcome to your Nevo dashboard. Track your pools and contributions here.
</p>
</header>

<section className="rounded-xl border border-slate-800/80 bg-slate-900/50 p-6 backdrop-blur-sm">
<h2 className="text-lg font-semibold text-white">Quick stats</h2>
<p className="mt-2 text-sm text-slate-500">
Stats and charts will appear in Part 2.
</p>
</section>
</div>
);
}
5 changes: 5 additions & 0 deletions frontend/src/app/dashboard/pools/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CreatePoolStepper } from "@/components/dashboard/CreatePoolStepper";

export default function CreatePoolPage() {
return <CreatePoolStepper />;
}
14 changes: 14 additions & 0 deletions frontend/src/app/dashboard/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default function SettingsPage() {
return (
<div className="space-y-8">
<header>
<h1 className="text-3xl font-bold tracking-tight text-white">
Settings
</h1>
<p className="mt-2 text-slate-400">
Manage your account and preferences.
</p>
</header>
</div>
);
}
247 changes: 247 additions & 0 deletions frontend/src/components/dashboard/CreatePoolStepper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { BasicInfoStep } from "./steps/BasicInfoStep";
import { FinancialsStep } from "./steps/FinancialsStep";
import { ReviewStep } from "./steps/ReviewStep";
import { cn } from "@/lib/utils";

export interface FormData {
// Step 1
poolName: string;
category: string;
description: string;
endDate: string;
// Step 2
fundingGoal: string;
minContribution: string;
beneficiaryWallet: string;
visibility: "Public" | "Private";
}

const INITIAL_FORM: FormData = {
poolName: "",
category: "",
description: "",
endDate: "",
fundingGoal: "",
minContribution: "",
beneficiaryWallet: "",
visibility: "Public",
};

const STEPS = [
{ id: 1, label: "Basic Info" },
{ id: 2, label: "Financials" },
{ id: 3, label: "Review" },
];

function isStep1Valid(data: FormData) {
return data.poolName.trim() !== "" && data.category !== "" && data.description.trim() !== "";
}

function isStep2Valid(data: FormData) {
return data.fundingGoal !== "" && Number(data.fundingGoal) > 0 && data.beneficiaryWallet.trim() !== "";
}

export function CreatePoolStepper() {
const router = useRouter();
const [currentStep, setCurrentStep] = useState(1);
const [formData, setFormData] = useState<FormData>(INITIAL_FORM);
const [submitting, setSubmitting] = useState(false);
const [direction, setDirection] = useState<"forward" | "backward">("forward");
const [animating, setAnimating] = useState(false);

const updateForm = (updates: Partial<FormData>) => {
setFormData((prev) => ({ ...prev, ...updates }));
};

const canProceed =
currentStep === 1
? isStep1Valid(formData)
: currentStep === 2
? isStep2Valid(formData)
: true;

const transitionToStep = (next: number, dir: "forward" | "backward") => {
setDirection(dir);
setAnimating(true);
setTimeout(() => {
setCurrentStep(next);
setAnimating(false);
}, 220);
};

const handleNext = () => {
if (currentStep < 3 && canProceed) transitionToStep(currentStep + 1, "forward");
};

const handleBack = () => {
if (currentStep > 1) transitionToStep(currentStep - 1, "backward");
};

const handleSubmit = async () => {
setSubmitting(true);
// Simulate async submission
await new Promise((r) => setTimeout(r, 1500));
setSubmitting(false);
router.push("/dashboard/my-pools");
};

const progressPercent = ((currentStep - 1) / (STEPS.length - 1)) * 100;

return (
<div className="mx-auto max-w-2xl space-y-8">
{/* Header */}
<div>
<h1 className="text-3xl font-bold tracking-tight text-white">Create a New Pool</h1>
<p className="mt-2 text-slate-400">
Complete each step to launch your donation pool on the Stellar blockchain.
</p>
</div>

{/* Progress bar + Step indicators */}
<div className="space-y-4">
{/* Step circles */}
<div className="flex items-center">
{STEPS.map((step, idx) => {
const isDone = currentStep > step.id;
const isActive = currentStep === step.id;
return (
<div key={step.id} className={cn("flex items-center", idx < STEPS.length - 1 && "flex-1")}>
<div className="flex flex-col items-center gap-1.5">
{/* Circle */}
<div
className={cn(
"flex h-9 w-9 items-center justify-center rounded-full border-2 text-sm font-bold transition-all duration-300",
isDone
? "border-emerald-500 bg-emerald-500 text-white shadow-lg shadow-emerald-500/30"
: isActive
? "border-emerald-400 bg-emerald-400/10 text-emerald-400 shadow-lg shadow-emerald-400/20"
: "border-slate-700 bg-slate-900 text-slate-500"
)}
>
{isDone ? (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
) : (
step.id
)}
</div>
{/* Label */}
<span
className={cn(
"text-xs font-medium transition-colors duration-200",
isActive ? "text-emerald-400" : isDone ? "text-emerald-500/70" : "text-slate-500"
)}
>
{step.label}
</span>
</div>

{/* Connector line */}
{idx < STEPS.length - 1 && (
<div className="relative mx-3 mt-[-20px] h-0.5 flex-1 overflow-hidden rounded-full bg-slate-800">
<div
className="absolute inset-y-0 left-0 rounded-full bg-gradient-to-r from-emerald-500 to-cyan-500 transition-all duration-500 ease-out"
style={{ width: isDone ? "100%" : "0%" }}
/>
</div>
)}
</div>
);
})}
</div>

{/* Thin progress bar */}
<div className="h-1 w-full overflow-hidden rounded-full bg-slate-800">
<div
className="h-full rounded-full bg-gradient-to-r from-emerald-500 to-cyan-400 transition-all duration-500 ease-out shadow-[0_0_8px_rgba(16,185,129,0.5)]"
style={{ width: `${progressPercent === 0 ? 6 : progressPercent}%` }}
/>
</div>
<p className="text-right text-xs text-slate-500">
Step {currentStep} of {STEPS.length}
</p>
</div>

{/* Step content card */}
<div
className={cn(
"rounded-2xl border border-slate-800/80 bg-slate-900/50 p-6 backdrop-blur-sm transition-all duration-220 lg:p-8",
animating
? direction === "forward"
? "translate-y-2 opacity-0"
: "-translate-y-2 opacity-0"
: "translate-y-0 opacity-100"
)}
>
{currentStep === 1 && <BasicInfoStep formData={formData} onChange={updateForm} />}
{currentStep === 2 && <FinancialsStep formData={formData} onChange={updateForm} />}
{currentStep === 3 && <ReviewStep formData={formData} />}
</div>

{/* Navigation footer */}
<div className="flex items-center justify-between gap-4 pt-2">
<button
onClick={handleBack}
disabled={currentStep === 1}
className={cn(
"flex items-center gap-2 rounded-xl border border-slate-700/80 bg-slate-900 px-5 py-3 text-sm font-medium transition-all duration-200",
currentStep === 1
? "cursor-not-allowed opacity-30 text-slate-500"
: "text-slate-300 hover:border-slate-600 hover:bg-slate-800 hover:text-white"
)}
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</svg>
Back
</button>

{currentStep < 3 ? (
<button
onClick={handleNext}
disabled={!canProceed}
className={cn(
"flex items-center gap-2 rounded-xl px-6 py-3 text-sm font-semibold transition-all duration-200",
canProceed
? "bg-gradient-to-r from-emerald-500 to-cyan-500 text-white shadow-lg shadow-emerald-500/30 hover:shadow-emerald-500/50 hover:brightness-110 active:scale-95"
: "cursor-not-allowed bg-slate-800 text-slate-500"
)}
>
Next
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
</button>
) : (
<button
onClick={handleSubmit}
disabled={submitting}
className="flex items-center gap-2 rounded-xl bg-gradient-to-r from-emerald-500 to-cyan-500 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-emerald-500/30 transition-all duration-200 hover:shadow-emerald-500/50 hover:brightness-110 active:scale-95 disabled:opacity-60"
>
{submitting ? (
<>
<svg className="h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Creating Pool…
</>
) : (
<>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
Create Pool
</>
)}
</button>
)}
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions frontend/src/components/dashboard/DashboardShell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import { DashboardSidebar } from "./DashboardSidebar";

interface DashboardShellProps {
children: React.ReactNode;
}

export function DashboardShell({ children }: DashboardShellProps) {
return (
<div className="flex min-h-screen flex-col bg-slate-950 text-slate-100 lg:flex-row">
<DashboardSidebar />
<main className="flex-1 overflow-auto">
<div className="mx-auto max-w-6xl p-6 lg:p-8">{children}</div>
</main>
</div>
);
}
Loading