Skip to content
Open
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/frontend/app/bounties/[id]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import BountyDetailSkeleton from "../../components/BountyDetailSkeleton";

export default function Loading() {
return <BountyDetailSkeleton />;
}
5 changes: 5 additions & 0 deletions apps/frontend/app/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import DashboardSkeleton from "../components/DashboardSkeleton";

export default function Loading() {
return <DashboardSkeleton />;
}
22 changes: 16 additions & 6 deletions apps/frontend/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import { useEffect, useState } from "react";
import { useWallet } from "../../components/WalletContext";
import { useAuth } from "../../lib/api";
import { StatusBadge } from "../../components/StatusBadge";

const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "";

type SubmissionStatus = "pending" | "approved" | "rejected";
type BountyStatus = "open" | "in_progress" | "completed" | "cancelled";

Expand All @@ -32,6 +31,7 @@ function EmptyState({ message }: { message: string }) {

export default function DashboardPage() {
const { publicKey, connect } = useWallet();
const { getToken, apiUrl } = useAuth();
const [activeTab, setActiveTab] = useState<"submissions" | "bounties">("submissions");
const [submissions, setSubmissions] = useState<Submission[]>([]);
const [bounties, setBounties] = useState<Bounty[]>([]);
Expand All @@ -45,16 +45,26 @@ export default function DashboardPage() {
setError(null);

Promise.all([
fetch(`${API_URL}/submissions?contributor=${publicKey}`).then((r) => r.json()),
fetch(`${API_URL}/api/v1/bounties?owner=${publicKey}`).then((r) => r.json()),
(async () => {
const token = await getToken(publicKey);
const res = await fetch(`${apiUrl}/api/v1/me/submissions`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (!res.ok) return [];
const body = await res.json();
/* Normalise the API response — the backend may return
* { submissions: [...] } or a plain array. */
return Array.isArray(body) ? body : body?.submissions ?? [];
})(),
fetch(`${apiUrl}/api/v1/bounties?owner=${publicKey}`).then((r) => r.json()),
])
.then(([subs, bounts]) => {
setSubmissions(subs);
setBounties(bounts);
})
.catch(() => setError("Failed to load dashboard data."))
.finally(() => setLoading(false));
}, [publicKey]);
}, [publicKey, getToken, apiUrl]);

if (!publicKey) {
return (
Expand Down Expand Up @@ -158,4 +168,4 @@ export default function DashboardPage() {
)}
</main>
);
}
}
17 changes: 3 additions & 14 deletions apps/frontend/app/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
const skeletonCards = Array.from({ length: 6 }, (_, index) => index);
import BountyListSkeleton from "./components/BountyListSkeleton";

export default function Loading() {
return (
<main className="min-h-[calc(100vh-73px)] bg-slate-50 px-4 py-10 text-slate-950 dark:bg-slate-950 dark:text-slate-100 sm:px-6 lg:px-8">
<div className="mx-auto max-w-7xl">
<div className="mb-10 h-64 animate-pulse rounded-3xl border border-slate-200 bg-slate-200 dark:border-slate-800 dark:bg-slate-900" />
<div className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3">
{skeletonCards.map((card) => (
<div key={card} className="h-44 animate-pulse rounded-2xl border border-slate-200 bg-slate-200 dark:border-slate-800 dark:bg-slate-900" />
))}
</div>
</div>
</main>
);
}
return <BountyListSkeleton />;
}
56 changes: 56 additions & 0 deletions apps/frontend/components/BountyDetailSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
export default function BountyDetailSkeleton() {
return (
<main className="min-h-[calc(100vh-73px)] overflow-x-hidden bg-slate-50 px-3 py-6 text-slate-950 transition-colors dark:bg-slate-950 dark:text-slate-100 sm:px-4 sm:py-10">
<div className="mx-auto grid w-full max-w-6xl gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(320px,380px)]">
{/* Main content skeleton */}
<section className="min-w-0 rounded-2xl border border-slate-200 bg-white p-4 shadow-2xl shadow-slate-200/70 dark:border-slate-800 dark:bg-slate-900/70 dark:shadow-slate-950/40 sm:p-6">
{/* Badge + metadata row */}
<div className="mb-6 flex min-w-0 flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center">
<div className="h-6 w-20 animate-pulse rounded-full bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-28 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-32 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
</div>

{/* Title */}
<div className="mb-4 h-8 w-3/4 animate-pulse rounded bg-slate-200 dark:bg-slate-700 sm:h-10" />
<div className="mb-2 h-8 w-1/2 animate-pulse rounded bg-slate-200 dark:bg-slate-700 sm:h-10" />

{/* Description */}
<div className="mt-5 space-y-2">
<div className="h-4 w-full animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-full animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-5/6 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-4/6 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
</div>

{/* Info cards */}
<dl className="mt-8 grid gap-4 sm:grid-cols-3">
{[0, 1, 2].map((i) => (
<div
key={i}
className="rounded-xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-950/60"
>
<div className="h-3 w-12 animate-pulse rounded bg-slate-300 dark:bg-slate-600" />
<div className="mt-2 h-4 w-24 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
</div>
))}
</dl>
</section>

{/* Sidebar skeleton */}
<aside className="min-w-0 rounded-2xl border border-slate-200 bg-white p-4 shadow-2xl shadow-slate-200/70 dark:border-slate-800 dark:bg-slate-900/70 dark:shadow-slate-950/40 sm:p-6">
{/* Title */}
<div className="h-6 w-28 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="mt-2 h-4 w-44 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />

{/* Form fields */}
<div className="mt-5 space-y-4">
<div className="h-12 w-full animate-pulse rounded-lg bg-slate-200 dark:bg-slate-700" />
<div className="h-32 w-full animate-pulse rounded-lg bg-slate-200 dark:bg-slate-700" />
<div className="h-11 w-full animate-pulse rounded-lg bg-slate-200 dark:bg-slate-700" />
</div>
</aside>
</div>
</main>
);
}
52 changes: 52 additions & 0 deletions apps/frontend/components/BountyListSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export default function BountyListSkeleton() {
return (
<main className="min-h-[calc(100vh-73px)] bg-slate-50 px-4 py-10 text-slate-950 transition-colors dark:bg-slate-950 dark:text-slate-100 sm:px-6 lg:px-8">
<div className="mx-auto max-w-7xl">
{/* Hero section skeleton */}
<section className="mb-10 flex flex-col justify-between gap-6 rounded-3xl border border-slate-200 bg-gradient-to-br from-white via-slate-50 to-slate-100 p-6 shadow-2xl shadow-slate-200/70 transition-colors dark:border-slate-800 dark:from-slate-900 dark:via-slate-950 dark:to-slate-900 dark:shadow-black/20 sm:p-8 lg:flex-row lg:items-end">
<div className="max-w-3xl">
<div className="mb-4 h-4 w-36 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="mb-4 h-10 w-4/5 animate-pulse rounded bg-slate-200 dark:bg-slate-700 sm:h-12" />
<div className="h-4 w-3/4 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="mt-2 h-4 w-2/3 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
</div>
<div className="h-12 w-36 animate-pulse rounded-xl bg-slate-200 dark:bg-slate-700" />
</section>

{/* Filters skeleton */}
<section className="mb-8 rounded-3xl border border-slate-200 bg-white p-4 shadow-xl shadow-slate-200/60 transition-colors dark:border-slate-800 dark:bg-slate-900/80 dark:shadow-black/10 sm:p-6">
<div className="grid grid-cols-1 gap-4 md:grid-cols-[minmax(0,1.6fr)_minmax(180px,0.8fr)_minmax(180px,0.8fr)_auto] md:items-end">
<div className="h-12 w-full animate-pulse rounded-2xl bg-slate-200 dark:bg-slate-700" />
<div className="h-12 w-full animate-pulse rounded-2xl bg-slate-200 dark:bg-slate-700" />
<div className="h-12 w-full animate-pulse rounded-2xl bg-slate-200 dark:bg-slate-700" />
<div className="flex gap-3">
<div className="h-12 w-28 animate-pulse rounded-2xl bg-slate-200 dark:bg-slate-700" />
<div className="h-12 w-28 animate-pulse rounded-2xl bg-slate-200 dark:bg-slate-700" />
</div>
</div>
</section>

{/* Bounty cards grid */}
<div className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3">
{[...Array(6)].map((_, i) => (
<div
key={i}
className="h-44 animate-pulse rounded-2xl border border-slate-200 bg-slate-100 p-5 dark:border-slate-800 dark:bg-slate-900/60"
>
<div className="flex items-start justify-between">
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-14 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
</div>
<div className="mt-4 h-5 w-4/5 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="mt-2 h-4 w-3/5 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="mt-6 flex items-center gap-4">
<div className="h-3 w-16 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-3 w-20 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
</div>
</div>
))}
</div>
</div>
</main>
);
}
34 changes: 34 additions & 0 deletions apps/frontend/components/DashboardSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
export default function DashboardSkeleton() {
return (
<main className="mx-auto max-w-5xl bg-slate-50 px-4 py-8 text-slate-950 dark:bg-slate-950 dark:text-slate-100 sm:px-6">
<div className="mb-6 h-8 w-36 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />

{/* Tabs */}
<div className="mb-6 flex border-b border-slate-300 dark:border-slate-700">
{["submissions", "bounties"].map((tab) => (
<div
key={tab}
className="h-10 w-32 animate-pulse rounded-t bg-slate-200 dark:bg-slate-700"
style={{ marginRight: tab === "submissions" ? 4 : 0 }}
/>
))}
</div>

{/* Table header */}
<div className="mb-3 flex gap-4">
<div className="h-4 w-16 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-20 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-14 animate-pulse rounded bg-slate-200 dark:bg-slate-700" />
</div>

{/* Table rows */}
{[...Array(4)].map((_, i) => (
<div
key={i}
className="h-12 animate-pulse rounded border-b border-slate-200 bg-slate-100 dark:border-slate-800 dark:bg-slate-900/60"
style={{ marginBottom: 4 }}
/>
))}
</main>
);
}
Loading