264 lines
11 KiB
TypeScript
264 lines
11 KiB
TypeScript
"use client"
|
||
|
||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||
import { Badge } from "@/components/ui/badge"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||
import { IconInput } from "@/components/ui/icon-input"
|
||
import { StatusBadge } from "@/components/ui/status-badge"
|
||
import { listGames, listPlayers, listShops } from "@/lib/api"
|
||
import { toApiError } from "@/lib/errors"
|
||
import { GameIcon } from "@/lib/game-icons"
|
||
import { ArrowRight, Search, ShoppingBag, Star } from "lucide-react"
|
||
import Link from "next/link"
|
||
import { useEffect, useState } from "react"
|
||
|
||
export default function HomePage() {
|
||
const [games, setGames] = useState<Awaited<ReturnType<typeof listGames>>>([])
|
||
const [players, setPlayers] = useState<Awaited<ReturnType<typeof listPlayers>>>([])
|
||
const [shops, setShops] = useState<Awaited<ReturnType<typeof listShops>>>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [loadingError, setLoadingError] = useState<string | null>(null)
|
||
|
||
useEffect(() => {
|
||
let cancelled = false
|
||
|
||
const load = async () => {
|
||
try {
|
||
const [nextGames, nextPlayers, nextShops] = await Promise.all([
|
||
listGames(),
|
||
listPlayers(),
|
||
listShops(),
|
||
])
|
||
|
||
if (cancelled) return
|
||
|
||
setGames(nextGames)
|
||
setPlayers(nextPlayers)
|
||
setShops(nextShops)
|
||
setLoading(false)
|
||
} catch (err: unknown) {
|
||
if (cancelled) return
|
||
setLoading(false)
|
||
setLoadingError(toApiError(err).msg)
|
||
}
|
||
}
|
||
|
||
void load()
|
||
|
||
return () => {
|
||
cancelled = true
|
||
}
|
||
}, [])
|
||
|
||
return (
|
||
<div className="container mx-auto py-8 px-4 space-y-12">
|
||
<section className="pb-8 pt-12 lg:pb-12 lg:pt-20 text-center">
|
||
<div className="space-y-8 max-w-3xl mx-auto">
|
||
<div className="space-y-4">
|
||
<h1 className="mb-3 text-3xl font-bold tracking-tight text-foreground lg:text-5xl">
|
||
找到你的<span className="text-primary">游戏搭档</span>
|
||
</h1>
|
||
<p className="mb-8 text-base text-muted-foreground lg:text-lg">
|
||
找人一起打游戏,从这里开始
|
||
</p>
|
||
</div>
|
||
|
||
<div id="discover-search" className="w-full max-w-2xl mx-auto">
|
||
<form action="/search" className="w-full max-w-xl mx-auto">
|
||
<IconInput
|
||
inputSize="lg"
|
||
icon={<Search />}
|
||
type="text"
|
||
name="q"
|
||
placeholder="搜索陪玩、店铺、游戏..."
|
||
className="w-full border-border bg-card shadow-sm transition-colors focus-visible:border-primary"
|
||
/>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{loading && <p className="text-sm text-muted-foreground text-center">加载中...</p>}
|
||
{!loading && loadingError && (
|
||
<p className="text-sm text-muted-foreground text-center">{loadingError}</p>
|
||
)}
|
||
|
||
<section>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h2 className="text-xl font-semibold">游戏分类</h2>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="text-muted-foreground hover:text-foreground"
|
||
asChild
|
||
>
|
||
<Link href="/search">
|
||
查看全部 <ArrowRight className="ml-1 h-4 w-4" />
|
||
</Link>
|
||
</Button>
|
||
</div>
|
||
<div className="flex flex-wrap gap-3">
|
||
{!loading && !loadingError
|
||
? games.map((game) => (
|
||
<Button key={game.id} variant="secondary" className="rounded-full" asChild>
|
||
<Link href={`/search?game=${encodeURIComponent(game.name)}`}>
|
||
<GameIcon name={game.icon} className="h-5 w-5" />
|
||
{game.name}
|
||
</Link>
|
||
</Button>
|
||
))
|
||
: null}
|
||
</div>
|
||
</section>
|
||
|
||
<section className="space-y-6">
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||
<h2 className="text-xl font-semibold">推荐内容</h2>
|
||
<div className="flex flex-wrap items-center gap-1 text-sm">
|
||
<Button variant="ghost" size="sm" asChild>
|
||
<Link href="/search?sort=composite">综合排序</Link>
|
||
</Button>
|
||
<Button variant="ghost" size="sm" className="text-muted-foreground" asChild>
|
||
<Link href="/search?sort=rating">评分最高</Link>
|
||
</Button>
|
||
<Button variant="ghost" size="sm" className="text-muted-foreground" asChild>
|
||
<Link href="/search?sort=orders">接单最多</Link>
|
||
</Button>
|
||
<Button variant="ghost" size="sm" className="text-muted-foreground" asChild>
|
||
<Link href="/search?sort=price_asc">价格最低</Link>
|
||
</Button>
|
||
<Button variant="ghost" size="sm" className="text-muted-foreground" asChild>
|
||
<Link href="/search?sort=price_desc">价格最高</Link>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{/* Players */}
|
||
{!loading && !loadingError
|
||
? players.map((player) => (
|
||
<Card key={player.id} className="flex flex-col h-full border-border/80 shadow-sm">
|
||
<CardHeader className="flex flex-row items-center gap-3 space-y-0 pb-3">
|
||
<Avatar className="h-12 w-12">
|
||
<AvatarImage src={player.user.avatar} />
|
||
<AvatarFallback>{player.user.nickname[0]}</AvatarFallback>
|
||
</Avatar>
|
||
<div className="flex-1 min-w-0">
|
||
<CardTitle className="text-base">{player.user.nickname}</CardTitle>
|
||
<div className="flex items-center gap-2 mt-1">
|
||
<div className="flex items-center text-sm">
|
||
<Star className="h-3.5 w-3.5 fill-warning text-warning mr-0.5" />
|
||
{player.rating}
|
||
</div>
|
||
<span className="text-xs text-muted-foreground">
|
||
{player.totalOrders} 单
|
||
</span>
|
||
<StatusBadge
|
||
status={
|
||
player.status === "available"
|
||
? "success"
|
||
: player.status === "busy"
|
||
? "warning"
|
||
: "neutral"
|
||
}
|
||
className="text-[10px] px-1.5 py-0 font-normal"
|
||
icon={false}
|
||
>
|
||
{player.status === "available"
|
||
? "可接单"
|
||
: player.status === "busy"
|
||
? "忙碌"
|
||
: "离线"}
|
||
</StatusBadge>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="pb-3 flex-grow">
|
||
<div className="flex flex-wrap gap-1.5">
|
||
{player.tags.map((tag) => (
|
||
<Badge
|
||
key={tag}
|
||
variant="secondary"
|
||
className="text-xs bg-muted/50 text-muted-foreground font-normal"
|
||
>
|
||
{tag}
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
{player.shopName && (
|
||
<p className="text-xs text-muted-foreground mt-3">
|
||
所属店铺: {player.shopName}
|
||
</p>
|
||
)}
|
||
</CardContent>
|
||
<CardFooter className="pt-0 flex items-end justify-between">
|
||
<div className="flex items-baseline gap-1">
|
||
<span className="text-lg font-bold leading-none">
|
||
{player.services?.[0]?.price ?? 35}
|
||
</span>
|
||
<span className="text-xs text-muted-foreground">
|
||
元/{player.services?.[0]?.unit ?? "时"}
|
||
</span>
|
||
</div>
|
||
<Button variant="outline" size="sm" asChild>
|
||
<Link href={`/player/${player.id}`}>查看详情</Link>
|
||
</Button>
|
||
</CardFooter>
|
||
</Card>
|
||
))
|
||
: null}
|
||
|
||
{/* Community Teaser */}
|
||
<Card className="flex flex-col h-full border-border/80 shadow-sm">
|
||
<CardHeader>
|
||
<CardTitle className="text-lg">逛逛社区</CardTitle>
|
||
<p className="text-sm text-muted-foreground">发现更多有趣的游戏日常和讨论</p>
|
||
</CardHeader>
|
||
<CardContent className="flex-grow flex items-center justify-center">
|
||
<div className="w-24 h-24 rounded-full bg-primary/10 flex items-center justify-center">
|
||
<Star className="h-10 w-10 text-primary" />
|
||
</div>
|
||
</CardContent>
|
||
<CardFooter>
|
||
<Button variant="outline" className="w-full" asChild>
|
||
<Link href="/community">进入社区</Link>
|
||
</Button>
|
||
</CardFooter>
|
||
</Card>
|
||
|
||
{/* Shops */}
|
||
{!loading && !loadingError
|
||
? shops.map((shop) => (
|
||
<Card key={shop.id} className="flex flex-col h-full border-border/80 shadow-sm">
|
||
<CardHeader>
|
||
<CardTitle className="text-lg">{shop.name}</CardTitle>
|
||
<p className="text-sm text-muted-foreground">{shop.description}</p>
|
||
</CardHeader>
|
||
<CardContent className="flex-grow">
|
||
<div className="flex items-center gap-4 text-sm">
|
||
<div className="flex items-center">
|
||
<Star className="h-3.5 w-3.5 fill-warning text-warning mr-0.5" />
|
||
{shop.rating}
|
||
</div>
|
||
<div className="flex items-center text-muted-foreground">
|
||
<ShoppingBag className="h-3.5 w-3.5 mr-0.5" />
|
||
{shop.totalOrders} 单
|
||
</div>
|
||
<span className="text-muted-foreground">{shop.playerCount} 名打手</span>
|
||
</div>
|
||
</CardContent>
|
||
<CardFooter>
|
||
<Button variant="outline" size="sm" className="w-full" asChild>
|
||
<Link href={`/shop/${shop.id}`}>进入店铺</Link>
|
||
</Button>
|
||
</CardFooter>
|
||
</Card>
|
||
))
|
||
: null}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
)
|
||
}
|