Fuck git (#17)
This commit is contained in:
		
						commit
						7aeae9020d
					
				
							
								
								
									
										16
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -18,6 +18,7 @@ | ||||
|         "@trpc/client": "^11.0.0", | ||||
|         "@trpc/react-query": "^11.0.0", | ||||
|         "@trpc/server": "^11.0.0", | ||||
|         "cuid": "^3.0.0", | ||||
|         "dompurify": "^3.2.5", | ||||
|         "github-markdown-css": "^5.8.1", | ||||
|         "gray-matter": "^4.0.3", | ||||
| @ -41,6 +42,7 @@ | ||||
|         "@eslint/eslintrc": "^3.3.1", | ||||
|         "@tailwindcss/postcss": "^4.0.15", | ||||
|         "@types/busboy": "^1.5.4", | ||||
|         "@types/mime-types": "^2.1.4", | ||||
|         "@types/node": "^20.14.10", | ||||
|         "@types/react": "^19.0.0", | ||||
|         "@types/react-dom": "^19.0.0", | ||||
| @ -2317,6 +2319,13 @@ | ||||
|         "@types/unist": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/mime-types": { | ||||
|       "version": "2.1.4", | ||||
|       "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", | ||||
|       "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", | ||||
|       "dev": true, | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/@types/ms": { | ||||
|       "version": "2.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", | ||||
| @ -3524,6 +3533,13 @@ | ||||
|       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/cuid": { | ||||
|       "version": "3.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/cuid/-/cuid-3.0.0.tgz", | ||||
|       "integrity": "sha512-WZYYkHdIDnaxdeP8Misq3Lah5vFjJwGuItJuV+tvMafosMzw0nF297T7mrm8IOWiPJkV6gc7sa8pzx27+w25Zg==", | ||||
|       "deprecated": "Cuid and other k-sortable and non-cryptographic ids (Ulid, ObjectId, KSUID, all UUIDs) are all insecure. Use @paralleldrive/cuid2 instead.", | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/cytoscape": { | ||||
|       "version": "3.32.0", | ||||
|       "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.0.tgz", | ||||
|  | ||||
| @ -30,6 +30,7 @@ | ||||
|     "@trpc/client": "^11.0.0", | ||||
|     "@trpc/react-query": "^11.0.0", | ||||
|     "@trpc/server": "^11.0.0", | ||||
|     "cuid": "^3.0.0", | ||||
|     "dompurify": "^3.2.5", | ||||
|     "github-markdown-css": "^5.8.1", | ||||
|     "gray-matter": "^4.0.3", | ||||
| @ -53,6 +54,7 @@ | ||||
|     "@eslint/eslintrc": "^3.3.1", | ||||
|     "@tailwindcss/postcss": "^4.0.15", | ||||
|     "@types/busboy": "^1.5.4", | ||||
|     "@types/mime-types": "^2.1.4", | ||||
|     "@types/node": "^20.14.10", | ||||
|     "@types/react": "^19.0.0", | ||||
|     "@types/react-dom": "^19.0.0", | ||||
|  | ||||
| @ -1,21 +1,70 @@ | ||||
| import React from 'react'; | ||||
| import React, { Suspense } from "react"; | ||||
| import { HomeButton } from "~/app/_components/HomeButton"; // Import the client component
 | ||||
| import { Toaster } from "react-hot-toast"; | ||||
| import { | ||||
|   FileActionsContainer, | ||||
| } from "~/app/_components/ActionButtons"; // Import the client component
 | ||||
| 
 | ||||
| const LoadingSkeleton = () => ( | ||||
|   <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 animate-pulse"> | ||||
|     {/* Title Skeleton */} | ||||
|     <div className="h-16 w-80 rounded bg-white/20 mb-4" /> | ||||
|     {/* FileGrid Skeleton */} | ||||
|     <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 w-full max-w-4xl"> | ||||
|       {[...Array(6)].map((_, i) => ( | ||||
|         <div key={i} className="h-32 rounded bg-white/10" /> | ||||
|       ))} | ||||
|     </div> | ||||
|     {/* UploadForm Skeleton */} | ||||
|     <div className="mt-8 w-full max-w-md flex flex-col gap-4"> | ||||
|       <div className="h-10 rounded bg-white/20" /> | ||||
|       <div className="h-10 rounded bg-white/10" /> | ||||
|     </div> | ||||
|   </div> | ||||
| const LoadingSkeleton: React.FC = () => ( | ||||
|     <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> | ||||
|       <div className="absolute top-4 left-4"> | ||||
|          <HomeButton /> | ||||
|       </div> | ||||
|       <Toaster position="top-right" reverseOrder={false} /> | ||||
|       <div className="container flex flex-col items-center gap-12 px-4 py-16"> | ||||
|         <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> | ||||
|           <span className="text-[hsl(280,100%,70%)]">File</span> Details | ||||
|         </h1> | ||||
|         <div className="mt-6"> | ||||
|           <svg | ||||
|                       className="h-6 w-6 animate-spin text-white/70" | ||||
|                       xmlns="http://www.w3.org/2000/svg" | ||||
|                       fill="none" | ||||
|                       viewBox="0 0 24 24" | ||||
|                     > | ||||
|                       <circle | ||||
|                         className="opacity-25" | ||||
|                         cx="12" | ||||
|                         cy="12" | ||||
|                         r="10" | ||||
|                         stroke="currentColor" | ||||
|                         strokeWidth="4" | ||||
|                       /> | ||||
|                       <path | ||||
|                         className="opacity-75" | ||||
|                         fill="currentColor" | ||||
|                         d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" | ||||
|                       /> | ||||
|                     </svg> | ||||
|         </div> | ||||
|         <div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md"> | ||||
|           <p> | ||||
|             <strong>Name:</strong> <span className="inline-block h-6 w-24 rounded bg-white/20 animate-pulse align-middle ml-2" /> | ||||
|           </p> | ||||
|           <p> | ||||
|             <strong>Size:</strong> <span className="inline-block h-6 w-16 rounded bg-white/20 animate-pulse align-middle ml-2" /> | ||||
|           </p> | ||||
|           <p> | ||||
|             <strong>Owner:</strong> <span className="inline-block h-6 w-20 rounded bg-white/20 animate-pulse align-middle ml-2" /> | ||||
|           </p> | ||||
|           <p> | ||||
|             <strong>Upload Date:</strong> <span className="inline-block h-6 w-28 rounded bg-white/20 animate-pulse align-middle ml-2" /> | ||||
|           </p> | ||||
|           <div> | ||||
|             <strong>Description:</strong> <span className="inline-block h-6 w-40 rounded bg-white/20 animate-pulse align-middle ml-2" /> | ||||
|           </div> | ||||
|           <div className="mt-4 flex justify-center"> | ||||
|             <FileActionsContainer | ||||
|               fileId={""} | ||||
|               fileName={""} | ||||
|               fileUrl={""} | ||||
|               isOwner={false} | ||||
|               isPublic={false} | ||||
|             /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </main> | ||||
| ); | ||||
| 
 | ||||
| export default LoadingSkeleton; | ||||
| @ -59,7 +59,7 @@ export function FileActionsContainer({ | ||||
|           console.error(err); | ||||
|         } | ||||
|         }} | ||||
|         className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-600" | ||||
|         className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-700" | ||||
|       > | ||||
|         <img src="/icons/delete.svg" alt="Remove" className="h-6 w-6" /> | ||||
|       </button> | ||||
|  | ||||
| @ -1,13 +1,11 @@ | ||||
| "use client"; | ||||
| 
 | ||||
| import { useEffect, useState } from "react"; | ||||
| import toast from "react-hot-toast"; | ||||
| import { useRouter } from "next/navigation"; | ||||
| import { env } from "~/env.js"; | ||||
| import { FilePreview } from "~/app/_components/FilePreview"; | ||||
| import { useFileActions } from "~/app/_components/FileActions"; | ||||
| import { FileActionsContainer } from "./ActionButtons"; | ||||
| import { checkOwner } from "~/utils/checkOwner"; // Import the client component
 | ||||
| 
 | ||||
| interface FileDetails { | ||||
|   id: string; | ||||
| @ -73,13 +71,15 @@ export default function FileGrid({ session }: FileGridProps) { | ||||
| 
 | ||||
|     const eventSource = new EventSource("/api/files/stream"); | ||||
|     eventSource.onmessage = (event) => { | ||||
|       const data: { type: string; file?: FileDetails; fileId?: string } = JSON.parse(event.data); | ||||
| 
 | ||||
|       if (data.type === "file-added" && data.file) { | ||||
|         setFiles((prevFiles) => (data.file ? [...prevFiles, data.file] : prevFiles)); | ||||
|         toast.success(`File "${data.file.name}" added!`); | ||||
|       const data: { type: string; fileId?: string } = JSON.parse(event.data); | ||||
|       console.log("SSE event:", data); | ||||
|       if (data.type === "file-added" && data.fileId) { | ||||
|         fetchFiles(); | ||||
|       } else if (data.type === "file-updated" && data.fileId) { | ||||
|         // Fetch the updated file details
 | ||||
|         fetchFiles(); | ||||
|       } else if (data.type === "file-removed" && data.fileId) { | ||||
|         setFiles((prevFiles) => prevFiles.filter((file) => file.id !== data.fileId)); | ||||
|         setFiles((prevFiles => prevFiles.filter(file => file.id !== data.fileId))); | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										45
									
								
								src/app/_components/GenerateMetadata.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/app/_components/GenerateMetadata.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,45 @@ | ||||
| import type { Metadata } from "next"; | ||||
| 
 | ||||
| export async function generateMetadata({ | ||||
|   searchParams, | ||||
| }: { | ||||
|   searchParams: { id?: string }; | ||||
| }): Promise<Metadata> { | ||||
|   const fileId = searchParams.id; | ||||
| 
 | ||||
|   if (!fileId) { | ||||
|     return { | ||||
|       title: "File Not Found", | ||||
|       description: "The file you are looking for does not exist.", | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   // Fetch file details for metadata
 | ||||
|   const response = await fetch( | ||||
|     `${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/share?id=${encodeURIComponent(fileId)}`, | ||||
|     { cache: "no-store" }, | ||||
|   ); | ||||
|   if (!response.ok) { | ||||
|     return { | ||||
|       title: "File Not Found", | ||||
|       description: "The file you are looking for does not exist.", | ||||
|     }; | ||||
|   } | ||||
|   const fileDetails = await response.json(); | ||||
| 
 | ||||
|   return { | ||||
|     title: fileDetails.name, | ||||
|     description: fileDetails.description || fileDetails.name, | ||||
|     openGraph: { | ||||
|       title: fileDetails.name, | ||||
|       description: fileDetails.description || fileDetails.name, | ||||
|       url: `${process.env.NEXT_PUBLIC_PAGE_URL}/share?id=${fileDetails.id}`, | ||||
|       images: [ | ||||
|         { | ||||
|           url: `${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/serv?id=${fileDetails.id}`, | ||||
|           alt: `${fileDetails.name} preview`, | ||||
|         }, | ||||
|       ], | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
| @ -10,6 +10,7 @@ export default function UploadForm() { | ||||
|   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 [isDragActive, setIsDragActive] = useState(false); // Track drag state
 | ||||
| 
 | ||||
|   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     if (e.target.files) { | ||||
| @ -20,6 +21,34 @@ export default function UploadForm() { | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   // Drag and drop handlers
 | ||||
|   const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { | ||||
|     e.preventDefault(); | ||||
|     e.stopPropagation(); | ||||
|     setIsDragActive(true); | ||||
|   }; | ||||
| 
 | ||||
|   const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => { | ||||
|     e.preventDefault(); | ||||
|     e.stopPropagation(); | ||||
|     setIsDragActive(false); | ||||
|   }; | ||||
| 
 | ||||
|   const handleDrop = (e: React.DragEvent<HTMLDivElement>) => { | ||||
|     e.preventDefault(); | ||||
|     e.stopPropagation(); | ||||
|     setIsDragActive(false); | ||||
|     if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { | ||||
|       setFile(e.dataTransfer.files[0] ?? null); | ||||
|       setUploadedFileUrl(null); | ||||
|       setProgress(0); | ||||
|       setUploading(false); | ||||
|       if (fileInputRef.current) { | ||||
|         fileInputRef.current.value = ""; | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const handleUpload = async () => { | ||||
|     if (!file) return toast.error("Please select a file to upload."); | ||||
|     setUploading(true); | ||||
| @ -41,15 +70,14 @@ export default function UploadForm() { | ||||
| 
 | ||||
|       xhr.onload = () => { | ||||
|         if (xhr.status === 200) { | ||||
|           const response: { url: string } = JSON.parse(xhr.responseText); // Explicitly type the response
 | ||||
|           setUploadedFileUrl(response.url); // Assume the API returns the uploaded file URL
 | ||||
|           notifyClients({type: "file-uploaded", fileUrl: response.url}); // Notify other clients about the new file
 | ||||
|           const response = JSON.parse(xhr.responseText); | ||||
|           setUploadedFileUrl(response.file?.url || null); // Use the new response structure
 | ||||
|           toast.success("File uploaded successfully!"); | ||||
| 
 | ||||
|           // Clear the file input and reset state
 | ||||
|           setFile(null); | ||||
|           if (fileInputRef.current) { | ||||
|             fileInputRef.current.value = ""; // Clear the file input
 | ||||
|             fileInputRef.current.value = ""; | ||||
|           } | ||||
|         } else { | ||||
|           console.error("Upload failed:", xhr.responseText); | ||||
| @ -86,42 +114,48 @@ export default function UploadForm() { | ||||
|       {/* 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> | ||||
|       {/* Drag and Drop Area */} | ||||
|       <div | ||||
|         className={`w-full max-w-md flex flex-col items-center justify-center border-2 border-dashed rounded-lg p-6 mb-2 transition-colors duration-200 ${isDragActive ? "border-blue-500 bg-blue-100/30" : "border-gray-400 bg-transparent hover:bg-gray-50/10"}`} | ||||
|         onDragOver={handleDragOver} | ||||
|         onDragLeave={handleDragLeave} | ||||
|         onDrop={handleDrop} | ||||
|         onClick={() => fileInputRef.current?.click()} | ||||
|         style={{ cursor: "pointer" }} | ||||
|       > | ||||
|         {/* Hidden file input for click-to-select */} | ||||
|         <input | ||||
|           id="file-upload" | ||||
|           ref={fileInputRef} // Attach the ref to the file input
 | ||||
|           type="file" | ||||
|           ref={fileInputRef} | ||||
|           style={{ display: "none" }} | ||||
|           onChange={handleFileChange} | ||||
|           className="hidden" // Hide the default file input
 | ||||
|         /> | ||||
|         <span className="text-gray-300"> | ||||
|           {isDragActive ? "Drop your file here" : "Drag & drop a file here, or click to select"} | ||||
|         </span> | ||||
|         {file && ( | ||||
|           <div className="mt-2 flex items-center gap-2"> | ||||
|         <span className="text-green-500 font-semibold">{file.name}</span> | ||||
|         {/* Add button to remove file */} | ||||
|         <button | ||||
|           onClick={e => { | ||||
|             e.stopPropagation(); | ||||
|             setFile(null); | ||||
|             if (fileInputRef.current) { | ||||
|           fileInputRef.current.value = ""; | ||||
|             } | ||||
|           }} | ||||
|           className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-700" | ||||
|           style={{ cursor: "pointer" }} | ||||
|         > | ||||
|           <img src="/icons/delete.svg" alt="Remove" className="h-6 w-6" /> | ||||
|         </button> | ||||
|           </div> | ||||
|         )} | ||||
|       </div> | ||||
|       {/* Show upload button only when file is selected */} | ||||
|       {file && ( | ||||
|       <div className="flex flex-row items-center gap-4"> | ||||
|         <button | ||||
|           onClick={handleUpload} | ||||
|           disabled={uploading || !file} | ||||
| @ -129,7 +163,7 @@ export default function UploadForm() { | ||||
|         > | ||||
|           {uploading ? "Uploading..." : "Upload"} | ||||
|         </button> | ||||
|       </div> | ||||
|       </div>)} | ||||
| 
 | ||||
|       {file && uploading && ( | ||||
|         <div className="w-full max-w-md flex items-center gap-2"> | ||||
| @ -142,31 +176,17 @@ export default function UploadForm() { | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       {uploadedFileUrl && ( | ||||
|       {/* {uploadedFileUrl && file && ( | ||||
|         <div className="flex flex-row items-center gap-4"> | ||||
|           <p className="text-white">{uploadedFileUrl}</p> | ||||
|           <p className="text-white">{file.name}</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> | ||||
|             <img src="/icons/copy.svg" alt="Copy URL" className="h-6 w-6" /> | ||||
|           </button> | ||||
|         </div> | ||||
|       )} | ||||
|       )} */} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @ -1,11 +1,12 @@ | ||||
| import { NextResponse } from "next/server"; | ||||
| import Busboy from "busboy"; | ||||
| import { Readable } from "stream"; | ||||
| import crypto from "crypto"; | ||||
| import { db } from "~/server/db"; | ||||
| import { auth } from "~/server/auth"; | ||||
| import { minioClient, ensureBucketExists } from "~/utils/minioClient"; | ||||
| import { getFileType } from "~/utils/fileType"; | ||||
| import cuid from 'cuid'; | ||||
| import { notifyClients } from "~/utils/notifyClients"; | ||||
| 
 | ||||
| export const config = { | ||||
|   api: { | ||||
| @ -23,7 +24,9 @@ export async function POST(req: Request) { | ||||
|   await ensureBucketExists(bucketName); | ||||
| 
 | ||||
|   return new Promise<Response>((resolve, reject) => { | ||||
|     const busboy = Busboy({ headers: { "content-type": req.headers.get("content-type") ?? "" } }); | ||||
|     const busboy = Busboy({ | ||||
|       headers: { "content-type": req.headers.get("content-type") ?? "" }, | ||||
|     }); | ||||
|     let fileName = ""; | ||||
|     let fileBuffer = Buffer.alloc(0); | ||||
| 
 | ||||
| @ -39,8 +42,11 @@ export async function POST(req: Request) { | ||||
|         fileBuffer = Buffer.concat(chunks); | ||||
| 
 | ||||
|         // Generate a unique ID for the file
 | ||||
|         const fileId = crypto.randomUUID(); | ||||
|         const fileId = session.user.id + "-" + cuid() | ||||
|         const objectName = `${fileId}-${fileName}`; | ||||
|         // Change UUID to CUID
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|         try { | ||||
|           // Upload the file to MinIO
 | ||||
| @ -57,8 +63,15 @@ export async function POST(req: Request) { | ||||
|               uploadedById: session.user.id, | ||||
|             }, | ||||
|           }); | ||||
|           notifyClients({ type: "file-added", fileId: fileId }); | ||||
| 
 | ||||
|           resolve(NextResponse.json({ message: "File uploaded successfully", file: newFile })); | ||||
|           resolve( | ||||
|             NextResponse.json({ | ||||
|               message: "File uploaded successfully", | ||||
|               file: newFile, | ||||
|               fileId: fileId, | ||||
|             }), | ||||
|           ); | ||||
|         } catch (error) { | ||||
|           console.error("Error uploading file to MinIO:", error); | ||||
|           reject(new Error("Failed to upload file")); | ||||
|  | ||||
							
								
								
									
										100
									
								
								src/app/page.tsx
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								src/app/page.tsx
									
									
									
									
									
								
							| @ -1,16 +1,53 @@ | ||||
| "use client"; | ||||
| 
 | ||||
| import Link from "next/link"; | ||||
| import { auth } from "~/server/auth"; | ||||
| import { HydrateClient } from "~/trpc/server"; | ||||
| import { useEffect, useState } from "react"; | ||||
| import FileGrid from "~/app/_components/FileGrid"; | ||||
| import UploadForm from "~/app/_components/UploadForm"; | ||||
| import { Toaster } from "react-hot-toast"; | ||||
| import { Suspense } from "react"; | ||||
| import LoadingSkeleton from "./LoadingSkeleton"; | ||||
| 
 | ||||
| export default async function Home() { | ||||
|   const session = await auth(); | ||||
| // Custom fallback for FileGrid
 | ||||
| function FileGridFallback() { | ||||
|   return ( | ||||
|     <div className="grid w-full max-w-4xl animate-pulse grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3"> | ||||
|       {[...Array(6)].map((_, i) => ( | ||||
|         <div key={i} className="flex flex-col items-center"> | ||||
|           <span className="mb-2 text-lg text-white/60">Loading</span> | ||||
|           <div className="h-32 w-full rounded bg-white/10" /> | ||||
|         </div> | ||||
|       ))} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| // Custom fallback for UploadForm
 | ||||
| function UploadFormFallback() { | ||||
|   return ( | ||||
|     <div className="mt-8 flex w-full max-w-md animate-pulse flex-col gap-4"> | ||||
|       <div className="h-10 rounded bg-white/20" /> | ||||
|       <div className="h-10 rounded bg-white/10" /> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function Home() { | ||||
|   const [session, setSession] = useState<{ user?: any } | null>(null); | ||||
|   const [loading, setLoading] = useState(true); | ||||
|   useEffect(() => { | ||||
|     async function fetchSession() { | ||||
|       setLoading(true); | ||||
|       const res = await fetch("/api/auth/session"); | ||||
|       const data = await res.json(); | ||||
|       setSession(data); | ||||
|       setLoading(false); | ||||
|     } | ||||
|     fetchSession(); | ||||
|   }, []); | ||||
| 
 | ||||
|   return ( | ||||
|     <HydrateClient> | ||||
|     <> | ||||
|       <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 */} | ||||
| @ -54,30 +91,59 @@ export default async function Home() { | ||||
|           {/* Conditionally render FileGrid and UploadForm if the user is logged in */} | ||||
|           {session?.user ? ( | ||||
|             <> | ||||
|               <Suspense fallback={<p className="text-center text-2xl text-white">Loading...</p>}> | ||||
|                 <FileGrid session={session} /> | ||||
|               <Suspense fallback={<FileGridFallback />}> | ||||
|                 <FileGrid session={session as { user: { id: string } }} /> | ||||
|               </Suspense> | ||||
|               <Suspense fallback={<UploadFormFallback />}> | ||||
|                 <UploadForm /> | ||||
|               </Suspense> | ||||
|               <UploadForm /> | ||||
|             </> | ||||
|           ) : ( | ||||
|           ) : !loading ? ( | ||||
|             <p className="text-center text-2xl text-white"> | ||||
|               Please log in to upload and view files. | ||||
|             </p> | ||||
|           )} | ||||
|           ) : null} | ||||
|           {!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" | ||||
|                 > | ||||
|                   {session ? "Sign out" : "Sign in"} | ||||
|                 </Link> | ||||
|                 {!loading ? ( | ||||
|                   <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" | ||||
|                   > | ||||
|                     {session ? "Sign out" : "Sign in"} | ||||
|                   </Link> | ||||
|                 ) : ( | ||||
|                   <div className="flex h-10 items-center justify-center"> | ||||
|                     <svg | ||||
|                       className="h-6 w-6 animate-spin text-white/70" | ||||
|                       xmlns="http://www.w3.org/2000/svg" | ||||
|                       fill="none" | ||||
|                       viewBox="0 0 24 24" | ||||
|                     > | ||||
|                       <circle | ||||
|                         className="opacity-25" | ||||
|                         cx="12" | ||||
|                         cy="12" | ||||
|                         r="10" | ||||
|                         stroke="currentColor" | ||||
|                         strokeWidth="4" | ||||
|                       /> | ||||
|                       <path | ||||
|                         className="opacity-75" | ||||
|                         fill="currentColor" | ||||
|                         d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" | ||||
|                       /> | ||||
|                     </svg> | ||||
|                   </div> | ||||
|                 )} | ||||
|               </div> | ||||
|             </div> | ||||
|           )} | ||||
|         </div> | ||||
|       </main> | ||||
|     </HydrateClient> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export default Home; | ||||
|  | ||||
| @ -16,23 +16,42 @@ const LoadingSkeleton: React.FC = () => ( | ||||
|           <span className="text-[hsl(280,100%,70%)]">File</span> Details | ||||
|         </h1> | ||||
|         <div className="mt-6"> | ||||
|           {" Loading..."} | ||||
|           <svg | ||||
|                       className="h-6 w-6 animate-spin text-white/70" | ||||
|                       xmlns="http://www.w3.org/2000/svg" | ||||
|                       fill="none" | ||||
|                       viewBox="0 0 24 24" | ||||
|                     > | ||||
|                       <circle | ||||
|                         className="opacity-25" | ||||
|                         cx="12" | ||||
|                         cy="12" | ||||
|                         r="10" | ||||
|                         stroke="currentColor" | ||||
|                         strokeWidth="4" | ||||
|                       /> | ||||
|                       <path | ||||
|                         className="opacity-75" | ||||
|                         fill="currentColor" | ||||
|                         d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" | ||||
|                       /> | ||||
|                     </svg> | ||||
|         </div> | ||||
|         <div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md"> | ||||
|           <p> | ||||
|             <strong>Name:</strong>{" Loading..."} | ||||
|             <strong>Name:</strong> <span className="inline-block h-6 w-24 rounded bg-white/20 animate-pulse align-middle ml-2" /> | ||||
|           </p> | ||||
|           <p> | ||||
|             <strong>Size:</strong>{" Loading..."} | ||||
|             <strong>Size:</strong> <span className="inline-block h-6 w-16 rounded bg-white/20 animate-pulse align-middle ml-2" /> | ||||
|           </p> | ||||
|           <p> | ||||
|             <strong>Owner:</strong>{" Loading..."} | ||||
|             <strong>Owner:</strong> <span className="inline-block h-6 w-20 rounded bg-white/20 animate-pulse align-middle ml-2" /> | ||||
|           </p> | ||||
|           <p> | ||||
|             <strong>Upload Date:</strong>{" Loading..."} | ||||
|             <strong>Upload Date:</strong> <span className="inline-block h-6 w-28 rounded bg-white/20 animate-pulse align-middle ml-2" /> | ||||
|           </p> | ||||
|           <div> | ||||
|             <strong>Description:</strong>{" Loading..."} | ||||
|             <strong>Description:</strong> <span className="inline-block h-6 w-40 rounded bg-white/20 animate-pulse align-middle ml-2" /> | ||||
|           </div> | ||||
|           <div className="mt-4 flex justify-center"> | ||||
|             <FileActionsContainer | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import { notFound } from "next/navigation"; | ||||
| import { Suspense } from "react"; | ||||
| import { FilePreview } from "~/app/_components/FilePreview"; | ||||
| import { HomeButton } from "~/app/_components/HomeButton"; // Import the client component
 | ||||
| import { Toaster } from "react-hot-toast"; | ||||
| @ -129,7 +130,9 @@ export default async function FilePreviewContainer({ | ||||
|         </h1> | ||||
|         <div className="mt-6"> | ||||
|           {fileDetails.type !== "unknown" && ( | ||||
|             <FilePreview fileId={fileDetails.id} fileType={fileDetails.type} share={true} /> | ||||
|             <Suspense fallback={<div className="text-white">Loading...</div>}> | ||||
|               <FilePreview fileId={fileDetails.id} fileType={fileDetails.type} share={true} /> | ||||
|             </Suspense> | ||||
|           )} | ||||
|         </div> | ||||
|         <div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md"> | ||||
| @ -148,32 +151,40 @@ export default async function FilePreviewContainer({ | ||||
|           </p> | ||||
|           <p> | ||||
|             <strong>Owner:</strong>{" "} | ||||
|             <img | ||||
|             <Suspense fallback={<div className="text-white">Loading...</div>}> | ||||
|               <img | ||||
|               className="inline size-5 rounded-md" | ||||
|               src={fileDetails.ownerAvatar || ""} | ||||
|               alt="Owner avatar" | ||||
|             />{" "} | ||||
|               />{" "} | ||||
|             {fileDetails.owner} | ||||
|             </Suspense> | ||||
|           </p> | ||||
|           <p> | ||||
|             <strong>Upload Date:</strong>{" "} | ||||
|             {new Date(fileDetails.uploadDate).toLocaleString()} | ||||
|             <Suspense fallback={<div className="text-white">Loading...</div>}> | ||||
|               {new Date(fileDetails.uploadDate).toLocaleString()} | ||||
|             </Suspense> | ||||
|           </p> | ||||
|           <div> | ||||
|             <strong>Description:</strong>{" "} | ||||
|             <FileDescriptionContainer | ||||
|               fileId={fileDetails.id} | ||||
|               fileDescription={fileDetails.description} | ||||
|             /> | ||||
|             <Suspense fallback={<div className="text-white">Loading...</div>}> | ||||
|               <FileDescriptionContainer | ||||
|                 fileId={fileDetails.id} | ||||
|                 fileDescription={fileDetails.description} | ||||
|               /> | ||||
|             </Suspense> | ||||
|           </div> | ||||
|           <div className="mt-4 flex justify-center"> | ||||
|             <FileActionsContainer | ||||
|               fileId={fileDetails.id} | ||||
|               fileName={fileDetails.name} | ||||
|               fileUrl={fileDetails.url} | ||||
|               isOwner={session?.user?.id ? await checkOwner(fileDetails.ownerId, session.user.id) : false} | ||||
|               isPublic={fileDetails.isPublic} | ||||
|             /> | ||||
|             <Suspense fallback={<div className="text-white">Loading...</div>}> | ||||
|               <FileActionsContainer | ||||
|                 fileId={fileDetails.id} | ||||
|                 fileName={fileDetails.name} | ||||
|                 fileUrl={fileDetails.url} | ||||
|                 isOwner={session?.user?.id ? await checkOwner(fileDetails.ownerId, session.user.id) : false} | ||||
|                 isPublic={fileDetails.isPublic} | ||||
|               /> | ||||
|             </Suspense> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
| @ -1,32 +1,48 @@ | ||||
| // This function takes a file name as input and returns the file type based on its extension.
 | ||||
| import mime from "mime-types"; | ||||
| 
 | ||||
| export function getFileType(fileName: string): string { | ||||
|     const extension = fileName.split(".").pop()?.toLowerCase(); | ||||
|     const fileTypes: Record<string, string> = { | ||||
|         // Video
 | ||||
|         "mp4": "video/mp4", | ||||
|         "webm": "video/webm", | ||||
|         "ogg": "video/ogg", | ||||
|         // Image
 | ||||
|         "jpg": "image/jpeg", | ||||
|         "jpeg": "image/jpeg", | ||||
|         "png": "image/png", | ||||
|         "gif": "image/gif", | ||||
|         "svg": "image/svg+xml", | ||||
|         // Audio
 | ||||
|         "mp3": "audio/mpeg", | ||||
|         "wav": "audio/wav", | ||||
|         // Archive
 | ||||
|         "zip": "archive/zip", | ||||
|         "rar": "archive/rar", | ||||
|         "jar": "archive/jar", | ||||
|         "iso": "archive/iso", | ||||
|         // Text
 | ||||
|         "pdf": "text/pdf", | ||||
|         "txt": "text/plain", | ||||
|         // Code
 | ||||
|         "c": "code/c", | ||||
|         "cpp": "code/cpp", | ||||
|         "py": "code/python", | ||||
|         "js": "code/javascript", | ||||
|         "html": "code/html", | ||||
|         "css": "code/css", | ||||
|         "md": "markdown/markdown", | ||||
|         "json": "code/json", | ||||
|         "xml": "code/xml", | ||||
|         "csv": "code/csv", | ||||
|         // Markdown
 | ||||
|         "md": "markdown/markdown", | ||||
|         // Applications
 | ||||
|         "exe": "application/executable", | ||||
|         "apk": "application/android", | ||||
|     }; | ||||
|     return extension ? fileTypes[extension] || "unknown" : "unknown"; | ||||
|     return extension ? fileTypes[extension] || | ||||
|     //get the file type using the mime type library
 | ||||
|     mime.lookup(extension) || "application/octet-stream" : "application/octet-stream"; | ||||
| 
 | ||||
|   }; | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user