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

View File

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

View File

@ -67,9 +67,9 @@ async function fetchFileDetails(fileId: string): Promise<FileDetails | null> {
try { try {
const response = await fetch( const response = await fetch(
`${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/share?id=${encodeURIComponent( `${process.env.NEXT_PUBLIC_PAGE_URL}/api/files/share?id=${encodeURIComponent(
fileId fileId,
)}`, )}`,
{ cache: "no-store" } { cache: "no-store" },
); );
if (!response.ok) { if (!response.ok) {
@ -96,7 +96,6 @@ export default async function FilePreviewContainer({
} }
const fileDetails = await fetchFileDetails(fileId); const fileDetails = await fetchFileDetails(fileId);
if (!fileDetails) { if (!fileDetails) {
return ( return (
@ -138,10 +137,10 @@ export default async function FilePreviewContainer({
{fileDetails.size > 1024 * 1024 * 1024 {fileDetails.size > 1024 * 1024 * 1024
? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB" ? (fileDetails.size / (1024 * 1024 * 1024)).toFixed(2) + " GB"
: fileDetails.size > 1024 * 1024 : fileDetails.size > 1024 * 1024
? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB" ? (fileDetails.size / (1024 * 1024)).toFixed(2) + " MB"
: fileDetails.size > 1024 : fileDetails.size > 1024
? (fileDetails.size / 1024).toFixed(2) + " KB" ? (fileDetails.size / 1024).toFixed(2) + " KB"
: fileDetails.size + " Bytes"} : fileDetails.size + " Bytes"}
</p> </p>
<p> <p>
<strong>Owner:</strong>{" "} <strong>Owner:</strong>{" "}
@ -163,46 +162,14 @@ export default async function FilePreviewContainer({
fileDescription={fileDetails.description} fileDescription={fileDetails.description}
/> />
</div> </div>
<div className="bg-white/10 shadow-md rounded-lg p-6 w-full max-w-md text-white"> <div className="mt-4 flex justify-center">
<p> <FileActionsContainer
<strong>Name:</strong> {fileDetails.name} fileId={fileDetails.id}
</p> fileName={fileDetails.name}
<p> fileUrl={fileDetails.url}
<strong>Size:</strong>{" "} isOwner={true}
{fileDetails.size > 1024 * 1024 * 1024 isPublic={fileDetails.isPublic}
? (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> </div>
</div> </div>
</div> </div>