Skip to content

Commit fc0a344

Browse files
committed
feat(product): Add product listing and details components
1 parent 1e786b2 commit fc0a344

File tree

8 files changed

+425
-12
lines changed

8 files changed

+425
-12
lines changed

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
VITE_APP_API_URL=
1+
VITE_APP_API_URL=
2+
VITE_APP_API_PATH=

app/components/ui/badge.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as React from "react"
2+
import { cva, type VariantProps } from "class-variance-authority"
3+
4+
import { cn } from "~/lib/utils"
5+
6+
const badgeVariants = cva(
7+
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8+
{
9+
variants: {
10+
variant: {
11+
default:
12+
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13+
secondary:
14+
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15+
destructive:
16+
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17+
outline: "text-foreground",
18+
},
19+
},
20+
defaultVariants: {
21+
variant: "default",
22+
},
23+
}
24+
)
25+
26+
export interface BadgeProps
27+
extends React.HTMLAttributes<HTMLDivElement>,
28+
VariantProps<typeof badgeVariants> {}
29+
30+
function Badge({ className, variant, ...props }: BadgeProps) {
31+
return (
32+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
33+
)
34+
}
35+
36+
export { Badge, badgeVariants }

app/components/ui/table.tsx

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import * as React from "react"
2+
3+
import { cn } from "~/lib/utils"
4+
5+
const Table = React.forwardRef<
6+
HTMLTableElement,
7+
React.HTMLAttributes<HTMLTableElement>
8+
>(({ className, ...props }, ref) => (
9+
<div className="relative w-full overflow-auto">
10+
<table
11+
ref={ref}
12+
className={cn("w-full caption-bottom text-sm", className)}
13+
{...props}
14+
/>
15+
</div>
16+
))
17+
Table.displayName = "Table"
18+
19+
const TableHeader = React.forwardRef<
20+
HTMLTableSectionElement,
21+
React.HTMLAttributes<HTMLTableSectionElement>
22+
>(({ className, ...props }, ref) => (
23+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
24+
))
25+
TableHeader.displayName = "TableHeader"
26+
27+
const TableBody = React.forwardRef<
28+
HTMLTableSectionElement,
29+
React.HTMLAttributes<HTMLTableSectionElement>
30+
>(({ className, ...props }, ref) => (
31+
<tbody
32+
ref={ref}
33+
className={cn("[&_tr:last-child]:border-0", className)}
34+
{...props}
35+
/>
36+
))
37+
TableBody.displayName = "TableBody"
38+
39+
const TableFooter = React.forwardRef<
40+
HTMLTableSectionElement,
41+
React.HTMLAttributes<HTMLTableSectionElement>
42+
>(({ className, ...props }, ref) => (
43+
<tfoot
44+
ref={ref}
45+
className={cn(
46+
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
47+
className
48+
)}
49+
{...props}
50+
/>
51+
))
52+
TableFooter.displayName = "TableFooter"
53+
54+
const TableRow = React.forwardRef<
55+
HTMLTableRowElement,
56+
React.HTMLAttributes<HTMLTableRowElement>
57+
>(({ className, ...props }, ref) => (
58+
<tr
59+
ref={ref}
60+
className={cn(
61+
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
62+
className
63+
)}
64+
{...props}
65+
/>
66+
))
67+
TableRow.displayName = "TableRow"
68+
69+
const TableHead = React.forwardRef<
70+
HTMLTableCellElement,
71+
React.ThHTMLAttributes<HTMLTableCellElement>
72+
>(({ className, ...props }, ref) => (
73+
<th
74+
ref={ref}
75+
className={cn(
76+
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
77+
className
78+
)}
79+
{...props}
80+
/>
81+
))
82+
TableHead.displayName = "TableHead"
83+
84+
const TableCell = React.forwardRef<
85+
HTMLTableCellElement,
86+
React.TdHTMLAttributes<HTMLTableCellElement>
87+
>(({ className, ...props }, ref) => (
88+
<td
89+
ref={ref}
90+
className={cn(
91+
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
92+
className
93+
)}
94+
{...props}
95+
/>
96+
))
97+
TableCell.displayName = "TableCell"
98+
99+
const TableCaption = React.forwardRef<
100+
HTMLTableCaptionElement,
101+
React.HTMLAttributes<HTMLTableCaptionElement>
102+
>(({ className, ...props }, ref) => (
103+
<caption
104+
ref={ref}
105+
className={cn("mt-4 text-sm text-muted-foreground", className)}
106+
{...props}
107+
/>
108+
))
109+
TableCaption.displayName = "TableCaption"
110+
111+
export {
112+
Table,
113+
TableHeader,
114+
TableBody,
115+
TableFooter,
116+
TableHead,
117+
TableRow,
118+
TableCell,
119+
TableCaption,
120+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
2+
import { Badge } from "@/components/ui/badge";
3+
4+
import { type Product } from "~/features/product";
5+
6+
type ProductContentPropsType = {
7+
product: Product;
8+
};
9+
10+
export default function ProductContent(props: ProductContentPropsType) {
11+
const { product } = props;
12+
13+
return (
14+
<Card className="border-0 shadow-xl bg-white/95 backdrop-blur-sm">
15+
<CardHeader className="bg-amber-100/50">
16+
<CardTitle className="text-amber-900 font-serif">產品細節</CardTitle>
17+
</CardHeader>
18+
19+
<CardContent className="p-6">
20+
{product ? (
21+
<>
22+
<div className="relative mb-4">
23+
<img
24+
src={product.imageUrl}
25+
className="w-full h-64 object-cover rounded-md"
26+
alt={product.title}
27+
/>
28+
<Badge className="absolute top-2 right-2 bg-amber-100 text-amber-800 hover:bg-amber-200">
29+
{product.category}
30+
</Badge>
31+
</div>
32+
33+
<h3 className="text-xl font-semibold mb-3 text-amber-900">
34+
{product.title}
35+
</h3>
36+
37+
<div className="mb-4">
38+
<h4 className="text-sm font-semibold mb-2 text-amber-800">
39+
商品描述
40+
</h4>
41+
<p className="text-amber-700">{product.description}</p>
42+
</div>
43+
44+
<div className="mb-4">
45+
<h4 className="text-sm font-semibold mb-2 text-amber-800">
46+
商品規格
47+
</h4>
48+
<p className="text-amber-700">{product.content}</p>
49+
</div>
50+
51+
<div className="flex items-center mb-4">
52+
<del className="text-amber-400 mr-2">
53+
NT$ {product.origin_price}
54+
</del>
55+
<span className="text-xl font-semibold text-red-700">
56+
NT$ {product.price}
57+
</span>
58+
<small className="text-amber-600 ml-2">/ {product.unit}</small>
59+
</div>
60+
61+
{product.imagesUrl?.length > 0 && (
62+
<div>
63+
<h4 className="text-sm font-semibold mb-3 text-amber-800 hidden">
64+
更多圖片
65+
</h4>
66+
<div className="grid grid-cols-2 gap-2">
67+
{product.imagesUrl.map((url) =>
68+
url ? (
69+
<div key={url}>
70+
<img
71+
src={url}
72+
className="w-full h-32 object-cover rounded-md"
73+
alt="商品圖片"
74+
/>
75+
</div>
76+
) : null
77+
)}
78+
</div>
79+
</div>
80+
)}
81+
</>
82+
) : (
83+
<div className="text-center py-12 text-amber-800">
84+
<svg
85+
xmlns="http://www.w3.org/2000/svg"
86+
className="mx-auto h-12 w-12 mb-3"
87+
fill="none"
88+
viewBox="0 0 24 24"
89+
stroke="currentColor"
90+
>
91+
<path
92+
strokeLinecap="round"
93+
strokeLinejoin="round"
94+
strokeWidth={2}
95+
d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"
96+
/>
97+
</svg>
98+
<p className="text-amber-700">請選擇商品以查看詳細資訊</p>
99+
</div>
100+
)}
101+
</CardContent>
102+
</Card>
103+
);
104+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
2+
import {
3+
Table,
4+
TableBody,
5+
TableCell,
6+
TableHead,
7+
TableHeader,
8+
TableRow,
9+
} from "@/components/ui/table";
10+
import { Badge } from "@/components/ui/badge";
11+
import { Button } from "@/components/ui/button";
12+
13+
import { type Product } from "~/features/product";
14+
15+
type ProductListPropsType = {
16+
products: Product[];
17+
onSelectProductId: (productId: string) => void;
18+
};
19+
20+
export default function ProductList(props: ProductListPropsType) {
21+
const { products, onSelectProductId } = props;
22+
23+
return (
24+
<Card className="border-0 shadow-xl bg-white/95 backdrop-blur-sm">
25+
<CardHeader className="bg-amber-100/50">
26+
<CardTitle className="text-amber-900 font-serif">產品列表</CardTitle>
27+
</CardHeader>
28+
29+
<CardContent className="p-0">
30+
<Table>
31+
<TableHeader className="bg-amber-50/50">
32+
<TableRow>
33+
<TableHead className="px-4 text-amber-800">產品名稱</TableHead>
34+
<TableHead className="text-center text-amber-800">原價</TableHead>
35+
<TableHead className="text-center text-amber-800">售價</TableHead>
36+
<TableHead className="text-center text-amber-800">狀態</TableHead>
37+
<TableHead className="text-center text-amber-800">操作</TableHead>
38+
</TableRow>
39+
</TableHeader>
40+
41+
<TableBody>
42+
{products.map((item) => (
43+
<TableRow key={item.id}>
44+
<TableCell className="px-4 text-amber-900">
45+
{item.title}
46+
</TableCell>
47+
<TableCell className="text-center text-amber-600">
48+
<del>NT$ {item.origin_price}</del>
49+
</TableCell>
50+
<TableCell className="text-center text-red-700 font-semibold">
51+
NT$ {item.price}
52+
</TableCell>
53+
<TableCell className="text-center">
54+
<Badge
55+
variant="secondary"
56+
className={
57+
item.is_enabled === 1
58+
? "bg-amber-100 text-amber-800 hover:bg-amber-200"
59+
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
60+
}
61+
>
62+
{item.is_enabled === 1 ? "啟用" : "未啟用"}
63+
</Badge>
64+
</TableCell>
65+
<TableCell className="text-center">
66+
<Button
67+
variant="outline"
68+
size="sm"
69+
className="rounded-full bg-amber-700 text-amber-50 hover:bg-amber-800 border-0"
70+
onClick={() => onSelectProductId(item.id)}
71+
>
72+
查看細節
73+
</Button>
74+
</TableCell>
75+
</TableRow>
76+
))}
77+
</TableBody>
78+
</Table>
79+
</CardContent>
80+
</Card>
81+
);
82+
}

0 commit comments

Comments
 (0)