refactor: migrate to vite and restructure repo

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2025-10-18 14:02:20 +00:00
parent 83a5f92506
commit 25796ab609
94 changed files with 478 additions and 312 deletions

View File

@@ -0,0 +1,14 @@
import type { Doc } from "@fileone/convex/_generated/dataModel"
import type { FileSystemItem } from "@fileone/convex/filesystem"
import type { DirectoryInfo } from "@fileone/convex/types"
import { createContext } from "react"
type DirectoryPageContextType = {
rootDirectory: Doc<"directories">
directory: DirectoryInfo
directoryContent: FileSystemItem[]
}
export const DirectoryPageContext = createContext<DirectoryPageContextType>(
null as unknown as DirectoryPageContextType,
)

View File

@@ -0,0 +1,116 @@
import { api } from "@fileone/convex/_generated/api"
import { newFileSystemHandle } from "@fileone/convex/filesystem"
import { useMutation } from "@tanstack/react-query"
import { useMutation as useContextMutation } from "convex/react"
import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai"
import { TextCursorInputIcon, TrashIcon } from "lucide-react"
import { toast } from "sonner"
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
} from "@/components/ui/context-menu"
import {
contextMenuTargeItemsAtom,
itemBeingRenamedAtom,
optimisticDeletedItemsAtom,
} from "./state"
export function DirectoryContentContextMenu({
children,
}: {
children: React.ReactNode
}) {
const store = useStore()
const [target, setTarget] = useAtom(contextMenuTargeItemsAtom)
const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom)
const moveToTrashMutation = useContextMutation(api.filesystem.moveToTrash)
const { mutate: moveToTrash } = useMutation({
mutationFn: moveToTrashMutation,
onMutate: ({ handles }) => {
setOptimisticDeletedItems(
(prev) =>
new Set([...prev, ...handles.map((handle) => handle.id)]),
)
},
onSuccess: ({ deleted, errors }, { handles }) => {
setOptimisticDeletedItems((prev) => {
const newSet = new Set(prev)
for (const handle of handles) {
newSet.delete(handle.id)
}
return newSet
})
if (errors.length === 0 && deleted.length === handles.length) {
toast.success(`Moved ${handles.length} items to trash`)
} else if (errors.length === handles.length) {
toast.error("Failed to move to trash")
} else {
toast.info(
`Moved ${deleted.length} items to trash; failed to move ${errors.length} items`,
)
}
},
})
const handleDelete = () => {
const selectedItems = store.get(contextMenuTargeItemsAtom)
if (selectedItems.length > 0) {
moveToTrash({
handles: selectedItems.map(newFileSystemHandle),
})
}
}
return (
<ContextMenu
onOpenChange={(open) => {
if (!open) {
setTarget([])
}
}}
>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
{target && (
<ContextMenuContent>
<RenameMenuItem />
<ContextMenuItem onClick={handleDelete}>
<TrashIcon />
Move to trash
</ContextMenuItem>
</ContextMenuContent>
)}
</ContextMenu>
)
}
function RenameMenuItem() {
const store = useStore()
const target = useAtomValue(contextMenuTargeItemsAtom)
const setItemBeingRenamed = useSetAtom(itemBeingRenamedAtom)
const handleRename = () => {
const selectedItems = store.get(contextMenuTargeItemsAtom)
if (selectedItems.length === 1) {
// biome-ignore lint/style/noNonNullAssertion: length is checked
const selectedItem = selectedItems[0]!
setItemBeingRenamed({
originalItem: selectedItem,
name: selectedItem.doc.name,
})
}
}
// Only render if exactly one item is selected
if (target.length !== 1) {
return null
}
return (
<ContextMenuItem onClick={handleRename}>
<TextCursorInputIcon />
Rename
</ContextMenuItem>
)
}

View File

@@ -0,0 +1,68 @@
import { Skeleton } from "@/components/ui/skeleton"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
export function DirectoryContentTableSkeleton({ rows = 8 }: { rows?: number }) {
return (
<div className="overflow-hidden">
<Table>
<TableHeader>
<TableRow className="px-4">
<TableHead
className="first:pl-4 last:pr-4"
style={{ width: 24 }}
>
<Skeleton className="size-4 rounded" />
</TableHead>
<TableHead
className="first:pl-4 last:pr-4"
style={{ width: 1000 }}
>
<Skeleton className="h-4 w-12" />
</TableHead>
<TableHead className="first:pl-4 last:pr-4">
<Skeleton className="h-4 w-8" />
</TableHead>
<TableHead className="first:pl-4 last:pr-4">
<Skeleton className="h-4 w-20" />
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{Array.from({ length: rows }).map((_, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: this is static so ok
<DirectoryContentTableRowSkeleton key={index} />
))}
</TableBody>
</Table>
</div>
)
}
function DirectoryContentTableRowSkeleton() {
return (
<TableRow>
<TableCell className="first:pl-4 last:pr-4" style={{ width: 24 }}>
<Skeleton className="size-4 rounded" />
</TableCell>
<TableCell className="first:pl-4 last:pr-4" style={{ width: 1000 }}>
<div className="flex w-full items-center gap-2">
<Skeleton className="size-4 rounded" />
<Skeleton className="h-4 w-32" />
</div>
</TableCell>
<TableCell className="first:pl-4 last:pr-4">
<Skeleton className="h-4 w-16" />
</TableCell>
<TableCell className="first:pl-4 last:pr-4">
<Skeleton className="h-4 w-24" />
</TableCell>
</TableRow>
)
}

View File

@@ -0,0 +1,411 @@
import type { Doc } from "@fileone/convex/_generated/dataModel"
import {
type DirectoryHandle,
type FileHandle,
type FileSystemHandle,
type FileSystemItem,
FileType,
isSameHandle,
newDirectoryHandle,
newFileHandle,
newFileSystemHandle,
} from "@fileone/convex/filesystem"
import { Link, useNavigate } from "@tanstack/react-router"
import {
type ColumnDef,
flexRender,
getCoreRowModel,
type Row,
type Table as TableType,
useReactTable,
} from "@tanstack/react-table"
import { type PrimitiveAtom, useSetAtom, useStore } from "jotai"
import { useContext, useEffect, useMemo, useRef } from "react"
import { DirectoryIcon } from "@/components/icons/directory-icon"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
isControlOrCommandKeyActive,
keyboardModifierAtom,
} from "@/lib/keyboard"
import { TextFileIcon } from "../../components/icons/text-file-icon"
import { type FileDragInfo, useFileDrop } from "../../files/use-file-drop"
import { cn } from "../../lib/utils"
import { DirectoryPageContext } from "./context"
type DirectoryContentTableProps = {
filterFn: (item: FileSystemItem) => boolean
directoryUrlFn: (directory: Doc<"directories">) => string
fileDragInfoAtom: PrimitiveAtom<FileDragInfo | null>
onContextMenu: (
row: Row<FileSystemItem>,
table: TableType<FileSystemItem>,
) => void
onOpenFile: (file: Doc<"files">) => void
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 B"
const k = 1024
const sizes = ["B", "KB", "MB", "GB", "TB", "PB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`
}
function useTableColumns(
onOpenFile: (file: Doc<"files">) => void,
directoryUrlFn: (directory: Doc<"directories">) => string,
): ColumnDef<FileSystemItem>[] {
return useMemo(
() => [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(value) => {
table.toggleAllPageRowsSelected(!!value)
}}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onClick={(e) => {
e.stopPropagation()
}}
onCheckedChange={row.getToggleSelectedHandler()}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
size: 24,
},
{
header: "Name",
accessorKey: "doc.name",
cell: ({ row }) => {
switch (row.original.kind) {
case FileType.File:
return (
<FileNameCell
file={row.original.doc}
onOpenFile={onOpenFile}
/>
)
case FileType.Directory:
return (
<DirectoryNameCell
directory={row.original.doc}
directoryUrlFn={directoryUrlFn}
/>
)
}
},
size: 1000,
},
{
header: "Size",
accessorKey: "size",
cell: ({ row }) => {
switch (row.original.kind) {
case FileType.File:
return (
<div>
{formatFileSize(row.original.doc.size)}
</div>
)
case FileType.Directory:
return <div className="font-mono">-</div>
}
},
},
{
header: "Created At",
accessorKey: "createdAt",
cell: ({ row }) => {
return (
<div>
{new Date(
row.original.doc.createdAt,
).toLocaleString()}
</div>
)
},
},
],
[onOpenFile, directoryUrlFn],
)
}
export function DirectoryContentTable({
filterFn,
directoryUrlFn,
onContextMenu,
fileDragInfoAtom,
onOpenFile,
}: DirectoryContentTableProps) {
const { directoryContent } = useContext(DirectoryPageContext)
const store = useStore()
const navigate = useNavigate()
const table = useReactTable({
data: directoryContent || [],
columns: useTableColumns(onOpenFile, directoryUrlFn),
getCoreRowModel: getCoreRowModel(),
enableRowSelection: true,
enableGlobalFilter: true,
globalFilterFn: (row, _columnId, _filterValue, _addMeta) =>
filterFn(row.original),
getRowId: (row) => row.doc._id,
})
useEffect(
function escapeToClearSelections() {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") {
table.setRowSelection({})
}
}
window.addEventListener("keydown", handleEscape)
return () => window.removeEventListener("keydown", handleEscape)
},
[table.setRowSelection],
)
const handleRowContextMenu = (
row: Row<FileSystemItem>,
_event: React.MouseEvent,
) => {
if (!row.getIsSelected()) {
selectRow(row)
}
onContextMenu(row, table)
}
const selectRow = (row: Row<FileSystemItem>) => {
const keyboardModifiers = store.get(keyboardModifierAtom)
const isMultiSelectMode = isControlOrCommandKeyActive(keyboardModifiers)
const isRowSelected = row.getIsSelected()
if (isRowSelected && isMultiSelectMode) {
row.toggleSelected(false)
} else if (isRowSelected && !isMultiSelectMode) {
table.setRowSelection({
[row.id]: true,
})
row.toggleSelected(true)
} else if (!isRowSelected) {
if (isMultiSelectMode) {
row.toggleSelected(true)
} else {
table.setRowSelection({
[row.id]: true,
})
}
}
}
const handleRowDoubleClick = (row: Row<FileSystemItem>) => {
if (row.original.kind === FileType.Directory) {
navigate({
to: `/directories/${row.original.doc._id}`,
})
}
}
return (
<div className="overflow-hidden">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow className="px-4" key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
className="first:pl-4 last:pr-4"
key={header.id}
style={{ width: header.getSize() }}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<FileItemRow
key={row.id}
table={table}
row={row}
onClick={() => selectRow(row)}
fileDragInfoAtom={fileDragInfoAtom}
onContextMenu={(e) =>
handleRowContextMenu(row, e)
}
onDoubleClick={() => {
handleRowDoubleClick(row)
}}
/>
))
) : (
<NoResultsRow />
)}
</TableBody>
</Table>
</div>
)
}
function NoResultsRow() {
return (
<TableRow className="hover:bg-transparent">
<TableCell colSpan={4} className="text-center">
No results.
</TableCell>
</TableRow>
)
}
function FileItemRow({
table,
row,
onClick,
onContextMenu,
onDoubleClick,
fileDragInfoAtom,
}: {
table: TableType<FileSystemItem>
row: Row<FileSystemItem>
onClick: () => void
onContextMenu: (e: React.MouseEvent) => void
onDoubleClick: () => void
fileDragInfoAtom: PrimitiveAtom<FileDragInfo | null>
}) {
const ref = useRef<HTMLTableRowElement>(null)
const setFileDragInfo = useSetAtom(fileDragInfoAtom)
const { isDraggedOver, dropHandlers } = useFileDrop({
destItem:
row.original.kind === FileType.Directory
? newDirectoryHandle(row.original.doc._id)
: null,
dragInfoAtom: fileDragInfoAtom,
})
const handleDragStart = (_e: React.DragEvent) => {
let source: DirectoryHandle | FileHandle
switch (row.original.kind) {
case FileType.File:
source = newFileHandle(row.original.doc._id)
break
case FileType.Directory:
source = newDirectoryHandle(row.original.doc._id)
break
}
let draggedItems: FileSystemHandle[]
// drag all selections, but only if the currently dragged row is also selected
if (row.getIsSelected()) {
draggedItems = table
.getSelectedRowModel()
.rows.map((row) => newFileSystemHandle(row.original))
if (!draggedItems.some((item) => isSameHandle(item, source))) {
draggedItems.push(source)
}
} else {
draggedItems = [source]
}
setFileDragInfo({
source,
items: draggedItems,
})
}
const handleDragEnd = () => {
setFileDragInfo(null)
}
return (
<TableRow
draggable
ref={ref}
key={row.id}
data-state={row.getIsSelected() && "selected"}
onClick={onClick}
onDoubleClick={onDoubleClick}
onContextMenu={onContextMenu}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
{...dropHandlers}
className={cn({ "bg-muted": isDraggedOver })}
>
{row.getVisibleCells().map((cell) => (
<TableCell
className="first:pl-4 last:pr-4"
key={cell.id}
style={{ width: cell.column.getSize() }}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
}
function DirectoryNameCell({
directory,
directoryUrlFn,
}: {
directory: Doc<"directories">
directoryUrlFn: (directory: Doc<"directories">) => string
}) {
return (
<div className="flex w-full items-center gap-2">
<DirectoryIcon className="size-4" />
<Link className="hover:underline" to={directoryUrlFn(directory)}>
{directory.name}
</Link>
</div>
)
}
function FileNameCell({
file,
onOpenFile,
}: {
file: Doc<"files">
onOpenFile: (file: Doc<"files">) => void
}) {
return (
<div className="flex w-full items-center gap-2">
<TextFileIcon className="size-4" />
<button
type="button"
className="hover:underline cursor-pointer"
onClick={() => {
onOpenFile(file)
}}
>
{file.name}
</button>
</div>
)
}

View File

@@ -0,0 +1,44 @@
import { Skeleton } from "@/components/ui/skeleton"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
} from "../../components/ui/breadcrumb"
import { Button } from "../../components/ui/button"
import { DirectoryContentTableSkeleton } from "./directory-content-table-skeleton"
export function DirectoryPageSkeleton() {
return (
<>
<header className="flex py-1 shrink-0 items-center gap-2 border-b px-4 w-full">
<BreadcrumbSkeleton />
<div className="ml-auto flex flex-row gap-2">
<Button size="sm" type="button" variant="outline" disabled>
<Skeleton className="size-4 rounded" />
<Skeleton className="h-4 w-8" />
<Skeleton className="size-4 rounded" />
</Button>
<Button size="sm" type="button" disabled>
<Skeleton className="size-4 rounded" />
<Skeleton className="h-4 w-16" />
</Button>
</div>
</header>
<div className="w-full">
<DirectoryContentTableSkeleton />
</div>
</>
)
}
function BreadcrumbSkeleton() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<Skeleton className="h-4 w-16" />
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}

View File

@@ -0,0 +1,106 @@
import type { Id } from "@fileone/convex/_generated/dataModel"
import type {
DirectoryHandle,
DirectoryPathComponent,
} from "@fileone/convex/filesystem"
import { Link } from "@tanstack/react-router"
import { Fragment, useContext } from "react"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "../../components/ui/breadcrumb"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../components/ui/tooltip"
import { useFileDrop } from "../../files/use-file-drop"
import { cn } from "../../lib/utils"
import { DirectoryPageContext } from "./context"
import { dragInfoAtom } from "./state"
export function FilePathBreadcrumb({
rootLabel,
directoryUrlFn,
}: {
rootLabel: string
directoryUrlFn: (directory: Id<"directories">) => string
}) {
const { rootDirectory, directory } = useContext(DirectoryPageContext)
const breadcrumbItems: React.ReactNode[] = []
for (let i = 1; i < directory.path.length - 1; i++) {
breadcrumbItems.push(
<Fragment key={directory.path[i]?.handle.id}>
<BreadcrumbSeparator />
<FilePathBreadcrumbItem
component={directory.path[i]!}
rootLabel={rootLabel}
directoryUrlFn={directoryUrlFn}
/>
</Fragment>,
)
}
return (
<Breadcrumb>
<BreadcrumbList>
{rootDirectory._id === directory._id ? (
<BreadcrumbItem>
<BreadcrumbPage>{rootLabel}</BreadcrumbPage>
</BreadcrumbItem>
) : (
<FilePathBreadcrumbItem
component={directory.path[0]!}
rootLabel={rootLabel}
directoryUrlFn={directoryUrlFn}
/>
)}
{breadcrumbItems}
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{directory.name}</BreadcrumbPage>{" "}
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}
function FilePathBreadcrumbItem({
component,
rootLabel,
directoryUrlFn,
}: {
component: DirectoryPathComponent
rootLabel: string
directoryUrlFn: (directory: Id<"directories">) => string
}) {
const { isDraggedOver, dropHandlers } = useFileDrop({
destItem: component.handle as DirectoryHandle,
dragInfoAtom,
})
const dirName = component.name || rootLabel
return (
<Tooltip open={isDraggedOver}>
<TooltipTrigger asChild>
<BreadcrumbItem
className={cn({ "bg-muted": isDraggedOver })}
{...dropHandlers}
>
<BreadcrumbLink asChild>
<Link to={directoryUrlFn(component.handle.id)}>
{dirName}
</Link>
</BreadcrumbLink>
</BreadcrumbItem>
</TooltipTrigger>
<TooltipContent>Move to {dirName}</TooltipContent>
</Tooltip>
)
}

View File

@@ -0,0 +1,72 @@
import { api } from "@fileone/convex/_generated/api"
import type { Id } from "@fileone/convex/_generated/dataModel"
import { useMutation } from "@tanstack/react-query"
import { useMutation as useContextMutation } from "convex/react"
import { useId } from "react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
export function NewDirectoryDialog({
open,
onOpenChange,
directoryId,
}: {
open: boolean
onOpenChange: (open: boolean) => void
directoryId: Id<"directories">
}) {
const formId = useId()
const { mutate: createDirectory, isPending: isCreating } = useMutation({
mutationFn: useContextMutation(api.files.createDirectory),
onSuccess: () => {
onOpenChange(false)
toast.success("Directory created successfully")
},
})
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const name = formData.get("directoryName") as string
if (name) {
createDirectory({ name, directoryId })
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>New Directory</DialogTitle>
</DialogHeader>
<form id={formId} onSubmit={onSubmit}>
<Input name="directoryName" />
</form>
<DialogFooter>
<DialogClose asChild>
<Button loading={isCreating} variant="outline">
<span>Cancel</span>
</Button>
</DialogClose>
<Button loading={isCreating} type="submit" form={formId}>
<span>Create</span>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,93 @@
import { api } from "@fileone/convex/_generated/api"
import { type FileSystemItem, FileType } from "@fileone/convex/filesystem"
import { useMutation } from "@tanstack/react-query"
import { useMutation as useContextMutation } from "convex/react"
import { useId } from "react"
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
type RenameFileDialogProps = {
item: FileSystemItem
onRenameSuccess: () => void
onClose: () => void
}
export function RenameFileDialog({
item,
onRenameSuccess,
onClose,
}: RenameFileDialogProps) {
const formId = useId()
const { mutate: renameFile, isPending: isRenaming } = useMutation({
mutationFn: useContextMutation(api.files.renameFile),
onSuccess: () => {
onRenameSuccess()
},
})
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const formData = new FormData(event.currentTarget)
const newName = formData.get("itemName") as string
if (newName) {
switch (item.kind) {
case FileType.File:
renameFile({
directoryId: item.doc.directoryId,
itemId: item.doc._id,
newName,
})
break
default:
break
}
}
}
return (
<Dialog
open
onOpenChange={(open) => {
if (!open) {
onClose()
}
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Rename File</DialogTitle>
</DialogHeader>
<form id={formId} onSubmit={onSubmit}>
<RenameFileInput initialValue={item.doc.name} />
</form>
<DialogFooter>
<DialogClose asChild>
<Button loading={isRenaming} variant="outline">
<span>Cancel</span>
</Button>
</DialogClose>
<Button loading={isRenaming} type="submit" form={formId}>
<span>Rename</span>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
function RenameFileInput({ initialValue }: { initialValue: string }) {
return <Input defaultValue={initialValue} name="itemName" />
}

View File

@@ -0,0 +1,46 @@
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { DirectoryContentTableSkeleton } from "./directory-content-table-skeleton"
import { DirectoryPageSkeleton } from "./directory-page-skeleton"
export function SkeletonDemo() {
const [showPageSkeleton, setShowPageSkeleton] = useState(false)
const [showTableSkeleton, setShowTableSkeleton] = useState(false)
return (
<div className="p-4 space-y-4">
<div className="flex gap-2">
<Button
onClick={() => setShowPageSkeleton(!showPageSkeleton)}
variant={showPageSkeleton ? "default" : "outline"}
>
{showPageSkeleton ? "Hide" : "Show"} Page Skeleton
</Button>
<Button
onClick={() => setShowTableSkeleton(!showTableSkeleton)}
variant={showTableSkeleton ? "default" : "outline"}
>
{showTableSkeleton ? "Hide" : "Show"} Table Skeleton
</Button>
</div>
{showPageSkeleton && (
<div className="border rounded-lg p-4">
<h3 className="text-lg font-semibold mb-4">
Directory Page Skeleton
</h3>
<DirectoryPageSkeleton />
</div>
)}
{showTableSkeleton && (
<div className="border rounded-lg p-4">
<h3 className="text-lg font-semibold mb-4">
Directory Content Table Skeleton
</h3>
<DirectoryContentTableSkeleton rows={5} />
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,21 @@
import type { Doc, Id } from "@fileone/convex/_generated/dataModel"
import type { FileSystemItem, FileType } from "@fileone/convex/filesystem"
import type { RowSelectionState } from "@tanstack/react-table"
import { atom } from "jotai"
import type { FileDragInfo } from "../../files/use-file-drop"
export const contextMenuTargeItemsAtom = atom<FileSystemItem[]>([])
export const optimisticDeletedItemsAtom = atom(
new Set<Id<"files"> | Id<"directories">>(),
)
export const selectedFileRowsAtom = atom<RowSelectionState>({})
export const itemBeingRenamedAtom = atom<{
originalItem: FileSystemItem
name: string
} | null>(null)
export const openedFileAtom = atom<Doc<"files"> | null>(null)
export const dragInfoAtom = atom<FileDragInfo | null>(null)