feat: add description field to File model and update related components for file details and actions
This commit is contained in:
		
							parent
							
								
									3048c00648
								
							
						
					
					
						commit
						e1097ba1be
					
				| @ -82,7 +82,7 @@ model File { | ||||
|     size       Int      // Size in bytes | ||||
|     extension  String | ||||
|     uploadDate DateTime @default(now()) | ||||
| 
 | ||||
|     description String @default("") | ||||
|     uploadedBy User?    @relation(fields: [uploadedById], references: [id], onDelete: SetNull) | ||||
|     uploadedById String? | ||||
| } | ||||
|  | ||||
							
								
								
									
										109
									
								
								src/app/_components/FileActions.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/app/_components/FileActions.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,109 @@ | ||||
| import toast from "react-hot-toast"; | ||||
| import { env } from "~/env.js"; | ||||
| 
 | ||||
| export const useFileActions = ( | ||||
|   setFiles: (callback: (prevFiles: any[]) => any[]) => void, | ||||
|   setDescription?: (description: string) => void, | ||||
|   fileId?: string | ||||
| ) => { | ||||
|   const pageUrl = `${env.NEXT_PUBLIC_PAGE_URL}/share?id=`; | ||||
| 
 | ||||
|   // Handle file download
 | ||||
|   const handleDownload = async (fileId: string, fileName: string) => { | ||||
|     try { | ||||
|       const response = await fetch( | ||||
|         `/api/files/download?fileId=${encodeURIComponent(fileId)}&fileName=${encodeURIComponent(fileName)}` | ||||
|       ); | ||||
|       if (!response.ok) throw new Error("Failed to download file"); | ||||
| 
 | ||||
|       const blob = await response.blob(); | ||||
|       const url = window.URL.createObjectURL(blob); | ||||
|       const a = document.createElement("a"); | ||||
|       a.href = url; | ||||
|       a.download = fileName; | ||||
|       document.body.appendChild(a); | ||||
|       a.click(); | ||||
|       a.remove(); | ||||
|       window.URL.revokeObjectURL(url); | ||||
| 
 | ||||
|       toast.success(`File "${fileName}" downloaded successfully!`); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       toast.error("Failed to download file."); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   // Copy file URL to clipboard
 | ||||
|   const handleCopyUrl = (url: string) => { | ||||
|     navigator.clipboard | ||||
|       .writeText(pageUrl + url) | ||||
|       .then(() => toast.success("File URL copied to clipboard!")) | ||||
|       .catch(() => toast.error("Failed to copy URL.")); | ||||
|   }; | ||||
| 
 | ||||
|   // Remove a file
 | ||||
|   const handleRemove = async (fileId: string) => { | ||||
|     try { | ||||
|       const response = await fetch(`/api/remove`, { | ||||
|         method: "DELETE", | ||||
|         headers: { "Content-Type": "application/json" }, | ||||
|         body: JSON.stringify({ id: fileId }), | ||||
|       }); | ||||
|       if (!response.ok) throw new Error("Failed to delete file"); | ||||
| 
 | ||||
|       setFiles((prevFiles) => prevFiles.filter((file) => file.id !== fileId)); | ||||
|       toast.success("File removed successfully!"); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       toast.error("Failed to remove file."); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   // Handle description change
 | ||||
|   const handleDescriptionChange = ( | ||||
|     e: React.ChangeEvent<HTMLTextAreaElement>, | ||||
|     debounceTimer: React.RefObject<NodeJS.Timeout | null> | ||||
|   ) => { | ||||
|     if (!setDescription) return; | ||||
| 
 | ||||
|     const newDescription = e.target.value; | ||||
|     setDescription(newDescription); | ||||
| 
 | ||||
|     if (debounceTimer.current) { | ||||
|       clearTimeout(debounceTimer.current); | ||||
|     } | ||||
|     debounceTimer.current = setTimeout(() => { | ||||
|       handleDescriptionSave(newDescription); | ||||
|     }, 1000); | ||||
|   }; | ||||
| 
 | ||||
|   // Save updated description
 | ||||
|   const handleDescriptionSave = async (description: string) => { | ||||
|     if (!fileId) { | ||||
|       toast.error("File ID is required."); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     try { | ||||
|       const response = await fetch(`/api/share?id=${encodeURIComponent(fileId)}`, { | ||||
|         method: "PUT", | ||||
|         headers: { "Content-Type": "application/json" }, | ||||
|         body: JSON.stringify({ description }), | ||||
|       }); | ||||
|       if (!response.ok) throw new Error("Failed to update description"); | ||||
| 
 | ||||
|       toast.success("Description updated successfully!"); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       toast.error("Failed to update description."); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return { | ||||
|     handleDownload, | ||||
|     handleCopyUrl, | ||||
|     handleRemove, | ||||
|     handleDescriptionChange, | ||||
|     handleDescriptionSave, | ||||
|   }; | ||||
| }; | ||||
| @ -1,16 +1,18 @@ | ||||
| //!eslint-disable @typescript-eslint/no-unsafe-assignment
 | ||||
| //!eslint-disable @typescript-eslint/no-unsafe-argument
 | ||||
| 
 | ||||
| "use client"; | ||||
| 
 | ||||
| import { useEffect, useState } from "react"; | ||||
| import toast from "react-hot-toast"; | ||||
| import { useRouter } from "next/navigation"; | ||||
| import { env } from "~/env.js"; | ||||
| interface File { | ||||
| import { FilePreview } from "~/app/_components/FilePreview"; | ||||
| import { useFileActions } from "~/app/_components/FileActions"; | ||||
| 
 | ||||
| interface FileDetails { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   url: string; | ||||
|   description: string; | ||||
|   extension: string; | ||||
| } | ||||
| 
 | ||||
| interface FileGridProps { | ||||
| @ -18,18 +20,20 @@ interface FileGridProps { | ||||
| } | ||||
| 
 | ||||
| export default function FileGrid({ session }: FileGridProps) { | ||||
|   const [files, setFiles] = useState<File[]>([]); | ||||
|   const [files, setFiles] = useState<FileDetails[]>([]); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const router = useRouter(); | ||||
|   const pageUrl = env.NEXT_PUBLIC_PAGE_URL; // Assuming PAGE_URL is defined in your environment variables
 | ||||
|   const pageUrl = env.NEXT_PUBLIC_PAGE_URL; | ||||
| 
 | ||||
|   const { handleDownload, handleCopyUrl, handleRemove } = useFileActions(setFiles); | ||||
| 
 | ||||
|   // Fetch files from the server
 | ||||
|   const fetchFiles = async () => { | ||||
|     try { | ||||
|       const response = await fetch("/api/files"); | ||||
|       if (!response.ok) { | ||||
|         throw new Error("Failed to fetch files"); | ||||
|       } | ||||
|       const data = await response.json() as { files: File[] }; // Explicitly type the response
 | ||||
|       if (!response.ok) throw new Error("Failed to fetch files"); | ||||
| 
 | ||||
|       const data = (await response.json()) as { files: FileDetails[] }; | ||||
|       setFiles(data.files); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
| @ -37,49 +41,38 @@ export default function FileGrid({ session }: FileGridProps) { | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleDownload = async (fileId: string, fileName: string) => { | ||||
|     try { | ||||
|       const response = await fetch(`/api/files/download?fileId=${encodeURIComponent(fileId)}&fileName=${encodeURIComponent(fileName)}`); | ||||
|       if (!response.ok) { | ||||
|         throw new Error("Failed to download file"); | ||||
|       } | ||||
|       // Download the file with the correct filename
 | ||||
|       const blob = await response.blob(); | ||||
|       const url = window.URL.createObjectURL(blob); | ||||
|       const a = document.createElement("a"); | ||||
|       a.href = url; | ||||
|       a.download = fileName; | ||||
|       document.body.appendChild(a); | ||||
|       a.click(); | ||||
|       a.remove(); | ||||
|       window.URL.revokeObjectURL(url); | ||||
| 
 | ||||
|       toast.success(`File "${fileName}" downloaded successfully!`); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       toast.error("Failed to download file."); | ||||
|     } | ||||
|   // Determine file type based on extension
 | ||||
|   const getFileType = (extension: string): string => { | ||||
|     const fileTypes: Record<string, string> = { | ||||
|       ".mp4": "video/mp4", | ||||
|       ".webm": "video/webm", | ||||
|       ".ogg": "video/ogg", | ||||
|       ".jpg": "image/jpeg", | ||||
|       ".jpeg": "image/jpeg", | ||||
|       ".png": "image/png", | ||||
|       ".gif": "image/gif", | ||||
|       ".svg": "image/svg+xml", | ||||
|       ".mp3": "audio/mpeg", | ||||
|       ".wav": "audio/wav", | ||||
|     }; | ||||
|     return fileTypes[extension] || "unknown"; | ||||
|   }; | ||||
| 
 | ||||
|   // Handle real-time updates via SSE
 | ||||
|   useEffect(() => { | ||||
|     if (!session?.user) { | ||||
|       setError("You must be logged in to view files."); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // Fetch files initially
 | ||||
|     void fetchFiles(); | ||||
| 
 | ||||
|     // Listen for real-time updates via SSE
 | ||||
|     const eventSource = new EventSource("/api/files/stream"); | ||||
| 
 | ||||
|     eventSource.onmessage = (event) => { | ||||
|       const data: { type: string; file?: File; fileId?: string } = JSON.parse(event.data); // Explicitly type the parsed data
 | ||||
|       const data: { type: string; file?: FileDetails; fileId?: string } = JSON.parse(event.data); | ||||
| 
 | ||||
|       if (data.type === "file-added" && data.file) { | ||||
|         if (data.file) { | ||||
|           setFiles((prevFiles) => [...prevFiles, data.file as File]); | ||||
|         } | ||||
|         setFiles((prevFiles) => (data.file ? [...prevFiles, data.file] : prevFiles)); | ||||
|         toast.success(`File "${data.file.name}" added!`); | ||||
|       } else if (data.type === "file-removed" && data.fileId) { | ||||
|         setFiles((prevFiles) => prevFiles.filter((file) => file.id !== data.fileId)); | ||||
| @ -91,59 +84,35 @@ export default function FileGrid({ session }: FileGridProps) { | ||||
|       eventSource.close(); | ||||
|     }; | ||||
| 
 | ||||
|     return () => { | ||||
|       eventSource.close(); // Cleanup on unmount
 | ||||
|     }; | ||||
|     return () => eventSource.close(); | ||||
|   }, [session]); | ||||
| 
 | ||||
|   const handleCopyUrl = (url: string) => { | ||||
|     navigator.clipboard | ||||
|       .writeText(pageUrl + url) | ||||
|       .then(() => toast.success("File URL copied to clipboard!")) | ||||
|       .catch(() => toast.error("Failed to copy URL.")); | ||||
|   }; | ||||
| 
 | ||||
|   const handleRemove = async (fileId: string) => { | ||||
|     try { | ||||
|       const response = await fetch(`/api/remove`, { | ||||
|         method: "DELETE", | ||||
|         headers: { | ||||
|           "Content-Type": "application/json", | ||||
|         }, | ||||
|         body: JSON.stringify({ id: fileId }), | ||||
|       }); | ||||
| 
 | ||||
|       if (!response.ok) { | ||||
|         throw new Error("Failed to delete file"); | ||||
|       } | ||||
| 
 | ||||
|       setFiles((prevFiles) => prevFiles.filter((file) => file.id !== fileId)); | ||||
|       toast.success("File removed successfully!"); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       toast.error("Failed to remove file."); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   if (error) { | ||||
|     return <div className="text-red-500">{error}</div>; | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-8"> | ||||
|         {files.map((file) => ( | ||||
|       {files.map((file) => { | ||||
|         const fileType = getFileType(file.extension); | ||||
| 
 | ||||
|         return ( | ||||
|           <div | ||||
|             key={file.id} | ||||
|             className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" | ||||
|             className="flex place-content-end max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" | ||||
|           > | ||||
|             {fileType !== "unknown" && <div className=" self-center max-w-50"><FilePreview fileId={file.id} fileType={fileType} /></div>} | ||||
| 
 | ||||
|             <button onClick={() => router.push(env.NEXT_PUBLIC_PAGE_URL + file.url)}> | ||||
|             <button onClick={() => router.push(pageUrl + file.url)}> | ||||
|               <h3 className="text-2xl font-bold">{file.name}</h3> | ||||
|             </button> | ||||
|             <div className="flex gap-2"> | ||||
|             {file.description && (<p className="text-sm text-gray-400">Description: {file.description}</p>)} | ||||
|              | ||||
| 
 | ||||
|             <div className="flex self-center gap-2"> | ||||
|               {/* Download Button */} | ||||
|               <button | ||||
|                 onClick={() => handleDownload(file.id,file.name)} | ||||
|                 onClick={() => handleDownload(file.id, file.name)} | ||||
|                 className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600" | ||||
|               > | ||||
|                 <svg | ||||
| @ -191,7 +160,7 @@ export default function FileGrid({ session }: FileGridProps) { | ||||
|                 <svg | ||||
|                   xmlns="http://www.w3.org/2000/svg" | ||||
|                   fill="none" | ||||
|                   viewBox="0 24 24" | ||||
|                   viewBox="0 0 24 24" | ||||
|                   strokeWidth={2} | ||||
|                   stroke="currentColor" | ||||
|                   className="h-5 w-5 text-white" | ||||
| @ -205,7 +174,8 @@ export default function FileGrid({ session }: FileGridProps) { | ||||
|               </button> | ||||
|             </div> | ||||
|           </div> | ||||
|         ))} | ||||
|         ); | ||||
|       })} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @ -2,12 +2,12 @@ | ||||
| 
 | ||||
| import { useEffect, useState } from "react"; | ||||
| 
 | ||||
| interface SharePageProps { | ||||
| interface FilePreviewProps { | ||||
|   fileId: string; | ||||
|   fileType: string; // Pass the file type as a prop
 | ||||
| } | ||||
| 
 | ||||
| export function SharePage({ fileId, fileType }: SharePageProps) { | ||||
| export function FilePreview({ fileId, fileType }: FilePreviewProps) { | ||||
|   const [mediaSrc, setMediaSrc] = useState<string | null>(null); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
| 
 | ||||
| @ -24,16 +24,40 @@ export async function GET(req: Request) { | ||||
|     return NextResponse.json({ | ||||
|       name: file.name, | ||||
|       size: file.size, | ||||
|       owner: file.uploadedBy?.name ?? null, // Use nullish coalescing
 | ||||
|       owneravatar: file.uploadedBy?.image ?? null, | ||||
|       owner: file.uploadedBy?.name ?? null, | ||||
|       ownerAvatar: file.uploadedBy?.image ?? null, | ||||
|       uploadDate: file.uploadDate, | ||||
|       id: file.id, | ||||
|       isOwner: session?.user?.id === file.uploadedById, | ||||
|       type: file.extension, | ||||
|       url: file.url, | ||||
|       description: file.description, | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     console.error("Error fetching file details:", error); | ||||
|     return NextResponse.json({ error: "Failed to fetch file details" }, { status: 500 }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export async function PUT(req: Request) { | ||||
|   const session = await auth(); | ||||
|   const url = new URL(req.url); | ||||
|   const fileId = url.searchParams.get("id"); | ||||
|   const { description = "" } = await req.json(); | ||||
| 
 | ||||
|   if (!fileId) { | ||||
|     return NextResponse.json({ error: "File name is required" }, { status: 400 }); | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     const file = await db.file.update({ | ||||
|       where: { id: fileId }, | ||||
|       data: { description }, | ||||
|     }); | ||||
| 
 | ||||
|     return NextResponse.json(file); | ||||
|   } catch (error) { | ||||
|     console.error("Error updating file description:", error); | ||||
|     return NextResponse.json({ error: "Failed to update file description" }, { status: 500 }); | ||||
|   } | ||||
| } | ||||
| @ -1,52 +1,55 @@ | ||||
| "use client"; | ||||
| 
 | ||||
| import { Suspense } from "react"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useEffect, useState, useRef } from "react"; | ||||
| import { useSearchParams, useRouter } from "next/navigation"; | ||||
| import toast, { Toaster } from "react-hot-toast"; | ||||
| import { SharePage } from "~/app/_components/SharePage"; | ||||
| import { FilePreview } from "~/app/_components/FilePreview"; | ||||
| import { useFileActions } from "~/app/_components/FileActions"; | ||||
| import Head from "next/head"; | ||||
| 
 | ||||
| interface FileDetails { | ||||
|   name: string; | ||||
|   size: number; | ||||
|   owner: string; | ||||
|   owneravatar: string | null; | ||||
|   ownerAvatar: string | null; | ||||
|   uploadDate: string; | ||||
|   id: string; | ||||
|   isOwner: boolean; | ||||
|   type: string; | ||||
|   url: string; | ||||
|   description: string; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function Details() { | ||||
| export default function FilePreviewContainer() { | ||||
|   const searchParams = useSearchParams(); | ||||
|   const router = useRouter(); | ||||
|   const fileId = searchParams.get("id"); | ||||
|   const [fileDetails, setFileDetails] = useState<FileDetails | null>(null); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
|   const [description, setDescription] = useState<string>(""); | ||||
|   const debounceTimer = useRef<NodeJS.Timeout | null>(null); | ||||
| 
 | ||||
|   // Determine the file type based on the file extension
 | ||||
|   const fileType = fileDetails?.type === ".mp4" | ||||
|     ? "video/mp4" | ||||
|     : fileDetails?.type === ".webm" | ||||
|     ? "video/webm" | ||||
|     : fileDetails?.type === ".ogg" | ||||
|     ? "video/ogg" | ||||
|     : fileDetails?.type === ".jpg" || fileDetails?.type === ".jpeg" | ||||
|     ? "image/jpeg" | ||||
|     : fileDetails?.type === ".png" | ||||
|     ? "image/png" | ||||
|     : fileDetails?.type === ".gif" | ||||
|     ? "image/gif" | ||||
|     : fileDetails?.type === ".svg" | ||||
|     ? "image/svg+xml" | ||||
|     : fileDetails?.type === ".mp3" | ||||
|     ? "audio/mpeg" | ||||
|     : fileDetails?.type === ".wav" | ||||
|     ? "audio/wav" | ||||
|     // if fileType is not one of the above, set it to unknown
 | ||||
|     : "unknown"; | ||||
|   const { handleDescriptionChange, handleCopyUrl, handleDownload, handleRemove } = useFileActions( | ||||
|     () => {}, | ||||
|     setDescription, | ||||
|     fileId || undefined | ||||
|   ); | ||||
|    | ||||
|   const getFileType = (extension: string): string => { | ||||
|     const fileTypes: Record<string, string> = { | ||||
|       ".mp4": "video/mp4", | ||||
|       ".webm": "video/webm", | ||||
|       ".ogg": "video/ogg", | ||||
|       ".jpg": "image/jpeg", | ||||
|       ".jpeg": "image/jpeg", | ||||
|       ".png": "image/png", | ||||
|       ".gif": "image/gif", | ||||
|       ".svg": "image/svg+xml", | ||||
|       ".mp3": "audio/mpeg", | ||||
|       ".wav": "audio/wav", | ||||
|     }; | ||||
|     return fileTypes[extension] || "unknown"; | ||||
|   } | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!fileId) { | ||||
| @ -57,12 +60,11 @@ function Details() { | ||||
|     const fetchFileDetails = async () => { | ||||
|       try { | ||||
|         const response = await fetch(`/api/share?id=${encodeURIComponent(fileId)}`); | ||||
|         if (!response.ok) { | ||||
|           throw new Error("Failed to fetch file details"); | ||||
|         } | ||||
|         if (!response.ok) throw new Error("Failed to fetch file details"); | ||||
| 
 | ||||
|         const data: FileDetails = await response.json(); | ||||
|         setFileDetails(data); | ||||
|         setDescription(data.description); | ||||
|       } catch (err) { | ||||
|         console.error(err); | ||||
|         setError("Failed to load file details."); | ||||
| @ -78,19 +80,19 @@ function Details() { | ||||
| 
 | ||||
|   if (!fileDetails) { | ||||
|     return ( | ||||
|       <main className="relative flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> | ||||
|       <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> | ||||
|         <Toaster position="top-right" reverseOrder={false} /> | ||||
|         <div className="absolute top-4 left-4"> | ||||
|           <button | ||||
|             onClick={() => router.push("/")} | ||||
|             className="rounded-full bg-white/10 px-4 py-2 font-semibold no-underline transition hover:bg-white/20" | ||||
|             className="rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20" | ||||
|           > | ||||
|             Home | ||||
|           </button> | ||||
|         </div> | ||||
|         <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16"> | ||||
|         <div className="container flex flex-col items-center gap-12 px-4 py-16"> | ||||
|           <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> | ||||
|             <span className="text-[hsl(280,100%,70%)]">File</span> Details | ||||
|             <span className="text-[hsl(280,100%,70%)]">No</span> File Found | ||||
|           </h1> | ||||
|         </div> | ||||
|       </main> | ||||
| @ -98,60 +100,155 @@ function Details() { | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <main className="relative flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> | ||||
|       <Toaster position="top-right" reverseOrder={false} /> | ||||
|       <div className="absolute top-4 left-4"> | ||||
|         <button | ||||
|           onClick={() => router.push("/")} | ||||
|           className="rounded-full bg-white/10 px-4 py-2 font-semibold no-underline transition hover:bg-white/20" | ||||
|         > | ||||
|           Home | ||||
|         </button> | ||||
|       </div> | ||||
|       <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16"> | ||||
|         <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> | ||||
|           <span className="text-[hsl(280,100%,70%)]">File</span> Details | ||||
|         </h1> | ||||
|         <div className="mt-6"> | ||||
|           {// if fileType is not ubknown, show the media player
 | ||||
|             fileType && !fileType.startsWith("unknown") ? ( | ||||
|               <SharePage fileId={fileDetails.id} fileType={fileType} /> | ||||
|             ) : null} | ||||
|            | ||||
|         </div> | ||||
|         <div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white"> | ||||
|           <p> | ||||
|             <strong>Name:</strong> {fileDetails.name} | ||||
|           </p> | ||||
|           <p> | ||||
|             <strong>Size:</strong> { // format size
 | ||||
|               fileDetails.size > 1024 * 1024 * 1024 * 1024 | ||||
|                 ? (fileDetails.size / (1024 * 1024 * 1024 * 1024)).toFixed(2) + " TB" | ||||
|                 : fileDetails.size > 1024 * 1024 * 1024 | ||||
|     <> | ||||
|       <Head> | ||||
|         <title>{fileDetails.name} - File Details</title> | ||||
|         <meta | ||||
|           property="og:title" | ||||
|           content={`${fileDetails.name} - File Details`} | ||||
|         /> | ||||
|         <meta | ||||
|           property="og:description" | ||||
|           content={`Size: ${ | ||||
|             fileDetails.size > 1024 * 1024 * 1024 | ||||
|               ? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB" | ||||
|               : fileDetails.size > 1024 * 1024 | ||||
|               ? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB" | ||||
|               : fileDetails.size > 1024 | ||||
|               ? (fileDetails.size / 1024).toFixed(2) + " KB" | ||||
|               : fileDetails.size + " Bytes" | ||||
|             } | ||||
|           }, Owner: ${fileDetails.owner}, Uploaded on: ${new Date( | ||||
|             fileDetails.uploadDate | ||||
|           ).toLocaleString()}`}
 | ||||
|         /> | ||||
|         <meta property="og:type" content="website" /> | ||||
|       </Head> | ||||
|       <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> | ||||
|         <Toaster position="top-right" reverseOrder={false} /> | ||||
|         <div className="absolute top-4 left-4"> | ||||
|           <button | ||||
|             onClick={() => router.push("/")} | ||||
|             className="rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20" | ||||
|           > | ||||
|             Home | ||||
|           </button> | ||||
|         </div> | ||||
|         <div className="container flex flex-col items-center gap-12 px-4 py-16"> | ||||
|           <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> | ||||
|             <span className="text-[hsl(280,100%,70%)]">File</span> Details | ||||
|           </h1> | ||||
|           <div className="mt-6"> | ||||
|             {fileDetails.type !== "unknown" && ( | ||||
|                | ||||
|               <FilePreview fileId={fileDetails.id} fileType={getFileType(fileDetails.type)} /> | ||||
|             )} | ||||
|           </div> | ||||
|           <div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white"> | ||||
|             <p> | ||||
|               <strong>Name:</strong> {fileDetails.name} | ||||
|             </p> | ||||
|             <p> | ||||
|             <strong>Owner:</strong> <img className="rounded-md inline size-5" src={ fileDetails.owneravatar ? ( fileDetails.owneravatar) : ""} alt="owner image" /> {fileDetails.owner} | ||||
|               <strong>Size:</strong>{" "} | ||||
|               {fileDetails.size > 1024 * 1024 * 1024 | ||||
|                 ? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB" | ||||
|                 : fileDetails.size > 1024 * 1024 | ||||
|                 ? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB" | ||||
|                 : fileDetails.size > 1024 | ||||
|                 ? (fileDetails.size / 1024).toFixed(2) + " KB" | ||||
|                 : fileDetails.size + " Bytes"} | ||||
|             </p> | ||||
|             <p> | ||||
|             <strong>Upload Date:</strong> {new Date(fileDetails.uploadDate).toLocaleString()} | ||||
|               <strong>Owner:</strong>{" "} | ||||
|               <img | ||||
|                 className="rounded-md inline size-5" | ||||
|                 src={fileDetails.ownerAvatar || ""} | ||||
|                 alt="Owner avatar" | ||||
|               />{" "} | ||||
|               {fileDetails.owner} | ||||
|             </p> | ||||
|             <p> | ||||
|               <strong>Upload Date:</strong>{" "} | ||||
|               {new Date(fileDetails.uploadDate).toLocaleString()} | ||||
|             </p> | ||||
|             <p> | ||||
|               <strong>Description:</strong>{" "} | ||||
|               {fileDetails.isOwner ? ( | ||||
|                 <textarea | ||||
|                   value={description} | ||||
|                   onChange={(e) => handleDescriptionChange(e, debounceTimer)} | ||||
|                   className="w-full h-24 p-2 bg-white/10 rounded-lg text-white" | ||||
|                 /> | ||||
|               ) : fileDetails.description === "" ? ( | ||||
|                 <span>No description available</span> | ||||
|               ) : ( | ||||
|                 <span>{fileDetails.description}</span> | ||||
|               )} | ||||
|             </p> | ||||
|             {fileDetails.isOwner && ( | ||||
|               <div className="flex place-content-center gap-4 mt-4"> | ||||
|                 <button | ||||
|                   onClick={() => handleDownload(fileDetails.id, fileDetails.name)} | ||||
|                   className="rounded-full bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 flex items-center gap-2" | ||||
|                 > | ||||
|                   <svg | ||||
|                   xmlns="http://www.w3.org/2000/svg" | ||||
|                   fill="none" | ||||
|                   viewBox="0 0 24 24" | ||||
|                   strokeWidth={2} | ||||
|                   stroke="currentColor" | ||||
|                   className="h-5 w-5 text-white" | ||||
|                 > | ||||
|                   <path | ||||
|                     strokeLinecap="round" | ||||
|                     strokeLinejoin="round" | ||||
|                     d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V3" | ||||
|                   /> | ||||
|                 </svg> | ||||
|                 </button> | ||||
|                 <button | ||||
|                   onClick={() => handleCopyUrl(fileDetails.url)} | ||||
|                   className="rounded-full bg-green-500 px-4 py-2 text-white hover:bg-green-600 flex items-center gap-2" | ||||
|                 > | ||||
|                   <svg | ||||
|                   xmlns="http://www.w3.org/2000/svg" | ||||
|                   fill="none" | ||||
|                   viewBox="0 0 24 24" | ||||
|                   strokeWidth={2} | ||||
|                   stroke="currentColor" | ||||
|                   className="h-5 w-5 text-white" | ||||
|                 > | ||||
|                   <path | ||||
|                     strokeLinecap="round" | ||||
|                     strokeLinejoin="round" | ||||
|                     d="M8 9h8m-6 4h4m-7 8h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z" | ||||
|                   /> | ||||
|                 </svg> | ||||
|                 </button> | ||||
|                 <button | ||||
|                   onClick={() => handleRemove(fileDetails.id)} | ||||
|                   className="rounded-full bg-red-500 px-4 py-2 text-white hover:bg-red-600 flex items-center gap-2" | ||||
|                 > | ||||
|                   <svg | ||||
|                   xmlns="http://www.w3.org/2000/svg" | ||||
|                   fill="none" | ||||
|                   viewBox="0 0 24 24" | ||||
|                   strokeWidth={2} | ||||
|                   stroke="currentColor" | ||||
|                   className="h-5 w-5 text-white" | ||||
|                 > | ||||
|                   <path | ||||
|                     strokeLinecap="round" | ||||
|                     strokeLinejoin="round" | ||||
|                     d="M6 18L18 6M6 6l12 12" | ||||
|                   /> | ||||
|                 </svg> | ||||
|                 </button> | ||||
|               </div> | ||||
|             )} | ||||
|           </div> | ||||
|         </div> | ||||
|       </main> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export default function Page() { | ||||
|   return ( | ||||
|     <Suspense fallback={<div>Loading...</div>}> | ||||
|       <Details /> | ||||
|     </Suspense> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @ -6,13 +6,19 @@ const clients: Set<Client> = new Set<Client>(); | ||||
| 
 | ||||
| export function notifyClients(data: unknown) { | ||||
|   const message = JSON.stringify(data); | ||||
|   const closedClients: Client[] = []; | ||||
| 
 | ||||
|   clients.forEach((client) => { | ||||
|     try { | ||||
|       client.send(message); | ||||
|     } catch (error) { | ||||
|       console.error("Failed to send message to a client:", error); | ||||
|       closedClients.push(client); // Mark the client for removal
 | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   // Remove closed clients from the set
 | ||||
|   closedClients.forEach((client) => clients.delete(client)); | ||||
| } | ||||
| 
 | ||||
| export { clients }; | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user