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"; | import "./src/env.js"; | ||||||
| 
 | 
 | ||||||
| /** @type {import("next").NextConfig} */ | const nextConfig = { | ||||||
| const config = {}; |   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", |         "next-auth": "5.0.0-beta.25", | ||||||
|         "react": "^19.0.0", |         "react": "^19.0.0", | ||||||
|         "react-dom": "^19.0.0", |         "react-dom": "^19.0.0", | ||||||
|  |         "react-hot-toast": "^2.5.2", | ||||||
|         "server-only": "^0.0.1", |         "server-only": "^0.0.1", | ||||||
|         "superjson": "^2.2.1", |         "superjson": "^2.2.1", | ||||||
|         "zod": "^3.24.2" |         "zod": "^3.24.2" | ||||||
| @ -27,6 +28,7 @@ | |||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@eslint/eslintrc": "^3.3.1", |         "@eslint/eslintrc": "^3.3.1", | ||||||
|         "@tailwindcss/postcss": "^4.0.15", |         "@tailwindcss/postcss": "^4.0.15", | ||||||
|  |         "@types/busboy": "^1.5.4", | ||||||
|         "@types/node": "^20.14.10", |         "@types/node": "^20.14.10", | ||||||
|         "@types/react": "^19.0.0", |         "@types/react": "^19.0.0", | ||||||
|         "@types/react-dom": "^19.0.0", |         "@types/react-dom": "^19.0.0", | ||||||
| @ -1836,6 +1838,16 @@ | |||||||
|         "tslib": "^2.4.0" |         "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": { |     "node_modules/@types/cookie": { | ||||||
|       "version": "0.6.0", |       "version": "0.6.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", |       "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", | ||||||
| @ -2880,7 +2892,6 @@ | |||||||
|       "version": "3.1.3", |       "version": "3.1.3", | ||||||
|       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", |       "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", | ||||||
|       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", |       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", | ||||||
|       "dev": true, |  | ||||||
|       "license": "MIT" |       "license": "MIT" | ||||||
|     }, |     }, | ||||||
|     "node_modules/damerau-levenshtein": { |     "node_modules/damerau-levenshtein": { | ||||||
| @ -4041,6 +4052,15 @@ | |||||||
|         "url": "https://github.com/sponsors/ljharb" |         "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": { |     "node_modules/gopd": { | ||||||
|       "version": "1.2.0", |       "version": "1.2.0", | ||||||
|       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", |       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", | ||||||
| @ -5842,6 +5862,23 @@ | |||||||
|         "react": "^19.1.0" |         "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": { |     "node_modules/react-is": { | ||||||
|       "version": "16.13.1", |       "version": "16.13.1", | ||||||
|       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", |       "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", | ||||||
|  | |||||||
| @ -32,6 +32,7 @@ | |||||||
|     "next-auth": "5.0.0-beta.25", |     "next-auth": "5.0.0-beta.25", | ||||||
|     "react": "^19.0.0", |     "react": "^19.0.0", | ||||||
|     "react-dom": "^19.0.0", |     "react-dom": "^19.0.0", | ||||||
|  |     "react-hot-toast": "^2.5.2", | ||||||
|     "server-only": "^0.0.1", |     "server-only": "^0.0.1", | ||||||
|     "superjson": "^2.2.1", |     "superjson": "^2.2.1", | ||||||
|     "zod": "^3.24.2" |     "zod": "^3.24.2" | ||||||
| @ -39,6 +40,7 @@ | |||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@eslint/eslintrc": "^3.3.1", |     "@eslint/eslintrc": "^3.3.1", | ||||||
|     "@tailwindcss/postcss": "^4.0.15", |     "@tailwindcss/postcss": "^4.0.15", | ||||||
|  |     "@types/busboy": "^1.5.4", | ||||||
|     "@types/node": "^20.14.10", |     "@types/node": "^20.14.10", | ||||||
|     "@types/react": "^19.0.0", |     "@types/react": "^19.0.0", | ||||||
|     "@types/react-dom": "^19.0.0", |     "@types/react-dom": "^19.0.0", | ||||||
|  | |||||||
| @ -63,6 +63,7 @@ model User { | |||||||
|     accounts      Account[] |     accounts      Account[] | ||||||
|     sessions      Session[] |     sessions      Session[] | ||||||
|     posts         Post[] |     posts         Post[] | ||||||
|  |     files         File[]    // Relation to the File model | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| model VerificationToken { | model VerificationToken { | ||||||
| @ -72,3 +73,15 @@ model VerificationToken { | |||||||
| 
 | 
 | ||||||
|     @@unique([identifier, token]) |     @@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"; | import { TRPCReactProvider } from "~/trpc/react"; | ||||||
| 
 | 
 | ||||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||||
|   title: "Create T3 App", |   title: "File Hosting - Suchodupin", | ||||||
|   description: "Generated by create-t3-app", |   description: "A simple file hosting service", | ||||||
|  |   authors: [{ name: "Suchodupin" }], | ||||||
|   icons: [{ rel: "icon", url: "/favicon.ico" }], |   icons: [{ rel: "icon", url: "/favicon.ico" }], | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,57 +1,48 @@ | |||||||
| import Link from "next/link"; | import Link from "next/link"; | ||||||
| 
 |  | ||||||
| import { LatestPost } from "~/app/_components/post"; |  | ||||||
| import { auth } from "~/server/auth"; | 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() { | export default async function Home() { | ||||||
|   const hello = await api.post.hello({ text: "from tRPC" }); |  | ||||||
|   const session = await auth(); |   const session = await auth(); | ||||||
| 
 | 
 | ||||||
|   if (session?.user) { |  | ||||||
|     void api.post.getLatest.prefetch(); |  | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <HydrateClient> |     <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"> |         <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]"> |           <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> |           </h1> | ||||||
|           <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-8"> |           {/* Conditionally render FileGrid and UploadForm if the user is logged in */} | ||||||
|             <Link |           {session?.user ? ( | ||||||
|               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" |               <FileGrid session={session} /> | ||||||
|               target="_blank" |               <UploadForm/> | ||||||
|             > |             </> | ||||||
|               <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"> |  | ||||||
|             <p className="text-center text-2xl text-white"> |             <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> |             </p> | ||||||
|  |           )} | ||||||
|  |           {!session?.user && ( | ||||||
|  |           <div className="flex flex-col items-center gap-2"> | ||||||
|  |             <div className="flex flex-col items-center justify-center gap-4"> | ||||||
|               <Link |               <Link | ||||||
|                 href={session ? "/api/auth/signout" : "/api/auth/signin"} |                 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" |                 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> |               </Link> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
| 
 |           )} | ||||||
|           {session?.user && <LatestPost />} |  | ||||||
|         </div> |         </div> | ||||||
|       </main> |       </main> | ||||||
|     </HydrateClient> |     </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