مدیریت فایلها
پیشنمایش تمامصفحهبارگذاری، فضای مصرفی، جستوجو و جدول فایلها با عملیات.
۱. نصب کامپوننتها
این بلاک از ۱۱ آیتم دیگ استفاده میکند: buttoncontrol-sizedropdown-menuempty-statefile-uploadiconspersianprogresssearch-fieldtabletoast
پکیجمنیجر پیشفرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیجمنیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.jsonpnpm dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.jsonyarn dlx digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.jsonbunx --bun digdesign@latest add https://docs.digdesign.ir/r/button.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/dropdown-menu.json https://docs.digdesign.ir/r/empty-state.json https://docs.digdesign.ir/r/file-upload.json https://docs.digdesign.ir/r/icons.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/search-field.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toast.json۲. کد صفحه
کد را در app/<route>/page.tsx بگذارید و متنها، داده و هندلرها را با محصول خودتان عوض کنید.
file-manager.tsx
"use client"
import * as React from "react"
import { DownloadIcon, EditIcon, MoreHorizontalIcon, TrashIcon } from "@/components/icons"
import { Button } from "@/components/ui/button"
import { ControlSizeProvider } from "@/components/ui/control-size"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { EmptyState, EmptyStateDescription, EmptyStateTitle } from "@/components/ui/empty-state"
import { FileUpload } from "@/components/ui/file-upload"
import { Progress } from "@/components/ui/progress"
import { SearchField } from "@/components/ui/search-field"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { toast } from "@/components/ui/toast"
import { formatJalali, formatNumber, toSearchKey } from "@/lib/persian"
type StoredFile = { id: string; name: string; size: number; date: string }
const INITIAL: StoredFile[] = [
{ id: "1", name: "قرارداد-همکاری-۱۴۰۵.pdf", size: 1_240_000, date: "2026-09-28" },
{ id: "2", name: "گزارش-فروش-شهریور.xlsx", size: 380_000, date: "2026-09-22" },
{ id: "3", name: "لوگو-نهایی.png", size: 96_000, date: "2026-09-10" },
]
const QUOTA = 50 * 1024 * 1024
function formatSize(bytes: number) {
if (bytes >= 1024 * 1024) return `${formatNumber(Number((bytes / 1024 / 1024).toFixed(1)))} مگابایت`
return `${formatNumber(Math.round(bytes / 1024))} کیلوبایت`
}
export default function FileManagerBlock() {
const [files, setFiles] = React.useState(INITIAL)
const [query, setQuery] = React.useState("")
const used = files.reduce((sum, file) => sum + file.size, 0)
const visible = files.filter((file) => toSearchKey(file.name).includes(toSearchKey(query)))
return (
<ControlSizeProvider size="xl" asChild>
<main className="mx-auto flex w-full max-w-4xl flex-col gap-6 p-4 sm:p-6">
<div className="flex flex-wrap items-end justify-between gap-4">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold">فایلها</h1>
<p className="text-sm text-muted-foreground">اسناد و تصاویر حساب؛ هر فایل تا ۱۰ مگابایت.</p>
</div>
<div className="flex w-full max-w-xs flex-col gap-1 text-sm">
<Progress value={(used / QUOTA) * 100} size="sm" aria-label="فضای استفادهشده" />
<span className="text-muted-foreground">
{formatSize(used)} از {formatSize(QUOTA)}
</span>
</div>
</div>
<FileUpload
name="files"
multiple
maxSize={10 * 1024 * 1024}
hint="هر فایل تا ۱۰ مگابایت"
onFilesChange={(added) =>
setFiles((list) => [
...added
.filter((file) => !list.some((item) => item.name === file.name))
.map((file) => ({ id: `${file.name}-${file.size}`, name: file.name, size: file.size, date: new Date().toISOString() })),
...list,
])
}
/>
<SearchField aria-label="جستوجو در فایلها" placeholder="نام فایل" value={query} onValueChange={setQuery} className="max-w-sm" />
{visible.length === 0 ? (
<EmptyState variant="bordered">
<EmptyStateTitle>{files.length === 0 ? "هنوز فایلی بارگذاری نکردهاید" : "فایلی با این نام پیدا نشد"}</EmptyStateTitle>
<EmptyStateDescription>فایلها را در کادر بالا رها کنید.</EmptyStateDescription>
</EmptyState>
) : (
<Table stackOnMobile>
<TableHeader>
<TableRow>
<TableHead>نام</TableHead>
<TableHead>حجم</TableHead>
<TableHead>تاریخ</TableHead>
<TableHead className="w-12">
<span className="sr-only">عملیات</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{visible.map((file) => (
<TableRow key={file.id}>
<TableCell className="max-w-64 truncate">
<bdi>{file.name}</bdi>
</TableCell>
<TableCell className="text-muted-foreground">{formatSize(file.size)}</TableCell>
<TableCell className="text-muted-foreground">{formatJalali(new Date(file.date))}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" aria-label={`عملیات ${file.name}`}>
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<DownloadIcon />
دانلود
</DropdownMenuItem>
<DropdownMenuItem>
<EditIcon />
تغییر نام
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
variant="destructive"
onSelect={() => {
const index = files.findIndex((item) => item.id === file.id)
setFiles((list) => list.filter((item) => item.id !== file.id))
toast(`«${file.name}» حذف شد.`, { action: { label: "برگردان", onClick: () => setFiles((list) => (list.some((item) => item.id === file.id) ? list : [...list.slice(0, index), file, ...list.slice(index)])) } })
}}
>
<TrashIcon />
حذف
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</main>
</ControlSizeProvider>
)
}