From 6a08874267a37fe1a1aa7be3aa784d765707955c Mon Sep 17 00:00:00 2001 From: Daniel Luiz Alves Date: Fri, 6 Jun 2025 13:43:16 -0300 Subject: [PATCH] feat: enhance reverse share upload functionality with improved localization and error handling - Updated localization files to include new strings for the reverse share upload process, enhancing user experience in multiple languages. - Implemented a new layout for reverse share uploads, integrating file upload sections and status messages for better feedback. - Added error handling for various upload scenarios, including password protection, link expiration, and file validation. - Refactored components to utilize hooks for managing upload state and responses, improving code organization and maintainability. - Introduced a new password modal for handling protected links, enhancing security during file uploads. - Enhanced the user interface with dynamic status messages and visual feedback during the upload process. --- apps/web/messages/pt-BR.json | 85 +++- .../r/[alias]/components/default-layout.tsx | 142 +++++- .../components/file-upload-section.tsx | 475 +++++++++++------- .../(shares)/r/[alias]/components/index.ts | 6 + .../r/[alias]/components/password-modal.tsx | 21 +- .../components/shared/status-message.tsx | 101 ++++ .../[alias]/components/transparent-footer.tsx | 27 + .../[alias]/components/we-transfer-layout.tsx | 247 +++++---- .../app/(shares)/r/[alias]/constants/index.ts | 84 ++++ .../[alias]/hooks/use-reverse-share-upload.ts | 150 ++++++ .../web/src/app/(shares)/r/[alias]/layout.tsx | 13 +- apps/web/src/app/(shares)/r/[alias]/page.tsx | 138 +++-- .../app/(shares)/r/[alias]/types/index.tsx | 49 ++ .../(shares)/reverse-shares/types/index.tsx | 0 14 files changed, 1136 insertions(+), 402 deletions(-) create mode 100644 apps/web/src/app/(shares)/r/[alias]/components/index.ts create mode 100644 apps/web/src/app/(shares)/r/[alias]/components/shared/status-message.tsx create mode 100644 apps/web/src/app/(shares)/r/[alias]/components/transparent-footer.tsx create mode 100644 apps/web/src/app/(shares)/r/[alias]/constants/index.ts create mode 100644 apps/web/src/app/(shares)/r/[alias]/hooks/use-reverse-share-upload.ts create mode 100644 apps/web/src/app/(shares)/r/[alias]/types/index.tsx create mode 100644 apps/web/src/app/(shares)/reverse-shares/types/index.tsx diff --git a/apps/web/messages/pt-BR.json b/apps/web/messages/pt-BR.json index 19431f9..36ed333 100644 --- a/apps/web/messages/pt-BR.json +++ b/apps/web/messages/pt-BR.json @@ -1183,7 +1183,7 @@ }, "allowedFileTypes": { "label": "Tipos de Arquivo Permitidos", - "placeholder": "Ex: .pdf,.jpg,.png,.docx", + "placeholder": "Ex: pdf, jpg, png, docx", "description": "Opcional. Especifique as extensões permitidas separadas por vírgula." }, "pageLayout": { @@ -1227,6 +1227,89 @@ "confirmButton": "Excluir Link", "cancelButton": "Cancelar", "deleting": "Excluindo..." + }, + "upload": { + "metadata": { + "title": "Enviar Arquivos - Palmr", + "description": "Envie arquivos através do link compartilhado" + }, + "layout": { + "defaultTitle": "Enviar Arquivos", + "importantInfo": "Informações importantes:", + "maxFiles": "Máximo de {count} arquivo(s)", + "maxFileSize": "Tamanho máximo por arquivo: {size}MB", + "allowedTypes": "Tipos permitidos: {types}", + "loading": "Carregando..." + }, + "password": { + "title": "Link Protegido", + "description": "Este link está protegido por senha. Digite a senha para continuar.", + "label": "Senha", + "placeholder": "Digite a senha", + "cancel": "Cancelar", + "submit": "Continuar", + "verifying": "Verificando..." + }, + "errors": { + "loadFailed": "Falha ao carregar informações. Tente novamente.", + "passwordIncorrect": "Senha incorreta. Tente novamente.", + "linkNotFound": "Link não encontrado ou expirado.", + "linkInactive": "Este link está inativo.", + "linkExpired": "Este link expirou.", + "uploadFailed": "Erro ao enviar arquivo", + "fileTooLarge": "Arquivo muito grande. Tamanho máximo: {maxSize}", + "fileTypeNotAllowed": "Tipo de arquivo não permitido. Tipos aceitos: {allowedTypes}", + "maxFilesExceeded": "Máximo de {maxFiles} arquivos permitidos", + "selectAtLeastOneFile": "Selecione pelo menos um arquivo", + "provideNameOrEmail": "Informe seu nome ou e-mail" + }, + "fileDropzone": { + "dragActive": "Solte os arquivos aqui", + "dragInactive": "Arraste arquivos aqui ou clique para selecionar", + "acceptedTypes": "Tipos aceitos: {types}", + "maxFileSize": "Tamanho máximo: {size}", + "maxFiles": "Máximo de {count} arquivos", + "remainingFiles": "Restam {remaining} de {max} arquivos" + }, + "fileList": { + "title": "Arquivos selecionados:", + "statusUploaded": "Enviado", + "statusError": "Erro" + }, + "form": { + "nameLabel": "Nome", + "namePlaceholder": "Seu nome", + "emailLabel": "E-mail", + "emailPlaceholder": "seu@email.com", + "descriptionLabel": "Descrição (opcional)", + "descriptionPlaceholder": "Adicione uma descrição aos arquivos...", + "uploadButton": "Enviar {count} arquivo(s)", + "uploading": "Enviando..." + }, + "success": { + "title": "Arquivos enviados com sucesso! 🎉", + "description": "Você pode fechar esta página.", + "countMessage": "{count} arquivo(s) enviado(s) com sucesso!" + }, + "maxFilesReached": { + "title": "Limite de arquivos atingido", + "description": "Este link já recebeu o número máximo de {maxFiles} arquivo(s) permitido(s).", + "contactOwner": "Se houve algum erro ou você precisa enviar mais arquivos, entre em contato com o proprietário do link." + }, + "linkInactive": { + "title": "Link inativo", + "description": "Este link de recebimento está temporariamente inativo.", + "contactOwner": "Entre em contato com o proprietário do link para mais informações." + }, + "linkNotFound": { + "title": "Link não encontrado", + "description": "Este link pode ter sido removido ou nunca existiu." + }, + "linkExpired": { + "title": "Link expirado", + "description": "Este link de recebimento expirou e não está mais aceitando arquivos.", + "contactOwner": "Entre em contato com o proprietário do link se precisar enviar arquivos." + } } } } \ No newline at end of file diff --git a/apps/web/src/app/(shares)/r/[alias]/components/default-layout.tsx b/apps/web/src/app/(shares)/r/[alias]/components/default-layout.tsx index 725fd05..4e164f1 100644 --- a/apps/web/src/app/(shares)/r/[alias]/components/default-layout.tsx +++ b/apps/web/src/app/(shares)/r/[alias]/components/default-layout.tsx @@ -1,24 +1,122 @@ "use client"; import Link from "next/link"; +import { IconAlertTriangle, IconCheck, IconClock, IconInfoCircle } from "@tabler/icons-react"; +import { useTranslations } from "next-intl"; import { LanguageSwitcher } from "@/components/general/language-switcher"; import { ModeToggle } from "@/components/general/mode-toggle"; import { DefaultFooter } from "@/components/ui/default-footer"; import { useAppInfo } from "@/contexts/app-info-context"; -import type { GetReverseShareForUploadResult } from "@/http/endpoints/reverse-shares/types"; +import type { DefaultLayoutProps } from "../types"; import { FileUploadSection } from "./file-upload-section"; +import { StatusMessage } from "./shared/status-message"; -type ReverseShareInfo = GetReverseShareForUploadResult["data"]["reverseShare"]; - -interface DefaultLayoutProps { - reverseShare: ReverseShareInfo; - password: string; - alias: string; -} - -export function DefaultLayout({ reverseShare, password, alias }: DefaultLayoutProps) { +export function DefaultLayout({ + reverseShare, + password, + alias, + isMaxFilesReached, + hasUploadedSuccessfully, + onUploadSuccess, + isLinkInactive, + isLinkNotFound, + isLinkExpired, +}: DefaultLayoutProps) { const { appName, appLogo } = useAppInfo(); + const t = useTranslations(); + + const getUploadStatus = () => { + if (hasUploadedSuccessfully) { + return { + component: ( + + ), + }; + } + + if (isLinkInactive) { + return { + component: ( + + ), + }; + } + + if (isLinkNotFound || !reverseShare) { + return { + component: ( + + ), + }; + } + + if (isLinkExpired) { + return { + component: ( + + ), + }; + } + + if (isMaxFilesReached) { + return { + component: ( + + ), + }; + } + + return { + component: ( + + ), + }; + }; + + const showUploadLimits = + !hasUploadedSuccessfully && + !isMaxFilesReached && + !isLinkInactive && + !isLinkNotFound && + !isLinkExpired && + reverseShare && + (reverseShare.maxFiles || reverseShare.maxFileSize || reverseShare.allowedFileTypes); return (
@@ -42,9 +140,9 @@ export function DefaultLayout({ reverseShare, password, alias }: DefaultLayoutPr {/* Header da página */}

- {reverseShare.name || "Enviar Arquivos"} + {reverseShare?.name || t("reverseShares.upload.layout.defaultTitle")}

- {reverseShare.description && ( + {reverseShare?.description && (

{reverseShare.description}

@@ -53,17 +151,23 @@ export function DefaultLayout({ reverseShare, password, alias }: DefaultLayoutPr {/* Seção de upload */}
- + {getUploadStatus().component}
- {/* Informações adicionais (se houver limites) */} - {(reverseShare.maxFiles || reverseShare.maxFileSize || reverseShare.allowedFileTypes) && ( + {/* Informações adicionais */} + {showUploadLimits && (
-

Informações importantes:

+

{t("reverseShares.upload.layout.importantInfo")}

- {reverseShare.maxFiles &&

• Máximo de {reverseShare.maxFiles} arquivo(s)

} - {reverseShare.maxFileSize &&

• Tamanho máximo por arquivo: {reverseShare.maxFileSize}MB

} - {reverseShare.allowedFileTypes &&

• Tipos permitidos: {reverseShare.allowedFileTypes}

} + {reverseShare?.maxFiles && ( +

• {t("reverseShares.upload.layout.maxFiles", { count: reverseShare.maxFiles })}

+ )} + {reverseShare?.maxFileSize && ( +

• {t("reverseShares.upload.layout.maxFileSize", { size: reverseShare.maxFileSize })}

+ )} + {reverseShare?.allowedFileTypes && ( +

• {t("allowedTypes", { types: reverseShare.allowedFileTypes })}

+ )}
)} diff --git a/apps/web/src/app/(shares)/r/[alias]/components/file-upload-section.tsx b/apps/web/src/app/(shares)/r/[alias]/components/file-upload-section.tsx index 820fce9..894ba00 100644 --- a/apps/web/src/app/(shares)/r/[alias]/components/file-upload-section.tsx +++ b/apps/web/src/app/(shares)/r/[alias]/components/file-upload-section.tsx @@ -1,7 +1,8 @@ "use client"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { IconCheck, IconFile, IconMail, IconUpload, IconUser, IconX } from "@tabler/icons-react"; +import { useTranslations } from "next-intl"; import { useDropzone } from "react-dropzone"; import { toast } from "sonner"; @@ -12,79 +13,86 @@ import { Label } from "@/components/ui/label"; import { Progress } from "@/components/ui/progress"; import { Textarea } from "@/components/ui/textarea"; import { getPresignedUrlForUploadByAlias, registerFileUploadByAlias } from "@/http/endpoints"; -import type { GetReverseShareForUploadResult } from "@/http/endpoints/reverse-shares/types"; import { formatFileSize } from "@/utils/format-file-size"; +import { FILE_STATUS, UPLOAD_CONFIG, UPLOAD_PROGRESS } from "../constants"; +import { FileUploadSectionProps, FileWithProgress } from "../types"; -type ReverseShareInfo = GetReverseShareForUploadResult["data"]["reverseShare"]; - -interface FileUploadSectionProps { - reverseShare: ReverseShareInfo; - password: string; - alias: string; -} - -interface FileWithProgress { - file: File; - progress: number; - status: "pending" | "uploading" | "success" | "error"; - error?: string; -} - -export function FileUploadSection({ reverseShare, password, alias }: FileUploadSectionProps) { +export function FileUploadSection({ reverseShare, password, alias, onUploadSuccess }: FileUploadSectionProps) { const [files, setFiles] = useState([]); const [uploaderName, setUploaderName] = useState(""); const [uploaderEmail, setUploaderEmail] = useState(""); const [description, setDescription] = useState(""); const [isUploading, setIsUploading] = useState(false); - const validateFile = (file: File): string | null => { - // Check file size - if (reverseShare.maxFileSize) { - const maxSize = reverseShare.maxFileSize; - if (file.size > maxSize) { - return `Arquivo muito grande. Tamanho máximo: ${formatFileSize(maxSize)}`; - } - } + const t = useTranslations(); - // Check file type - if (reverseShare.allowedFileTypes) { - const allowedTypes = reverseShare.allowedFileTypes.split(",").map((type) => type.trim().toLowerCase()); - const fileExtension = file.name.split(".").pop()?.toLowerCase(); - if (fileExtension && !allowedTypes.includes(fileExtension)) { - return `Tipo de arquivo não permitido. Tipos aceitos: ${reverseShare.allowedFileTypes}`; - } - } + const validateFileSize = (file: File): string | null => { + if (!reverseShare.maxFileSize) return null; - // Check file count - if (reverseShare.maxFiles) { - const totalFiles = files.length + 1 + reverseShare.currentFileCount; - if (totalFiles > reverseShare.maxFiles) { - return `Máximo de ${reverseShare.maxFiles} arquivos permitidos`; - } + if (file.size > reverseShare.maxFileSize) { + return t("reverseShares.upload.errors.fileTooLarge", { + maxSize: formatFileSize(reverseShare.maxFileSize), + }); } - return null; }; + const validateFileType = (file: File): string | null => { + if (!reverseShare.allowedFileTypes) return null; + + const allowedTypes = reverseShare.allowedFileTypes.split(",").map((type) => type.trim().toLowerCase()); + + const fileExtension = file.name.split(".").pop()?.toLowerCase(); + + if (fileExtension && !allowedTypes.includes(fileExtension)) { + return t("reverseShares.upload.errors.fileTypeNotAllowed", { + allowedTypes: reverseShare.allowedFileTypes, + }); + } + return null; + }; + + const validateFileCount = (): string | null => { + if (!reverseShare.maxFiles) return null; + + const totalFiles = files.length + 1 + reverseShare.currentFileCount; + if (totalFiles > reverseShare.maxFiles) { + return t("reverseShares.upload.errors.maxFilesExceeded", { + maxFiles: reverseShare.maxFiles, + }); + } + return null; + }; + + const validateFile = (file: File): string | null => { + return validateFileSize(file) || validateFileType(file) || validateFileCount(); + }; + + const createFileWithProgress = (file: File): FileWithProgress => ({ + file, + progress: UPLOAD_PROGRESS.INITIAL, + status: FILE_STATUS.PENDING, + }); + + const processAcceptedFiles = (acceptedFiles: File[]): FileWithProgress[] => { + const validFiles: FileWithProgress[] = []; + + for (const file of acceptedFiles) { + const validationError = validateFile(file); + if (validationError) { + toast.error(validationError); + continue; + } + validFiles.push(createFileWithProgress(file)); + } + + return validFiles; + }; + const onDrop = useCallback( (acceptedFiles: File[]) => { - const newFiles: FileWithProgress[] = []; - - for (const file of acceptedFiles) { - const error = validateFile(file); - if (error) { - toast.error(error); - continue; - } - - newFiles.push({ - file, - progress: 0, - status: "pending", - }); - } - - setFiles((prev) => [...prev, ...newFiles]); + const newFiles = processAcceptedFiles(acceptedFiles); + setFiles((previousFiles) => [...previousFiles, ...newFiles]); }, [files, reverseShare] ); @@ -96,98 +104,132 @@ export function FileUploadSection({ reverseShare, password, alias }: FileUploadS }); const removeFile = (index: number) => { - setFiles((prev) => prev.filter((_, i) => i !== index)); + setFiles((previousFiles) => previousFiles.filter((_, i) => i !== index)); + }; + + const updateFileStatus = (index: number, updates: Partial) => { + setFiles((previousFiles) => previousFiles.map((file, i) => (i === index ? { ...file, ...updates } : file))); + }; + + const generateObjectName = (fileName: string): string => { + const timestamp = Date.now(); + return `reverse-shares/${alias}/${timestamp}-${fileName}`; + }; + + const getFileExtension = (fileName: string): string => { + return fileName.split(".").pop() || ""; + }; + + const uploadFileToStorage = async (file: File, presignedUrl: string): Promise => { + const response = await fetch(presignedUrl, { + method: "PUT", + body: file, + headers: { + "Content-Type": file.type, + }, + }); + + if (!response.ok) { + throw new Error("Failed to upload file to storage"); + } + }; + + const registerUploadedFile = async (file: File, objectName: string): Promise => { + const fileExtension = getFileExtension(file.name); + + await registerFileUploadByAlias( + alias, + { + name: file.name, + description: description || undefined, + extension: fileExtension, + size: file.size, + objectName, + uploaderEmail: uploaderEmail || undefined, + uploaderName: uploaderName || undefined, + }, + password ? { password } : undefined + ); }; const uploadFile = async (fileWithProgress: FileWithProgress, index: number): Promise => { const { file } = fileWithProgress; try { - // Update status to uploading - setFiles((prev) => prev.map((f, i) => (i === index ? { ...f, status: "uploading" as const, progress: 0 } : f))); + // Start upload + updateFileStatus(index, { + status: FILE_STATUS.UPLOADING, + progress: UPLOAD_PROGRESS.INITIAL, + }); - // Generate object name for the file - const timestamp = Date.now(); - const fileExtension = file.name.split(".").pop() || ""; - const objectName = `reverse-shares/${alias}/${timestamp}-${file.name}`; - - // Get presigned URL + // Generate object name and get presigned URL + const objectName = generateObjectName(file.name); const presignedResponse = await getPresignedUrlForUploadByAlias( alias, { objectName }, password ? { password } : undefined ); - const { url } = presignedResponse.data; + // Upload to storage + await uploadFileToStorage(file, presignedResponse.data.url); - // Upload file to presigned URL - const uploadResponse = await fetch(url, { - method: "PUT", - body: file, - headers: { - "Content-Type": file.type, - }, - }); - - if (!uploadResponse.ok) { - throw new Error("Failed to upload file to storage"); - } - - // Update progress to 100% - setFiles((prev) => prev.map((f, i) => (i === index ? { ...f, progress: 100 } : f))); + // Update progress + updateFileStatus(index, { progress: UPLOAD_PROGRESS.COMPLETE }); // Register file upload - await registerFileUploadByAlias( - alias, - { - name: file.name, - description: description || undefined, - extension: fileExtension, - size: file.size, - objectName, - uploaderEmail: uploaderEmail || undefined, - uploaderName: uploaderName || undefined, - }, - password ? { password } : undefined - ); + await registerUploadedFile(file, objectName); - // Update status to success - setFiles((prev) => prev.map((f, i) => (i === index ? { ...f, status: "success" as const } : f))); + // Mark as successful + updateFileStatus(index, { status: FILE_STATUS.SUCCESS }); } catch (error: any) { console.error("Upload error:", error); - const errorMessage = error.response?.data?.error || "Erro ao enviar arquivo"; + const errorMessage = error.response?.data?.error || t("reverseShares.upload.errors.uploadFailed"); - setFiles((prev) => - prev.map((f, i) => (i === index ? { ...f, status: "error" as const, error: errorMessage } : f)) - ); + updateFileStatus(index, { + status: FILE_STATUS.ERROR, + error: errorMessage, + }); toast.error(errorMessage); } }; - const handleUpload = async () => { + const validateUploadRequirements = (): boolean => { if (files.length === 0) { - toast.error("Selecione pelo menos um arquivo"); - return; + toast.error(t("reverseShares.upload.errors.selectAtLeastOneFile")); + return false; } - if (!uploaderName && !uploaderEmail) { - toast.error("Informe seu nome ou e-mail"); - return; + if (!uploaderName.trim() && !uploaderEmail.trim()) { + toast.error(t("reverseShares.upload.errors.provideNameOrEmail")); + return false; } + return true; + }; + + const processAllUploads = async (): Promise => { + const uploadPromises = files.map((fileWithProgress, index) => uploadFile(fileWithProgress, index)); + + await Promise.all(uploadPromises); + + const successfulUploads = files.filter((file) => file.status === FILE_STATUS.SUCCESS); + if (successfulUploads.length > 0) { + toast.success( + t("reverseShares.upload.success.countMessage", { + count: successfulUploads.length, + }) + ); + } + }; + + const handleUpload = async () => { + if (!validateUploadRequirements()) return; + setIsUploading(true); try { - // Upload all files - const uploadPromises = files.map((fileWithProgress, index) => uploadFile(fileWithProgress, index)); - - await Promise.all(uploadPromises); - - const successCount = files.filter((f) => f.status === "success").length; - if (successCount > 0) { - toast.success(`${successCount} arquivo(s) enviado(s) com sucesso!`); - } + await processAllUploads(); } catch (error) { console.error("Upload error:", error); } finally { @@ -196,83 +238,130 @@ export function FileUploadSection({ reverseShare, password, alias }: FileUploadS }; const canUpload = files.length > 0 && (uploaderName.trim() || uploaderEmail.trim()) && !isUploading; - const allFilesProcessed = files.every((f) => f.status === "success" || f.status === "error"); + const allFilesProcessed = files.every( + (file) => file.status === FILE_STATUS.SUCCESS || file.status === FILE_STATUS.ERROR + ); + const hasSuccessfulUploads = files.some((file) => file.status === FILE_STATUS.SUCCESS); + + // Call onUploadSuccess when all files are processed and there are successful uploads + useEffect(() => { + if (allFilesProcessed && hasSuccessfulUploads && files.length > 0) { + onUploadSuccess?.(); + } + }, [allFilesProcessed, hasSuccessfulUploads, files.length, onUploadSuccess]); + + const getDragActiveStyles = () => { + if (isDragActive) { + return "border-green-500 bg-blue-50 dark:bg-green-950/20"; + } + return "border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500"; + }; + + const getDropzoneStyles = () => { + const baseStyles = "border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors"; + const dragStyles = getDragActiveStyles(); + const disabledStyles = isUploading ? "opacity-50 cursor-not-allowed" : ""; + + return `${baseStyles} ${dragStyles} ${disabledStyles}`.trim(); + }; + + const renderFileRestrictions = () => { + // Calculate remaining files that can be uploaded + const calculateRemainingFiles = (): number => { + if (!reverseShare.maxFiles) return 0; + const currentTotal = reverseShare.currentFileCount + files.length; + const remaining = reverseShare.maxFiles - currentTotal; + return Math.max(0, remaining); + }; + + const remainingFiles = calculateRemainingFiles(); + + return ( +

+ {reverseShare.allowedFileTypes && ( + <> + {t("reverseShares.upload.fileDropzone.acceptedTypes", { types: reverseShare.allowedFileTypes })} +
+ + )} + {reverseShare.maxFileSize && ( + <> + {t("reverseShares.upload.fileDropzone.maxFileSize", { size: formatFileSize(reverseShare.maxFileSize) })} +
+ + )} + {reverseShare.maxFiles && ( + <> + {t("reverseShares.upload.fileDropzone.remainingFiles", { + remaining: remainingFiles, + max: reverseShare.maxFiles, + })} + + )} +

+ ); + }; + + const renderFileStatusBadge = (fileWithProgress: FileWithProgress) => { + if (fileWithProgress.status === FILE_STATUS.SUCCESS) { + return ( + + + {t("reverseShares.upload.fileList.statusUploaded")} + + ); + } + + if (fileWithProgress.status === FILE_STATUS.ERROR) { + return {t("reverseShares.upload.fileList.statusError")}; + } + + return null; + }; + + const renderFileItem = (fileWithProgress: FileWithProgress, index: number) => ( +
+ +
+

{fileWithProgress.file.name}

+

{formatFileSize(fileWithProgress.file.size)}

+ {fileWithProgress.status === FILE_STATUS.UPLOADING && ( + + )} + {fileWithProgress.status === FILE_STATUS.ERROR && ( +

{fileWithProgress.error}

+ )} +
+
+ {renderFileStatusBadge(fileWithProgress)} + {fileWithProgress.status === FILE_STATUS.PENDING && ( + + )} +
+
+ ); return (
{/* File Drop Zone */} -
+

- {isDragActive ? "Solte os arquivos aqui" : "Arraste arquivos aqui ou clique para selecionar"} + {isDragActive + ? t("reverseShares.upload.fileDropzone.dragActive") + : t("reverseShares.upload.fileDropzone.dragInactive")}

-

- {reverseShare.allowedFileTypes && ( - <> - Tipos aceitos: {reverseShare.allowedFileTypes} -
- - )} - {reverseShare.maxFileSize && ( - <> - Tamanho máximo: {formatFileSize(reverseShare.maxFileSize)} -
- - )} - {reverseShare.maxFiles && <>Máximo de {reverseShare.maxFiles} arquivos} -

+ {renderFileRestrictions()}
{/* File List */} {files.length > 0 && (
-

Arquivos selecionados:

- {files.map((fileWithProgress, index) => ( -
- -
-

- {fileWithProgress.file.name} -

-

{formatFileSize(fileWithProgress.file.size)}

- {fileWithProgress.status === "uploading" && ( - - )} - {fileWithProgress.status === "error" && ( -

{fileWithProgress.error}

- )} -
-
- {fileWithProgress.status === "success" && ( - - - Enviado - - )} - {fileWithProgress.status === "error" && Erro} - {fileWithProgress.status === "pending" && ( - - )} -
-
- ))} +

{t("reverseShares.upload.fileList.title")}

+ {files.map(renderFileItem)}
)} @@ -282,11 +371,11 @@ export function FileUploadSection({ reverseShare, password, alias }: FileUploadS
setUploaderName(e.target.value)} disabled={isUploading} @@ -295,12 +384,12 @@ export function FileUploadSection({ reverseShare, password, alias }: FileUploadS
setUploaderEmail(e.target.value)} disabled={isUploading} @@ -308,29 +397,31 @@ export function FileUploadSection({ reverseShare, password, alias }: FileUploadS
- +