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