fix(home): load catalog client-side
This commit is contained in:
+154
-100
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user