feat: enhance file actions with toast notifications and update public/private toggle logic
This commit is contained in:
		
							parent
							
								
									551cf2e2cb
								
							
						
					
					
						commit
						b368473216
					
				
							
								
								
									
										1
									
								
								public/icons/private.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								public/icons/private.svg
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1
									
								
								public/icons/public.svg
									
									
									
									
									
										Normal 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 | 
| @ -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> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user