Fuck git
This commit is contained in:
		
							parent
							
								
									c9274a0caa
								
							
						
					
					
						commit
						85fa1942e9
					
				
							
								
								
									
										16
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										16
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -18,6 +18,7 @@ | |||||||
|         "@trpc/client": "^11.0.0", |         "@trpc/client": "^11.0.0", | ||||||
|         "@trpc/react-query": "^11.0.0", |         "@trpc/react-query": "^11.0.0", | ||||||
|         "@trpc/server": "^11.0.0", |         "@trpc/server": "^11.0.0", | ||||||
|  |         "cuid": "^3.0.0", | ||||||
|         "dompurify": "^3.2.5", |         "dompurify": "^3.2.5", | ||||||
|         "github-markdown-css": "^5.8.1", |         "github-markdown-css": "^5.8.1", | ||||||
|         "gray-matter": "^4.0.3", |         "gray-matter": "^4.0.3", | ||||||
| @ -41,6 +42,7 @@ | |||||||
|         "@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/busboy": "^1.5.4", | ||||||
|  |         "@types/mime-types": "^2.1.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", | ||||||
| @ -2317,6 +2319,13 @@ | |||||||
|         "@types/unist": "*" |         "@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": { |     "node_modules/@types/ms": { | ||||||
|       "version": "2.1.0", |       "version": "2.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", |       "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", | ||||||
| @ -3524,6 +3533,13 @@ | |||||||
|       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", |       "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", | ||||||
|       "license": "MIT" |       "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": { |     "node_modules/cytoscape": { | ||||||
|       "version": "3.32.0", |       "version": "3.32.0", | ||||||
|       "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.0.tgz", |       "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.0.tgz", | ||||||
|  | |||||||
| @ -30,6 +30,7 @@ | |||||||
|     "@trpc/client": "^11.0.0", |     "@trpc/client": "^11.0.0", | ||||||
|     "@trpc/react-query": "^11.0.0", |     "@trpc/react-query": "^11.0.0", | ||||||
|     "@trpc/server": "^11.0.0", |     "@trpc/server": "^11.0.0", | ||||||
|  |     "cuid": "^3.0.0", | ||||||
|     "dompurify": "^3.2.5", |     "dompurify": "^3.2.5", | ||||||
|     "github-markdown-css": "^5.8.1", |     "github-markdown-css": "^5.8.1", | ||||||
|     "gray-matter": "^4.0.3", |     "gray-matter": "^4.0.3", | ||||||
| @ -53,6 +54,7 @@ | |||||||
|     "@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/busboy": "^1.5.4", | ||||||
|  |     "@types/mime-types": "^2.1.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", | ||||||
|  | |||||||
| @ -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 = () => ( | const LoadingSkeleton: React.FC = () => ( | ||||||
|   <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 animate-pulse"> |     <main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> | ||||||
|     {/* Title Skeleton */} |       <div className="absolute top-4 left-4"> | ||||||
|     <div className="h-16 w-80 rounded bg-white/20 mb-4" /> |          <HomeButton /> | ||||||
|     {/* FileGrid Skeleton */} |       </div> | ||||||
|     <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 w-full max-w-4xl"> |       <Toaster position="top-right" reverseOrder={false} /> | ||||||
|       {[...Array(6)].map((_, i) => ( |       <div className="container flex flex-col items-center gap-12 px-4 py-16"> | ||||||
|         <div key={i} className="h-32 rounded bg-white/10" /> |         <h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> | ||||||
|       ))} |           <span className="text-[hsl(280,100%,70%)]">File</span> Details | ||||||
|     </div> |         </h1> | ||||||
|     {/* UploadForm Skeleton */} |         <div className="mt-6"> | ||||||
|     <div className="mt-8 w-full max-w-md flex flex-col gap-4"> |           <svg | ||||||
|       <div className="h-10 rounded bg-white/20" /> |                       className="h-6 w-6 animate-spin text-white/70" | ||||||
|       <div className="h-10 rounded bg-white/10" /> |                       xmlns="http://www.w3.org/2000/svg" | ||||||
|     </div> |                       fill="none" | ||||||
|   </div> |                       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; | export default LoadingSkeleton; | ||||||
| @ -59,7 +59,7 @@ export function FileActionsContainer({ | |||||||
|           console.error(err); |           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" /> |         <img src="/icons/delete.svg" alt="Remove" className="h-6 w-6" /> | ||||||
|       </button> |       </button> | ||||||
|  | |||||||
| @ -1,13 +1,11 @@ | |||||||
| "use client"; | "use client"; | ||||||
| 
 | 
 | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import toast from "react-hot-toast"; |  | ||||||
| import { useRouter } from "next/navigation"; | import { useRouter } from "next/navigation"; | ||||||
| import { env } from "~/env.js"; | import { env } from "~/env.js"; | ||||||
| import { FilePreview } from "~/app/_components/FilePreview"; | import { FilePreview } from "~/app/_components/FilePreview"; | ||||||
| import { useFileActions } from "~/app/_components/FileActions"; | import { useFileActions } from "~/app/_components/FileActions"; | ||||||
| import { FileActionsContainer } from "./ActionButtons"; | import { FileActionsContainer } from "./ActionButtons"; | ||||||
| import { checkOwner } from "~/utils/checkOwner"; // Import the client component
 |  | ||||||
| 
 | 
 | ||||||
| interface FileDetails { | interface FileDetails { | ||||||
|   id: string; |   id: string; | ||||||
| @ -73,13 +71,15 @@ export default function FileGrid({ session }: FileGridProps) { | |||||||
| 
 | 
 | ||||||
|     const eventSource = new EventSource("/api/files/stream"); |     const eventSource = new EventSource("/api/files/stream"); | ||||||
|     eventSource.onmessage = (event) => { |     eventSource.onmessage = (event) => { | ||||||
|       const data: { type: string; file?: FileDetails; fileId?: string } = JSON.parse(event.data); |       const data: { type: string; fileId?: string } = JSON.parse(event.data); | ||||||
| 
 |       console.log("SSE event:", data); | ||||||
|       if (data.type === "file-added" && data.file) { |       if (data.type === "file-added" && data.fileId) { | ||||||
|         setFiles((prevFiles) => (data.file ? [...prevFiles, data.file] : prevFiles)); |         fetchFiles(); | ||||||
|         toast.success(`File "${data.file.name}" added!`); |       } else if (data.type === "file-updated" && data.fileId) { | ||||||
|  |         // Fetch the updated file details
 | ||||||
|  |         fetchFiles(); | ||||||
|       } else if (data.type === "file-removed" && data.fileId) { |       } else if (data.type === "file-removed" && data.fileId) { | ||||||
|         setFiles((prevFiles) => prevFiles.filter((file) => file.id !== data.fileId)); |         setFiles((prevFiles => prevFiles.filter(file => file.id !== data.fileId))); | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										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 [uploadedFileUrl, setUploadedFileUrl] = useState<string | null>(null); | ||||||
|   const [progress, setProgress] = useState<number>(0); // Track upload progress
 |   const [progress, setProgress] = useState<number>(0); // Track upload progress
 | ||||||
|   const fileInputRef = useRef<HTMLInputElement | null>(null); // Ref for the file input
 |   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>) => { |   const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|     if (e.target.files) { |     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 () => { |   const handleUpload = async () => { | ||||||
|     if (!file) return toast.error("Please select a file to upload."); |     if (!file) return toast.error("Please select a file to upload."); | ||||||
|     setUploading(true); |     setUploading(true); | ||||||
| @ -41,15 +70,14 @@ export default function UploadForm() { | |||||||
| 
 | 
 | ||||||
|       xhr.onload = () => { |       xhr.onload = () => { | ||||||
|         if (xhr.status === 200) { |         if (xhr.status === 200) { | ||||||
|           const response: { url: string } = JSON.parse(xhr.responseText); // Explicitly type the response
 |           const response = JSON.parse(xhr.responseText); | ||||||
|           setUploadedFileUrl(response.url); // Assume the API returns the uploaded file URL
 |           setUploadedFileUrl(response.file?.url || null); // Use the new response structure
 | ||||||
|           notifyClients({type: "file-uploaded", fileUrl: response.url}); // Notify other clients about the new file
 |  | ||||||
|           toast.success("File uploaded successfully!"); |           toast.success("File uploaded successfully!"); | ||||||
| 
 | 
 | ||||||
|           // Clear the file input and reset state
 |           // Clear the file input and reset state
 | ||||||
|           setFile(null); |           setFile(null); | ||||||
|           if (fileInputRef.current) { |           if (fileInputRef.current) { | ||||||
|             fileInputRef.current.value = ""; // Clear the file input
 |             fileInputRef.current.value = ""; | ||||||
|           } |           } | ||||||
|         } else { |         } else { | ||||||
|           console.error("Upload failed:", xhr.responseText); |           console.error("Upload failed:", xhr.responseText); | ||||||
| @ -86,42 +114,48 @@ export default function UploadForm() { | |||||||
|       {/* Toast container */} |       {/* Toast container */} | ||||||
|       <Toaster position="top-right" reverseOrder={false} /> |       <Toaster position="top-right" reverseOrder={false} /> | ||||||
| 
 | 
 | ||||||
|       <div className="flex flex-row items-center gap-4"> |       {/* Drag and Drop Area */} | ||||||
|         {/* Custom file input */} |       <div | ||||||
|         <label |         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"}`} | ||||||
|           htmlFor="file-upload" |         onDragOver={handleDragOver} | ||||||
|           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" |         onDragLeave={handleDragLeave} | ||||||
|         > |         onDrop={handleDrop} | ||||||
|           {file ? ( |         onClick={() => fileInputRef.current?.click()} | ||||||
|             <> |         style={{ cursor: "pointer" }} | ||||||
|               File Selected |       > | ||||||
|               {/* SVG Icon */} |         {/* Hidden file input for click-to-select */} | ||||||
|               <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 |         <input | ||||||
|           id="file-upload" |  | ||||||
|           ref={fileInputRef} // Attach the ref to the file input
 |  | ||||||
|           type="file" |           type="file" | ||||||
|  |           ref={fileInputRef} | ||||||
|  |           style={{ display: "none" }} | ||||||
|           onChange={handleFileChange} |           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 |         <button | ||||||
|           onClick={handleUpload} |           onClick={handleUpload} | ||||||
|           disabled={uploading || !file} |           disabled={uploading || !file} | ||||||
| @ -129,7 +163,7 @@ export default function UploadForm() { | |||||||
|         > |         > | ||||||
|           {uploading ? "Uploading..." : "Upload"} |           {uploading ? "Uploading..." : "Upload"} | ||||||
|         </button> |         </button> | ||||||
|       </div> |       </div>)} | ||||||
| 
 | 
 | ||||||
|       {file && uploading && ( |       {file && uploading && ( | ||||||
|         <div className="w-full max-w-md flex items-center gap-2"> |         <div className="w-full max-w-md flex items-center gap-2"> | ||||||
| @ -142,31 +176,17 @@ export default function UploadForm() { | |||||||
|         </div> |         </div> | ||||||
|       )} |       )} | ||||||
| 
 | 
 | ||||||
|       {uploadedFileUrl && ( |       {/* {uploadedFileUrl && file && ( | ||||||
|         <div className="flex flex-row items-center gap-4"> |         <div className="flex flex-row items-center gap-4"> | ||||||
|           <p className="text-white">{uploadedFileUrl}</p> |           <p className="text-white">{file.name}</p> | ||||||
|           <button |           <button | ||||||
|             onClick={handleCopyUrl} |             onClick={handleCopyUrl} | ||||||
|             className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600" |             className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600" | ||||||
|           > |           > | ||||||
|             {/* Copy Icon */} |             <img src="/icons/copy.svg" alt="Copy URL" className="h-6 w-6" /> | ||||||
|             <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> |           </button> | ||||||
|         </div> |         </div> | ||||||
|       )} |       )} */} | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
| @ -1,11 +1,12 @@ | |||||||
| import { NextResponse } from "next/server"; | import { NextResponse } from "next/server"; | ||||||
| import Busboy from "busboy"; | import Busboy from "busboy"; | ||||||
| import { Readable } from "stream"; | import { Readable } from "stream"; | ||||||
| import crypto from "crypto"; |  | ||||||
| import { db } from "~/server/db"; | import { db } from "~/server/db"; | ||||||
| import { auth } from "~/server/auth"; | import { auth } from "~/server/auth"; | ||||||
| import { minioClient, ensureBucketExists } from "~/utils/minioClient"; | import { minioClient, ensureBucketExists } from "~/utils/minioClient"; | ||||||
| import { getFileType } from "~/utils/fileType"; | import { getFileType } from "~/utils/fileType"; | ||||||
|  | import cuid from 'cuid'; | ||||||
|  | import { notifyClients } from "~/utils/notifyClients"; | ||||||
| 
 | 
 | ||||||
| export const config = { | export const config = { | ||||||
|   api: { |   api: { | ||||||
| @ -23,7 +24,9 @@ export async function POST(req: Request) { | |||||||
|   await ensureBucketExists(bucketName); |   await ensureBucketExists(bucketName); | ||||||
| 
 | 
 | ||||||
|   return new Promise<Response>((resolve, reject) => { |   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 fileName = ""; | ||||||
|     let fileBuffer = Buffer.alloc(0); |     let fileBuffer = Buffer.alloc(0); | ||||||
| 
 | 
 | ||||||
| @ -39,8 +42,11 @@ export async function POST(req: Request) { | |||||||
|         fileBuffer = Buffer.concat(chunks); |         fileBuffer = Buffer.concat(chunks); | ||||||
| 
 | 
 | ||||||
|         // Generate a unique ID for the file
 |         // Generate a unique ID for the file
 | ||||||
|         const fileId = crypto.randomUUID(); |         const fileId = session.user.id + "-" + cuid() | ||||||
|         const objectName = `${fileId}-${fileName}`; |         const objectName = `${fileId}-${fileName}`; | ||||||
|  |         // Change UUID to CUID
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|         try { |         try { | ||||||
|           // Upload the file to MinIO
 |           // Upload the file to MinIO
 | ||||||
| @ -57,8 +63,15 @@ export async function POST(req: Request) { | |||||||
|               uploadedById: session.user.id, |               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) { |         } catch (error) { | ||||||
|           console.error("Error uploading file to MinIO:", error); |           console.error("Error uploading file to MinIO:", error); | ||||||
|           reject(new Error("Failed to upload file")); |           reject(new Error("Failed to upload file")); | ||||||
| @ -87,4 +100,4 @@ export async function POST(req: Request) { | |||||||
|       nodeStream.pipe(busboy); |       nodeStream.pipe(busboy); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										100
									
								
								src/app/page.tsx
									
									
									
									
									
								
							
							
						
						
									
										100
									
								
								src/app/page.tsx
									
									
									
									
									
								
							| @ -1,16 +1,53 @@ | |||||||
|  | "use client"; | ||||||
|  | 
 | ||||||
| import Link from "next/link"; | import Link from "next/link"; | ||||||
| import { auth } from "~/server/auth"; | import { useEffect, useState } from "react"; | ||||||
| import { HydrateClient } from "~/trpc/server"; |  | ||||||
| import FileGrid from "~/app/_components/FileGrid"; | import FileGrid from "~/app/_components/FileGrid"; | ||||||
| import UploadForm from "~/app/_components/UploadForm"; | import UploadForm from "~/app/_components/UploadForm"; | ||||||
| import { Toaster } from "react-hot-toast"; | import { Toaster } from "react-hot-toast"; | ||||||
| import { Suspense } from "react"; | import { Suspense } from "react"; | ||||||
|  | import LoadingSkeleton from "./LoadingSkeleton"; | ||||||
| 
 | 
 | ||||||
| export default async function Home() { | // Custom fallback for FileGrid
 | ||||||
|   const session = await auth(); | 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 ( |   return ( | ||||||
|     <HydrateClient> |     <> | ||||||
|       <Toaster position="top-right" reverseOrder={false} /> |       <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"> |       <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 */} |         {/* 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 */} |           {/* Conditionally render FileGrid and UploadForm if the user is logged in */} | ||||||
|           {session?.user ? ( |           {session?.user ? ( | ||||||
|             <> |             <> | ||||||
|               <Suspense fallback={<p className="text-center text-2xl text-white">Loading...</p>}> |               <Suspense fallback={<FileGridFallback />}> | ||||||
|                 <FileGrid session={session} /> |                 <FileGrid session={session as { user: { id: string } }} /> | ||||||
|  |               </Suspense> | ||||||
|  |               <Suspense fallback={<UploadFormFallback />}> | ||||||
|  |                 <UploadForm /> | ||||||
|               </Suspense> |               </Suspense> | ||||||
|               <UploadForm /> |  | ||||||
|             </> |             </> | ||||||
|           ) : ( |           ) : !loading ? ( | ||||||
|             <p className="text-center text-2xl text-white"> |             <p className="text-center text-2xl text-white"> | ||||||
|               Please log in to upload and view files. |               Please log in to upload and view files. | ||||||
|             </p> |             </p> | ||||||
|           )} |           ) : null} | ||||||
|           {!session?.user && ( |           {!session?.user && ( | ||||||
|             <div className="flex flex-col items-center gap-2"> |             <div className="flex flex-col items-center gap-2"> | ||||||
|               <div className="flex flex-col items-center justify-center gap-4"> |               <div className="flex flex-col items-center justify-center gap-4"> | ||||||
|                 <Link |                 {!loading ? ( | ||||||
|                   href={session ? "/api/auth/signout" : "/api/auth/signin"} |                   <Link | ||||||
|                   className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20" |                     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> |                     {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> |             </div> | ||||||
|           )} |           )} | ||||||
|         </div> |         </div> | ||||||
|       </main> |       </main> | ||||||
|     </HydrateClient> |     </> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export default Home; | ||||||
|  | |||||||
| @ -16,23 +16,42 @@ const LoadingSkeleton: React.FC = () => ( | |||||||
|           <span className="text-[hsl(280,100%,70%)]">File</span> Details |           <span className="text-[hsl(280,100%,70%)]">File</span> Details | ||||||
|         </h1> |         </h1> | ||||||
|         <div className="mt-6"> |         <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> | ||||||
|         <div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md"> |         <div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md"> | ||||||
|           <p> |           <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> | ||||||
|           <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> | ||||||
|           <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> | ||||||
|           <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> |           </p> | ||||||
|           <div> |           <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> | ||||||
|           <div className="mt-4 flex justify-center"> |           <div className="mt-4 flex justify-center"> | ||||||
|             <FileActionsContainer |             <FileActionsContainer | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import { notFound } from "next/navigation"; | import { notFound } from "next/navigation"; | ||||||
|  | import { Suspense } from "react"; | ||||||
| import { FilePreview } from "~/app/_components/FilePreview"; | import { FilePreview } from "~/app/_components/FilePreview"; | ||||||
| import { HomeButton } from "~/app/_components/HomeButton"; // Import the client component
 | import { HomeButton } from "~/app/_components/HomeButton"; // Import the client component
 | ||||||
| import { Toaster } from "react-hot-toast"; | import { Toaster } from "react-hot-toast"; | ||||||
| @ -129,7 +130,9 @@ export default async function FilePreviewContainer({ | |||||||
|         </h1> |         </h1> | ||||||
|         <div className="mt-6"> |         <div className="mt-6"> | ||||||
|           {fileDetails.type !== "unknown" && ( |           {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> | ||||||
|         <div className="w-full max-w-md rounded-lg bg-white/10 p-6 text-white shadow-md"> |         <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> | ||||||
|           <p> |           <p> | ||||||
|             <strong>Owner:</strong>{" "} |             <strong>Owner:</strong>{" "} | ||||||
|             <img |             <Suspense fallback={<div className="text-white">Loading...</div>}> | ||||||
|  |               <img | ||||||
|               className="inline size-5 rounded-md" |               className="inline size-5 rounded-md" | ||||||
|               src={fileDetails.ownerAvatar || ""} |               src={fileDetails.ownerAvatar || ""} | ||||||
|               alt="Owner avatar" |               alt="Owner avatar" | ||||||
|             />{" "} |               />{" "} | ||||||
|             {fileDetails.owner} |             {fileDetails.owner} | ||||||
|  |             </Suspense> | ||||||
|           </p> |           </p> | ||||||
|           <p> |           <p> | ||||||
|             <strong>Upload Date:</strong>{" "} |             <strong>Upload Date:</strong>{" "} | ||||||
|             {new Date(fileDetails.uploadDate).toLocaleString()} |             <Suspense fallback={<div className="text-white">Loading...</div>}> | ||||||
|  |               {new Date(fileDetails.uploadDate).toLocaleString()} | ||||||
|  |             </Suspense> | ||||||
|           </p> |           </p> | ||||||
|           <div> |           <div> | ||||||
|             <strong>Description:</strong>{" "} |             <strong>Description:</strong>{" "} | ||||||
|             <FileDescriptionContainer |             <Suspense fallback={<div className="text-white">Loading...</div>}> | ||||||
|               fileId={fileDetails.id} |               <FileDescriptionContainer | ||||||
|               fileDescription={fileDetails.description} |                 fileId={fileDetails.id} | ||||||
|             /> |                 fileDescription={fileDetails.description} | ||||||
|  |               /> | ||||||
|  |             </Suspense> | ||||||
|           </div> |           </div> | ||||||
|           <div className="mt-4 flex justify-center"> |           <div className="mt-4 flex justify-center"> | ||||||
|             <FileActionsContainer |             <Suspense fallback={<div className="text-white">Loading...</div>}> | ||||||
|               fileId={fileDetails.id} |               <FileActionsContainer | ||||||
|               fileName={fileDetails.name} |                 fileId={fileDetails.id} | ||||||
|               fileUrl={fileDetails.url} |                 fileName={fileDetails.name} | ||||||
|               isOwner={session?.user?.id ? await checkOwner(fileDetails.ownerId, session.user.id) : false} |                 fileUrl={fileDetails.url} | ||||||
|               isPublic={fileDetails.isPublic} |                 isOwner={session?.user?.id ? await checkOwner(fileDetails.ownerId, session.user.id) : false} | ||||||
|             /> |                 isPublic={fileDetails.isPublic} | ||||||
|  |               /> | ||||||
|  |             </Suspense> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  | |||||||
| @ -1,32 +1,48 @@ | |||||||
| // This function takes a file name as input and returns the file type based on its extension.
 | // 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 { | export function getFileType(fileName: string): string { | ||||||
|     const extension = fileName.split(".").pop()?.toLowerCase(); |     const extension = fileName.split(".").pop()?.toLowerCase(); | ||||||
|     const fileTypes: Record<string, string> = { |     const fileTypes: Record<string, string> = { | ||||||
|  |         // Video
 | ||||||
|         "mp4": "video/mp4", |         "mp4": "video/mp4", | ||||||
|         "webm": "video/webm", |         "webm": "video/webm", | ||||||
|         "ogg": "video/ogg", |         "ogg": "video/ogg", | ||||||
|  |         // Image
 | ||||||
|         "jpg": "image/jpeg", |         "jpg": "image/jpeg", | ||||||
|         "jpeg": "image/jpeg", |         "jpeg": "image/jpeg", | ||||||
|         "png": "image/png", |         "png": "image/png", | ||||||
|         "gif": "image/gif", |         "gif": "image/gif", | ||||||
|         "svg": "image/svg+xml", |         "svg": "image/svg+xml", | ||||||
|  |         // Audio
 | ||||||
|         "mp3": "audio/mpeg", |         "mp3": "audio/mpeg", | ||||||
|         "wav": "audio/wav", |         "wav": "audio/wav", | ||||||
|  |         // Archive
 | ||||||
|         "zip": "archive/zip", |         "zip": "archive/zip", | ||||||
|         "rar": "archive/rar", |         "rar": "archive/rar", | ||||||
|  |         "jar": "archive/jar", | ||||||
|  |         "iso": "archive/iso", | ||||||
|  |         // Text
 | ||||||
|         "pdf": "text/pdf", |         "pdf": "text/pdf", | ||||||
|         "txt": "text/plain", |         "txt": "text/plain", | ||||||
|  |         // Code
 | ||||||
|         "c": "code/c", |         "c": "code/c", | ||||||
|         "cpp": "code/cpp", |         "cpp": "code/cpp", | ||||||
|         "py": "code/python", |         "py": "code/python", | ||||||
|         "js": "code/javascript", |         "js": "code/javascript", | ||||||
|         "html": "code/html", |         "html": "code/html", | ||||||
|         "css": "code/css", |         "css": "code/css", | ||||||
|         "md": "markdown/markdown", |  | ||||||
|         "json": "code/json", |         "json": "code/json", | ||||||
|         "xml": "code/xml", |         "xml": "code/xml", | ||||||
|         "csv": "code/csv", |         "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