Files
ai-learning-platform/frontend/src/app/marketplace/page.tsx
T
TradeMate Dev 00a3904eba feat: 落地打赏(个人码捐赠)模式并将付费面改为免费+赞助(无ICP证合规)
- 新增 Donation 模型/迁移/后端 API(GET,POST /donations)
- 新增前端 /donate 页:收款码+感谢留言+感谢墙
- 会员页/技能广场/用量包 下架付费,改免费开放+赞助入口
- 沙箱用量耗尽引导至 /donate 赞助
- 导航与页脚新增 赞助/联盟返佣 入口,补充 i18n
2026-07-11 15:02:08 +08:00

123 lines
4.6 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { Skeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useT } from '@/i18n';
import { API_BASE } from '@/lib/config';
import { ShoppingBag, CheckCircle, Heart } from 'lucide-react';
interface MarketplaceSkill {
id: string; name: string; description: string; icon: string;
category: string; difficulty: string; tags: string[];
price: number | null; purchased: boolean; sortOrder: number;
}
export default function MarketplacePage() {
const t = useT();
const [skills, setSkills] = useState<MarketplaceSkill[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const url = `${API_BASE}/skills/marketplace`;
fetch(url, { credentials: 'include' })
.then((r) => r.json())
.then((data) => {
setSkills(Array.isArray(data) ? data : []);
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);
return (
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="mb-6">
<h1 className="text-3xl font-bold text-foreground">{t.marketplace.title}</h1>
<p className="mt-2 text-muted-foreground">{t.marketplace.desc}</p>
</div>
<div className="flex items-center justify-between gap-3 bg-rose-50 dark:bg-rose-950/20 border border-rose-200 dark:border-rose-900 rounded-xl p-4 mb-8">
<div className="flex items-center gap-3">
<Heart className="w-5 h-5 text-rose-600 dark:text-rose-400 shrink-0" />
<p className="text-sm text-foreground">{t.marketplace.freeNote}</p>
</div>
<Link
href="/donate"
className="shrink-0 text-sm font-medium text-rose-600 dark:text-rose-400 hover:underline"
>
{t.donate.toDonate}
</Link>
</div>
{loading ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5, 6].map((i) => (
<Skeleton key={i} className="h-52 rounded-2xl" />
))}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{skills.map((skill) => (
<div
key={skill.id}
className="bg-card rounded-2xl border-2 border-border p-6 transition-all hover:shadow-md flex flex-col"
>
<div className="flex items-start gap-3 mb-3">
<span className="text-3xl">{skill.icon}</span>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-foreground">{skill.name}</h3>
<p className="text-sm text-muted-foreground mt-0.5 line-clamp-2">{skill.description}</p>
</div>
</div>
<div className="flex items-center gap-2 mb-3">
<span
className={`text-xs px-2 py-0.5 rounded-full ${
skill.difficulty === 'beginner'
? 'bg-green-100 text-green-700'
: skill.difficulty === 'intermediate'
? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-700'
}`}
>
{(t.skills as any)[skill.difficulty]}
</span>
<span className="text-xs text-muted-foreground">
{(t.skills.categories as any)[skill.category] || skill.category}
</span>
</div>
<div className="flex flex-wrap gap-1 mb-4">
{skill.tags.slice(0, 3).map((tag) => (
<span key={tag} className="text-xs px-1.5 py-0.5 bg-muted text-muted-foreground rounded">
{tag}
</span>
))}
</div>
<div className="mt-auto flex items-center gap-2">
<Badge variant="secondary" className="gap-1">
<CheckCircle className="h-3.5 w-3.5" />
{t.marketplace.free}
</Badge>
<Button asChild size="sm" variant="default" className="ml-auto">
<Link href={`/sandbox?skill=${skill.id}`}>{t.skills.apply}</Link>
</Button>
</div>
</div>
))}
</div>
)}
{!loading && skills.length === 0 && (
<div className="text-center py-20">
<ShoppingBag className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground">{t.common.noData}</p>
</div>
)}
</div>
);
}