Compare commits

..

No commits in common. "7aeae9020d8b6b50aa5920bdeb7e5c06b6262b33" and "c9274a0caa860b590f3109f96670cc42efbf3518" have entirely different histories.

12 changed files with 129 additions and 386 deletions

16
package-lock.json generated
View File

@ -18,7 +18,6 @@
"@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",
@ -42,7 +41,6 @@
"@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",
@ -2319,13 +2317,6 @@
"@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",
@ -3533,13 +3524,6 @@
"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",

View File

@ -30,7 +30,6 @@
"@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",
@ -54,7 +53,6 @@
"@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",

View File

@ -1,70 +1,21 @@
import React, { Suspense } from "react"; import React 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: React.FC = () => ( const LoadingSkeleton = () => (
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white"> <div className="container flex flex-col items-center justify-center gap-12 px-4 py-16 animate-pulse">
<div className="absolute top-4 left-4"> {/* Title Skeleton */}
<HomeButton /> <div className="h-16 w-80 rounded bg-white/20 mb-4" />
</div> {/* FileGrid Skeleton */}
<Toaster position="top-right" reverseOrder={false} /> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 w-full max-w-4xl">
<div className="container flex flex-col items-center gap-12 px-4 py-16"> {[...Array(6)].map((_, i) => (
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]"> <div key={i} className="h-32 rounded bg-white/10" />
<span className="text-[hsl(280,100%,70%)]">File</span> Details ))}
</h1> </div>
<div className="mt-6"> {/* UploadForm Skeleton */}
<svg <div className="mt-8 w-full max-w-md flex flex-col gap-4">
className="h-6 w-6 animate-spin text-white/70" <div className="h-10 rounded bg-white/20" />
xmlns="http://www.w3.org/2000/svg" <div className="h-10 rounded bg-white/10" />
fill="none" </div>
viewBox="0 0 24 24" </div>
>
<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;

View File

@ -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-700" className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-600"
> >
<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>

View File

@ -1,11 +1,13 @@
"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;
@ -71,15 +73,13 @@ 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; fileId?: string } = JSON.parse(event.data); const data: { type: string; file?: FileDetails; fileId?: string } = JSON.parse(event.data);
console.log("SSE event:", data);
if (data.type === "file-added" && data.fileId) { if (data.type === "file-added" && data.file) {
fetchFiles(); setFiles((prevFiles) => (data.file ? [...prevFiles, data.file] : prevFiles));
} else if (data.type === "file-updated" && data.fileId) { toast.success(`File "${data.file.name}" added!`);
// 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));
} }
}; };

View File

@ -1,45 +0,0 @@
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,7 +10,6 @@ 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) {
@ -21,34 +20,6 @@ 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);
@ -70,14 +41,15 @@ export default function UploadForm() {
xhr.onload = () => { xhr.onload = () => {
if (xhr.status === 200) { if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText); const response: { url: string } = JSON.parse(xhr.responseText); // Explicitly type the response
setUploadedFileUrl(response.file?.url || null); // Use the new response structure 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
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 = ""; fileInputRef.current.value = ""; // Clear the file input
} }
} else { } else {
console.error("Upload failed:", xhr.responseText); console.error("Upload failed:", xhr.responseText);
@ -114,48 +86,42 @@ export default function UploadForm() {
{/* Toast container */} {/* Toast container */}
<Toaster position="top-right" reverseOrder={false} /> <Toaster position="top-right" reverseOrder={false} />
{/* 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
type="file"
ref={fileInputRef}
style={{ display: "none" }}
onChange={handleFileChange}
/>
<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"> <div className="flex flex-row items-center gap-4">
{/* Custom file input */}
<label
htmlFor="file-upload"
className="cursor-pointer flex items-center gap-2 rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
>
{file ? (
<>
File Selected
{/* SVG Icon */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="h-5 w-5 text-green-500"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 13l4 4L19 7"
/>
</svg>
</>
) : (
"Select File"
)}
</label>
<input
id="file-upload"
ref={fileInputRef} // Attach the ref to the file input
type="file"
onChange={handleFileChange}
className="hidden" // Hide the default file input
/>
<button <button
onClick={handleUpload} onClick={handleUpload}
disabled={uploading || !file} disabled={uploading || !file}
@ -163,7 +129,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">
@ -176,17 +142,31 @@ export default function UploadForm() {
</div> </div>
)} )}
{/* {uploadedFileUrl && file && ( {uploadedFileUrl && (
<div className="flex flex-row items-center gap-4"> <div className="flex flex-row items-center gap-4">
<p className="text-white">{file.name}</p> <p className="text-white">{uploadedFileUrl}</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"
> >
<img src="/icons/copy.svg" alt="Copy URL" className="h-6 w-6" /> {/* Copy Icon */}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="h-5 w-5 text-white"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M8 16h8M8 12h8m-7 8h6a2 2 0 002-2V6a2 2 0 00-2-2H9a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</button> </button>
</div> </div>
)} */} )}
</div> </div>
); );
} }

View File

@ -1,12 +1,11 @@
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: {
@ -24,9 +23,7 @@ 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({ const busboy = Busboy({ headers: { "content-type": req.headers.get("content-type") ?? "" } });
headers: { "content-type": req.headers.get("content-type") ?? "" },
});
let fileName = ""; let fileName = "";
let fileBuffer = Buffer.alloc(0); let fileBuffer = Buffer.alloc(0);
@ -42,11 +39,8 @@ 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 = session.user.id + "-" + cuid() const fileId = crypto.randomUUID();
const objectName = `${fileId}-${fileName}`; const objectName = `${fileId}-${fileName}`;
// Change UUID to CUID
try { try {
// Upload the file to MinIO // Upload the file to MinIO
@ -63,15 +57,8 @@ export async function POST(req: Request) {
uploadedById: session.user.id, uploadedById: session.user.id,
}, },
}); });
notifyClients({ type: "file-added", fileId: fileId });
resolve( resolve(NextResponse.json({ message: "File uploaded successfully", file: newFile }));
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"));
@ -100,4 +87,4 @@ export async function POST(req: Request) {
nodeStream.pipe(busboy); nodeStream.pipe(busboy);
} }
}); });
} }

View File

@ -1,53 +1,16 @@
"use client";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react"; import { auth } from "~/server/auth";
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";
// Custom fallback for FileGrid export default async function Home() {
function FileGridFallback() { const session = await auth();
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 */}
@ -91,59 +54,30 @@ 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={<FileGridFallback />}> <Suspense fallback={<p className="text-center text-2xl text-white">Loading...</p>}>
<FileGrid session={session as { user: { id: string } }} /> <FileGrid session={session} />
</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">
{!loading ? ( <Link
<Link href={session ? "/api/auth/signout" : "/api/auth/signin"}
href={session ? "/api/auth/signout" : "/api/auth/signin"} className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20"
className="rounded-full bg-white/10 px-10 py-3 font-semibold no-underline transition hover:bg-white/20" >
> {session ? "Sign out" : "Sign in"}
{session ? "Sign out" : "Sign in"} </Link>
</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;

View File

@ -16,42 +16,23 @@ 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">
<svg {" Loading..."}
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> <span className="inline-block h-6 w-24 rounded bg-white/20 animate-pulse align-middle ml-2" /> <strong>Name:</strong>{" Loading..."}
</p> </p>
<p> <p>
<strong>Size:</strong> <span className="inline-block h-6 w-16 rounded bg-white/20 animate-pulse align-middle ml-2" /> <strong>Size:</strong>{" Loading..."}
</p> </p>
<p> <p>
<strong>Owner:</strong> <span className="inline-block h-6 w-20 rounded bg-white/20 animate-pulse align-middle ml-2" /> <strong>Owner:</strong>{" Loading..."}
</p> </p>
<p> <p>
<strong>Upload Date:</strong> <span className="inline-block h-6 w-28 rounded bg-white/20 animate-pulse align-middle ml-2" /> <strong>Upload Date:</strong>{" Loading..."}
</p> </p>
<div> <div>
<strong>Description:</strong> <span className="inline-block h-6 w-40 rounded bg-white/20 animate-pulse align-middle ml-2" /> <strong>Description:</strong>{" Loading..."}
</div> </div>
<div className="mt-4 flex justify-center"> <div className="mt-4 flex justify-center">
<FileActionsContainer <FileActionsContainer

View File

@ -1,5 +1,4 @@
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";
@ -130,9 +129,7 @@ export default async function FilePreviewContainer({
</h1> </h1>
<div className="mt-6"> <div className="mt-6">
{fileDetails.type !== "unknown" && ( {fileDetails.type !== "unknown" && (
<Suspense fallback={<div className="text-white">Loading...</div>}> <FilePreview fileId={fileDetails.id} fileType={fileDetails.type} share={true} />
<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">
@ -151,40 +148,32 @@ export default async function FilePreviewContainer({
</p> </p>
<p> <p>
<strong>Owner:</strong>{" "} <strong>Owner:</strong>{" "}
<Suspense fallback={<div className="text-white">Loading...</div>}> <img
<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>{" "}
<Suspense fallback={<div className="text-white">Loading...</div>}> {new Date(fileDetails.uploadDate).toLocaleString()}
{new Date(fileDetails.uploadDate).toLocaleString()}
</Suspense>
</p> </p>
<div> <div>
<strong>Description:</strong>{" "} <strong>Description:</strong>{" "}
<Suspense fallback={<div className="text-white">Loading...</div>}> <FileDescriptionContainer
<FileDescriptionContainer fileId={fileDetails.id}
fileId={fileDetails.id} fileDescription={fileDetails.description}
fileDescription={fileDetails.description} />
/>
</Suspense>
</div> </div>
<div className="mt-4 flex justify-center"> <div className="mt-4 flex justify-center">
<Suspense fallback={<div className="text-white">Loading...</div>}> <FileActionsContainer
<FileActionsContainer fileId={fileDetails.id}
fileId={fileDetails.id} fileName={fileDetails.name}
fileName={fileDetails.name} fileUrl={fileDetails.url}
fileUrl={fileDetails.url} isOwner={session?.user?.id ? await checkOwner(fileDetails.ownerId, session.user.id) : false}
isOwner={session?.user?.id ? await checkOwner(fileDetails.ownerId, session.user.id) : false} isPublic={fileDetails.isPublic}
isPublic={fileDetails.isPublic} />
/>
</Suspense>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,48 +1,32 @@
// 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] || return extension ? fileTypes[extension] || "unknown" : "unknown";
//get the file type using the mime type library
mime.lookup(extension) || "application/octet-stream" : "application/octet-stream";
}; };