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