Refactor code structure for improved readability and maintainability
This commit is contained in:
		
							parent
							
								
									3c19fc0fba
								
							
						
					
					
						commit
						08d1278f11
					
				| @ -4,7 +4,9 @@ | ||||
|  */ | ||||
| import "./src/env.js"; | ||||
| 
 | ||||
| /** @type {import("next").NextConfig} */ | ||||
| const config = {}; | ||||
| const nextConfig = { | ||||
|   reactStrictMode: true, | ||||
|   experimental: {}, | ||||
| }; | ||||
| 
 | ||||
| export default config; | ||||
| export default nextConfig; | ||||
|  | ||||
							
								
								
									
										39
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										39
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -20,6 +20,7 @@ | ||||
|         "next-auth": "5.0.0-beta.25", | ||||
|         "react": "^19.0.0", | ||||
|         "react-dom": "^19.0.0", | ||||
|         "react-hot-toast": "^2.5.2", | ||||
|         "server-only": "^0.0.1", | ||||
|         "superjson": "^2.2.1", | ||||
|         "zod": "^3.24.2" | ||||
| @ -27,6 +28,7 @@ | ||||
|       "devDependencies": { | ||||
|         "@eslint/eslintrc": "^3.3.1", | ||||
|         "@tailwindcss/postcss": "^4.0.15", | ||||
|         "@types/busboy": "^1.5.4", | ||||
|         "@types/node": "^20.14.10", | ||||
|         "@types/react": "^19.0.0", | ||||
|         "@types/react-dom": "^19.0.0", | ||||
| @ -1836,6 +1838,16 @@ | ||||
|         "tslib": "^2.4.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/busboy": { | ||||
|       "version": "1.5.4", | ||||
|       "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", | ||||
|       "integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@types/node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/cookie": { | ||||
|       "version": "0.6.0", | ||||
|       "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", | ||||
| @ -2880,7 +2892,6 @@ | ||||
|       "version": "3.1.3", | ||||
|       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", | ||||
|       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", | ||||
|       "dev": true, | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/damerau-levenshtein": { | ||||
| @ -4041,6 +4052,15 @@ | ||||
|         "url": "https://github.com/sponsors/ljharb" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/goober": { | ||||
|       "version": "2.1.16", | ||||
|       "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", | ||||
|       "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", | ||||
|       "license": "MIT", | ||||
|       "peerDependencies": { | ||||
|         "csstype": "^3.0.10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/gopd": { | ||||
|       "version": "1.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", | ||||
| @ -5842,6 +5862,23 @@ | ||||
|         "react": "^19.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/react-hot-toast": { | ||||
|       "version": "2.5.2", | ||||
|       "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", | ||||
|       "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "csstype": "^3.1.3", | ||||
|         "goober": "^2.1.16" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "react": ">=16", | ||||
|         "react-dom": ">=16" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/react-is": { | ||||
|       "version": "16.13.1", | ||||
|       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", | ||||
|  | ||||
| @ -32,6 +32,7 @@ | ||||
|     "next-auth": "5.0.0-beta.25", | ||||
|     "react": "^19.0.0", | ||||
|     "react-dom": "^19.0.0", | ||||
|     "react-hot-toast": "^2.5.2", | ||||
|     "server-only": "^0.0.1", | ||||
|     "superjson": "^2.2.1", | ||||
|     "zod": "^3.24.2" | ||||
| @ -39,6 +40,7 @@ | ||||
|   "devDependencies": { | ||||
|     "@eslint/eslintrc": "^3.3.1", | ||||
|     "@tailwindcss/postcss": "^4.0.15", | ||||
|     "@types/busboy": "^1.5.4", | ||||
|     "@types/node": "^20.14.10", | ||||
|     "@types/react": "^19.0.0", | ||||
|     "@types/react-dom": "^19.0.0", | ||||
|  | ||||
| @ -63,6 +63,7 @@ model User { | ||||
|     accounts      Account[] | ||||
|     sessions      Session[] | ||||
|     posts         Post[] | ||||
|     files         File[]    // Relation to the File model | ||||
| } | ||||
| 
 | ||||
| model VerificationToken { | ||||
| @ -72,3 +73,15 @@ model VerificationToken { | ||||
| 
 | ||||
|     @@unique([identifier, token]) | ||||
| } | ||||
| 
 | ||||
| model File { | ||||
|     id         String   @id @default(cuid()) | ||||
|     url        String | ||||
|     name       String | ||||
|     size       Int      // Size in bytes | ||||
|     extension  String | ||||
|     uploadDate DateTime @default(now()) | ||||
| 
 | ||||
|     uploadedBy User?    @relation(fields: [uploadedById], references: [id], onDelete: SetNull) | ||||
|     uploadedById String? | ||||
| } | ||||
|  | ||||
							
								
								
									
										207
									
								
								src/app/_components/FileGrid.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								src/app/_components/FileGrid.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,207 @@ | ||||
| "use client"; | ||||
| 
 | ||||
| import { useEffect, useState } from "react"; | ||||
| import toast from "react-hot-toast"; | ||||
| import { useRouter } from "next/navigation"; | ||||
| 
 | ||||
| interface File { | ||||
|   id: string; | ||||
|   name: string; | ||||
|   url: string; | ||||
| } | ||||
| 
 | ||||
| interface FileGridProps { | ||||
|   session: { user: { id: string } } | null; | ||||
| } | ||||
| 
 | ||||
| export default function FileGrid({ session }: FileGridProps) { | ||||
|   const [files, setFiles] = useState<File[]>([]); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
| 
 | ||||
|    | ||||
| 
 | ||||
|   const fetchFiles = async () => { | ||||
|     try { | ||||
|       const response = await fetch("/api/files"); | ||||
|       if (!response.ok) { | ||||
|         throw new Error("Failed to fetch files"); | ||||
|       } | ||||
|       const data: { files: File[] } = await response.json(); | ||||
|       setFiles(data.files); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       setError("Failed to load files."); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleDownload = async (fileName: string) => { | ||||
|     try { | ||||
|       const response = await fetch(`/api/files/download?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."); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   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 = JSON.parse(event.data); | ||||
| 
 | ||||
|       if (data.type === "file-added") { | ||||
|         setFiles((prevFiles) => [...prevFiles, data.file]); | ||||
|         toast.success(`File "${data.file.name}" added!`); | ||||
|       } else if (data.type === "file-removed") { | ||||
|         setFiles((prevFiles) => prevFiles.filter((file) => file.id !== data.fileId)); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     eventSource.onerror = (err) => { | ||||
|       console.error("SSE error:", err); | ||||
|       eventSource.close(); | ||||
|     }; | ||||
| 
 | ||||
|     return () => { | ||||
|       eventSource.close(); // Cleanup on unmount
 | ||||
|     }; | ||||
|   }, [session]); | ||||
| 
 | ||||
|   const handleCopyUrl = (url: string) => { | ||||
|     navigator.clipboard | ||||
|       .writeText(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>; | ||||
|   } | ||||
|   const router = useRouter(); | ||||
| 
 | ||||
|   return ( | ||||
|       <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-8"> | ||||
|         {files.map((file) => ( | ||||
|           <div | ||||
|             key={file.id} | ||||
|             className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" | ||||
|           > | ||||
|              | ||||
|             <button onClick={() => router.push(file.url)}> | ||||
|               <h3 className="text-2xl font-bold">{file.name}</h3> | ||||
|             </button> | ||||
|             <div className="flex gap-2"> | ||||
|               {/* Download Button */} | ||||
|               <button | ||||
|                 onClick={() => handleDownload(file.name)} | ||||
|                 className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600" | ||||
|               > | ||||
|                 <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> | ||||
| 
 | ||||
|               {/* Copy URL Button */} | ||||
|               <button | ||||
|                 onClick={() => handleCopyUrl(file.url)} | ||||
|                 className="flex items-center justify-center rounded-full bg-green-500 p-2 hover:bg-green-600" | ||||
|               > | ||||
|                 <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> | ||||
| 
 | ||||
|               {/* Remove Button */} | ||||
|               <button | ||||
|                 onClick={() => handleRemove(file.id)} | ||||
|                 className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-600" | ||||
|               > | ||||
|                 <svg | ||||
|                   xmlns="http://www.w3.org/2000/svg" | ||||
|                   fill="none" | ||||
|                   viewBox="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> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										170
									
								
								src/app/_components/UploadForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								src/app/_components/UploadForm.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,170 @@ | ||||
| "use client"; | ||||
| 
 | ||||
| import { useState, useRef } from "react"; | ||||
| import toast, { Toaster } from "react-hot-toast"; | ||||
| 
 | ||||
| export default function UploadForm() { | ||||
|   const [file, setFile] = useState<File | null>(null); | ||||
|   const [uploading, setUploading] = useState(false); | ||||
|   const [uploadedFileUrl, setUploadedFileUrl] = useState<string | null>(null); | ||||
|   const [progress, setProgress] = useState<number>(0); // Track upload progress
 | ||||
|   const fileInputRef = useRef<HTMLInputElement | null>(null); // Ref for the file input
 | ||||
| 
 | ||||
|   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     if (e.target.files) { | ||||
|       setFile(e.target.files[0] ?? null); | ||||
|       setUploadedFileUrl(null); // Reset the uploaded file URL when a new file is selected
 | ||||
|       setProgress(0); // Reset progress
 | ||||
|       setUploading(false); // Reset uploading state
 | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleUpload = async () => { | ||||
|     if (!file) return toast.error("Please select a file to upload."); | ||||
|     setUploading(true); | ||||
| 
 | ||||
|     try { | ||||
|       const formData = new FormData(); | ||||
|       formData.append("file", file); | ||||
| 
 | ||||
|       const xhr = new XMLHttpRequest(); | ||||
|       xhr.open("POST", "/api/upload", true); | ||||
| 
 | ||||
|       // Track upload progress
 | ||||
|       xhr.upload.onprogress = (event) => { | ||||
|         if (event.lengthComputable && event.total > 0) { | ||||
|           const percentComplete = Math.round((event.loaded / event.total) * 100); | ||||
|           setProgress(percentComplete); | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       xhr.onload = () => { | ||||
|         if (xhr.status === 200) { | ||||
|           const response = JSON.parse(xhr.responseText); | ||||
|           setUploadedFileUrl(response.url); // Assume the API returns the uploaded file URL
 | ||||
|           toast.success("File uploaded successfully!"); | ||||
| 
 | ||||
|           // Clear the file input and reset state
 | ||||
|           setFile(null); | ||||
|           if (fileInputRef.current) { | ||||
|             fileInputRef.current.value = ""; // Clear the file input
 | ||||
|           } | ||||
|         } else { | ||||
|           console.error("Upload failed:", xhr.responseText); | ||||
|           toast.error("Failed to upload file."); | ||||
|         } | ||||
|         setUploading(false); | ||||
|       }; | ||||
| 
 | ||||
|       xhr.onerror = () => { | ||||
|         console.error("Upload failed"); | ||||
|         toast.error("Failed to upload file."); | ||||
|         setUploading(false); | ||||
|       }; | ||||
| 
 | ||||
|       xhr.send(formData); | ||||
|     } catch (error) { | ||||
|       console.error(error); | ||||
|       toast.error("Failed to upload file."); | ||||
|       setUploading(false); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleCopyUrl = () => { | ||||
|     if (uploadedFileUrl) { | ||||
|       navigator.clipboard | ||||
|         .writeText(uploadedFileUrl) | ||||
|         .then(() => toast.success("File URL copied to clipboard!")) | ||||
|         .catch(() => toast.error("Failed to copy URL.")); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="flex flex-col items-center gap-4"> | ||||
|       {/* Toast container */} | ||||
|       <Toaster position="top-right" reverseOrder={false} /> | ||||
| 
 | ||||
|       <div className="flex flex-row items-center gap-4"> | ||||
|         {/* Custom file input */} | ||||
|         <label | ||||
|           htmlFor="file-upload" | ||||
|           className="cursor-pointer flex items-center gap-2 rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20" | ||||
|         > | ||||
|           {file ? ( | ||||
|             <> | ||||
|               File Selected | ||||
|               {/* SVG Icon */} | ||||
|               <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-green-500" | ||||
|               > | ||||
|                 <path | ||||
|                   strokeLinecap="round" | ||||
|                   strokeLinejoin="round" | ||||
|                   d="M5 13l4 4L19 7" | ||||
|                 /> | ||||
|               </svg> | ||||
|             </> | ||||
|           ) : ( | ||||
|             "Select File" | ||||
|           )} | ||||
|         </label> | ||||
|         <input | ||||
|           id="file-upload" | ||||
|           ref={fileInputRef} // Attach the ref to the file input
 | ||||
|           type="file" | ||||
|           onChange={handleFileChange} | ||||
|           className="hidden" // Hide the default file input
 | ||||
|         /> | ||||
|         <button | ||||
|           onClick={handleUpload} | ||||
|           disabled={uploading || !file} | ||||
|           className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20" | ||||
|         > | ||||
|           {uploading ? "Uploading..." : "Upload"} | ||||
|         </button> | ||||
|       </div> | ||||
| 
 | ||||
|       {file && uploading && ( | ||||
|         <div className="w-full max-w-md flex items-center gap-2"> | ||||
|           <div className="relative h-5 flex-1 rounded-full bg-gray-200"> | ||||
|             <div | ||||
|               className="absolute h-5 rounded-full bg-blue-500" | ||||
|               style={{ width: `${progress}%` }} | ||||
|             ></div> | ||||
|           </div> | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       {uploadedFileUrl && ( | ||||
|         <div className="flex flex-row items-center gap-4"> | ||||
|           <p className="text-white">{uploadedFileUrl}</p> | ||||
|           <button | ||||
|             onClick={handleCopyUrl} | ||||
|             className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600" | ||||
|           > | ||||
|             {/* Copy Icon */} | ||||
|             <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 16h8M8 12h8m-7 8h6a2 2 0 002-2V6a2 2 0 00-2-2H9a2 2 0 00-2 2v12a2 2 0 002 2z" | ||||
|               /> | ||||
|             </svg> | ||||
|           </button> | ||||
|         </div> | ||||
|       )} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @ -1,50 +0,0 @@ | ||||
| "use client"; | ||||
| 
 | ||||
| import { useState } from "react"; | ||||
| 
 | ||||
| import { api } from "~/trpc/react"; | ||||
| 
 | ||||
| export function LatestPost() { | ||||
|   const [latestPost] = api.post.getLatest.useSuspenseQuery(); | ||||
| 
 | ||||
|   const utils = api.useUtils(); | ||||
|   const [name, setName] = useState(""); | ||||
|   const createPost = api.post.create.useMutation({ | ||||
|     onSuccess: async () => { | ||||
|       await utils.post.invalidate(); | ||||
|       setName(""); | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   return ( | ||||
|     <div className="w-full max-w-xs"> | ||||
|       {latestPost ? ( | ||||
|         <p className="truncate">Your most recent post: {latestPost.name}</p> | ||||
|       ) : ( | ||||
|         <p>You have no posts yet.</p> | ||||
|       )} | ||||
|       <form | ||||
|         onSubmit={(e) => { | ||||
|           e.preventDefault(); | ||||
|           createPost.mutate({ name }); | ||||
|         }} | ||||
|         className="flex flex-col gap-2" | ||||
|       > | ||||
|         <input | ||||
|           type="text" | ||||
|           placeholder="Title" | ||||
|           value={name} | ||||
|           onChange={(e) => setName(e.target.value)} | ||||
|           className="w-full rounded-full bg-white/10 px-4 py-2 text-white" | ||||
|         /> | ||||
|         <button | ||||
|           type="submit" | ||||
|           className="rounded-full bg-white/10 px-10 py-3 font-semibold transition hover:bg-white/20" | ||||
|           disabled={createPost.isPending} | ||||
|         > | ||||
|           {createPost.isPending ? "Submitting..." : "Submit"} | ||||
|         </button> | ||||
|       </form> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										27
									
								
								src/app/api/files/download/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/app/api/files/download/route.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| import { NextResponse } from "next/server"; | ||||
| import path from "path"; | ||||
| import { promises as fs } from "fs"; | ||||
| 
 | ||||
| export async function GET(req: Request) { | ||||
|   const url = new URL(req.url); | ||||
|   const fileName = url.searchParams.get("fileName"); | ||||
| 
 | ||||
|   if (!fileName) { | ||||
|     return NextResponse.json({ error: "File name is required" }, { status: 400 }); | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     const filePath = path.join(process.cwd(), "uploads", fileName); | ||||
|     const fileBuffer = await fs.readFile(filePath); | ||||
| 
 | ||||
|     return new Response(fileBuffer, { | ||||
|       headers: { | ||||
|         "Content-Type": "application/octet-stream", | ||||
|         "Content-Disposition": `attachment; filename="${fileName}"`, | ||||
|       }, | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     console.error("Error reading file:", error); | ||||
|     return NextResponse.json({ error: "File not found" }, { status: 404 }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										23
									
								
								src/app/api/files/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/app/api/files/route.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| import { NextResponse } from "next/server"; | ||||
| import { db } from "~/server/db"; // Ensure this points to your Prisma client setup
 | ||||
| import { auth } from "~/server/auth"; | ||||
| 
 | ||||
| export async function GET() { | ||||
|   const session = await auth(); | ||||
| 
 | ||||
|   if (!session?.user) { | ||||
|     return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     const files = await db.file.findMany({ | ||||
|       where: { uploadedById: session.user.id }, | ||||
|       orderBy: { uploadDate: "desc" }, // Replace 'uploadDate' with the correct field name from your schema
 | ||||
|     }); | ||||
| 
 | ||||
|     return NextResponse.json({ files }); | ||||
|   } catch (error) { | ||||
|     console.error("Error fetching files:", error); | ||||
|     return NextResponse.json({ error: "Failed to fetch files" }, { status: 500 }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										56
									
								
								src/app/api/files/stream/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/app/api/files/stream/route.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| import { NextResponse } from "next/server"; | ||||
| 
 | ||||
| const clients: Set<any> = new Set(); | ||||
| 
 | ||||
| export async function GET() { | ||||
|   const stream = new ReadableStream({ | ||||
|     start(controller) { | ||||
|       const abortController = new AbortController(); | ||||
|       const signal = abortController.signal; | ||||
| 
 | ||||
|       const client = { | ||||
|         send: (data: string) => { | ||||
|           controller.enqueue(new TextEncoder().encode(`data: ${data}\n\n`)); | ||||
|         }, | ||||
|         close: () => { | ||||
|           controller.close(); | ||||
|           abortController.abort(); | ||||
|         }, | ||||
|       }; | ||||
| 
 | ||||
|       clients.add(client); | ||||
| 
 | ||||
|       // Remove the client when the stream is closed
 | ||||
|       const abortListener = () => { | ||||
|         clients.delete(client); | ||||
|         controller.close(); // Ensure the stream is closed when the client disconnects
 | ||||
|       }; | ||||
|       signal.addEventListener("abort", abortListener); | ||||
| 
 | ||||
|       // Cleanup the abort listener when the stream is closed
 | ||||
|       signal.addEventListener("abort", () => { | ||||
|         signal.removeEventListener("abort", abortListener); | ||||
|       }); | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   return new Response(stream, { | ||||
|     headers: { | ||||
|       "Content-Type": "text/event-stream", | ||||
|       "Cache-Control": "no-cache", | ||||
|       Connection: "keep-alive", | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| // Notify all connected clients about a file change
 | ||||
| export function notifyClients(data: any) { | ||||
|   const message = JSON.stringify(data); | ||||
|   clients.forEach((client) => { | ||||
|     try { | ||||
|       client.send(message); | ||||
|     } catch (error) { | ||||
|       console.error("Failed to send message to a client:", error); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										48
									
								
								src/app/api/remove/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/app/api/remove/route.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| import { NextResponse } from "next/server"; | ||||
| import { db } from "~/server/db"; | ||||
| import { auth } from "~/server/auth"; | ||||
| import path from "path"; | ||||
| import { promises as fs } from "fs"; | ||||
| import { notifyClients } from "../files/stream/route"; | ||||
| 
 | ||||
| export async function DELETE(req: Request) { | ||||
|   const session = await auth(); | ||||
| 
 | ||||
|   if (!session?.user) { | ||||
|     return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     const body = await req.json().catch(() => null); // Handle empty or invalid JSON
 | ||||
|     if (!body || !body.id) { | ||||
|       return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); | ||||
|     } | ||||
| 
 | ||||
|     const resourceId = body.id; | ||||
| 
 | ||||
|     const resource = await db.file.findUnique({ | ||||
|       where: { id: resourceId }, | ||||
|     }); | ||||
| 
 | ||||
|     if (!resource || resource.uploadedById !== session.user.id) { | ||||
|       return NextResponse.json({ error: "Resource not found or unauthorized" }, { status: 404 }); | ||||
|     } | ||||
| 
 | ||||
|     const filePath = path.join(process.cwd(), "uploads", path.basename(resource.name)); | ||||
|     await fs.unlink(filePath).catch((err) => { | ||||
|       console.error("Error deleting file from filesystem:", err); | ||||
|     }); | ||||
| 
 | ||||
|     await db.file.delete({ | ||||
|       where: { id: resourceId }, | ||||
|     }); | ||||
| 
 | ||||
|     // Notify clients about the deleted file
 | ||||
|     notifyClients({ type: "file-removed", fileId: resourceId }); | ||||
| 
 | ||||
|     return NextResponse.json({ message: "Resource deleted successfully" }); | ||||
|   } catch (error) { | ||||
|     console.error("Error deleting resource:", error); | ||||
|     return NextResponse.json({ error: "Failed to delete resource" }, { status: 500 }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/app/api/serv/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/app/api/serv/route.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| import { NextResponse } from "next/server"; | ||||
| import path from "path"; | ||||
| import { promises as fs } from "fs"; | ||||
| import { db } from "~/server/db"; | ||||
| 
 | ||||
| export async function GET(req: Request) { | ||||
|   const url = new URL(req.url); | ||||
|   const fileId = url.searchParams.get("id"); // Get the `id` parameter from the query
 | ||||
| 
 | ||||
|   if (!fileId) { | ||||
|     return NextResponse.json({ error: "File ID is required" }, { status: 400 }); | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     // Fetch file metadata from the database
 | ||||
|     const file = await db.file.findFirst({ | ||||
|       where: { name: fileId }, | ||||
|     }); | ||||
| 
 | ||||
|     if (!file) { | ||||
|       return NextResponse.json({ error: "File not found" }, { status: 404 }); | ||||
|     } | ||||
| 
 | ||||
|     // Construct the file path
 | ||||
|     const filePath = path.join(process.cwd(), "uploads", file.name); | ||||
| 
 | ||||
|     // Read the file from the filesystem
 | ||||
|     const fileBuffer = await fs.readFile(filePath); | ||||
| 
 | ||||
|     // Return the file as a binary response
 | ||||
|     return new Response(fileBuffer, { | ||||
|       headers: { | ||||
|         "Content-Type": file.extension.startsWith(".png") ? "image/png" : "application/octet-stream", | ||||
|         "Content-Disposition": `inline; filename="${file.name}"`, | ||||
|       }, | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     console.error("Error fetching file:", error); | ||||
|     return NextResponse.json({ error: "Failed to fetch file" }, { status: 500 }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										40
									
								
								src/app/api/share/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/app/api/share/route.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| import { NextResponse } from "next/server"; | ||||
| import { db } from "~/server/db"; | ||||
| import { auth } from "~/server/auth"; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| export async function GET(req: Request) { | ||||
|   const session = await auth(); | ||||
|   const url = new URL(req.url); | ||||
|   const fileName = url.searchParams.get("file"); | ||||
| 
 | ||||
|   if (!fileName) { | ||||
|     return NextResponse.json({ error: "File name is required" }, { status: 400 }); | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     const file = await db.file.findFirst({ | ||||
|       where: { name: fileName }, | ||||
|       include: { uploadedBy: true }, | ||||
|     }); | ||||
| 
 | ||||
|     if (!file) { | ||||
|       return NextResponse.json({ error: "File not found" }, { status: 404 }); | ||||
|     } | ||||
| 
 | ||||
|     return NextResponse.json({ | ||||
|       name: file.name, | ||||
|       size: file.size, | ||||
|       owner: file.uploadedBy?.name || "Unknown", | ||||
|       uploadDate: file.uploadDate, | ||||
|       id: file.id, | ||||
|       isOwner: session?.user?.id === file.uploadedById, // Check if the current user is the owner
 | ||||
|       type: file.extension, // Add file type
 | ||||
|       url: file.url, // Add file URL
 | ||||
|     }); | ||||
|   } catch (error) { | ||||
|     console.error("Error fetching file details:", error); | ||||
|     return NextResponse.json({ error: "Failed to fetch file details" }, { status: 500 }); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										111
									
								
								src/app/api/upload/route.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/app/api/upload/route.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,111 @@ | ||||
| import { NextResponse } from "next/server"; | ||||
| import { promises as fs } from "fs"; | ||||
| import path from "path"; | ||||
| import { db } from "~/server/db"; | ||||
| import { auth } from "~/server/auth"; | ||||
| import Busboy from "busboy"; | ||||
| import { Readable } from "stream"; | ||||
| import { notifyClients } from "../files/stream/route"; | ||||
| 
 | ||||
| export const config = { | ||||
|   api: { | ||||
|     bodyParser: false, // Disable Next.js body parsing
 | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export async function POST(req: Request) { | ||||
|   const session = await auth(); | ||||
| 
 | ||||
|   if (!session?.user) { | ||||
|     return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||||
|   } | ||||
| 
 | ||||
|   const uploadDir = path.join(process.cwd(), "uploads"); | ||||
|   await fs.mkdir(uploadDir, { recursive: true }); | ||||
| 
 | ||||
|   return new Promise<Response>((resolve, reject) => { | ||||
|     const busboy = Busboy({ headers: { "content-type": req.headers.get("content-type") ?? "" } }); | ||||
|     let fileName = ""; | ||||
|     let fileBuffer = Buffer.alloc(0); | ||||
| 
 | ||||
|     busboy.on("file", async (fieldname, file, info) => { | ||||
|       fileName = info.filename || "uploaded-file"; | ||||
|       const chunks: Buffer[] = []; | ||||
| 
 | ||||
|       // Check if a file with the same name already exists for the user
 | ||||
|       const existingFile = await db.file.findFirst({ | ||||
|         where: { | ||||
|           name: fileName, | ||||
|           uploadedById: session.user.id, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       if (existingFile) { | ||||
|         // Modify the file name to make it unique
 | ||||
|         const fileExtension = path.extname(fileName); | ||||
|         const baseName = path.basename(fileName, fileExtension); | ||||
|         fileName = `${baseName}-${Date.now()}${fileExtension}`; | ||||
|       } | ||||
| 
 | ||||
|       file.on("data", (chunk) => { | ||||
|         chunks.push(chunk); | ||||
|       }); | ||||
| 
 | ||||
|       file.on("end", () => { | ||||
|         fileBuffer = Buffer.concat(chunks); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     busboy.on("finish", () => { | ||||
|       void (async () => { | ||||
|         try { | ||||
|           const filePath = path.join(uploadDir, fileName); | ||||
|           await fs.writeFile(filePath, fileBuffer); | ||||
|           const pageurl = new URL(req.url); | ||||
|           //get root path of the url
 | ||||
|           const pagePath = `${pageurl.protocol}//${pageurl.host}`; | ||||
| 
 | ||||
|           // Save file metadata to the database
 | ||||
|           const newFile = await db.file.create({ | ||||
|             data: { | ||||
|               url: `${pagePath}/share?file=${fileName}`, | ||||
|               name: fileName, | ||||
|               size: fileBuffer.length, | ||||
|               extension: path.extname(fileName), | ||||
|               uploadedById: session.user.id, | ||||
|             }, | ||||
|           }); | ||||
| 
 | ||||
|           // Notify clients about the new file
 | ||||
|           notifyClients({ type: "file-added", file: newFile }); | ||||
| 
 | ||||
|           resolve(NextResponse.json({ message: "File uploaded successfully" })); | ||||
|         } catch (error) { | ||||
|           console.error("Error handling upload:", error); | ||||
|           resolve(NextResponse.json({ error: "Failed to upload file" }, { status: 500 })); | ||||
|         } | ||||
|       })(); | ||||
|     }); | ||||
| 
 | ||||
|     busboy.on("error", (error: unknown) => { | ||||
|       console.error("Error parsing form data:", error); | ||||
|       reject(new Error("Failed to parse form data")); | ||||
|     }); | ||||
| 
 | ||||
|     if (req.body) { | ||||
|       const reader = req.body.getReader(); | ||||
|       const nodeStream = new Readable({ | ||||
|         async read() { | ||||
|           const { done, value } = await reader.read(); | ||||
|           if (done) { | ||||
|             this.push(null); // End the stream
 | ||||
|           } else { | ||||
|             this.push(value); // Push data to the stream
 | ||||
|           } | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       nodeStream.pipe(busboy); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| @ -6,8 +6,9 @@ import { Geist } from "next/font/google"; | ||||
| import { TRPCReactProvider } from "~/trpc/react"; | ||||
| 
 | ||||
| export const metadata: Metadata = { | ||||
|   title: "Create T3 App", | ||||
|   description: "Generated by create-t3-app", | ||||
|   title: "File Hosting - Suchodupin", | ||||
|   description: "A simple file hosting service", | ||||
|   authors: [{ name: "Suchodupin" }], | ||||
|   icons: [{ rel: "icon", url: "/favicon.ico" }], | ||||
| }; | ||||
| 
 | ||||
|  | ||||
| @ -1,57 +1,48 @@ | ||||
| import Link from "next/link"; | ||||
| 
 | ||||
| import { LatestPost } from "~/app/_components/post"; | ||||
| import { auth } from "~/server/auth"; | ||||
| import { api, HydrateClient } from "~/trpc/server"; | ||||
| import { HydrateClient } from "~/trpc/server"; | ||||
| import FileGrid from "~/app/_components/FileGrid"; | ||||
| import UploadForm from "~/app/_components/UploadForm"; | ||||
| import toast, { Toaster } from "react-hot-toast"; | ||||
| 
 | ||||
| export default async function Home() { | ||||
|   const hello = await api.post.hello({ text: "from tRPC" }); | ||||
|   const session = await auth(); | ||||
| 
 | ||||
|   if (session?.user) { | ||||
|     void api.post.getLatest.prefetch(); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <HydrateClient> | ||||
|       <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} /> | ||||
|       <main className="relative flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> | ||||
|         {/* Top-right corner sign-out button */} | ||||
|         {session?.user && ( | ||||
|           <div className="absolute top-4 right-4"> | ||||
|             <Link | ||||
|               href="/api/auth/signout" | ||||
|               className="rounded-full bg-white/10 px-4 py-2 font-semibold no-underline transition hover:bg-white/20" | ||||
|             > | ||||
|               Sign out | ||||
|             </Link> | ||||
|           </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]"> | ||||
|             Create <span className="text-[hsl(280,100%,70%)]">T3</span> App | ||||
|             <span className="text-[hsl(280,100%,70%)]">File</span> Hosting | ||||
|           </h1> | ||||
|           <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8"> | ||||
|             <Link | ||||
|               className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" | ||||
|               href="https://create.t3.gg/en/usage/first-steps" | ||||
|               target="_blank" | ||||
|             > | ||||
|               <h3 className="text-2xl font-bold">First Steps →</h3> | ||||
|               <div className="text-lg"> | ||||
|                 Just the basics - Everything you need to know to set up your | ||||
|                 database and authentication. | ||||
|               </div> | ||||
|             </Link> | ||||
|             <Link | ||||
|               className="flex max-w-xs flex-col gap-4 rounded-xl bg-white/10 p-4 hover:bg-white/20" | ||||
|               href="https://create.t3.gg/en/introduction" | ||||
|               target="_blank" | ||||
|             > | ||||
|               <h3 className="text-2xl font-bold">Documentation →</h3> | ||||
|               <div className="text-lg"> | ||||
|                 Learn more about Create T3 App, the libraries it uses, and how | ||||
|                 to deploy it. | ||||
|               </div> | ||||
|             </Link> | ||||
|           </div> | ||||
|           <div className="flex flex-col items-center gap-2"> | ||||
|             <p className="text-2xl text-white"> | ||||
|               {hello ? hello.greeting : "Loading tRPC query..."} | ||||
|             </p> | ||||
| 
 | ||||
|             <div className="flex flex-col items-center justify-center gap-4"> | ||||
|           {/* Conditionally render FileGrid and UploadForm if the user is logged in */} | ||||
|           {session?.user ? ( | ||||
|             <> | ||||
|               <FileGrid session={session} /> | ||||
|               <UploadForm/> | ||||
|             </> | ||||
|           ) : ( | ||||
|             <p className="text-center text-2xl text-white"> | ||||
|                 {session && <span>Logged in as {session.user?.name}</span>} | ||||
|               Please log in to upload and view files. | ||||
|             </p> | ||||
|           )} | ||||
|           {!session?.user && ( | ||||
|           <div className="flex flex-col items-center gap-2"> | ||||
|             <div className="flex flex-col items-center justify-center gap-4"> | ||||
|               <Link | ||||
|                 href={session ? "/api/auth/signout" : "/api/auth/signin"} | ||||
|                 className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20" | ||||
| @ -60,8 +51,7 @@ export default async function Home() { | ||||
|               </Link> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           {session?.user && <LatestPost />} | ||||
|           )} | ||||
|         </div> | ||||
|       </main> | ||||
|     </HydrateClient> | ||||
|  | ||||
							
								
								
									
										248
									
								
								src/app/share/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								src/app/share/page.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,248 @@ | ||||
| "use client"; | ||||
| 
 | ||||
| import { useEffect, useState } from "react"; | ||||
| import { useSearchParams, useRouter } from "next/navigation"; | ||||
| import toast, { Toaster } from "react-hot-toast"; | ||||
| 
 | ||||
| interface FileDetails { | ||||
|   name: string; | ||||
|   size: number; | ||||
|   owner: string; | ||||
|   uploadDate: string; | ||||
|   id: string; // Add an ID for the file
 | ||||
|   isOwner: boolean; // Add a flag to indicate ownership
 | ||||
|   type: string; // Add a type field to differentiate between file types
 | ||||
|   url: string; // Add a URL field for the file
 | ||||
| } | ||||
| 
 | ||||
| export default function UploadsPage() { | ||||
|   const searchParams = useSearchParams(); | ||||
|   const router = useRouter(); | ||||
|   const fileName = searchParams.get("file"); | ||||
|   const [fileDetails, setFileDetails] = useState<FileDetails | null>(null); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!fileName) { | ||||
|       setError("File name is required."); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const fetchFileDetails = async () => { | ||||
|       try { | ||||
|         const response = await fetch(`/api/share?file=${encodeURIComponent(fileName)}`); | ||||
|         if (!response.ok) { | ||||
|           throw new Error("Failed to fetch file details"); | ||||
|         } | ||||
| 
 | ||||
|         const data = await response.json(); | ||||
|         setFileDetails(data); | ||||
|       } catch (err) { | ||||
|         console.error(err); | ||||
|         setError("Failed to load file details."); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     fetchFileDetails(); | ||||
|   }, [fileName]); | ||||
| 
 | ||||
|   const handleDownload = async () => { | ||||
|     try { | ||||
|       const response = await fetch(`/api/files/download?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 || "downloaded-file"; | ||||
|       document.body.appendChild(a); | ||||
|       a.click(); | ||||
|       a.remove(); | ||||
|       window.URL.revokeObjectURL(url); | ||||
| 
 | ||||
|       toast.success("File downloaded successfully!"); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       toast.error("Failed to download file."); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleShare = () => { | ||||
|     if (fileDetails) { | ||||
|       const shareableLink = `${window.location.origin}/share?id=${fileDetails.name}`; | ||||
|       navigator.clipboard | ||||
|         .writeText(shareableLink) | ||||
|         .then(() => toast.success("Shareable link copied to clipboard!")) | ||||
|         .catch(() => toast.error("Failed to copy link.")); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleRemove = async () => { | ||||
|     try { | ||||
|       const response = await fetch(`/api/remove`, { | ||||
|         method: "DELETE", | ||||
|         headers: { | ||||
|           "Content-Type": "application/json", | ||||
|         }, | ||||
|         body: JSON.stringify({ id: fileDetails?.id }), // Use the ID of the file
 | ||||
|       }); | ||||
| 
 | ||||
|       if (!response.ok) { | ||||
|         throw new Error("Failed to remove file"); | ||||
|       } | ||||
| 
 | ||||
|       toast.success("File removed successfully!"); | ||||
|       router.push("/"); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|       toast.error("Failed to remove file."); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   if (error) { | ||||
|     return <div className="text-red-500">{error}</div>; | ||||
|   } | ||||
| 
 | ||||
|   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"> | ||||
|       <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> | ||||
|     </main> | ||||
|   ); | ||||
|   } | ||||
| 
 | ||||
|   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"> | ||||
|             {(fileDetails.type.startsWith(".png") || fileDetails.type.startsWith(".jpg") || fileDetails.type.startsWith(".jpeg") || fileDetails.type.startsWith(".gif")) && ( | ||||
|               <SharePage /> | ||||
|             )} | ||||
|             {(fileDetails.type.startsWith(".mp4") || fileDetails.type.startsWith(".webm") || fileDetails.type.startsWith(".ogg")) && ( | ||||
|               <video controls className="max-w-full max-h-96 rounded-lg shadow-md"> | ||||
|               <SharePage /> | ||||
|               Your browser does not support the video tag. | ||||
|               </video> | ||||
|             )} | ||||
|           </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).toFixed(2)} KB | ||||
|           </p> | ||||
|           <p> | ||||
|             <strong>Owner:</strong> {fileDetails.owner} | ||||
|           </p> | ||||
|           <p> | ||||
|             <strong>Upload Date:</strong> {new Date(fileDetails.uploadDate).toLocaleString()} | ||||
|           </p> | ||||
|         </div> | ||||
|         {/* Preview Section */} | ||||
|         <div className="flex gap-4 mt-6"> | ||||
|           <button | ||||
|             onClick={handleDownload} | ||||
|             className="rounded-full bg-blue-500 px-10 py-3 font-semibold no-underline transition hover:bg-blue-600" | ||||
|           > | ||||
|             Download | ||||
|           </button> | ||||
|           {fileDetails.isOwner && ( | ||||
|             <> | ||||
|               <button | ||||
|                 onClick={handleShare} | ||||
|                 className="rounded-full bg-green-500 px-10 py-3 font-semibold no-underline transition hover:bg-green-600" | ||||
|               > | ||||
|                 Share | ||||
|               </button> | ||||
|               <button | ||||
|                 onClick={handleRemove} | ||||
|                 className="rounded-full bg-red-500 px-10 py-3 font-semibold no-underline transition hover:bg-red-600" | ||||
|               > | ||||
|                 Remove | ||||
|               </button> | ||||
|             </> | ||||
|           )} | ||||
|         </div> | ||||
|       </div> | ||||
|     </main> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export function SharePage() { | ||||
|   const searchParams = useSearchParams(); | ||||
|   const fileName = searchParams.get("file"); | ||||
|   const [imageSrc, setImageSrc] = useState<string | null>(null); | ||||
|   const [error, setError] = useState<string | null>(null); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
| 
 | ||||
|     const fetchImage = async () => { | ||||
|       try { | ||||
|         if (!fileName) { | ||||
|           throw new Error("File name is required."); | ||||
|         } | ||||
|         const response = await fetch(`/api/serv?id=${encodeURIComponent(fileName)}`); | ||||
|         if (!response.ok) { | ||||
|           throw new Error("Failed to fetch image"); | ||||
|         } | ||||
| 
 | ||||
|         const blob = await response.blob(); | ||||
|         const objectUrl = URL.createObjectURL(blob); | ||||
|         setImageSrc(objectUrl); | ||||
|       } catch (err) { | ||||
|         console.error(err); | ||||
|         setError("Failed to load image."); | ||||
|         console.log(err); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     fetchImage(); | ||||
| 
 | ||||
|     return () => { | ||||
|       if (imageSrc) { | ||||
|         URL.revokeObjectURL(imageSrc); // Clean up the object URL
 | ||||
|       } | ||||
|     }; | ||||
|   }, [fileName]); | ||||
| 
 | ||||
|   if (error) { | ||||
|     return <div className="text-red-500">{error}</div>; | ||||
|   } | ||||
| 
 | ||||
|   if (!imageSrc) { | ||||
|     return <div>Loading...</div>; | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <source src={imageSrc} className="max-w-full max-h-96 rounded-lg shadow-md" /> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								uploads/Szymanoch-Toyota.zip
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								uploads/Szymanoch-Toyota.zip
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								uploads/naturaltanczy.mp4
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								uploads/naturaltanczy.mp4
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user