Files
work-board/src/components/BountyCard.tsx
T
zeekayandHanzo Dev 436a88e5f6 Zoo DAO work board: standalone repo + CI + k8s manifests
On-chain bounty kanban (Vite+React+viem static SPA) that reads the live
BountyV1/EscrowV1/ReputationV1 work-market on Zoo (chain 200200) directly
over the CORS-enabled JSON-RPC. Split out of luxfi/dao into its own repo so
CI builds a clean image.

- .github/workflows/docker.yml: canonical hanzoai/.github reusable build,
  bakes VITE_RPC_URL=https://api.zoo.network/v1/bc/C/rpc, pushes
  ghcr.io/luxfi/work-board:sha-<commit> on self-hosted arcd runners.
- k8s/: Deployment (2 replicas) + Service (80->3000) + Ingress
  (hanzoai/ingress class, host work.zoo.network, letsencrypt-prod).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-16 20:31:29 -07:00

104 lines
4.0 KiB
TypeScript

import type { BountyView } from '../types';
import { STATE_TITLE } from '../types';
import { ACCENT } from '../theme';
import { formatAmount, isZero, refToLink, relTime, short, tokenLabel } from '../format';
function AddrRow({ label, addr }: { label: string; addr: string }) {
return (
<div className="flex items-center justify-between gap-2">
<span className="text-neutral-500">{label}</span>
<span className="font-mono text-neutral-300" title={addr}>
{short(addr)}
</span>
</div>
);
}
function RefRow({ label, value }: { label: string; value: string }) {
const { href, text } = refToLink(value);
return (
<div className="flex items-center gap-1.5 min-w-0">
<span className="text-neutral-500 shrink-0">{label}</span>
{href ? (
<a
href={href}
target="_blank"
rel="noreferrer noopener"
className="truncate text-sky-400 hover:text-sky-300 underline decoration-dotted underline-offset-2"
>
{text}
</a>
) : (
<span className="truncate text-neutral-300">{text}</span>
)}
</div>
);
}
export function BountyCard({ b }: { b: BountyView }) {
const accent = ACCENT[b.state as keyof typeof ACCENT];
const claimed = !isZero(b.worker);
const hasDeadline = b.claimDeadline > 0n || b.reviewDeadline > 0n;
return (
<article className="rounded-lg bg-neutral-900 ring-1 ring-inset ring-neutral-800 hover:ring-neutral-700 transition-colors p-3 flex flex-col gap-2.5">
<div className="flex items-center justify-between">
<span className="font-mono text-sm text-neutral-400">#{b.id}</span>
<span className={`text-[11px] px-2 py-0.5 rounded-full ${accent.badge}`}>{STATE_TITLE[b.state as keyof typeof STATE_TITLE]}</span>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-xl font-semibold text-neutral-100 tabular-nums">{formatAmount(b.reward)}</span>
<span className="text-xs text-neutral-500">{tokenLabel(b.token)}</span>
<span className="ml-auto text-xs text-neutral-500 tabular-nums">stake {formatAmount(b.stake)}</span>
</div>
<div className="flex flex-col gap-1 text-xs">
<AddrRow label="funder" addr={b.funder} />
<AddrRow label="approver" addr={b.approver} />
<div className="flex items-center justify-between gap-2">
<span className="text-neutral-500">worker</span>
{claimed ? (
<span className="font-mono text-neutral-300" title={b.worker}>
{short(b.worker)}
</span>
) : (
<span className="text-neutral-600 italic">unclaimed</span>
)}
</div>
</div>
{claimed && (
<div
className="rounded-md bg-emerald-500/10 ring-1 ring-inset ring-emerald-500/30 px-2.5 py-1.5"
title="Portable on-chain reputation (ReputationV1) โ€” travels with the worker's address, not the platform"
>
<div className="flex items-center gap-1.5">
<span className="text-emerald-400">โœ“</span>
<span className="text-[10px] uppercase tracking-wider text-emerald-500/80 font-semibold">
on-chain rep
</span>
<span className="ml-auto text-xs text-emerald-200 tabular-nums">
{b.reputation.completed.toString()} completed ยท {formatAmount(b.reputation.earned)} earned
</span>
</div>
</div>
)}
{(b.issueRef || b.deliverableRef) && (
<div className="flex flex-col gap-1 text-xs">
{b.issueRef && <RefRow label="issue" value={b.issueRef} />}
{b.deliverableRef && <RefRow label="PR" value={b.deliverableRef} />}
</div>
)}
{hasDeadline && (
<div className="flex items-center gap-3 text-[11px] text-neutral-500 pt-1.5 border-t border-neutral-800/70">
{b.claimDeadline > 0n && <span>claim {relTime(b.claimDeadline)}</span>}
{b.reviewDeadline > 0n && <span>review {relTime(b.reviewDeadline)}</span>}
</div>
)}
</article>
);
}