Files
file-one/packages/web/src/files/files-page.tsx

178 lines
4.2 KiB
TypeScript
Raw Normal View History

2025-09-16 23:17:01 +00:00
import { api } from "@fileone/convex/_generated/api"
2025-09-17 22:47:55 +00:00
import { baseName, splitPath } from "@fileone/path"
2025-09-15 23:31:03 +00:00
import { useMutation } from "@tanstack/react-query"
2025-09-17 22:47:55 +00:00
import { Link } from "@tanstack/react-router"
2025-09-15 23:31:03 +00:00
import { useMutation as useConvexMutation } from "convex/react"
import { useSetAtom } from "jotai"
import {
ChevronDownIcon,
Loader2Icon,
PlusIcon,
UploadCloudIcon,
} from "lucide-react"
2025-09-17 00:04:12 +00:00
import { type ChangeEvent, Fragment, 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,
2025-09-17 22:47:55 +00:00
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
2025-09-16 22:16:53 +00:00
BreadcrumbSeparator,
} from "../components/ui/breadcrumb"
import { Button } from "../components/ui/button"
import { FileTable } from "./file-table"
import { newItemKindAtom } from "./state"
2025-09-17 00:04:12 +00:00
export function FilesPage({ path }: { path: string }) {
return (
<>
2025-09-17 00:04:12 +00:00
<header className="flex py-1 shrink-0 items-center gap-2 border-b px-4 w-full">
2025-09-17 22:47:55 +00:00
<FilePathBreadcrumb path={path} />
<div className="ml-auto flex flex-row gap-2">
<NewDirectoryItemDropdown />
<UploadFileButton />
</div>
</header>
<div className="w-full">
2025-09-17 00:04:12 +00:00
<FileTable path={path} />
</div>
</>
)
}
2025-09-17 22:47:55 +00:00
function FilePathBreadcrumb({ path }: { path: string }) {
const pathComponents = splitPath(path)
const base = baseName(path)
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to="/files">All Files</Link>
</BreadcrumbLink>
</BreadcrumbItem>
{pathComponents.map((p) => (
<Fragment key={p}>
<BreadcrumbSeparator />
{p === base ? (
<BreadcrumbPage>{p}</BreadcrumbPage>
) : (
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link to={`/files/${p}`}>{p}</Link>
</BreadcrumbLink>
</BreadcrumbItem>
)}
</Fragment>
))}
</BreadcrumbList>
</Breadcrumb>
)
}
2025-09-14 18:49:28 +00:00
// tags: upload, uploadfile, uploadfilebutton, fileupload, fileuploadbutton
function UploadFileButton() {
const generateUploadUrl = useConvexMutation(api.files.generateUploadUrl)
const saveFile = useConvexMutation(api.files.saveFile)
2025-09-15 23:31:03 +00:00
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<HTMLInputElement>(null)
const handleClick = () => {
fileInputRef.current?.click()
}
const onFileUpload = async (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
2025-09-15 23:31:03 +00:00
uploadFile(file)
}
}
return (
<>
<input
hidden
onChange={onFileUpload}
ref={fileInputRef}
type="file"
name="files"
/>
<Button
size="sm"
type="button"
onClick={handleClick}
disabled={isUploading}
>
{isUploading ? (
<Loader2Icon className="animate-spin size-4" />
) : (
<UploadCloudIcon className="size-4" />
)}
{isUploading ? "Uploading" : "Upload File"}
</Button>
</>
)
}
function NewDirectoryItemDropdown() {
const setNewItemKind = useSetAtom(newItemKindAtom)
const addNewDirectory = () => {
setNewItemKind("directory")
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" type="button" variant="outline">
<PlusIcon className="size-4" />
New
<ChevronDownIcon className="pl-1 size-4 shrink-0" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<TextFileIcon />
Text file
</DropdownMenuItem>
<DropdownMenuItem onClick={addNewDirectory}>
<DirectoryIcon />
Directory
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}