diff --git a/app/page.tsx b/app/page.tsx index 3b9c101..7ee27e7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -6,6 +6,7 @@ import { CTA } from "@/components/cta"; import { Footer } from "@/components/footer"; import { Testimonial } from "@/components/testimonial"; import { Faq } from "@/components/faq"; +import { FundedWork } from "@/components/funded-work"; export default function Home() { return ( @@ -14,6 +15,7 @@ export default function Home() { + diff --git a/components/funded-work.tsx b/components/funded-work.tsx new file mode 100644 index 0000000..097f3c1 --- /dev/null +++ b/components/funded-work.tsx @@ -0,0 +1,101 @@ +import Link from "next/link"; +import { ArrowUpRight, CircleCheck, Eye, ShieldCheck } from "lucide-react"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { getFundedWork, type FundedWorkItem } from "@/lib/funded-work"; + +function formatAmount(amount: number, symbol: string) { + return `${new Intl.NumberFormat("en-US", { + maximumFractionDigits: 2, + }).format(amount)} ${symbol}`; +} + +function formatDeadline(deadline?: string) { + if (!deadline) return "No deadline listed"; + const date = new Date(deadline); + if (Number.isNaN(date.getTime())) return "Deadline listed in task"; + return `Due ${new Intl.DateTimeFormat("en-US", { + month: "short", + day: "numeric", + year: "numeric", + timeZone: "UTC", + }).format(date)}`; +} + +function WorkCard({ item }: { item: FundedWorkItem }) { + return ( + + +
+ + + + {formatAmount(item.amount, item.symbol)} + +
+ {item.title} + + {item.username ? `Posted by @${item.username} ยท ` : ""} + {formatDeadline(item.deadline)} + +
+ +
+
+

+ Funding is an observation, not a promise of award. Review eligibility and submission rules. +

+ + Inspect task
+
+ ); +} + +export async function FundedWork() { + const items = await getFundedWork(); + + return ( +
+
+ + +

+ Find work with funding you can inspect +

+

+ Live cards read Gibwork's public task feed and read-only vault endpoint. + A green badge means a positive balance was observed when this page rendered. +

+
+ + {items.length > 0 ? ( +
+ {items.map((item) => )} +
+ ) : ( + + + + + )} +
+ ); +} diff --git a/lib/funded-work.ts b/lib/funded-work.ts new file mode 100644 index 0000000..dc35e4b --- /dev/null +++ b/lib/funded-work.ts @@ -0,0 +1,129 @@ +const GIBWORK_API = "https://api.gib.work"; + +type ExploreResponse = { + results?: ExploreTask[]; +}; + +type ExploreTask = { + id?: string; + title?: string; + deadline?: string; + isOpen?: boolean; + remainingAmount?: number | string; + asset?: { + symbol?: string; + price?: number | string; + }; + tags?: string[]; + user?: { + username?: string; + }; +}; + +type VaultResponse = { + balance?: number | string; + totalDeposited?: number | string; + totalWithdrawn?: number | string; + isAccountClosed?: boolean; +}; + +export type FundedWorkItem = { + id: string; + title: string; + amount: number; + symbol: string; + deadline?: string; + username?: string; + escrowBalance: number; + isAccountClosed: boolean; +}; + +function toNumber(value: unknown): number | null { + if (typeof value === "number" && Number.isFinite(value)) return value; + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value); + if (Number.isFinite(parsed)) return parsed; + } + return null; +} + +async function getJson(url: string, init?: RequestInit): Promise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + + try { + const response = await fetch(url, { + ...init, + signal: controller.signal, + headers: { + accept: "application/json", + ...(init?.headers ?? {}), + }, + }); + + if (!response.ok) return null; + return (await response.json()) as T; + } catch { + return null; + } finally { + clearTimeout(timeout); + } +} + +/** + * Read-only funding check for the landing page. The vault endpoint is treated + * as an observation, never as a guarantee that a participant will be paid. + */ +export async function getFundedWork(): Promise { + const payload = await getJson( + `${GIBWORK_API}/explore?page=1&limit=50`, + { next: { revalidate: 300 } }, + ); + + const candidates = (payload?.results ?? []) + .filter( + (task) => + task.id && + task.title && + task.isOpen && + // Keep the public strip focused on build work; it must not encourage + // visitors to use someone else's social accounts. + task.tags?.some((tag) => ["Development", "JavaScript", "TypeScript", "Rust", "Solana"].includes(tag)), + ) + .map((task) => ({ + task, + amount: toNumber(task.remainingAmount), + })) + .filter( + (candidate): candidate is { task: ExploreTask & { id: string; title: string }; amount: number } => + candidate.amount !== null && candidate.amount > 0, + ) + .slice(0, 6); + + const checked: Array = await Promise.all( + candidates.map(async ({ task, amount }) => { + const vault = await getJson(`${GIBWORK_API}/vaults`, { + method: "POST", + body: JSON.stringify({ id: task.id, type: "task" }), + headers: { "content-type": "application/json" }, + cache: "no-store", + }); + + const balance = toNumber(vault?.balance); + if (balance === null || balance <= 0) return null; + + return { + id: task.id, + title: task.title, + amount, + symbol: task.asset?.symbol ?? "USDC", + deadline: task.deadline, + username: task.user?.username, + escrowBalance: balance, + isAccountClosed: vault?.isAccountClosed === true, + } satisfies FundedWorkItem; + }), + ); + + return checked.filter((item) => item !== null) as FundedWorkItem[]; +}