diff --git a/packages/convex/files.ts b/packages/convex/files.ts index b6ad573..18c8022 100644 --- a/packages/convex/files.ts +++ b/packages/convex/files.ts @@ -12,6 +12,15 @@ export const generateUploadUrl = authenticatedMutation({ }, }) +export const generateFileUrl = authenticatedQuery({ + args: { + storageId: v.id("_storage"), + }, + handler: async (ctx, { storageId }) => { + return await ctx.storage.getUrl(storageId) + }, +}) + export const fetchFiles = authenticatedQuery({ args: { directoryId: v.optional(v.id("directories")), diff --git a/packages/web/src/components/image-preview-dialog.tsx b/packages/web/src/components/image-preview-dialog.tsx new file mode 100644 index 0000000..00868fa --- /dev/null +++ b/packages/web/src/components/image-preview-dialog.tsx @@ -0,0 +1,195 @@ +import { api } from "@fileone/convex/_generated/api" +import type { Doc } from "@fileone/convex/_generated/dataModel" +import { DialogTitle } from "@radix-ui/react-dialog" +import { useQuery as useConvexQuery } from "convex/react" +import { atom, useAtom, useAtomValue, useSetAtom } from "jotai" +import { + Maximize2Icon, + Minimize2Icon, + XIcon, + ZoomInIcon, + ZoomOutIcon, +} from "lucide-react" +import { useEffect, useRef } from "react" +import { Button } from "./ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogHeader, + DialogOverlay, +} from "./ui/dialog" +import { LoadingSpinner } from "./ui/loading-spinner" + +const zoomLevelAtom = atom( + 1, + (get, set, update: number | ((current: number) => number)) => { + const current = get(zoomLevelAtom) + console.log("current", current) + const newValue = typeof update === "function" ? update(current) : update + if (newValue >= 0.1) { + set(zoomLevelAtom, newValue) + } + }, +) + +export function ImagePreviewDialog({ + file, + onClose, +}: { + file: Doc<"files"> + onClose: () => void +}) { + const fileUrl = useConvexQuery(api.files.generateFileUrl, { + storageId: file.storageId, + }) + const setZoomLevel = useSetAtom(zoomLevelAtom) + + useEffect( + () => () => { + setZoomLevel(1) + }, + [setZoomLevel], + ) + + return ( + { + if (!open) { + onClose() + } + }} + > + + {!fileUrl ? ( + + ) : null} + + {fileUrl ? : null} + + ) +} + +function PreviewContent({ + fileUrl, + file, +}: { + fileUrl: string + file: Doc<"files"> +}) { + return ( + + + {file.name} +
+ + +
+
+
+ +
+
+ ) +} + +function Toolbar() { + const setZoomLevel = useSetAtom(zoomLevelAtom) + const zoomInterval = useRef | null>(null) + + useEffect( + () => () => { + if (zoomInterval.current) { + clearInterval(zoomInterval.current) + console.log("clearInterval") + zoomInterval.current = null + } + }, + [], + ) + + function startZooming(delta: number) { + setZoomLevel((zoom) => zoom + delta) + zoomInterval.current = setInterval(() => { + setZoomLevel((zoom) => zoom + delta) + }, 100) + } + + function stopZooming() { + if (zoomInterval.current) { + clearInterval(zoomInterval.current) + zoomInterval.current = null + } + } + + return ( +
+ + + +
+ ) +} + +function ResetZoomButton() { + const [zoomLevel, setZoomLevel] = useAtom(zoomLevelAtom) + + if (zoomLevel === 1) { + return null + } + + return ( + + ) +} + +function ImagePreview({ + fileUrl, + file, +}: { + fileUrl: string + file: Doc<"files"> +}) { + const zoomLevel = useAtomValue(zoomLevelAtom) + return ( + {file.name} + ) +} diff --git a/packages/web/src/components/ui/dialog.tsx b/packages/web/src/components/ui/dialog.tsx index 6cb123b..ba114f5 100644 --- a/packages/web/src/components/ui/dialog.tsx +++ b/packages/web/src/components/ui/dialog.tsx @@ -1,141 +1,144 @@ -import * as React from "react" import * as DialogPrimitive from "@radix-ui/react-dialog" import { XIcon } from "lucide-react" +import type * as React from "react" import { cn } from "@/lib/utils" function Dialog({ - ...props + ...props }: React.ComponentProps) { - return + return } function DialogTrigger({ - ...props + ...props }: React.ComponentProps) { - return + return } function DialogPortal({ - ...props + ...props }: React.ComponentProps) { - return + return } function DialogClose({ - ...props + ...props }: React.ComponentProps) { - return + return } function DialogOverlay({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ) } function DialogContent({ - className, - children, - showCloseButton = true, - ...props + className, + children, + showCloseButton = true, + ...props }: React.ComponentProps & { - showCloseButton?: boolean + showCloseButton?: boolean }) { - return ( - - - - {children} - {showCloseButton && ( - - - Close - - )} - - - ) + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) } function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ) } function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) + return ( +
+ ) } function DialogTitle({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ) } function DialogDescription({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ) } export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, } diff --git a/packages/web/src/directories/directory-page/context.ts b/packages/web/src/directories/directory-page/context.ts index 86edb1e..2a65743 100644 --- a/packages/web/src/directories/directory-page/context.ts +++ b/packages/web/src/directories/directory-page/context.ts @@ -3,6 +3,7 @@ import type { DirectoryItem } from "@fileone/convex/model/directories" import { createContext } from "react" type DirectoryPageContextType = { + rootDirectory: Doc<"directories"> directory: Doc<"directories"> directoryContent: DirectoryItem[] } diff --git a/packages/web/src/directories/directory-page/directory-content-table.tsx b/packages/web/src/directories/directory-page/directory-content-table.tsx index 1dfe4c4..e6554aa 100644 --- a/packages/web/src/directories/directory-page/directory-content-table.tsx +++ b/packages/web/src/directories/directory-page/directory-content-table.tsx @@ -41,6 +41,7 @@ import { contextMenuTargeItemAtom, itemBeingRenamedAtom, newItemKindAtom, + openedFileAtom, optimisticDeletedItemsAtom, } from "./state" @@ -83,7 +84,7 @@ const columns: ColumnDef[] = [ cell: ({ row }) => { switch (row.original.kind) { case "file": - return + return case "directory": return } @@ -413,11 +414,20 @@ function DirectoryNameCell({ directory }: { directory: Doc<"directories"> }) { ) } -function FileNameCell({ initialName }: { initialName: string }) { +function FileNameCell({ file }: { file: Doc<"files"> }) { + const setOpenedFile = useSetAtom(openedFileAtom) return (
- {initialName} +
) } diff --git a/packages/web/src/directories/directory-page/directory-page.tsx b/packages/web/src/directories/directory-page/directory-page.tsx index 446b72a..fa2a114 100644 --- a/packages/web/src/directories/directory-page/directory-page.tsx +++ b/packages/web/src/directories/directory-page/directory-page.tsx @@ -3,7 +3,7 @@ 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 { useAtom, useSetAtom } from "jotai" import { ChevronDownIcon, Loader2Icon, @@ -12,6 +12,7 @@ import { } from "lucide-react" import { type ChangeEvent, Fragment, useContext, useRef } from "react" import { toast } from "sonner" +import { ImagePreviewDialog } from "@/components/image-preview-dialog" import { DropdownMenu, DropdownMenuContent, @@ -32,7 +33,7 @@ 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" +import { newItemKindAtom, openedFileAtom } from "./state" export function DirectoryPage() { const { directory } = useContext(DirectoryPageContext) @@ -49,6 +50,7 @@ export function DirectoryPage() {
+ ) } @@ -184,3 +186,25 @@ function NewDirectoryItemDropdown() { ) } + +function PreviewDialog() { + const [openedFile, setOpenedFile] = useAtom(openedFileAtom) + + if (!openedFile) return null + + console.log("openedFile", openedFile) + + switch (openedFile.mimeType) { + case "image/jpeg": + case "image/png": + case "image/gif": + return ( + setOpenedFile(null)} + /> + ) + default: + return null + } +} diff --git a/packages/web/src/directories/directory-page/state.ts b/packages/web/src/directories/directory-page/state.ts index 68720c4..dfde8ed 100644 --- a/packages/web/src/directories/directory-page/state.ts +++ b/packages/web/src/directories/directory-page/state.ts @@ -1,4 +1,4 @@ -import type { Id } from "@fileone/convex/_generated/dataModel" +import type { Doc, Id } from "@fileone/convex/_generated/dataModel" import type { DirectoryItem, DirectoryItemKind, @@ -20,3 +20,5 @@ export const itemBeingRenamedAtom = atom<{ originalItem: DirectoryItem name: string } | null>(null) + +export const openedFileAtom = atom | null>(null)