Completed slices: - S01: Desire Embedding & Clustering - S02: Fulfillment Flow & Frontend Branch: milestone/M001
108 lines
3.5 KiB
TypeScript
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>
|
|
);
|
|
}
|