Compare commits
No commits in common. "1d302eb2171d85bb335e2d3fd398d72bef125dfc" and "8bc35d979da54d0278440f1ab49881818f159a61" have entirely different histories.
1d302eb217
...
8bc35d979d
@ -1,120 +0,0 @@
|
||||
'use client';
|
||||
import { useRef, useState } from "react";
|
||||
import { useFileActions } from "~/app/_components/FileActions";
|
||||
|
||||
export function FileActionsContainer({
|
||||
fileId,
|
||||
fileName,
|
||||
fileUrl,
|
||||
isOwner,
|
||||
}: {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
fileUrl: string;
|
||||
isOwner: boolean;
|
||||
}) {
|
||||
const { handleDownload, handleCopyUrl, handleRemove} = useFileActions(() => fileId, (description: string) => {
|
||||
if (isOwner) {
|
||||
console.log(description);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex self-center gap-2">
|
||||
{/* Download Button */}
|
||||
<button
|
||||
onClick={() => handleDownload(fileId, fileName)}
|
||||
className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600"
|
||||
>
|
||||
<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="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V3"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Copy URL Button */}
|
||||
<button
|
||||
onClick={() => handleCopyUrl(fileUrl)}
|
||||
className="flex items-center justify-center rounded-full bg-green-500 p-2 hover:bg-green-600"
|
||||
>
|
||||
<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 9h8m-6 4h4m-7 8h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Remove Button */}
|
||||
<button
|
||||
onClick={() => handleRemove(fileId)}
|
||||
className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-600"
|
||||
>
|
||||
<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="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FileDescriptionContainer({
|
||||
fileId,
|
||||
fileDescriprtion,
|
||||
}: {
|
||||
fileId: string;
|
||||
fileDescriprtion?: string;
|
||||
}) {
|
||||
|
||||
const [description, setDescription] = useState(fileDescriprtion || ""); // Add state for description
|
||||
const { handleDescriptionChange } = useFileActions(() => {}, (description: string) => {
|
||||
setDescription(description);
|
||||
return undefined;
|
||||
}, fileId); // Wrap setDescription in a function
|
||||
const debounceTimer = useRef<NodeJS.Timeout | null>(null); // Initialize debounce timer
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
handleDescriptionChange(e, debounceTimer); // Pass the debounce timer
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex self-center gap-2">
|
||||
<textarea
|
||||
className="w-full h-24 p-2 border rounded-md bg-gray-800 text-white"
|
||||
value={description} // Use state value
|
||||
onChange={handleChange}
|
||||
placeholder="Enter file description..."
|
||||
maxLength={200} // Limit to 200 characters
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,13 +1,12 @@
|
||||
import toast from "react-hot-toast";
|
||||
import { env } from "~/env.js";
|
||||
import { notifyClients } from "~/utils/notifyClients";
|
||||
|
||||
export const useFileActions = (
|
||||
setFiles: (callback: (prevFiles: any[]) => any[]) => void,
|
||||
setDescription?: (description: string) => undefined,
|
||||
setDescription?: (description: string) => void,
|
||||
fileId?: string
|
||||
) => {
|
||||
const pageUrl = `${env.NEXT_PUBLIC_PAGE_URL}`;
|
||||
const pageUrl = `${env.NEXT_PUBLIC_PAGE_URL}/share?id=`;
|
||||
|
||||
// Handle file download
|
||||
const handleDownload = async (fileId: string, fileName: string) => {
|
||||
@ -45,25 +44,15 @@ export const useFileActions = (
|
||||
// Remove a file
|
||||
const handleRemove = async (fileId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/files/remove`, {
|
||||
const response = await fetch(`/api/remove`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ id: fileId }),
|
||||
});
|
||||
|
||||
if (response.status === 403) {
|
||||
toast.error("You are not authorized to remove this file.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) throw new Error("Failed to delete file");
|
||||
|
||||
setFiles((prevFiles) => prevFiles.filter((file) => file.id !== fileId));
|
||||
toast.success("File removed successfully!");
|
||||
// Go to the home page after removing the file
|
||||
window.location.href = `${pageUrl}/`;
|
||||
notifyClients({ type: "file-removed", fileId });
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to remove file.");
|
||||
@ -75,9 +64,7 @@ export const useFileActions = (
|
||||
e: React.ChangeEvent<HTMLTextAreaElement>,
|
||||
debounceTimer: React.RefObject<NodeJS.Timeout | null>
|
||||
) => {
|
||||
if (setDescription === undefined) {console.error("setDescription function is not provided")
|
||||
return;
|
||||
};
|
||||
if (!setDescription) return;
|
||||
|
||||
const newDescription = e.target.value;
|
||||
setDescription(newDescription);
|
||||
@ -85,9 +72,7 @@ export const useFileActions = (
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current);
|
||||
}
|
||||
|
||||
debounceTimer.current = setTimeout(() => {
|
||||
console.log("Calling handleDescriptionSave"); // Debug log
|
||||
handleDescriptionSave(newDescription);
|
||||
}, 1000);
|
||||
};
|
||||
@ -100,22 +85,14 @@ export const useFileActions = (
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/files/share?id=${encodeURIComponent(fileId)}`, {
|
||||
const response = await fetch(`/api/share?id=${encodeURIComponent(fileId)}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
// pass the fileId and description in the request body
|
||||
body: JSON.stringify({ description, id: fileId }),
|
||||
body: JSON.stringify({ description }),
|
||||
});
|
||||
|
||||
if (response.status === 403) {
|
||||
toast.error("You are not authorized to modify this file's description.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) throw new Error("Failed to update description");
|
||||
|
||||
toast.success("Description updated successfully!");
|
||||
notifyClients({ type: "file-updated", fileId });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error("Failed to update description.");
|
||||
|
||||
@ -21,7 +21,7 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) {
|
||||
|
||||
const fetchMedia = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/files/serv?id=${encodeURIComponent(fileId)}`);
|
||||
const response = await fetch(`/api/serv?id=${encodeURIComponent(fileId)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch media");
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export function HomeButton() {
|
||||
return (
|
||||
<button
|
||||
onClick={() => (window.location.href = "/")}
|
||||
className="rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20"
|
||||
>
|
||||
Home
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import { notifyClients } from "~/utils/notifyClients";
|
||||
|
||||
export default function UploadForm() {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
@ -43,7 +42,6 @@ export default function UploadForm() {
|
||||
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
|
||||
toast.success("File uploaded successfully!");
|
||||
|
||||
// Clear the file input and reset state
|
||||
|
||||
@ -22,12 +22,8 @@ export async function DELETE(req: Request) {
|
||||
where: { id: body.id },
|
||||
});
|
||||
|
||||
if (!resource) {
|
||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (resource.uploadedById !== session.user.id) {
|
||||
return NextResponse.json({ error: "You are not authorized to delete this file" }, { status: 403 });
|
||||
if (!resource || resource.uploadedById !== session.user.id) {
|
||||
return NextResponse.json({ error: "Resource not found or unauthorized" }, { status: 404 });
|
||||
}
|
||||
|
||||
const filePath = path.join(process.cwd(), "uploads", path.basename(body.id));
|
||||
@ -41,9 +37,9 @@ export async function DELETE(req: Request) {
|
||||
|
||||
notifyClients({ type: "file-removed", fileId: body.id });
|
||||
|
||||
return NextResponse.json({ message: "File deleted successfully" });
|
||||
return NextResponse.json({ message: "Resource deleted successfully" });
|
||||
} catch (error) {
|
||||
console.error("Error deleting file:", error);
|
||||
return NextResponse.json({ error: "Failed to delete file" }, { status: 500 });
|
||||
console.error("Error deleting resource:", error);
|
||||
return NextResponse.json({ error: "Failed to delete resource" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -41,38 +41,23 @@ export async function GET(req: Request) {
|
||||
|
||||
export async function PUT(req: Request) {
|
||||
const session = await auth();
|
||||
const url = new URL(req.url);
|
||||
const fileId = url.searchParams.get("id");
|
||||
const { description = "" } = await req.json();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
if (!fileId) {
|
||||
return NextResponse.json({ error: "File name is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = (await req.json()) as { id: string; description: string } | null;
|
||||
if (!body?.id || body.description === undefined) {
|
||||
// Allow empty description but ensure id is present
|
||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const resource = await db.file.findUnique({
|
||||
where: { id: body.id },
|
||||
const file = await db.file.update({
|
||||
where: { id: fileId },
|
||||
data: { description },
|
||||
});
|
||||
|
||||
if (!resource) {
|
||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (resource.uploadedById !== session.user.id) {
|
||||
return NextResponse.json({ error: "You are not authorized to modify this file" }, { status: 403 });
|
||||
}
|
||||
|
||||
await db.file.update({
|
||||
where: { id: body.id },
|
||||
data: { description: body.description },
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: "Description updated successfully" });
|
||||
return NextResponse.json(file);
|
||||
} catch (error) {
|
||||
console.error("Error updating description:", error);
|
||||
return NextResponse.json({ error: "Failed to update description" }, { status: 500 });
|
||||
console.error("Error updating file description:", error);
|
||||
return NextResponse.json({ error: "Failed to update file description" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
import { notFound } from "next/navigation";
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef, Suspense } from "react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
import toast, { Toaster } from "react-hot-toast";
|
||||
import { FilePreview } from "~/app/_components/FilePreview";
|
||||
import { HomeButton } from "~/app/_components/HomeButton"; // Import the client component
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { FileActionsContainer, FileDescriptionContainer } from "~/app/_components/ActionButtons"; // Import the client component
|
||||
import { useFileActions } from "~/app/_components/FileActions";
|
||||
import Head from "next/head";
|
||||
|
||||
interface FileDetails {
|
||||
@ -18,45 +20,75 @@ interface FileDetails {
|
||||
description: string;
|
||||
}
|
||||
|
||||
async function fetchFileDetails(fileId: string): Promise<FileDetails | null> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/share?id=${encodeURIComponent(
|
||||
fileId
|
||||
)}`,
|
||||
{ cache: "no-store" }
|
||||
);
|
||||
function FilePreviewContainerContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const fileId = searchParams.get("id");
|
||||
const [fileDetails, setFileDetails] = useState<FileDetails | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const debounceTimer = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
const { handleDescriptionChange, handleCopyUrl, handleDownload, handleRemove } = useFileActions(
|
||||
() => {},
|
||||
setDescription,
|
||||
fileId || undefined
|
||||
);
|
||||
|
||||
const getFileType = (extension: string): string => {
|
||||
const fileTypes: Record<string, string> = {
|
||||
".mp4": "video/mp4",
|
||||
".webm": "video/webm",
|
||||
".ogg": "video/ogg",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".svg": "image/svg+xml",
|
||||
".mp3": "audio/mpeg",
|
||||
".wav": "audio/wav",
|
||||
};
|
||||
return fileTypes[extension] || "unknown";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!fileId) {
|
||||
setError("File name is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch file details:", err);
|
||||
return null;
|
||||
const fetchFileDetails = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/share?id=${encodeURIComponent(fileId)}`);
|
||||
if (!response.ok) throw new Error("Failed to fetch file details");
|
||||
|
||||
const data: FileDetails = await response.json();
|
||||
setFileDetails(data);
|
||||
setDescription(data.description);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError("Failed to load file details.");
|
||||
}
|
||||
};
|
||||
|
||||
fetchFileDetails();
|
||||
}, [fileId]);
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-500">{error}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function FilePreviewContainer({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { id?: string };
|
||||
}) {
|
||||
const fileId = searchParams.id;
|
||||
|
||||
if (!fileId) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const fileDetails = await fetchFileDetails(fileId);
|
||||
|
||||
if (!fileDetails) {
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
|
||||
<Toaster position="top-right" reverseOrder={false} />
|
||||
<div className="absolute top-4 left-4">
|
||||
<HomeButton />
|
||||
<button
|
||||
onClick={() => router.push("/")}
|
||||
className="rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20"
|
||||
>
|
||||
Home
|
||||
</button>
|
||||
</div>
|
||||
<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]">
|
||||
@ -90,19 +122,16 @@ export default async function FilePreviewContainer({
|
||||
).toLocaleString()}`}
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content={`${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/serv?id=${fileId}`}
|
||||
/>
|
||||
<meta property="og:image:alt" content={`${fileDetails.name} preview`} />
|
||||
<meta
|
||||
property="og:url"
|
||||
content={`${process.env.NEXT_PUBLIC_PAGE_URL}/share?id=${fileId}`}
|
||||
/>
|
||||
</Head>
|
||||
<main className="flex min-h-screen flex-col items-center justify-center bg-gradient-to-b from-[#2e026d] to-[#15162c] text-white">
|
||||
<Toaster position="top-right" reverseOrder={false} />
|
||||
<div className="absolute top-4 left-4">
|
||||
<HomeButton /> {/* Use the client component */}
|
||||
<button
|
||||
onClick={() => router.push("/")}
|
||||
className="rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20"
|
||||
>
|
||||
Home
|
||||
</button>
|
||||
</div>
|
||||
<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]">
|
||||
@ -110,10 +139,7 @@ export default async function FilePreviewContainer({
|
||||
</h1>
|
||||
<div className="mt-6">
|
||||
{fileDetails.type !== "unknown" && (
|
||||
<FilePreview
|
||||
fileId={fileDetails.id}
|
||||
fileType={fileDetails.type}
|
||||
/>
|
||||
<FilePreview fileId={fileDetails.id} fileType={getFileType(fileDetails.type)} />
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white">
|
||||
@ -143,18 +169,81 @@ export default async function FilePreviewContainer({
|
||||
<strong>Upload Date:</strong>{" "}
|
||||
{new Date(fileDetails.uploadDate).toLocaleString()}
|
||||
</p>
|
||||
<div>
|
||||
<p>
|
||||
<strong>Description:</strong>{" "}
|
||||
<FileDescriptionContainer fileId={fileDetails.id} fileDescriprtion={fileDetails.description}/>
|
||||
</div>
|
||||
<div className="mt-4 flex justify-center">
|
||||
<FileActionsContainer
|
||||
fileId={fileDetails.id}
|
||||
fileName={fileDetails.name}
|
||||
fileUrl={fileDetails.url}
|
||||
isOwner={fileDetails.isOwner}
|
||||
/>
|
||||
</div>
|
||||
{fileDetails.isOwner ? (
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => handleDescriptionChange(e, debounceTimer)}
|
||||
className="w-full h-24 p-2 bg-white/10 rounded-lg text-white"
|
||||
/>
|
||||
) : fileDetails.description === "" ? (
|
||||
<span>No description available</span>
|
||||
) : (
|
||||
<span>{fileDetails.description}</span>
|
||||
)}
|
||||
</p>
|
||||
{fileDetails.isOwner && (
|
||||
<div className="flex place-content-center gap-4 mt-4">
|
||||
<button
|
||||
onClick={() => handleDownload(fileDetails.id, fileDetails.name)}
|
||||
className="rounded-full bg-blue-500 px-4 py-2 text-white hover:bg-blue-600 flex items-center gap-2"
|
||||
>
|
||||
<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="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2M7 10l5 5m0 0l5-5m-5 5V3"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleCopyUrl(fileDetails.url)}
|
||||
className="rounded-full bg-green-500 px-4 py-2 text-white hover:bg-green-600 flex items-center gap-2"
|
||||
>
|
||||
<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 9h8m-6 4h4m-7 8h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRemove(fileDetails.id)}
|
||||
className="rounded-full bg-red-500 px-4 py-2 text-white hover:bg-red-600 flex items-center gap-2"
|
||||
>
|
||||
<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="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@ -162,3 +251,11 @@ export default async function FilePreviewContainer({
|
||||
);
|
||||
}
|
||||
|
||||
export default function FilePreviewContainer() {
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<FilePreviewContainerContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user