Compare commits

..

3 Commits

9 changed files with 257 additions and 178 deletions

View File

@ -0,0 +1,120 @@
'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>
);
}

View File

@ -1,12 +1,13 @@
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) => void,
setDescription?: (description: string) => undefined,
fileId?: string
) => {
const pageUrl = `${env.NEXT_PUBLIC_PAGE_URL}/share?id=`;
const pageUrl = `${env.NEXT_PUBLIC_PAGE_URL}`;
// Handle file download
const handleDownload = async (fileId: string, fileName: string) => {
@ -44,15 +45,25 @@ export const useFileActions = (
// Remove a file
const handleRemove = async (fileId: string) => {
try {
const response = await fetch(`/api/remove`, {
const response = await fetch(`/api/files/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.");
@ -64,7 +75,9 @@ export const useFileActions = (
e: React.ChangeEvent<HTMLTextAreaElement>,
debounceTimer: React.RefObject<NodeJS.Timeout | null>
) => {
if (!setDescription) return;
if (setDescription === undefined) {console.error("setDescription function is not provided")
return;
};
const newDescription = e.target.value;
setDescription(newDescription);
@ -72,7 +85,9 @@ export const useFileActions = (
if (debounceTimer.current) {
clearTimeout(debounceTimer.current);
}
debounceTimer.current = setTimeout(() => {
console.log("Calling handleDescriptionSave"); // Debug log
handleDescriptionSave(newDescription);
}, 1000);
};
@ -85,14 +100,22 @@ export const useFileActions = (
}
try {
const response = await fetch(`/api/share?id=${encodeURIComponent(fileId)}`, {
const response = await fetch(`/api/files/share?id=${encodeURIComponent(fileId)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ description }),
// pass the fileId and description in the request body
body: JSON.stringify({ description, id: fileId }),
});
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.");

View File

@ -21,7 +21,7 @@ export function FilePreview({ fileId, fileType }: FilePreviewProps) {
const fetchMedia = async () => {
try {
const response = await fetch(`/api/serv?id=${encodeURIComponent(fileId)}`);
const response = await fetch(`/api/files/serv?id=${encodeURIComponent(fileId)}`);
if (!response.ok) {
throw new Error("Failed to fetch media");
}

View File

@ -0,0 +1,12 @@
"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>
);
}

View File

@ -2,6 +2,7 @@
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);
@ -42,6 +43,7 @@ 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

View File

@ -22,8 +22,12 @@ export async function DELETE(req: Request) {
where: { id: body.id },
});
if (!resource || resource.uploadedById !== session.user.id) {
return NextResponse.json({ error: "Resource not found or unauthorized" }, { status: 404 });
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 });
}
const filePath = path.join(process.cwd(), "uploads", path.basename(body.id));
@ -37,9 +41,9 @@ export async function DELETE(req: Request) {
notifyClients({ type: "file-removed", fileId: body.id });
return NextResponse.json({ message: "Resource deleted successfully" });
return NextResponse.json({ message: "File deleted successfully" });
} catch (error) {
console.error("Error deleting resource:", error);
return NextResponse.json({ error: "Failed to delete resource" }, { status: 500 });
console.error("Error deleting file:", error);
return NextResponse.json({ error: "Failed to delete file" }, { status: 500 });
}
}

View File

@ -41,23 +41,38 @@ 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 (!fileId) {
return NextResponse.json({ error: "File name is required" }, { status: 400 });
if (!session?.user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const file = await db.file.update({
where: { id: fileId },
data: { description },
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 },
});
return NextResponse.json(file);
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" });
} catch (error) {
console.error("Error updating file description:", error);
return NextResponse.json({ error: "Failed to update file description" }, { status: 500 });
console.error("Error updating description:", error);
return NextResponse.json({ error: "Failed to update description" }, { status: 500 });
}
}

View File

@ -1,10 +1,8 @@
"use client";
import { useEffect, useState, useRef, Suspense } from "react";
import { useSearchParams, useRouter } from "next/navigation";
import toast, { Toaster } from "react-hot-toast";
import { notFound } from "next/navigation";
import { FilePreview } from "~/app/_components/FilePreview";
import { useFileActions } from "~/app/_components/FileActions";
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 Head from "next/head";
interface FileDetails {
@ -20,75 +18,45 @@ interface FileDetails {
description: string;
}
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);
const { handleDescriptionChange, handleCopyUrl, handleDownload, handleRemove } = useFileActions(
() => {},
setDescription,
fileId || undefined
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" }
);
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;
if (!response.ok) {
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);
return response.json();
} catch (err) {
console.error(err);
setError("Failed to load file details.");
console.error("Failed to fetch file details:", err);
return null;
}
};
}
fetchFileDetails();
}, [fileId]);
export default async function FilePreviewContainer({
searchParams,
}: {
searchParams: { id?: string };
}) {
const fileId = searchParams.id;
if (error) {
return <div className="text-red-500">{error}</div>;
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">
<button
onClick={() => router.push("/")}
className="rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20"
>
Home
</button>
<HomeButton />
</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]">
@ -122,16 +90,19 @@ function FilePreviewContainerContent() {
).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">
<button
onClick={() => router.push("/")}
className="rounded-full bg-white/10 px-4 py-2 font-semibold hover:bg-white/20"
>
Home
</button>
<HomeButton /> {/* Use the client component */}
</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]">
@ -139,7 +110,10 @@ function FilePreviewContainerContent() {
</h1>
<div className="mt-6">
{fileDetails.type !== "unknown" && (
<FilePreview fileId={fileDetails.id} fileType={getFileType(fileDetails.type)} />
<FilePreview
fileId={fileDetails.id}
fileType={fileDetails.type}
/>
)}
</div>
<div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white">
@ -169,81 +143,18 @@ function FilePreviewContainerContent() {
<strong>Upload Date:</strong>{" "}
{new Date(fileDetails.uploadDate).toLocaleString()}
</p>
<p>
<div>
<strong>Description:</strong>{" "}
{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>
<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>
)}
</div>
</div>
</main>
@ -251,11 +162,3 @@ function FilePreviewContainerContent() {
);
}
export default function FilePreviewContainer() {
return (
<Suspense fallback={<div>Loading...</div>}>
<FilePreviewContainerContent />
</Suspense>
);
}