feat: enhance file actions with toast notifications and update public/private toggle logic

This commit is contained in:
ZareMate 2025-05-12 10:11:32 +02:00
parent 551cf2e2cb
commit b368473216
Signed by: zaremate
GPG Key ID: 369A0E45E03A81C3
5 changed files with 96 additions and 109 deletions

1
public/icons/private.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z" /></svg>

After

Width:  |  Height:  |  Size: 314 B

1
public/icons/public.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.9,17.39C17.64,16.59 16.89,16 16,16H15V13A1,1 0 0,0 14,12H8V10H10A1,1 0 0,0 11,9V7H13A2,2 0 0,0 15,5V4.59C17.93,5.77 20,8.64 20,12C20,14.08 19.2,15.97 17.9,17.39M11,19.93C7.05,19.44 4,16.08 4,12C4,11.38 4.08,10.78 4.21,10.21L9,15V16A2,2 0 0,0 11,18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></svg>

After

Width:  |  Height:  |  Size: 406 B

View File

@ -1,13 +1,14 @@
"use client";
import { useRef, useState } from "react";
import { useFileActions } from "~/app/_components/FileActions";
import toast from "react-hot-toast";
export function FileActionsContainer({
fileId,
fileName,
fileUrl,
isOwner,
isPublic,
isPublic: initialIsPublic, // Rename to avoid conflict with local state
}: {
fileId: string;
fileName: string;
@ -15,77 +16,91 @@ export function FileActionsContainer({
isOwner: boolean;
isPublic: boolean;
}) {
const { handleDownload, handleCopyUrl, handleRemove} = useFileActions(() => fileId, (description: string) => {
if (isOwner) {
console.log(description);
const [isPublic, setIsPublic] = useState(initialIsPublic); // Local state for toggle
const { handleDownload, handleCopyUrl, handleRemove } = useFileActions(
() => fileId,
(description: string) => {
if (isOwner) {
console.log(description);
}
}
});
);
return (
<div className="flex self-center gap-2">
<div className="flex gap-2 self-center">
{/* Download Button */}
<button
onClick={() => handleDownload(fileId, fileName)}
className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600"
onClick={() => handleDownload(fileId, fileName)}
className="flex items-center justify-center rounded-full bg-blue-500 p-2 hover:bg-blue-600"
>
<img src="/icons/download.svg" alt="Download" className="w-6 h-6" />
<img src="/icons/download.svg" alt="Download" className="h-6 w-6" />
</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"
onClick={() => {
handleCopyUrl(fileUrl);
toast.success("File URL copied to clipboard!");
}}
className="flex items-center justify-center rounded-full bg-green-500 p-2 hover:bg-green-600"
>
<img src="/icons/copy.svg" alt="Copy URL" className="w-6 h-6" />
<img src="/icons/copy.svg" alt="Copy URL" className="h-6 w-6" />
</button>
{/* Remove Button */}
{isOwner && (
<button
onClick={() => handleRemove(fileId)}
onClick={async () => {
try {
await handleRemove(fileId);
toast.success("File removed successfully!");
} catch (err) {
toast.error("Failed to remove file.");
console.error(err);
}
}}
className="flex items-center justify-center rounded-full bg-red-500 p-2 hover:bg-red-600"
>
<img src="/icons/delete.svg" alt="Remove" className="w-6 h-6" />
<img src="/icons/delete.svg" alt="Remove" className="h-6 w-6" />
</button>
)}
{/* Public/Private Toggle */}
{isOwner && (
<div className="mt-4 flex items-center gap-2">
<label className="text-sm">
Public:
</label>
<label
htmlFor={`public-toggle-${fileId}`}
className="relative inline-flex items-center cursor-pointer"
>
<input
id={`public-toggle-${fileId}`}
type="checkbox"
checked={isPublic} // Ensure this reflects the prop
onChange={async (e) => {
const newIsPublic = e.target.checked;
try {
const response = await fetch(`/api/files/update-public`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ fileId: fileId, isPublic: newIsPublic }),
});
if (!response.ok) {
throw new Error("Failed to update public status");
}
console.log("Public status updated successfully");
} catch (err) {
console.error("Error updating public status:", err);
}
}}
className="sr-only peer"
/>
{/* Toggle Background */}
<div className="w-10 h-5 bg-gray-300 rounded-full peer-checked:bg-green-500 peer-focus:ring-2 peer-focus:ring-green-300 transition-colors"></div>
{/* Toggle Handle */}
<div className="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow-md peer-checked:translate-x-5 transition-transform"></div>
</label>
</div>
<button
onClick={async () => {
const newIsPublic = !isPublic;
try {
const response = await fetch(`/api/files/update-public`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
fileId: fileId,
isPublic: newIsPublic,
}),
});
if (!response.ok) {
throw new Error("Failed to update public status");
}
setIsPublic(newIsPublic); // Update local state
toast.success(
`File is now ${newIsPublic ? "Public" : "Private"}!`
);
} catch (err) {
toast.error("Error updating public status.");
console.error(err);
}
}}
className="flex items-center justify-center rounded-full bg-gray-500 p-2 hover:bg-gray-600"
>
<img
src={isPublic ? "/icons/public.svg" : "/icons/private.svg"}
alt={isPublic ? "Public" : "Private"}
className="h-6 w-6"
/>
</button>
)}
</div>
);
@ -98,12 +113,15 @@ export function FileDescriptionContainer({
fileId: string;
fileDescription?: string;
}) {
const [description, setDescription] = useState(fileDescription || ""); // Add state for description
const { handleDescriptionChange } = useFileActions(() => {}, (description: string) => {
setDescription(description);
return undefined;
}, fileId); // Wrap setDescription in a function
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>) => {
@ -111,9 +129,9 @@ export function FileDescriptionContainer({
};
return (
<div className="flex self-center gap-2">
<div className="flex gap-2 self-center">
<textarea
className="w-full h-24 p-2 border rounded-md bg-gray-800 text-white"
className="h-24 w-full rounded-md border bg-gray-800 p-2 text-white"
value={description} // Use state value
onChange={handleChange}
placeholder="Enter file description..."
@ -121,4 +139,4 @@ export function FileDescriptionContainer({
/>
</div>
);
}
}

View File

@ -15,7 +15,7 @@ interface FileDetails {
description: string;
extension: string;
isOwner: boolean; // Indicates if the user owns the file
isPublic: boolean; // Indicates if the file is public
public: boolean; // Indicates if the file is public
}
interface FileGridProps {
@ -118,7 +118,7 @@ export default function FileGrid({ session }: FileGridProps) {
fileName={file.name}
fileUrl={file.url}
isOwner={true}
isPublic={file.isPublic}
isPublic={file.public}
/>
</div>
</div>

View File

@ -67,9 +67,9 @@ 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
fileId,
)}`,
{ cache: "no-store" }
{ cache: "no-store" },
);
if (!response.ok) {
@ -96,7 +96,6 @@ export default async function FilePreviewContainer({
}
const fileDetails = await fetchFileDetails(fileId);
if (!fileDetails) {
return (
@ -138,10 +137,10 @@ export default async function FilePreviewContainer({
{fileDetails.size > 1024 * 1024 * 1024
? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB"
: fileDetails.size > 1024 * 1024
? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB"
: fileDetails.size > 1024
? (fileDetails.size / 1024).toFixed(2) + " KB"
: fileDetails.size + " Bytes"}
? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB"
: fileDetails.size > 1024
? (fileDetails.size / 1024).toFixed(2) + " KB"
: fileDetails.size + " Bytes"}
</p>
<p>
<strong>Owner:</strong>{" "}
@ -163,46 +162,14 @@ export default async function FilePreviewContainer({
fileDescription={fileDetails.description}
/>
</div>
<div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white">
<p>
<strong>Name:</strong> {fileDetails.name}
</p>
<p>
<strong>Size:</strong>{" "}
{fileDetails.size > 1024 * 1024 * 1024
? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB"
: fileDetails.size > 1024 * 1024
? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB"
: fileDetails.size > 1024
? (fileDetails.size / 1024).toFixed(2) + " KB"
: fileDetails.size + " Bytes"}
</p>
<p>
<strong>Owner:</strong>{" "}
<img
className="rounded-md inline size-5"
src={fileDetails.ownerAvatar || ""}
alt="Owner avatar"
/>{" "}
{fileDetails.owner}
</p>
<p>
<strong>Upload Date:</strong>{" "}
{new Date(fileDetails.uploadDate).toLocaleString()}
</p>
<div>
<strong>Description:</strong>{" "}
<FileDescriptionContainer fileId={fileDetails.id} fileDescription={fileDetails.description}/>
</div>
<div className="mt-4 flex justify-center">
<FileActionsContainer
fileId={fileDetails.id}
fileName={fileDetails.name}
fileUrl={fileDetails.url}
isOwner={fileDetails.isOwner}
isPublic={fileDetails.isPublic}
/>
</div>
<div className="mt-4 flex justify-center">
<FileActionsContainer
fileId={fileDetails.id}
fileName={fileDetails.name}
fileUrl={fileDetails.url}
isOwner={true}
isPublic={fileDetails.isPublic}
/>
</div>
</div>
</div>