mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
feat(files): add file management components and hooks
Introduce new components and hooks for managing files, including file list, search bar, empty state, and modals. This includes the addition of a breadcrumb component for better navigation and the use of client-side rendering for specific components. The changes aim to improve the user experience and maintainability of the file management system.
This commit is contained in:
@@ -68,8 +68,8 @@
|
|||||||
"downloadError": "Failed to download file"
|
"downloadError": "Failed to download file"
|
||||||
},
|
},
|
||||||
"fileSelector": {
|
"fileSelector": {
|
||||||
"availableFiles": "Available Files ({{count}})",
|
"availableFiles": "Available Files ({count})",
|
||||||
"shareFiles": "Share Files ({{count}})",
|
"shareFiles": "Share Files ({count})",
|
||||||
"searchPlaceholder": "Search files...",
|
"searchPlaceholder": "Search files...",
|
||||||
"noMatchingFiles": "No matching files",
|
"noMatchingFiles": "No matching files",
|
||||||
"noAvailableFiles": "No files available",
|
"noAvailableFiles": "No files available",
|
||||||
@@ -249,7 +249,7 @@
|
|||||||
"recipientSelector": {
|
"recipientSelector": {
|
||||||
"emailPlaceholder": "Enter recipient email",
|
"emailPlaceholder": "Enter recipient email",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"recipients": "Recipients ({{count}})",
|
"recipients": "Recipients ({count})",
|
||||||
"notifyAll": "Notify All",
|
"notifyAll": "Notify All",
|
||||||
"noRecipients": "No recipients added yet",
|
"noRecipients": "No recipients added yet",
|
||||||
"addSuccess": "Recipient added successfully",
|
"addSuccess": "Recipient added successfully",
|
||||||
@@ -285,7 +285,7 @@
|
|||||||
},
|
},
|
||||||
"searchBar": {
|
"searchBar": {
|
||||||
"placeholder": "Search files...",
|
"placeholder": "Search files...",
|
||||||
"results": "Found {{filtered}} of {{total}} files"
|
"results": "Found {filtered} of {total} files"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"groups": {
|
"groups": {
|
||||||
@@ -379,7 +379,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"save": "Save {{group}}"
|
"save": "Save {group}"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"loadFailed": "Failed to load settings",
|
"loadFailed": "Failed to load settings",
|
||||||
@@ -387,7 +387,7 @@
|
|||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"noChanges": "No changes to save",
|
"noChanges": "No changes to save",
|
||||||
"updateSuccess": "{{group}} settings updated successfully"
|
"updateSuccess": "{group} settings updated successfully"
|
||||||
},
|
},
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"breadcrumb": "Settings",
|
"breadcrumb": "Settings",
|
||||||
@@ -412,8 +412,8 @@
|
|||||||
},
|
},
|
||||||
"details": {
|
"details": {
|
||||||
"untitled": "Untitled Share",
|
"untitled": "Untitled Share",
|
||||||
"created": "Created: {{date}}",
|
"created": "Created: {date}",
|
||||||
"expires": "Expires: {{date}}"
|
"expires": "Expires: {date}"
|
||||||
},
|
},
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"title": "Share Not Found",
|
"title": "Share Not Found",
|
||||||
@@ -498,7 +498,7 @@
|
|||||||
"title": "All Shares",
|
"title": "All Shares",
|
||||||
"createButton": "Create Share",
|
"createButton": "Create Share",
|
||||||
"placeholder": "Search shares...",
|
"placeholder": "Search shares...",
|
||||||
"results": "Found {{filtered}} of {{total}} shares"
|
"results": "Found {filtered} of {total} shares"
|
||||||
},
|
},
|
||||||
"pageTitle": "Shares"
|
"pageTitle": "Shares"
|
||||||
},
|
},
|
||||||
@@ -561,7 +561,7 @@
|
|||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"loadFailed": "Failed to load users",
|
"loadFailed": "Failed to load users",
|
||||||
"submitFailed": "Failed to {{mode}} user",
|
"submitFailed": "Failed to {mode} user",
|
||||||
"deleteFailed": "Failed to delete user",
|
"deleteFailed": "Failed to delete user",
|
||||||
"statusUpdateFailed": "Failed to update user status"
|
"statusUpdateFailed": "Failed to update user status"
|
||||||
},
|
},
|
||||||
@@ -580,7 +580,7 @@
|
|||||||
},
|
},
|
||||||
"delete": {
|
"delete": {
|
||||||
"title": "Confirm Delete User",
|
"title": "Confirm Delete User",
|
||||||
"confirmation": "Are you sure you want to delete user {{firstName}} {{lastName}}? This action cannot be undone.",
|
"confirmation": "Are you sure you want to delete user {firstName} {lastName}? This action cannot be undone.",
|
||||||
"confirm": "Delete User"
|
"confirm": "Delete User"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
@@ -601,7 +601,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"title": "Confirm Status Change",
|
"title": "Confirm Status Change",
|
||||||
"confirmation": "Are you sure you want to {{action}} user {{firstName}} {{lastName}}?",
|
"confirmation": "Are you sure you want to {action} user {firstName} {lastName}?",
|
||||||
"activate": "activate",
|
"activate": "activate",
|
||||||
"deactivate": "deactivate",
|
"deactivate": "deactivate",
|
||||||
"user": "User"
|
"user": "User"
|
||||||
|
18
apps/app/src/app/dashboard/layout.tsx
Normal file
18
apps/app/src/app/dashboard/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Metadata } from "next";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
|
const translate = await getTranslations();
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: translate("dashboard.pageTitle"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardLayout({ children }: LayoutProps) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
19
apps/app/src/app/files/components/empty-state.tsx
Normal file
19
apps/app/src/app/files/components/empty-state.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { EmptyStateProps } from "../types";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { IconCloudUpload, IconFolder } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export function EmptyState({ onUpload }: EmptyStateProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-center py-6 flex flex-col items-center gap-2">
|
||||||
|
<IconFolder className="h-8 w-8 text-muted-foreground" />
|
||||||
|
<p className="text-muted-foreground">{t("emptyState.noFiles")}</p>
|
||||||
|
<Button variant="default" size="sm" onClick={onUpload}>
|
||||||
|
<IconCloudUpload className="mr-2 h-4 w-4" />
|
||||||
|
{t("emptyState.uploadFile")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
36
apps/app/src/app/files/components/file-list.tsx
Normal file
36
apps/app/src/app/files/components/file-list.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { FileListProps } from "../types";
|
||||||
|
import { EmptyState } from "./empty-state";
|
||||||
|
import { Header } from "./header";
|
||||||
|
import { SearchBar } from "./search-bar";
|
||||||
|
import { FilesTable } from "@/components/tables/files-table";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
|
||||||
|
export function FileList({ files, filteredFiles, fileManager, searchQuery, onSearch, onUpload }: FileListProps) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<Header onUpload={onUpload} />
|
||||||
|
<SearchBar
|
||||||
|
filteredCount={filteredFiles.length}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
totalFiles={files.length}
|
||||||
|
onSearch={onSearch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{files.length > 0 ? (
|
||||||
|
<FilesTable
|
||||||
|
files={filteredFiles}
|
||||||
|
onDelete={fileManager.setFileToDelete}
|
||||||
|
onDownload={fileManager.handleDownload}
|
||||||
|
onPreview={fileManager.setPreviewFile}
|
||||||
|
onRename={fileManager.setFileToRename}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EmptyState onUpload={onUpload} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
18
apps/app/src/app/files/components/header.tsx
Normal file
18
apps/app/src/app/files/components/header.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { HeaderProps } from "../types";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { IconCloudUpload } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export function Header({ onUpload }: HeaderProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h2 className="text-xl font-semibold">{t("files.title")}</h2>
|
||||||
|
<Button variant="default" onClick={onUpload}>
|
||||||
|
<IconCloudUpload className="mr-2 h-4 w-4" />
|
||||||
|
{t("files.uploadFile")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
27
apps/app/src/app/files/components/search-bar.tsx
Normal file
27
apps/app/src/app/files/components/search-bar.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { SearchBarProps } from "../types";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { IconSearch } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
export function SearchBar({ searchQuery, onSearch, totalFiles, filteredCount }: SearchBarProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="relative max-w-xs">
|
||||||
|
<IconSearch className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
className="pl-9"
|
||||||
|
placeholder={t("searchBar.placeholder")}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => onSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{searchQuery && (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{t("searchBar.results", { filtered: filteredCount, total: totalFiles })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
54
apps/app/src/app/files/hooks/use-files.ts
Normal file
54
apps/app/src/app/files/hooks/use-files.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useFileManager } from "@/hooks/use-file-manager";
|
||||||
|
import { listFiles } from "@/http/endpoints";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
export function useFiles() {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [files, setFiles] = useState<any[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const loadFiles = async () => {
|
||||||
|
try {
|
||||||
|
const response = await listFiles();
|
||||||
|
const allFiles = response.data.files || [];
|
||||||
|
const sortedFiles = [...allFiles].sort(
|
||||||
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||||
|
);
|
||||||
|
|
||||||
|
setFiles(sortedFiles);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(t("files.loadError"));
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileManager = useFileManager(loadFiles);
|
||||||
|
const filteredFiles = files.filter((file) => file.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadFiles();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
files,
|
||||||
|
searchQuery,
|
||||||
|
modals: {
|
||||||
|
isUploadModalOpen,
|
||||||
|
onOpenUploadModal: () => setIsUploadModalOpen(true),
|
||||||
|
onCloseUploadModal: () => setIsUploadModalOpen(false),
|
||||||
|
},
|
||||||
|
fileManager,
|
||||||
|
filteredFiles,
|
||||||
|
handleSearch: setSearchQuery,
|
||||||
|
loadFiles,
|
||||||
|
};
|
||||||
|
}
|
18
apps/app/src/app/files/layout.tsx
Normal file
18
apps/app/src/app/files/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Metadata } from "next";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
|
const translate = await getTranslations();
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: translate("files.pageTitle"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashboardLayout({ children }: LayoutProps) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
27
apps/app/src/app/files/modals/files-modals.tsx
Normal file
27
apps/app/src/app/files/modals/files-modals.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { FilesModalsProps } from "../types";
|
||||||
|
import { FileActionsModals } from "@/components/modals/file-actions-modals";
|
||||||
|
import { FilePreviewModal } from "@/components/modals/file-preview-modal";
|
||||||
|
import { UploadFileModal } from "@/components/modals/upload-file-modal";
|
||||||
|
|
||||||
|
export function FilesModals({ fileManager, modals, onSuccess }: FilesModalsProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UploadFileModal isOpen={modals.isUploadModalOpen} onClose={modals.onCloseUploadModal} onSuccess={onSuccess} />
|
||||||
|
|
||||||
|
<FilePreviewModal
|
||||||
|
file={fileManager.previewFile || { name: "", objectName: "" }}
|
||||||
|
isOpen={!!fileManager.previewFile}
|
||||||
|
onClose={() => fileManager.setPreviewFile(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FileActionsModals
|
||||||
|
fileToDelete={fileManager.fileToDelete}
|
||||||
|
fileToRename={fileManager.fileToRename}
|
||||||
|
onCloseDelete={() => fileManager.setFileToDelete(null)}
|
||||||
|
onCloseRename={() => fileManager.setFileToRename(null)}
|
||||||
|
onDelete={fileManager.handleDelete}
|
||||||
|
onRename={fileManager.handleRename}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
39
apps/app/src/app/files/page.tsx
Normal file
39
apps/app/src/app/files/page.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { FileList } from "./components/file-list";
|
||||||
|
import { useFiles } from "./hooks/use-files";
|
||||||
|
import { FilesModals } from "./modals/files-modals";
|
||||||
|
import { FileManagerLayout } from "@/components/layout/file-manager-layout";
|
||||||
|
import { LoadingScreen } from "@/components/layout/loading-screen";
|
||||||
|
import { IconFolderOpen } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
|
||||||
|
export default function FilesPage() {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const { isLoading, files, searchQuery, modals, fileManager, filteredFiles, handleSearch, loadFiles } = useFiles();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingScreen />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileManagerLayout
|
||||||
|
breadcrumbLabel={t("files.breadcrumb")}
|
||||||
|
icon={<IconFolderOpen size={20}/>}
|
||||||
|
title={t("files.pageTitle")}
|
||||||
|
>
|
||||||
|
<FileList
|
||||||
|
fileManager={fileManager}
|
||||||
|
files={files}
|
||||||
|
filteredFiles={filteredFiles}
|
||||||
|
searchQuery={searchQuery}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onUpload={modals.onOpenUploadModal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FilesModals fileManager={fileManager} modals={modals} onSuccess={loadFiles} />
|
||||||
|
</FileManagerLayout>
|
||||||
|
);
|
||||||
|
}
|
34
apps/app/src/app/files/types/index.ts
Normal file
34
apps/app/src/app/files/types/index.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { FileManagerHook } from "@/hooks/use-file-manager";
|
||||||
|
|
||||||
|
export interface EmptyStateProps {
|
||||||
|
onUpload: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HeaderProps {
|
||||||
|
onUpload: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileListProps {
|
||||||
|
files: any[];
|
||||||
|
filteredFiles: any[];
|
||||||
|
fileManager: FileManagerHook;
|
||||||
|
searchQuery: string;
|
||||||
|
onSearch: (query: string) => void;
|
||||||
|
onUpload: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchBarProps {
|
||||||
|
searchQuery: string;
|
||||||
|
onSearch: (query: string) => void;
|
||||||
|
totalFiles: number;
|
||||||
|
filteredCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilesModalsProps {
|
||||||
|
fileManager: FileManagerHook;
|
||||||
|
modals: {
|
||||||
|
isUploadModalOpen: boolean;
|
||||||
|
onCloseUploadModal: () => void;
|
||||||
|
};
|
||||||
|
onSuccess: () => Promise<void>;
|
||||||
|
}
|
@@ -6,6 +6,13 @@ import { useTranslations } from "next-intl";
|
|||||||
import { Navbar } from "@/components/layout/navbar";
|
import { Navbar } from "@/components/layout/navbar";
|
||||||
import { DefaultFooter } from "@/components/ui/default-footer";
|
import { DefaultFooter } from "@/components/ui/default-footer";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from "@/components/ui/breadcrumb";
|
||||||
|
|
||||||
interface FileManagerLayoutProps {
|
interface FileManagerLayoutProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -36,24 +43,24 @@ export function FileManagerLayout({
|
|||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
{showBreadcrumb && breadcrumbLabel && (
|
{showBreadcrumb && breadcrumbLabel && (
|
||||||
<nav className="flex" aria-label="Breadcrumb">
|
<Breadcrumb>
|
||||||
<ol className="flex items-center gap-2">
|
<BreadcrumbList>
|
||||||
<li className="flex items-center">
|
<BreadcrumbItem>
|
||||||
<Link
|
<BreadcrumbLink asChild>
|
||||||
href="/dashboard"
|
<Link href="/dashboard" className="flex items-center">
|
||||||
className="flex items-center text-muted-foreground hover:text-foreground transition-colors"
|
<IconLayoutDashboard size={20} className="mr-2" />
|
||||||
>
|
{t("navigation.dashboard")}
|
||||||
<IconLayoutDashboard className="h-4 w-4 mr-1" />
|
</Link>
|
||||||
{t("navigation.dashboard")}
|
</BreadcrumbLink>
|
||||||
</Link>
|
</BreadcrumbItem>
|
||||||
</li>
|
<BreadcrumbSeparator />
|
||||||
<li className="flex items-center before:content-['/'] before:mx-2 before:text-muted-foreground">
|
<BreadcrumbItem>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center gap-2">
|
||||||
{icon} {breadcrumbLabel}
|
{icon} {breadcrumbLabel}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</BreadcrumbItem>
|
||||||
</ol>
|
</BreadcrumbList>
|
||||||
</nav>
|
</Breadcrumb>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
"use client";
|
||||||
/* eslint-disable jsx-a11y/media-has-caption */
|
/* eslint-disable jsx-a11y/media-has-caption */
|
||||||
import { getDownloadUrl } from "@/http/endpoints";
|
import { getDownloadUrl } from "@/http/endpoints";
|
||||||
import { getFileIcon } from "@/utils/file-icons";
|
import { getFileIcon } from "@/utils/file-icons";
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { getPresignedUrl, registerFile } from "@/http/endpoints";
|
import { getPresignedUrl, registerFile } from "@/http/endpoints";
|
||||||
import { generateSafeFileName } from "@/utils/file-utils";
|
import { generateSafeFileName } from "@/utils/file-utils";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -12,7 +14,7 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import { IconCloudUpload, IconFile, IconFileText, IconPhoto, IconFileTypography, IconFileTypePdf } from "@tabler/icons-react";
|
import { IconCloudUpload, IconFileText, IconPhoto, IconFileTypography, IconFileTypePdf } from "@tabler/icons-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface UploadFileModalProps {
|
interface UploadFileModalProps {
|
||||||
|
109
apps/app/src/components/ui/breadcrumb.tsx
Normal file
109
apps/app/src/components/ui/breadcrumb.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { ChevronRight, MoreHorizontal } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||||
|
return (
|
||||||
|
<ol
|
||||||
|
data-slot="breadcrumb-list"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-item"
|
||||||
|
className={cn("inline-flex items-center gap-1.5", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbLink({
|
||||||
|
asChild,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"a"> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "a"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="breadcrumb-link"
|
||||||
|
className={cn("hover:text-foreground transition-colors", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-page"
|
||||||
|
role="link"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-current="page"
|
||||||
|
className={cn("text-foreground font-normal", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbSeparator({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"li">) {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-slot="breadcrumb-separator"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("[&>svg]:size-3.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children ?? <ChevronRight />}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BreadcrumbEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="breadcrumb-ellipsis"
|
||||||
|
role="presentation"
|
||||||
|
aria-hidden="true"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="size-4" />
|
||||||
|
<span className="sr-only">More</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbPage,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
BreadcrumbEllipsis,
|
||||||
|
}
|
Reference in New Issue
Block a user