From 85fa1942e9020432e0c63b8cbee6bb74d7dfb937 Mon Sep 17 00:00:00 2001 From: ZareMate <0.zaremate@gmail.com> Date: Tue, 20 May 2025 13:10:37 +0200 Subject: [PATCH] Fuck git --- package-lock.json | 16 +++ package.json | 2 + src/app/LoadingSkeleton.tsx | 83 ++++++++++++--- src/app/_components/ActionButtons.tsx | 2 +- src/app/_components/FileGrid.tsx | 16 +-- src/app/_components/GenerateMetadata.tsx | 45 ++++++++ src/app/_components/UploadForm.tsx | 130 +++++++++++++---------- src/app/api/upload/route.ts | 23 +++- src/app/page.tsx | 100 ++++++++++++++--- src/app/share/LoadingSkeleton.tsx | 31 ++++-- src/app/share/page.tsx | 41 ++++--- src/utils/fileType.ts | 20 +++- 12 files changed, 383 insertions(+), 126 deletions(-) create mode 100644 src/app/_components/GenerateMetadata.tsx diff --git a/package-lock.json b/package-lock.json index ddf8714..3d30c2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 052bf01..61b4188 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/LoadingSkeleton.tsx b/src/app/LoadingSkeleton.tsx index fdfd7ce..0aeb0eb 100644 --- a/src/app/LoadingSkeleton.tsx +++ b/src/app/LoadingSkeleton.tsx @@ -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 = () => ( -
- {/* Title Skeleton */} -
- {/* FileGrid Skeleton */} -
- {[...Array(6)].map((_, i) => ( -
- ))} -
- {/* UploadForm Skeleton */} -
-
-
-
-
+const LoadingSkeleton: React.FC = () => ( +
+
+ +
+ +
+

+ File Details +

+
+ + + + +
+
+

+ Name: +

+

+ Size: +

+

+ Owner: +

+

+ Upload Date: +

+
+ Description: +
+
+ +
+
+
+
); export default LoadingSkeleton; \ No newline at end of file diff --git a/src/app/_components/ActionButtons.tsx b/src/app/_components/ActionButtons.tsx index ba85138..b62fe95 100644 --- a/src/app/_components/ActionButtons.tsx +++ b/src/app/_components/ActionButtons.tsx @@ -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" > Remove diff --git a/src/app/_components/FileGrid.tsx b/src/app/_components/FileGrid.tsx index 21a23ed..cf3779a 100644 --- a/src/app/_components/FileGrid.tsx +++ b/src/app/_components/FileGrid.tsx @@ -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))); } }; diff --git a/src/app/_components/GenerateMetadata.tsx b/src/app/_components/GenerateMetadata.tsx new file mode 100644 index 0000000..f593917 --- /dev/null +++ b/src/app/_components/GenerateMetadata.tsx @@ -0,0 +1,45 @@ +import type { Metadata } from "next"; + +export async function generateMetadata({ + searchParams, +}: { + searchParams: { id?: string }; +}): Promise { + 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`, + }, + ], + }, + }; +} diff --git a/src/app/_components/UploadForm.tsx b/src/app/_components/UploadForm.tsx index a25dff7..044b234 100644 --- a/src/app/_components/UploadForm.tsx +++ b/src/app/_components/UploadForm.tsx @@ -10,6 +10,7 @@ export default function UploadForm() { const [uploadedFileUrl, setUploadedFileUrl] = useState(null); const [progress, setProgress] = useState(0); // Track upload progress const fileInputRef = useRef(null); // Ref for the file input + const [isDragActive, setIsDragActive] = useState(false); // Track drag state const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files) { @@ -20,6 +21,34 @@ export default function UploadForm() { } }; + // Drag and drop handlers + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragActive(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragActive(false); + }; + + const handleDrop = (e: React.DragEvent) => { + 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 */} -
- {/* Custom file input */} - + {/* Drag and Drop Area */} +
fileInputRef.current?.click()} + style={{ cursor: "pointer" }} + > + {/* Hidden file input for click-to-select */} + + {isDragActive ? "Drop your file here" : "Drag & drop a file here, or click to select"} + + {file && ( +
+ {file.name} + {/* Add button to remove file */} + +
+ )} +
+ {/* Show upload button only when file is selected */} + {file && ( +
-
+
)} {file && uploading && (
@@ -142,31 +176,17 @@ export default function UploadForm() {
)} - {uploadedFileUrl && ( + {/* {uploadedFileUrl && file && (
-

{uploadedFileUrl}

+

{file.name}

- )} + )} */}
); } \ No newline at end of file diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index fe8a512..bddb4a4 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -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((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")); @@ -87,4 +100,4 @@ export async function POST(req: Request) { nodeStream.pipe(busboy); } }); -} \ No newline at end of file +} diff --git a/src/app/page.tsx b/src/app/page.tsx index e76b85e..6df5912 100644 --- a/src/app/page.tsx +++ b/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 ( +
+ {[...Array(6)].map((_, i) => ( +
+ Loading +
+
+ ))} +
+ ); +} + +// Custom fallback for UploadForm +function UploadFormFallback() { + return ( +
+
+
+
+ ); +} + +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 ( - + <>
{/* 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 ? ( <> - Loading...

}> - + }> + + + }> + - - ) : ( + ) : !loading ? (

Please log in to upload and view files.

- )} + ) : null} {!session?.user && (
- - {session ? "Sign out" : "Sign in"} - + {!loading ? ( + + {session ? "Sign out" : "Sign in"} + + ) : ( +
+ + + + +
+ )}
)}
- + ); } + +export default Home; diff --git a/src/app/share/LoadingSkeleton.tsx b/src/app/share/LoadingSkeleton.tsx index dfddfdc..0aeb0eb 100644 --- a/src/app/share/LoadingSkeleton.tsx +++ b/src/app/share/LoadingSkeleton.tsx @@ -16,23 +16,42 @@ const LoadingSkeleton: React.FC = () => ( File Details
- {" Loading..."} + + + +

- Name:{" Loading..."} + Name:

- Size:{" Loading..."} + Size:

- Owner:{" Loading..."} + Owner:

- Upload Date:{" Loading..."} + Upload Date:

- Description:{" Loading..."} + Description:
{fileDetails.type !== "unknown" && ( - + Loading...
}> + + )}
@@ -148,32 +151,40 @@ export default async function FilePreviewContainer({

Owner:{" "} - Loading...

}> + Owner avatar{" "} + />{" "} {fileDetails.owner} +

Upload Date:{" "} - {new Date(fileDetails.uploadDate).toLocaleString()} + Loading...

}> + {new Date(fileDetails.uploadDate).toLocaleString()} +

Description:{" "} - + Loading...
}> + +
- + Loading...
}> + +
diff --git a/src/utils/fileType.ts b/src/utils/fileType.ts index 59b9efc..4487466 100644 --- a/src/utils/fileType.ts +++ b/src/utils/fileType.ts @@ -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 = { + // 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"; + }; \ No newline at end of file