mirror of
https://github.com/kyantech/Palmr.git
synced 2025-11-02 13:03:15 +00:00
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:
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -65,6 +65,8 @@ export default function ReverseSharesPage() {
|
||||
totalReverseShares={reverseShares.length}
|
||||
onCreateReverseShare={() => setIsCreateModalOpen(true)}
|
||||
onSearchChange={setSearchQuery}
|
||||
onRefresh={loadReverseShares}
|
||||
isRefreshing={isLoading}
|
||||
/>
|
||||
|
||||
<ReverseSharesCardsContainer
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user