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

View File

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

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} iconos de {libraryCount} bibliotecas", "stats": "{iconCount} iconos de {libraryCount} bibliotecas",
"categoryBadge": "{category} ({count} iconos)" "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": { "login": {
"welcome": "Bienvenido a", "welcome": "Bienvenido a",
"signInToContinue": "Inicia sesión para continuar", "signInToContinue": "Inicia sesión para continuar",
@@ -1719,11 +1725,5 @@
"passwordRequired": "Se requiere la contraseña", "passwordRequired": "Se requiere la contraseña",
"nameRequired": "El nombre es obligatorio", "nameRequired": "El nombre es obligatorio",
"required": "Este campo 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", "stats": "{iconCount} icônes de {libraryCount} bibliothèques",
"categoryBadge": "{category} ({count} icônes)" "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": { "login": {
"welcome": "Bienvenue à", "welcome": "Bienvenue à",
"signInToContinue": "Connectez-vous pour continuer", "signInToContinue": "Connectez-vous pour continuer",
@@ -1719,11 +1725,5 @@
"passwordRequired": "Le mot de passe est requis", "passwordRequired": "Le mot de passe est requis",
"nameRequired": "Nome é obrigatório", "nameRequired": "Nome é obrigatório",
"required": "Este campo é 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} आइकन", "stats": "{libraryCount} लाइब्रेरी से {iconCount} आइकन",
"categoryBadge": "{category} ({count} आइकन)" "categoryBadge": "{category} ({count} आइकन)"
}, },
"imageEdit": {
"title": "छवि संपादित करें",
"rotate": "घुमाएं",
"zoom": "ज़ूम",
"cropInstructions": "छवि को पुनः स्थानांतरित करने के लिए खींचें, कोणों को समायोजित करने के लिए आकार बदलें"
},
"login": { "login": {
"welcome": "स्वागत है में", "welcome": "स्वागत है में",
"signInToContinue": "जारी रखने के लिए साइन इन करें", "signInToContinue": "जारी रखने के लिए साइन इन करें",
@@ -1719,11 +1725,5 @@
"passwordRequired": "पासवर्ड आवश्यक है", "passwordRequired": "पासवर्ड आवश्यक है",
"nameRequired": "नाम आवश्यक है", "nameRequired": "नाम आवश्यक है",
"required": "यह फ़ील्ड आवश्यक है" "required": "यह फ़ील्ड आवश्यक है"
},
"imageEdit": {
"title": "छवि संपादित करें",
"rotate": "घुमाएं",
"zoom": "ज़ूम",
"cropInstructions": "छवि को पुनः स्थानांतरित करने के लिए खींचें, कोणों को समायोजित करने के लिए आकार बदलें"
} }
} }

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} icone da {libraryCount} librerie", "stats": "{iconCount} icone da {libraryCount} librerie",
"categoryBadge": "{category} ({count} icone)" "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": { "login": {
"welcome": "Benvenuto in", "welcome": "Benvenuto in",
"signInToContinue": "Accedi per continuare", "signInToContinue": "Accedi per continuare",
@@ -1719,11 +1725,5 @@
"passwordMinLength": "La password deve contenere almeno 6 caratteri", "passwordMinLength": "La password deve contenere almeno 6 caratteri",
"nameRequired": "Il nome è obbligatorio", "nameRequired": "Il nome è obbligatorio",
"required": "Questo campo è 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}個のアイコン", "stats": "{libraryCount}ライブラリから{iconCount}個のアイコン",
"categoryBadge": "{category}{count}個のアイコン)" "categoryBadge": "{category}{count}個のアイコン)"
}, },
"imageEdit": {
"title": "画像を編集",
"rotate": "回転",
"zoom": "ズーム",
"cropInstructions": "位置を変更するにはドラッグし、カット領域を調整するには角をリサイズしてください"
},
"login": { "login": {
"welcome": "ようこそへ", "welcome": "ようこそへ",
"signInToContinue": "続行するにはサインインしてください", "signInToContinue": "続行するにはサインインしてください",
@@ -1719,11 +1725,5 @@
"passwordRequired": "パスワードは必須です", "passwordRequired": "パスワードは必須です",
"nameRequired": "名前は必須です", "nameRequired": "名前は必須です",
"required": "このフィールドは必須です" "required": "このフィールドは必須です"
},
"imageEdit": {
"title": "画像を編集",
"rotate": "回転",
"zoom": "ズーム",
"cropInstructions": "位置を変更するにはドラッグし、カット領域を調整するには角をリサイズしてください"
} }
} }

View File

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

View File

@@ -355,6 +355,12 @@
"stats": "{iconCount} pictogrammen van {libraryCount} bibliotheken", "stats": "{iconCount} pictogrammen van {libraryCount} bibliotheken",
"categoryBadge": "{category} ({count} pictogrammen)" "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": { "login": {
"welcome": "Welkom bij", "welcome": "Welkom bij",
"signInToContinue": "Log in om door te gaan", "signInToContinue": "Log in om door te gaan",
@@ -1719,11 +1725,5 @@
"passwordMinLength": "Wachtwoord moet minimaal 6 tekens bevatten", "passwordMinLength": "Wachtwoord moet minimaal 6 tekens bevatten",
"nameRequired": "Naam is verplicht", "nameRequired": "Naam is verplicht",
"required": "Dit veld 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", "stats": "{iconCount} ikon z {libraryCount} bibliotek",
"categoryBadge": "{category} ({count} ikon)" "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": { "login": {
"welcome": "Witaj w", "welcome": "Witaj w",
"signInToContinue": "Zaloguj się, aby kontynuować", "signInToContinue": "Zaloguj się, aby kontynuować",
@@ -1719,11 +1725,5 @@
"passwordMinLength": "Hasło musi mieć co najmniej 6 znaków", "passwordMinLength": "Hasło musi mieć co najmniej 6 znaków",
"nameRequired": "Nazwa jest wymagana", "nameRequired": "Nazwa jest wymagana",
"required": "To pole jest wymagane" "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", "stats": "{iconCount} ícones de {libraryCount} bibliotecas",
"categoryBadge": "{category} ({count} ícones)" "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": { "login": {
"welcome": "Bem-vindo ao", "welcome": "Bem-vindo ao",
"signInToContinue": "Faça login para continuar", "signInToContinue": "Faça login para continuar",
@@ -1719,11 +1725,5 @@
"lastNameRequired": "O sobrenome é necessário", "lastNameRequired": "O sobrenome é necessário",
"usernameLength": "O nome de usuário deve ter pelo menos 3 caracteres", "usernameLength": "O nome de usuário deve ter pelo menos 3 caracteres",
"usernameSpaces": "O nome de usuário não pode conter espaços" "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} библиотек", "stats": "{iconCount} иконок из {libraryCount} библиотек",
"categoryBadge": "{category} ({count} иконок)" "categoryBadge": "{category} ({count} иконок)"
}, },
"imageEdit": {
"title": "Редактировать изображение",
"rotate": "Повернуть",
"zoom": "Увеличить",
"cropInstructions": "Перетащите, чтобы переместить, измените размер углов, чтобы отрегулировать область обрезки"
},
"login": { "login": {
"welcome": "Добро пожаловать в", "welcome": "Добро пожаловать в",
"signInToContinue": "Войдите, чтобы продолжить", "signInToContinue": "Войдите, чтобы продолжить",
@@ -1719,11 +1725,5 @@
"passwordRequired": "Требуется пароль", "passwordRequired": "Требуется пароль",
"nameRequired": "Требуется имя", "nameRequired": "Требуется имя",
"required": "Это поле обязательно" "required": "Это поле обязательно"
},
"imageEdit": {
"title": "Редактировать изображение",
"rotate": "Повернуть",
"zoom": "Увеличить",
"cropInstructions": "Перетащите, чтобы переместить, измените размер углов, чтобы отрегулировать область обрезки"
} }
} }

View File

@@ -355,6 +355,12 @@
"stats": "{libraryCount} kütüphaneden {iconCount} simge", "stats": "{libraryCount} kütüphaneden {iconCount} simge",
"categoryBadge": "{category} ({count} 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": { "login": {
"welcome": "Hoş geldiniz'e", "welcome": "Hoş geldiniz'e",
"signInToContinue": "Devam etmek için oturum açın", "signInToContinue": "Devam etmek için oturum açın",
@@ -1719,11 +1725,5 @@
"passwordRequired": "Şifre gerekli", "passwordRequired": "Şifre gerekli",
"nameRequired": "İsim gereklidir", "nameRequired": "İsim gereklidir",
"required": "Bu alan zorunludur" "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} 个图标", "stats": "来自 {libraryCount} 个库的 {iconCount} 个图标",
"categoryBadge": "{category}{count} 个图标)" "categoryBadge": "{category}{count} 个图标)"
}, },
"imageEdit": {
"title": "编辑图片",
"rotate": "旋转",
"zoom": "缩放",
"cropInstructions": "拖动以重新定位,调整角落大小以调整裁剪区域"
},
"login": { "login": {
"welcome": "欢迎您", "welcome": "欢迎您",
"signInToContinue": "请登录以继续", "signInToContinue": "请登录以继续",
@@ -1483,11 +1489,7 @@
"copyToClipboard": "复制到剪贴板", "copyToClipboard": "复制到剪贴板",
"savedMessage": "我已保存备用码", "savedMessage": "我已保存备用码",
"available": "可用备用码:{count}个", "available": "可用备用码:{count}个",
"instructions": [ "instructions": ["• 将这些代码保存在安全的位置", "• 每个备用码只能使用一次", "• 您可以随时生成新的备用码"]
"• 将这些代码保存在安全的位置",
"• 每个备用码只能使用一次",
"• 您可以随时生成新的备用码"
]
}, },
"verification": { "verification": {
"title": "双重认证", "title": "双重认证",
@@ -1719,11 +1721,5 @@
"passwordRequired": "密码为必填项", "passwordRequired": "密码为必填项",
"nameRequired": "名称为必填项", "nameRequired": "名称为必填项",
"required": "此字段为必填项" "required": "此字段为必填项"
},
"imageEdit": {
"title": "编辑图片",
"rotate": "旋转",
"zoom": "缩放",
"cropInstructions": "拖动以重新定位,调整角落大小以调整裁剪区域"
} }
} }

View File

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

View File

@@ -3,6 +3,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { import {
IconCopy, IconCopy,
IconDownload,
IconEdit, IconEdit,
IconLink, IconLink,
IconLock, IconLock,
@@ -11,6 +12,7 @@ import {
IconToggleRight, IconToggleRight,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import QRCode from "react-qr-code";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -42,6 +44,7 @@ interface ReverseShareDetailsModalProps {
onCopyLink?: (reverseShare: ReverseShare) => void; onCopyLink?: (reverseShare: ReverseShare) => void;
onToggleActive?: (id: string, isActive: boolean) => Promise<void>; onToggleActive?: (id: string, isActive: boolean) => Promise<void>;
onUpdatePassword?: (id: string, data: { hasPassword: boolean; password?: string }) => Promise<void>; onUpdatePassword?: (id: string, data: { hasPassword: boolean; password?: string }) => Promise<void>;
onViewQrCode?: (reverseShare: ReverseShare) => void;
refreshTrigger?: number; refreshTrigger?: number;
onSuccess?: () => void; onSuccess?: () => void;
} }
@@ -55,10 +58,12 @@ export function ReverseShareDetailsModal({
onCopyLink, onCopyLink,
onToggleActive, onToggleActive,
onUpdatePassword, onUpdatePassword,
onViewQrCode,
onSuccess, onSuccess,
}: ReverseShareDetailsModalProps) { }: ReverseShareDetailsModalProps) {
const t = useTranslations(); const t = useTranslations();
const [pendingChanges, setPendingChanges] = useState<Record<string, any>>({}); const [pendingChanges, setPendingChanges] = useState<Record<string, any>>({});
const [isDownloading, setIsDownloading] = useState(false);
const { const {
showAliasModal, showAliasModal,
@@ -140,6 +145,7 @@ export function ReverseShareDetailsModal({
isActive={reverseShare.isActive} isActive={reverseShare.isActive}
/> />
<div className="grid grid-cols-2 gap-4">
{/* Informações Básicas */} {/* Informações Básicas */}
<div className="space-y-3"> <div className="space-y-3">
<h3 className="text-base font-medium text-foreground border-b pb-2"> <h3 className="text-base font-medium text-foreground border-b pb-2">
@@ -182,6 +188,78 @@ export function ReverseShareDetailsModal({
/> />
</div> </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 */} {/* Link de Compartilhamento */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center gap-2 border-b pb-2"> <div className="flex items-center gap-2 border-b pb-2">

View File

@@ -10,6 +10,7 @@ interface ReverseSharesCardsContainerProps {
onGenerateLink: (reverseShare: ReverseShare) => void; onGenerateLink: (reverseShare: ReverseShare) => void;
onViewDetails: (reverseShare: ReverseShare) => void; onViewDetails: (reverseShare: ReverseShare) => void;
onViewFiles: (reverseShare: ReverseShare) => void; onViewFiles: (reverseShare: ReverseShare) => void;
onViewQrCode?: (reverseShare: ReverseShare) => void;
onCreateReverseShare: () => void; onCreateReverseShare: () => void;
onUpdateReverseShare?: (id: string, data: any) => Promise<any>; onUpdateReverseShare?: (id: string, data: any) => Promise<any>;
onToggleActive?: (id: string, isActive: boolean) => Promise<any>; onToggleActive?: (id: string, isActive: boolean) => Promise<any>;
@@ -24,6 +25,7 @@ export function ReverseSharesCardsContainer({
onGenerateLink, onGenerateLink,
onViewDetails, onViewDetails,
onViewFiles, onViewFiles,
onViewQrCode,
onCreateReverseShare, onCreateReverseShare,
onUpdateReverseShare, onUpdateReverseShare,
onToggleActive, onToggleActive,
@@ -45,6 +47,7 @@ export function ReverseSharesCardsContainer({
onGenerateLink={onGenerateLink} onGenerateLink={onGenerateLink}
onViewDetails={onViewDetails} onViewDetails={onViewDetails}
onViewFiles={onViewFiles} onViewFiles={onViewFiles}
onViewQrCode={onViewQrCode}
onUpdateReverseShare={onUpdateReverseShare} onUpdateReverseShare={onUpdateReverseShare}
onToggleActive={onToggleActive} onToggleActive={onToggleActive}
onUpdatePassword={onUpdatePassword} 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 type { CreateReverseShareBody, UpdateReverseShareBody } from "@/http/endpoints/reverse-shares/types";
import { ReverseShare } from "../hooks/use-reverse-shares"; import { ReverseShare } from "../hooks/use-reverse-shares";
import { CreateReverseShareModal } from "./create-reverse-share-modal"; import { CreateReverseShareModal } from "./create-reverse-share-modal";
@@ -20,14 +21,17 @@ interface ReverseSharesModalsProps {
reverseShareToGenerateLink: ReverseShare | null; reverseShareToGenerateLink: ReverseShare | null;
reverseShareToDelete: ReverseShare | null; reverseShareToDelete: ReverseShare | null;
reverseShareToViewFiles: ReverseShare | null; reverseShareToViewFiles: ReverseShare | null;
reverseShareToViewQrCode: ReverseShare | null;
isDeleting: boolean; isDeleting: boolean;
onCloseViewDetails: () => void; onCloseViewDetails: () => void;
onCloseGenerateLink: () => void; onCloseGenerateLink: () => void;
onCloseDeleteModal: () => void; onCloseDeleteModal: () => void;
onCloseViewFiles: () => void; onCloseViewFiles: () => void;
onCloseViewQrCode: () => void;
onConfirmDelete: (reverseShare: ReverseShare) => Promise<void>; onConfirmDelete: (reverseShare: ReverseShare) => Promise<void>;
onCreateAlias: (reverseShareId: string, alias: string) => Promise<void>; onCreateAlias: (reverseShareId: string, alias: string) => Promise<void>;
onCopyLink: (reverseShare: ReverseShare) => void; onCopyLink: (reverseShare: ReverseShare) => void;
onViewQrCode: (reverseShare: ReverseShare) => void;
onUpdateReverseShareData?: (id: string, data: any) => Promise<any>; onUpdateReverseShareData?: (id: string, data: any) => Promise<any>;
onUpdatePassword?: (id: string, data: { hasPassword: boolean; password?: string }) => Promise<any>; onUpdatePassword?: (id: string, data: { hasPassword: boolean; password?: string }) => Promise<any>;
onToggleActive?: (id: string, isActive: boolean) => Promise<any>; onToggleActive?: (id: string, isActive: boolean) => Promise<any>;
@@ -48,14 +52,17 @@ export function ReverseSharesModals({
reverseShareToGenerateLink, reverseShareToGenerateLink,
reverseShareToDelete, reverseShareToDelete,
reverseShareToViewFiles, reverseShareToViewFiles,
reverseShareToViewQrCode,
isDeleting, isDeleting,
onCloseViewDetails, onCloseViewDetails,
onCloseGenerateLink, onCloseGenerateLink,
onCloseDeleteModal, onCloseDeleteModal,
onCloseViewFiles, onCloseViewFiles,
onCloseViewQrCode,
onConfirmDelete, onConfirmDelete,
onCreateAlias, onCreateAlias,
onCopyLink, onCopyLink,
onViewQrCode,
onUpdateReverseShareData, onUpdateReverseShareData,
onUpdatePassword, onUpdatePassword,
onToggleActive, onToggleActive,
@@ -103,6 +110,7 @@ export function ReverseSharesModals({
onCopyLink={onCopyLink} onCopyLink={onCopyLink}
onUpdatePassword={onUpdatePassword} onUpdatePassword={onUpdatePassword}
onToggleActive={onToggleActive} onToggleActive={onToggleActive}
onViewQrCode={onViewQrCode}
/> />
<ReceivedFilesModal <ReceivedFilesModal
@@ -112,6 +120,17 @@ export function ReverseSharesModals({
onRefresh={onRefreshData} onRefresh={onRefreshData}
refreshReverseShare={refreshReverseShare} 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 [reverseShareToDelete, setReverseShareToDelete] = useState<ReverseShare | null>(null);
const [reverseShareToEdit, setReverseShareToEdit] = useState<ReverseShare | null>(null); const [reverseShareToEdit, setReverseShareToEdit] = useState<ReverseShare | null>(null);
const [reverseShareToViewFiles, setReverseShareToViewFiles] = useState<ReverseShare | null>(null); const [reverseShareToViewFiles, setReverseShareToViewFiles] = useState<ReverseShare | null>(null);
const [reverseShareToViewQrCode, setReverseShareToViewQrCode] = useState<ReverseShare | null>(null);
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
@@ -277,6 +278,7 @@ export function useReverseShares() {
reverseShareToDelete, reverseShareToDelete,
reverseShareToEdit, reverseShareToEdit,
reverseShareToViewFiles, reverseShareToViewFiles,
reverseShareToViewQrCode,
isDeleting, isDeleting,
isCreateModalOpen, isCreateModalOpen,
isCreating, isCreating,
@@ -288,6 +290,7 @@ export function useReverseShares() {
setReverseShareToDelete, setReverseShareToDelete,
setReverseShareToEdit, setReverseShareToEdit,
setReverseShareToViewFiles, setReverseShareToViewFiles,
setReverseShareToViewQrCode,
setIsCreateModalOpen, setIsCreateModalOpen,
handleCopyLink, handleCopyLink,
handleDeleteReverseShare, handleDeleteReverseShare,

View File

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

View File

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

View File

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

View File

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