diff --git a/apps/web/src/app/api/(proxy)/shares/create-with-files/route.ts b/apps/web/src/app/api/(proxy)/shares/create-with-files/route.ts new file mode 100644 index 0000000..6e7a4f8 --- /dev/null +++ b/apps/web/src/app/api/(proxy)/shares/create-with-files/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from "next/server"; + +const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:3333"; + +export async function POST(req: NextRequest) { + const cookieHeader = req.headers.get("cookie"); + + // Get the multipart form data directly + const formData = await req.formData(); + + const url = `${API_BASE_URL}/shares/create-with-files`; + + const apiRes = await fetch(url, { + method: "POST", + headers: { + cookie: cookieHeader || "", + // Don't set Content-Type, let fetch set it with boundary + }, + body: formData, + redirect: "manual", + }); + + const resBody = await apiRes.text(); + + const res = new NextResponse(resBody, { + status: apiRes.status, + headers: { + "Content-Type": "application/json", + }, + }); + + const setCookie = apiRes.headers.getSetCookie?.() || []; + if (setCookie.length > 0) { + res.headers.set("Set-Cookie", setCookie.join(",")); + } + + return res; +} diff --git a/apps/web/src/components/modals/create-share-modal.tsx b/apps/web/src/components/modals/create-share-modal.tsx index 0bca9cf..a0f43d4 100644 --- a/apps/web/src/components/modals/create-share-modal.tsx +++ b/apps/web/src/components/modals/create-share-modal.tsx @@ -1,8 +1,9 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { IconCalendar, IconEye, IconLock, IconShare } from "@tabler/icons-react"; +import { IconCalendar, IconEye, IconLock, IconShare, IconUpload, IconX } from "@tabler/icons-react"; import { useTranslations } from "next-intl"; +import { useDropzone } from "react-dropzone"; import { toast } from "sonner"; import { FileTree, TreeFile, TreeFolder } from "@/components/tables/files-tree"; @@ -13,7 +14,8 @@ import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; -import { createShare } from "@/http/endpoints"; +import { createShare, createShareWithFiles } from "@/http/endpoints"; +import { formatFileSize } from "@/utils/format-file-size"; interface CreateShareModalProps { isOpen: boolean; @@ -39,6 +41,7 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol const [files, setFiles] = useState([]); const [folders, setFolders] = useState([]); const [searchQuery, setSearchQuery] = useState(""); + const [newFiles, setNewFiles] = useState([]); const [isLoading, setIsLoading] = useState(false); const [isLoadingData, setIsLoadingData] = useState(false); @@ -85,18 +88,35 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol maxViews: "", }); setSelectedItems([]); + setNewFiles([]); setCurrentTab("details"); } }, [isOpen, loadData]); + const onDrop = useCallback((acceptedFiles: File[]) => { + setNewFiles((prev) => [...prev, ...acceptedFiles]); + }, []); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + multiple: true, + }); + + const removeNewFile = (index: number) => { + setNewFiles((prev) => prev.filter((_, i) => i !== index)); + }; + const handleSubmit = async () => { if (!formData.name.trim()) { toast.error("Share name is required"); return; } - if (selectedItems.length === 0) { - toast.error("Please select at least one file or folder"); + const hasExistingItems = selectedItems.length > 0; + const hasNewFiles = newFiles.length > 0; + + if (!hasExistingItems && !hasNewFiles) { + toast.error("Please select at least one file/folder or upload new files"); return; } @@ -106,23 +126,40 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol const selectedFiles = selectedItems.filter((id) => files.some((file) => file.id === id)); const selectedFolders = selectedItems.filter((id) => folders.some((folder) => folder.id === id)); - await createShare({ - name: formData.name, - description: formData.description || undefined, - password: formData.isPasswordProtected ? formData.password : undefined, - expiration: formData.expiresAt - ? (() => { - const dateValue = formData.expiresAt; - if (dateValue.length === 10) { - return new Date(dateValue + "T23:59:59").toISOString(); - } - return new Date(dateValue).toISOString(); - })() - : undefined, - maxViews: formData.maxViews ? parseInt(formData.maxViews) : undefined, - files: selectedFiles, - folders: selectedFolders, - }); + const expiration = formData.expiresAt + ? (() => { + const dateValue = formData.expiresAt; + if (dateValue.length === 10) { + return new Date(dateValue + "T23:59:59").toISOString(); + } + return new Date(dateValue).toISOString(); + })() + : undefined; + + // Use the new endpoint if there are new files to upload + if (hasNewFiles) { + await createShareWithFiles({ + name: formData.name, + description: formData.description || undefined, + password: formData.isPasswordProtected ? formData.password : undefined, + expiration: expiration, + maxViews: formData.maxViews ? parseInt(formData.maxViews) : undefined, + existingFiles: selectedFiles.length > 0 ? selectedFiles : undefined, + existingFolders: selectedFolders.length > 0 ? selectedFolders : undefined, + newFiles: newFiles, + }); + } else { + // Use the traditional endpoint if only selecting existing files + await createShare({ + name: formData.name, + description: formData.description || undefined, + password: formData.isPasswordProtected ? formData.password : undefined, + expiration: expiration, + maxViews: formData.maxViews ? parseInt(formData.maxViews) : undefined, + files: selectedFiles, + folders: selectedFolders, + }); + } toast.success(t("createShare.success")); onSuccess(); @@ -146,8 +183,10 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol }; const selectedCount = selectedItems.length; + const newFilesCount = newFiles.length; + const totalCount = selectedCount + newFilesCount; const canProceedToFiles = formData.name.trim().length > 0; - const canSubmit = formData.name.trim().length > 0 && selectedCount > 0; + const canSubmit = formData.name.trim().length > 0 && totalCount > 0; return ( @@ -161,7 +200,7 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol
- + {t("createShare.tabs.shareDetails")} {t("createShare.tabs.selectFiles")} @@ -171,6 +210,14 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol )} + + {t("createShare.tabs.uploadFiles") || "Upload Files"} + {newFilesCount > 0 && ( + + {newFilesCount} + + )} + @@ -320,6 +367,87 @@ export function CreateShareModal({ isOpen, onClose, onSuccess, getAllFilesAndFol +
+ + +
+
+ + + +
+
+ + + {isDragActive ? ( +

Drop files here...

+ ) : ( +
+

+ {t("createShare.upload.dragDrop") || "Drag & drop files here"} +

+

+ {t("createShare.upload.orClick") || "or click to browse"} +

+
+ )} +
+ + {newFiles.length > 0 && ( +
+ +
+ {newFiles.map((file, index) => ( +
+
+ + {file.name} + + {formatFileSize(file.size)} + +
+ +
+ ))} +
+
+ )} + +
+ {totalCount > 0 ? ( + + {totalCount} {totalCount === 1 ? "item" : "items"} selected ({selectedCount} existing,{" "} + {newFilesCount} new) + + ) : ( + No items selected + )} +
+
+ +
+