first commit
This commit is contained in:
51
resources/js/Components/shared/BreadcrumbNav.tsx
Normal file
51
resources/js/Components/shared/BreadcrumbNav.tsx
Normal 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;
|
||||
60
resources/js/Components/shared/CopyButton.tsx
Normal file
60
resources/js/Components/shared/CopyButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
74
resources/js/Components/shared/Pagination.tsx
Normal file
74
resources/js/Components/shared/Pagination.tsx
Normal 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("«")) label = "Previous";
|
||||
if (label.includes("»")) 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>
|
||||
);
|
||||
}
|
||||
34
resources/js/Components/shared/SearchToolbar.tsx
Normal file
34
resources/js/Components/shared/SearchToolbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
BIN
resources/js/Components/shared/SearchToolbar.tsx:Zone.Identifier
Normal file
BIN
resources/js/Components/shared/SearchToolbar.tsx:Zone.Identifier
Normal file
Binary file not shown.
30
resources/js/Components/shared/StatsCard.tsx
Normal file
30
resources/js/Components/shared/StatsCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
BIN
resources/js/Components/shared/StatsCard.tsx:Zone.Identifier
Normal file
BIN
resources/js/Components/shared/StatsCard.tsx:Zone.Identifier
Normal file
Binary file not shown.
Reference in New Issue
Block a user