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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions apps/frontend/app/components/BountyCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Link from "next/link";

export type BountyCardData = {
id: string | number;
title: string;
reward?: string | number | null;
deadline?: string | null;
status?: string | null;
};

type BountyCardProps = {
bounty: BountyCardData;
};

function formatReward(reward: BountyCardData["reward"]) {
if (reward === null || reward === undefined || reward === "") {
return "Reward TBD";
}

return typeof reward === "number" ? `${reward.toLocaleString()} XLM` : reward;
}

function formatDeadline(deadline: BountyCardData["deadline"]) {
if (!deadline) {
return "No deadline";
}

const parsed = new Date(deadline);
if (Number.isNaN(parsed.getTime())) {
return deadline;
}

return new Intl.DateTimeFormat("en", {
month: "short",
day: "numeric",
year: "numeric",
}).format(parsed);
}

export default function BountyCard({ bounty }: BountyCardProps) {
const status = bounty.status ?? "open";

return (
<Link
href={`/bounties/${bounty.id}`}
className="group flex h-full flex-col rounded-2xl border border-slate-800 bg-slate-900/70 p-5 shadow-xl shadow-black/10 transition hover:-translate-y-1 hover:border-yellow-400/60 hover:bg-slate-900"
>
<div className="flex items-start justify-between gap-3">
<h2 className="line-clamp-2 text-lg font-semibold text-slate-100 group-hover:text-yellow-100">
{bounty.title}
</h2>
<span className="shrink-0 rounded-full border border-emerald-400/30 bg-emerald-400/10 px-2.5 py-1 text-xs font-medium capitalize text-emerald-300">
{status}
</span>
</div>

<div className="mt-6 flex flex-1 items-end justify-between gap-4">
<div>
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">Reward</p>
<p className="mt-1 text-2xl font-bold text-yellow-400">{formatReward(bounty.reward)}</p>
</div>
<div className="text-right">
<p className="text-xs uppercase tracking-[0.2em] text-slate-500">Deadline</p>
<p className="mt-1 text-sm text-slate-300">{formatDeadline(bounty.deadline)}</p>
</div>
</div>
</Link>
);
}
16 changes: 16 additions & 0 deletions apps/frontend/app/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const skeletonCards = Array.from({ length: 6 }, (_, index) => index);

export default function Loading() {
return (
<main className="min-h-[calc(100vh-73px)] bg-slate-950 px-4 py-10 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-800 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-800 bg-slate-900" />
))}
</div>
</div>
</main>
);
}
84 changes: 70 additions & 14 deletions apps/frontend/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,81 @@
import Link from "next/link";
import BountyCard, { type BountyCardData } from "@/app/components/BountyCard";

export const revalidate = 60;

type ApiBounty = Partial<BountyCardData> & {
_id?: string;
amount?: string | number | null;
rewardAmount?: string | number | null;
dueDate?: string | null;
};

async function getBounties(): Promise<BountyCardData[]> {
const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";

try {
const response = await fetch(`${apiUrl}/bounties`, { next: { revalidate } });

if (!response.ok || !response.headers.get("content-type")?.includes("application/json")) {
return [];
}

const bounties = (await response.json()) as ApiBounty[];

return bounties.map((bounty, index) => ({
id: bounty.id ?? bounty._id ?? index,
title: bounty.title ?? "Untitled bounty",
reward: bounty.reward ?? bounty.rewardAmount ?? bounty.amount ?? null,
deadline: bounty.deadline ?? bounty.dueDate ?? null,
status: bounty.status ?? "open",
}));
} catch {
return [];
}
}

export default async function Home() {
const bounties = await getBounties();

export default function Home() {
return (
<main className="flex min-h-[calc(100vh-73px)] items-center justify-center px-4">
<div className="text-center">
<h1 className="text-4xl font-bold">StellarBounty</h1>
<p className="mt-3 text-slate-400">Decentralized bounty marketplace on Stellar.</p>
<div className="mt-8 flex gap-4 justify-center">
<main className="min-h-[calc(100vh-73px)] bg-slate-950 px-4 py-10 text-slate-100 sm:px-6 lg:px-8">
<div className="mx-auto max-w-7xl">
<section className="mb-10 flex flex-col justify-between gap-6 rounded-3xl border border-slate-800 bg-gradient-to-br from-slate-900 via-slate-950 to-slate-900 p-6 shadow-2xl shadow-black/20 sm:p-8 lg:flex-row lg:items-end">
<div className="max-w-3xl">
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-yellow-400">StellarBounty</p>
<h1 className="mt-4 text-4xl font-black tracking-tight text-white sm:text-5xl">
Open bounties ready for builders
</h1>
<p className="mt-4 max-w-2xl text-base leading-7 text-slate-400">
Browse funded work, compare rewards and deadlines, then jump into a task that matches your skills.
</p>
</div>
<Link
href="/bounties/new"
className="px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
className="inline-flex items-center justify-center rounded-xl bg-yellow-400 px-5 py-3 font-semibold text-slate-950 transition hover:bg-yellow-300"
>
Create Bounty
</Link>
<Link
href="/bounties/demo"
className="px-5 py-2.5 bg-slate-800 hover:bg-slate-700 text-slate-200 font-medium rounded-lg border border-slate-700 transition-colors"
>
View Demo Bounty
</Link>
</div>
</section>

{bounties.length > 0 ? (
<section className="grid grid-cols-1 gap-5 md:grid-cols-2 xl:grid-cols-3">
{bounties.map((bounty) => (
<BountyCard key={bounty.id} bounty={bounty} />
))}
</section>
) : (
<section className="rounded-3xl border border-dashed border-slate-700 bg-slate-900/50 px-6 py-16 text-center">
<p className="text-lg font-semibold text-slate-200">No bounties available yet.</p>
<p className="mt-2 text-slate-400">Create the first bounty and bring new work onto Stellar.</p>
<Link
href="/bounties/new"
className="mt-6 inline-flex rounded-xl border border-slate-700 px-5 py-3 font-medium text-slate-200 transition hover:border-yellow-400 hover:text-yellow-300"
>
Post a bounty
</Link>
</section>
)}
</div>
</main>
);
Expand Down
Loading