Merge branch 'dev'

This commit is contained in:
ZareMate 2025-05-04 16:10:09 +02:00
commit 344334592e
Signed by: zaremate
GPG Key ID: 369A0E45E03A81C3
9 changed files with 213 additions and 173 deletions

View File

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

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,
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.");
@ -85,14 +96,21 @@ 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 }),
});
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,37 @@ 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) {
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 } 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]);
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">
<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">
@ -171,79 +145,16 @@ function FilePreviewContainerContent() {
</p>
<p>
<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>
)}
{fileDetails.description || "No description available"}
</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"
<div className="mt-4 flex justify-center">
<FileActionsContainer
fileId={fileDetails.id}
fileName={fileDetails.name}
fileUrl={fileDetails.url}
isOwner={fileDetails.isOwner}
/>
</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>
@ -251,11 +162,3 @@ function FilePreviewContainerContent() {
);
}
export default function FilePreviewContainer() {
return (
<Suspense fallback={<div>Loading...</div>}>
<FilePreviewContainerContent />
</Suspense>
);
}