fractafrag/services/frontend/src/pages/BountyDetail.tsx
John Lightner 5936ab167e feat(M001): Desire Economy
Completed slices:
- S01: Desire Embedding & Clustering
- S02: Fulfillment Flow & Frontend

Branch: milestone/M001
2026-03-25 02:22:50 -05:00

108 lines
3.5 KiB
TypeScript

/**
* Bounty detail page — single desire with fulfillment option.
*/
import { useParams, Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import api from '@/lib/api';
export default function BountyDetail() {
const { id } = useParams<{ id: string }>();
const { data: desire, isLoading } = useQuery({
queryKey: ['desire', id],
queryFn: async () => {
const { data } = await api.get(`/desires/${id}`);
return data;
},
enabled: !!id,
});
if (isLoading) {
return (
<div className="max-w-2xl mx-auto px-4 py-10">
<div className="card p-6 animate-pulse space-y-4">
<div className="h-6 bg-surface-3 rounded w-3/4" />
<div className="h-4 bg-surface-3 rounded w-1/2" />
</div>
</div>
);
}
if (!desire) {
return (
<div className="max-w-2xl mx-auto px-4 py-10 text-center text-red-400">
Desire not found
</div>
);
}
return (
<div className="max-w-2xl mx-auto px-4 py-6">
<Link to="/bounties" className="text-sm text-gray-500 hover:text-gray-300 mb-4 inline-block">
Back to Bounties
</Link>
<div className="card p-6">
<div className="flex items-start justify-between">
<div>
<h1 className="text-xl font-bold">{desire.prompt_text}</h1>
<div className="flex items-center gap-3 mt-3 text-sm text-gray-500">
<span>🔥 Heat: {desire.heat_score.toFixed(1)}</span>
{desire.cluster_count > 1 && (
<span className="text-purple-400">
👥 {desire.cluster_count} similar
</span>
)}
{desire.tip_amount_cents > 0 && (
<span className="text-green-400">
💰 ${(desire.tip_amount_cents / 100).toFixed(2)} bounty
</span>
)}
<span>{new Date(desire.created_at).toLocaleDateString()}</span>
</div>
</div>
<span className={`text-sm px-3 py-1 rounded-full ${
desire.status === 'open' ? 'bg-green-600/20 text-green-400' :
desire.status === 'fulfilled' ? 'bg-blue-600/20 text-blue-400' :
'bg-gray-600/20 text-gray-400'
}`}>
{desire.status}
</span>
</div>
{desire.style_hints && (
<div className="mt-4 p-3 bg-surface-2 rounded-lg">
<h3 className="text-sm font-medium text-gray-400 mb-2">Style hints</h3>
<pre className="text-xs text-gray-500 font-mono">
{JSON.stringify(desire.style_hints, null, 2)}
</pre>
</div>
)}
{desire.status === 'open' && (
<div className="mt-6 pt-4 border-t border-surface-3">
<Link to={`/editor?fulfill=${desire.id}`} className="btn-primary">
Fulfill this Desire
</Link>
<p className="text-xs text-gray-500 mt-2">
Write a shader that matches this description, then submit it as fulfillment.
</p>
</div>
)}
{desire.fulfilled_by_shader && (
<div className="mt-6 pt-4 border-t border-surface-3">
<h3 className="text-sm font-medium text-gray-400 mb-2">Fulfilled by</h3>
<Link
to={`/shader/${desire.fulfilled_by_shader}`}
className="text-fracta-400 hover:text-fracta-300"
>
View shader
</Link>
</div>
)}
</div>
</div>
);
}