first commit

This commit is contained in:
2025-12-30 15:03:19 +08:00
commit c735c36009
902 changed files with 83591 additions and 0 deletions

View File

@@ -0,0 +1,51 @@
import React from "react";
import { Link } from "@inertiajs/react";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/Components/ui/breadcrumb";
export interface BreadcrumbItemType {
label: string;
href?: string;
isPage?: boolean;
}
interface BreadcrumbNavProps {
items: BreadcrumbItemType[];
className?: string;
}
const BreadcrumbNav = ({ items, className }: BreadcrumbNavProps) => {
return (
<Breadcrumb className={className}>
<BreadcrumbList>
{items.map((item, index) => (
<React.Fragment key={index}>
<BreadcrumbItem>
{item.isPage ? (
<BreadcrumbPage className="text-gray-500">{item.label}</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
<Link
href={item.href || "#"}
className="text-[#01ab83] hover:text-[#018a6a] font-medium transition-colors"
>
{item.label}
</Link>
</BreadcrumbLink>
)}
</BreadcrumbItem>
{index < items.length - 1 && <BreadcrumbSeparator />}
</React.Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
);
};
export default BreadcrumbNav;

View File

@@ -0,0 +1,60 @@
/**
* 點擊複製按鈕組件
*/
import { useState } from "react";
import { Check, Copy } from "lucide-react";
import { Button } from "@/Components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/Components/ui/tooltip";
import { toast } from "sonner";
interface CopyButtonProps {
text: string;
label?: string;
className?: string;
}
export default function CopyButton({ text, label = "複製", className }: CopyButtonProps) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
toast.success(`${label}已複製`);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
toast.error("複製失敗");
}
};
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className={`h-6 w-6 p-0 hover:bg-gray-100 ${className}`}
onClick={handleCopy}
>
{copied ? (
<Check className="h-3.5 w-3.5 text-green-600" />
) : (
<Copy className="h-3.5 w-3.5 text-gray-400" />
)}
<span className="sr-only">{label}</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{copied ? "已複製!" : label}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View File

@@ -0,0 +1,74 @@
import { Link } from "@inertiajs/react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { cn } from "@/lib/utils";
interface PaginationProps {
links: {
url: string | null;
label: string;
active: boolean;
}[];
className?: string;
}
export default function Pagination({ links, className }: PaginationProps) {
// 如果只有一頁,不顯示分頁
if (links.length <= 3) return null;
return (
<div className={cn("flex flex-wrap justify-center gap-1", className)}>
{links.map((link, key) => {
// 處理特殊標籤
let label = link.label;
if (label.includes("&laquo;")) label = "Previous";
if (label.includes("&raquo;")) label = "Next";
const isPrevious = label === "Previous";
const isNext = label === "Next";
// 如果是 Previous/Next 但沒有 URL則不渲染或者渲染為 disabled
if ((isPrevious || isNext) && !link.url) {
return (
<div
key={key}
className={cn(
"flex h-9 items-center justify-center rounded-md border border-input bg-transparent px-3 text-sm text-muted-foreground opacity-50 cursor-not-allowed",
isPrevious || isNext ? "px-2" : ""
)}
>
{isPrevious && <ChevronLeft className="h-4 w-4" />}
{isNext && <ChevronRight className="h-4 w-4" />}
{!isPrevious && !isNext && <span dangerouslySetInnerHTML={{ __html: link.label }} />}
</div>
);
}
return link.url ? (
<Link
key={key}
href={link.url}
preserveScroll
className={cn(
"flex h-9 items-center justify-center rounded-md border px-3 text-sm transition-colors hover:bg-accent hover:text-accent-foreground",
link.active
? "border-primary bg-primary text-primary-foreground hover:bg-primary/90 hover:text-primary-foreground"
: "border-input bg-transparent text-foreground",
isPrevious || isNext ? "px-2" : ""
)}
>
{isPrevious && <ChevronLeft className="h-4 w-4" />}
{isNext && <ChevronRight className="h-4 w-4" />}
{!isPrevious && !isNext && <span dangerouslySetInnerHTML={{ __html: link.label }} />}
</Link>
) : (
<div
key={key}
className="flex h-9 items-center justify-center rounded-md border border-input bg-transparent px-3 text-sm text-foreground"
>
<span dangerouslySetInnerHTML={{ __html: link.label }} />
</div>
);
})}
</div>
);
}

View File

@@ -0,0 +1,34 @@
/**
* 搜尋工具列元件
* 提供搜尋輸入框功能
*/
import { Search } from "lucide-react";
import { Input } from "../ui/input";
interface SearchToolbarProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
className?: string;
}
export default function SearchToolbar({
value,
onChange,
placeholder = "搜尋...",
className = "",
}: SearchToolbarProps) {
return (
<div className={`relative ${className}`}>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
className="pl-10"
/>
</div>
);
}

View File

@@ -0,0 +1,30 @@
/**
* 統計卡片元件
* 用於顯示各種統計資訊
*/
import { LucideIcon } from "lucide-react";
interface StatsCardProps {
label: string;
value: string | number;
icon?: LucideIcon;
valueClassName?: string;
}
export default function StatsCard({
label,
value,
icon: Icon,
valueClassName = "text-primary-main",
}: StatsCardProps) {
return (
<div className="bg-white rounded-lg shadow-sm border p-5">
<div className="flex items-center gap-2 text-grey-2 mb-1">
{Icon && <Icon className="h-4 w-4" />}
<span>{label}</span>
</div>
<div className={valueClassName}>{value}</div>
</div>
);
}