Fuck git (#17)

This commit is contained in:
ZareMate 2025-05-20 13:14:01 +02:00 committed by GitHub
commit 7aeae9020d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 383 additions and 126 deletions

16
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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;

View File

@ -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>

View File

@ -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)));
}
};

View 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`,
},
],
},
};
}

View File

@ -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>
);
}

View File

@ -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"));
@ -87,4 +100,4 @@ export async function POST(req: Request) {
nodeStream.pipe(busboy);
}
});
}
}

View File

@ -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;

View File

@ -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

View File

@ -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>

View File

@ -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";
};