feat: enhance reverse share functionality with QR code support

- Added QR code viewing and downloading capabilities in the reverse shares section.
- Updated UI components to include QR code options in share details and cards.
- Introduced new state management for handling QR code visibility.
- Enhanced translations for QR code interactions across multiple languages.
This commit is contained in:
Daniel Luiz Alves
2025-07-18 01:50:33 -03:00
parent 4779671323
commit 6fb55005d4
25 changed files with 295 additions and 177 deletions

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} أيقونة من {libraryCount} مكتبة",
"categoryBadge": "{category} ({count} أيقونات)"
},
"imageEdit": {
"title": "تعديل الصورة",
"rotate": "تدوير",
"zoom": "تكبير/تصغير",
"cropInstructions": "اسحب لإعادة تحديد الموضع، غير حجم الزوايا لضبط منطقة القص"
},
"login": {
"welcome": "مرحبا بك",
"signInToContinue": "قم بتسجيل الدخول للمتابعة",
@@ -1721,11 +1727,5 @@
"passwordRequired": "كلمة المرور مطلوبة",
"nameRequired": "الاسم مطلوب",
"required": "هذا الحقل مطلوب"
},
"imageEdit": {
"title": "تعديل الصورة",
"rotate": "تدوير",
"zoom": "تكبير/تصغير",
"cropInstructions": "اسحب لإعادة تحديد الموضع، غير حجم الزوايا لضبط منطقة القص"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} Symbole aus {libraryCount} Bibliotheken",
"categoryBadge": "{category} ({count} Symbole)"
},
"imageEdit": {
"title": "Bild bearbeiten",
"rotate": "Drehen",
"zoom": "Zoom",
"cropInstructions": "Ziehen Sie, um die Position zu ändern, ändern Sie die Größe der Ecken, um die Zuschneidefläche anzupassen"
},
"login": {
"welcome": "Willkommen zu",
"signInToContinue": "Melden Sie sich an, um fortzufahren",
@@ -1719,11 +1725,5 @@
"passwordRequired": "Passwort ist erforderlich",
"nameRequired": "Name ist erforderlich",
"required": "Dieses Feld ist erforderlich"
},
"imageEdit": {
"title": "Bild bearbeiten",
"rotate": "Drehen",
"zoom": "Zoom",
"cropInstructions": "Ziehen Sie, um die Position zu ändern, ändern Sie die Größe der Ecken, um die Zuschneidefläche anzupassen"
}
}
}

View File

@@ -360,6 +360,12 @@
"stats": "{iconCount} icons from {libraryCount} libraries",
"categoryBadge": "{category} ({count} icons)"
},
"imageEdit": {
"title": "Edit Image",
"rotate": "Rotate",
"zoom": "Zoom",
"cropInstructions": "Drag to reposition, resize corners to adjust crop area"
},
"login": {
"welcome": "Welcome to",
"signInToContinue": "Sign in to continue",
@@ -405,12 +411,6 @@
"navigation": {
"dashboard": "Dashboard"
},
"imageEdit": {
"title": "Edit Image",
"rotate": "Rotate",
"zoom": "Zoom",
"cropInstructions": "Drag to reposition, resize corners to adjust crop area"
},
"profile": {
"password": {
"title": "Change Password",
@@ -453,6 +453,11 @@
},
"pageTitle": "Profile"
},
"qrCodeModal": {
"title": "Share QR Code",
"description": "Scan this QR code to access the link.",
"download": "Download QR Code"
},
"quickAccess": {
"files": {
"title": "My Files",
@@ -618,6 +623,7 @@
"expired": "Expired",
"expires": "Expires",
"viewDetails": "View details",
"viewQrCode": "View QR Code",
"copyLink": "Copy Link",
"openInNewTab": "Open in New Tab",
"editLink": "Edit Link",
@@ -640,7 +646,8 @@
"viewDetails": "View Details",
"edit": "Edit",
"delete": "Delete",
"viewFiles": "Received Files"
"viewFiles": "Received Files",
"viewQrCode": "View QR Code"
},
"empty": {
"title": "No receive links created",
@@ -1461,11 +1468,6 @@
"dark": "Dark",
"system": "System"
},
"qrCodeModal": {
"title": "Share QR Code",
"description": "Scan this QR code to access the shared files.",
"download": "Download QR Code"
},
"twoFactor": {
"title": "Two-Factor Authentication",
"description": "Add an extra layer of security to your account",

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} iconos de {libraryCount} bibliotecas",
"categoryBadge": "{category} ({count} iconos)"
},
"imageEdit": {
"title": "Editar Imagen",
"rotate": "Rotar",
"zoom": "Zoom",
"cropInstructions": "Arrastra para reubicar, ajusta las esquinas para ajustar el área de recorte"
},
"login": {
"welcome": "Bienvenido a",
"signInToContinue": "Inicia sesión para continuar",
@@ -1719,11 +1725,5 @@
"passwordRequired": "Se requiere la contraseña",
"nameRequired": "El nombre es obligatorio",
"required": "Este campo es obligatorio"
},
"imageEdit": {
"title": "Editar Imagen",
"rotate": "Rotar",
"zoom": "Zoom",
"cropInstructions": "Arrastra para reubicar, ajusta las esquinas para ajustar el área de recorte"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} icônes de {libraryCount} bibliothèques",
"categoryBadge": "{category} ({count} icônes)"
},
"imageEdit": {
"title": "Modifier l'Image",
"rotate": "Tourner",
"zoom": "Zoom",
"cropInstructions": "Glisser pour répositionner, redimensionner les coins pour ajuster la zone de découpe"
},
"login": {
"welcome": "Bienvenue à",
"signInToContinue": "Connectez-vous pour continuer",
@@ -1719,11 +1725,5 @@
"passwordRequired": "Le mot de passe est requis",
"nameRequired": "Nome é obrigatório",
"required": "Este campo é obrigatório"
},
"imageEdit": {
"title": "Modifier l'Image",
"rotate": "Tourner",
"zoom": "Zoom",
"cropInstructions": "Glisser pour répositionner, redimensionner les coins pour ajuster la zone de découpe"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "{libraryCount} लाइब्रेरी से {iconCount} आइकन",
"categoryBadge": "{category} ({count} आइकन)"
},
"imageEdit": {
"title": "छवि संपादित करें",
"rotate": "घुमाएं",
"zoom": "ज़ूम",
"cropInstructions": "छवि को पुनः स्थानांतरित करने के लिए खींचें, कोणों को समायोजित करने के लिए आकार बदलें"
},
"login": {
"welcome": "स्वागत है में",
"signInToContinue": "जारी रखने के लिए साइन इन करें",
@@ -1719,11 +1725,5 @@
"passwordRequired": "पासवर्ड आवश्यक है",
"nameRequired": "नाम आवश्यक है",
"required": "यह फ़ील्ड आवश्यक है"
},
"imageEdit": {
"title": "छवि संपादित करें",
"rotate": "घुमाएं",
"zoom": "ज़ूम",
"cropInstructions": "छवि को पुनः स्थानांतरित करने के लिए खींचें, कोणों को समायोजित करने के लिए आकार बदलें"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} icone da {libraryCount} librerie",
"categoryBadge": "{category} ({count} icone)"
},
"imageEdit": {
"title": "Modifica Immagine",
"rotate": "Ruota",
"zoom": "Zoom",
"cropInstructions": "Trascina per riposizionare, ridimensiona gli angoli per adattare l'area di ritaglio"
},
"login": {
"welcome": "Benvenuto in",
"signInToContinue": "Accedi per continuare",
@@ -1719,11 +1725,5 @@
"passwordMinLength": "La password deve contenere almeno 6 caratteri",
"nameRequired": "Il nome è obbligatorio",
"required": "Questo campo è obbligatorio"
},
"imageEdit": {
"title": "Modifica Immagine",
"rotate": "Ruota",
"zoom": "Zoom",
"cropInstructions": "Trascina per riposizionare, ridimensiona gli angoli per adattare l'area di ritaglio"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "{libraryCount}ライブラリから{iconCount}個のアイコン",
"categoryBadge": "{category}{count}個のアイコン)"
},
"imageEdit": {
"title": "画像を編集",
"rotate": "回転",
"zoom": "ズーム",
"cropInstructions": "位置を変更するにはドラッグし、カット領域を調整するには角をリサイズしてください"
},
"login": {
"welcome": "ようこそへ",
"signInToContinue": "続行するにはサインインしてください",
@@ -1719,11 +1725,5 @@
"passwordRequired": "パスワードは必須です",
"nameRequired": "名前は必須です",
"required": "このフィールドは必須です"
},
"imageEdit": {
"title": "画像を編集",
"rotate": "回転",
"zoom": "ズーム",
"cropInstructions": "位置を変更するにはドラッグし、カット領域を調整するには角をリサイズしてください"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "{libraryCount}개의 라이브러리에서 {iconCount}개의 아이콘",
"categoryBadge": "{category} ({count}개의 아이콘)"
},
"imageEdit": {
"title": "이미지 편집",
"rotate": "회전",
"zoom": "확대/축소",
"cropInstructions": "위치를 변경하려면 드래그하고, 자르기 영역을 조정하려면 모서리를 확대/축소하세요"
},
"login": {
"welcome": "에 오신 것을 환영합니다",
"signInToContinue": "계속하려면 로그인하세요",
@@ -1719,11 +1725,5 @@
"passwordRequired": "비밀번호는 필수입니다",
"nameRequired": "이름은 필수입니다",
"required": "이 필드는 필수입니다"
},
"imageEdit": {
"title": "이미지 편집",
"rotate": "회전",
"zoom": "확대/축소",
"cropInstructions": "위치를 변경하려면 드래그하고, 자르기 영역을 조정하려면 모서리를 확대/축소하세요"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} pictogrammen van {libraryCount} bibliotheken",
"categoryBadge": "{category} ({count} pictogrammen)"
},
"imageEdit": {
"title": "Afbeelding bewerken",
"rotate": "Draai",
"zoom": "Vergroot",
"cropInstructions": "Sleep om te herpositioneren, verander de grootte van de hoeken om de uitsnijdgebied aan te passen"
},
"login": {
"welcome": "Welkom bij",
"signInToContinue": "Log in om door te gaan",
@@ -1719,11 +1725,5 @@
"passwordMinLength": "Wachtwoord moet minimaal 6 tekens bevatten",
"nameRequired": "Naam is verplicht",
"required": "Dit veld is verplicht"
},
"imageEdit": {
"title": "Afbeelding bewerken",
"rotate": "Draai",
"zoom": "Vergroot",
"cropInstructions": "Sleep om te herpositioneren, verander de grootte van de hoeken om de uitsnijdgebied aan te passen"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} ikon z {libraryCount} bibliotek",
"categoryBadge": "{category} ({count} ikon)"
},
"imageEdit": {
"title": "Edytuj obraz",
"rotate": "Obróć",
"zoom": "Powiększ",
"cropInstructions": "Przeciągnij, aby przesunąć, zmień rozmiar rogów, aby dostosować obszar przycięcia"
},
"login": {
"welcome": "Witaj w",
"signInToContinue": "Zaloguj się, aby kontynuować",
@@ -1719,11 +1725,5 @@
"passwordMinLength": "Hasło musi mieć co najmniej 6 znaków",
"nameRequired": "Nazwa jest wymagana",
"required": "To pole jest wymagane"
},
"imageEdit": {
"title": "Edytuj obraz",
"rotate": "Obróć",
"zoom": "Powiększ",
"cropInstructions": "Przeciągnij, aby przesunąć, zmień rozmiar rogów, aby dostosować obszar przycięcia"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} ícones de {libraryCount} bibliotecas",
"categoryBadge": "{category} ({count} ícones)"
},
"imageEdit": {
"title": "Editar imagem",
"rotate": "Girar",
"zoom": "Ampliar",
"cropInstructions": "Arraste para reposicionar, redimensione os cantos para ajustar a área de recorte"
},
"login": {
"welcome": "Bem-vindo ao",
"signInToContinue": "Faça login para continuar",
@@ -1719,11 +1725,5 @@
"lastNameRequired": "O sobrenome é necessário",
"usernameLength": "O nome de usuário deve ter pelo menos 3 caracteres",
"usernameSpaces": "O nome de usuário não pode conter espaços"
},
"imageEdit": {
"title": "Editar imagem",
"rotate": "Girar",
"zoom": "Ampliar",
"cropInstructions": "Arraste para reposicionar, redimensione os cantos para ajustar a área de recorte"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} иконок из {libraryCount} библиотек",
"categoryBadge": "{category} ({count} иконок)"
},
"imageEdit": {
"title": "Редактировать изображение",
"rotate": "Повернуть",
"zoom": "Увеличить",
"cropInstructions": "Перетащите, чтобы переместить, измените размер углов, чтобы отрегулировать область обрезки"
},
"login": {
"welcome": "Добро пожаловать в",
"signInToContinue": "Войдите, чтобы продолжить",
@@ -1719,11 +1725,5 @@
"passwordRequired": "Требуется пароль",
"nameRequired": "Требуется имя",
"required": "Это поле обязательно"
},
"imageEdit": {
"title": "Редактировать изображение",
"rotate": "Повернуть",
"zoom": "Увеличить",
"cropInstructions": "Перетащите, чтобы переместить, измените размер углов, чтобы отрегулировать область обрезки"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "{libraryCount} kütüphaneden {iconCount} simge",
"categoryBadge": "{category} ({count} simge)"
},
"imageEdit": {
"title": "Resmi Düzenle",
"rotate": "Döndür",
"zoom": "Yakınlaştır",
"cropInstructions": "Yerleştirmek için sürükleyin, kırpma alanını ayarlamak için köşeleri yeniden boyutlandırın"
},
"login": {
"welcome": "Hoş geldiniz'e",
"signInToContinue": "Devam etmek için oturum açın",
@@ -1719,11 +1725,5 @@
"passwordRequired": "Şifre gerekli",
"nameRequired": "İsim gereklidir",
"required": "Bu alan zorunludur"
},
"imageEdit": {
"title": "Resmi Düzenle",
"rotate": "Döndür",
"zoom": "Yakınlaştır",
"cropInstructions": "Yerleştirmek için sürükleyin, kırpma alanını ayarlamak için köşeleri yeniden boyutlandırın"
}
}
}

View File

@@ -355,6 +355,12 @@
"stats": "来自 {libraryCount} 个库的 {iconCount} 个图标",
"categoryBadge": "{category}{count} 个图标)"
},
"imageEdit": {
"title": "编辑图片",
"rotate": "旋转",
"zoom": "缩放",
"cropInstructions": "拖动以重新定位,调整角落大小以调整裁剪区域"
},
"login": {
"welcome": "欢迎您",
"signInToContinue": "请登录以继续",
@@ -1483,11 +1489,7 @@
"copyToClipboard": "复制到剪贴板",
"savedMessage": "我已保存备用码",
"available": "可用备用码:{count}个",
"instructions": [
"• 将这些代码保存在安全的位置",
"• 每个备用码只能使用一次",
"• 您可以随时生成新的备用码"
]
"instructions": ["• 将这些代码保存在安全的位置", "• 每个备用码只能使用一次", "• 您可以随时生成新的备用码"]
},
"verification": {
"title": "双重认证",
@@ -1719,11 +1721,5 @@
"passwordRequired": "密码为必填项",
"nameRequired": "名称为必填项",
"required": "此字段为必填项"
},
"imageEdit": {
"title": "编辑图片",
"rotate": "旋转",
"zoom": "缩放",
"cropInstructions": "拖动以重新定位,调整角落大小以调整裁剪区域"
}
}
}

View File

@@ -99,4 +99,4 @@
"tailwindcss": "4.1.11",
"typescript": "5.8.3"
}
}
}

View File

@@ -11,6 +11,7 @@ import {
IconLink,
IconLock,
IconLockOpen,
IconQrcode,
IconToggleLeft,
IconToggleRight,
IconTrash,
@@ -38,6 +39,7 @@ interface ReverseShareCardProps {
onGenerateLink: (reverseShare: ReverseShare) => void;
onViewDetails: (reverseShare: ReverseShare) => void;
onViewFiles: (reverseShare: ReverseShare) => void;
onViewQrCode?: (reverseShare: ReverseShare) => void;
onUpdateReverseShare?: (id: string, data: any) => Promise<any>;
onToggleActive?: (id: string, isActive: boolean) => Promise<any>;
onUpdatePassword?: (id: string, data: { hasPassword: boolean; password?: string }) => Promise<any>;
@@ -51,6 +53,7 @@ export function ReverseShareCard({
onGenerateLink,
onViewDetails,
onViewFiles,
onViewQrCode,
onUpdateReverseShare,
onToggleActive,
onUpdatePassword,
@@ -230,6 +233,18 @@ export function ReverseShareCard({
</div>
<div className="flex items-center gap-1">
{hasAlias && onViewQrCode && (
<Button
variant="outline"
size="sm"
className="h-6 w-6 p-0 hover:bg-background/80 rounded-sm"
onClick={() => onViewQrCode(reverseShare)}
title={t("reverseShares.card.viewQrCode")}
>
<IconQrcode className="h-3 w-3" />
</Button>
)}
<Button
variant="outline"
size="sm"
@@ -239,7 +254,6 @@ export function ReverseShareCard({
>
<IconEye className="h-3 w-3" />
</Button>
<Button
variant="outline"
size="sm"
@@ -257,6 +271,11 @@ export function ReverseShareCard({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onViewDetails(reverseShare)}>
<IconEye className="h-4 w-4" />
{t("reverseShares.card.viewDetails")}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => onCopyLink(reverseShare)}>
<IconCopy className="h-4 w-4" />
{t("reverseShares.card.copyLink")}
@@ -286,6 +305,13 @@ export function ReverseShareCard({
{t("reverseShares.actions.viewFiles")}
</DropdownMenuItem>
{hasAlias && onViewQrCode && (
<DropdownMenuItem onClick={() => onViewQrCode(reverseShare)}>
<IconQrcode className="h-4 w-4" />
{t("reverseShares.actions.viewQrCode")}
</DropdownMenuItem>
)}
<DropdownMenuItem className="text-destructive" onClick={() => onDelete(reverseShare)}>
<IconTrash className="h-4 w-4" />
{t("reverseShares.card.delete")}

View File

@@ -3,6 +3,7 @@
import { useEffect, useState } from "react";
import {
IconCopy,
IconDownload,
IconEdit,
IconLink,
IconLock,
@@ -11,6 +12,7 @@ import {
IconToggleRight,
} from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import QRCode from "react-qr-code";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -42,6 +44,7 @@ interface ReverseShareDetailsModalProps {
onCopyLink?: (reverseShare: ReverseShare) => void;
onToggleActive?: (id: string, isActive: boolean) => Promise<void>;
onUpdatePassword?: (id: string, data: { hasPassword: boolean; password?: string }) => Promise<void>;
onViewQrCode?: (reverseShare: ReverseShare) => void;
refreshTrigger?: number;
onSuccess?: () => void;
}
@@ -55,10 +58,12 @@ export function ReverseShareDetailsModal({
onCopyLink,
onToggleActive,
onUpdatePassword,
onViewQrCode,
onSuccess,
}: ReverseShareDetailsModalProps) {
const t = useTranslations();
const [pendingChanges, setPendingChanges] = useState<Record<string, any>>({});
const [isDownloading, setIsDownloading] = useState(false);
const {
showAliasModal,
@@ -140,46 +145,119 @@ export function ReverseShareDetailsModal({
isActive={reverseShare.isActive}
/>
{/* Informações Básicas */}
<div className="space-y-3">
<h3 className="text-base font-medium text-foreground border-b pb-2">
{t("reverseShares.modals.details.basicInfo")}
</h3>
<div className="grid grid-cols-2 gap-4">
{/* Informações Básicas */}
<div className="space-y-3">
<h3 className="text-base font-medium text-foreground border-b pb-2">
{t("reverseShares.modals.details.basicInfo")}
</h3>
<EditableField
label={t("reverseShares.form.name.label")}
value={getDisplayValue(reverseShare, "name", pendingChanges)}
onSave={(value) => handleUpdateField("name", value)}
placeholder={t("reverseShares.card.untitled")}
disabled={!onUpdateReverseShare}
/>
<EditableField
label={t("reverseShares.form.name.label")}
value={getDisplayValue(reverseShare, "name", pendingChanges)}
onSave={(value) => handleUpdateField("name", value)}
placeholder={t("reverseShares.card.untitled")}
disabled={!onUpdateReverseShare}
/>
<EditableField
label={t("reverseShares.labels.description")}
value={getDisplayValue(reverseShare, "description", pendingChanges)}
onSave={(value) => handleUpdateField("description", value)}
placeholder={t("reverseShares.card.noDescription")}
disabled={!onUpdateReverseShare}
/>
<EditableField
label={t("reverseShares.labels.description")}
value={getDisplayValue(reverseShare, "description", pendingChanges)}
onSave={(value) => handleUpdateField("description", value)}
placeholder={t("reverseShares.card.noDescription")}
disabled={!onUpdateReverseShare}
/>
<EditableField
label={t("reverseShares.labels.pageLayout")}
value={getDisplayValue(reverseShare, "pageLayout", pendingChanges)}
onSave={(value) => handleUpdateField("pageLayout", value)}
type="select"
options={[
{ value: "DEFAULT", label: t("reverseShares.labels.layoutOptions.default") },
{ value: "WETRANSFER", label: t("reverseShares.labels.layoutOptions.wetransfer") },
]}
disabled={!onUpdateReverseShare}
renderValue={(value) => (
<Badge variant="secondary" className="bg-purple-500/20 text-purple-700 border-purple-200">
{value === "WETRANSFER"
? t("reverseShares.labels.layoutOptions.wetransfer")
: t("reverseShares.labels.layoutOptions.default")}
</Badge>
)}
/>
<EditableField
label={t("reverseShares.labels.pageLayout")}
value={getDisplayValue(reverseShare, "pageLayout", pendingChanges)}
onSave={(value) => handleUpdateField("pageLayout", value)}
type="select"
options={[
{ value: "DEFAULT", label: t("reverseShares.labels.layoutOptions.default") },
{ value: "WETRANSFER", label: t("reverseShares.labels.layoutOptions.wetransfer") },
]}
disabled={!onUpdateReverseShare}
renderValue={(value) => (
<Badge variant="secondary" className="bg-purple-500/20 text-purple-700 border-purple-200">
{value === "WETRANSFER"
? t("reverseShares.labels.layoutOptions.wetransfer")
: t("reverseShares.labels.layoutOptions.default")}
</Badge>
)}
/>
</div>
{/* QR Code */}
{reverseShareLink && (
<div className="space-y-3">
<div className="flex items-center gap-2 border-b pb-2">
<h3
className="text-base font-medium text-foreground cursor-pointer"
onClick={() => onViewQrCode && onViewQrCode(reverseShare)}
>
{t("qrCodeModal.title")}
</h3>
<Button
size="icon"
variant="ghost"
className="h-5 w-5 text-muted-foreground hover:text-foreground"
onClick={() => {
const svg = document.getElementById("reverse-share-details-qr-code");
if (!svg) return;
setIsDownloading(true);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const padding = 20;
canvas.width = 200 + padding * 2;
canvas.height = 200 + padding * 2;
if (ctx) {
ctx.fillStyle = "#FFFFFF";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const svgData = new XMLSerializer().serializeToString(svg);
const img = new Image();
img.onload = () => {
ctx.drawImage(img, padding, padding, 200, 200);
const link = document.createElement("a");
link.download = `${reverseShare?.name?.replace(/[^a-z0-9]/gi, "-").toLowerCase() || "reverse-share"}-qr-code.png`;
link.href = canvas.toDataURL("image/png");
link.click();
setIsDownloading(false);
};
img.src = `data:image/svg+xml;base64,${btoa(svgData)}`;
} else {
setIsDownloading(false);
}
}}
disabled={isDownloading}
title={t("qrCodeModal.download")}
>
<IconDownload className="h-3 w-3" />
</Button>
</div>
<div className="flex flex-col items-start justify-start">
<div
className="p-2 bg-white rounded-lg cursor-pointer hover:opacity-80 transition-opacity duration-300"
onClick={() => onViewQrCode && onViewQrCode(reverseShare)}
title={t("reverseShares.actions.viewQrCode")}
>
<QRCode
id="reverse-share-details-qr-code"
value={reverseShareLink}
size={100}
level="H"
fgColor="#000000"
bgColor="#FFFFFF"
/>
</div>
</div>
</div>
)}
</div>
{/* Link de Compartilhamento */}

View File

@@ -10,6 +10,7 @@ interface ReverseSharesCardsContainerProps {
onGenerateLink: (reverseShare: ReverseShare) => void;
onViewDetails: (reverseShare: ReverseShare) => void;
onViewFiles: (reverseShare: ReverseShare) => void;
onViewQrCode?: (reverseShare: ReverseShare) => void;
onCreateReverseShare: () => void;
onUpdateReverseShare?: (id: string, data: any) => Promise<any>;
onToggleActive?: (id: string, isActive: boolean) => Promise<any>;
@@ -24,6 +25,7 @@ export function ReverseSharesCardsContainer({
onGenerateLink,
onViewDetails,
onViewFiles,
onViewQrCode,
onCreateReverseShare,
onUpdateReverseShare,
onToggleActive,
@@ -45,6 +47,7 @@ export function ReverseSharesCardsContainer({
onGenerateLink={onGenerateLink}
onViewDetails={onViewDetails}
onViewFiles={onViewFiles}
onViewQrCode={onViewQrCode}
onUpdateReverseShare={onUpdateReverseShare}
onToggleActive={onToggleActive}
onUpdatePassword={onUpdatePassword}

View File

@@ -1,3 +1,4 @@
import { QrCodeModal } from "@/components/modals/qr-code-modal";
import type { CreateReverseShareBody, UpdateReverseShareBody } from "@/http/endpoints/reverse-shares/types";
import { ReverseShare } from "../hooks/use-reverse-shares";
import { CreateReverseShareModal } from "./create-reverse-share-modal";
@@ -20,14 +21,17 @@ interface ReverseSharesModalsProps {
reverseShareToGenerateLink: ReverseShare | null;
reverseShareToDelete: ReverseShare | null;
reverseShareToViewFiles: ReverseShare | null;
reverseShareToViewQrCode: ReverseShare | null;
isDeleting: boolean;
onCloseViewDetails: () => void;
onCloseGenerateLink: () => void;
onCloseDeleteModal: () => void;
onCloseViewFiles: () => void;
onCloseViewQrCode: () => void;
onConfirmDelete: (reverseShare: ReverseShare) => Promise<void>;
onCreateAlias: (reverseShareId: string, alias: string) => Promise<void>;
onCopyLink: (reverseShare: ReverseShare) => void;
onViewQrCode: (reverseShare: ReverseShare) => void;
onUpdateReverseShareData?: (id: string, data: any) => Promise<any>;
onUpdatePassword?: (id: string, data: { hasPassword: boolean; password?: string }) => Promise<any>;
onToggleActive?: (id: string, isActive: boolean) => Promise<any>;
@@ -48,14 +52,17 @@ export function ReverseSharesModals({
reverseShareToGenerateLink,
reverseShareToDelete,
reverseShareToViewFiles,
reverseShareToViewQrCode,
isDeleting,
onCloseViewDetails,
onCloseGenerateLink,
onCloseDeleteModal,
onCloseViewFiles,
onCloseViewQrCode,
onConfirmDelete,
onCreateAlias,
onCopyLink,
onViewQrCode,
onUpdateReverseShareData,
onUpdatePassword,
onToggleActive,
@@ -103,6 +110,7 @@ export function ReverseSharesModals({
onCopyLink={onCopyLink}
onUpdatePassword={onUpdatePassword}
onToggleActive={onToggleActive}
onViewQrCode={onViewQrCode}
/>
<ReceivedFilesModal
@@ -112,6 +120,17 @@ export function ReverseSharesModals({
onRefresh={onRefreshData}
refreshReverseShare={refreshReverseShare}
/>
<QrCodeModal
isOpen={!!reverseShareToViewQrCode}
onClose={onCloseViewQrCode}
shareLink={
reverseShareToViewQrCode?.alias?.alias
? `${typeof window !== "undefined" ? window.location.origin : ""}/r/${reverseShareToViewQrCode.alias.alias}`
: ""
}
shareName={reverseShareToViewQrCode?.name || "Reverse Share"}
/>
</>
);
}

View File

@@ -30,6 +30,7 @@ export function useReverseShares() {
const [reverseShareToDelete, setReverseShareToDelete] = useState<ReverseShare | null>(null);
const [reverseShareToEdit, setReverseShareToEdit] = useState<ReverseShare | null>(null);
const [reverseShareToViewFiles, setReverseShareToViewFiles] = useState<ReverseShare | null>(null);
const [reverseShareToViewQrCode, setReverseShareToViewQrCode] = useState<ReverseShare | null>(null);
const [isDeleting, setIsDeleting] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isCreating, setIsCreating] = useState(false);
@@ -277,6 +278,7 @@ export function useReverseShares() {
reverseShareToDelete,
reverseShareToEdit,
reverseShareToViewFiles,
reverseShareToViewQrCode,
isDeleting,
isCreateModalOpen,
isCreating,
@@ -288,6 +290,7 @@ export function useReverseShares() {
setReverseShareToDelete,
setReverseShareToEdit,
setReverseShareToViewFiles,
setReverseShareToViewQrCode,
setIsCreateModalOpen,
handleCopyLink,
handleDeleteReverseShare,

View File

@@ -23,6 +23,7 @@ export default function ReverseSharesPage() {
reverseShareToDelete,
reverseShareToEdit,
reverseShareToViewFiles,
reverseShareToViewQrCode,
isDeleting,
isCreateModalOpen,
isCreating,
@@ -37,6 +38,7 @@ export default function ReverseSharesPage() {
setReverseShareToDelete,
setReverseShareToEdit,
setReverseShareToViewFiles,
setReverseShareToViewQrCode,
handleCreateAlias,
handleUpdatePassword,
handleUpdateReverseShareData,
@@ -77,6 +79,7 @@ export default function ReverseSharesPage() {
onGenerateLink={setReverseShareToGenerateLink}
onViewDetails={setReverseShareToViewDetails}
onViewFiles={setReverseShareToViewFiles}
onViewQrCode={setReverseShareToViewQrCode}
onCreateReverseShare={() => setIsCreateModalOpen(true)}
onUpdateReverseShare={handleUpdateReverseShareData}
onToggleActive={handleToggleActive}
@@ -99,14 +102,17 @@ export default function ReverseSharesPage() {
reverseShareToViewDetails={reverseShareToViewDetails}
reverseShareToDelete={reverseShareToDelete}
reverseShareToViewFiles={reverseShareToViewFiles}
reverseShareToViewQrCode={reverseShareToViewQrCode}
isDeleting={isDeleting}
onCloseGenerateLink={() => setReverseShareToGenerateLink(null)}
onCloseViewDetails={() => setReverseShareToViewDetails(null)}
onCloseDeleteModal={() => setReverseShareToDelete(null)}
onCloseViewFiles={() => setReverseShareToViewFiles(null)}
onCloseViewQrCode={() => setReverseShareToViewQrCode(null)}
onConfirmDelete={handleDeleteReverseShare}
onCreateAlias={handleCreateAlias}
onCopyLink={handleCopyLink}
onViewQrCode={setReverseShareToViewQrCode}
onUpdateReverseShareData={handleUpdateReverseShareData}
onUpdatePassword={handleUpdatePassword}
onToggleActive={handleToggleActive}

View File

@@ -173,8 +173,8 @@ export function GenerateShareLinkModal({
<DialogFooter>
<Button onClick={downloadQRCode} disabled={isDownloading}>
<IconDownload className="h-4 w-4 mr-2" />
{t("qrCodeModal.download", { defaultValue: "Download QR Code" })}
<IconDownload className="h-4 w-4" />
{t("qrCodeModal.download")}
</Button>
</DialogFooter>
</div>

View File

@@ -93,7 +93,7 @@ export function QrCodeModal({ isOpen, onClose, shareLink, shareName }: QrCodeMod
{t("common.close")}
</Button>
<Button onClick={downloadQRCode} className="mt-2 sm:mt-0" disabled={isDownloading}>
<IconDownload className="h-4 w-4 mr-2" />
<IconDownload className="h-4 w-4" />
{t("qrCodeModal.download", { defaultValue: "Download QR Code" })}
</Button>
</DialogFooter>

View File

@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -23,20 +19,9 @@
}
],
"paths": {
"@/*": [
"./src/*"
]
"@/*": ["./src/*"]
}
},
"exclude": [
"node_modules",
".next/types/app/api/(proxy)/**/*",
".next/types/**/*.ts"
],
"include": [
"**/*.ts",
"**/*.tsx",
"next-env.d.ts",
".next/types/**/*.ts"
]
}
"exclude": ["node_modules", ".next/types/app/api/(proxy)/**/*", ".next/types/**/*.ts"],
"include": ["**/*.ts", "**/*.tsx", "next-env.d.ts", ".next/types/**/*.ts"]
}