diff --git a/src/app/dashboard/files/[...path]/page.tsx b/src/app/dashboard/files/[...path]/page.tsx new file mode 100755 index 0000000..dff4c12 --- /dev/null +++ b/src/app/dashboard/files/[...path]/page.tsx @@ -0,0 +1,17 @@ +"use client" + +import { Suspense } from "react" +import { useParams } from "next/navigation" +import { FileBrowser } from "@/components/files/file-browser" + +export default function FilesPathPage() { + const params = useParams() + const path = (params.path as string[]) ?? [] + const decodedPath = path.map(decodeURIComponent) + + return ( + + + + ) +} diff --git a/src/app/dashboard/files/layout.tsx b/src/app/dashboard/files/layout.tsx new file mode 100755 index 0000000..b618961 --- /dev/null +++ b/src/app/dashboard/files/layout.tsx @@ -0,0 +1,17 @@ +"use client" + +import { Toaster } from "sonner" +import { FilesProvider } from "@/hooks/use-files" + +export default function FilesLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + + ) +} diff --git a/src/app/dashboard/files/page.tsx b/src/app/dashboard/files/page.tsx new file mode 100755 index 0000000..af61c02 --- /dev/null +++ b/src/app/dashboard/files/page.tsx @@ -0,0 +1,12 @@ +"use client" + +import { Suspense } from "react" +import { FileBrowser } from "@/components/files/file-browser" + +export default function FilesPage() { + return ( + + + + ) +} diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 0c33698..b03fcc9 100755 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -4,15 +4,18 @@ import * as React from "react" import { IconCalendarStats, IconDashboard, + IconFiles, IconFolder, IconHelp, IconInnerShadowTop, IconSearch, IconSettings, } from "@tabler/icons-react" +import { usePathname } from "next/navigation" import { NavMain } from "@/components/nav-main" import { NavSecondary } from "@/components/nav-secondary" +import { NavFiles } from "@/components/nav-files" import { NavUser } from "@/components/nav-user" import { Sidebar, @@ -46,6 +49,11 @@ const data = { url: "/dashboard/projects/demo-project-1/schedule", icon: IconCalendarStats, }, + { + title: "Files", + url: "/dashboard/files", + icon: IconFiles, + }, ], navSecondary: [ { @@ -67,6 +75,9 @@ const data = { } export function AppSidebar({ ...props }: React.ComponentProps) { + const pathname = usePathname() + const isFilesMode = pathname?.startsWith("/dashboard/files") + return ( @@ -85,8 +96,29 @@ export function AppSidebar({ ...props }: React.ComponentProps) { - - +
+
+ + +
+
+ + + +
+
diff --git a/src/components/files/file-breadcrumb.tsx b/src/components/files/file-breadcrumb.tsx new file mode 100755 index 0000000..015c42c --- /dev/null +++ b/src/components/files/file-breadcrumb.tsx @@ -0,0 +1,51 @@ +"use client" + +import Link from "next/link" +import { IconChevronRight } from "@tabler/icons-react" + +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb" + +export function FileBreadcrumb({ path }: { path: string[] }) { + return ( + + + + {path.length === 0 ? ( + My Files + ) : ( + + My Files + + )} + + {path.map((segment, i) => { + const isLast = i === path.length - 1 + const href = `/dashboard/files/${path.slice(0, i + 1).join("/")}` + return ( + + + + + + {isLast ? ( + {segment} + ) : ( + + {segment} + + )} + + + ) + })} + + + ) +} diff --git a/src/components/files/file-browser.tsx b/src/components/files/file-browser.tsx new file mode 100755 index 0000000..919b155 --- /dev/null +++ b/src/components/files/file-browser.tsx @@ -0,0 +1,145 @@ +"use client" + +import { useState, useCallback } from "react" +import { useSearchParams } from "next/navigation" +import { toast } from "sonner" + +import type { FileItem } from "@/lib/files-data" +import { useFiles, type FileView } from "@/hooks/use-files" +import { useFileSelection } from "@/hooks/use-file-selection" +import { FileBreadcrumb } from "./file-breadcrumb" +import { FileToolbar, type NewFileType } from "./file-toolbar" +import { FileGrid } from "./file-grid" +import { FileList } from "./file-list" +import { FileUploadDialog } from "./file-upload-dialog" +import { FileNewFolderDialog } from "./file-new-folder-dialog" +import { FileRenameDialog } from "./file-rename-dialog" +import { FileMoveDialog } from "./file-move-dialog" +import { FileDropZone } from "./file-drop-zone" +import { ScrollArea } from "@/components/ui/scroll-area" + +export function FileBrowser({ path }: { path: string[] }) { + const searchParams = useSearchParams() + const viewParam = searchParams.get("view") as FileView | null + const currentView = viewParam ?? "my-files" + + const { state, dispatch, getFilesForView } = useFiles() + const files = getFilesForView(currentView, path) + + const [uploadOpen, setUploadOpen] = useState(false) + const [newFolderOpen, setNewFolderOpen] = useState(false) + const [renameFile, setRenameFile] = useState(null) + const [moveFile, setMoveFile] = useState(null) + + const { handleClick } = useFileSelection(files, state.selectedIds, { + select: (ids) => dispatch({ type: "SET_SELECTED", payload: ids }), + }) + + const handleBackgroundClick = useCallback( + (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + dispatch({ type: "CLEAR_SELECTION" }) + } + }, + [dispatch] + ) + + const parentId = (() => { + if (path.length === 0) return null + const folder = state.files.find( + (f) => + f.type === "folder" && + f.name === path[path.length - 1] && + JSON.stringify(f.path) === JSON.stringify(path.slice(0, -1)) + ) + return folder?.id ?? null + })() + + const handleNew = useCallback( + (type: NewFileType) => { + if (type === "folder") { + setNewFolderOpen(true) + return + } + + const names: Record = { + document: "Untitled Document", + spreadsheet: "Untitled Spreadsheet", + presentation: "Untitled Presentation", + } + const fileType = type === "presentation" ? "document" : type + + dispatch({ + type: "CREATE_FILE", + payload: { + name: names[type], + fileType: fileType as FileItem["type"], + parentId, + path, + }, + }) + toast.success(`${names[type]} created`) + }, + [dispatch, parentId, path] + ) + + const viewTitle: Record = { + "my-files": "", + shared: "Shared with me", + recent: "Recent", + starred: "Starred", + trash: "Trash", + } + + return ( +
+ {currentView !== "my-files" && ( +

{viewTitle[currentView]}

+ )} + {currentView === "my-files" && } + setUploadOpen(true)} + /> + setUploadOpen(true)}> + + {state.viewMode === "grid" ? ( + + ) : ( + + )} + + + + + + !open && setRenameFile(null)} + file={renameFile} + /> + !open && setMoveFile(null)} + file={moveFile} + /> +
+ ) +} diff --git a/src/components/files/file-context-menu.tsx b/src/components/files/file-context-menu.tsx new file mode 100755 index 0000000..097b746 --- /dev/null +++ b/src/components/files/file-context-menu.tsx @@ -0,0 +1,109 @@ +"use client" + +import { + IconDownload, + IconEdit, + IconFolderSymlink, + IconShare, + IconStar, + IconStarFilled, + IconTrash, + IconTrashOff, +} from "@tabler/icons-react" + +import type { FileItem } from "@/lib/files-data" +import { useFiles } from "@/hooks/use-files" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu" +import { toast } from "sonner" + +export function FileContextMenu({ + file, + children, + onRename, + onMove, +}: { + file: FileItem + children: React.ReactNode + onRename: (file: FileItem) => void + onMove: (file: FileItem) => void +}) { + const { dispatch } = useFiles() + + return ( + + {children} + + {file.type === "folder" && ( + toast.info("Opening folder...")}> + + Open + + )} + toast.info("Share dialog coming soon")}> + + Share + + {!file.trashed && ( + <> + toast.success("Download started")}> + + Download + + + onRename(file)}> + + Rename + + onMove(file)}> + + Move to + + dispatch({ type: "STAR_FILE", payload: file.id })} + > + {file.starred ? ( + <> + + Unstar + + ) : ( + <> + + Star + + )} + + + { + dispatch({ type: "TRASH_FILE", payload: file.id }) + toast.success(`"${file.name}" moved to trash`) + }} + > + + Delete + + + )} + {file.trashed && ( + { + dispatch({ type: "RESTORE_FILE", payload: file.id }) + toast.success(`"${file.name}" restored`) + }} + > + + Restore + + )} + + + ) +} diff --git a/src/components/files/file-drop-zone.tsx b/src/components/files/file-drop-zone.tsx new file mode 100755 index 0000000..96f9f6c --- /dev/null +++ b/src/components/files/file-drop-zone.tsx @@ -0,0 +1,81 @@ +"use client" + +import { useState, useCallback } from "react" +import { IconCloudUpload } from "@tabler/icons-react" +import { cn } from "@/lib/utils" + +export function FileDropZone({ + children, + onDrop, +}: { + children: React.ReactNode + onDrop: () => void +}) { + const [dragging, setDragging] = useState(false) + const [dragCounter, setDragCounter] = useState(0) + + const handleDragEnter = useCallback( + (e: React.DragEvent) => { + e.preventDefault() + setDragCounter((c) => c + 1) + if (e.dataTransfer.types.includes("Files")) { + setDragging(true) + } + }, + [] + ) + + const handleDragLeave = useCallback( + (e: React.DragEvent) => { + e.preventDefault() + setDragCounter((c) => { + const next = c - 1 + if (next <= 0) setDragging(false) + return Math.max(0, next) + }) + }, + [] + ) + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault() + }, []) + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault() + setDragging(false) + setDragCounter(0) + if (e.dataTransfer.files.length > 0) { + onDrop() + } + }, + [onDrop] + ) + + return ( +
+ {children} +
+
+ +

Drop files to upload

+
+
+
+ ) +} diff --git a/src/components/files/file-grid.tsx b/src/components/files/file-grid.tsx new file mode 100755 index 0000000..fa5fa24 --- /dev/null +++ b/src/components/files/file-grid.tsx @@ -0,0 +1,74 @@ +"use client" + +import type { FileItem as FileItemType } from "@/lib/files-data" +import { FolderCard, FileCard } from "./file-item" +import { FileContextMenu } from "./file-context-menu" +import { IconFile } from "@tabler/icons-react" + +export function FileGrid({ + files, + selectedIds, + onItemClick, + onRename, + onMove, +}: { + files: FileItemType[] + selectedIds: Set + onItemClick: (id: string, e: React.MouseEvent) => void + onRename: (file: FileItemType) => void + onMove: (file: FileItemType) => void +}) { + if (files.length === 0) { + return ( +
+ +

No files here

+

Upload files or create a folder to get started

+
+ ) + } + + const folders = files.filter((f) => f.type === "folder") + const regularFiles = files.filter((f) => f.type !== "folder") + + return ( +
+ {folders.length > 0 && ( +
+

+ Folders +

+
+ {folders.map((file) => ( + + onItemClick(file.id, e)} + /> + + ))} +
+
+ )} + {regularFiles.length > 0 && ( +
+

+ Files +

+
+ {regularFiles.map((file) => ( + + onItemClick(file.id, e)} + /> + + ))} +
+
+ )} +
+ ) +} diff --git a/src/components/files/file-icon.tsx b/src/components/files/file-icon.tsx new file mode 100755 index 0000000..6207bb5 --- /dev/null +++ b/src/components/files/file-icon.tsx @@ -0,0 +1,40 @@ +import { + IconFolder, + IconFileText, + IconTable, + IconPhoto, + IconVideo, + IconFileTypePdf, + IconCode, + IconFileZip, + IconMusic, + IconFile, +} from "@tabler/icons-react" +import type { FileType } from "@/lib/files-data" +import { cn } from "@/lib/utils" + +const iconMap: Record = { + folder: { icon: IconFolder, color: "text-amber-500" }, + document: { icon: IconFileText, color: "text-blue-500" }, + spreadsheet: { icon: IconTable, color: "text-green-600" }, + image: { icon: IconPhoto, color: "text-purple-500" }, + video: { icon: IconVideo, color: "text-red-500" }, + pdf: { icon: IconFileTypePdf, color: "text-red-600" }, + code: { icon: IconCode, color: "text-emerald-500" }, + archive: { icon: IconFileZip, color: "text-orange-500" }, + audio: { icon: IconMusic, color: "text-pink-500" }, + unknown: { icon: IconFile, color: "text-muted-foreground" }, +} + +export function FileIcon({ + type, + className, + size = 20, +}: { + type: FileType + className?: string + size?: number +}) { + const { icon: Icon, color } = iconMap[type] + return +} diff --git a/src/components/files/file-item.tsx b/src/components/files/file-item.tsx new file mode 100755 index 0000000..c7dcf62 --- /dev/null +++ b/src/components/files/file-item.tsx @@ -0,0 +1,143 @@ +"use client" + +import { forwardRef } from "react" +import { IconStar, IconStarFilled, IconUsers, IconDots } from "@tabler/icons-react" +import { useRouter } from "next/navigation" + +import type { FileItem as FileItemType } from "@/lib/files-data" +import { formatRelativeDate } from "@/lib/file-utils" +import { FileIcon } from "./file-icon" +import { useFiles } from "@/hooks/use-files" +import { cn } from "@/lib/utils" + +const fileTypeColors: Record = { + document: "bg-blue-50 dark:bg-blue-950/30", + spreadsheet: "bg-green-50 dark:bg-green-950/30", + image: "bg-purple-50 dark:bg-purple-950/30", + video: "bg-red-50 dark:bg-red-950/30", + pdf: "bg-red-50 dark:bg-red-950/30", + code: "bg-emerald-50 dark:bg-emerald-950/30", + archive: "bg-orange-50 dark:bg-orange-950/30", + audio: "bg-pink-50 dark:bg-pink-950/30", + unknown: "bg-muted", +} + +export const FolderCard = forwardRef< + HTMLDivElement, + { + file: FileItemType + selected: boolean + onClick: (e: React.MouseEvent) => void + } +>(function FolderCard({ file, selected, onClick, ...props }, ref) { + const router = useRouter() + const { dispatch } = useFiles() + + const handleDoubleClick = () => { + const folderPath = [...file.path, file.name].join("/") + router.push(`/dashboard/files/${folderPath}`) + } + + return ( +
+ + {file.name} + {file.shared && ( + + )} + +
+ ) +}) + +export const FileCard = forwardRef< + HTMLDivElement, + { + file: FileItemType + selected: boolean + onClick: (e: React.MouseEvent) => void + } +>(function FileCard({ file, selected, onClick, ...props }, ref) { + const { dispatch } = useFiles() + + return ( +
+
+ +
+
+ +
+

{file.name}

+

+ {formatRelativeDate(file.modifiedAt)} + {file.shared && " · Shared"} +

+
+
+ + +
+
+
+ ) +}) diff --git a/src/components/files/file-list.tsx b/src/components/files/file-list.tsx new file mode 100755 index 0000000..064cc27 --- /dev/null +++ b/src/components/files/file-list.tsx @@ -0,0 +1,62 @@ +"use client" + +import type { FileItem as FileItemType } from "@/lib/files-data" +import { FileRow } from "./file-row" +import { FileContextMenu } from "./file-context-menu" +import { + Table, + TableBody, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { IconFile } from "@tabler/icons-react" + +export function FileList({ + files, + selectedIds, + onItemClick, + onRename, + onMove, +}: { + files: FileItemType[] + selectedIds: Set + onItemClick: (id: string, e: React.MouseEvent) => void + onRename: (file: FileItemType) => void + onMove: (file: FileItemType) => void +}) { + if (files.length === 0) { + return ( +
+ +

No files here

+

Upload files or create a folder to get started

+
+ ) + } + + return ( + + + + Name + Modified + Owner + Size + + + + + {files.map((file) => ( + + onItemClick(file.id, e)} + /> + + ))} + +
+ ) +} diff --git a/src/components/files/file-move-dialog.tsx b/src/components/files/file-move-dialog.tsx new file mode 100755 index 0000000..3a0a54d --- /dev/null +++ b/src/components/files/file-move-dialog.tsx @@ -0,0 +1,100 @@ +"use client" + +import { useState } from "react" +import { IconFolder, IconFolderSymlink } from "@tabler/icons-react" + +import type { FileItem } from "@/lib/files-data" +import { useFiles } from "@/hooks/use-files" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { ScrollArea } from "@/components/ui/scroll-area" +import { cn } from "@/lib/utils" +import { toast } from "sonner" + +export function FileMoveDialog({ + open, + onOpenChange, + file, +}: { + open: boolean + onOpenChange: (open: boolean) => void + file: FileItem | null +}) { + const { dispatch, getFolders } = useFiles() + const [selectedFolderId, setSelectedFolderId] = useState(null) + + const folders = getFolders().filter((f) => f.id !== file?.id) + + const handleMove = () => { + if (!file) return + + const targetFolder = folders.find((f) => f.id === selectedFolderId) + const targetPath = targetFolder + ? [...targetFolder.path, targetFolder.name] + : [] + + dispatch({ + type: "MOVE_FILE", + payload: { + id: file.id, + targetFolderId: selectedFolderId, + targetPath, + }, + }) + toast.success( + `Moved "${file.name}" to ${targetFolder?.name ?? "My Files"}` + ) + onOpenChange(false) + } + + return ( + + + + + + Move to + + + + + {folders.map((folder) => ( + + ))} + + + + + + + + ) +} diff --git a/src/components/files/file-new-folder-dialog.tsx b/src/components/files/file-new-folder-dialog.tsx new file mode 100755 index 0000000..eacc71b --- /dev/null +++ b/src/components/files/file-new-folder-dialog.tsx @@ -0,0 +1,74 @@ +"use client" + +import { useState } from "react" +import { IconFolderPlus } from "@tabler/icons-react" + +import { useFiles } from "@/hooks/use-files" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { toast } from "sonner" + +export function FileNewFolderDialog({ + open, + onOpenChange, + currentPath, + parentId, +}: { + open: boolean + onOpenChange: (open: boolean) => void + currentPath: string[] + parentId: string | null +}) { + const [name, setName] = useState("") + const { dispatch } = useFiles() + + const handleCreate = () => { + const trimmed = name.trim() + if (!trimmed) return + + dispatch({ + type: "CREATE_FOLDER", + payload: { name: trimmed, parentId, path: currentPath }, + }) + toast.success(`Folder "${trimmed}" created`) + setName("") + onOpenChange(false) + } + + return ( + + + + + + New folder + + +
+ setName(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleCreate()} + autoFocus + /> +
+ + + + +
+
+ ) +} diff --git a/src/components/files/file-rename-dialog.tsx b/src/components/files/file-rename-dialog.tsx new file mode 100755 index 0000000..6fc7f0e --- /dev/null +++ b/src/components/files/file-rename-dialog.tsx @@ -0,0 +1,76 @@ +"use client" + +import { useState, useEffect } from "react" +import { IconEdit } from "@tabler/icons-react" + +import type { FileItem } from "@/lib/files-data" +import { useFiles } from "@/hooks/use-files" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { toast } from "sonner" + +export function FileRenameDialog({ + open, + onOpenChange, + file, +}: { + open: boolean + onOpenChange: (open: boolean) => void + file: FileItem | null +}) { + const [name, setName] = useState("") + const { dispatch } = useFiles() + + useEffect(() => { + if (file) setName(file.name) + }, [file]) + + const handleRename = () => { + if (!file) return + const trimmed = name.trim() + if (!trimmed || trimmed === file.name) return + + dispatch({ type: "RENAME_FILE", payload: { id: file.id, name: trimmed } }) + toast.success(`Renamed to "${trimmed}"`) + onOpenChange(false) + } + + return ( + + + + + + Rename + + +
+ setName(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleRename()} + autoFocus + /> +
+ + + + +
+
+ ) +} diff --git a/src/components/files/file-row.tsx b/src/components/files/file-row.tsx new file mode 100755 index 0000000..25f09fb --- /dev/null +++ b/src/components/files/file-row.tsx @@ -0,0 +1,79 @@ +"use client" + +import { forwardRef } from "react" +import { useRouter } from "next/navigation" +import { IconStar, IconStarFilled, IconUsers } from "@tabler/icons-react" + +import type { FileItem } from "@/lib/files-data" +import { formatFileSize, formatRelativeDate } from "@/lib/file-utils" +import { FileIcon } from "./file-icon" +import { useFiles } from "@/hooks/use-files" +import { TableCell, TableRow } from "@/components/ui/table" +import { cn } from "@/lib/utils" + +export const FileRow = forwardRef< + HTMLTableRowElement, + { + file: FileItem + selected: boolean + onClick: (e: React.MouseEvent) => void + } +>(function FileRow({ file, selected, onClick, ...props }, ref) { + const router = useRouter() + const { dispatch } = useFiles() + + const handleDoubleClick = () => { + if (file.type === "folder") { + const folderPath = [...file.path, file.name].join("/") + router.push(`/dashboard/files/${folderPath}`) + } + } + + return ( + + +
+ + {file.name} + {file.shared && } +
+
+ + {formatRelativeDate(file.modifiedAt)} + + + {file.owner.name} + + + {file.type === "folder" ? "—" : formatFileSize(file.size)} + + + + +
+ ) +}) diff --git a/src/components/files/file-toolbar.tsx b/src/components/files/file-toolbar.tsx new file mode 100755 index 0000000..df5db8d --- /dev/null +++ b/src/components/files/file-toolbar.tsx @@ -0,0 +1,157 @@ +"use client" + +import { useState } from "react" +import { + IconLayoutGrid, + IconList, + IconPlus, + IconUpload, + IconSearch, + IconSortAscending, + IconSortDescending, + IconFolder, + IconFileText, + IconTable, + IconPresentation, +} from "@tabler/icons-react" + +import { useFiles, type SortField, type ViewMode } from "@/hooks/use-files" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export type NewFileType = "folder" | "document" | "spreadsheet" | "presentation" + +export function FileToolbar({ + onNew, + onUpload, +}: { + onNew: (type: NewFileType) => void + onUpload: () => void +}) { + const { state, dispatch } = useFiles() + const [searchFocused, setSearchFocused] = useState(false) + + const sortLabels: Record = { + name: "Name", + modified: "Modified", + size: "Size", + type: "Type", + } + + const handleSort = (field: SortField) => { + const direction = + state.sortBy === field && state.sortDirection === "asc" ? "desc" : "asc" + dispatch({ type: "SET_SORT", payload: { field, direction } }) + } + + return ( +
+
+ + + + + + onNew("folder")}> + + Folder + + + onNew("document")}> + + Document + + onNew("spreadsheet")}> + + Spreadsheet + + onNew("presentation")}> + + Presentation + + + + + File upload + + + +
+ +
+ +
+ + + dispatch({ type: "SET_SEARCH", payload: e.target.value }) + } + onFocus={() => setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + className="h-8 pl-8 text-sm" + /> +
+ + + + + + + {(Object.keys(sortLabels) as SortField[]).map((field) => ( + handleSort(field)}> + {sortLabels[field]} + {state.sortBy === field && ( + + {state.sortDirection === "asc" ? "↑" : "↓"} + + )} + + ))} + + + + { + if (v) dispatch({ type: "SET_VIEW_MODE", payload: v as ViewMode }) + }} + size="sm" + > + + + + + + + +
+ ) +} diff --git a/src/components/files/file-upload-dialog.tsx b/src/components/files/file-upload-dialog.tsx new file mode 100755 index 0000000..313802f --- /dev/null +++ b/src/components/files/file-upload-dialog.tsx @@ -0,0 +1,76 @@ +"use client" + +import { useState, useEffect } from "react" +import { IconUpload } from "@tabler/icons-react" + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Progress } from "@/components/ui/progress" +import { toast } from "sonner" + +export function FileUploadDialog({ + open, + onOpenChange, +}: { + open: boolean + onOpenChange: (open: boolean) => void +}) { + const [progress, setProgress] = useState(0) + const [uploading, setUploading] = useState(false) + + useEffect(() => { + if (!open) { + setProgress(0) + setUploading(false) + return + } + + setUploading(true) + const interval = setInterval(() => { + setProgress((prev) => { + if (prev >= 100) { + clearInterval(interval) + setTimeout(() => { + onOpenChange(false) + toast.success("File uploaded successfully") + }, 300) + return 100 + } + return prev + Math.random() * 15 + }) + }, 200) + + return () => clearInterval(interval) + }, [open, onOpenChange]) + + return ( + + + + + + Uploading file + + +
+
+ example-file.pdf + + {Math.min(100, Math.round(progress))}% + +
+ + {uploading && progress < 100 && ( +

+ Uploading to cloud storage... +

+ )} +
+
+
+ ) +} diff --git a/src/components/files/storage-indicator.tsx b/src/components/files/storage-indicator.tsx new file mode 100755 index 0000000..6ba9f7d --- /dev/null +++ b/src/components/files/storage-indicator.tsx @@ -0,0 +1,22 @@ +"use client" + +import { Progress } from "@/components/ui/progress" +import { formatFileSize } from "@/lib/file-utils" +import type { StorageUsage } from "@/lib/files-data" + +export function StorageIndicator({ usage }: { usage: StorageUsage }) { + const percent = Math.round((usage.used / usage.total) * 100) + + return ( +
+
+ Storage + {percent}% used +
+ +

+ {formatFileSize(usage.used)} of {formatFileSize(usage.total)} +

+
+ ) +} diff --git a/src/components/nav-files.tsx b/src/components/nav-files.tsx new file mode 100755 index 0000000..a0300cf --- /dev/null +++ b/src/components/nav-files.tsx @@ -0,0 +1,94 @@ +"use client" + +import { + IconArrowLeft, + IconFiles, + IconUsers, + IconClock, + IconStar, + IconTrash, +} from "@tabler/icons-react" +import Link from "next/link" +import { usePathname, useSearchParams } from "next/navigation" + +import { mockStorageUsage } from "@/lib/files-data" +import { StorageIndicator } from "@/components/files/storage-indicator" +import { + SidebarGroup, + SidebarGroupContent, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarSeparator, +} from "@/components/ui/sidebar" +import { cn } from "@/lib/utils" + +type FileView = "my-files" | "shared" | "recent" | "starred" | "trash" + +const fileNavItems: { title: string; view: FileView; icon: typeof IconFiles }[] = [ + { title: "My Files", view: "my-files", icon: IconFiles }, + { title: "Shared with me", view: "shared", icon: IconUsers }, + { title: "Recent", view: "recent", icon: IconClock }, + { title: "Starred", view: "starred", icon: IconStar }, + { title: "Trash", view: "trash", icon: IconTrash }, +] + +export function NavFiles() { + const pathname = usePathname() + const searchParams = useSearchParams() + const activeView = searchParams.get("view") ?? "my-files" + + return ( + <> + + + + + + + + Back + + + + + + + + + + + {fileNavItems.map((item) => ( + + + + + {item.title} + + + + ))} + + + +
+ + +
+ + ) +} diff --git a/src/hooks/use-file-selection.ts b/src/hooks/use-file-selection.ts new file mode 100755 index 0000000..4253846 --- /dev/null +++ b/src/hooks/use-file-selection.ts @@ -0,0 +1,56 @@ +"use client" + +import { useCallback, useRef } from "react" +import type { FileItem } from "@/lib/files-data" + +type SelectionAction = { + select: (ids: Set) => void +} + +export function useFileSelection( + files: FileItem[], + selectedIds: Set, + actions: SelectionAction +) { + const lastClickedRef = useRef(null) + + const handleClick = useCallback( + (id: string, event: React.MouseEvent) => { + if (event.metaKey || event.ctrlKey) { + const next = new Set(selectedIds) + if (next.has(id)) next.delete(id) + else next.add(id) + actions.select(next) + lastClickedRef.current = id + return + } + + if (event.shiftKey && lastClickedRef.current) { + const lastIdx = files.findIndex((f) => f.id === lastClickedRef.current) + const currIdx = files.findIndex((f) => f.id === id) + if (lastIdx !== -1 && currIdx !== -1) { + const start = Math.min(lastIdx, currIdx) + const end = Math.max(lastIdx, currIdx) + const range = files.slice(start, end + 1).map((f) => f.id) + actions.select(new Set([...selectedIds, ...range])) + return + } + } + + actions.select(new Set([id])) + lastClickedRef.current = id + }, + [files, selectedIds, actions] + ) + + const selectAll = useCallback(() => { + actions.select(new Set(files.map((f) => f.id))) + }, [files, actions]) + + const clearSelection = useCallback(() => { + actions.select(new Set()) + lastClickedRef.current = null + }, [actions]) + + return { handleClick, selectAll, clearSelection } +} diff --git a/src/hooks/use-files.tsx b/src/hooks/use-files.tsx new file mode 100755 index 0000000..01ef1d6 --- /dev/null +++ b/src/hooks/use-files.tsx @@ -0,0 +1,291 @@ +"use client" + +import { + createContext, + useContext, + useReducer, + useCallback, + type ReactNode, +} from "react" +import { mockFiles, mockStorageUsage, type FileItem } from "@/lib/files-data" + +export type FileView = + | "my-files" + | "shared" + | "recent" + | "starred" + | "trash" + +export type SortField = "name" | "modified" | "size" | "type" +export type SortDirection = "asc" | "desc" +export type ViewMode = "grid" | "list" + +type FilesState = { + viewMode: ViewMode + currentView: FileView + selectedIds: Set + sortBy: SortField + sortDirection: SortDirection + searchQuery: string + files: FileItem[] +} + +type FilesAction = + | { type: "SET_VIEW_MODE"; payload: ViewMode } + | { type: "SET_CURRENT_VIEW"; payload: FileView } + | { type: "SET_SELECTED"; payload: Set } + | { type: "TOGGLE_SELECTED"; payload: string } + | { type: "CLEAR_SELECTION" } + | { type: "SET_SORT"; payload: { field: SortField; direction: SortDirection } } + | { type: "SET_SEARCH"; payload: string } + | { type: "STAR_FILE"; payload: string } + | { type: "TRASH_FILE"; payload: string } + | { type: "RESTORE_FILE"; payload: string } + | { type: "RENAME_FILE"; payload: { id: string; name: string } } + | { type: "CREATE_FOLDER"; payload: { name: string; parentId: string | null; path: string[] } } + | { type: "CREATE_FILE"; payload: { name: string; fileType: FileItem["type"]; parentId: string | null; path: string[] } } + | { type: "MOVE_FILE"; payload: { id: string; targetFolderId: string | null; targetPath: string[] } } + +function filesReducer(state: FilesState, action: FilesAction): FilesState { + switch (action.type) { + case "SET_VIEW_MODE": + return { ...state, viewMode: action.payload } + case "SET_CURRENT_VIEW": + return { ...state, currentView: action.payload, selectedIds: new Set() } + case "SET_SELECTED": + return { ...state, selectedIds: action.payload } + case "TOGGLE_SELECTED": { + const next = new Set(state.selectedIds) + if (next.has(action.payload)) next.delete(action.payload) + else next.add(action.payload) + return { ...state, selectedIds: next } + } + case "CLEAR_SELECTION": + return { ...state, selectedIds: new Set() } + case "SET_SORT": + return { + ...state, + sortBy: action.payload.field, + sortDirection: action.payload.direction, + } + case "SET_SEARCH": + return { ...state, searchQuery: action.payload, selectedIds: new Set() } + case "STAR_FILE": + return { + ...state, + files: state.files.map((f) => + f.id === action.payload ? { ...f, starred: !f.starred } : f + ), + } + case "TRASH_FILE": + return { + ...state, + files: state.files.map((f) => + f.id === action.payload ? { ...f, trashed: true } : f + ), + selectedIds: new Set(), + } + case "RESTORE_FILE": + return { + ...state, + files: state.files.map((f) => + f.id === action.payload ? { ...f, trashed: false } : f + ), + } + case "RENAME_FILE": + return { + ...state, + files: state.files.map((f) => + f.id === action.payload.id + ? { ...f, name: action.payload.name, modifiedAt: new Date().toISOString() } + : f + ), + } + case "CREATE_FOLDER": { + const newFolder: FileItem = { + id: `folder-${Date.now()}`, + name: action.payload.name, + type: "folder", + size: 0, + path: action.payload.path, + createdAt: new Date().toISOString(), + modifiedAt: new Date().toISOString(), + owner: { name: "Martine Vogel" }, + starred: false, + shared: false, + trashed: false, + parentId: action.payload.parentId, + } + return { ...state, files: [...state.files, newFolder] } + } + case "CREATE_FILE": { + const newFile: FileItem = { + id: `file-${Date.now()}`, + name: action.payload.name, + type: action.payload.fileType, + size: 0, + path: action.payload.path, + createdAt: new Date().toISOString(), + modifiedAt: new Date().toISOString(), + owner: { name: "Martine Vogel" }, + starred: false, + shared: false, + trashed: false, + parentId: action.payload.parentId, + } + return { ...state, files: [...state.files, newFile] } + } + case "MOVE_FILE": + return { + ...state, + files: state.files.map((f) => + f.id === action.payload.id + ? { + ...f, + parentId: action.payload.targetFolderId, + path: action.payload.targetPath, + modifiedAt: new Date().toISOString(), + } + : f + ), + } + default: + return state + } +} + +const initialState: FilesState = { + viewMode: "grid", + currentView: "my-files", + selectedIds: new Set(), + sortBy: "name", + sortDirection: "asc", + searchQuery: "", + files: mockFiles, +} + +type FilesContextValue = { + state: FilesState + dispatch: React.Dispatch + getFilesForPath: (path: string[]) => FileItem[] + getFilesForView: (view: FileView, path: string[]) => FileItem[] + storageUsage: typeof mockStorageUsage + getFolders: () => FileItem[] +} + +const FilesContext = createContext(null) + +export function FilesProvider({ children }: { children: ReactNode }) { + const [state, dispatch] = useReducer(filesReducer, initialState) + + const getFilesForPath = useCallback( + (path: string[]) => { + return state.files.filter((f) => { + if (f.trashed) return false + if (path.length === 0) return f.parentId === null + const parentFolder = state.files.find( + (folder) => + folder.type === "folder" && + folder.name === path[path.length - 1] && + JSON.stringify(folder.path) === JSON.stringify(path.slice(0, -1)) + ) + return parentFolder && f.parentId === parentFolder.id + }) + }, + [state.files] + ) + + const getFilesForView = useCallback( + (view: FileView, path: string[]) => { + let files: FileItem[] + + switch (view) { + case "my-files": + files = getFilesForPath(path) + break + case "shared": + files = state.files.filter((f) => !f.trashed && f.shared) + break + case "recent": { + const cutoff = new Date() + cutoff.setDate(cutoff.getDate() - 30) + files = state.files + .filter((f) => !f.trashed && new Date(f.modifiedAt) > cutoff) + .sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime()) + break + } + case "starred": + files = state.files.filter((f) => !f.trashed && f.starred) + break + case "trash": + files = state.files.filter((f) => f.trashed) + break + default: + files = [] + } + + if (state.searchQuery) { + const q = state.searchQuery.toLowerCase() + files = files.filter((f) => f.name.toLowerCase().includes(q)) + } + + return sortFiles(files, state.sortBy, state.sortDirection) + }, + [state.files, state.searchQuery, state.sortBy, state.sortDirection, getFilesForPath] + ) + + const getFolders = useCallback(() => { + return state.files.filter((f) => f.type === "folder" && !f.trashed) + }, [state.files]) + + return ( + + {children} + + ) +} + +export function useFiles() { + const ctx = useContext(FilesContext) + if (!ctx) throw new Error("useFiles must be used within FilesProvider") + return ctx +} + +function sortFiles( + files: FileItem[], + sortBy: SortField, + direction: SortDirection +): FileItem[] { + const sorted = [...files].sort((a, b) => { + // folders always first + if (a.type === "folder" && b.type !== "folder") return -1 + if (a.type !== "folder" && b.type === "folder") return 1 + + let cmp = 0 + switch (sortBy) { + case "name": + cmp = a.name.localeCompare(b.name) + break + case "modified": + cmp = new Date(a.modifiedAt).getTime() - new Date(b.modifiedAt).getTime() + break + case "size": + cmp = a.size - b.size + break + case "type": + cmp = a.type.localeCompare(b.type) + break + } + return direction === "asc" ? cmp : -cmp + }) + return sorted +} diff --git a/src/lib/file-utils.ts b/src/lib/file-utils.ts new file mode 100755 index 0000000..f6f3578 --- /dev/null +++ b/src/lib/file-utils.ts @@ -0,0 +1,104 @@ +import type { FileType } from "./files-data" + +export function formatFileSize(bytes: number): string { + if (bytes === 0) return "—" + const units = ["B", "KB", "MB", "GB", "TB"] + const i = Math.floor(Math.log(bytes) / Math.log(1024)) + const size = bytes / Math.pow(1024, i) + return `${size >= 10 ? Math.round(size) : size.toFixed(1)} ${units[i]}` +} + +export function getFileTypeFromExtension(filename: string): FileType { + const ext = filename.split(".").pop()?.toLowerCase() + if (!ext) return "unknown" + + const typeMap: Record = { + // documents + doc: "document", + docx: "document", + txt: "document", + rtf: "document", + odt: "document", + pptx: "document", + ppt: "document", + // spreadsheets + xls: "spreadsheet", + xlsx: "spreadsheet", + csv: "spreadsheet", + ods: "spreadsheet", + // images + png: "image", + jpg: "image", + jpeg: "image", + gif: "image", + svg: "image", + webp: "image", + fig: "image", + // video + mp4: "video", + mov: "video", + avi: "video", + mkv: "video", + webm: "video", + // audio + mp3: "audio", + wav: "audio", + ogg: "audio", + flac: "audio", + // pdf + pdf: "pdf", + // code + js: "code", + ts: "code", + tsx: "code", + jsx: "code", + py: "code", + go: "code", + rs: "code", + md: "code", + json: "code", + yaml: "code", + yml: "code", + toml: "code", + html: "code", + css: "code", + // archive + zip: "archive", + tar: "archive", + gz: "archive", + "7z": "archive", + rar: "archive", + } + + return typeMap[ext] ?? "unknown" +} + +export function formatRelativeDate(isoDate: string): string { + const date = new Date(isoDate) + const now = new Date() + const diffMs = now.getTime() - date.getTime() + const diffMins = Math.floor(diffMs / 60_000) + const diffHours = Math.floor(diffMs / 3_600_000) + const diffDays = Math.floor(diffMs / 86_400_000) + + if (diffMins < 1) return "Just now" + if (diffMins < 60) return `${diffMins}m ago` + if (diffHours < 24) return `${diffHours}h ago` + if (diffDays < 7) return `${diffDays}d ago` + + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: date.getFullYear() !== now.getFullYear() + ? "numeric" + : undefined, + }) +} + +export function formatDate(isoDate: string): string { + return new Date(isoDate).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }) +} diff --git a/src/lib/files-data.ts b/src/lib/files-data.ts new file mode 100755 index 0000000..677d66e --- /dev/null +++ b/src/lib/files-data.ts @@ -0,0 +1,720 @@ +export type FileType = + | "folder" + | "document" + | "spreadsheet" + | "image" + | "video" + | "pdf" + | "code" + | "archive" + | "audio" + | "unknown" + +export type SharedRole = "viewer" | "editor" + +export type SharedUser = { + name: string + avatar?: string + role: SharedRole +} + +export type FileOwner = { + name: string + avatar?: string +} + +export type FileItem = { + id: string + name: string + type: FileType + mimeType?: string + size: number + path: string[] + createdAt: string + modifiedAt: string + owner: FileOwner + starred: boolean + shared: boolean + sharedWith?: SharedUser[] + trashed: boolean + parentId: string | null +} + +export type StorageUsage = { + used: number + total: number +} + +const owner = { name: "Martine Vogel", avatar: "/avatars/martine.jpg" } +const teamMember1 = { name: "Alex Chen", avatar: "/avatars/alex.jpg" } +const teamMember2 = { name: "Jordan Park" } +const teamMember3 = { name: "Sam Rivera", avatar: "/avatars/sam.jpg" } + +export const mockFiles: FileItem[] = [ + // root folders + { + id: "folder-documents", + name: "Documents", + type: "folder", + size: 0, + path: [], + createdAt: "2025-09-15T10:00:00Z", + modifiedAt: "2026-01-20T14:30:00Z", + owner, + starred: true, + shared: false, + trashed: false, + parentId: null, + }, + { + id: "folder-images", + name: "Images", + type: "folder", + size: 0, + path: [], + createdAt: "2025-10-01T09:00:00Z", + modifiedAt: "2026-01-18T11:00:00Z", + owner, + starred: false, + shared: true, + sharedWith: [ + { ...teamMember1, role: "editor" }, + { ...teamMember2, role: "viewer" }, + ], + trashed: false, + parentId: null, + }, + { + id: "folder-projects", + name: "Projects", + type: "folder", + size: 0, + path: [], + createdAt: "2025-08-20T08:00:00Z", + modifiedAt: "2026-01-22T16:45:00Z", + owner, + starred: true, + shared: true, + sharedWith: [ + { ...teamMember1, role: "editor" }, + { ...teamMember3, role: "editor" }, + ], + trashed: false, + parentId: null, + }, + { + id: "folder-archive", + name: "Archive", + type: "folder", + size: 0, + path: [], + createdAt: "2025-06-10T12:00:00Z", + modifiedAt: "2025-12-05T09:30:00Z", + owner, + starred: false, + shared: false, + trashed: false, + parentId: null, + }, + { + id: "folder-shared-assets", + name: "Shared Assets", + type: "folder", + size: 0, + path: [], + createdAt: "2025-11-01T10:00:00Z", + modifiedAt: "2026-01-15T13:20:00Z", + owner: teamMember1, + starred: false, + shared: true, + sharedWith: [ + { ...owner, role: "editor" }, + { ...teamMember2, role: "viewer" }, + ], + trashed: false, + parentId: null, + }, + + // Documents folder contents + { + id: "doc-proposal", + name: "Project Proposal.docx", + type: "document", + mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + size: 245_000, + path: ["Documents"], + createdAt: "2025-11-10T14:00:00Z", + modifiedAt: "2026-01-19T10:15:00Z", + owner, + starred: true, + shared: true, + sharedWith: [{ ...teamMember1, role: "editor" }], + trashed: false, + parentId: "folder-documents", + }, + { + id: "doc-meeting-notes", + name: "Meeting Notes - Jan 2026.docx", + type: "document", + mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + size: 128_500, + path: ["Documents"], + createdAt: "2026-01-05T09:00:00Z", + modifiedAt: "2026-01-22T11:30:00Z", + owner, + starred: false, + shared: false, + trashed: false, + parentId: "folder-documents", + }, + { + id: "doc-budget", + name: "Budget 2026.xlsx", + type: "spreadsheet", + mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + size: 890_000, + path: ["Documents"], + createdAt: "2025-12-01T08:00:00Z", + modifiedAt: "2026-01-21T15:00:00Z", + owner, + starred: true, + shared: true, + sharedWith: [ + { ...teamMember1, role: "editor" }, + { ...teamMember3, role: "viewer" }, + ], + trashed: false, + parentId: "folder-documents", + }, + { + id: "doc-contract", + name: "Vendor Contract.pdf", + type: "pdf", + mimeType: "application/pdf", + size: 1_540_000, + path: ["Documents"], + createdAt: "2025-10-20T11:00:00Z", + modifiedAt: "2025-10-20T11:00:00Z", + owner, + starred: false, + shared: false, + trashed: false, + parentId: "folder-documents", + }, + { + id: "doc-timeline", + name: "Project Timeline.xlsx", + type: "spreadsheet", + mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + size: 456_000, + path: ["Documents"], + createdAt: "2025-11-28T13:00:00Z", + modifiedAt: "2026-01-18T09:45:00Z", + owner, + starred: false, + shared: true, + sharedWith: [{ ...teamMember2, role: "viewer" }], + trashed: false, + parentId: "folder-documents", + }, + { + id: "folder-documents-reports", + name: "Reports", + type: "folder", + size: 0, + path: ["Documents"], + createdAt: "2025-10-05T10:00:00Z", + modifiedAt: "2026-01-15T14:00:00Z", + owner, + starred: false, + shared: false, + trashed: false, + parentId: "folder-documents", + }, + + // Documents/Reports subfolder + { + id: "doc-q4-report", + name: "Q4 2025 Report.pdf", + type: "pdf", + mimeType: "application/pdf", + size: 2_100_000, + path: ["Documents", "Reports"], + createdAt: "2026-01-02T10:00:00Z", + modifiedAt: "2026-01-10T16:00:00Z", + owner, + starred: false, + shared: true, + sharedWith: [ + { ...teamMember1, role: "viewer" }, + { ...teamMember3, role: "viewer" }, + ], + trashed: false, + parentId: "folder-documents-reports", + }, + { + id: "doc-analytics", + name: "Site Analytics.xlsx", + type: "spreadsheet", + mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + size: 670_000, + path: ["Documents", "Reports"], + createdAt: "2025-12-20T14:00:00Z", + modifiedAt: "2026-01-15T14:00:00Z", + owner: teamMember1, + starred: false, + shared: true, + sharedWith: [{ ...owner, role: "editor" }], + trashed: false, + parentId: "folder-documents-reports", + }, + + // Images folder contents + { + id: "img-hero", + name: "hero-banner.png", + type: "image", + mimeType: "image/png", + size: 3_200_000, + path: ["Images"], + createdAt: "2025-12-10T10:00:00Z", + modifiedAt: "2025-12-10T10:00:00Z", + owner, + starred: false, + shared: true, + sharedWith: [{ ...teamMember2, role: "viewer" }], + trashed: false, + parentId: "folder-images", + }, + { + id: "img-logo", + name: "compass-logo.svg", + type: "image", + mimeType: "image/svg+xml", + size: 12_400, + path: ["Images"], + createdAt: "2025-09-01T08:00:00Z", + modifiedAt: "2025-11-15T16:30:00Z", + owner, + starred: true, + shared: true, + sharedWith: [ + { ...teamMember1, role: "editor" }, + { ...teamMember2, role: "viewer" }, + ], + trashed: false, + parentId: "folder-images", + }, + { + id: "img-mockup", + name: "dashboard-mockup.fig", + type: "image", + mimeType: "application/x-figma", + size: 8_900_000, + path: ["Images"], + createdAt: "2025-11-20T14:00:00Z", + modifiedAt: "2026-01-10T11:00:00Z", + owner: teamMember1, + starred: false, + shared: true, + sharedWith: [{ ...owner, role: "editor" }], + trashed: false, + parentId: "folder-images", + }, + { + id: "img-photo1", + name: "team-photo.jpg", + type: "image", + mimeType: "image/jpeg", + size: 4_500_000, + path: ["Images"], + createdAt: "2025-12-20T15:00:00Z", + modifiedAt: "2025-12-20T15:00:00Z", + owner: teamMember3, + starred: false, + shared: true, + sharedWith: [ + { ...owner, role: "viewer" }, + { ...teamMember1, role: "viewer" }, + ], + trashed: false, + parentId: "folder-images", + }, + { + id: "img-screenshot", + name: "app-screenshot.png", + type: "image", + mimeType: "image/png", + size: 1_800_000, + path: ["Images"], + createdAt: "2026-01-08T09:00:00Z", + modifiedAt: "2026-01-08T09:00:00Z", + owner, + starred: false, + shared: false, + trashed: false, + parentId: "folder-images", + }, + + // Projects folder contents + { + id: "folder-projects-compass", + name: "Compass", + type: "folder", + size: 0, + path: ["Projects"], + createdAt: "2025-08-20T08:00:00Z", + modifiedAt: "2026-01-22T16:45:00Z", + owner, + starred: true, + shared: true, + sharedWith: [ + { ...teamMember1, role: "editor" }, + { ...teamMember3, role: "editor" }, + ], + trashed: false, + parentId: "folder-projects", + }, + { + id: "folder-projects-website", + name: "Website Redesign", + type: "folder", + size: 0, + path: ["Projects"], + createdAt: "2025-10-15T09:00:00Z", + modifiedAt: "2026-01-20T10:00:00Z", + owner, + starred: false, + shared: true, + sharedWith: [{ ...teamMember1, role: "editor" }], + trashed: false, + parentId: "folder-projects", + }, + { + id: "proj-spec", + name: "Technical Spec.md", + type: "code", + mimeType: "text/markdown", + size: 34_000, + path: ["Projects"], + createdAt: "2025-09-10T10:00:00Z", + modifiedAt: "2026-01-15T08:00:00Z", + owner, + starred: false, + shared: true, + sharedWith: [{ ...teamMember1, role: "editor" }], + trashed: false, + parentId: "folder-projects", + }, + + // Projects/Compass subfolder + { + id: "compass-readme", + name: "README.md", + type: "code", + mimeType: "text/markdown", + size: 8_200, + path: ["Projects", "Compass"], + createdAt: "2025-08-20T08:30:00Z", + modifiedAt: "2026-01-22T16:45:00Z", + owner, + starred: false, + shared: true, + sharedWith: [{ ...teamMember1, role: "editor" }], + trashed: false, + parentId: "folder-projects-compass", + }, + { + id: "compass-design", + name: "Design System.fig", + type: "image", + mimeType: "application/x-figma", + size: 15_600_000, + path: ["Projects", "Compass"], + createdAt: "2025-09-05T14:00:00Z", + modifiedAt: "2026-01-18T11:30:00Z", + owner: teamMember1, + starred: true, + shared: true, + sharedWith: [ + { ...owner, role: "editor" }, + { ...teamMember3, role: "viewer" }, + ], + trashed: false, + parentId: "folder-projects-compass", + }, + { + id: "compass-api-doc", + name: "API Documentation.pdf", + type: "pdf", + mimeType: "application/pdf", + size: 980_000, + path: ["Projects", "Compass"], + createdAt: "2025-11-01T10:00:00Z", + modifiedAt: "2026-01-12T14:00:00Z", + owner, + starred: false, + shared: true, + sharedWith: [{ ...teamMember3, role: "viewer" }], + trashed: false, + parentId: "folder-projects-compass", + }, + + // Projects/Website Redesign subfolder + { + id: "web-wireframes", + name: "Wireframes.fig", + type: "image", + mimeType: "application/x-figma", + size: 12_300_000, + path: ["Projects", "Website Redesign"], + createdAt: "2025-10-20T11:00:00Z", + modifiedAt: "2026-01-20T10:00:00Z", + owner: teamMember1, + starred: false, + shared: true, + sharedWith: [{ ...owner, role: "editor" }], + trashed: false, + parentId: "folder-projects-website", + }, + { + id: "web-copy", + name: "Website Copy.docx", + type: "document", + mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + size: 67_000, + path: ["Projects", "Website Redesign"], + createdAt: "2025-11-15T09:00:00Z", + modifiedAt: "2026-01-16T13:00:00Z", + owner, + starred: false, + shared: true, + sharedWith: [{ ...teamMember1, role: "editor" }], + trashed: false, + parentId: "folder-projects-website", + }, + + // Shared Assets folder (owned by team member) + { + id: "shared-brand-guide", + name: "Brand Guidelines.pdf", + type: "pdf", + mimeType: "application/pdf", + size: 5_400_000, + path: ["Shared Assets"], + createdAt: "2025-11-01T10:00:00Z", + modifiedAt: "2025-11-01T10:00:00Z", + owner: teamMember1, + starred: false, + shared: true, + sharedWith: [ + { ...owner, role: "editor" }, + { ...teamMember2, role: "viewer" }, + ], + trashed: false, + parentId: "folder-shared-assets", + }, + { + id: "shared-icons", + name: "Icon Set.zip", + type: "archive", + mimeType: "application/zip", + size: 2_300_000, + path: ["Shared Assets"], + createdAt: "2025-11-05T14:00:00Z", + modifiedAt: "2025-11-05T14:00:00Z", + owner: teamMember1, + starred: false, + shared: true, + sharedWith: [{ ...owner, role: "viewer" }], + trashed: false, + parentId: "folder-shared-assets", + }, + { + id: "shared-fonts", + name: "Custom Fonts.zip", + type: "archive", + mimeType: "application/zip", + size: 1_100_000, + path: ["Shared Assets"], + createdAt: "2025-11-10T09:00:00Z", + modifiedAt: "2025-11-10T09:00:00Z", + owner: teamMember1, + starred: false, + shared: true, + sharedWith: [{ ...owner, role: "editor" }], + trashed: false, + parentId: "folder-shared-assets", + }, + + // Root-level files + { + id: "root-video", + name: "Product Demo.mp4", + type: "video", + mimeType: "video/mp4", + size: 45_000_000, + path: [], + createdAt: "2025-12-15T16:00:00Z", + modifiedAt: "2025-12-15T16:00:00Z", + owner, + starred: false, + shared: true, + sharedWith: [ + { ...teamMember1, role: "viewer" }, + { ...teamMember2, role: "viewer" }, + { ...teamMember3, role: "viewer" }, + ], + trashed: false, + parentId: null, + }, + { + id: "root-audio", + name: "Podcast Recording.mp3", + type: "audio", + mimeType: "audio/mpeg", + size: 18_500_000, + path: [], + createdAt: "2026-01-05T11:00:00Z", + modifiedAt: "2026-01-05T11:00:00Z", + owner: teamMember3, + starred: false, + shared: true, + sharedWith: [{ ...owner, role: "viewer" }], + trashed: false, + parentId: null, + }, + { + id: "root-presentation", + name: "Q1 Kickoff.pptx", + type: "document", + mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation", + size: 6_700_000, + path: [], + createdAt: "2026-01-02T08:00:00Z", + modifiedAt: "2026-01-20T17:00:00Z", + owner, + starred: true, + shared: true, + sharedWith: [ + { ...teamMember1, role: "editor" }, + { ...teamMember3, role: "editor" }, + ], + trashed: false, + parentId: null, + }, + { + id: "root-config", + name: "deploy-config.yaml", + type: "code", + mimeType: "text/yaml", + size: 2_400, + path: [], + createdAt: "2025-11-20T10:00:00Z", + modifiedAt: "2026-01-10T09:00:00Z", + owner, + starred: false, + shared: false, + trashed: false, + parentId: null, + }, + + // Trashed files + { + id: "trash-old-draft", + name: "Old Draft.docx", + type: "document", + mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + size: 156_000, + path: ["Documents"], + createdAt: "2025-08-10T10:00:00Z", + modifiedAt: "2025-12-28T14:00:00Z", + owner, + starred: false, + shared: false, + trashed: true, + parentId: "folder-documents", + }, + { + id: "trash-screenshot", + name: "old-screenshot.png", + type: "image", + mimeType: "image/png", + size: 2_400_000, + path: ["Images"], + createdAt: "2025-07-15T09:00:00Z", + modifiedAt: "2025-12-20T11:00:00Z", + owner, + starred: false, + shared: false, + trashed: true, + parentId: "folder-images", + }, + { + id: "trash-backup", + name: "backup-nov.zip", + type: "archive", + mimeType: "application/zip", + size: 34_000_000, + path: [], + createdAt: "2025-11-30T22:00:00Z", + modifiedAt: "2026-01-05T08:00:00Z", + owner, + starred: false, + shared: false, + trashed: true, + parentId: null, + }, + + // Archive folder contents + { + id: "archive-old-project", + name: "2024 Campaign.zip", + type: "archive", + mimeType: "application/zip", + size: 89_000_000, + path: ["Archive"], + createdAt: "2025-01-15T10:00:00Z", + modifiedAt: "2025-06-10T12:00:00Z", + owner, + starred: false, + shared: false, + trashed: false, + parentId: "folder-archive", + }, + { + id: "archive-legacy-docs", + name: "Legacy Documentation.pdf", + type: "pdf", + mimeType: "application/pdf", + size: 3_200_000, + path: ["Archive"], + createdAt: "2025-03-20T14:00:00Z", + modifiedAt: "2025-03-20T14:00:00Z", + owner, + starred: false, + shared: false, + trashed: false, + parentId: "folder-archive", + }, + { + id: "archive-old-video", + name: "Training Video 2024.mp4", + type: "video", + mimeType: "video/mp4", + size: 120_000_000, + path: ["Archive"], + createdAt: "2024-11-10T16:00:00Z", + modifiedAt: "2024-11-10T16:00:00Z", + owner, + starred: false, + shared: false, + trashed: false, + parentId: "folder-archive", + }, +] + +export const mockStorageUsage: StorageUsage = { + used: 412_000_000, + total: 5_000_000_000, +}