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 |     size       Int      // Size in bytes | ||||||
|     extension  String |     extension  String | ||||||
|     uploadDate DateTime @default(now()) |     uploadDate DateTime @default(now()) | ||||||
| 
 |     description String @default("") | ||||||
|     uploadedBy User?    @relation(fields: [uploadedById], references: [id], onDelete: SetNull) |     uploadedBy User?    @relation(fields: [uploadedById], references: [id], onDelete: SetNull) | ||||||
|     uploadedById String? |     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"; | "use client"; | ||||||
| 
 | 
 | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import toast from "react-hot-toast"; | import toast from "react-hot-toast"; | ||||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||||
| import { env } from "~/env.js"; | import { env } from "~/env.js"; | ||||||
| interface File { | import { FilePreview } from "~/app/_components/FilePreview"; | ||||||
|  | import { useFileActions } from "~/app/_components/FileActions"; | ||||||
|  | 
 | ||||||
|  | interface FileDetails { | ||||||
|   id: string; |   id: string; | ||||||
|   name: string; |   name: string; | ||||||
|   url: string; |   url: string; | ||||||
|  |   description: string; | ||||||
|  |   extension: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| interface FileGridProps { | interface FileGridProps { | ||||||
| @ -18,18 +20,20 @@ interface FileGridProps { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default function FileGrid({ session }: 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 [error, setError] = useState<string | null>(null); | ||||||
|   const router = useRouter(); |   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 () => { |   const fetchFiles = async () => { | ||||||
|     try { |     try { | ||||||
|       const response = await fetch("/api/files"); |       const response = await fetch("/api/files"); | ||||||
|       if (!response.ok) { |       if (!response.ok) throw new Error("Failed to fetch files"); | ||||||
|         throw new Error("Failed to fetch files"); | 
 | ||||||
|       } |       const data = (await response.json()) as { files: FileDetails[] }; | ||||||
|       const data = await response.json() as { files: File[] }; // Explicitly type the response
 |  | ||||||
|       setFiles(data.files); |       setFiles(data.files); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|       console.error(err); |       console.error(err); | ||||||
| @ -37,49 +41,38 @@ export default function FileGrid({ session }: FileGridProps) { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   const handleDownload = async (fileId: string, fileName: string) => { |   // Determine file type based on extension
 | ||||||
|     try { |   const getFileType = (extension: string): string => { | ||||||
|       const response = await fetch(`/api/files/download?fileId=${encodeURIComponent(fileId)}&fileName=${encodeURIComponent(fileName)}`); |     const fileTypes: Record<string, string> = { | ||||||
|       if (!response.ok) { |       ".mp4": "video/mp4", | ||||||
|         throw new Error("Failed to download file"); |       ".webm": "video/webm", | ||||||
|       } |       ".ogg": "video/ogg", | ||||||
|       // Download the file with the correct filename
 |       ".jpg": "image/jpeg", | ||||||
|       const blob = await response.blob(); |       ".jpeg": "image/jpeg", | ||||||
|       const url = window.URL.createObjectURL(blob); |       ".png": "image/png", | ||||||
|       const a = document.createElement("a"); |       ".gif": "image/gif", | ||||||
|       a.href = url; |       ".svg": "image/svg+xml", | ||||||
|       a.download = fileName; |       ".mp3": "audio/mpeg", | ||||||
|       document.body.appendChild(a); |       ".wav": "audio/wav", | ||||||
|       a.click(); |     }; | ||||||
|       a.remove(); |     return fileTypes[extension] || "unknown"; | ||||||
|       window.URL.revokeObjectURL(url); |  | ||||||
| 
 |  | ||||||
|       toast.success(`File "${fileName}" downloaded successfully!`); |  | ||||||
|     } catch (err) { |  | ||||||
|       console.error(err); |  | ||||||
|       toast.error("Failed to download file."); |  | ||||||
|     } |  | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   // Handle real-time updates via SSE
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (!session?.user) { |     if (!session?.user) { | ||||||
|       setError("You must be logged in to view files."); |       setError("You must be logged in to view files."); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Fetch files initially
 |  | ||||||
|     void fetchFiles(); |     void fetchFiles(); | ||||||
| 
 | 
 | ||||||
|     // Listen for real-time updates via SSE
 |  | ||||||
|     const eventSource = new EventSource("/api/files/stream"); |     const eventSource = new EventSource("/api/files/stream"); | ||||||
| 
 |  | ||||||
|     eventSource.onmessage = (event) => { |     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.type === "file-added" && data.file) { | ||||||
|         if (data.file) { |         setFiles((prevFiles) => (data.file ? [...prevFiles, data.file] : prevFiles)); | ||||||
|           setFiles((prevFiles) => [...prevFiles, data.file as File]); |  | ||||||
|         } |  | ||||||
|         toast.success(`File "${data.file.name}" added!`); |         toast.success(`File "${data.file.name}" added!`); | ||||||
|       } else if (data.type === "file-removed" && data.fileId) { |       } else if (data.type === "file-removed" && data.fileId) { | ||||||
|         setFiles((prevFiles) => prevFiles.filter((file) => file.id !== data.fileId)); |         setFiles((prevFiles) => prevFiles.filter((file) => file.id !== data.fileId)); | ||||||
| @ -91,59 +84,35 @@ export default function FileGrid({ session }: FileGridProps) { | |||||||
|       eventSource.close(); |       eventSource.close(); | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     return () => { |     return () => eventSource.close(); | ||||||
|       eventSource.close(); // Cleanup on unmount
 |  | ||||||
|     }; |  | ||||||
|   }, [session]); |   }, [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) { |   if (error) { | ||||||
|     return <div className="text-red-500">{error}</div>; |     return <div className="text-red-500">{error}</div>; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|       <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-8"> |     <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 |           <div | ||||||
|             key={file.id} |             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> |               <h3 className="text-2xl font-bold">{file.name}</h3> | ||||||
|             </button> |             </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 */} |               {/* Download Button */} | ||||||
|               <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" |                 className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600" | ||||||
|               > |               > | ||||||
|                 <svg |                 <svg | ||||||
| @ -191,7 +160,7 @@ export default function FileGrid({ session }: FileGridProps) { | |||||||
|                 <svg |                 <svg | ||||||
|                   xmlns="http://www.w3.org/2000/svg" |                   xmlns="http://www.w3.org/2000/svg" | ||||||
|                   fill="none" |                   fill="none" | ||||||
|                   viewBox="0 24 24" |                   viewBox="0 0 24 24" | ||||||
|                   strokeWidth={2} |                   strokeWidth={2} | ||||||
|                   stroke="currentColor" |                   stroke="currentColor" | ||||||
|                   className="h-5 w-5 text-white" |                   className="h-5 w-5 text-white" | ||||||
| @ -205,7 +174,8 @@ export default function FileGrid({ session }: FileGridProps) { | |||||||
|               </button> |               </button> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|         ))} |         ); | ||||||
|       </div> |       })} | ||||||
|  |     </div> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| @ -2,12 +2,12 @@ | |||||||
| 
 | 
 | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| 
 | 
 | ||||||
| interface SharePageProps { | interface FilePreviewProps { | ||||||
|   fileId: string; |   fileId: string; | ||||||
|   fileType: string; // Pass the file type as a prop
 |   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 [mediaSrc, setMediaSrc] = useState<string | null>(null); | ||||||
|   const [error, setError] = useState<string | null>(null); |   const [error, setError] = useState<string | null>(null); | ||||||
| 
 | 
 | ||||||
| @ -24,16 +24,40 @@ export async function GET(req: Request) { | |||||||
|     return NextResponse.json({ |     return NextResponse.json({ | ||||||
|       name: file.name, |       name: file.name, | ||||||
|       size: file.size, |       size: file.size, | ||||||
|       owner: file.uploadedBy?.name ?? null, // Use nullish coalescing
 |       owner: file.uploadedBy?.name ?? null, | ||||||
|       owneravatar: file.uploadedBy?.image ?? null, |       ownerAvatar: file.uploadedBy?.image ?? null, | ||||||
|       uploadDate: file.uploadDate, |       uploadDate: file.uploadDate, | ||||||
|       id: file.id, |       id: file.id, | ||||||
|       isOwner: session?.user?.id === file.uploadedById, |       isOwner: session?.user?.id === file.uploadedById, | ||||||
|       type: file.extension, |       type: file.extension, | ||||||
|       url: file.url, |       url: file.url, | ||||||
|  |       description: file.description, | ||||||
|     }); |     }); | ||||||
|   } catch (error) { |   } catch (error) { | ||||||
|     console.error("Error fetching file details:", error); |     console.error("Error fetching file details:", error); | ||||||
|     return NextResponse.json({ error: "Failed to fetch file details" }, { status: 500 }); |     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"; | "use client"; | ||||||
| 
 | 
 | ||||||
| import { Suspense } from "react"; | import { useEffect, useState, useRef } from "react"; | ||||||
| import { useEffect, useState } from "react"; |  | ||||||
| import { useSearchParams, useRouter } from "next/navigation"; | import { useSearchParams, useRouter } from "next/navigation"; | ||||||
| import toast, { Toaster } from "react-hot-toast"; | 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 { | interface FileDetails { | ||||||
|   name: string; |   name: string; | ||||||
|   size: number; |   size: number; | ||||||
|   owner: string; |   owner: string; | ||||||
|   owneravatar: string | null; |   ownerAvatar: string | null; | ||||||
|   uploadDate: string; |   uploadDate: string; | ||||||
|   id: string; |   id: string; | ||||||
|   isOwner: boolean; |   isOwner: boolean; | ||||||
|   type: string; |   type: string; | ||||||
|   url: string; |   url: string; | ||||||
|  |   description: string; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | export default function FilePreviewContainer() { | ||||||
| function Details() { |  | ||||||
|   const searchParams = useSearchParams(); |   const searchParams = useSearchParams(); | ||||||
|   const router = useRouter(); |   const router = useRouter(); | ||||||
|   const fileId = searchParams.get("id"); |   const fileId = searchParams.get("id"); | ||||||
|   const [fileDetails, setFileDetails] = useState<FileDetails | null>(null); |   const [fileDetails, setFileDetails] = useState<FileDetails | null>(null); | ||||||
|   const [error, setError] = useState<string | 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 { handleDescriptionChange, handleCopyUrl, handleDownload, handleRemove } = useFileActions( | ||||||
|   const fileType = fileDetails?.type === ".mp4" |     () => {}, | ||||||
|     ? "video/mp4" |     setDescription, | ||||||
|     : fileDetails?.type === ".webm" |     fileId || undefined | ||||||
|     ? "video/webm" |   ); | ||||||
|     : fileDetails?.type === ".ogg" |    | ||||||
|     ? "video/ogg" |   const getFileType = (extension: string): string => { | ||||||
|     : fileDetails?.type === ".jpg" || fileDetails?.type === ".jpeg" |     const fileTypes: Record<string, string> = { | ||||||
|     ? "image/jpeg" |       ".mp4": "video/mp4", | ||||||
|     : fileDetails?.type === ".png" |       ".webm": "video/webm", | ||||||
|     ? "image/png" |       ".ogg": "video/ogg", | ||||||
|     : fileDetails?.type === ".gif" |       ".jpg": "image/jpeg", | ||||||
|     ? "image/gif" |       ".jpeg": "image/jpeg", | ||||||
|     : fileDetails?.type === ".svg" |       ".png": "image/png", | ||||||
|     ? "image/svg+xml" |       ".gif": "image/gif", | ||||||
|     : fileDetails?.type === ".mp3" |       ".svg": "image/svg+xml", | ||||||
|     ? "audio/mpeg" |       ".mp3": "audio/mpeg", | ||||||
|     : fileDetails?.type === ".wav" |       ".wav": "audio/wav", | ||||||
|     ? "audio/wav" |     }; | ||||||
|     // if fileType is not one of the above, set it to unknown
 |     return fileTypes[extension] || "unknown"; | ||||||
|     : "unknown"; |   } | ||||||
| 
 | 
 | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (!fileId) { |     if (!fileId) { | ||||||
| @ -57,12 +60,11 @@ function Details() { | |||||||
|     const fetchFileDetails = async () => { |     const fetchFileDetails = async () => { | ||||||
|       try { |       try { | ||||||
|         const response = await fetch(`/api/share?id=${encodeURIComponent(fileId)}`); |         const response = await fetch(`/api/share?id=${encodeURIComponent(fileId)}`); | ||||||
|         if (!response.ok) { |         if (!response.ok) throw new Error("Failed to fetch file details"); | ||||||
|           throw new Error("Failed to fetch file details"); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         const data: FileDetails = await response.json(); |         const data: FileDetails = await response.json(); | ||||||
|         setFileDetails(data); |         setFileDetails(data); | ||||||
|  |         setDescription(data.description); | ||||||
|       } catch (err) { |       } catch (err) { | ||||||
|         console.error(err); |         console.error(err); | ||||||
|         setError("Failed to load file details."); |         setError("Failed to load file details."); | ||||||
| @ -78,19 +80,19 @@ function Details() { | |||||||
| 
 | 
 | ||||||
|   if (!fileDetails) { |   if (!fileDetails) { | ||||||
|     return ( |     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} /> |         <Toaster position="top-right" reverseOrder={false} /> | ||||||
|         <div className="absolute top-4 left-4"> |         <div className="absolute top-4 left-4"> | ||||||
|           <button |           <button | ||||||
|             onClick={() => router.push("/")} |             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 |             Home | ||||||
|           </button> |           </button> | ||||||
|         </div> |         </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]"> |           <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> |           </h1> | ||||||
|         </div> |         </div> | ||||||
|       </main> |       </main> | ||||||
| @ -98,60 +100,155 @@ function Details() { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   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} /> |       <Head> | ||||||
|       <div className="absolute top-4 left-4"> |         <title>{fileDetails.name} - File Details</title> | ||||||
|         <button |         <meta | ||||||
|           onClick={() => router.push("/")} |           property="og:title" | ||||||
|           className="rounded-full bg-white/10 px-4 py-2 font-semibold no-underline transition hover:bg-white/20" |           content={`${fileDetails.name} - File Details`} | ||||||
|         > |         /> | ||||||
|           Home |         <meta | ||||||
|         </button> |           property="og:description" | ||||||
|       </div> |           content={`Size: ${ | ||||||
|       <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16"> |             fileDetails.size > 1024 * 1024 * 1024 | ||||||
|         <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> |               ? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB" | ||||||
|           <span className="text-[hsl(280,100%,70%)]">File</span> Details |               : fileDetails.size > 1024 * 1024 | ||||||
|         </h1> |               ? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB" | ||||||
|         <div className="mt-6"> |               : fileDetails.size > 1024 | ||||||
|           {// if fileType is not ubknown, show the media player
 |               ? (fileDetails.size / 1024).toFixed(2) + " KB" | ||||||
|             fileType && !fileType.startsWith("unknown") ? ( |               : fileDetails.size + " Bytes" | ||||||
|               <SharePage fileId={fileDetails.id} fileType={fileType} /> |           }, Owner: ${fileDetails.owner}, Uploaded on: ${new Date( | ||||||
|             ) : null} |             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> | ||||||
|         <div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white"> |         <div className="container flex flex-col items-center gap-12 px-4 py-16"> | ||||||
|           <p> |           <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> | ||||||
|             <strong>Name:</strong> {fileDetails.name} |             <span className="text-[hsl(280,100%,70%)]">File</span> Details | ||||||
|           </p> |           </h1> | ||||||
|           <p> |           <div className="mt-6"> | ||||||
|             <strong>Size:</strong> { // format size
 |             {fileDetails.type !== "unknown" && ( | ||||||
|               fileDetails.size > 1024 * 1024 * 1024 * 1024 |                | ||||||
|                 ? (fileDetails.size / (1024 * 1024 * 1024 * 1024)).toFixed(2) + " TB" |               <FilePreview fileId={fileDetails.id} fileType={getFileType(fileDetails.type)} /> | ||||||
|                 : fileDetails.size > 1024 * 1024 * 1024 |             )} | ||||||
|  |           </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>{" "} | ||||||
|  |               {fileDetails.size > 1024 * 1024 * 1024 | ||||||
|                 ? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB" |                 ? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB" | ||||||
|                 : fileDetails.size > 1024 * 1024 |                 : fileDetails.size > 1024 * 1024 | ||||||
|                 ? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB" |                 ? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB" | ||||||
|                 : fileDetails.size > 1024 |                 : fileDetails.size > 1024 | ||||||
|                 ? (fileDetails.size / 1024).toFixed(2) + " KB" |                 ? (fileDetails.size / 1024).toFixed(2) + " KB" | ||||||
|                 : fileDetails.size + " Bytes" |                 : fileDetails.size + " Bytes"} | ||||||
|             } |             </p> | ||||||
|           </p> |             <p> | ||||||
|           <p> |               <strong>Owner:</strong>{" "} | ||||||
|             <strong>Owner:</strong> <img className="rounded-md inline size-5" src={ fileDetails.owneravatar ? ( fileDetails.owneravatar) : ""} alt="owner image" /> {fileDetails.owner} |               <img | ||||||
|           </p> |                 className="rounded-md inline size-5" | ||||||
|           <p> |                 src={fileDetails.ownerAvatar || ""} | ||||||
|             <strong>Upload Date:</strong> {new Date(fileDetails.uploadDate).toLocaleString()} |                 alt="Owner avatar" | ||||||
|           </p> |               />{" "} | ||||||
|  |               {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> |         </div> | ||||||
|       </div> |       </main> | ||||||
|     </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) { | export function notifyClients(data: unknown) { | ||||||
|   const message = JSON.stringify(data); |   const message = JSON.stringify(data); | ||||||
|  |   const closedClients: Client[] = []; | ||||||
|  | 
 | ||||||
|   clients.forEach((client) => { |   clients.forEach((client) => { | ||||||
|     try { |     try { | ||||||
|       client.send(message); |       client.send(message); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       console.error("Failed to send message to a client:", 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 }; | export { clients }; | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user