mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
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.
This commit is contained in:
@@ -1183,7 +1183,7 @@
|
|||||||
},
|
},
|
||||||
"allowedFileTypes": {
|
"allowedFileTypes": {
|
||||||
"label": "Tipos de Arquivo Permitidos",
|
"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."
|
"description": "Opcional. Especifique as extensões permitidas separadas por vírgula."
|
||||||
},
|
},
|
||||||
"pageLayout": {
|
"pageLayout": {
|
||||||
@@ -1227,6 +1227,89 @@
|
|||||||
"confirmButton": "Excluir Link",
|
"confirmButton": "Excluir Link",
|
||||||
"cancelButton": "Cancelar",
|
"cancelButton": "Cancelar",
|
||||||
"deleting": "Excluindo..."
|
"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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,24 +1,122 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
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 { LanguageSwitcher } from "@/components/general/language-switcher";
|
||||||
import { ModeToggle } from "@/components/general/mode-toggle";
|
import { ModeToggle } from "@/components/general/mode-toggle";
|
||||||
import { DefaultFooter } from "@/components/ui/default-footer";
|
import { DefaultFooter } from "@/components/ui/default-footer";
|
||||||
import { useAppInfo } from "@/contexts/app-info-context";
|
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 { FileUploadSection } from "./file-upload-section";
|
||||||
|
import { StatusMessage } from "./shared/status-message";
|
||||||
|
|
||||||
type ReverseShareInfo = GetReverseShareForUploadResult["data"]["reverseShare"];
|
export function DefaultLayout({
|
||||||
|
reverseShare,
|
||||||
interface DefaultLayoutProps {
|
password,
|
||||||
reverseShare: ReverseShareInfo;
|
alias,
|
||||||
password: string;
|
isMaxFilesReached,
|
||||||
alias: string;
|
hasUploadedSuccessfully,
|
||||||
}
|
onUploadSuccess,
|
||||||
|
isLinkInactive,
|
||||||
export function DefaultLayout({ reverseShare, password, alias }: DefaultLayoutProps) {
|
isLinkNotFound,
|
||||||
|
isLinkExpired,
|
||||||
|
}: DefaultLayoutProps) {
|
||||||
const { appName, appLogo } = useAppInfo();
|
const { appName, appLogo } = useAppInfo();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const getUploadStatus = () => {
|
||||||
|
if (hasUploadedSuccessfully) {
|
||||||
|
return {
|
||||||
|
component: (
|
||||||
|
<StatusMessage
|
||||||
|
icon={IconCheck}
|
||||||
|
title={t("reverseShares.upload.success.title")}
|
||||||
|
description={t("reverseShares.upload.success.description")}
|
||||||
|
variant="success"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinkInactive) {
|
||||||
|
return {
|
||||||
|
component: (
|
||||||
|
<StatusMessage
|
||||||
|
icon={IconAlertTriangle}
|
||||||
|
title={t("reverseShares.upload.linkInactive.title")}
|
||||||
|
description={t("reverseShares.upload.linkInactive.description")}
|
||||||
|
additionalText={t("reverseShares.upload.linkInactive.contactOwner")}
|
||||||
|
variant="error"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinkNotFound || !reverseShare) {
|
||||||
|
return {
|
||||||
|
component: (
|
||||||
|
<StatusMessage
|
||||||
|
icon={IconAlertTriangle}
|
||||||
|
title={t("reverseShares.upload.linkNotFound.title")}
|
||||||
|
description={t("reverseShares.upload.linkNotFound.description")}
|
||||||
|
variant="neutral"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinkExpired) {
|
||||||
|
return {
|
||||||
|
component: (
|
||||||
|
<StatusMessage
|
||||||
|
icon={IconClock}
|
||||||
|
title={t("reverseShares.upload.linkExpired.title")}
|
||||||
|
description={t("reverseShares.upload.linkExpired.description")}
|
||||||
|
additionalText={t("reverseShares.upload.linkExpired.contactOwner")}
|
||||||
|
variant="info"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMaxFilesReached) {
|
||||||
|
return {
|
||||||
|
component: (
|
||||||
|
<StatusMessage
|
||||||
|
icon={IconInfoCircle}
|
||||||
|
title={t("reverseShares.upload.maxFilesReached.title")}
|
||||||
|
description={t("reverseShares.upload.maxFilesReached.description", {
|
||||||
|
maxFiles: reverseShare?.maxFiles || 0,
|
||||||
|
})}
|
||||||
|
additionalText={t("reverseShares.upload.maxFilesReached.contactOwner")}
|
||||||
|
variant="warning"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
component: (
|
||||||
|
<FileUploadSection
|
||||||
|
reverseShare={reverseShare}
|
||||||
|
password={password}
|
||||||
|
alias={alias}
|
||||||
|
onUploadSuccess={onUploadSuccess}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const showUploadLimits =
|
||||||
|
!hasUploadedSuccessfully &&
|
||||||
|
!isMaxFilesReached &&
|
||||||
|
!isLinkInactive &&
|
||||||
|
!isLinkNotFound &&
|
||||||
|
!isLinkExpired &&
|
||||||
|
reverseShare &&
|
||||||
|
(reverseShare.maxFiles || reverseShare.maxFileSize || reverseShare.allowedFileTypes);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
@@ -42,9 +140,9 @@ export function DefaultLayout({ reverseShare, password, alias }: DefaultLayoutPr
|
|||||||
{/* Header da página */}
|
{/* Header da página */}
|
||||||
<div className="text-center space-y-4">
|
<div className="text-center space-y-4">
|
||||||
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-foreground">
|
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold text-foreground">
|
||||||
{reverseShare.name || "Enviar Arquivos"}
|
{reverseShare?.name || t("reverseShares.upload.layout.defaultTitle")}
|
||||||
</h1>
|
</h1>
|
||||||
{reverseShare.description && (
|
{reverseShare?.description && (
|
||||||
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
||||||
{reverseShare.description}
|
{reverseShare.description}
|
||||||
</p>
|
</p>
|
||||||
@@ -53,17 +151,23 @@ export function DefaultLayout({ reverseShare, password, alias }: DefaultLayoutPr
|
|||||||
|
|
||||||
{/* Seção de upload */}
|
{/* Seção de upload */}
|
||||||
<div className="bg-card rounded-xl shadow-sm border border-border p-6 md:p-8 lg:p-10">
|
<div className="bg-card rounded-xl shadow-sm border border-border p-6 md:p-8 lg:p-10">
|
||||||
<FileUploadSection reverseShare={reverseShare} password={password} alias={alias} />
|
{getUploadStatus().component}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Informações adicionais (se houver limites) */}
|
{/* Informações adicionais */}
|
||||||
{(reverseShare.maxFiles || reverseShare.maxFileSize || reverseShare.allowedFileTypes) && (
|
{showUploadLimits && (
|
||||||
<div className="bg-muted/30 rounded-lg p-4 space-y-2">
|
<div className="bg-muted/30 rounded-lg p-4 space-y-2">
|
||||||
<h3 className="text-sm font-medium text-foreground">Informações importantes:</h3>
|
<h3 className="text-sm font-medium text-foreground">{t("reverseShares.upload.layout.importantInfo")}</h3>
|
||||||
<div className="text-xs text-muted-foreground space-y-1">
|
<div className="text-xs text-muted-foreground space-y-1">
|
||||||
{reverseShare.maxFiles && <p>• Máximo de {reverseShare.maxFiles} arquivo(s)</p>}
|
{reverseShare?.maxFiles && (
|
||||||
{reverseShare.maxFileSize && <p>• Tamanho máximo por arquivo: {reverseShare.maxFileSize}MB</p>}
|
<p>• {t("reverseShares.upload.layout.maxFiles", { count: reverseShare.maxFiles })}</p>
|
||||||
{reverseShare.allowedFileTypes && <p>• Tipos permitidos: {reverseShare.allowedFileTypes}</p>}
|
)}
|
||||||
|
{reverseShare?.maxFileSize && (
|
||||||
|
<p>• {t("reverseShares.upload.layout.maxFileSize", { size: reverseShare.maxFileSize })}</p>
|
||||||
|
)}
|
||||||
|
{reverseShare?.allowedFileTypes && (
|
||||||
|
<p>• {t("allowedTypes", { types: reverseShare.allowedFileTypes })}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"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 { IconCheck, IconFile, IconMail, IconUpload, IconUser, IconX } from "@tabler/icons-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -12,79 +13,86 @@ import { Label } from "@/components/ui/label";
|
|||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { getPresignedUrlForUploadByAlias, registerFileUploadByAlias } from "@/http/endpoints";
|
import { getPresignedUrlForUploadByAlias, registerFileUploadByAlias } from "@/http/endpoints";
|
||||||
import type { GetReverseShareForUploadResult } from "@/http/endpoints/reverse-shares/types";
|
|
||||||
import { formatFileSize } from "@/utils/format-file-size";
|
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"];
|
export function FileUploadSection({ reverseShare, password, alias, onUploadSuccess }: FileUploadSectionProps) {
|
||||||
|
|
||||||
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) {
|
|
||||||
const [files, setFiles] = useState<FileWithProgress[]>([]);
|
const [files, setFiles] = useState<FileWithProgress[]>([]);
|
||||||
const [uploaderName, setUploaderName] = useState("");
|
const [uploaderName, setUploaderName] = useState("");
|
||||||
const [uploaderEmail, setUploaderEmail] = useState("");
|
const [uploaderEmail, setUploaderEmail] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
|
||||||
const validateFile = (file: File): string | null => {
|
const t = useTranslations();
|
||||||
// Check file size
|
|
||||||
if (reverseShare.maxFileSize) {
|
|
||||||
const maxSize = reverseShare.maxFileSize;
|
|
||||||
if (file.size > maxSize) {
|
|
||||||
return `Arquivo muito grande. Tamanho máximo: ${formatFileSize(maxSize)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check file type
|
const validateFileSize = (file: File): string | null => {
|
||||||
if (reverseShare.allowedFileTypes) {
|
if (!reverseShare.maxFileSize) 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 `Tipo de arquivo não permitido. Tipos aceitos: ${reverseShare.allowedFileTypes}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check file count
|
if (file.size > reverseShare.maxFileSize) {
|
||||||
if (reverseShare.maxFiles) {
|
return t("reverseShares.upload.errors.fileTooLarge", {
|
||||||
const totalFiles = files.length + 1 + reverseShare.currentFileCount;
|
maxSize: formatFileSize(reverseShare.maxFileSize),
|
||||||
if (totalFiles > reverseShare.maxFiles) {
|
});
|
||||||
return `Máximo de ${reverseShare.maxFiles} arquivos permitidos`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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(
|
const onDrop = useCallback(
|
||||||
(acceptedFiles: File[]) => {
|
(acceptedFiles: File[]) => {
|
||||||
const newFiles: FileWithProgress[] = [];
|
const newFiles = processAcceptedFiles(acceptedFiles);
|
||||||
|
setFiles((previousFiles) => [...previousFiles, ...newFiles]);
|
||||||
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]);
|
|
||||||
},
|
},
|
||||||
[files, reverseShare]
|
[files, reverseShare]
|
||||||
);
|
);
|
||||||
@@ -96,98 +104,132 @@ export function FileUploadSection({ reverseShare, password, alias }: FileUploadS
|
|||||||
});
|
});
|
||||||
|
|
||||||
const removeFile = (index: number) => {
|
const removeFile = (index: number) => {
|
||||||
setFiles((prev) => prev.filter((_, i) => i !== index));
|
setFiles((previousFiles) => previousFiles.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFileStatus = (index: number, updates: Partial<FileWithProgress>) => {
|
||||||
|
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<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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<void> => {
|
const uploadFile = async (fileWithProgress: FileWithProgress, index: number): Promise<void> => {
|
||||||
const { file } = fileWithProgress;
|
const { file } = fileWithProgress;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Update status to uploading
|
// Start upload
|
||||||
setFiles((prev) => prev.map((f, i) => (i === index ? { ...f, status: "uploading" as const, progress: 0 } : f)));
|
updateFileStatus(index, {
|
||||||
|
status: FILE_STATUS.UPLOADING,
|
||||||
|
progress: UPLOAD_PROGRESS.INITIAL,
|
||||||
|
});
|
||||||
|
|
||||||
// Generate object name for the file
|
// Generate object name and get presigned URL
|
||||||
const timestamp = Date.now();
|
const objectName = generateObjectName(file.name);
|
||||||
const fileExtension = file.name.split(".").pop() || "";
|
|
||||||
const objectName = `reverse-shares/${alias}/${timestamp}-${file.name}`;
|
|
||||||
|
|
||||||
// Get presigned URL
|
|
||||||
const presignedResponse = await getPresignedUrlForUploadByAlias(
|
const presignedResponse = await getPresignedUrlForUploadByAlias(
|
||||||
alias,
|
alias,
|
||||||
{ objectName },
|
{ objectName },
|
||||||
password ? { password } : undefined
|
password ? { password } : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const { url } = presignedResponse.data;
|
// Upload to storage
|
||||||
|
await uploadFileToStorage(file, presignedResponse.data.url);
|
||||||
|
|
||||||
// Upload file to presigned URL
|
// Update progress
|
||||||
const uploadResponse = await fetch(url, {
|
updateFileStatus(index, { progress: UPLOAD_PROGRESS.COMPLETE });
|
||||||
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)));
|
|
||||||
|
|
||||||
// Register file upload
|
// Register file upload
|
||||||
await registerFileUploadByAlias(
|
await registerUploadedFile(file, objectName);
|
||||||
alias,
|
|
||||||
{
|
|
||||||
name: file.name,
|
|
||||||
description: description || undefined,
|
|
||||||
extension: fileExtension,
|
|
||||||
size: file.size,
|
|
||||||
objectName,
|
|
||||||
uploaderEmail: uploaderEmail || undefined,
|
|
||||||
uploaderName: uploaderName || undefined,
|
|
||||||
},
|
|
||||||
password ? { password } : undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update status to success
|
// Mark as successful
|
||||||
setFiles((prev) => prev.map((f, i) => (i === index ? { ...f, status: "success" as const } : f)));
|
updateFileStatus(index, { status: FILE_STATUS.SUCCESS });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Upload error:", error);
|
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) =>
|
updateFileStatus(index, {
|
||||||
prev.map((f, i) => (i === index ? { ...f, status: "error" as const, error: errorMessage } : f))
|
status: FILE_STATUS.ERROR,
|
||||||
);
|
error: errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const validateUploadRequirements = (): boolean => {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
toast.error("Selecione pelo menos um arquivo");
|
toast.error(t("reverseShares.upload.errors.selectAtLeastOneFile"));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!uploaderName && !uploaderEmail) {
|
if (!uploaderName.trim() && !uploaderEmail.trim()) {
|
||||||
toast.error("Informe seu nome ou e-mail");
|
toast.error(t("reverseShares.upload.errors.provideNameOrEmail"));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const processAllUploads = async (): Promise<void> => {
|
||||||
|
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);
|
setIsUploading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Upload all files
|
await processAllUploads();
|
||||||
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!`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Upload error:", error);
|
console.error("Upload error:", error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -196,83 +238,130 @@ export function FileUploadSection({ reverseShare, password, alias }: FileUploadS
|
|||||||
};
|
};
|
||||||
|
|
||||||
const canUpload = files.length > 0 && (uploaderName.trim() || uploaderEmail.trim()) && !isUploading;
|
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 (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{reverseShare.allowedFileTypes && (
|
||||||
|
<>
|
||||||
|
{t("reverseShares.upload.fileDropzone.acceptedTypes", { types: reverseShare.allowedFileTypes })}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{reverseShare.maxFileSize && (
|
||||||
|
<>
|
||||||
|
{t("reverseShares.upload.fileDropzone.maxFileSize", { size: formatFileSize(reverseShare.maxFileSize) })}
|
||||||
|
<br />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{reverseShare.maxFiles && (
|
||||||
|
<>
|
||||||
|
{t("reverseShares.upload.fileDropzone.remainingFiles", {
|
||||||
|
remaining: remainingFiles,
|
||||||
|
max: reverseShare.maxFiles,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFileStatusBadge = (fileWithProgress: FileWithProgress) => {
|
||||||
|
if (fileWithProgress.status === FILE_STATUS.SUCCESS) {
|
||||||
|
return (
|
||||||
|
<Badge variant="default" className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
|
||||||
|
<IconCheck className="h-3 w-3 mr-1" />
|
||||||
|
{t("reverseShares.upload.fileList.statusUploaded")}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileWithProgress.status === FILE_STATUS.ERROR) {
|
||||||
|
return <Badge variant="destructive">{t("reverseShares.upload.fileList.statusError")}</Badge>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderFileItem = (fileWithProgress: FileWithProgress, index: number) => (
|
||||||
|
<div key={index} className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||||
|
<IconFile className="h-5 w-5 text-gray-500 flex-shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">{fileWithProgress.file.name}</p>
|
||||||
|
<p className="text-xs text-gray-500">{formatFileSize(fileWithProgress.file.size)}</p>
|
||||||
|
{fileWithProgress.status === FILE_STATUS.UPLOADING && (
|
||||||
|
<Progress value={fileWithProgress.progress} className="mt-2 h-2" />
|
||||||
|
)}
|
||||||
|
{fileWithProgress.status === FILE_STATUS.ERROR && (
|
||||||
|
<p className="text-xs text-red-500 mt-1">{fileWithProgress.error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{renderFileStatusBadge(fileWithProgress)}
|
||||||
|
{fileWithProgress.status === FILE_STATUS.PENDING && (
|
||||||
|
<Button size="sm" variant="ghost" onClick={() => removeFile(index)} disabled={isUploading}>
|
||||||
|
<IconX className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* File Drop Zone */}
|
{/* File Drop Zone */}
|
||||||
<div
|
<div {...getRootProps()} className={getDropzoneStyles()}>
|
||||||
{...getRootProps()}
|
|
||||||
className={`
|
|
||||||
border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors
|
|
||||||
${
|
|
||||||
isDragActive
|
|
||||||
? "border-green-500 bg-blue-50 dark:bg-green-950/20"
|
|
||||||
: "border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500"
|
|
||||||
}
|
|
||||||
${isUploading ? "opacity-50 cursor-not-allowed" : ""}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<input {...getInputProps()} />
|
<input {...getInputProps()} />
|
||||||
<IconUpload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
<IconUpload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
|
||||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||||
{isDragActive ? "Solte os arquivos aqui" : "Arraste arquivos aqui ou clique para selecionar"}
|
{isDragActive
|
||||||
|
? t("reverseShares.upload.fileDropzone.dragActive")
|
||||||
|
: t("reverseShares.upload.fileDropzone.dragInactive")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
{renderFileRestrictions()}
|
||||||
{reverseShare.allowedFileTypes && (
|
|
||||||
<>
|
|
||||||
Tipos aceitos: {reverseShare.allowedFileTypes}
|
|
||||||
<br />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{reverseShare.maxFileSize && (
|
|
||||||
<>
|
|
||||||
Tamanho máximo: {formatFileSize(reverseShare.maxFileSize)}
|
|
||||||
<br />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{reverseShare.maxFiles && <>Máximo de {reverseShare.maxFiles} arquivos</>}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* File List */}
|
{/* File List */}
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white">Arquivos selecionados:</h4>
|
<h4 className="font-medium text-gray-900 dark:text-white">{t("reverseShares.upload.fileList.title")}</h4>
|
||||||
{files.map((fileWithProgress, index) => (
|
{files.map(renderFileItem)}
|
||||||
<div key={index} className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
||||||
<IconFile className="h-5 w-5 text-gray-500 flex-shrink-0" />
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
||||||
{fileWithProgress.file.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500">{formatFileSize(fileWithProgress.file.size)}</p>
|
|
||||||
{fileWithProgress.status === "uploading" && (
|
|
||||||
<Progress value={fileWithProgress.progress} className="mt-2 h-2" />
|
|
||||||
)}
|
|
||||||
{fileWithProgress.status === "error" && (
|
|
||||||
<p className="text-xs text-red-500 mt-1">{fileWithProgress.error}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{fileWithProgress.status === "success" && (
|
|
||||||
<Badge
|
|
||||||
variant="default"
|
|
||||||
className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
|
|
||||||
>
|
|
||||||
<IconCheck className="h-3 w-3 mr-1" />
|
|
||||||
Enviado
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{fileWithProgress.status === "error" && <Badge variant="destructive">Erro</Badge>}
|
|
||||||
{fileWithProgress.status === "pending" && (
|
|
||||||
<Button size="sm" variant="ghost" onClick={() => removeFile(index)} disabled={isUploading}>
|
|
||||||
<IconX className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -282,11 +371,11 @@ export function FileUploadSection({ reverseShare, password, alias }: FileUploadS
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="name">
|
<Label htmlFor="name">
|
||||||
<IconUser className="inline h-4 w-4" />
|
<IconUser className="inline h-4 w-4" />
|
||||||
Nome
|
{t("reverseShares.upload.form.nameLabel")}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
placeholder="Seu nome"
|
placeholder={t("reverseShares.upload.form.namePlaceholder")}
|
||||||
value={uploaderName}
|
value={uploaderName}
|
||||||
onChange={(e) => setUploaderName(e.target.value)}
|
onChange={(e) => setUploaderName(e.target.value)}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
@@ -295,12 +384,12 @@ export function FileUploadSection({ reverseShare, password, alias }: FileUploadS
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="email">
|
<Label htmlFor="email">
|
||||||
<IconMail className="inline h-4 w-4" />
|
<IconMail className="inline h-4 w-4" />
|
||||||
E-mail
|
{t("reverseShares.upload.form.emailLabel")}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="seu@email.com"
|
placeholder={t("reverseShares.upload.form.emailPlaceholder")}
|
||||||
value={uploaderEmail}
|
value={uploaderEmail}
|
||||||
onChange={(e) => setUploaderEmail(e.target.value)}
|
onChange={(e) => setUploaderEmail(e.target.value)}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
@@ -308,29 +397,31 @@ export function FileUploadSection({ reverseShare, password, alias }: FileUploadS
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="description">Descrição (opcional)</Label>
|
<Label htmlFor="description">{t("reverseShares.upload.form.descriptionLabel")}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
placeholder="Adicione uma descrição aos arquivos..."
|
placeholder={t("reverseShares.upload.form.descriptionPlaceholder")}
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
rows={3}
|
rows={UPLOAD_CONFIG.TEXTAREA_ROWS}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upload Button */}
|
{/* Upload Button */}
|
||||||
<Button onClick={handleUpload} disabled={!canUpload} className="w-full text-white" size="lg" variant="default">
|
<Button onClick={handleUpload} disabled={!canUpload} className="w-full text-white" size="lg" variant="default">
|
||||||
{isUploading ? "Enviando..." : `Enviar ${files.length} arquivo(s)`}
|
{isUploading
|
||||||
|
? t("reverseShares.upload.form.uploading")
|
||||||
|
: t("reverseShares.upload.form.uploadButton", { count: files.length })}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Success Message */}
|
{/* Success Message */}
|
||||||
{allFilesProcessed && files.some((f) => f.status === "success") && (
|
{allFilesProcessed && hasSuccessfulUploads && (
|
||||||
<div className="text-center p-4 bg-green-50 dark:bg-green-950/20 rounded-lg">
|
<div className="text-center p-4 bg-green-50 dark:bg-green-950/20 rounded-lg">
|
||||||
<p className="text-green-800 dark:text-green-200 font-medium">Arquivos enviados com sucesso! 🎉</p>
|
<p className="text-green-800 dark:text-green-200 font-medium">{t("reverseShares.upload.success.title")}</p>
|
||||||
<p className="text-sm text-green-600 dark:text-green-300 mt-1">
|
<p className="text-sm text-green-600 dark:text-green-300 mt-1">
|
||||||
Você pode fechar esta página ou enviar mais arquivos.
|
{t("reverseShares.upload.success.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
6
apps/web/src/app/(shares)/r/[alias]/components/index.ts
Normal file
6
apps/web/src/app/(shares)/r/[alias]/components/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export { DefaultLayout } from "./default-layout";
|
||||||
|
export { WeTransferLayout } from "./we-transfer-layout";
|
||||||
|
export { PasswordModal } from "./password-modal";
|
||||||
|
export { FileUploadSection } from "./file-upload-section";
|
||||||
|
export { StatusMessage, WeTransferStatusMessage } from "./shared/status-message";
|
||||||
|
export { TransparentFooter } from "./transparent-footer";
|
@@ -2,21 +2,18 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { IconLock } from "@tabler/icons-react";
|
import { IconLock } from "@tabler/icons-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { PasswordModalProps } from "../types";
|
||||||
interface PasswordModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onSubmit: (password: string) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PasswordModal({ isOpen, onSubmit, onClose }: PasswordModalProps) {
|
export function PasswordModal({ isOpen, onSubmit, onClose }: PasswordModalProps) {
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -43,17 +40,17 @@ export function PasswordModal({ isOpen, onSubmit, onClose }: PasswordModalProps)
|
|||||||
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-yellow-100 dark:bg-yellow-900/20">
|
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-yellow-100 dark:bg-yellow-900/20">
|
||||||
<IconLock className="h-6 w-6 text-yellow-600 dark:text-yellow-400" />
|
<IconLock className="h-6 w-6 text-yellow-600 dark:text-yellow-400" />
|
||||||
</div>
|
</div>
|
||||||
<DialogTitle>Link Protegido</DialogTitle>
|
<DialogTitle>{t("reverseShares.upload.password.title")}</DialogTitle>
|
||||||
<DialogDescription>Este link está protegido por senha. Digite a senha para continuar.</DialogDescription>
|
<DialogDescription>{t("reverseShares.upload.password.description")}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="password">Senha</Label>
|
<Label htmlFor="password">{t("reverseShares.upload.password.label")}</Label>
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Digite a senha"
|
placeholder={t("reverseShares.upload.password.placeholder")}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@@ -64,10 +61,10 @@ export function PasswordModal({ isOpen, onSubmit, onClose }: PasswordModalProps)
|
|||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button type="button" variant="outline" className="flex-1" onClick={onClose} disabled={isSubmitting}>
|
<Button type="button" variant="outline" className="flex-1" onClick={onClose} disabled={isSubmitting}>
|
||||||
Cancelar
|
{t("reverseShares.upload.password.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" className="flex-1" disabled={!password.trim() || isSubmitting}>
|
<Button type="submit" className="flex-1" disabled={!password.trim() || isSubmitting}>
|
||||||
{isSubmitting ? "Verificando..." : "Continuar"}
|
{isSubmitting ? t("reverseShares.upload.password.verifying") : t("reverseShares.upload.password.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -0,0 +1,101 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { MESSAGE_TYPES, STATUS_VARIANTS } from "../../constants";
|
||||||
|
import type { ReverseShareInfo } from "../../types";
|
||||||
|
|
||||||
|
interface StatusMessageProps {
|
||||||
|
variant: "success" | "warning" | "error" | "info" | "neutral";
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
additionalText?: string;
|
||||||
|
size?: "default" | "compact";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WeTransferStatusMessageProps {
|
||||||
|
type: keyof typeof MESSAGE_TYPES;
|
||||||
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
|
titleKey: string;
|
||||||
|
descriptionKey: string;
|
||||||
|
showContactOwner?: boolean;
|
||||||
|
reverseShare?: ReverseShareInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusMessage({
|
||||||
|
icon: Icon,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
additionalText,
|
||||||
|
variant,
|
||||||
|
size = "default",
|
||||||
|
}: StatusMessageProps) {
|
||||||
|
const styles = STATUS_VARIANTS[variant];
|
||||||
|
const isCompact = size === "compact";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`text-center space-y-4 ${isCompact ? "py-6" : "py-8"}`}>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className={`${styles.iconBg} p-3 rounded-full`}>
|
||||||
|
<Icon className={`${isCompact ? "h-6 w-6" : "h-8 w-8"} ${styles.iconColor}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className={`${isCompact ? "text-lg" : "text-xl"} font-semibold ${styles.titleColor}`}>{title}</h3>
|
||||||
|
<p
|
||||||
|
className={`${styles.descriptionColor} ${isCompact ? "text-sm" : ""} ${isCompact ? "" : "max-w-md mx-auto"}`}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
{additionalText && (
|
||||||
|
<p className={`${isCompact ? "text-xs" : "text-sm"} text-muted-foreground`}>{additionalText}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WeTransferStatusMessage({
|
||||||
|
type,
|
||||||
|
icon: Icon,
|
||||||
|
titleKey,
|
||||||
|
descriptionKey,
|
||||||
|
showContactOwner = false,
|
||||||
|
reverseShare,
|
||||||
|
}: WeTransferStatusMessageProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
// Map message types to variants
|
||||||
|
const getVariant = (): "success" | "warning" | "error" | "info" | "neutral" => {
|
||||||
|
switch (type) {
|
||||||
|
case MESSAGE_TYPES.SUCCESS:
|
||||||
|
return "success";
|
||||||
|
case MESSAGE_TYPES.MAX_FILES:
|
||||||
|
return "warning";
|
||||||
|
case MESSAGE_TYPES.INACTIVE:
|
||||||
|
return "error";
|
||||||
|
case MESSAGE_TYPES.EXPIRED:
|
||||||
|
return "info";
|
||||||
|
case MESSAGE_TYPES.NOT_FOUND:
|
||||||
|
default:
|
||||||
|
return "neutral";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const description =
|
||||||
|
type === MESSAGE_TYPES.MAX_FILES ? t(descriptionKey, { maxFiles: reverseShare?.maxFiles || 0 }) : t(descriptionKey);
|
||||||
|
|
||||||
|
const additionalText = showContactOwner ? t("reverseShares.upload.maxFilesReached.contactOwner") : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusMessage
|
||||||
|
variant={getVariant()}
|
||||||
|
icon={Icon}
|
||||||
|
title={t(titleKey)}
|
||||||
|
description={description}
|
||||||
|
additionalText={additionalText}
|
||||||
|
size="compact"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -0,0 +1,27 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import { version } from "../../../../../../package.json";
|
||||||
|
|
||||||
|
export function TransparentFooter() {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="absolute bottom-0 left-0 right-0 z-50 w-full flex items-center justify-center py-3 h-16 pointer-events-none">
|
||||||
|
<div className="flex flex-col items-center pointer-events-auto">
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
className="flex items-center gap-1 text-white/80 hover:text-primary transition-colors"
|
||||||
|
href="https://kyantech.com.br"
|
||||||
|
title={t("footer.kyanHomepage")}
|
||||||
|
>
|
||||||
|
<span className="text-white/70 text-xs sm:text-sm">{t("footer.poweredBy")}</span>
|
||||||
|
<p className="text-white text-xs sm:text-sm font-medium cursor-pointer hover:text-primary">
|
||||||
|
Kyantech Solutions
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
<span className="text-white text-[11px] mt-1">v{version}</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
@@ -1,153 +1,194 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import { IconAlertTriangle, IconCheck, IconClock, IconInfoCircle } from "@tabler/icons-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
import { LanguageSwitcher } from "@/components/general/language-switcher";
|
import { LanguageSwitcher } from "@/components/general/language-switcher";
|
||||||
import { ModeToggle } from "@/components/general/mode-toggle";
|
import { ModeToggle } from "@/components/general/mode-toggle";
|
||||||
import type { GetReverseShareForUploadResult } from "@/http/endpoints/reverse-shares/types";
|
import { BACKGROUND_IMAGES, MESSAGE_TYPES } from "../constants";
|
||||||
import { version } from "../../../../../../package.json";
|
import { WeTransferLayoutProps } from "../types";
|
||||||
import { FileUploadSection } from "./file-upload-section";
|
import { FileUploadSection } from "./file-upload-section";
|
||||||
|
import { WeTransferStatusMessage } from "./shared/status-message";
|
||||||
type ReverseShareInfo = GetReverseShareForUploadResult["data"]["reverseShare"];
|
import { TransparentFooter } from "./transparent-footer";
|
||||||
|
|
||||||
interface WeTransferLayoutProps {
|
|
||||||
reverseShare: ReverseShareInfo;
|
|
||||||
password: string;
|
|
||||||
alias: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lista de imagens de background
|
|
||||||
const backgroundImages = [
|
|
||||||
"/assets/wetransfer-bgs/1.jpg",
|
|
||||||
"/assets/wetransfer-bgs/2.jpg",
|
|
||||||
"/assets/wetransfer-bgs/3.jpg",
|
|
||||||
"/assets/wetransfer-bgs/4.jpg",
|
|
||||||
"/assets/wetransfer-bgs/5.jpg",
|
|
||||||
"/assets/wetransfer-bgs/6.jpg",
|
|
||||||
"/assets/wetransfer-bgs/7.jpg",
|
|
||||||
"/assets/wetransfer-bgs/8.jpg",
|
|
||||||
];
|
|
||||||
|
|
||||||
// Função para escolher uma imagem aleatória
|
// Função para escolher uma imagem aleatória
|
||||||
function getRandomImage(images: string[]): string {
|
const getRandomBackgroundImage = (): string => {
|
||||||
const randomIndex = Math.floor(Math.random() * images.length);
|
const randomIndex = Math.floor(Math.random() * BACKGROUND_IMAGES.length);
|
||||||
const selectedImage = images[randomIndex];
|
return BACKGROUND_IMAGES[randomIndex];
|
||||||
|
};
|
||||||
|
|
||||||
return selectedImage;
|
// Hook para gerenciar a imagem de background
|
||||||
}
|
const useBackgroundImage = () => {
|
||||||
|
|
||||||
// Footer transparente para o layout WeTransfer
|
|
||||||
function TransparentFooter() {
|
|
||||||
const t = useTranslations();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<footer className="absolute bottom-0 left-0 right-0 z-50 w-full flex items-center justify-center py-3 h-16 pointer-events-none">
|
|
||||||
<div className="flex flex-col items-center pointer-events-auto">
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
className="flex items-center gap-1 text-white/80 hover:text-primary transition-colors"
|
|
||||||
href="https://kyantech.com.br"
|
|
||||||
title={t("footer.kyanHomepage")}
|
|
||||||
>
|
|
||||||
<span className="text-white/70 text-xs sm:text-sm">{t("footer.poweredBy")}</span>
|
|
||||||
<p className="text-white text-xs sm:text-sm font-medium cursor-pointer hover:text-primary">
|
|
||||||
Kyantech Solutions
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
<span className="text-white text-[11px] mt-1">v{version}</span>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WeTransferLayout({ reverseShare, password, alias }: WeTransferLayoutProps) {
|
|
||||||
const [selectedImage, setSelectedImage] = useState<string>("");
|
const [selectedImage, setSelectedImage] = useState<string>("");
|
||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
|
|
||||||
// Escolher uma imagem aleatória no início
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const randomImage = getRandomImage(backgroundImages);
|
setSelectedImage(getRandomBackgroundImage());
|
||||||
setSelectedImage(randomImage);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Precarregar a imagem selecionada
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedImage) return;
|
if (!selectedImage) return;
|
||||||
|
|
||||||
const preloadImage = () => {
|
const img = new Image();
|
||||||
const img = new Image();
|
img.onload = () => setImageLoaded(true);
|
||||||
img.onload = () => setImageLoaded(true);
|
img.onerror = () => {
|
||||||
img.onerror = () => {
|
console.error("Error loading background image:", selectedImage);
|
||||||
console.error("Erro ao carregar imagem de background:", selectedImage);
|
setImageLoaded(true);
|
||||||
setImageLoaded(true);
|
|
||||||
};
|
|
||||||
img.src = selectedImage;
|
|
||||||
};
|
};
|
||||||
|
img.src = selectedImage;
|
||||||
preloadImage();
|
|
||||||
}, [selectedImage]);
|
}, [selectedImage]);
|
||||||
|
|
||||||
|
return { selectedImage, imageLoaded };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Componente para controles do header
|
||||||
|
const HeaderControls = () => (
|
||||||
|
<div className="absolute top-4 right-4 md:top-6 md:right-6 z-40 flex items-center gap-2">
|
||||||
|
<div className="bg-white/10 dark:bg-black/20 backdrop-blur-xs border border-white/20 dark:border-white/10 rounded-lg p-1">
|
||||||
|
<LanguageSwitcher />
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/10 dark:bg-black/20 backdrop-blur-xs border border-white/20 dark:border-white/10 rounded-lg p-1">
|
||||||
|
<ModeToggle />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Componente para o fundo com imagem
|
||||||
|
const BackgroundLayer = ({ selectedImage, imageLoaded }: { selectedImage: string; imageLoaded: boolean }) => (
|
||||||
|
<>
|
||||||
|
<div className="absolute inset-0 z-0 bg-background" />
|
||||||
|
{imageLoaded && selectedImage && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-10"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${selectedImage})`,
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="absolute inset-0 bg-black/40 z-20" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export function WeTransferLayout({
|
||||||
|
reverseShare,
|
||||||
|
password,
|
||||||
|
alias,
|
||||||
|
isMaxFilesReached,
|
||||||
|
hasUploadedSuccessfully,
|
||||||
|
onUploadSuccess,
|
||||||
|
isLinkInactive,
|
||||||
|
isLinkNotFound,
|
||||||
|
isLinkExpired,
|
||||||
|
}: WeTransferLayoutProps) {
|
||||||
|
const { selectedImage, imageLoaded } = useBackgroundImage();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const getUploadSectionContent = () => {
|
||||||
|
if (hasUploadedSuccessfully) {
|
||||||
|
return (
|
||||||
|
<WeTransferStatusMessage
|
||||||
|
type={MESSAGE_TYPES.SUCCESS}
|
||||||
|
icon={IconCheck}
|
||||||
|
titleKey="reverseShares.upload.success.title"
|
||||||
|
descriptionKey="reverseShares.upload.success.description"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinkInactive) {
|
||||||
|
return (
|
||||||
|
<WeTransferStatusMessage
|
||||||
|
type={MESSAGE_TYPES.INACTIVE}
|
||||||
|
icon={IconAlertTriangle}
|
||||||
|
titleKey="reverseShares.upload.linkInactive.title"
|
||||||
|
descriptionKey="reverseShares.upload.linkInactive.description"
|
||||||
|
showContactOwner
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinkNotFound || !reverseShare) {
|
||||||
|
return (
|
||||||
|
<WeTransferStatusMessage
|
||||||
|
type={MESSAGE_TYPES.NOT_FOUND}
|
||||||
|
icon={IconAlertTriangle}
|
||||||
|
titleKey="reverseShares.upload.linkNotFound.title"
|
||||||
|
descriptionKey="reverseShares.upload.linkNotFound.description"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinkExpired) {
|
||||||
|
return (
|
||||||
|
<WeTransferStatusMessage
|
||||||
|
type={MESSAGE_TYPES.EXPIRED}
|
||||||
|
icon={IconClock}
|
||||||
|
titleKey="reverseShares.upload.linkExpired.title"
|
||||||
|
descriptionKey="reverseShares.upload.linkExpired.description"
|
||||||
|
showContactOwner
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMaxFilesReached) {
|
||||||
|
return (
|
||||||
|
<WeTransferStatusMessage
|
||||||
|
type={MESSAGE_TYPES.MAX_FILES}
|
||||||
|
icon={IconInfoCircle}
|
||||||
|
titleKey="reverseShares.upload.maxFilesReached.title"
|
||||||
|
descriptionKey="reverseShares.upload.maxFilesReached.description"
|
||||||
|
showContactOwner
|
||||||
|
reverseShare={reverseShare}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileUploadSection
|
||||||
|
reverseShare={reverseShare}
|
||||||
|
password={password}
|
||||||
|
alias={alias}
|
||||||
|
onUploadSuccess={onUploadSuccess}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen relative overflow-hidden">
|
<div className="min-h-screen relative overflow-hidden">
|
||||||
{/* Fallback gradient background */}
|
<BackgroundLayer selectedImage={selectedImage} imageLoaded={imageLoaded} />
|
||||||
<div className="absolute inset-0 z-0 bg-background" />
|
<HeaderControls />
|
||||||
|
|
||||||
{/* Background Image - imagem única aleatória */}
|
{/* Loading indicator */}
|
||||||
{imageLoaded && selectedImage && (
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 z-10"
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url(${selectedImage})`,
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
backgroundRepeat: "no-repeat",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Dark overlay para melhor legibilidade */}
|
|
||||||
<div className="absolute inset-0 bg-black/40 z-20" />
|
|
||||||
|
|
||||||
{/* Controles - Topo Direito */}
|
|
||||||
<div className="absolute top-4 right-4 md:top-6 md:right-6 z-40 flex items-center gap-2">
|
|
||||||
<div className="bg-white/10 dark:bg-black/20 backdrop-blur-xs border border-white/20 dark:border-white/10 rounded-lg p-1">
|
|
||||||
<LanguageSwitcher />
|
|
||||||
</div>
|
|
||||||
<div className="bg-white/10 dark:bg-black/20 backdrop-blur-xs border border-white/20 dark:border-white/10 rounded-lg p-1">
|
|
||||||
<ModeToggle />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Loading indicator para a imagem */}
|
|
||||||
{!imageLoaded && (
|
{!imageLoaded && (
|
||||||
<div className="absolute inset-0 z-30 flex items-center justify-center">
|
<div className="absolute inset-0 z-30 flex items-center justify-center">
|
||||||
<div className="animate-pulse text-white/70 text-sm">Carregando...</div>
|
<div className="animate-pulse text-white/70 text-sm">{t("reverseShares.upload.layout.loading")}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Content - Alinhado à esquerda */}
|
{/* Main Content */}
|
||||||
<div className="relative z-30 min-h-screen flex items-center justify-start p-4 md:p-8 lg:p-12 xl:p-16">
|
<div className="relative z-30 min-h-screen flex items-center justify-start p-4 md:p-8 lg:p-12 xl:p-16">
|
||||||
<div className="w-full max-w-md lg:max-w-lg xl:max-w-xl">
|
<div className="w-full max-w-md lg:max-w-lg xl:max-w-xl">
|
||||||
<div className="bg-white dark:bg-black rounded-2xl shadow-2xl p-6 md:p-8 backdrop-blur-sm border border-white/20">
|
<div className="bg-white dark:bg-black rounded-2xl shadow-2xl p-6 md:p-8 backdrop-blur-sm border border-white/20">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-left mb-6 md:mb-8">
|
<div className="text-left mb-6 md:mb-8">
|
||||||
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
{reverseShare.name || "Enviar Arquivos"}
|
{reverseShare?.name || t("reverseShares.upload.layout.defaultTitle")}
|
||||||
</h1>
|
</h1>
|
||||||
{reverseShare.description && (
|
{reverseShare?.description && (
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-sm md:text-base">{reverseShare.description}</p>
|
<p className="text-gray-600 dark:text-gray-300 text-sm md:text-base">{reverseShare.description}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upload Section */}
|
{/* Upload Section */}
|
||||||
<FileUploadSection reverseShare={reverseShare} password={password} alias={alias} />
|
{getUploadSectionContent()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer transparente */}
|
|
||||||
<TransparentFooter />
|
<TransparentFooter />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
84
apps/web/src/app/(shares)/r/[alias]/constants/index.ts
Normal file
84
apps/web/src/app/(shares)/r/[alias]/constants/index.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// HTTP Status Constants
|
||||||
|
export const HTTP_STATUS = {
|
||||||
|
UNAUTHORIZED: 401,
|
||||||
|
FORBIDDEN: 403,
|
||||||
|
NOT_FOUND: 404,
|
||||||
|
GONE: 410,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Error Messages
|
||||||
|
export const ERROR_MESSAGES = {
|
||||||
|
PASSWORD_REQUIRED: "Password required",
|
||||||
|
INVALID_PASSWORD: "Invalid password",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Error types
|
||||||
|
export type ErrorType = "inactive" | "notFound" | "expired" | "generic" | null;
|
||||||
|
|
||||||
|
export const STATUS_VARIANTS = {
|
||||||
|
success: {
|
||||||
|
iconBg: "bg-green-100 dark:bg-green-900/20",
|
||||||
|
iconColor: "text-green-600 dark:text-green-400",
|
||||||
|
titleColor: "text-green-800 dark:text-green-200",
|
||||||
|
descriptionColor: "text-green-600 dark:text-green-300",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
iconBg: "bg-amber-100 dark:bg-amber-900/20",
|
||||||
|
iconColor: "text-amber-600 dark:text-amber-400",
|
||||||
|
titleColor: "text-foreground",
|
||||||
|
descriptionColor: "text-muted-foreground",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
iconBg: "bg-red-100 dark:bg-red-900/20",
|
||||||
|
iconColor: "text-red-600 dark:text-red-400",
|
||||||
|
titleColor: "text-foreground",
|
||||||
|
descriptionColor: "text-muted-foreground",
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
iconBg: "bg-orange-100 dark:bg-orange-900/20",
|
||||||
|
iconColor: "text-orange-600 dark:text-orange-400",
|
||||||
|
titleColor: "text-foreground",
|
||||||
|
descriptionColor: "text-muted-foreground",
|
||||||
|
},
|
||||||
|
neutral: {
|
||||||
|
iconBg: "bg-gray-100 dark:bg-gray-900/20",
|
||||||
|
iconColor: "text-gray-600 dark:text-gray-400",
|
||||||
|
titleColor: "text-foreground",
|
||||||
|
descriptionColor: "text-muted-foreground",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UPLOAD_PROGRESS = {
|
||||||
|
INITIAL: 0,
|
||||||
|
COMPLETE: 100,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const FILE_STATUS = {
|
||||||
|
PENDING: "pending",
|
||||||
|
UPLOADING: "uploading",
|
||||||
|
SUCCESS: "success",
|
||||||
|
ERROR: "error",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const UPLOAD_CONFIG = {
|
||||||
|
TEXTAREA_ROWS: 3,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const MESSAGE_TYPES = {
|
||||||
|
SUCCESS: "SUCCESS",
|
||||||
|
MAX_FILES: "MAX_FILES",
|
||||||
|
INACTIVE: "INACTIVE",
|
||||||
|
NOT_FOUND: "NOT_FOUND",
|
||||||
|
EXPIRED: "EXPIRED",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const BACKGROUND_IMAGES = [
|
||||||
|
"/assets/wetransfer-bgs/1.jpg",
|
||||||
|
"/assets/wetransfer-bgs/2.jpg",
|
||||||
|
"/assets/wetransfer-bgs/3.jpg",
|
||||||
|
"/assets/wetransfer-bgs/4.jpg",
|
||||||
|
"/assets/wetransfer-bgs/5.jpg",
|
||||||
|
"/assets/wetransfer-bgs/6.jpg",
|
||||||
|
"/assets/wetransfer-bgs/7.jpg",
|
||||||
|
"/assets/wetransfer-bgs/8.jpg",
|
||||||
|
] as const;
|
@@ -0,0 +1,150 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { getReverseShareForUploadByAlias } from "@/http/endpoints";
|
||||||
|
import { ERROR_MESSAGES, HTTP_STATUS, type ErrorType } from "../constants";
|
||||||
|
import type { ReverseShareInfo } from "../types";
|
||||||
|
|
||||||
|
interface UseReverseShareUploadProps {
|
||||||
|
alias: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReverseShareUpload({ alias }: UseReverseShareUploadProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
// States
|
||||||
|
const [reverseShare, setReverseShare] = useState<ReverseShareInfo | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
|
||||||
|
const [currentPassword, setCurrentPassword] = useState("");
|
||||||
|
const [hasUploadedSuccessfully, setHasUploadedSuccessfully] = useState(false);
|
||||||
|
const [error, setError] = useState<{ type: ErrorType }>({ type: null });
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
const redirectToHome = () => router.push("/");
|
||||||
|
|
||||||
|
const checkIfMaxFilesReached = (reverseShareData: ReverseShareInfo): boolean => {
|
||||||
|
if (!reverseShareData.maxFiles) return false;
|
||||||
|
return reverseShareData.currentFileCount >= reverseShareData.maxFiles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleErrorResponse = (responseError: any) => {
|
||||||
|
const status = responseError.response?.status;
|
||||||
|
const errorMessage = responseError.response?.data?.error;
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case HTTP_STATUS.UNAUTHORIZED:
|
||||||
|
if (errorMessage === ERROR_MESSAGES.PASSWORD_REQUIRED) {
|
||||||
|
setIsPasswordModalOpen(true);
|
||||||
|
} else if (errorMessage === ERROR_MESSAGES.INVALID_PASSWORD) {
|
||||||
|
setIsPasswordModalOpen(true);
|
||||||
|
toast.error(t("reverseShares.upload.errors.passwordIncorrect"));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HTTP_STATUS.NOT_FOUND:
|
||||||
|
setError({ type: "notFound" });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HTTP_STATUS.FORBIDDEN:
|
||||||
|
setError({ type: "inactive" });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case HTTP_STATUS.GONE:
|
||||||
|
setError({ type: "expired" });
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
setError({ type: "generic" });
|
||||||
|
toast.error(t("reverseShares.upload.errors.loadFailed"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadReverseShare = async (passwordAttempt?: string) => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError({ type: null });
|
||||||
|
|
||||||
|
const response = await getReverseShareForUploadByAlias(
|
||||||
|
alias,
|
||||||
|
passwordAttempt ? { password: passwordAttempt } : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
setReverseShare(response.data.reverseShare);
|
||||||
|
setIsPasswordModalOpen(false);
|
||||||
|
setCurrentPassword(passwordAttempt || "");
|
||||||
|
} catch (responseError: any) {
|
||||||
|
console.error("Failed to load reverse share:", responseError);
|
||||||
|
handleErrorResponse(responseError);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordSubmit = (passwordValue: string) => {
|
||||||
|
loadReverseShare(passwordValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordModalClose = () => {
|
||||||
|
redirectToHome();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadSuccess = () => {
|
||||||
|
setHasUploadedSuccessfully(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetUploadSuccess = () => {
|
||||||
|
setHasUploadedSuccessfully(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (alias) {
|
||||||
|
loadReverseShare();
|
||||||
|
}
|
||||||
|
}, [alias]);
|
||||||
|
|
||||||
|
// Computed values
|
||||||
|
const isMaxFilesReached = reverseShare ? checkIfMaxFilesReached(reverseShare) : false;
|
||||||
|
const isWeTransferLayout = reverseShare?.pageLayout === "WETRANSFER";
|
||||||
|
const hasError = error.type !== null || (!reverseShare && !isLoading && !isPasswordModalOpen);
|
||||||
|
|
||||||
|
// Error state booleans for backward compatibility
|
||||||
|
const isLinkInactive = error.type === "inactive";
|
||||||
|
const isLinkNotFound = error.type === "notFound" || (!reverseShare && !isLoading && !isPasswordModalOpen);
|
||||||
|
const isLinkExpired = error.type === "expired";
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Data
|
||||||
|
reverseShare,
|
||||||
|
currentPassword,
|
||||||
|
alias,
|
||||||
|
|
||||||
|
// States
|
||||||
|
isLoading,
|
||||||
|
isPasswordModalOpen,
|
||||||
|
hasUploadedSuccessfully,
|
||||||
|
error: error.type,
|
||||||
|
isMaxFilesReached,
|
||||||
|
isWeTransferLayout,
|
||||||
|
hasError,
|
||||||
|
|
||||||
|
// Error states (for backward compatibility)
|
||||||
|
isLinkInactive,
|
||||||
|
isLinkNotFound,
|
||||||
|
isLinkExpired,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
handlePasswordSubmit,
|
||||||
|
handlePasswordModalClose,
|
||||||
|
handleUploadSuccess,
|
||||||
|
resetUploadSuccess,
|
||||||
|
loadReverseShare,
|
||||||
|
redirectToHome,
|
||||||
|
};
|
||||||
|
}
|
@@ -1,9 +1,14 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
title: "Enviar Arquivos - Palmr",
|
const t = await getTranslations();
|
||||||
description: "Envie arquivos através do link compartilhado",
|
|
||||||
};
|
return {
|
||||||
|
title: t("reverseShares.upload.metadata.title"),
|
||||||
|
description: t("reverseShares.upload.metadata.description"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function ReverseShareLayout({ children }: { children: React.ReactNode }) {
|
export default function ReverseShareLayout({ children }: { children: React.ReactNode }) {
|
||||||
return children;
|
return children;
|
||||||
|
@@ -1,93 +1,89 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useParams } from "next/navigation";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
import { LoadingScreen } from "@/components/layout/loading-screen";
|
import { LoadingScreen } from "@/components/layout/loading-screen";
|
||||||
import { getReverseShareForUploadByAlias } from "@/http/endpoints";
|
import { DefaultLayout, PasswordModal, WeTransferLayout } from "./components";
|
||||||
import type { GetReverseShareForUploadResult } from "@/http/endpoints/reverse-shares/types";
|
import { useReverseShareUpload } from "./hooks/use-reverse-share-upload";
|
||||||
import { DefaultLayout } from "./components/default-layout";
|
|
||||||
import { PasswordModal } from "./components/password-modal";
|
|
||||||
import { WeTransferLayout } from "./components/we-transfer-layout";
|
|
||||||
|
|
||||||
type ReverseShareInfo = GetReverseShareForUploadResult["data"]["reverseShare"];
|
|
||||||
|
|
||||||
export default function ReverseShareUploadPage() {
|
export default function ReverseShareUploadPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const shareAlias = params?.alias as string;
|
||||||
const alias = params?.alias as string;
|
|
||||||
|
|
||||||
const [reverseShare, setReverseShare] = useState<ReverseShareInfo | null>(null);
|
const {
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
reverseShare,
|
||||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
currentPassword,
|
||||||
const [password, setPassword] = useState("");
|
isLoading,
|
||||||
|
isPasswordModalOpen,
|
||||||
const loadReverseShare = async (passwordAttempt?: string) => {
|
hasUploadedSuccessfully,
|
||||||
try {
|
isMaxFilesReached,
|
||||||
setIsLoading(true);
|
isWeTransferLayout,
|
||||||
const response = await getReverseShareForUploadByAlias(
|
hasError,
|
||||||
alias,
|
isLinkInactive,
|
||||||
passwordAttempt ? { password: passwordAttempt } : undefined
|
isLinkNotFound,
|
||||||
);
|
isLinkExpired,
|
||||||
setReverseShare(response.data.reverseShare);
|
handlePasswordSubmit,
|
||||||
setShowPasswordModal(false);
|
handlePasswordModalClose,
|
||||||
setPassword(passwordAttempt || "");
|
handleUploadSuccess,
|
||||||
} catch (error: any) {
|
} = useReverseShareUpload({ alias: shareAlias });
|
||||||
console.error("Failed to load reverse share:", error);
|
|
||||||
|
|
||||||
if (error.response?.status === 401 && error.response?.data?.error === "Password required") {
|
|
||||||
setShowPasswordModal(true);
|
|
||||||
} else if (error.response?.status === 401 && error.response?.data?.error === "Invalid password") {
|
|
||||||
setShowPasswordModal(true);
|
|
||||||
toast.error("Senha incorreta. Tente novamente.");
|
|
||||||
} else if (error.response?.status === 404) {
|
|
||||||
toast.error("Link não encontrado ou expirado.");
|
|
||||||
router.push("/");
|
|
||||||
} else if (error.response?.status === 403) {
|
|
||||||
toast.error("Este link está inativo.");
|
|
||||||
router.push("/");
|
|
||||||
} else if (error.response?.status === 410) {
|
|
||||||
toast.error("Este link expirou.");
|
|
||||||
router.push("/");
|
|
||||||
} else {
|
|
||||||
toast.error("Erro ao carregar informações. Tente novamente.");
|
|
||||||
router.push("/");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (alias) {
|
|
||||||
loadReverseShare();
|
|
||||||
}
|
|
||||||
}, [alias]);
|
|
||||||
|
|
||||||
const handlePasswordSubmit = (passwordValue: string) => {
|
|
||||||
loadReverseShare(passwordValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// Loading state
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingScreen />;
|
return <LoadingScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showPasswordModal) {
|
// Password required state
|
||||||
|
if (isPasswordModalOpen) {
|
||||||
return (
|
return (
|
||||||
<PasswordModal isOpen={showPasswordModal} onSubmit={handlePasswordSubmit} onClose={() => router.push("/")} />
|
<PasswordModal isOpen={isPasswordModalOpen} onSubmit={handlePasswordSubmit} onClose={handlePasswordModalClose} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!reverseShare) {
|
// Error states or missing data - always use DefaultLayout for simplicity
|
||||||
return <LoadingScreen />;
|
if (hasError) {
|
||||||
|
return (
|
||||||
|
<DefaultLayout
|
||||||
|
reverseShare={reverseShare}
|
||||||
|
password={currentPassword}
|
||||||
|
alias={shareAlias}
|
||||||
|
isMaxFilesReached={false}
|
||||||
|
hasUploadedSuccessfully={false}
|
||||||
|
onUploadSuccess={handleUploadSuccess}
|
||||||
|
isLinkInactive={isLinkInactive}
|
||||||
|
isLinkNotFound={isLinkNotFound}
|
||||||
|
isLinkExpired={isLinkExpired}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Escolher o layout baseado no pageLayout
|
// Render appropriate layout for normal states
|
||||||
if (reverseShare.pageLayout === "WETRANSFER") {
|
if (isWeTransferLayout) {
|
||||||
return <WeTransferLayout reverseShare={reverseShare} password={password} alias={alias} />;
|
return (
|
||||||
|
<WeTransferLayout
|
||||||
|
reverseShare={reverseShare}
|
||||||
|
password={currentPassword}
|
||||||
|
alias={shareAlias}
|
||||||
|
isMaxFilesReached={isMaxFilesReached}
|
||||||
|
hasUploadedSuccessfully={hasUploadedSuccessfully}
|
||||||
|
onUploadSuccess={handleUploadSuccess}
|
||||||
|
isLinkInactive={false}
|
||||||
|
isLinkNotFound={false}
|
||||||
|
isLinkExpired={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layout padrão
|
return (
|
||||||
return <DefaultLayout reverseShare={reverseShare} password={password} alias={alias} />;
|
<DefaultLayout
|
||||||
|
reverseShare={reverseShare}
|
||||||
|
password={currentPassword}
|
||||||
|
alias={shareAlias}
|
||||||
|
isMaxFilesReached={isMaxFilesReached}
|
||||||
|
hasUploadedSuccessfully={hasUploadedSuccessfully}
|
||||||
|
onUploadSuccess={handleUploadSuccess}
|
||||||
|
isLinkInactive={false}
|
||||||
|
isLinkNotFound={false}
|
||||||
|
isLinkExpired={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
49
apps/web/src/app/(shares)/r/[alias]/types/index.tsx
Normal file
49
apps/web/src/app/(shares)/r/[alias]/types/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { GetReverseShareForUploadResult } from "@/http/endpoints/reverse-shares/types";
|
||||||
|
import { FILE_STATUS } from "../constants";
|
||||||
|
|
||||||
|
export type ReverseShareInfo = GetReverseShareForUploadResult["data"]["reverseShare"];
|
||||||
|
export type FileStatus = (typeof FILE_STATUS)[keyof typeof FILE_STATUS];
|
||||||
|
|
||||||
|
export interface DefaultLayoutProps {
|
||||||
|
reverseShare: ReverseShareInfo | null;
|
||||||
|
password: string;
|
||||||
|
alias: string;
|
||||||
|
isMaxFilesReached: boolean;
|
||||||
|
hasUploadedSuccessfully: boolean;
|
||||||
|
onUploadSuccess: () => void;
|
||||||
|
isLinkInactive: boolean;
|
||||||
|
isLinkNotFound: boolean;
|
||||||
|
isLinkExpired: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileUploadSectionProps {
|
||||||
|
reverseShare: ReverseShareInfo;
|
||||||
|
password: string;
|
||||||
|
alias: string;
|
||||||
|
onUploadSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileWithProgress {
|
||||||
|
file: File;
|
||||||
|
progress: number;
|
||||||
|
status: FileStatus;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PasswordModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onSubmit: (password: string) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeTransferLayoutProps {
|
||||||
|
reverseShare: ReverseShareInfo | null;
|
||||||
|
password: string;
|
||||||
|
alias: string;
|
||||||
|
isMaxFilesReached: boolean;
|
||||||
|
hasUploadedSuccessfully: boolean;
|
||||||
|
onUploadSuccess: () => void;
|
||||||
|
isLinkInactive: boolean;
|
||||||
|
isLinkNotFound: boolean;
|
||||||
|
isLinkExpired: boolean;
|
||||||
|
}
|
Reference in New Issue
Block a user