Files
juwan-frontend/app/(main)/page.tsx
T
2026-04-25 20:12:23 +08:00

264 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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-shadow focus-visible:shadow-md"
/>
</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>
)
}