Files
juwan-frontend/app/(main)/shop/[id]/page.tsx
T
2026-04-25 14:49:57 +08:00

277 lines
13 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.
import { FavoriteButton } from "@/components/favorite-button"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Separator } from "@/components/ui/separator"
import { getShopById, listPlayersByShop, listReviews, listServices } from "@/lib/api"
import { getShopSections } from "@/lib/domain/shop-template"
import { Gamepad2, Megaphone, ShoppingBag, Star, Users } from "lucide-react"
import Image from "next/image"
import Link from "next/link"
import { notFound } from "next/navigation"
interface PageProps {
params: Promise<{ id: string }>
}
export default async function ShopPage({ params }: PageProps) {
const { id } = await params
const shop = await getShopById(id)
if (!shop) {
notFound()
}
const [shopPlayers, allServices] = await Promise.all([listPlayersByShop(shop.id), listServices()])
const playerIds = shopPlayers.map((p) => p.id)
const shopServices = allServices.filter((s) => playerIds.includes(s.playerId))
const shopReviews = await listReviews()
const sortedSections = getShopSections(shop).filter((s) => s.enabled)
return (
<div className="container mx-auto py-6 space-y-8">
{sortedSections.map((section) => {
switch (section.type) {
case "banner":
return (
<div
key="banner"
className="relative h-48 md:h-64 rounded-xl overflow-hidden bg-muted"
>
{shop.banner ? (
<Image src={shop.banner} alt={shop.name} fill className="object-cover" />
) : (
<div className="w-full h-full bg-gradient-to-r from-primary/20 to-primary/10" />
)}
<div className="absolute inset-0 bg-black/40 flex items-center justify-center">
<h1 className="text-3xl md:text-5xl font-bold text-primary-foreground text-center px-4">
{shop.name}
</h1>
</div>
</div>
)
case "intro":
return (
<Card key="intro">
<CardContent className="pt-6">
<div className="flex flex-col md:flex-row gap-6 items-start">
<div className="flex-1 space-y-4">
<h2 className="text-2xl font-bold">{shop.name}</h2>
<p className="text-muted-foreground">{shop.description}</p>
<div className="flex flex-wrap gap-4 text-sm">
<div className="flex items-center gap-1">
<Star className="w-4 h-4 text-yellow-500 fill-yellow-500" />
<span className="font-medium">{shop.rating}</span>
<span className="text-muted-foreground"></span>
</div>
<Separator orientation="vertical" className="h-4" />
<div className="flex items-center gap-1">
<ShoppingBag className="w-4 h-4" />
<span className="font-medium">{shop.totalOrders}</span>
<span className="text-muted-foreground"></span>
</div>
<Separator orientation="vertical" className="h-4" />
<div className="flex items-center gap-1">
<Users className="w-4 h-4" />
<span className="font-medium">{shop.playerCount}</span>
<span className="text-muted-foreground"></span>
</div>
</div>
</div>
<div className="flex items-center gap-4">
<Avatar className="w-16 h-16">
<AvatarImage src={shop.owner.avatar} />
<AvatarFallback>{shop.owner.nickname[0] ?? "店"}</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">{shop.owner.nickname}</p>
<p className="text-sm text-muted-foreground">
{shop.owner.bio || "暂无介绍"}
</p>
</div>
<FavoriteButton
initialFavorited={false}
targetType="shop"
targetId={shop.id}
/>
</div>
</div>
</CardContent>
</Card>
)
case "announcements":
if (shop.announcements.length === 0) return null
return (
<Card key="announcements">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Megaphone className="w-5 h-5 text-primary" />
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{shop.announcements.map((announcement) => (
<li key={announcement} className="flex items-start gap-2 text-sm">
<span className="mt-1.5 w-1.5 h-1.5 rounded-full bg-primary shrink-0" />
<span>{announcement}</span>
</li>
))}
</ul>
</CardContent>
</Card>
)
case "services":
if (shopServices.length === 0) return null
return (
<div key="services" className="space-y-4">
<h3 className="text-xl font-bold flex items-center gap-2">
<Gamepad2 className="w-5 h-5" />
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{shopServices.map((service) => (
<Card key={service.id} className="flex flex-col h-full">
<CardHeader className="pb-2">
<div className="flex justify-between items-start">
<Badge variant="outline">{service.gameName}</Badge>
<div className="text-right">
<span className="text-lg font-bold text-primary">¥{service.price}</span>
<span className="text-xs text-muted-foreground">/{service.unit}</span>
</div>
</div>
<CardTitle className="text-base mt-2">{service.title}</CardTitle>
</CardHeader>
<CardContent className="flex-grow flex flex-col">
<p className="text-sm text-muted-foreground line-clamp-2 mb-2">
{service.description}
</p>
{service.rankRange && (
<div className="text-xs bg-muted px-2 py-1 rounded-md inline-block w-fit">
{service.rankRange}
</div>
)}
<div className="mt-auto pt-3">
<Button className="w-full" size="sm" asChild>
<Link href={`/order/new?serviceId=${service.id}`}></Link>
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
)
case "players":
if (shopPlayers.length === 0) return null
return (
<div key="players" className="space-y-4">
<h3 className="text-xl font-bold flex items-center gap-2">
<Users className="w-5 h-5" />
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{shopPlayers.map((player) => (
<Link key={player.id} href={`/player/${player.id}`}>
<Card className="h-full">
<CardContent className="pt-6 text-center space-y-3">
<div className="relative inline-block">
<Avatar className="w-20 h-20 mx-auto">
<AvatarImage src={player.user.avatar} />
<AvatarFallback>{player.user.nickname[0]}</AvatarFallback>
</Avatar>
<span
className={`absolute bottom-0 right-0 w-4 h-4 border-2 border-background rounded-full ${
player.status === "available"
? "bg-green-500"
: player.status === "busy"
? "bg-yellow-500"
: "bg-gray-500"
}`}
/>
</div>
<div>
<h4 className="font-bold">{player.user.nickname}</h4>
<div className="flex items-center justify-center gap-1 text-sm text-muted-foreground mt-1">
<Star className="w-3 h-3 fill-yellow-500 text-yellow-500" />
<span>{player.rating}</span>
<span></span>
<span>{player.totalOrders}</span>
</div>
</div>
<div className="flex flex-wrap justify-center gap-1">
{player.games.slice(0, 3).map((game) => (
<Badge key={game} variant="secondary" className="text-xs">
{game}
</Badge>
))}
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</div>
)
case "reviews":
if (shopReviews.length === 0) return null
return (
<div key="reviews" className="space-y-4">
<h3 className="text-xl font-bold flex items-center gap-2">
<Star className="w-5 h-5" />
</h3>
<div className="space-y-4">
{shopReviews.map((review) => (
<Card key={review.id}>
<CardContent className="pt-6">
<div className="flex gap-4">
<Avatar>
<AvatarImage src="" />
<AvatarFallback>{review.fromUserName[0]}</AvatarFallback>
</Avatar>
<div className="flex-1 space-y-1">
<div className="flex justify-between items-start">
<div>
<p className="font-medium text-sm">{review.fromUserName}</p>
<div className="flex items-center gap-0.5">
{[1, 2, 3, 4, 5].map((star) => (
<Star
key={`star-${star}`}
className={`w-3 h-3 ${
star <= review.rating
? "fill-yellow-500 text-yellow-500"
: "text-muted-foreground"
}`}
/>
))}
</div>
</div>
<span className="text-xs text-muted-foreground">
{new Date(review.createdAt).toLocaleDateString()}
</span>
</div>
<p className="text-sm text-muted-foreground">{review.content}</p>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
)
default:
return null
}
})}
</div>
)
}