feat: impl image preview dialog
This commit is contained in:
@@ -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({
|
export const fetchFiles = authenticatedQuery({
|
||||||
args: {
|
args: {
|
||||||
directoryId: v.optional(v.id("directories")),
|
directoryId: v.optional(v.id("directories")),
|
||||||
|
195
packages/web/src/components/image-preview-dialog.tsx
Normal file
195
packages/web/src/components/image-preview-dialog.tsx
Normal file
@@ -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 (
|
||||||
|
<Dialog
|
||||||
|
open
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogOverlay className="flex items-center justify-center">
|
||||||
|
{!fileUrl ? (
|
||||||
|
<LoadingSpinner className="text-neutral-200 size-10" />
|
||||||
|
) : null}
|
||||||
|
</DialogOverlay>
|
||||||
|
{fileUrl ? <PreviewContent fileUrl={fileUrl} file={file} /> : null}
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PreviewContent({
|
||||||
|
fileUrl,
|
||||||
|
file,
|
||||||
|
}: {
|
||||||
|
fileUrl: string
|
||||||
|
file: Doc<"files">
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogContent
|
||||||
|
showCloseButton={false}
|
||||||
|
className="p-0 lg:min-w-1/3 gap-0"
|
||||||
|
>
|
||||||
|
<DialogHeader className="border-b border-b-border p-4 flex flex-row items-center justify-between">
|
||||||
|
<DialogTitle>{file.name}</DialogTitle>
|
||||||
|
<div className="flex flex-row items-center space-x-2">
|
||||||
|
<Toolbar />
|
||||||
|
<Button variant="ghost" size="icon" asChild>
|
||||||
|
<DialogClose>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogClose>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="w-full h-full flex items-center justify-center max-h-[calc(100vh-10rem)] overflow-auto">
|
||||||
|
<ImagePreview fileUrl={fileUrl} file={file} />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Toolbar() {
|
||||||
|
const setZoomLevel = useSetAtom(zoomLevelAtom)
|
||||||
|
const zoomInterval = useRef<ReturnType<typeof setInterval> | 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 (
|
||||||
|
<div className="flex flex-row items-center space-x-2 border-r border-r-border pr-2">
|
||||||
|
<ResetZoomButton />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onMouseDown={() => {
|
||||||
|
startZooming(0.1)
|
||||||
|
}}
|
||||||
|
onMouseLeave={stopZooming}
|
||||||
|
onMouseUp={stopZooming}
|
||||||
|
>
|
||||||
|
<ZoomInIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onMouseDown={() => {
|
||||||
|
startZooming(-0.1)
|
||||||
|
}}
|
||||||
|
onMouseLeave={stopZooming}
|
||||||
|
onMouseUp={stopZooming}
|
||||||
|
>
|
||||||
|
<ZoomOutIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResetZoomButton() {
|
||||||
|
const [zoomLevel, setZoomLevel] = useAtom(zoomLevelAtom)
|
||||||
|
|
||||||
|
if (zoomLevel === 1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
setZoomLevel(1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{zoomLevel > 1 ? <Minimize2Icon /> : <Maximize2Icon />}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImagePreview({
|
||||||
|
fileUrl,
|
||||||
|
file,
|
||||||
|
}: {
|
||||||
|
fileUrl: string
|
||||||
|
file: Doc<"files">
|
||||||
|
}) {
|
||||||
|
const zoomLevel = useAtomValue(zoomLevelAtom)
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={fileUrl}
|
||||||
|
alt={file.name}
|
||||||
|
style={{ transform: `scale(${zoomLevel})` }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
@@ -1,141 +1,144 @@
|
|||||||
import * as React from "react"
|
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
import { XIcon } from "lucide-react"
|
import { XIcon } from "lucide-react"
|
||||||
|
import type * as React from "react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTrigger({
|
function DialogTrigger({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogPortal({
|
function DialogPortal({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogClose({
|
function DialogClose({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogOverlay({
|
function DialogOverlay({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50 backdrop-blur-xs",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogContent({
|
function DialogContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close
|
<DialogPrimitive.Close
|
||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogPrimitive.Close>
|
||||||
)}
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogPrimitive.Content>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-header"
|
data-slot="dialog-header"
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
className={cn(
|
||||||
{...props}
|
"flex flex-col gap-2 text-center sm:text-left",
|
||||||
/>
|
className,
|
||||||
)
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTitle({
|
function DialogTitle({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogDescription({
|
function DialogDescription({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
className={cn("text-muted-foreground text-sm", className)}
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogOverlay,
|
DialogOverlay,
|
||||||
DialogPortal,
|
DialogPortal,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ import type { DirectoryItem } from "@fileone/convex/model/directories"
|
|||||||
import { createContext } from "react"
|
import { createContext } from "react"
|
||||||
|
|
||||||
type DirectoryPageContextType = {
|
type DirectoryPageContextType = {
|
||||||
|
rootDirectory: Doc<"directories">
|
||||||
directory: Doc<"directories">
|
directory: Doc<"directories">
|
||||||
directoryContent: DirectoryItem[]
|
directoryContent: DirectoryItem[]
|
||||||
}
|
}
|
||||||
|
@@ -41,6 +41,7 @@ import {
|
|||||||
contextMenuTargeItemAtom,
|
contextMenuTargeItemAtom,
|
||||||
itemBeingRenamedAtom,
|
itemBeingRenamedAtom,
|
||||||
newItemKindAtom,
|
newItemKindAtom,
|
||||||
|
openedFileAtom,
|
||||||
optimisticDeletedItemsAtom,
|
optimisticDeletedItemsAtom,
|
||||||
} from "./state"
|
} from "./state"
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ const columns: ColumnDef<DirectoryItem>[] = [
|
|||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
switch (row.original.kind) {
|
switch (row.original.kind) {
|
||||||
case "file":
|
case "file":
|
||||||
return <FileNameCell initialName={row.original.doc.name} />
|
return <FileNameCell file={row.original.doc} />
|
||||||
case "directory":
|
case "directory":
|
||||||
return <DirectoryNameCell directory={row.original.doc} />
|
return <DirectoryNameCell directory={row.original.doc} />
|
||||||
}
|
}
|
||||||
@@ -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 (
|
return (
|
||||||
<div className="flex w-full items-center gap-2">
|
<div className="flex w-full items-center gap-2">
|
||||||
<TextFileIcon className="size-4" />
|
<TextFileIcon className="size-4" />
|
||||||
{initialName}
|
<button
|
||||||
|
type="button"
|
||||||
|
className="hover:underline cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setOpenedFile(file)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{file.name}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ import { baseName, splitPath } from "@fileone/path"
|
|||||||
import { useMutation } from "@tanstack/react-query"
|
import { useMutation } from "@tanstack/react-query"
|
||||||
import { Link } from "@tanstack/react-router"
|
import { Link } from "@tanstack/react-router"
|
||||||
import { useMutation as useConvexMutation } from "convex/react"
|
import { useMutation as useConvexMutation } from "convex/react"
|
||||||
import { useSetAtom } from "jotai"
|
import { useAtom, useSetAtom } from "jotai"
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
Loader2Icon,
|
Loader2Icon,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { type ChangeEvent, Fragment, useContext, useRef } from "react"
|
import { type ChangeEvent, Fragment, useContext, useRef } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
import { ImagePreviewDialog } from "@/components/image-preview-dialog"
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -32,7 +33,7 @@ import { Button } from "../../components/ui/button"
|
|||||||
import { DirectoryPageContext } from "./context"
|
import { DirectoryPageContext } from "./context"
|
||||||
import { DirectoryContentTable } from "./directory-content-table"
|
import { DirectoryContentTable } from "./directory-content-table"
|
||||||
import { RenameFileDialog } from "./rename-file-dialog"
|
import { RenameFileDialog } from "./rename-file-dialog"
|
||||||
import { newItemKindAtom } from "./state"
|
import { newItemKindAtom, openedFileAtom } from "./state"
|
||||||
|
|
||||||
export function DirectoryPage() {
|
export function DirectoryPage() {
|
||||||
const { directory } = useContext(DirectoryPageContext)
|
const { directory } = useContext(DirectoryPageContext)
|
||||||
@@ -49,6 +50,7 @@ export function DirectoryPage() {
|
|||||||
<DirectoryContentTable />
|
<DirectoryContentTable />
|
||||||
</div>
|
</div>
|
||||||
<RenameFileDialog />
|
<RenameFileDialog />
|
||||||
|
<PreviewDialog />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -184,3 +186,25 @@ function NewDirectoryItemDropdown() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<ImagePreviewDialog
|
||||||
|
file={openedFile}
|
||||||
|
onClose={() => setOpenedFile(null)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import type { Id } from "@fileone/convex/_generated/dataModel"
|
import type { Doc, Id } from "@fileone/convex/_generated/dataModel"
|
||||||
import type {
|
import type {
|
||||||
DirectoryItem,
|
DirectoryItem,
|
||||||
DirectoryItemKind,
|
DirectoryItemKind,
|
||||||
@@ -20,3 +20,5 @@ export const itemBeingRenamedAtom = atom<{
|
|||||||
originalItem: DirectoryItem
|
originalItem: DirectoryItem
|
||||||
name: string
|
name: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
|
export const openedFileAtom = atom<Doc<"files"> | null>(null)
|
||||||
|
Reference in New Issue
Block a user