Files

315 lines
14 KiB
TypeScript
Raw Permalink 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 { EmptyState } from "@/components/ui/empty-state"
import { Separator } from "@/components/ui/separator"
import { getShopById, listPlayersByShop, listReviews } 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 = await listPlayersByShop(shop.id)
const shopServices = shopPlayers.flatMap((player) => player.services)
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 text-warning">
<Star className="w-4 h-4 fill-warning" />
<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 (
<EmptyState
key="announcements"
title="暂无公告"
description="店长还没有发布任何公告内容"
icon={Megaphone}
/>
)
}
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 (
<EmptyState
key="services"
title="暂无服务"
description="这家店铺还没上架任何服务"
icon={Gamepad2}
/>
)
}
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="info" className="font-normal">
{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 (
<EmptyState
key="players"
title="暂无打手"
description="这家店目前还没有招募打手"
icon={Users}
/>
)
}
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-success"
: player.status === "busy"
? "bg-warning"
: "bg-neutral"
}`}
/>
</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-warning text-warning" />
<span className="text-foreground">{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 (
<EmptyState
key="reviews"
title="暂无评价"
description="这家店还没收到任何评价"
icon={Star}
/>
)
}
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-warning text-warning"
: "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>
)
}