fix(home): load catalog client-side

This commit is contained in:
zetaloop
2026-03-01 22:33:42 +08:00
parent 6ee14f6eef
commit eba8fc7e65
+154 -100
View File
@@ -1,15 +1,54 @@
"use client"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { IconInput } from "@/components/ui/icon-input" import { IconInput } from "@/components/ui/icon-input"
import { listGames, listPlayers, listShops } from "@/lib/api" import { listGames, listPlayers, listShops } from "@/lib/api"
import { toApiError } from "@/lib/errors"
import { GameIcon } from "@/lib/game-icons" import { GameIcon } from "@/lib/game-icons"
import { ArrowRight, Search, ShoppingBag, Star } from "lucide-react" import { ArrowRight, Search, ShoppingBag, Star } from "lucide-react"
import Link from "next/link" import Link from "next/link"
import { useEffect, useState } from "react"
export default async function HomePage() { export default function HomePage() {
const [games, players, shops] = await Promise.all([listGames(), listPlayers(), listShops()]) 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 ( return (
<div className="container mx-auto py-8 px-4 space-y-12"> <div className="container mx-auto py-8 px-4 space-y-12">
@@ -42,6 +81,11 @@ export default async function HomePage() {
</div> </div>
</section> </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> <section>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold"></h2> <h2 className="text-xl font-semibold"></h2>
@@ -57,14 +101,16 @@ export default async function HomePage() {
</Button> </Button>
</div> </div>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{games.map((game) => ( {!loading && !loadingError
<Button key={game.id} variant="secondary" className="rounded-full" asChild> ? games.map((game) => (
<Link href={`/search?game=${encodeURIComponent(game.name)}`}> <Button key={game.id} variant="secondary" className="rounded-full" asChild>
<GameIcon name={game.icon} className="h-5 w-5" /> <Link href={`/search?game=${encodeURIComponent(game.name)}`}>
{game.name} <GameIcon name={game.icon} className="h-5 w-5" />
</Link> {game.name}
</Button> </Link>
))} </Button>
))
: null}
</div> </div>
</section> </section>
@@ -92,68 +138,74 @@ export default async function HomePage() {
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{/* Players */} {/* Players */}
{players.map((player) => ( {!loading && !loadingError
<Card ? players.map((player) => (
key={player.id} <Card
className="hover:shadow-md transition-shadow flex flex-col h-full border-muted/60" key={player.id}
> className="hover:shadow-md transition-shadow flex flex-col h-full border-muted/60"
<CardHeader className="flex flex-row items-center gap-3 space-y-0 pb-3"> >
<Avatar className="h-12 w-12"> <CardHeader className="flex flex-row items-center gap-3 space-y-0 pb-3">
<AvatarImage src={player.user.avatar} /> <Avatar className="h-12 w-12">
<AvatarFallback>{player.user.nickname[0]}</AvatarFallback> <AvatarImage src={player.user.avatar} />
</Avatar> <AvatarFallback>{player.user.nickname[0]}</AvatarFallback>
<div className="flex-1 min-w-0"> </Avatar>
<CardTitle className="text-base">{player.user.nickname}</CardTitle> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mt-1"> <CardTitle className="text-base">{player.user.nickname}</CardTitle>
<div className="flex items-center text-sm"> <div className="flex items-center gap-2 mt-1">
<Star className="h-3.5 w-3.5 fill-primary text-primary mr-0.5" /> <div className="flex items-center text-sm">
{player.rating} <Star className="h-3.5 w-3.5 fill-primary text-primary mr-0.5" />
{player.rating}
</div>
<span className="text-xs text-muted-foreground">
{player.totalOrders}
</span>
<Badge
variant={player.status === "available" ? "default" : "secondary"}
className="text-[10px] px-1.5 py-0 font-normal"
>
{player.status === "available"
? "可接单"
: player.status === "busy"
? "忙碌"
: "离线"}
</Badge>
</div>
</div> </div>
<span className="text-xs text-muted-foreground">{player.totalOrders} </span> </CardHeader>
<Badge <CardContent className="pb-3 flex-grow">
variant={player.status === "available" ? "default" : "secondary"} <div className="flex flex-wrap gap-1.5">
className="text-[10px] px-1.5 py-0 font-normal" {player.tags.map((tag) => (
> <Badge
{player.status === "available" key={tag}
? "可接单" variant="secondary"
: player.status === "busy" className="text-xs bg-muted/50 text-muted-foreground font-normal"
? "忙碌" >
: "离线"} {tag}
</Badge> </Badge>
</div> ))}
</div> </div>
</CardHeader> {player.shopName && (
<CardContent className="pb-3 flex-grow"> <p className="text-xs text-muted-foreground mt-3">
<div className="flex flex-wrap gap-1.5"> : {player.shopName}
{player.tags.map((tag) => ( </p>
<Badge )}
key={tag} </CardContent>
variant="secondary" <CardFooter className="pt-0 flex items-end justify-between">
className="text-xs bg-muted/50 text-muted-foreground font-normal" <div className="flex items-baseline gap-1">
> <span className="text-lg font-bold leading-none">
{tag} {player.services?.[0]?.price ?? 35}
</Badge> </span>
))} <span className="text-xs text-muted-foreground">
</div> /{player.services?.[0]?.unit ?? "时"}
{player.shopName && ( </span>
<p className="text-xs text-muted-foreground mt-3">: {player.shopName}</p> </div>
)} <Button variant="outline" size="sm" asChild>
</CardContent> <Link href={`/player/${player.id}`}></Link>
<CardFooter className="pt-0 flex items-end justify-between"> </Button>
<div className="flex items-baseline gap-1"> </CardFooter>
<span className="text-lg font-bold leading-none"> </Card>
{player.services?.[0]?.price ?? 35} ))
</span> : null}
<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>
))}
{/* Community Teaser */} {/* Community Teaser */}
<Card className="hover:shadow-md transition-shadow flex flex-col h-full border-muted/60"> <Card className="hover:shadow-md transition-shadow flex flex-col h-full border-muted/60">
@@ -174,35 +226,37 @@ export default async function HomePage() {
</Card> </Card>
{/* Shops */} {/* Shops */}
{shops.map((shop) => ( {!loading && !loadingError
<Card ? shops.map((shop) => (
key={shop.id} <Card
className="hover:shadow-md transition-shadow flex flex-col h-full border-muted/60" key={shop.id}
> className="hover:shadow-md transition-shadow flex flex-col h-full border-muted/60"
<CardHeader> >
<CardTitle className="text-lg">{shop.name}</CardTitle> <CardHeader>
<p className="text-sm text-muted-foreground">{shop.description}</p> <CardTitle className="text-lg">{shop.name}</CardTitle>
</CardHeader> <p className="text-sm text-muted-foreground">{shop.description}</p>
<CardContent className="flex-grow"> </CardHeader>
<div className="flex items-center gap-4 text-sm"> <CardContent className="flex-grow">
<div className="flex items-center"> <div className="flex items-center gap-4 text-sm">
<Star className="h-3.5 w-3.5 fill-primary text-primary mr-0.5" /> <div className="flex items-center">
{shop.rating} <Star className="h-3.5 w-3.5 fill-primary text-primary mr-0.5" />
</div> {shop.rating}
<div className="flex items-center text-muted-foreground"> </div>
<ShoppingBag className="h-3.5 w-3.5 mr-0.5" /> <div className="flex items-center text-muted-foreground">
{shop.totalOrders} <ShoppingBag className="h-3.5 w-3.5 mr-0.5" />
</div> {shop.totalOrders}
<span className="text-muted-foreground">{shop.playerCount} </span> </div>
</div> <span className="text-muted-foreground">{shop.playerCount} </span>
</CardContent> </div>
<CardFooter> </CardContent>
<Button variant="outline" size="sm" className="w-full" asChild> <CardFooter>
<Link href={`/shop/${shop.id}`}></Link> <Button variant="outline" size="sm" className="w-full" asChild>
</Button> <Link href={`/shop/${shop.id}`}></Link>
</CardFooter> </Button>
</Card> </CardFooter>
))} </Card>
))
: null}
</div> </div>
</section> </section>
</div> </div>