From 211382bfe97a8ca0791fbf44ec10e5e049579957 Mon Sep 17 00:00:00 2001 From: kenneth Date: Fri, 19 Sep 2025 23:01:44 +0000 Subject: [PATCH] refactor: top level dir + moved route create a root directory entry in table for each user and move file browser under /directories/$id --- packages/convex/files.ts | 17 +- packages/convex/model/directories.ts | 33 +- packages/convex/model/user.ts | 17 +- .../web/src/dashboard/dashboard-sidebar.tsx | 34 +- .../src/directories/directory-page/context.ts | 12 + .../directory-content-table-skeleton.tsx | 68 +++ .../directory-content-table.tsx | 423 ++++++++++++++++++ .../directory-page-skeleton.tsx | 44 ++ .../directory-page/directory-page.tsx | 181 ++++++++ .../directory-page/rename-file-dialog.tsx | 111 +++++ .../directory-page/skeleton-demo.tsx | 42 ++ .../src/directories/directory-page/state.ts | 22 + packages/web/src/routeTree.gen.ts | 35 +- .../routes/_authenticated/_sidebar-layout.tsx | 1 + .../directories.$directoryId.tsx | 32 ++ 15 files changed, 1047 insertions(+), 25 deletions(-) create mode 100644 packages/web/src/directories/directory-page/context.ts create mode 100644 packages/web/src/directories/directory-page/directory-content-table-skeleton.tsx create mode 100644 packages/web/src/directories/directory-page/directory-content-table.tsx create mode 100644 packages/web/src/directories/directory-page/directory-page-skeleton.tsx create mode 100644 packages/web/src/directories/directory-page/directory-page.tsx create mode 100644 packages/web/src/directories/directory-page/rename-file-dialog.tsx create mode 100644 packages/web/src/directories/directory-page/skeleton-demo.tsx create mode 100644 packages/web/src/directories/directory-page/state.ts create mode 100644 packages/web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx diff --git a/packages/convex/files.ts b/packages/convex/files.ts index 354dfa3..b7a742e 100644 --- a/packages/convex/files.ts +++ b/packages/convex/files.ts @@ -26,6 +26,21 @@ export const fetchFiles = authenticatedQuery({ }, }) +export const fetchRootDirectory = authenticatedQuery({ + handler: async (ctx) => { + return await Directories.fetchRoot(ctx) + }, +}) + +export const fetchDirectory = authenticatedQuery({ + args: { + directoryId: v.id("directories"), + }, + handler: async (ctx, { directoryId }) => { + return await Directories.fetch(ctx, { directoryId }) + }, +}) + export const fetchDirectoryContent = authenticatedQuery({ args: { directoryId: v.optional(v.id("directories")), @@ -39,7 +54,7 @@ export const fetchDirectoryContent = authenticatedQuery({ export const createDirectory = authenticatedMutation({ args: { name: v.string(), - directoryId: v.optional(v.id("directories")), + directoryId: v.id("directories"), }, handler: async (ctx, { name, directoryId }): Promise> => { return await Directories.create(ctx, { diff --git a/packages/convex/model/directories.ts b/packages/convex/model/directories.ts index 3b14bff..bd52405 100644 --- a/packages/convex/model/directories.ts +++ b/packages/convex/model/directories.ts @@ -19,6 +19,22 @@ type File = { export type DirectoryItem = Directory | File export type DirectoryItemKind = DirectoryItem["kind"] +export async function fetchRoot(ctx: AuthenticatedQueryCtx) { + return await ctx.db + .query("directories") + .withIndex("byParentId", (q) => + q.eq("userId", ctx.user._id).eq("parentId", undefined), + ) + .first() +} + +export async function fetch( + ctx: AuthenticatedQueryCtx, + { directoryId }: { directoryId: Id<"directories"> }, +) { + return await ctx.db.get(directoryId) +} + export async function fetchContent( ctx: AuthenticatedQueryCtx, { @@ -76,17 +92,14 @@ export async function fetchContent( export async function create( ctx: AuthenticatedMutationCtx, - { name, parentId }: { name: string; parentId?: Id<"directories"> }, + { name, parentId }: { name: string; parentId: Id<"directories"> }, ): Promise> { - let parentDir: Doc<"directories"> | null = null - if (parentId) { - parentDir = await ctx.db.get(parentId) - if (!parentDir) { - throw Err.create( - Err.Code.DirectoryNotFound, - `Parent directory ${parentId} not found`, - ) - } + const parentDir = await ctx.db.get(parentId) + if (!parentDir) { + throw Err.create( + Err.Code.DirectoryNotFound, + `Parent directory ${parentId} not found`, + ) } const existing = await ctx.db diff --git a/packages/convex/model/user.ts b/packages/convex/model/user.ts index 68e27c6..5ffb316 100644 --- a/packages/convex/model/user.ts +++ b/packages/convex/model/user.ts @@ -1,4 +1,3 @@ -import type { Id } from "../_generated/dataModel" import type { MutationCtx, QueryCtx } from "../_generated/server" import type { AuthenticatedMutationCtx } from "../functions" import * as Err from "./error" @@ -40,7 +39,17 @@ export async function userOrThrow(ctx: QueryCtx | MutationCtx) { } export async function register(ctx: AuthenticatedMutationCtx) { - await ctx.db.insert("users", { - jwtSubject: ctx.identity.subject, - }) + const now = new Date().toISOString() + await Promise.all([ + ctx.db.insert("users", { + jwtSubject: ctx.identity.subject, + }), + ctx.db.insert("directories", { + name: "", + path: "", + userId: ctx.user._id, + createdAt: now, + updatedAt: now, + }), + ]) } diff --git a/packages/web/src/dashboard/dashboard-sidebar.tsx b/packages/web/src/dashboard/dashboard-sidebar.tsx index 1e51f77..aaf7364 100644 --- a/packages/web/src/dashboard/dashboard-sidebar.tsx +++ b/packages/web/src/dashboard/dashboard-sidebar.tsx @@ -1,5 +1,7 @@ +import { api } from "@fileone/convex/_generated/api" import { Link, useLocation } from "@tanstack/react-router" import { useAuth } from "@workos-inc/authkit-react" +import { useQuery as useConvexQuery } from "convex/react" import { ChevronDownIcon, FilesIcon, @@ -59,18 +61,34 @@ function MainSidebarMenu() { - - - - - All Files - - - + ) } +function AllFilesItem() { + const location = useLocation() + const rootDirectory = useConvexQuery(api.files.fetchRootDirectory) + + if (!rootDirectory) return null + + return ( + + + + + All Files + + + + ) +} + function UserMenu() { const { signOut } = useAuth() diff --git a/packages/web/src/directories/directory-page/context.ts b/packages/web/src/directories/directory-page/context.ts new file mode 100644 index 0000000..86edb1e --- /dev/null +++ b/packages/web/src/directories/directory-page/context.ts @@ -0,0 +1,12 @@ +import type { Doc } from "@fileone/convex/_generated/dataModel" +import type { DirectoryItem } from "@fileone/convex/model/directories" +import { createContext } from "react" + +type DirectoryPageContextType = { + directory: Doc<"directories"> + directoryContent: DirectoryItem[] +} + +export const DirectoryPageContext = createContext( + null as unknown as DirectoryPageContextType, +) diff --git a/packages/web/src/directories/directory-page/directory-content-table-skeleton.tsx b/packages/web/src/directories/directory-page/directory-content-table-skeleton.tsx new file mode 100644 index 0000000..762fbc1 --- /dev/null +++ b/packages/web/src/directories/directory-page/directory-content-table-skeleton.tsx @@ -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 ( +
+ + + + + + + + + + + + + + + + + + + {Array.from({ length: rows }).map((_, index) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: this is static so ok + + ))} + +
+
+ ) +} + +function DirectoryContentTableRowSkeleton() { + return ( + + + + + +
+ + +
+
+ + + + + + +
+ ) +} diff --git a/packages/web/src/directories/directory-page/directory-content-table.tsx b/packages/web/src/directories/directory-page/directory-content-table.tsx new file mode 100644 index 0000000..714e4f5 --- /dev/null +++ b/packages/web/src/directories/directory-page/directory-content-table.tsx @@ -0,0 +1,423 @@ +import { api } from "@fileone/convex/_generated/api" +import type { Doc } from "@fileone/convex/_generated/dataModel" +import type { DirectoryItem } from "@fileone/convex/model/directories" +import { useMutation } from "@tanstack/react-query" +import { Link } from "@tanstack/react-router" +import { + type ColumnDef, + flexRender, + getCoreRowModel, + type Row, + useReactTable, +} from "@tanstack/react-table" +import { useMutation as useContextMutation } from "convex/react" +import { useAtom, useAtomValue, useSetAtom, useStore } from "jotai" +import { CheckIcon, TextCursorInputIcon, TrashIcon, XIcon } from "lucide-react" +import { useContext, useEffect, useId, useRef } from "react" +import { toast } from "sonner" +import { DirectoryIcon } from "@/components/icons/directory-icon" +import { Checkbox } from "@/components/ui/checkbox" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@/components/ui/context-menu" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { TextFileIcon } from "../../components/icons/text-file-icon" +import { Button } from "../../components/ui/button" +import { LoadingSpinner } from "../../components/ui/loading-spinner" +import { withDefaultOnError } from "../../lib/error" +import { cn } from "../../lib/utils" +import { DirectoryPageContext } from "./context" +import { + contextMenuTargeItemAtom, + itemBeingRenamedAtom, + newItemKindAtom, + optimisticDeletedItemsAtom, +} from "./state" + +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]}` +} + +const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + + table.toggleAllPageRowsSelected(!!value) + } + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + + ), + enableSorting: false, + enableHiding: false, + size: 24, + }, + { + header: "Name", + accessorKey: "doc.name", + cell: ({ row }) => { + switch (row.original.kind) { + case "file": + return + case "directory": + return + } + }, + size: 1000, + }, + { + header: "Size", + accessorKey: "size", + cell: ({ row }) => { + switch (row.original.kind) { + case "file": + return
{formatFileSize(row.original.doc.size)}
+ case "directory": + return
-
+ } + }, + }, + { + header: "Created At", + accessorKey: "createdAt", + cell: ({ row }) => { + return ( +
+ {new Date(row.original.doc.createdAt).toLocaleString()} +
+ ) + }, + }, +] + +export function DirectoryContentTable() { + return ( + +
+ +
+
+ ) +} + +export function DirectoryContentTableContextMenu({ + children, +}: { + children: React.ReactNode +}) { + const store = useStore() + const target = useAtomValue(contextMenuTargeItemAtom) + const setOptimisticDeletedItems = useSetAtom(optimisticDeletedItemsAtom) + const moveToTrashMutation = useContextMutation(api.files.moveToTrash) + const setItemBeingRenamed = useSetAtom(itemBeingRenamedAtom) + const { mutate: moveToTrash } = useMutation({ + mutationFn: moveToTrashMutation, + onMutate: ({ itemId }) => { + setOptimisticDeletedItems((prev) => new Set([...prev, itemId])) + }, + onSuccess: (itemId) => { + setOptimisticDeletedItems((prev) => { + const newSet = new Set(prev) + newSet.delete(itemId) + return newSet + }) + toast.success("Moved to trash") + }, + }) + + const handleRename = () => { + const selectedItem = store.get(contextMenuTargeItemAtom) + if (selectedItem) { + setItemBeingRenamed({ + kind: selectedItem.kind, + originalItem: selectedItem, + name: selectedItem.doc.name, + }) + } + } + + const handleDelete = () => { + const selectedItem = store.get(contextMenuTargeItemAtom) + if (selectedItem) { + moveToTrash({ + kind: selectedItem.kind, + itemId: selectedItem.doc._id, + }) + } + } + + return ( + + {children} + {target && ( + + + + Rename + + + + Move to trash + + + )} + + ) +} + +export function DirectoryContentTableContent() { + const { directoryContent } = useContext(DirectoryPageContext) + const optimisticDeletedItems = useAtomValue(optimisticDeletedItemsAtom) + const setContextMenuTargetItem = useSetAtom(contextMenuTargeItemAtom) + const store = useStore() + + const handleRowContextMenu = ( + row: Row, + _event: React.MouseEvent, + ) => { + const target = store.get(contextMenuTargeItemAtom) + if (target === row.original) { + setContextMenuTargetItem(null) + } else { + selectRow(row) + setContextMenuTargetItem(row.original) + } + } + + const table = useReactTable({ + data: directoryContent || [], + columns, + getCoreRowModel: getCoreRowModel(), + enableRowSelection: true, + enableGlobalFilter: true, + globalFilterFn: (row, _columnId, _filterValue, _addMeta) => { + return !optimisticDeletedItems.has(row.original.doc._id) + }, + getRowId: (row) => row.doc._id, + }) + + const selectRow = (row: Row) => { + console.log("row.getIsSelected()", row.getIsSelected()) + if (!row.getIsSelected()) { + table.toggleAllPageRowsSelected(false) + row.toggleSelected(true) + } + } + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + { + selectRow(row) + }} + onContextMenu={(e) => { + handleRowContextMenu(row, e) + }} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + )} + + +
+
+ ) +} + +function NoResultsRow() { + const newItemKind = useAtomValue(newItemKindAtom) + if (newItemKind) { + return null + } + return ( + + + No results. + + + ) +} + +function NewItemRow() { + const { directory } = useContext(DirectoryPageContext) + const inputRef = useRef(null) + const newItemFormId = useId() + const [newItemKind, setNewItemKind] = useAtom(newItemKindAtom) + const { mutate: createDirectory, isPending } = useMutation({ + mutationFn: useContextMutation(api.files.createDirectory), + onSuccess: () => { + setNewItemKind(null) + }, + onError: withDefaultOnError(() => { + setTimeout(() => { + inputRef.current?.focus() + }, 1) + }), + }) + + useEffect(() => { + if (newItemKind) { + setTimeout(() => { + inputRef.current?.focus() + }, 1) + } + }, [newItemKind]) + + if (!newItemKind) { + return null + } + + const onSubmit = (event: React.FormEvent) => { + event.preventDefault() + + const formData = new FormData(event.currentTarget) + const itemName = formData.get("itemName") as string + + if (itemName) { + createDirectory({ name: itemName, directoryId: directory._id }) + } else { + toast.error("Please enter a name.") + } + } + + const clearNewItemKind = () => { + // setItemBeingAdded(null) + setNewItemKind(null) + } + + return ( + + + +
+ {isPending ? ( + + ) : ( + + )} +
+ +
+
+
+ + + {!isPending ? ( + <> + + + + ) : null} + +
+ ) +} + +function DirectoryNameCell({ directory }: { directory: Doc<"directories"> }) { + return ( +
+ + + {directory.name} + +
+ ) +} + +function FileNameCell({ initialName }: { initialName: string }) { + return ( +
+ + {initialName} +
+ ) +} diff --git a/packages/web/src/directories/directory-page/directory-page-skeleton.tsx b/packages/web/src/directories/directory-page/directory-page-skeleton.tsx new file mode 100644 index 0000000..367147b --- /dev/null +++ b/packages/web/src/directories/directory-page/directory-page-skeleton.tsx @@ -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 ( + <> +
+ +
+ + +
+
+
+ +
+ + ) +} + +function BreadcrumbSkeleton() { + return ( + + + + + + + + ) +} diff --git a/packages/web/src/directories/directory-page/directory-page.tsx b/packages/web/src/directories/directory-page/directory-page.tsx new file mode 100644 index 0000000..e74074a --- /dev/null +++ b/packages/web/src/directories/directory-page/directory-page.tsx @@ -0,0 +1,181 @@ +import { api } from "@fileone/convex/_generated/api" +import { baseName, splitPath } from "@fileone/path" +import { useMutation } from "@tanstack/react-query" +import { Link } from "@tanstack/react-router" +import { useMutation as useConvexMutation } from "convex/react" +import { useSetAtom } from "jotai" +import { + ChevronDownIcon, + Loader2Icon, + PlusIcon, + UploadCloudIcon, +} from "lucide-react" +import { type ChangeEvent, Fragment, useContext, useRef } from "react" +import { toast } from "sonner" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { DirectoryIcon } from "../../components/icons/directory-icon" +import { TextFileIcon } from "../../components/icons/text-file-icon" +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "../../components/ui/breadcrumb" +import { Button } from "../../components/ui/button" +import { DirectoryPageContext } from "./context" +import { DirectoryContentTable } from "./directory-content-table" +import { RenameFileDialog } from "./rename-file-dialog" +import { newItemKindAtom } from "./state" + +export function DirectoryPage() { + const { directory } = useContext(DirectoryPageContext) + return ( + <> +
+ +
+ + +
+
+
+ +
+ + + ) +} + +function FilePathBreadcrumb({ path }: { path: string }) { + const pathComponents = splitPath(path) + const base = baseName(path) + return ( + + + + + All Files + + + {pathComponents.map((p) => ( + + + {p === base ? ( + {p} + ) : ( + + + {p} + + + )} + + ))} + + + ) +} + +// tags: upload, uploadfile, uploadfilebutton, fileupload, fileuploadbutton +function UploadFileButton() { + const generateUploadUrl = useConvexMutation(api.files.generateUploadUrl) + const saveFile = useConvexMutation(api.files.saveFile) + const { mutate: uploadFile, isPending: isUploading } = useMutation({ + mutationFn: async (file: File) => { + const uploadUrl = await generateUploadUrl() + const uploadResult = await fetch(uploadUrl, { + method: "POST", + body: file, + headers: { + "Content-Type": file.type, + }, + }) + const { storageId } = await uploadResult.json() + + await saveFile({ + storageId, + name: file.name, + size: file.size, + mimeType: file.type, + }) + }, + onSuccess: () => { + toast.success("File uploaded successfully.") + }, + }) + + const fileInputRef = useRef(null) + + const handleClick = () => { + fileInputRef.current?.click() + } + + const onFileUpload = async (e: ChangeEvent) => { + const file = e.target.files?.[0] + if (file) { + uploadFile(file) + } + } + + return ( + <> + + + + ) +} + +function NewDirectoryItemDropdown() { + const setNewItemKind = useSetAtom(newItemKindAtom) + + const addNewDirectory = () => { + setNewItemKind("directory") + } + + return ( + + + + + + + + Text file + + + + Directory + + + + ) +} diff --git a/packages/web/src/directories/directory-page/rename-file-dialog.tsx b/packages/web/src/directories/directory-page/rename-file-dialog.tsx new file mode 100644 index 0000000..83c3a88 --- /dev/null +++ b/packages/web/src/directories/directory-page/rename-file-dialog.tsx @@ -0,0 +1,111 @@ +import { api } from "@fileone/convex/_generated/api" +import { useMutation } from "@tanstack/react-query" +import { useMutation as useContextMutation } from "convex/react" +import { atom, useAtom, useStore } from "jotai" +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" +import { itemBeingRenamedAtom } from "./state" + +const fielNameAtom = atom( + (get) => get(itemBeingRenamedAtom)?.name, + (get, set, newName: string) => { + const current = get(itemBeingRenamedAtom) + if (current) { + set(itemBeingRenamedAtom, { + ...current, + name: newName, + }) + } + }, +) + +export function RenameFileDialog() { + const [itemBeingRenamed, setItemBeingRenamed] = + useAtom(itemBeingRenamedAtom) + const store = useStore() + const formId = useId() + + const { mutate: renameFile, isPending: isRenaming } = useMutation({ + mutationFn: useContextMutation(api.files.renameFile), + onSuccess: () => { + setItemBeingRenamed(null) + toast.success("File renamed successfully") + }, + }) + + const onSubmit = (event: React.FormEvent) => { + event.preventDefault() + + const itemBeingRenamed = store.get(itemBeingRenamedAtom) + if (itemBeingRenamed) { + const formData = new FormData(event.currentTarget) + const newName = formData.get("itemName") as string + + if (newName) { + switch (itemBeingRenamed.originalItem.kind) { + case "file": + renameFile({ + directoryId: + itemBeingRenamed.originalItem.doc.directoryId, + itemId: itemBeingRenamed.originalItem.doc._id, + newName, + }) + break + default: + break + } + } + } + } + + return ( + + setItemBeingRenamed(open ? itemBeingRenamed : null) + } + > + + + Rename File + + +
+ + + + + + + + + +
+
+ ) +} + +function RenameFileInput() { + const [fileName, setFileName] = useAtom(fielNameAtom) + return ( + setFileName(e.target.value)} + /> + ) +} diff --git a/packages/web/src/directories/directory-page/skeleton-demo.tsx b/packages/web/src/directories/directory-page/skeleton-demo.tsx new file mode 100644 index 0000000..a2e85cf --- /dev/null +++ b/packages/web/src/directories/directory-page/skeleton-demo.tsx @@ -0,0 +1,42 @@ +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 ( +
+
+ + +
+ + {showPageSkeleton && ( +
+

Directory Page Skeleton

+ +
+ )} + + {showTableSkeleton && ( +
+

Directory Content Table Skeleton

+ +
+ )} +
+ ) +} \ No newline at end of file diff --git a/packages/web/src/directories/directory-page/state.ts b/packages/web/src/directories/directory-page/state.ts new file mode 100644 index 0000000..68720c4 --- /dev/null +++ b/packages/web/src/directories/directory-page/state.ts @@ -0,0 +1,22 @@ +import type { Id } from "@fileone/convex/_generated/dataModel" +import type { + DirectoryItem, + DirectoryItemKind, +} from "@fileone/convex/model/directories" +import type { RowSelectionState } from "@tanstack/react-table" +import { atom } from "jotai" + +export const contextMenuTargeItemAtom = atom(null) +export const optimisticDeletedItemsAtom = atom( + new Set | Id<"directories">>(), +) + +export const selectedFileRowsAtom = atom({}) + +export const newItemKindAtom = atom(null) + +export const itemBeingRenamedAtom = atom<{ + kind: DirectoryItemKind + originalItem: DirectoryItem + name: string +} | null>(null) diff --git a/packages/web/src/routeTree.gen.ts b/packages/web/src/routeTree.gen.ts index 2edaade..8fe6e55 100644 --- a/packages/web/src/routeTree.gen.ts +++ b/packages/web/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as AuthenticatedIndexRouteImport } from './routes/_authenticated/ import { Route as LoginCallbackRouteImport } from './routes/login_.callback' import { Route as AuthenticatedSidebarLayoutRouteImport } from './routes/_authenticated/_sidebar-layout' import { Route as AuthenticatedSidebarLayoutFilesSplatRouteImport } from './routes/_authenticated/_sidebar-layout/files.$' +import { Route as AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport } from './routes/_authenticated/_sidebar-layout/directories.$directoryId' const LoginRoute = LoginRouteImport.update({ id: '/login', @@ -46,17 +47,25 @@ const AuthenticatedSidebarLayoutFilesSplatRoute = path: '/files/$', getParentRoute: () => AuthenticatedSidebarLayoutRoute, } as any) +const AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute = + AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport.update({ + id: '/directories/$directoryId', + path: '/directories/$directoryId', + getParentRoute: () => AuthenticatedSidebarLayoutRoute, + } as any) export interface FileRoutesByFullPath { '/login': typeof LoginRoute '/login/callback': typeof LoginCallbackRoute '/': typeof AuthenticatedIndexRoute + '/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute '/files/$': typeof AuthenticatedSidebarLayoutFilesSplatRoute } export interface FileRoutesByTo { '/login': typeof LoginRoute '/login/callback': typeof LoginCallbackRoute '/': typeof AuthenticatedIndexRoute + '/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute '/files/$': typeof AuthenticatedSidebarLayoutFilesSplatRoute } export interface FileRoutesById { @@ -66,13 +75,24 @@ export interface FileRoutesById { '/_authenticated/_sidebar-layout': typeof AuthenticatedSidebarLayoutRouteWithChildren '/login_/callback': typeof LoginCallbackRoute '/_authenticated/': typeof AuthenticatedIndexRoute + '/_authenticated/_sidebar-layout/directories/$directoryId': typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute '/_authenticated/_sidebar-layout/files/$': typeof AuthenticatedSidebarLayoutFilesSplatRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/login' | '/login/callback' | '/' | '/files/$' + fullPaths: + | '/login' + | '/login/callback' + | '/' + | '/directories/$directoryId' + | '/files/$' fileRoutesByTo: FileRoutesByTo - to: '/login' | '/login/callback' | '/' | '/files/$' + to: + | '/login' + | '/login/callback' + | '/' + | '/directories/$directoryId' + | '/files/$' id: | '__root__' | '/_authenticated' @@ -80,6 +100,7 @@ export interface FileRouteTypes { | '/_authenticated/_sidebar-layout' | '/login_/callback' | '/_authenticated/' + | '/_authenticated/_sidebar-layout/directories/$directoryId' | '/_authenticated/_sidebar-layout/files/$' fileRoutesById: FileRoutesById } @@ -133,15 +154,25 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedSidebarLayoutFilesSplatRouteImport parentRoute: typeof AuthenticatedSidebarLayoutRoute } + '/_authenticated/_sidebar-layout/directories/$directoryId': { + id: '/_authenticated/_sidebar-layout/directories/$directoryId' + path: '/directories/$directoryId' + fullPath: '/directories/$directoryId' + preLoaderRoute: typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRouteImport + parentRoute: typeof AuthenticatedSidebarLayoutRoute + } } } interface AuthenticatedSidebarLayoutRouteChildren { + AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute: typeof AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute AuthenticatedSidebarLayoutFilesSplatRoute: typeof AuthenticatedSidebarLayoutFilesSplatRoute } const AuthenticatedSidebarLayoutRouteChildren: AuthenticatedSidebarLayoutRouteChildren = { + AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute: + AuthenticatedSidebarLayoutDirectoriesDirectoryIdRoute, AuthenticatedSidebarLayoutFilesSplatRoute: AuthenticatedSidebarLayoutFilesSplatRoute, } diff --git a/packages/web/src/routes/_authenticated/_sidebar-layout.tsx b/packages/web/src/routes/_authenticated/_sidebar-layout.tsx index 73159bb..a16a9a6 100644 --- a/packages/web/src/routes/_authenticated/_sidebar-layout.tsx +++ b/packages/web/src/routes/_authenticated/_sidebar-layout.tsx @@ -1,4 +1,5 @@ import { createFileRoute, Outlet } from "@tanstack/react-router" +import { useQuery as useConvexQuery } from "convex/react" import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar" import { Toaster } from "@/components/ui/sonner" import { DashboardSidebar } from "@/dashboard/dashboard-sidebar" diff --git a/packages/web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx b/packages/web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx new file mode 100644 index 0000000..5efc3c0 --- /dev/null +++ b/packages/web/src/routes/_authenticated/_sidebar-layout/directories.$directoryId.tsx @@ -0,0 +1,32 @@ +import { api } from "@fileone/convex/_generated/api" +import { createFileRoute } from "@tanstack/react-router" +import { useQuery as useConvexQuery } from "convex/react" +import { DirectoryPageContext } from "@/directories/directory-page/context" +import { DirectoryPage } from "@/directories/directory-page/directory-page" +import { DirectoryPageSkeleton } from "@/directories/directory-page/directory-page-skeleton" + +export const Route = createFileRoute( + "/_authenticated/_sidebar-layout/directories/$directoryId", +)({ + component: RouteComponent, +}) + +function RouteComponent() { + const { directoryId } = Route.useParams() + const directory = useConvexQuery(api.files.fetchDirectory, { + directoryId, + }) + const directoryContent = useConvexQuery(api.files.fetchDirectoryContent, { + directoryId, + }) + + if (!directory || !directoryContent) { + return + } + + return ( + + + + ) +}