feat: enhance file upload section and reverse shares components

- Updated the file upload section to use axios for file uploads, enabling progress tracking during uploads.
- Removed unnecessary validation checks for optional fields in the reverse shares form, allowing both fields to be truly optional.
- Added refresh functionality to the reverse shares page, improving user experience by allowing data to be reloaded easily.
- Enhanced the reverse shares search component with a refresh button, providing a more intuitive interface for users.
This commit is contained in:
Daniel Luiz Alves
2025-07-01 02:37:13 -03:00
parent 00174fd9af
commit 8a954e14fa
4 changed files with 129 additions and 26 deletions

View File

@@ -2,6 +2,7 @@
import { useCallback, useEffect, useState } from "react";
import { IconCheck, IconFile, IconMail, IconUpload, IconUser, IconX } from "@tabler/icons-react";
import axios from "axios";
import { useTranslations } from "next-intl";
import { useDropzone } from "react-dropzone";
import { toast } from "sonner";
@@ -132,18 +133,22 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
return fileName.split(".").pop() || "";
};
const uploadFileToStorage = async (file: File, presignedUrl: string): Promise<void> => {
const response = await fetch(presignedUrl, {
method: "PUT",
body: file,
const uploadFileToStorage = async (
file: File,
presignedUrl: string,
onProgress?: (progress: number) => void
): Promise<void> => {
await axios.put(presignedUrl, file, {
headers: {
"Content-Type": file.type,
},
onUploadProgress: (progressEvent) => {
if (onProgress && progressEvent.total) {
const progress = (progressEvent.loaded / progressEvent.total) * 100;
onProgress(Math.round(progress));
}
},
});
if (!response.ok) {
throw new Error("Failed to upload file to storage");
}
};
const registerUploadedFile = async (file: File, objectName: string): Promise<void> => {
@@ -180,7 +185,9 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
password ? { password } : undefined
);
await uploadFileToStorage(file, presignedResponse.data.url);
await uploadFileToStorage(file, presignedResponse.data.url, (progress) => {
updateFileStatus(index, { progress });
});
updateFileStatus(index, { progress: UPLOAD_PROGRESS.COMPLETE });
@@ -220,12 +227,8 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
return false;
}
if (reverseShare.nameFieldRequired === "OPTIONAL" && reverseShare.emailFieldRequired === "OPTIONAL") {
if (!uploaderName.trim() && !uploaderEmail.trim()) {
toast.error(t("reverseShares.upload.errors.provideNameOrEmail"));
return false;
}
}
// Remove the validation that requires at least one field when both are optional
// When both fields are OPTIONAL, they should be truly optional (can be empty)
return true;
};
@@ -272,9 +275,8 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
if (emailRequired && !uploaderEmail.trim()) return false;
if (reverseShare.nameFieldRequired === "OPTIONAL" && reverseShare.emailFieldRequired === "OPTIONAL") {
return !!(uploaderName.trim() || uploaderEmail.trim());
}
// When both fields are OPTIONAL, they should be truly optional (can be empty)
// Remove the check that requires at least one field to be filled
return true;
};

View File

@@ -1,4 +1,4 @@
import { IconPlus, IconSearch } from "@tabler/icons-react";
import { IconPlus, IconRefresh, IconSearch } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
@@ -8,6 +8,8 @@ export interface ReverseSharesSearchProps {
searchQuery: string;
onSearchChange: (value: string) => void;
onCreateReverseShare: () => void;
onRefresh: () => void;
isRefreshing?: boolean;
totalReverseShares: number;
filteredCount: number;
}
@@ -16,6 +18,8 @@ export function ReverseSharesSearch({
searchQuery,
onSearchChange,
onCreateReverseShare,
onRefresh,
isRefreshing = false,
totalReverseShares,
filteredCount,
}: ReverseSharesSearchProps) {
@@ -25,10 +29,15 @@ export function ReverseSharesSearch({
<div className="flex flex-col gap-6">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">{t("reverseShares.search.title")}</h2>
<Button onClick={onCreateReverseShare}>
<IconPlus className="h-4 w-4" />
{t("reverseShares.search.createButton")}
</Button>
<div className="flex items-center gap-2">
<Button variant="outline" size="icon" onClick={onRefresh} disabled={isRefreshing}>
<IconRefresh className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
</Button>
<Button onClick={onCreateReverseShare}>
<IconPlus className="h-4 w-4" />
{t("reverseShares.search.createButton")}
</Button>
</div>
</div>
<div className="flex items-center gap-2">

View File

@@ -65,6 +65,8 @@ export default function ReverseSharesPage() {
totalReverseShares={reverseShares.length}
onCreateReverseShare={() => setIsCreateModalOpen(true)}
onSearchChange={setSearchQuery}
onRefresh={loadReverseShares}
isRefreshing={isLoading}
/>
<ReverseSharesCardsContainer

View File

@@ -1,4 +1,11 @@
"use client";
import { useEffect, useState } from "react";
import { IconMaximize, IconX } from "@tabler/icons-react";
import { createPortal } from "react-dom";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Button } from "@/components/ui/button";
interface ImagePreviewProps {
src: string;
@@ -6,9 +13,92 @@ interface ImagePreviewProps {
}
export function ImagePreview({ src, alt }: ImagePreviewProps) {
const [isFullscreen, setIsFullscreen] = useState(false);
const handleExpandClick = () => {
setIsFullscreen(true);
};
const handleCloseFullscreen = () => {
setIsFullscreen(false);
};
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
setIsFullscreen(false);
}
};
// Handle ESC key press
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && isFullscreen) {
setIsFullscreen(false);
}
};
if (isFullscreen) {
document.addEventListener("keydown", handleKeyDown);
// Prevent body scroll when fullscreen is open
document.body.style.overflow = "hidden";
}
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = "unset";
};
}, [isFullscreen]);
return (
<AspectRatio ratio={16 / 9} className="bg-muted">
<img src={src} alt={alt} className="object-contain w-full h-full rounded-md" />
</AspectRatio>
<>
<AspectRatio ratio={16 / 9} className="bg-muted">
<div className="relative group w-full h-full">
<img
src={src}
alt={alt}
className="object-contain w-full h-full rounded-md cursor-pointer"
onClick={handleExpandClick}
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-all duration-300 rounded-md">
<Button
variant="outline"
size="icon"
className="absolute bottom-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-300 bg-white/90 hover:bg-white text-black shadow-lg h-8 w-8"
onClick={handleExpandClick}
>
<IconMaximize className="h-4 w-4" />
</Button>
</div>
</div>
</AspectRatio>
{isFullscreen &&
typeof window !== "undefined" &&
createPortal(
<div
className="fixed inset-0 z-[99999] bg-black/95 backdrop-blur-sm flex items-center justify-center"
onClick={handleBackdropClick}
style={{ margin: 0, padding: 0 }}
>
<Button
variant="outline"
size="icon"
className="cursor-pointer absolute top-6 right-6 z-10 bg-white/10 hover:bg-white/20 text-white border-white/20 h-10 w-10"
onClick={handleCloseFullscreen}
>
<IconX className="h-6 w-6 cursor-pointer" />
</Button>
<img
src={src}
alt={alt}
className="max-w-screen max-h-screen w-auto h-auto object-contain p-4"
onClick={(e) => e.stopPropagation()}
style={{ maxWidth: "100vw", maxHeight: "100vh" }}
/>
</div>,
document.body
)}
</>
);
}