feat: enhance file sharing and management features

- Added support for file descriptions in sharing modals, allowing users to provide additional context when sharing files.
- Implemented bulk sharing functionality, enabling users to share multiple files at once with a single action.
- Enhanced file management capabilities with new modals for bulk downloads and deletions, improving user experience.
- Updated localization files to include new strings related to file sharing and management across multiple languages.
- Improved the file manager to handle bulk actions and state management more effectively.
This commit is contained in:
Daniel Luiz Alves
2025-06-02 03:09:15 -03:00
parent d69453e4ae
commit ff6e171e91
41 changed files with 5077 additions and 3123 deletions

View File

@@ -15,13 +15,15 @@
"createShare": {
"title": "إنشاء مشاركة",
"nameLabel": "اسم المشاركة",
"descriptionLabel": "الوصف",
"descriptionPlaceholder": "أدخل وصفًا (اختياري)",
"expirationLabel": "تاريخ الانتهاء",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"maxViewsLabel": "أقصى عدد للمشاهدات",
"maxViewsPlaceholder": "اتركه فارغاً للمحدودية غير المحدودة",
"passwordProtection": "حماية بكلمة المرور",
"maxViewsLabel": "الحد الأقصى للمشاهدات",
"maxViewsPlaceholder": "اتركه فارغًا للحصول على عدد غير محدود",
"passwordProtection": "محمي بكلمة مرور",
"passwordLabel": "كلمة المرور",
"create": "إنشاء المشاركة",
"create": "إنشاء مشاركة",
"success": "تم إنشاء المشاركة بنجاح",
"error": "فشل في إنشاء المشاركة"
},
@@ -82,20 +84,27 @@
"saveChanges": "احفظ التغييرات"
},
"files": {
"title": "كل الملفات",
"title": "جميع الملفات",
"uploadFile": "رفع ملف",
"loadError": "فشل في تحميل الملفات",
"pageTitle": "ملفاتي",
"breadcrumb": "ملفاتي",
"downloadStart": "بدأ التنزيل",
"downloadError": "فشل في تنزيل الملف",
"downloadStart": "بدأ التحميل",
"downloadError": "فشل في تحميل الملف",
"updateSuccess": "تم تحديث الملف بنجاح",
"updateError": "فشل في تحديث الملف",
"deleteSuccess": "تم حذف الملف بنجاح",
"deleteError": "فشل في حذف الملف"
"deleteError": "فشل في حذف الملف",
"bulkDownloadSuccess": "بدأ تحميل الملفات بنجاح",
"bulkDownloadError": "خطأ في إنشاء ملف ZIP",
"bulkDownloadFileError": "خطأ في تحميل الملف {fileName}",
"bulkDeleteSuccess": "{count, plural, =1 {تم حذف ملف واحد بنجاح} other {تم حذف # ملفات بنجاح}}",
"bulkDeleteError": "خطأ في حذف الملفات المحددة"
},
"filesTable": {
"ariaLabel": "جدول الملفات",
"selectAll": "تحديد الكل",
"selectFile": "تحديد الملف {fileName}",
"columns": {
"name": "الاسم",
"description": "الوصف",
@@ -106,11 +115,18 @@
},
"actions": {
"menu": "قائمة إجراءات الملف",
"preview": "المعاينة",
"edit": عديل",
"preview": "معاينة",
"edit": حرير",
"share": "مشاركة",
"download": "تحميل",
"delete": "حذف"
},
"bulkActions": {
"selected": "{count, plural, =1 {تم تحديد ملف واحد} other {تم تحديد # ملفات}}",
"actions": "الإجراءات",
"download": "تحميل المحدد",
"share": "مشاركة المحدد",
"delete": "حذف المحدد"
}
},
"footer": {
@@ -457,42 +473,53 @@
},
"shareActions": {
"deleteTitle": "حذف المشاركة",
"deleteConfirmation": "هل أنت متأكد من رغبتك في حذف هذه المشاركة؟ هذا الإجراء لا يمكن التراجع عنه.",
"editTitle": عديل المشاركة",
"deleteConfirmation": "هل أنت متأكد من أنك تريد حذف هذه المشاركة؟ لا يمكن التراجع عن هذا الإجراء.",
"editTitle": حرير المشاركة",
"nameLabel": "اسم المشاركة",
"expirationLabel": "تاريخ الانتهاء",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"maxViewsLabel": "أقصى عدد للمشاهدات",
"maxViewsPlaceholder": "اتركه فارغاً للمحدودية غير المحدودة",
"passwordProtection": "محمي بكلمة المرور",
"descriptionLabel": "الوصف",
"descriptionPlaceholder": "أدخل وصفاً (اختياري)",
"expirationLabel": "تاريخ انتهاء الصلاحية",
"expirationPlaceholder": "YYYY/MM/DD HH:MM",
"maxViewsLabel": "الحد الأقصى للمشاهدات",
"maxViewsPlaceholder": "اتركه فارغاً للمشاهدات غير المحدودة",
"passwordProtection": "محمي بكلمة مرور",
"passwordLabel": "كلمة المرور",
"passwordPlaceholder": "أدخل كلمة المرور",
"newPasswordLabel": "كلمة مرور جديدة (اتركه فارغاً للاحتفاظ بالحالي)",
"newPasswordPlaceholder": "أدخل كلمة مرور جديدة",
"newPasswordLabel": "كلمة المرور الجديدة (اتركها فارغة للاحتفاظ بالحالية)",
"newPasswordPlaceholder": "أدخل كلمة المرور الجديدة",
"manageFilesTitle": "إدارة الملفات",
"manageRecipientsTitle": "إدارة المستلمين",
"manageRecipientsTitle": "إدارة المستقبلين",
"editSuccess": "تم تحديث المشاركة بنجاح",
"editError": "فشل في تحديث المشاركة"
},
"shareDetails": {
"title": "تفاصيل المشاركة",
"subtitle": "معلومات مفصلة عن هذه المشاركة",
"subtitle": "معلومات تفصيلية حول هذه المشاركة",
"basicInfo": "المعلومات الأساسية",
"name": "الاسم",
"description": "الوصف",
"noDescription": "لم يتم توفير وصف",
"untitled": "بدون عنوان",
"shareLink": "رابط المشاركة",
"editLink": "تحرير الرابط",
"generateLink": "إنشاء رابط",
"noLink": "لم يتم إنشاء رابط بعد",
"copyLink": "نسخ الرابط",
"openLink": "فتح في علامة تبويب جديدة",
"linkCopied": "تم نسخ الرابط إلى الحافظة",
"views": "المشاهدات",
"dates": "التواريخ",
"created": "تم الإنشاء",
"expires": "تنتهي",
"never": "أبدًا",
"expires": "ينتهي",
"never": "أبداً",
"security": "الأمان",
"passwordProtected": "محمي بكلمة المرور",
"publicAccess": "الوصول العام",
"maxViews": "أقصى عدد للمشاهدات:",
"passwordProtected": "محمي بكلمة مرور",
"publicAccess": "وصول عام",
"maxViews": "المشاهدات القصوى:",
"files": "الملفات",
"recipients": "المستلمون",
"notAvailable": "غير متوفر",
"invalidDate": "تاريخ غير صالح",
"recipients": "المستقبلون",
"notAvailable": "غير متاح",
"invalidDate": "تاريخ غير صحيح",
"loadError": "فشل في تحميل تفاصيل المشاركة"
},
"shareManager": {
@@ -538,38 +565,39 @@
},
"sharesTable": {
"ariaLabel": "جدول المشاركات",
"never": "أبدًا",
"never": "أبداً",
"columns": {
"name": "الاسم",
"description": "الوصف",
"createdAt": "تاريخ الإنشاء",
"expiresAt": "تاريخ الانتهاء",
"expiresAt": "تاريخ انتهاء الصلاحية",
"status": "الحالة",
"security": "الأمان",
"files": "الملفات",
"recipients": "المستلمون",
"recipients": "المستقبلون",
"actions": "الإجراءات"
},
"status": {
"neverExpires": "لا تنتهي",
"active": "نشطة",
"expired": "منتهية"
"neverExpires": "لا تنتهي صلاحيتها أبداً",
"active": "نشط",
"expired": "منتهي الصلاحية"
},
"security": {
"protected": "محمي",
"public": "عام"
},
"filesCount": "ملف",
"recipientsCount": "مستلم",
"filesCount": "ملفات",
"recipientsCount": "مستقبلين",
"actions": {
"menu": "قائمة إجراءات المشاركة",
"edit": عديل",
"edit": حرير",
"manageFiles": "إدارة الملفات",
"manageRecipients": "إدارة المستلمين",
"manageRecipients": "إدارة المستقبلين",
"viewDetails": "عرض التفاصيل",
"generateLink": "إنشاء الرابط",
"editLink": عديل الرابط",
"generateLink": "إنشاء رابط",
"editLink": حرير الرابط",
"copyLink": "نسخ الرابط",
"notifyRecipients": علام المستلمين",
"notifyRecipients": شعار المستقبلين",
"delete": "حذف"
}
},
@@ -694,14 +722,16 @@
"passwordRequired": "كلمة المرور مطلوبة"
},
"shareFile": {
"title": "مشاركة الملف",
"linkTitle": "إنشاء الرابط",
"title": "مشاركة ملف",
"linkTitle": "إنشاء رابط",
"nameLabel": "اسم المشاركة",
"namePlaceholder": "أدخل اسم المشاركة",
"descriptionLabel": "الوصف",
"descriptionPlaceholder": "أدخل وصفاً (اختياري)",
"expirationLabel": "تاريخ انتهاء الصلاحية",
"expirationPlaceholder": "يوم/شهر/سنة ساعة:دقيقة",
"expirationPlaceholder": "YYYY/MM/DD HH:MM",
"maxViewsLabel": "الحد الأقصى للمشاهدات",
"maxViewsPlaceholder": "اتركه فارغاً للمشاهدات اللامحدودة",
"maxViewsPlaceholder": "اتركه فارغاً للمشاهدات غير المحدودة",
"passwordProtection": "محمي بكلمة مرور",
"passwordLabel": "كلمة المرور",
"passwordPlaceholder": "أدخل كلمة المرور",
@@ -710,7 +740,29 @@
"aliasPlaceholder": "أدخل اسماً مستعاراً مخصصاً",
"linkReady": "رابط المشاركة جاهز:",
"createShare": "إنشاء مشاركة",
"generateLink": "إنشاء الرابط",
"generateLink": "إنشاء رابط",
"copyLink": "نسخ الرابط"
},
"bulkDownload": {
"title": "التحميل بالجملة",
"zipNameLabel": "اسم ملف ZIP",
"zipNamePlaceholder": "أدخل اسم الملف",
"description": "{count, plural, =1 {سيتم ضغط ملف واحد} other {سيتم ضغط # ملفات}}",
"download": "تحميل ZIP"
},
"deleteConfirmation": {
"filesToDelete": "الملفات المراد حذفها"
},
"shareMultipleFiles": {
"title": "مشاركة ملفات متعددة",
"shareNameLabel": "اسم المشاركة",
"shareNamePlaceholder": "أدخل اسم المشاركة",
"descriptionLabel": "الوصف",
"descriptionPlaceholder": "أدخل وصفًا (اختياري)",
"filesToShare": "الملفات للمشاركة",
"files": "ملفات",
"totalSize": "الحجم الإجمالي",
"creating": "جاري الإنشاء...",
"create": "إنشاء مشاركة"
}
}

View File

@@ -13,15 +13,17 @@
"back": "Zurück"
},
"createShare": {
"title": "Freigabe erstellen",
"nameLabel": "Freigabename",
"title": "Freigabe Erstellen",
"nameLabel": "Freigabe-Name",
"descriptionLabel": "Beschreibung",
"descriptionPlaceholder": "Geben Sie eine Beschreibung ein (optional)",
"expirationLabel": "Ablaufdatum",
"expirationPlaceholder": "MM/TT/JJJJ SS:MM",
"expirationPlaceholder": "TT.MM.JJJJ HH:MM",
"maxViewsLabel": "Maximale Ansichten",
"maxViewsPlaceholder": "Leer lassen für unbegrenzt",
"passwordProtection": "Passwortgeschützt",
"passwordProtection": "Passwort-geschützt",
"passwordLabel": "Passwort",
"create": "Freigabe erstellen",
"create": "Freigabe Erstellen",
"success": "Freigabe erfolgreich erstellt",
"error": "Fehler beim Erstellen der Freigabe"
},
@@ -83,7 +85,7 @@
},
"files": {
"title": "Alle Dateien",
"uploadFile": "Datei hochladen",
"uploadFile": "Datei Hochladen",
"loadError": "Fehler beim Laden der Dateien",
"pageTitle": "Meine Dateien",
"breadcrumb": "Meine Dateien",
@@ -92,10 +94,17 @@
"updateSuccess": "Datei erfolgreich aktualisiert",
"updateError": "Fehler beim Aktualisieren der Datei",
"deleteSuccess": "Datei erfolgreich gelöscht",
"deleteError": "Fehler beim Löschen der Datei"
"deleteError": "Fehler beim Löschen der Datei",
"bulkDownloadSuccess": "Datei-Download erfolgreich gestartet",
"bulkDownloadError": "Fehler beim Erstellen der ZIP-Datei",
"bulkDownloadFileError": "Fehler beim Herunterladen der Datei {fileName}",
"bulkDeleteSuccess": "{count, plural, =1 {1 Datei erfolgreich gelöscht} other {# Dateien erfolgreich gelöscht}}",
"bulkDeleteError": "Fehler beim Löschen der ausgewählten Dateien"
},
"filesTable": {
"ariaLabel": "Dateitabelle",
"ariaLabel": "Dateien-Tabelle",
"selectAll": "Alle auswählen",
"selectFile": "Datei {fileName} auswählen",
"columns": {
"name": "NAME",
"description": "BESCHREIBUNG",
@@ -111,6 +120,13 @@
"share": "Teilen",
"download": "Herunterladen",
"delete": "Löschen"
},
"bulkActions": {
"selected": "{count, plural, =1 {1 Datei ausgewählt} other {# Dateien ausgewählt}}",
"actions": "Aktionen",
"download": "Ausgewählte Herunterladen",
"share": "Ausgewählte Teilen",
"delete": "Ausgewählte Löschen"
}
},
"footer": {
@@ -456,44 +472,55 @@
"pageTitle": "Freigabe"
},
"shareActions": {
"deleteTitle": "Freigabe löschen",
"deleteTitle": "Freigabe Löschen",
"deleteConfirmation": "Sind Sie sicher, dass Sie diese Freigabe löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
"editTitle": "Freigabe bearbeiten",
"nameLabel": "Freigabename",
"editTitle": "Freigabe Bearbeiten",
"nameLabel": "Freigabe-Name",
"descriptionLabel": "Beschreibung",
"descriptionPlaceholder": "Geben Sie eine Beschreibung ein (optional)",
"expirationLabel": "Ablaufdatum",
"expirationPlaceholder": "MM/TT/JJJJ SS:MM",
"expirationPlaceholder": "TT.MM.JJJJ HH:MM",
"maxViewsLabel": "Maximale Ansichten",
"maxViewsPlaceholder": "Leer lassen für unbegrenzt",
"passwordProtection": "Passwortgeschützt",
"passwordProtection": "Passwort-geschützt",
"passwordLabel": "Passwort",
"passwordPlaceholder": "Passwort eingeben",
"newPasswordLabel": "Neues Passwort (leer lassen, um das aktuelle zu behalten)",
"newPasswordPlaceholder": "Neues Passwort eingeben",
"manageFilesTitle": "Dateien verwalten",
"manageRecipientsTitle": "Empfänger verwalten",
"manageFilesTitle": "Dateien Verwalten",
"manageRecipientsTitle": "Empfänger Verwalten",
"editSuccess": "Freigabe erfolgreich aktualisiert",
"editError": "Fehler beim Aktualisieren der Freigabe"
},
"shareDetails": {
"title": "Freigabedetails",
"subtitle": "Detaillierte Informationen zu dieser Freigabe",
"title": "Freigabe-Details",
"subtitle": "Detaillierte Informationen über diese Freigabe",
"basicInfo": "Grundinformationen",
"name": "Name",
"untitled": "Ohne Titel",
"views": "Aufrufe",
"description": "Beschreibung",
"noDescription": "Keine Beschreibung angegeben",
"untitled": "Unbenannt",
"shareLink": "Freigabe-Link",
"editLink": "Link Bearbeiten",
"generateLink": "Link Generieren",
"noLink": "Noch kein Link generiert",
"copyLink": "Link kopieren",
"openLink": "In neuem Tab öffnen",
"linkCopied": "Link in Zwischenablage kopiert",
"views": "Ansichten",
"dates": "Daten",
"created": "Erstellt",
"expires": "Läuft ab",
"never": "Nie",
"never": "Niemals",
"security": "Sicherheit",
"passwordProtected": "Passwortgeschützt",
"publicAccess": "Öffentlicher Zugriff",
"maxViews": "Maximale Ansichten:",
"passwordProtected": "Passwort-geschützt",
"publicAccess": "Öffentlicher Zugang",
"maxViews": "Max. Ansichten:",
"files": "Dateien",
"recipients": "Empfänger",
"notAvailable": "N/V",
"invalidDate": "Ungültiges Datum",
"loadError": "Fehler beim Laden der Freigabedetails"
"loadError": "Fehler beim Laden der Freigabe-Details"
},
"shareManager": {
"deleteSuccess": "Freigabe erfolgreich gelöscht",
@@ -537,12 +564,13 @@
"pageTitle": "Freigaben"
},
"sharesTable": {
"ariaLabel": "Freigabetabelle",
"never": "Nie",
"ariaLabel": "Freigaben-Tabelle",
"never": "Niemals",
"columns": {
"name": "NAME",
"description": "BESCHREIBUNG",
"createdAt": "ERSTELLT AM",
"expiresAt": "LÄUFT AB",
"expiresAt": "LÄUFT AB AM",
"status": "STATUS",
"security": "SICHERHEIT",
"files": "DATEIEN",
@@ -550,7 +578,7 @@
"actions": "AKTIONEN"
},
"status": {
"neverExpires": "Läuft nie ab",
"neverExpires": "Läuft Nie Ab",
"active": "Aktiv",
"expired": "Abgelaufen"
},
@@ -561,15 +589,15 @@
"filesCount": "Dateien",
"recipientsCount": "Empfänger",
"actions": {
"menu": "Freigabeaktionsmenü",
"menu": "Freigabe-Aktionen-Menü",
"edit": "Bearbeiten",
"manageFiles": "Dateien verwalten",
"manageRecipients": "Empfänger verwalten",
"viewDetails": "Details anzeigen",
"generateLink": "Link generieren",
"editLink": "Link bearbeiten",
"copyLink": "Link kopieren",
"notifyRecipients": "Empfänger benachrichtigen",
"manageFiles": "Dateien Verwalten",
"manageRecipients": "Empfänger Verwalten",
"viewDetails": "Details Anzeigen",
"generateLink": "Link Generieren",
"editLink": "Link Bearbeiten",
"copyLink": "Link Kopieren",
"notifyRecipients": "Empfänger Benachrichtigen",
"delete": "Löschen"
}
},
@@ -694,23 +722,47 @@
"passwordRequired": "Passwort ist erforderlich"
},
"shareFile": {
"title": "Datei teilen",
"linkTitle": "Link generieren",
"title": "Datei Freigeben",
"linkTitle": "Link Generieren",
"nameLabel": "Freigabe-Name",
"namePlaceholder": "Freigabe-Name eingeben",
"namePlaceholder": "Freigabe-Namen eingeben",
"descriptionLabel": "Beschreibung",
"descriptionPlaceholder": "Geben Sie eine Beschreibung ein (optional)",
"expirationLabel": "Ablaufdatum",
"expirationPlaceholder": "TT.MM.JJJJ HH:MM",
"maxViewsLabel": "Maximale Aufrufe",
"maxViewsLabel": "Maximale Ansichten",
"maxViewsPlaceholder": "Leer lassen für unbegrenzt",
"passwordProtection": "Passwort geschützt",
"passwordProtection": "Passwort-geschützt",
"passwordLabel": "Passwort",
"passwordPlaceholder": "Passwort eingeben",
"linkDescription": "Generieren Sie einen benutzerdefinierten Link zum Teilen der Datei",
"linkDescription": "Einen benutzerdefinierten Link zum Teilen der Datei generieren",
"aliasLabel": "Link-Alias",
"aliasPlaceholder": "Benutzerdefinierten Alias eingeben",
"linkReady": "Ihr Freigabe-Link ist bereit:",
"createShare": "Freigabe erstellen",
"generateLink": "Link generieren",
"copyLink": "Link kopieren"
"createShare": "Freigabe Erstellen",
"generateLink": "Link Generieren",
"copyLink": "Link Kopieren"
},
"bulkDownload": {
"title": "Massen-Download",
"zipNameLabel": "ZIP-Dateiname",
"zipNamePlaceholder": "Dateiname eingeben",
"description": "{count, plural, =1 {1 Datei wird komprimiert} other {# Dateien werden komprimiert}}",
"download": "ZIP Herunterladen"
},
"deleteConfirmation": {
"filesToDelete": "Zu löschende Dateien"
},
"shareMultipleFiles": {
"title": "Mehrere Dateien Teilen",
"shareNameLabel": "Freigabe-Name",
"shareNamePlaceholder": "Freigabe-Name eingeben",
"descriptionLabel": "Beschreibung",
"descriptionPlaceholder": "Geben Sie eine Beschreibung ein (optional)",
"filesToShare": "Zu teilende Dateien",
"files": "Dateien",
"totalSize": "Gesamtgröße",
"creating": "Erstellen...",
"create": "Freigabe Erstellen"
}
}

View File

@@ -15,6 +15,8 @@
"createShare": {
"title": "Create Share",
"nameLabel": "Share Name",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Enter a description (optional)",
"expirationLabel": "Expiration Date",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"maxViewsLabel": "Max Views",
@@ -92,10 +94,17 @@
"updateSuccess": "File updated successfully",
"updateError": "Failed to update file",
"deleteSuccess": "File deleted successfully",
"deleteError": "Failed to delete file"
"deleteError": "Failed to delete file",
"bulkDownloadSuccess": "Files download started successfully",
"bulkDownloadError": "Error creating ZIP file",
"bulkDownloadFileError": "Error downloading file {fileName}",
"bulkDeleteSuccess": "{count, plural, =1 {1 file deleted successfully} other {# files deleted successfully}}",
"bulkDeleteError": "Error deleting selected files"
},
"filesTable": {
"ariaLabel": "Files table",
"selectAll": "Select all",
"selectFile": "Select file {fileName}",
"columns": {
"name": "NAME",
"description": "DESCRIPTION",
@@ -111,6 +120,13 @@
"share": "Share",
"download": "Download",
"delete": "Delete"
},
"bulkActions": {
"selected": "{count, plural, =1 {1 file selected} other {# files selected}}",
"actions": "Actions",
"download": "Download Selected",
"share": "Share Selected",
"delete": "Delete Selected"
}
},
"footer": {
@@ -460,6 +476,8 @@
"deleteConfirmation": "Are you sure you want to delete this share? This action cannot be undone.",
"editTitle": "Edit Share",
"nameLabel": "Share Name",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Enter a description (optional)",
"expirationLabel": "Expiration Date",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"maxViewsLabel": "Max Views",
@@ -479,7 +497,16 @@
"subtitle": "Detailed information about this share",
"basicInfo": "Basic Information",
"name": "Name",
"description": "Description",
"noDescription": "No description provided",
"untitled": "Untitled",
"shareLink": "Share Link",
"editLink": "Edit Link",
"generateLink": "Generate Link",
"noLink": "No link generated yet",
"copyLink": "Copy link",
"openLink": "Open in new tab",
"linkCopied": "Link copied to clipboard",
"views": "Views",
"dates": "Dates",
"created": "Created",
@@ -541,6 +568,7 @@
"never": "Never",
"columns": {
"name": "NAME",
"description": "DESCRIPTION",
"createdAt": "CREATED AT",
"expiresAt": "EXPIRES AT",
"status": "STATUS",
@@ -697,6 +725,8 @@
"linkTitle": "Generate Link",
"nameLabel": "Share Name",
"namePlaceholder": "Enter share name",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Enter a description (optional)",
"expirationLabel": "Expiration Date",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"maxViewsLabel": "Maximum Views",
@@ -711,5 +741,27 @@
"createShare": "Create Share",
"generateLink": "Generate Link",
"copyLink": "Copy Link"
},
"bulkDownload": {
"title": "Bulk Download",
"zipNameLabel": "ZIP file name",
"zipNamePlaceholder": "Enter file name",
"description": "{count, plural, =1 {1 file will be compressed} other {# files will be compressed}}",
"download": "Download ZIP"
},
"deleteConfirmation": {
"filesToDelete": "Files to be deleted"
},
"shareMultipleFiles": {
"title": "Share Multiple Files",
"shareNameLabel": "Share Name",
"shareNamePlaceholder": "Enter share name",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Enter a description (optional)",
"filesToShare": "Files to share",
"files": "files",
"totalSize": "Total size",
"creating": "Creating...",
"create": "Create Share"
}
}

View File

@@ -13,17 +13,19 @@
"back": "Volver"
},
"createShare": {
"title": "Crear compartición",
"nameLabel": "Nombre de la compartición",
"expirationLabel": "Fecha de expiración",
"expirationPlaceholder": "MM/DD/AAAA HH:MM",
"maxViewsLabel": "Máximo de visualizaciones",
"maxViewsPlaceholder": "Dejar vacío para ilimitado",
"passwordProtection": "Protección por contraseña",
"title": "Crear Compartir",
"nameLabel": "Nombre del Compartir",
"descriptionLabel": "Descripción",
"descriptionPlaceholder": "Ingrese una descripción (opcional)",
"expirationLabel": "Fecha de Expiración",
"expirationPlaceholder": "DD/MM/AAAA HH:MM",
"maxViewsLabel": "Vistas Máximas",
"maxViewsPlaceholder": "Deje vacío para ilimitado",
"passwordProtection": "Protegido por Contraseña",
"passwordLabel": "Contraseña",
"create": "Crear compartición",
"success": "Compartición creada exitosamente",
"error": "Error al crear la compartición"
"create": "Crear Compartir",
"success": "Compartir creado exitosamente",
"error": "Error al crear compartir"
},
"dashboard": {
"loadError": "Error al cargar los datos del tablero",
@@ -82,35 +84,49 @@
"saveChanges": "Guardar cambios"
},
"files": {
"title": "Todos los archivos",
"uploadFile": "Subir archivo",
"loadError": "Error al cargar los archivos",
"pageTitle": "Mis archivos",
"breadcrumb": "Mis archivos",
"title": "Todos los Archivos",
"uploadFile": "Subir Archivo",
"loadError": "Error al cargar archivos",
"pageTitle": "Mis Archivos",
"breadcrumb": "Mis Archivos",
"downloadStart": "Descarga iniciada",
"downloadError": "Error al descargar el archivo",
"downloadError": "Error al descargar archivo",
"updateSuccess": "Archivo actualizado exitosamente",
"updateError": "Error al actualizar el archivo",
"updateError": "Error al actualizar archivo",
"deleteSuccess": "Archivo eliminado exitosamente",
"deleteError": "Error al eliminar el archivo"
"deleteError": "Error al eliminar archivo",
"bulkDownloadSuccess": "Descarga de archivos iniciada exitosamente",
"bulkDownloadError": "Error al crear archivo ZIP",
"bulkDownloadFileError": "Error al descargar archivo {fileName}",
"bulkDeleteSuccess": "{count, plural, =1 {1 archivo eliminado exitosamente} other {# archivos eliminados exitosamente}}",
"bulkDeleteError": "Error al eliminar archivos seleccionados"
},
"filesTable": {
"ariaLabel": "Tabla de archivos",
"selectAll": "Seleccionar todo",
"selectFile": "Seleccionar archivo {fileName}",
"columns": {
"name": "NOMBRE",
"description": "DESCRIPCIÓN",
"size": "TAMAÑO",
"createdAt": "CREADO",
"updatedAt": "ACTUALIZADO",
"createdAt": "CREADO EN",
"updatedAt": "ACTUALIZADO EN",
"actions": "ACCIONES"
},
"actions": {
"menu": "Menú de acciones del archivo",
"menu": "Menú de acciones de archivo",
"preview": "Vista previa",
"edit": "Editar",
"share": "Compartir",
"download": "Descargar",
"delete": "Eliminar"
},
"bulkActions": {
"selected": "{count, plural, =1 {1 archivo seleccionado} other {# archivos seleccionados}}",
"actions": "Acciones",
"download": "Descargar Seleccionados",
"share": "Compartir Seleccionados",
"delete": "Eliminar Seleccionados"
}
},
"footer": {
@@ -456,44 +472,55 @@
"pageTitle": "Compartición"
},
"shareActions": {
"deleteTitle": "Eliminar compartición",
"deleteTitle": "Eliminar Compartir",
"deleteConfirmation": "¿Estás seguro de que deseas eliminar esta compartición? Esta acción no se puede deshacer.",
"editTitle": "Editar compartición",
"nameLabel": "Nombre de la compartición",
"expirationLabel": "Fecha de expiración",
"expirationPlaceholder": "MM/DD/AAAA HH:MM",
"maxViewsLabel": "Máximo de visualizaciones",
"maxViewsPlaceholder": "Dejar vacío para ilimitado",
"passwordProtection": "Protegido con contraseña",
"editTitle": "Editar Compartir",
"nameLabel": "Nombre del Compartir",
"descriptionLabel": "Descripción",
"descriptionPlaceholder": "Ingrese una descripción (opcional)",
"expirationLabel": "Fecha de Expiración",
"expirationPlaceholder": "DD/MM/AAAA HH:MM",
"maxViewsLabel": "Vistas Máximas",
"maxViewsPlaceholder": "Deje vacío para ilimitado",
"passwordProtection": "Protegido por Contraseña",
"passwordLabel": "Contraseña",
"passwordPlaceholder": "Introduce la contraseña",
"newPasswordLabel": "Nueva contraseña (dejar vacío para mantener la actual)",
"newPasswordPlaceholder": "Introduce la nueva contraseña",
"manageFilesTitle": "Gestionar archivos",
"manageRecipientsTitle": "Gestionar destinatarios",
"editSuccess": "Compartición actualizada exitosamente",
"editError": "Error al actualizar la compartición"
"passwordPlaceholder": "Ingrese contraseña",
"newPasswordLabel": "Nueva Contraseña (deje vacío para mantener la actual)",
"newPasswordPlaceholder": "Ingrese nueva contraseña",
"manageFilesTitle": "Administrar Archivos",
"manageRecipientsTitle": "Administrar Destinatarios",
"editSuccess": "Compartir actualizado exitosamente",
"editError": "Error al actualizar compartir"
},
"shareDetails": {
"title": "Detalles de la compartición",
"subtitle": "Información detallada sobre esta compartición",
"basicInfo": "Información básica",
"title": "Detalles del Compartir",
"subtitle": "Información detallada sobre este compartir",
"basicInfo": "Información Básica",
"name": "Nombre",
"description": "Descripción",
"noDescription": "Sin descripción proporcionada",
"untitled": "Sin título",
"shareLink": "Enlace de Compartir",
"editLink": "Editar Enlace",
"generateLink": "Generar Enlace",
"noLink": "Ningún enlace generado aún",
"copyLink": "Copiar enlace",
"openLink": "Abrir en nueva pestaña",
"linkCopied": "Enlace copiado al portapapeles",
"views": "Visualizaciones",
"dates": "Fechas",
"created": "Creada",
"created": "Creado",
"expires": "Expira",
"never": "Nunca",
"security": "Seguridad",
"passwordProtected": "Protegido con contraseña",
"publicAccess": "Acceso público",
"maxViews": "Máximo de visualizaciones:",
"passwordProtected": "Protegido por Contraseña",
"publicAccess": "Acceso Público",
"maxViews": "Vistas Máx.:",
"files": "Archivos",
"recipients": "Destinatarios",
"notAvailable": "N/D",
"notAvailable": "N/A",
"invalidDate": "Fecha inválida",
"loadError": "Error al cargar los detalles de la compartición"
"loadError": "Error al cargar detalles del compartir"
},
"shareManager": {
"deleteSuccess": "Compartición eliminada exitosamente",
@@ -541,8 +568,9 @@
"never": "Nunca",
"columns": {
"name": "NOMBRE",
"createdAt": "CREADO",
"expiresAt": "EXPIRA",
"description": "DESCRIPCIÓN",
"createdAt": "CREADO EN",
"expiresAt": "EXPIRA EN",
"status": "ESTADO",
"security": "SEGURIDAD",
"files": "ARCHIVOS",
@@ -550,26 +578,26 @@
"actions": "ACCIONES"
},
"status": {
"neverExpires": "Nunca expira",
"active": "Activa",
"expired": "Expirada"
"neverExpires": "Nunca Expira",
"active": "Activo",
"expired": "Expirado"
},
"security": {
"protected": "Protegida",
"public": "Pública"
"protected": "Protegido",
"public": "Público"
},
"filesCount": "archivos",
"recipientsCount": "destinatarios",
"actions": {
"menu": "Menú de acciones de la compartición",
"menu": "Menú de acciones de compartir",
"edit": "Editar",
"manageFiles": "Gestionar archivos",
"manageRecipients": "Gestionar destinatarios",
"viewDetails": "Ver detalles",
"generateLink": "Generar enlace",
"editLink": "Editar enlace",
"copyLink": "Copiar enlace",
"notifyRecipients": "Notificar destinatarios",
"manageFiles": "Administrar Archivos",
"manageRecipients": "Administrar Destinatarios",
"viewDetails": "Ver Detalles",
"generateLink": "Generar Enlace",
"editLink": "Editar Enlace",
"copyLink": "Copiar Enlace",
"notifyRecipients": "Notificar Destinatarios",
"delete": "Eliminar"
}
},
@@ -694,23 +722,47 @@
"passwordRequired": "Se requiere la contraseña"
},
"shareFile": {
"title": "Compartir archivo",
"linkTitle": "Generar enlace",
"nameLabel": "Nombre del compartir",
"title": "Compartir Archivo",
"linkTitle": "Generar Enlace",
"nameLabel": "Nombre del Compartir",
"namePlaceholder": "Ingrese nombre del compartir",
"expirationLabel": "Fecha de expiración",
"descriptionLabel": "Descripción",
"descriptionPlaceholder": "Ingrese una descripción (opcional)",
"expirationLabel": "Fecha de Expiración",
"expirationPlaceholder": "DD/MM/AAAA HH:MM",
"maxViewsLabel": "Máximo de visualizaciones",
"maxViewsLabel": "Vistas Máximas",
"maxViewsPlaceholder": "Deje vacío para ilimitado",
"passwordProtection": "Protegido con contraseña",
"passwordProtection": "Protegido por Contraseña",
"passwordLabel": "Contraseña",
"passwordPlaceholder": "Ingrese contraseña",
"linkDescription": "Genere un enlace personalizado para compartir el archivo",
"aliasLabel": "Alias del enlace",
"aliasLabel": "Alias del Enlace",
"aliasPlaceholder": "Ingrese alias personalizado",
"linkReady": "Su enlace de compartir está listo:",
"createShare": "Crear compartir",
"generateLink": "Generar enlace",
"copyLink": "Copiar enlace"
"createShare": "Crear Compartir",
"generateLink": "Generar Enlace",
"copyLink": "Copiar Enlace"
},
"bulkDownload": {
"title": "Descarga Masiva",
"zipNameLabel": "Nombre del archivo ZIP",
"zipNamePlaceholder": "Ingrese nombre del archivo",
"description": "{count, plural, =1 {1 archivo será comprimido} other {# archivos serán comprimidos}}",
"download": "Descargar ZIP"
},
"deleteConfirmation": {
"filesToDelete": "Archivos que serán eliminados"
},
"shareMultipleFiles": {
"title": "Compartir Múltiples Archivos",
"shareNameLabel": "Nombre del Compartir",
"shareNamePlaceholder": "Ingrese nombre del compartir",
"descriptionLabel": "Descripción",
"descriptionPlaceholder": "Ingrese una descripción (opcional)",
"filesToShare": "Archivos para compartir",
"files": "archivos",
"totalSize": "Tamaño total",
"creating": "Creando...",
"create": "Crear Compartir"
}
}

View File

@@ -15,13 +15,15 @@
"createShare": {
"title": "Créer un Partage",
"nameLabel": "Nom du Partage",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Entrez une description (optionnel)",
"expirationLabel": "Date d'Expiration",
"expirationPlaceholder": "JJ/MM/AAAA HH:MM",
"expirationPlaceholder": "DD/MM/AAAA HH:MM",
"maxViewsLabel": "Vues Maximales",
"maxViewsPlaceholder": "Laisser vide pour illimité",
"maxViewsPlaceholder": "Laissez vide pour illimité",
"passwordProtection": "Protégé par Mot de Passe",
"passwordLabel": "Mot de Passe",
"create": "Créer le Partage",
"create": "Créer un Partage",
"success": "Partage créé avec succès",
"error": "Échec de la création du partage"
},
@@ -83,34 +85,48 @@
},
"files": {
"title": "Tous les Fichiers",
"uploadFile": "Envoyer un Fichier",
"uploadFile": "Télécharger un Fichier",
"loadError": "Échec du chargement des fichiers",
"pageTitle": "Mes Fichiers",
"breadcrumb": "Mes Fichiers",
"downloadStart": "Téléchargement commencé",
"downloadStart": "Téléchargement démarré",
"downloadError": "Échec du téléchargement du fichier",
"updateSuccess": "Fichier mis à jour avec succès",
"updateError": "Échec de la mise à jour du fichier",
"deleteSuccess": "Fichier supprimé avec succès",
"deleteError": "Échec de la suppression du fichier"
"deleteError": "Échec de la suppression du fichier",
"bulkDownloadSuccess": "Téléchargement des fichiers démarré avec succès",
"bulkDownloadError": "Erreur lors de la création du fichier ZIP",
"bulkDownloadFileError": "Erreur lors du téléchargement du fichier {fileName}",
"bulkDeleteSuccess": "{count, plural, =1 {1 fichier supprimé avec succès} other {# fichiers supprimés avec succès}}",
"bulkDeleteError": "Erreur lors de la suppression des fichiers sélectionnés"
},
"filesTable": {
"ariaLabel": "Tableau des fichiers",
"selectAll": "Tout sélectionner",
"selectFile": "Sélectionner le fichier {fileName}",
"columns": {
"name": "NOM",
"description": "DESCRIPTION",
"size": "TAILLE",
"createdAt": "CRÉÉ LE",
"updatedAt": "MODIFIÉ LE",
"updatedAt": "MIS À JOUR LE",
"actions": "ACTIONS"
},
"actions": {
"menu": "Menu d'actions de fichier",
"menu": "Menu des actions de fichier",
"preview": "Aperçu",
"edit": "Modifier",
"share": "Partager",
"download": "Télécharger",
"delete": "Supprimer"
},
"bulkActions": {
"selected": "{count, plural, =1 {1 fichier sélectionné} other {# fichiers sélectionnés}}",
"actions": "Actions",
"download": "Télécharger les Sélectionnés",
"share": "Partager les Sélectionnés",
"delete": "Supprimer les Sélectionnés"
}
},
"footer": {
@@ -460,14 +476,16 @@
"deleteConfirmation": "Êtes-vous sûr de vouloir supprimer ce partage ? Cette action ne peut pas être annulée.",
"editTitle": "Modifier le Partage",
"nameLabel": "Nom du Partage",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Entrez une description (optionnel)",
"expirationLabel": "Date d'Expiration",
"expirationPlaceholder": "JJ/MM/AAAA HH:MM",
"expirationPlaceholder": "DD/MM/AAAA HH:MM",
"maxViewsLabel": "Vues Maximales",
"maxViewsPlaceholder": "Laisser vide pour illimité",
"maxViewsPlaceholder": "Laissez vide pour illimité",
"passwordProtection": "Protégé par Mot de Passe",
"passwordLabel": "Mot de Passe",
"passwordPlaceholder": "Entrez le mot de passe",
"newPasswordLabel": "Nouveau Mot de Passe (laisser vide pour garder l'actuel)",
"newPasswordLabel": "Nouveau Mot de Passe (laissez vide pour conserver l'actuel)",
"newPasswordPlaceholder": "Entrez le nouveau mot de passe",
"manageFilesTitle": "Gérer les Fichiers",
"manageRecipientsTitle": "Gérer les Destinataires",
@@ -479,19 +497,28 @@
"subtitle": "Informations détaillées sur ce partage",
"basicInfo": "Informations de Base",
"name": "Nom",
"untitled": "Partage sans titre",
"description": "Description",
"noDescription": "Aucune description fournie",
"untitled": "Sans titre",
"shareLink": "Lien de Partage",
"editLink": "Modifier le Lien",
"generateLink": "Générer un Lien",
"noLink": "Aucun lien généré pour le moment",
"copyLink": "Copier le lien",
"openLink": "Ouvrir dans un nouvel onglet",
"linkCopied": "Lien copié dans le presse-papiers",
"views": "Vues",
"dates": "Dates",
"created": "Créé le: {date}",
"expires": "Expire le: {date}",
"created": "Créé",
"expires": "Expire",
"never": "Jamais",
"security": "Sécurité",
"passwordProtected": "Protégé par Mot de Passe",
"publicAccess": "Accès Public",
"maxViews": "Vues Maximales:",
"maxViews": "Vues Max.:",
"files": "Fichiers",
"recipients": "Destinataires",
"notAvailable": "N/D",
"notAvailable": "N/A",
"invalidDate": "Date invalide",
"loadError": "Échec du chargement des détails du partage"
},
@@ -541,6 +568,7 @@
"never": "Jamais",
"columns": {
"name": "NOM",
"description": "DESCRIPTION",
"createdAt": "CRÉÉ LE",
"expiresAt": "EXPIRE LE",
"status": "STATUT",
@@ -561,7 +589,7 @@
"filesCount": "fichiers",
"recipientsCount": "destinataires",
"actions": {
"menu": "Menu d'actions du partage",
"menu": "Menu d'actions de partage",
"edit": "Modifier",
"manageFiles": "Gérer les Fichiers",
"manageRecipients": "Gérer les Destinataires",
@@ -694,23 +722,47 @@
"passwordRequired": "Le mot de passe est requis"
},
"shareFile": {
"title": "Partager le fichier",
"linkTitle": "Générer le lien",
"nameLabel": "Nom du partage",
"title": "Partager un Fichier",
"linkTitle": "Générer un Lien",
"nameLabel": "Nom du Partage",
"namePlaceholder": "Entrez le nom du partage",
"expirationLabel": "Date d'expiration",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Entrez une description (optionnel)",
"expirationLabel": "Date d'Expiration",
"expirationPlaceholder": "DD/MM/AAAA HH:MM",
"maxViewsLabel": "Nombre maximum de vues",
"maxViewsLabel": "Vues Maximales",
"maxViewsPlaceholder": "Laissez vide pour illimité",
"passwordProtection": "Protégé par mot de passe",
"passwordLabel": "Mot de passe",
"passwordProtection": "Protégé par Mot de Passe",
"passwordLabel": "Mot de Passe",
"passwordPlaceholder": "Entrez le mot de passe",
"linkDescription": "Générez un lien personnalisé pour partager le fichier",
"aliasLabel": "Alias du lien",
"aliasLabel": "Alias du Lien",
"aliasPlaceholder": "Entrez un alias personnalisé",
"linkReady": "Votre lien de partage est prêt :",
"createShare": "Créer le partage",
"generateLink": "Générer le lien",
"copyLink": "Copier le lien"
"createShare": "Créer un Partage",
"generateLink": "Générer un Lien",
"copyLink": "Copier le Lien"
},
"bulkDownload": {
"title": "Téléchargement en Masse",
"zipNameLabel": "Nom du fichier ZIP",
"zipNamePlaceholder": "Entrez le nom du fichier",
"description": "{count, plural, =1 {1 fichier sera compressé} other {# fichiers seront compressés}}",
"download": "Télécharger le ZIP"
},
"deleteConfirmation": {
"filesToDelete": "Fichiers à supprimer"
},
"shareMultipleFiles": {
"title": "Partager Plusieurs Fichiers",
"shareNameLabel": "Nom du Partage",
"shareNamePlaceholder": "Entrez le nom du partage",
"descriptionLabel": "Description",
"descriptionPlaceholder": "Entrez une description (optionnel)",
"filesToShare": "Fichiers à partager",
"files": "fichiers",
"totalSize": "Taille totale",
"creating": "Création...",
"create": "Créer un Partage"
}
}

View File

@@ -13,15 +13,17 @@
"back": "वापस"
},
"createShare": {
"title": "साझा करें बनाएं",
"nameLabel": "साझाकरण का नाम",
"title": "साझाकर बनाएं",
"nameLabel": "साझाकरण नाम",
"descriptionLabel": "विवरण",
"descriptionPlaceholder": "विवरण दर्ज करें (वैकल्पिक)",
"expirationLabel": "समाप्ति तिथि",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"maxViewsLabel": "अधिकतम दृश्य",
"maxViewsPlaceholder": "अनलिमिटेड के लिए खाली छोड़ें",
"passwordProtection": "पासवर्ड सुरक्ष",
"maxViewsPlaceholder": "असीमित के लिए खाली छोड़ें",
"passwordProtection": "पासवर्ड सुरक्षित",
"passwordLabel": "पासवर्ड",
"create": "साझा करें बनाएं",
"create": "साझाकर बनाएं",
"success": "साझाकरण सफलतापूर्वक बनाया गया",
"error": "साझाकरण बनाने में विफल"
},
@@ -84,18 +86,25 @@
"files": {
"title": "सभी फाइलें",
"uploadFile": "फाइल अपलोड करें",
"loadError": "फाइलें लोड करने में त्रुटि",
"loadError": "फाइलें लोड करने में विफल",
"pageTitle": "मेरी फाइलें",
"breadcrumb": "मेरी फाइलें",
"downloadStart": "डाउनलोड शुरू हुआ",
"downloadError": "फाइल डाउनलोड करने में त्रुटि",
"updateSuccess": "फाइल सफलतापूर्वक अपडेट हुई",
"updateError": "फाइल अपडेट करने में त्रुटि",
"downloadError": "फाइल डाउनलोड करने में विफल",
"updateSuccess": "फाइल सफलतापूर्वक अपडेट की गई",
"updateError": "फाइल अपडेट करने में विफल",
"deleteSuccess": "फाइल सफलतापूर्वक हटाई गई",
"deleteError": "फाइल हटाने में त्रुटि"
"deleteError": "फाइल हटाने में विफल",
"bulkDownloadSuccess": "फाइलों का डाउनलोड सफलतापूर्वक शुरू हुआ",
"bulkDownloadError": "ZIP फाइल बनाने में त्रुटि",
"bulkDownloadFileError": "फाइल {fileName} डाउनलोड करने में त्रुटि",
"bulkDeleteSuccess": "{count, plural, =1 {1 फाइल सफलतापूर्वक हटाई गई} other {# फाइलें सफलतापूर्वक हटाई गईं}}",
"bulkDeleteError": "चयनित फाइलों को हटाने में त्रुटि"
},
"filesTable": {
"ariaLabel": "फाइल टेबल",
"ariaLabel": "फाइल तालिका",
"selectAll": "सभी चुनें",
"selectFile": "फाइल {fileName} चुनें",
"columns": {
"name": "नाम",
"description": "विवरण",
@@ -105,12 +114,19 @@
"actions": "क्रियाएं"
},
"actions": {
"menu": "फाइल क्शन मेनू",
"menu": "फाइल क्रिया मेनू",
"preview": "पूर्वावलोकन",
"edit": "संपादित करें",
"share": "साझा करें",
"download": "डाउनलोड करें",
"delete": "डिलीट करें"
"delete": "हटाएं"
},
"bulkActions": {
"selected": "{count, plural, =1 {1 फाइल चयनित} other {# फाइलें चयनित}}",
"actions": "क्रियाएं",
"download": "चयनित डाउनलोड करें",
"share": "चयनित साझा करें",
"delete": "चयनित हटाएं"
}
},
"footer": {
@@ -457,41 +473,52 @@
},
"shareActions": {
"deleteTitle": "साझाकरण हटाएं",
"deleteConfirmation": "क्या आप वाकई इस साझाकरण को हटाना चाहते हैं? यह क्रिया अपरिवर्तनीय है।",
"deleteConfirmation": "क्या आप वाकई इस साझाकरण को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती।",
"editTitle": "साझाकरण संपादित करें",
"nameLabel": "साझाकरण का नाम",
"nameLabel": "साझाकरण नाम",
"descriptionLabel": "विवरण",
"descriptionPlaceholder": "विवरण दर्ज करें (वैकल्पिक)",
"expirationLabel": "समाप्ति तिथि",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"expirationPlaceholder": "DD/MM/YYYY HH:MM",
"maxViewsLabel": "अधिकतम दृश्य",
"maxViewsPlaceholder": "अनलिमिटेड के लिए खाली छोड़ें",
"passwordProtection": "पासवर्ड सरक्ष",
"maxViewsPlaceholder": "असीमित के लिए खाली छोड़ें",
"passwordProtection": "पासवर्ड सरक्षित",
"passwordLabel": "पासवर्ड",
"passwordPlaceholder": "पासवर्ड दर्ज करें",
"newPasswordLabel": "नया पासवर्ड (वर्तमान रखने के लिए खाली छोड़ें)",
"newPasswordPlaceholder": "नया पासवर्ड दर्ज करें",
"manageFilesTitle": "फाइलें प्रबंधित करें",
"manageRecipientsTitle": "प्राप्तकर्ताओं का प्रबंध करें",
"editSuccess": "साझाकरण सफलतापूर्वक अपडेट हुआ",
"manageRecipientsTitle": "प्राप्तकर्ता प्रबंधित करें",
"editSuccess": "साझाकरण सफलतापूर्वक अपडेट किया गया",
"editError": "साझाकरण अपडेट करने में विफल"
},
"shareDetails": {
"title": "साझाकरण विवरण",
"subtitle": "इस साझाकरण क विस्तृत जानकारी",
"basicInfo": "मूल जानकारी",
"subtitle": "इस साझाकरण के बारे में विस्तृत जानकारी",
"basicInfo": "मूलभूत जानकारी",
"name": "नाम",
"untitled": "बिना शीर्षक के",
"description": "विवरण",
"noDescription": "कोई विवरण प्रदान नहीं किया गया",
"untitled": "बिना शीर्षक",
"shareLink": "साझाकरण लिंक",
"editLink": "लिंक संपादित करें",
"generateLink": "लिंक जेनरेट करें",
"noLink": "अभी तक कोई लिंक जेनरेट नहीं किया गया",
"copyLink": "लिंक कॉपी करें",
"openLink": "नए टैब में खोलें",
"linkCopied": "लिंक क्लिपबोर्ड में कॉपी कर दिया गया",
"views": "दृश्य",
"dates": "तिथिया",
"dates": "तिथिया",
"created": "बनाया गया",
"expires": "समाप्त होता है",
"never": "कभी नहीं",
"security": "सुरक्षा",
"passwordProtected": "पासवर्ड द्वारा संरक्षित",
"publicAccess": "सार्वजनिक पहुच",
"passwordProtected": "पासवर्ड संरक्षित",
"publicAccess": "सार्वजनिक पहुच",
"maxViews": "अधिकतम दृश्य:",
"files": "फाइलें",
"recipients": "प्राप्तकर्ता",
"notAvailable": "एन/ए",
"notAvailable": "उप/नहीं",
"invalidDate": "अमान्य तिथि",
"loadError": "साझाकरण विवरण लोड करने में विफल"
},
@@ -537,10 +564,11 @@
"pageTitle": "साझाकरण"
},
"sharesTable": {
"ariaLabel": "साझाकरण टेबल",
"ariaLabel": "साझाकरण तालिका",
"never": "कभी नहीं",
"columns": {
"name": "नाम",
"description": "विवरण",
"createdAt": "बनाया गया",
"expiresAt": "समाप्त होता है",
"status": "स्थिति",
@@ -550,7 +578,7 @@
"actions": "क्रियाएं"
},
"status": {
"neverExpires": "कभी समाप्त नहीं",
"neverExpires": "कभी समाप्त नहीं होता",
"active": "सक्रिय",
"expired": "समाप्त"
},
@@ -564,9 +592,9 @@
"menu": "साझाकरण क्रिया मेनू",
"edit": "संपादित करें",
"manageFiles": "फाइलें प्रबंधित करें",
"manageRecipients": "प्राप्तकर्ताओं का प्रबंध करें",
"manageRecipients": "प्राप्तकर्ता प्रबंधित करें",
"viewDetails": "विवरण देखें",
"generateLink": "लिंक उत्पन्न करें",
"generateLink": "लिंक जेनरेट करें",
"editLink": "लिंक संपादित करें",
"copyLink": "लिंक कॉपी करें",
"notifyRecipients": "प्राप्तकर्ताओं को सूचित करें",
@@ -694,23 +722,47 @@
"passwordRequired": "पासवर्ड आवश्यक है"
},
"shareFile": {
"title": "फाइल साझा करें",
"title": "फाइल साझा करें",
"linkTitle": "लिंक जेनरेट करें",
"nameLabel": "शेयर का नाम",
"namePlaceholder": "शेयर का नाम दर्ज करें",
"expirationLabel": "समाप्ति तारीख",
"nameLabel": "साझाकरण नाम",
"namePlaceholder": "साझाकरण नाम दर्ज करें",
"descriptionLabel": "विवरण",
"descriptionPlaceholder": "विवरण दर्ज करें (वैकल्पिक)",
"expirationLabel": "समाप्ति तिथि",
"expirationPlaceholder": "DD/MM/YYYY HH:MM",
"maxViewsLabel": "अधिकतम ्य",
"maxViewsLabel": "अधिकतम दृश्य",
"maxViewsPlaceholder": "असीमित के लिए खाली छोड़ें",
"passwordProtection": "पासवर्ड सरक्षित",
"passwordProtection": "पासवर्ड सरक्षित",
"passwordLabel": "पासवर्ड",
"passwordPlaceholder": "पासवर्ड दर्ज करें",
"linkDescription": "फाइल साझा करने के लिए एक कस्टम लिंक जेनरेट करें",
"aliasLabel": "लिंक एलियास",
"aliasPlaceholder": "कस्टम एलियास दर्ज करें",
"linkReady": "आपका शेयर लिंक तैयार है:",
"createShare": "शेयर बनाएं",
"linkDescription": "फाइल साझा करने के लिए कस्टम लिंक जेनरेट करें",
"aliasLabel": "लिंक उपनाम",
"aliasPlaceholder": "कस्टम उपनाम दर्ज करें",
"linkReady": "आपका साझाकरण लिंक तैयार है:",
"createShare": "साझाकरण बनाएं",
"generateLink": "लिंक जेनरेट करें",
"copyLink": "लिंक कॉपी करें"
},
"bulkDownload": {
"title": "बल्क डाउनलोड",
"zipNameLabel": "ZIP फाइल नाम",
"zipNamePlaceholder": "फाइल नाम दर्ज करें",
"description": "{count, plural, =1 {1 फाइल संपीड़ित की जाएगी} other {# फाइलें संपीड़ित की जाएंगी}}",
"download": "ZIP डाउनलोड करें"
},
"deleteConfirmation": {
"filesToDelete": "हटाई जाने वाली फाइलें"
},
"shareMultipleFiles": {
"title": "कई फाइलें साझा करें",
"shareNameLabel": "साझाकरण नाम",
"shareNamePlaceholder": "साझाकरण नाम दर्ज करें",
"descriptionLabel": "विवरण",
"descriptionPlaceholder": "विवरण दर्ज करें (वैकल्पिक)",
"filesToShare": "साझा करने के लिए फाइलें",
"files": "फाइलें",
"totalSize": "कुल आकार",
"creating": "बनाया जा रहा है...",
"create": "साझाकरण बनाएं"
}
}

View File

@@ -15,15 +15,17 @@
"createShare": {
"title": "Crea Condivisione",
"nameLabel": "Nome Condivisione",
"descriptionLabel": "Descrizione",
"descriptionPlaceholder": "Inserisci una descrizione (opzionale)",
"expirationLabel": "Data di Scadenza",
"expirationPlaceholder": "GG/MM/AAAA HH:MM",
"maxViewsLabel": "Visualizzazioni Massime",
"maxViewsPlaceholder": "Lascia vuoto per illimitate",
"passwordProtection": "Protetto da Parola d'accesso",
"passwordLabel": "Parola d'accesso",
"maxViewsPlaceholder": "Lascia vuoto per illimitato",
"passwordProtection": "Protetto da Password",
"passwordLabel": "Password",
"create": "Crea Condivisione",
"success": "Condivisione creata con successo",
"error": "Errore durante la creazione della condivisione"
"error": "Errore nella creazione della condivisione"
},
"dashboard": {
"loadError": "Errore durante il caricamento dei dati del pannello di controllo",
@@ -84,18 +86,25 @@
"files": {
"title": "Tutti i File",
"uploadFile": "Carica File",
"loadError": "Errore durante il caricamento dei file",
"loadError": "Errore nel caricamento dei file",
"pageTitle": "I Miei File",
"breadcrumb": "I Miei File",
"downloadStart": "Download iniziato",
"downloadError": "Errore durante il download del file",
"downloadStart": "Download avviato",
"downloadError": "Errore nel download del file",
"updateSuccess": "File aggiornato con successo",
"updateError": "Errore durante l'aggiornamento del file",
"updateError": "Errore nell'aggiornamento del file",
"deleteSuccess": "File eliminato con successo",
"deleteError": "Errore durante l'eliminazione del file"
"deleteError": "Errore nell'eliminazione del file",
"bulkDownloadSuccess": "Download dei file avviato con successo",
"bulkDownloadError": "Errore nella creazione del file ZIP",
"bulkDownloadFileError": "Errore nel download del file {fileName}",
"bulkDeleteSuccess": "{count, plural, =1 {1 file eliminato con successo} other {# file eliminati con successo}}",
"bulkDeleteError": "Errore nell'eliminazione dei file selezionati"
},
"filesTable": {
"ariaLabel": "Tabella dei file",
"selectAll": "Seleziona tutto",
"selectFile": "Seleziona file {fileName}",
"columns": {
"name": "NOME",
"description": "DESCRIZIONE",
@@ -111,6 +120,13 @@
"share": "Condividi",
"download": "Scarica",
"delete": "Elimina"
},
"bulkActions": {
"selected": "{count, plural, =1 {1 file selezionato} other {# file selezionati}}",
"actions": "Azioni",
"download": "Scarica Selezionati",
"share": "Condividi Selezionati",
"delete": "Elimina Selezionati"
}
},
"footer": {
@@ -460,40 +476,51 @@
"deleteConfirmation": "Sei sicuro di voler eliminare questa condivisione? Questa azione non può essere annullata.",
"editTitle": "Modifica Condivisione",
"nameLabel": "Nome Condivisione",
"descriptionLabel": "Descrizione",
"descriptionPlaceholder": "Inserisci una descrizione (opzionale)",
"expirationLabel": "Data di Scadenza",
"expirationPlaceholder": "GG/MM/AAAA HH:MM",
"maxViewsLabel": "Visualizzazioni Massime",
"maxViewsPlaceholder": "Lascia vuoto per illimitate",
"passwordProtection": "Protetto da Parola d'accesso",
"passwordLabel": "Parola d'accesso",
"passwordPlaceholder": "Inserisci parola d'accesso",
"newPasswordLabel": "Nuova Parola d'accesso (lascia vuoto per mantenere quella attuale)",
"newPasswordPlaceholder": "Inserisci nuova parola d'accesso",
"maxViewsPlaceholder": "Lasciare vuoto per illimitato",
"passwordProtection": "Protetto da Password",
"passwordLabel": "Password",
"passwordPlaceholder": "Inserisci password",
"newPasswordLabel": "Nuova Password (lasciare vuoto per mantenere quella attuale)",
"newPasswordPlaceholder": "Inserisci nuova password",
"manageFilesTitle": "Gestisci File",
"manageRecipientsTitle": "Gestisci Destinatari",
"editSuccess": "Condivisione aggiornata con successo",
"editError": "Errore durante l'aggiornamento della condivisione"
"editError": "Errore nell'aggiornamento della condivisione"
},
"shareDetails": {
"title": "Dettagli Condivisione",
"subtitle": "Informazioni dettagliate su questa condivisione",
"basicInfo": "Informazioni Base",
"name": "Nome",
"description": "Descrizione",
"noDescription": "Nessuna descrizione fornita",
"untitled": "Senza titolo",
"shareLink": "Link di Condivisione",
"editLink": "Modifica Link",
"generateLink": "Genera Link",
"noLink": "Nessun link generato ancora",
"copyLink": "Copia link",
"openLink": "Apri in nuova scheda",
"linkCopied": "Link copiato negli appunti",
"views": "Visualizzazioni",
"dates": "Date",
"created": "Creata",
"created": "Creato",
"expires": "Scade",
"never": "Mai",
"security": "Sicurezza",
"passwordProtected": "Protetta da Parola d'accesso",
"passwordProtected": "Protetto da Password",
"publicAccess": "Accesso Pubblico",
"maxViews": "Visualizzazioni Massime:",
"maxViews": "Visualizzazioni Max:",
"files": "File",
"recipients": "Destinatari",
"notAvailable": "N/D",
"invalidDate": "Data non valida",
"loadError": "Errore durante il caricamento dei dettagli della condivisione"
"loadError": "Errore nel caricamento dei dettagli della condivisione"
},
"shareManager": {
"deleteSuccess": "Condivisione eliminata con successo",
@@ -541,7 +568,8 @@
"never": "Mai",
"columns": {
"name": "NOME",
"createdAt": "CREATA IL",
"description": "DESCRIZIONE",
"createdAt": "CREATO IL",
"expiresAt": "SCADE IL",
"status": "STATO",
"security": "SICUREZZA",
@@ -551,12 +579,12 @@
},
"status": {
"neverExpires": "Non Scade Mai",
"active": "Attiva",
"expired": "Scaduta"
"active": "Attivo",
"expired": "Scaduto"
},
"security": {
"protected": "Protetta",
"public": "Pubblica"
"protected": "Protetto",
"public": "Pubblico"
},
"filesCount": "file",
"recipientsCount": "destinatari",
@@ -693,23 +721,47 @@
"passwordRequired": "La parola d'accesso è obbligatoria"
},
"shareFile": {
"title": "Condividi file",
"linkTitle": "Genera link",
"nameLabel": "Nome condivisione",
"title": "Condividi File",
"linkTitle": "Genera Link",
"nameLabel": "Nome Condivisione",
"namePlaceholder": "Inserisci nome condivisione",
"expirationLabel": "Data di scadenza",
"descriptionLabel": "Descrizione",
"descriptionPlaceholder": "Inserisci una descrizione (opzionale)",
"expirationLabel": "Data di Scadenza",
"expirationPlaceholder": "GG/MM/AAAA HH:MM",
"maxViewsLabel": "Visualizzazioni massime",
"maxViewsPlaceholder": "Lascia vuoto per illimitato",
"passwordProtection": "Protetto da password",
"maxViewsLabel": "Visualizzazioni Massime",
"maxViewsPlaceholder": "Lasciare vuoto per illimitato",
"passwordProtection": "Protetto da Password",
"passwordLabel": "Password",
"passwordPlaceholder": "Inserisci password",
"linkDescription": "Genera un link personalizzato per condividere il file",
"aliasLabel": "Alias del link",
"aliasLabel": "Alias Link",
"aliasPlaceholder": "Inserisci alias personalizzato",
"linkReady": "Il tuo link di condivisione è pronto:",
"createShare": "Crea condivisione",
"generateLink": "Genera link",
"copyLink": "Copia link"
"createShare": "Crea Condivisione",
"generateLink": "Genera Link",
"copyLink": "Copia Link"
},
"bulkDownload": {
"title": "Download di Massa",
"zipNameLabel": "Nome file ZIP",
"zipNamePlaceholder": "Inserisci nome file",
"description": "{count, plural, =1 {1 file sarà compresso} other {# file saranno compressi}}",
"download": "Scarica ZIP"
},
"deleteConfirmation": {
"filesToDelete": "File da eliminare"
},
"shareMultipleFiles": {
"title": "Condividi File Multipli",
"shareNameLabel": "Nome Condivisione",
"shareNamePlaceholder": "Inserisci nome condivisione",
"descriptionLabel": "Descrizione",
"descriptionPlaceholder": "Inserisci una descrizione (opzionale)",
"filesToShare": "File da condividere",
"files": "file",
"totalSize": "Dimensione totale",
"creating": "Creazione...",
"create": "Crea Condivisione"
}
}

View File

@@ -15,10 +15,12 @@
"createShare": {
"title": "共有を作成",
"nameLabel": "共有名",
"descriptionLabel": "説明",
"descriptionPlaceholder": "説明を入力してください(任意)",
"expirationLabel": "有効期限",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"maxViewsLabel": "最大ビュー数",
"maxViewsPlaceholder": "無制限の場合は空のままにする",
"maxViewsLabel": "最大表示回数",
"maxViewsPlaceholder": "無制限の場合は空のままにしてください",
"passwordProtection": "パスワード保護",
"passwordLabel": "パスワード",
"create": "共有を作成",
@@ -87,22 +89,29 @@
"loadError": "ファイルの読み込みに失敗しました",
"pageTitle": "マイファイル",
"breadcrumb": "マイファイル",
"downloadStart": "ダウンロード開始されました",
"downloadStart": "ダウンロード開始ました",
"downloadError": "ファイルのダウンロードに失敗しました",
"updateSuccess": "ファイルが正常に更新されました",
"updateError": "ファイルの更新に失敗しました",
"deleteSuccess": "ファイルが正常に削除されました",
"deleteError": "ファイルの削除に失敗しました"
"deleteError": "ファイルの削除に失敗しました",
"bulkDownloadSuccess": "ファイルのダウンロードが正常に開始されました",
"bulkDownloadError": "ZIPファイルの作成エラー",
"bulkDownloadFileError": "ファイル {fileName} のダウンロードエラー",
"bulkDeleteSuccess": "{count, plural, =1 {1つのファイルが正常に削除されました} other {#つのファイルが正常に削除されました}}",
"bulkDeleteError": "選択されたファイルの削除エラー"
},
"filesTable": {
"ariaLabel": "ファイルテーブル",
"selectAll": "すべて選択",
"selectFile": "ファイル {fileName} を選択",
"columns": {
"name": "名前",
"description": "説明",
"size": "サイズ",
"createdAt": "作成日",
"updatedAt": "更新日",
"actions": "操作"
"createdAt": "作成日",
"updatedAt": "更新日",
"actions": "アクション"
},
"actions": {
"menu": "ファイルアクションメニュー",
@@ -111,6 +120,13 @@
"share": "共有",
"download": "ダウンロード",
"delete": "削除"
},
"bulkActions": {
"selected": "{count, plural, =1 {1つのファイルが選択されています} other {#つのファイルが選択されています}}",
"actions": "アクション",
"download": "選択済みをダウンロード",
"share": "選択済みを共有",
"delete": "選択済みを削除"
}
},
"footer": {
@@ -457,43 +473,54 @@
},
"shareActions": {
"deleteTitle": "共有を削除",
"deleteConfirmation": "この共有を削除してもよろしいですか?この操作は元に戻ません。",
"deleteConfirmation": "この共有を削除しすか?この操作は元に戻すことができません。",
"editTitle": "共有を編集",
"nameLabel": "共有名",
"descriptionLabel": "説明",
"descriptionPlaceholder": "説明を入力してください(任意)",
"expirationLabel": "有効期限",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"maxViewsLabel": "最大ビュー数",
"maxViewsPlaceholder": "無制限の場合は空白のままにする",
"expirationPlaceholder": "YYYY/MM/DD HH:MM",
"maxViewsLabel": "最大表示回数",
"maxViewsPlaceholder": "無制限の場合は空白にしてください",
"passwordProtection": "パスワード保護",
"passwordLabel": "パスワード",
"passwordPlaceholder": "パスワードを入力してください",
"newPasswordLabel": "新しいパスワード(現在のパスワードを保持する場合は空白のまま",
"newPasswordLabel": "新しいパスワード(現在のパスワードを保持する場合は空白)",
"newPasswordPlaceholder": "新しいパスワードを入力してください",
"manageFilesTitle": "ファイル管理",
"manageRecipientsTitle": "受信者管理",
"manageFilesTitle": "ファイル管理",
"manageRecipientsTitle": "受信者管理",
"editSuccess": "共有が正常に更新されました",
"editError": "共有の更新に失敗しました"
},
"shareDetails": {
"title": "共有詳細",
"subtitle": "この共有詳細情報",
"title": "共有詳細",
"subtitle": "この共有に関する詳細情報",
"basicInfo": "基本情報",
"name": "名前",
"description": "説明",
"noDescription": "説明が提供されていません",
"untitled": "無題",
"views": "ビュー数",
"shareLink": "共有リンク",
"editLink": "リンクを編集",
"generateLink": "リンクを生成",
"noLink": "まだリンクが生成されていません",
"copyLink": "リンクをコピー",
"openLink": "新しいタブで開く",
"linkCopied": "リンクがクリップボードにコピーされました",
"views": "表示回数",
"dates": "日付",
"created": "作成日",
"expires": "有効期限",
"never": "なし",
"security": "セキュリティ",
"passwordProtected": "パスワード保護",
"publicAccess": "公開アクセス",
"maxViews": "最大ビュー数:",
"publicAccess": "パブリックアクセス",
"maxViews": "最大表示回数:",
"files": "ファイル",
"recipients": "受信者",
"notAvailable": "該当なし",
"invalidDate": "無効な日付です",
"loadError": "共有詳細の読み込みに失敗しました"
"notAvailable": "N/A",
"invalidDate": "無効な日付",
"loadError": "共有詳細の読み込みに失敗しました"
},
"shareManager": {
"deleteSuccess": "共有が正常に削除されました",
@@ -541,16 +568,17 @@
"never": "なし",
"columns": {
"name": "名前",
"description": "説明",
"createdAt": "作成日",
"expiresAt": "有効期限",
"status": "ステータス",
"security": "セキュリティ",
"files": "ファイル",
"recipients": "受信者",
"actions": "操作"
"actions": "アクション"
},
"status": {
"neverExpires": "期限",
"neverExpires": "期限なし",
"active": "アクティブ",
"expired": "期限切れ"
},
@@ -561,14 +589,14 @@
"filesCount": "ファイル",
"recipientsCount": "受信者",
"actions": {
"menu": "共有操作メニュー",
"menu": "共有アクションメニュー",
"edit": "編集",
"manageFiles": "ファイル管理",
"manageRecipients": "受信者管理",
"viewDetails": "詳細表示",
"generateLink": "リンク生成",
"editLink": "リンク編集",
"copyLink": "リンクコピー",
"viewDetails": "詳細表示",
"generateLink": "リンク生成",
"editLink": "リンク編集",
"copyLink": "リンクコピー",
"notifyRecipients": "受信者に通知",
"delete": "削除"
}
@@ -679,23 +707,47 @@
"passwordRequired": "パスワードは必須です"
},
"shareFile": {
"title": "ファイル共有",
"linkTitle": "リンク生成",
"title": "ファイル共有",
"linkTitle": "リンク生成",
"nameLabel": "共有名",
"namePlaceholder": "共有名を入力",
"namePlaceholder": "共有名を入力してください",
"descriptionLabel": "説明",
"descriptionPlaceholder": "説明を入力してください(任意)",
"expirationLabel": "有効期限",
"expirationPlaceholder": "YYYY/MM/DD HH:MM",
"maxViewsLabel": "最大閲覧回数",
"maxViewsPlaceholder": "無制限の場合は空",
"maxViewsLabel": "最大表示回数",
"maxViewsPlaceholder": "無制限の場合は空白にしてください",
"passwordProtection": "パスワード保護",
"passwordLabel": "パスワード",
"passwordPlaceholder": "パスワードを入力",
"linkDescription": "ファイルを共有するためのカスタムリンクを生成します",
"passwordPlaceholder": "パスワードを入力してください",
"linkDescription": "ファイルを共有するためのカスタムリンクを生成す",
"aliasLabel": "リンクエイリアス",
"aliasPlaceholder": "カスタムエイリアスを入力",
"aliasPlaceholder": "カスタムエイリアスを入力してください",
"linkReady": "共有リンクの準備ができました:",
"createShare": "共有を作成",
"generateLink": "リンク生成",
"copyLink": "リンクコピー"
"generateLink": "リンク生成",
"copyLink": "リンクコピー"
},
"bulkDownload": {
"title": "一括ダウンロード",
"zipNameLabel": "ZIPファイル名",
"zipNamePlaceholder": "ファイル名を入力してください",
"description": "{count, plural, =1 {1つのファイルが圧縮されます} other {#つのファイルが圧縮されます}}",
"download": "ZIPをダウンロード"
},
"deleteConfirmation": {
"filesToDelete": "削除するファイル"
},
"shareMultipleFiles": {
"title": "複数ファイルを共有",
"shareNameLabel": "共有名",
"shareNamePlaceholder": "共有名を入力してください",
"descriptionLabel": "説明",
"descriptionPlaceholder": "説明を入力してください(任意)",
"filesToShare": "共有するファイル",
"files": "ファイル",
"totalSize": "合計サイズ",
"creating": "作成中...",
"create": "共有を作成"
}
}

View File

@@ -15,10 +15,12 @@
"createShare": {
"title": "공유 생성",
"nameLabel": "공유 이름",
"descriptionLabel": "설명",
"descriptionPlaceholder": "설명을 입력하세요 (선택사항)",
"expirationLabel": "만료 날짜",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"maxViewsLabel": "최대 조회수",
"maxViewsPlaceholder": "무제한인 경우 비워두세요",
"maxViewsPlaceholder": "무제한 비워두세요",
"passwordProtection": "비밀번호 보호",
"passwordLabel": "비밀번호",
"create": "공유 생성",
@@ -92,10 +94,17 @@
"updateSuccess": "파일이 성공적으로 업데이트되었습니다",
"updateError": "파일 업데이트에 실패했습니다",
"deleteSuccess": "파일이 성공적으로 삭제되었습니다",
"deleteError": "파일 삭제에 실패했습니다"
"deleteError": "파일 삭제에 실패했습니다",
"bulkDownloadSuccess": "파일 다운로드가 성공적으로 시작되었습니다",
"bulkDownloadError": "ZIP 파일 생성 오류",
"bulkDownloadFileError": "파일 {fileName} 다운로드 오류",
"bulkDeleteSuccess": "{count, plural, =1 {1개 파일이 성공적으로 삭제되었습니다} other {#개 파일이 성공적으로 삭제되었습니다}}",
"bulkDeleteError": "선택된 파일 삭제 오류"
},
"filesTable": {
"ariaLabel": "파일 테이블",
"selectAll": "모두 선택",
"selectFile": "파일 {fileName} 선택",
"columns": {
"name": "이름",
"description": "설명",
@@ -111,6 +120,13 @@
"share": "공유",
"download": "다운로드",
"delete": "삭제"
},
"bulkActions": {
"selected": "{count, plural, =1 {1개 파일이 선택됨} other {#개 파일이 선택됨}}",
"actions": "작업",
"download": "선택된 항목 다운로드",
"share": "선택된 항목 공유",
"delete": "선택된 항목 삭제"
}
},
"footer": {
@@ -460,40 +476,51 @@
"deleteConfirmation": "이 공유를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
"editTitle": "공유 편집",
"nameLabel": "공유 이름",
"descriptionLabel": "설명",
"descriptionPlaceholder": "설명을 입력하세요 (선택사항)",
"expirationLabel": "만료 날짜",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"expirationPlaceholder": "YYYY/MM/DD HH:MM",
"maxViewsLabel": "최대 조회수",
"maxViewsPlaceholder": "무제한인 경우 비워두세요",
"maxViewsPlaceholder": "무제한은 공백으로 두세요",
"passwordProtection": "비밀번호 보호",
"passwordLabel": "비밀번호",
"passwordPlaceholder": "비밀번호를 입력하세요",
"newPasswordLabel": "새 비밀번호 (현재 비밀번호 유지 시 비워두세요)",
"newPasswordLabel": "새 비밀번호 (현재 비밀번호 유지하려면 공백으로 두세요)",
"newPasswordPlaceholder": "새 비밀번호를 입력하세요",
"manageFilesTitle": "파일 관리",
"manageRecipientsTitle": "수신자 관리",
"manageRecipientsTitle": "받는 사람 관리",
"editSuccess": "공유가 성공적으로 업데이트되었습니다",
"editError": "공유 업데이트에 실패했습니다"
},
"shareDetails": {
"title": "공유 세부 정보",
"subtitle": "이 공유에 대한 자세한 정보",
"subtitle": "이 공유에 대한 상세 정보",
"basicInfo": "기본 정보",
"name": "이름",
"description": "설명",
"noDescription": "설명이 제공되지 않았습니다",
"untitled": "제목 없음",
"shareLink": "공유 링크",
"editLink": "링크 편집",
"generateLink": "링크 생성",
"noLink": "아직 링크가 생성되지 않았습니다",
"copyLink": "링크 복사",
"openLink": "새 탭에서 열기",
"linkCopied": "링크가 클립보드에 복사되었습니다",
"views": "조회수",
"dates": "날짜",
"created": "생성됨",
"expires": "만료",
"expires": "만료",
"never": "없음",
"security": "보안",
"passwordProtected": "비밀번호 보호",
"publicAccess": "공개 접근",
"passwordProtected": "비밀번호 보호",
"publicAccess": "공개 액세스",
"maxViews": "최대 조회수:",
"files": "파일",
"recipients": "수신자",
"notAvailable": "해당 없음",
"invalidDate": "유효하지 않은 날짜입니다",
"loadError": "공유 세부 정보를 불러오는데 실패했습니다"
"recipients": "받는 사람",
"notAvailable": "N/A",
"invalidDate": "잘못된 날짜",
"loadError": "공유 세부 정보 로드에 실패했습니다"
},
"shareManager": {
"deleteSuccess": "공유가 성공적으로 삭제되었습니다",
@@ -541,12 +568,13 @@
"never": "없음",
"columns": {
"name": "이름",
"description": "설명",
"createdAt": "생성일",
"expiresAt": "만료일",
"status": "상태",
"security": "보안",
"files": "파일",
"recipients": "수신자",
"recipients": "받는 사람",
"actions": "작업"
},
"status": {
@@ -558,18 +586,18 @@
"protected": "보호됨",
"public": "공개"
},
"filesCount": "개의 파일",
"recipientsCount": "명의 수신자",
"filesCount": "파일",
"recipientsCount": "받는 사람",
"actions": {
"menu": "공유 작업 메뉴",
"edit": "편집",
"manageFiles": "파일 관리",
"manageRecipients": "수신자 관리",
"manageRecipients": "받는 사람 관리",
"viewDetails": "세부 정보 보기",
"generateLink": "링크 생성",
"editLink": "링크 편집",
"copyLink": "링크 복사",
"notifyRecipients": "수신자 알림",
"notifyRecipients": "받는 사람에게 알림",
"delete": "삭제"
}
},
@@ -697,20 +725,44 @@
"title": "파일 공유",
"linkTitle": "링크 생성",
"nameLabel": "공유 이름",
"namePlaceholder": "공유 이름 입력",
"namePlaceholder": "공유 이름 입력하세요",
"descriptionLabel": "설명",
"descriptionPlaceholder": "설명을 입력하세요 (선택사항)",
"expirationLabel": "만료 날짜",
"expirationPlaceholder": "YYYY.MM.DD HH:MM",
"expirationPlaceholder": "YYYY/MM/DD HH:MM",
"maxViewsLabel": "최대 조회수",
"maxViewsPlaceholder": "무제한은 비워두세요",
"maxViewsPlaceholder": "무제한은 공백으로 두세요",
"passwordProtection": "비밀번호 보호",
"passwordLabel": "비밀번호",
"passwordPlaceholder": "비밀번호 입력",
"linkDescription": "파일을 공유할 맞춤 링크를 생성합니다",
"passwordPlaceholder": "비밀번호 입력하세요",
"linkDescription": "파일을 공유할 맞춤 링크를 생성하세요",
"aliasLabel": "링크 별칭",
"aliasPlaceholder": "맞춤 별칭 입력",
"aliasPlaceholder": "맞춤 별칭 입력하세요",
"linkReady": "공유 링크가 준비되었습니다:",
"createShare": "공유 생성",
"generateLink": "링크 생성",
"copyLink": "링크 복사"
},
"bulkDownload": {
"title": "일괄 다운로드",
"zipNameLabel": "ZIP 파일명",
"zipNamePlaceholder": "파일명을 입력하세요",
"description": "{count, plural, =1 {1개 파일이 압축됩니다} other {#개 파일이 압축됩니다}}",
"download": "ZIP 다운로드"
},
"deleteConfirmation": {
"filesToDelete": "삭제할 파일"
},
"shareMultipleFiles": {
"title": "여러 파일 공유",
"shareNameLabel": "공유 이름",
"shareNamePlaceholder": "공유 이름을 입력하세요",
"descriptionLabel": "설명",
"descriptionPlaceholder": "설명을 입력하세요 (선택사항)",
"filesToShare": "공유할 파일",
"files": "파일",
"totalSize": "전체 크기",
"creating": "생성 중...",
"create": "공유 생성"
}
}

View File

@@ -14,16 +14,18 @@
},
"createShare": {
"title": "Delen Maken",
"nameLabel": "Naam van Delen",
"nameLabel": "Delen Naam",
"descriptionLabel": "Beschrijving",
"descriptionPlaceholder": "Voer een beschrijving in (optioneel)",
"expirationLabel": "Vervaldatum",
"expirationPlaceholder": "DD/MM/JJJJ UU:MM",
"maxViewsLabel": "Maximum Weergaven",
"maxViewsLabel": "Maximale Weergaves",
"maxViewsPlaceholder": "Laat leeg voor onbeperkt",
"passwordProtection": "Wachtwoord Beveiligd",
"passwordLabel": "Wachtwoord",
"create": "Delen Maken",
"success": "Delen succesvol aangemaakt",
"error": "Fout bij het maken van delen"
"error": "Fout bij het aanmaken van delen"
},
"dashboard": {
"loadError": "Fout bij het laden van controlepaneel gegevens",
@@ -92,10 +94,17 @@
"updateSuccess": "Bestand succesvol bijgewerkt",
"updateError": "Fout bij het bijwerken van bestand",
"deleteSuccess": "Bestand succesvol verwijderd",
"deleteError": "Fout bij het verwijderen van bestand"
"deleteError": "Fout bij het verwijderen van bestand",
"bulkDownloadSuccess": "Bestanden download succesvol gestart",
"bulkDownloadError": "Fout bij het maken van ZIP-bestand",
"bulkDownloadFileError": "Fout bij het downloaden van bestand {fileName}",
"bulkDeleteSuccess": "{count, plural, =1 {1 bestand succesvol verwijderd} other {# bestanden succesvol verwijderd}}",
"bulkDeleteError": "Fout bij het verwijderen van geselecteerde bestanden"
},
"filesTable": {
"ariaLabel": "Bestanden tabel",
"selectAll": "Alles selecteren",
"selectFile": "Bestand {fileName} selecteren",
"columns": {
"name": "NAAM",
"description": "BESCHRIJVING",
@@ -111,6 +120,13 @@
"share": "Delen",
"download": "Downloaden",
"delete": "Verwijderen"
},
"bulkActions": {
"selected": "{count, plural, =1 {1 bestand geselecteerd} other {# bestanden geselecteerd}}",
"actions": "Acties",
"download": "Geselecteerde Downloaden",
"share": "Geselecteerde Delen",
"delete": "Geselecteerde Verwijderen"
}
},
"footer": {
@@ -459,41 +475,52 @@
"deleteTitle": "Delen Verwijderen",
"deleteConfirmation": "Weet je zeker dat je dit delen wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
"editTitle": "Delen Bewerken",
"nameLabel": "Deel Naam",
"nameLabel": "Delen Naam",
"descriptionLabel": "Beschrijving",
"descriptionPlaceholder": "Voer een beschrijving in (optioneel)",
"expirationLabel": "Vervaldatum",
"expirationPlaceholder": "DD/MM/JJJJ UU:MM",
"maxViewsLabel": "Maximum Weergaven",
"maxViewsLabel": "Maximale Weergaven",
"maxViewsPlaceholder": "Laat leeg voor onbeperkt",
"passwordProtection": "Wachtwoord Beveiligd",
"passwordLabel": "Wachtwoord",
"passwordPlaceholder": "Voer wachtwoord in",
"newPasswordLabel": "Nieuw Wachtwoord (laat leeg om huidige te behouden)",
"newPasswordLabel": "Nieuw Wachtwoord (laat leeg om huidig te behouden)",
"newPasswordPlaceholder": "Voer nieuw wachtwoord in",
"manageFilesTitle": "Bestanden Beheren",
"manageRecipientsTitle": "Ontvangers Beheren",
"editSuccess": "Delen succesvol bijgewerkt",
"editError": "Fout bij het bijwerken van delen"
"editError": "Fout bij bijwerken van delen"
},
"shareDetails": {
"title": "Deel Details",
"title": "Delen Details",
"subtitle": "Gedetailleerde informatie over dit delen",
"basicInfo": "Basis Informatie",
"basicInfo": "Basisinformatie",
"name": "Naam",
"description": "Beschrijving",
"noDescription": "Geen beschrijving opgegeven",
"untitled": "Naamloos",
"shareLink": "Delen Link",
"editLink": "Link Bewerken",
"generateLink": "Link Genereren",
"noLink": "Nog geen link gegenereerd",
"copyLink": "Link kopiëren",
"openLink": "Openen in nieuw tabblad",
"linkCopied": "Link gekopieerd naar klembord",
"views": "Weergaven",
"dates": "Datums",
"dates": "Data",
"created": "Aangemaakt",
"expires": "Verloopt",
"never": "Nooit",
"security": "Beveiliging",
"passwordProtected": "Wachtwoord Beveiligd",
"publicAccess": "Publieke Toegang",
"maxViews": "Maximum Weergaven:",
"publicAccess": "Openbare Toegang",
"maxViews": "Max. Weergaven:",
"files": "Bestanden",
"recipients": "Ontvangers",
"notAvailable": "N.v.t.",
"notAvailable": "N/B",
"invalidDate": "Ongeldige datum",
"loadError": "Fout bij het laden van deel details"
"loadError": "Fout bij laden van delen details"
},
"shareManager": {
"deleteSuccess": "Delen succesvol verwijderd",
@@ -537,10 +564,11 @@
"pageTitle": "Delingen"
},
"sharesTable": {
"ariaLabel": "Delingen tabel",
"ariaLabel": "Delen tabel",
"never": "Nooit",
"columns": {
"name": "NAAM",
"description": "BESCHRIJVING",
"createdAt": "AANGEMAAKT OP",
"expiresAt": "VERLOOPT OP",
"status": "STATUS",
@@ -556,12 +584,12 @@
},
"security": {
"protected": "Beveiligd",
"public": "Publiek"
"public": "Openbaar"
},
"filesCount": "bestanden",
"recipientsCount": "ontvangers",
"actions": {
"menu": "Deel acties menu",
"menu": "Delen acties menu",
"edit": "Bewerken",
"manageFiles": "Bestanden Beheren",
"manageRecipients": "Ontvangers Beheren",
@@ -569,7 +597,7 @@
"generateLink": "Link Genereren",
"editLink": "Link Bewerken",
"copyLink": "Link Kopiëren",
"notifyRecipients": "Ontvangers Waarschuwen",
"notifyRecipients": "Ontvangers Informeren",
"delete": "Verwijderen"
}
},
@@ -693,23 +721,47 @@
"passwordRequired": "Wachtwoord is verplicht"
},
"shareFile": {
"title": "Bestand delen",
"linkTitle": "Link genereren",
"nameLabel": "Deel naam",
"namePlaceholder": "Voer deel naam in",
"title": "Bestand Delen",
"linkTitle": "Link Genereren",
"nameLabel": "Delen Naam",
"namePlaceholder": "Voer delen naam in",
"descriptionLabel": "Beschrijving",
"descriptionPlaceholder": "Voer een beschrijving in (optioneel)",
"expirationLabel": "Vervaldatum",
"expirationPlaceholder": "DD/MM/JJJJ UU:MM",
"maxViewsLabel": "Maximum weergaven",
"maxViewsLabel": "Maximale Weergaven",
"maxViewsPlaceholder": "Laat leeg voor onbeperkt",
"passwordProtection": "Wachtwoord beschermd",
"passwordProtection": "Wachtwoord Beveiligd",
"passwordLabel": "Wachtwoord",
"passwordPlaceholder": "Voer wachtwoord in",
"linkDescription": "Genereer een aangepaste link om het bestand te delen",
"aliasLabel": "Link alias",
"aliasLabel": "Link Alias",
"aliasPlaceholder": "Voer aangepaste alias in",
"linkReady": "Uw deel-link is klaar:",
"createShare": "Deel maken",
"generateLink": "Link genereren",
"copyLink": "Link kopiëren"
"linkReady": "Jouw delen link is klaar:",
"createShare": "Delen Aanmaken",
"generateLink": "Link Genereren",
"copyLink": "Link Kopiëren"
},
"bulkDownload": {
"title": "Bulk Download",
"zipNameLabel": "ZIP bestandsnaam",
"zipNamePlaceholder": "Voer bestandsnaam in",
"description": "{count, plural, =1 {1 bestand wordt gecomprimeerd} other {# bestanden worden gecomprimeerd}}",
"download": "ZIP Downloaden"
},
"deleteConfirmation": {
"filesToDelete": "Te verwijderen bestanden"
},
"shareMultipleFiles": {
"title": "Meerdere Bestanden Delen",
"shareNameLabel": "Delen Naam",
"shareNamePlaceholder": "Voer delen naam in",
"descriptionLabel": "Beschrijving",
"descriptionPlaceholder": "Voer een beschrijving in (optioneel)",
"filesToShare": "Bestanden om te delen",
"files": "bestanden",
"totalSize": "Totale grootte",
"creating": "Aanmaken...",
"create": "Delen Maken"
}
}

View File

@@ -15,6 +15,8 @@
"createShare": {
"title": "Criar Compartilhamento",
"nameLabel": "Nome do Compartilhamento",
"descriptionLabel": "Descrição",
"descriptionPlaceholder": "Digite uma descrição (opcional)",
"expirationLabel": "Data de Expiração",
"expirationPlaceholder": "DD/MM/AAAA HH:MM",
"maxViewsLabel": "Máximo de Visualizações",
@@ -92,10 +94,17 @@
"updateSuccess": "Arquivo atualizado com sucesso",
"updateError": "Erro ao atualizar o arquivo",
"deleteSuccess": "Arquivo excluído com sucesso",
"deleteError": "Erro ao excluir o arquivo"
"deleteError": "Erro ao excluir o arquivo",
"bulkDownloadSuccess": "Download dos arquivos iniciado com sucesso",
"bulkDownloadError": "Erro ao criar arquivo ZIP",
"bulkDownloadFileError": "Erro ao baixar arquivo {fileName}",
"bulkDeleteSuccess": "{count, plural, =1 {1 arquivo excluído com sucesso} other {# arquivos excluídos com sucesso}}",
"bulkDeleteError": "Erro ao excluir arquivos selecionados"
},
"filesTable": {
"ariaLabel": "Tabela de arquivos",
"selectAll": "Selecionar todos",
"selectFile": "Selecionar arquivo {fileName}",
"columns": {
"name": "NOME",
"description": "DESCRIÇÃO",
@@ -111,6 +120,13 @@
"share": "Compartilhar",
"download": "Baixar",
"delete": "Excluir"
},
"bulkActions": {
"selected": "{count, plural, =1 {1 arquivo selecionado} other {# arquivos selecionados}}",
"actions": "Ações",
"download": "Baixar Selecionados",
"share": "Compartilhar Selecionados",
"delete": "Excluir Selecionados"
}
},
"footer": {
@@ -147,6 +163,8 @@
"linkTitle": "Gerar Link",
"nameLabel": "Nome do Compartilhamento",
"namePlaceholder": "Digite o nome do compartilhamento",
"descriptionLabel": "Descrição",
"descriptionPlaceholder": "Digite uma descrição (opcional)",
"expirationLabel": "Data de Expiração",
"expirationPlaceholder": "DD/MM/AAAA HH:MM",
"maxViewsLabel": "Máximo de Visualizações",
@@ -480,6 +498,8 @@
"deleteConfirmation": "Tem certeza que deseja excluir este compartilhamento? Esta ação não pode ser desfeita.",
"editTitle": "Editar Compartilhamento",
"nameLabel": "Nome do Compartilhamento",
"descriptionLabel": "Descrição",
"descriptionPlaceholder": "Digite uma descrição (opcional)",
"expirationLabel": "Data de Expiração",
"expirationPlaceholder": "DD/MM/AAAA HH:MM",
"maxViewsLabel": "Máximo de Visualizações",
@@ -499,16 +519,25 @@
"subtitle": "Informações detalhadas sobre este compartilhamento",
"basicInfo": "Informações Básicas",
"name": "Nome",
"untitled": "Compartilhamento sem título",
"description": "Descrição",
"noDescription": "Nenhuma descrição fornecida",
"untitled": "Sem título",
"shareLink": "Link de Compartilhamento",
"editLink": "Editar Link",
"generateLink": "Gerar Link",
"noLink": "Nenhum link gerado ainda",
"copyLink": "Copiar link",
"openLink": "Abrir em nova guia",
"linkCopied": "Link copiado para a área de transferência",
"views": "Visualizações",
"dates": "Datas",
"created": "Criado em: {date}",
"expires": "Expira em: {date}",
"created": "Criado",
"expires": "Expira",
"never": "Nunca",
"security": "Segurança",
"passwordProtected": "Protegido por Senha",
"publicAccess": "Acesso Público",
"maxViews": "Máximo de Visualizações:",
"maxViews": "Máx. Visualizações:",
"files": "Arquivos",
"recipients": "Destinatários",
"notAvailable": "N/A",
@@ -561,6 +590,7 @@
"never": "Nunca",
"columns": {
"name": "NOME",
"description": "DESCRIÇÃO",
"createdAt": "CRIADO EM",
"expiresAt": "EXPIRA EM",
"status": "STATUS",
@@ -712,5 +742,27 @@
"passwordsMatch": "As senhas não coincidem",
"emailRequired": "Email é obrigatório",
"passwordRequired": "Senha é obrigatória"
},
"bulkDownload": {
"title": "Download em Lote",
"zipNameLabel": "Nome do arquivo ZIP",
"zipNamePlaceholder": "Digite o nome do arquivo",
"description": "{count, plural, =1 {1 arquivo será compactado} other {# arquivos serão compactados}}",
"download": "Baixar ZIP"
},
"deleteConfirmation": {
"filesToDelete": "Arquivos que serão excluídos"
},
"shareMultipleFiles": {
"title": "Compartilhar Múltiplos Arquivos",
"shareNameLabel": "Nome do Compartilhamento",
"shareNamePlaceholder": "Digite o nome do compartilhamento",
"descriptionLabel": "Descrição",
"descriptionPlaceholder": "Digite uma descrição (opcional)",
"filesToShare": "Arquivos para compartilhar",
"files": "arquivos",
"totalSize": "Tamanho total",
"creating": "Criando...",
"create": "Criar Compartilhamento"
}
}

View File

@@ -456,39 +456,50 @@
"pageTitle": "Общий доступ"
},
"shareActions": {
"deleteTitle": "Удалить общий доступ",
"deleteConfirmation": "Вы уверены, что хотите удалить этот общий доступ? Это действие необратимо.",
"editTitle": "Редактировать общий доступ",
"nameLabel": "Название общего доступа",
"expirationLabel": "Дата истечения",
"expirationPlaceholder": "MM/DD/YYYY ЧЧ:ММ",
"maxViewsLabel": "Максимальное количество просмотров",
"maxViewsPlaceholder": "Оставьте пустым для неограниченного доступа",
"passwordProtection": "Защищено паролем",
"deleteTitle": "Удалить Общий Доступ",
"deleteConfirmation": "Вы уверены, что хотите удалить этот общий доступ? Это действие нельзя отменить.",
"editTitle": "Редактировать Общий Доступ",
"nameLabel": "Название Общего Доступа",
"descriptionLabel": "Описание",
"descriptionPlaceholder": "Введите описание (опционально)",
"expirationLabel": "Дата Истечения",
"expirationPlaceholder": "ДД.ММ.ГГГГ ЧЧ:ММ",
"maxViewsLabel": "Максимальные Просмотры",
"maxViewsPlaceholder": "Оставьте пустым для неограниченного",
"passwordProtection": "Защищено Паролем",
"passwordLabel": "Пароль",
"passwordPlaceholder": "Введите пароль",
"newPasswordLabel": "Новый пароль (оставьте пустым, чтобы сохранить текущий)",
"newPasswordLabel": "Новый Пароль (оставьте пустым, чтобы сохранить текущий)",
"newPasswordPlaceholder": "Введите новый пароль",
"manageFilesTitle": "Управление файлами",
"manageRecipientsTitle": "Управление получателями",
"editSuccess": "Общий доступ успешно обновлён",
"editError": "Ошибка при обновлении общего доступа"
"manageFilesTitle": "Управление Файлами",
"manageRecipientsTitle": "Управление Получателями",
"editSuccess": "Общий доступ успешно обновлен",
"editError": "Ошибка обновления общего доступа"
},
"shareDetails": {
"title": "Детали общего доступа",
"title": "Детали Общего Доступа",
"subtitle": "Подробная информация об этом общем доступе",
"basicInfo": "Основная информация",
"basicInfo": "Основная Информация",
"name": "Название",
"description": "Описание",
"noDescription": "Описание не предоставлено",
"untitled": "Без названия",
"shareLink": "Ссылка Общего Доступа",
"editLink": "Редактировать Ссылку",
"generateLink": "Сгенерировать Ссылку",
"noLink": "Ссылка еще не сгенерирована",
"copyLink": "Скопировать ссылку",
"openLink": "Открыть в новой вкладке",
"linkCopied": "Ссылка скопирована в буфер обмена",
"views": "Просмотры",
"dates": "Даты",
"created": "Создано",
"expires": "Истекает",
"never": "Никогда",
"security": "Безопасность",
"passwordProtected": "Защищено паролем",
"publicAccess": "Публичный доступ",
"maxViews": "Максимум просмотров:",
"passwordProtected": "Защищено Паролем",
"publicAccess": "Публичный Доступ",
"maxViews": "Макс. Просмотры:",
"files": "Файлы",
"recipients": "Получатели",
"notAvailable": "Н/Д",
@@ -537,10 +548,11 @@
"pageTitle": "Общие доступы"
},
"sharesTable": {
"ariaLabel": "Таблица общего доступа",
"ariaLabel": "Таблица общих доступов",
"never": "Никогда",
"columns": {
"name": "ИМЯ",
"name": "НАЗВАНИЕ",
"description": "ОПИСАНИЕ",
"createdAt": "СОЗДАНО",
"expiresAt": "ИСТЕКАЕТ",
"status": "СТАТУС",
@@ -550,26 +562,26 @@
"actions": "ДЕЙСТВИЯ"
},
"status": {
"neverExpires": "Не истекает",
"active": "Активно",
"expired": "Истекло"
"neverExpires": "Никогда Не Истекает",
"active": "Активный",
"expired": "Истекший"
},
"security": {
"protected": "Защищено",
"public": "Публично"
"protected": "Защищен",
"public": "Публичный"
},
"filesCount": "файлов",
"recipientsCount": "получателей",
"actions": {
"menu": "Меню действий общего доступа",
"edit": "Редактировать",
"manageFiles": "Управление файлами",
"manageRecipients": "Управление получателями",
"viewDetails": "Просмотреть детали",
"generateLink": "Создать ссылку",
"editLink": "Редактировать ссылку",
"copyLink": "Скопировать ссылку",
"notifyRecipients": "Уведомить получателей",
"manageFiles": "Управление Файлами",
"manageRecipients": "Управление Получателями",
"viewDetails": "Просмотр Деталей",
"generateLink": "Сгенерировать Ссылку",
"editLink": "Редактировать Ссылку",
"copyLink": "Скопировать Ссылку",
"notifyRecipients": "Уведомить Получателей",
"delete": "Удалить"
}
},
@@ -679,23 +691,25 @@
"passwordRequired": "Требуется пароль"
},
"shareFile": {
"title": "Поделиться файлом",
"linkTitle": "Создать ссылку",
"nameLabel": "Название обмена",
"namePlaceholder": "Введите название обмена",
"expirationLabel": "Дата истечения",
"title": "Поделиться Файлом",
"linkTitle": "Сгенерировать Ссылку",
"nameLabel": "Название Общего Доступа",
"namePlaceholder": "Введите название общего доступа",
"descriptionLabel": "Описание",
"descriptionPlaceholder": "Введите описание (опционально)",
"expirationLabel": "Дата Истечения",
"expirationPlaceholder": "ДД.ММ.ГГГГ ЧЧ:ММ",
"maxViewsLabel": "Максимум просмотров",
"maxViewsLabel": "Максимальные Просмотры",
"maxViewsPlaceholder": "Оставьте пустым для неограниченного",
"passwordProtection": "Защищено паролем",
"passwordProtection": "Защищено Паролем",
"passwordLabel": "Пароль",
"passwordPlaceholder": "Введите пароль",
"linkDescription": "Создайте персональную ссылку для обмена файлом",
"aliasLabel": "Псевдоним ссылки",
"aliasPlaceholder": "Введите персональный псевдоним",
"linkReady": "Ваша ссылка для обмена готова:",
"createShare": "Создать обмен",
"generateLink": "Создать ссылку",
"copyLink": "Копировать ссылку"
"linkDescription": "Сгенерируйте пользовательскую ссылку для отправки файла",
"aliasLabel": "Псевдоним Ссылки",
"aliasPlaceholder": "Введите пользовательский псевдоним",
"linkReady": "Ваша ссылка для общего доступа готова:",
"createShare": "Создать Общий Доступ",
"generateLink": "Сгенерировать Ссылку",
"copyLink": "Скопировать Ссылку"
}
}

View File

@@ -457,43 +457,54 @@
},
"shareActions": {
"deleteTitle": "Paylaşımı Sil",
"deleteConfirmation": "Bu paylaşımı silmek istediğinize emin misiniz? Bu işlem geri alınamaz.",
"deleteConfirmation": "Bu paylaşımı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
"editTitle": "Paylaşımı Düzenle",
"nameLabel": "Paylaşım Adı",
"descriptionLabel": "Açıklama",
"descriptionPlaceholder": "Açıklama girin (isteğe bağlı)",
"expirationLabel": "Son Kullanma Tarihi",
"expirationPlaceholder": "MM/GG/YYYY SS:DD",
"expirationPlaceholder": "DD/MM/YYYY HH:MM",
"maxViewsLabel": "Maksimum Görüntüleme",
"maxViewsPlaceholder": "Sınırsız için boş bırakın",
"passwordProtection": "Şifre Korumalı",
"passwordLabel": "Şifre",
"passwordPlaceholder": "Şifreyi girin",
"passwordPlaceholder": "Şifre girin",
"newPasswordLabel": "Yeni Şifre (mevcut şifreyi korumak için boş bırakın)",
"newPasswordPlaceholder": "Yeni şifreyi girin",
"newPasswordPlaceholder": "Yeni şifre girin",
"manageFilesTitle": "Dosyaları Yönet",
"manageRecipientsTitle": "Alıcıları Yönet",
"editSuccess": "Paylaşım başarıyla güncellendi",
"editError": "Paylaşım güncellenemedi"
"editError": "Paylaşım güncelleme başarısız"
},
"shareDetails": {
"title": "Paylaşım Detayları",
"subtitle": "Bu paylaşım hakkında ayrıntılı bilgi",
"subtitle": "Bu paylaşım hakkında detaylı bilgi",
"basicInfo": "Temel Bilgiler",
"name": "Ad",
"untitled": "Adsız",
"description": "Açıklama",
"noDescription": "Açıklama sağlanmadı",
"untitled": "Başlıksız",
"shareLink": "Paylaşım Bağlantısı",
"editLink": "Bağlantıyı Düzenle",
"generateLink": "Bağlantı Oluştur",
"noLink": "Henüz bağlantı oluşturulmadı",
"copyLink": "Bağlantıyı kopyala",
"openLink": "Yeni sekmede aç",
"linkCopied": "Bağlantı panoya kopyalandı",
"views": "Görüntüleme",
"dates": "Tarihler",
"created": "Oluşturuldu",
"expires": "Sona Eriyor",
"never": "Asla",
"security": "Güvenlik",
"passwordProtected": "Şifre ile Korunuyor",
"passwordProtected": "Şifre Korumalı",
"publicAccess": "Genel Erişim",
"maxViews": "Maksimum Görüntüleme:",
"files": "Dosyala",
"recipients": "Alıcıla",
"notAvailable": "Mevcut Değil",
"maxViews": "Maks. Görüntüleme:",
"files": "Dosyalar",
"recipients": "Alıcılar",
"notAvailable": "M/D",
"invalidDate": "Geçersiz tarih",
"loadError": "Paylaşım detayları yüklenemedi"
"loadError": "Paylaşım detaylarını yükleme başarısız"
},
"shareManager": {
"deleteSuccess": "Paylaşım başarıyla silindi",
@@ -537,31 +548,32 @@
"pageTitle": "Paylaşımlar"
},
"sharesTable": {
"ariaLabel": "Paylaşım Tablosu",
"ariaLabel": "Paylaşımlar tablosu",
"never": "Asla",
"columns": {
"name": "Ad",
"createdAt": "Oluşturulma Tarihi",
"expiresAt": "Son Kullanma Tarihi",
"status": "Durum",
"security": "Güvenlik",
"files": "Dosyalar",
"recipients": "Alıcılar",
"actions": "İşlemler"
"name": "AD",
"description": "AÇIKLAMA",
"createdAt": "OLUŞTURULMA TARİHİ",
"expiresAt": "SON KULLANMA TARİHİ",
"status": "DURUM",
"security": "GÜVENLİK",
"files": "DOSYALAR",
"recipients": "ALICILAR",
"actions": "İŞLEMLER"
},
"status": {
"neverExpires": "Sona Ermez",
"neverExpires": "Asla Sona Ermez",
"active": "Aktif",
"expired": "Sona Erdi"
"expired": "Süresi Dolmuş"
},
"security": {
"protected": "Korunuyor",
"protected": "Korumalı",
"public": "Genel"
},
"filesCount": "dosya",
"recipientsCount": "alıcı",
"actions": {
"menu": "Paylaşım İşlem Menüsü",
"menu": "Paylaşım işlemleri menüsü",
"edit": "Düzenle",
"manageFiles": "Dosyaları Yönet",
"manageRecipients": "Alıcıları Yönet",
@@ -569,7 +581,7 @@
"generateLink": "Bağlantı Oluştur",
"editLink": "Bağlantıyı Düzenle",
"copyLink": "Bağlantıyı Kopyala",
"notifyRecipients": "Alıcıları Bildir",
"notifyRecipients": "Alıcıları Bilgilendir",
"delete": "Sil"
}
},
@@ -694,23 +706,25 @@
"passwordRequired": "Şifre gerekli"
},
"shareFile": {
"title": "Dosyayı paylaş",
"linkTitle": "Bağlantı oluştur",
"nameLabel": "Paylaşım adı",
"namePlaceholder": "Paylaşım adını girin",
"expirationLabel": "Son kullanma tarihi",
"expirationPlaceholder": "GG.AA.YYYY SS:DD",
"maxViewsLabel": "Maksimum görüntüleme",
"title": "Dosya Paylaş",
"linkTitle": "Bağlantı Oluştur",
"nameLabel": "Paylaşım Adı",
"namePlaceholder": "Paylaşım adı girin",
"descriptionLabel": "ıklama",
"descriptionPlaceholder": "ıklama girin (isteğe bağlı)",
"expirationLabel": "Son Kullanma Tarihi",
"expirationPlaceholder": "DD/MM/YYYY HH:MM",
"maxViewsLabel": "Maksimum Görüntüleme",
"maxViewsPlaceholder": "Sınırsız için boş bırakın",
"passwordProtection": "Şifre korumalı",
"passwordProtection": "Şifre Korumalı",
"passwordLabel": "Şifre",
"passwordPlaceholder": "Şifre girin",
"linkDescription": "Dosyayı paylaşmak için özel bir bağlantı oluşturun",
"aliasLabel": "Bağlantı takma adı",
"linkDescription": "Dosyayı paylaşmak için özel bağlantı oluşturun",
"aliasLabel": "Bağlantı Takma Adı",
"aliasPlaceholder": "Özel takma ad girin",
"linkReady": "Paylaşım bağlantınız hazır:",
"createShare": "Paylaşım oluştur",
"generateLink": "Bağlantı oluştur",
"copyLink": "Bağlantıyı kopyala"
"createShare": "Paylaşım Oluştur",
"generateLink": "Bağlantı Oluştur",
"copyLink": "Bağlantıyı Kopyala"
}
}

View File

@@ -13,17 +13,19 @@
"back": "返回"
},
"createShare": {
"title": "创建享",
"nameLabel": "享名称",
"title": "创建享",
"nameLabel": "享名称",
"descriptionLabel": "描述",
"descriptionPlaceholder": "输入描述(可选)",
"expirationLabel": "过期日期",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"maxViewsLabel": "最大查看次数",
"maxViewsPlaceholder": "留空表示无限",
"maxViewsPlaceholder": "留空无限",
"passwordProtection": "密码保护",
"passwordLabel": "密码",
"create": "创建享",
"success": "享创建成功",
"error": "创建享失败"
"create": "创建享",
"success": "享创建成功",
"error": "创建享失败"
},
"dashboard": {
"loadError": "加载仪表盘数据失败",
@@ -90,12 +92,19 @@
"downloadStart": "下载已开始",
"downloadError": "下载文件失败",
"updateSuccess": "文件更新成功",
"updateError": "文件更新失败",
"updateError": "更新文件失败",
"deleteSuccess": "文件删除成功",
"deleteError": "文件删除失败"
"deleteError": "删除文件失败",
"bulkDownloadSuccess": "文件下载成功开始",
"bulkDownloadError": "创建ZIP文件错误",
"bulkDownloadFileError": "下载文件 {fileName} 错误",
"bulkDeleteSuccess": "{count, plural, =1 {1个文件删除成功} other {#个文件删除成功}}",
"bulkDeleteError": "删除选中文件错误"
},
"filesTable": {
"ariaLabel": "文件表格",
"selectAll": "全选",
"selectFile": "选择文件 {fileName}",
"columns": {
"name": "名称",
"description": "描述",
@@ -111,6 +120,13 @@
"share": "分享",
"download": "下载",
"delete": "删除"
},
"bulkActions": {
"selected": "{count, plural, =1 {已选择1个文件} other {已选择#个文件}}",
"actions": "操作",
"download": "下载选中项",
"share": "分享选中项",
"delete": "删除选中项"
}
},
"footer": {
@@ -699,9 +715,9 @@
"nameLabel": "分享名称",
"namePlaceholder": "输入分享名称",
"expirationLabel": "过期日期",
"expirationPlaceholder": "年/月/日 时:分",
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
"maxViewsLabel": "最大查看次数",
"maxViewsPlaceholder": "留空表示无限制",
"maxViewsPlaceholder": "留空无限制",
"passwordProtection": "密码保护",
"passwordLabel": "密码",
"passwordPlaceholder": "输入密码",
@@ -712,5 +728,27 @@
"createShare": "创建分享",
"generateLink": "生成链接",
"copyLink": "复制链接"
},
"bulkDownload": {
"title": "批量下载",
"zipNameLabel": "ZIP文件名",
"zipNamePlaceholder": "输入文件名",
"description": "{count, plural, =1 {将压缩1个文件} other {将压缩#个文件}}",
"download": "下载ZIP"
},
"deleteConfirmation": {
"filesToDelete": "要删除的文件"
},
"shareMultipleFiles": {
"title": "分享多个文件",
"shareNameLabel": "分享名称",
"shareNamePlaceholder": "输入分享名称",
"descriptionLabel": "描述",
"descriptionPlaceholder": "输入描述(可选)",
"filesToShare": "要分享的文件",
"files": "文件",
"totalSize": "总大小",
"creating": "创建中...",
"create": "创建分享"
}
}

View File

@@ -26,6 +26,7 @@
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-aspect-ratio": "^1.1.3",
"@radix-ui/react-avatar": "^1.1.4",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-label": "^2.1.2",
@@ -41,6 +42,7 @@
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.6.3",
"jszip": "^3.10.1",
"lucide-react": "^0.487.0",
"nanoid": "^5.1.5",
"next": "15.2.4",

4200
apps/web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
import { IconDownload } from "@tabler/icons-react";
import { useState } from "react";
import { IconDownload, IconEye } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { FilePreviewModal } from "@/components/modals/file-preview-modal";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { getFileIcon } from "@/utils/file-icons";
@@ -9,6 +11,8 @@ import { ShareFilesTableProps } from "../types";
export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
const t = useTranslations();
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [selectedFile, setSelectedFile] = useState<{ name: string; objectName: string; type?: string } | null>(null);
const formatDateTime = (dateString: string) => {
const date = new Date(dateString);
@@ -22,6 +26,16 @@ export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
}).format(date);
};
const handlePreview = (file: { name: string; objectName: string }) => {
setSelectedFile(file);
setIsPreviewOpen(true);
};
const handleClosePreview = () => {
setIsPreviewOpen(false);
setSelectedFile(null);
};
return (
<div className="flex flex-col gap-4">
<div className="rounded-lg border shadow-sm overflow-hidden">
@@ -37,7 +51,7 @@ export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("filesTable.columns.createdAt")}
</TableHead>
<TableHead className="h-10 w-[70px] text-xs font-bold text-muted-foreground bg-muted/50 px-4">
<TableHead className="h-10 w-[110px] text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("filesTable.columns.actions")}
</TableHead>
</TableRow>
@@ -57,15 +71,26 @@ export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
<TableCell className="h-12 px-4">{formatFileSize(Number(file.size))}</TableCell>
<TableCell className="h-12 px-4">{formatDateTime(file.createdAt)}</TableCell>
<TableCell className="h-12 px-4">
<Button
size="icon"
variant="ghost"
className="h-8 w-8 hover:bg-muted"
onClick={() => onDownload(file.objectName, file.name)}
>
<IconDownload className="h-4 w-4" />
<span className="sr-only">{t("filesTable.actions.download")}</span>
</Button>
<div className="flex items-center gap-1">
<Button
size="icon"
variant="ghost"
className="h-8 w-8 hover:bg-muted"
onClick={() => handlePreview({ name: file.name, objectName: file.objectName })}
>
<IconEye className="h-4 w-4" />
<span className="sr-only">{t("filesTable.actions.preview")}</span>
</Button>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 hover:bg-muted"
onClick={() => onDownload(file.objectName, file.name)}
>
<IconDownload className="h-4 w-4" />
<span className="sr-only">{t("filesTable.actions.download")}</span>
</Button>
</div>
</TableCell>
</TableRow>
);
@@ -73,6 +98,8 @@ export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
</TableBody>
</Table>
</div>
{selectedFile && <FilePreviewModal isOpen={isPreviewOpen} onClose={handleClosePreview} file={selectedFile} />}
</div>
);
}

View File

@@ -1,3 +1,5 @@
import { useState } from "react";
import { CreateShareModal } from "@/components/modals/create-share-modal";
import { GenerateShareLinkModal } from "@/components/modals/generate-share-link-modal";
import { ShareActionsModals } from "@/components/modals/share-actions-modals";
@@ -10,13 +12,21 @@ export function SharesModals({
shareToViewDetails,
shareToGenerateLink,
shareManager,
fileManager,
onSuccess,
onCloseViewDetails,
onCloseGenerateLink,
}: SharesModalsProps) {
const [shareDetailsRefresh, setShareDetailsRefresh] = useState(0);
const handleShareSuccess = () => {
setShareDetailsRefresh((prev) => prev + 1);
onSuccess();
};
return (
<>
<CreateShareModal isOpen={isCreateModalOpen} onClose={onCloseCreateModal} onSuccess={onSuccess} />
<CreateShareModal isOpen={isCreateModalOpen} onClose={onCloseCreateModal} onSuccess={handleShareSuccess} />
<ShareActionsModals
shareToDelete={shareManager.shareToDelete}
@@ -31,17 +41,27 @@ export function SharesModals({
onEdit={shareManager.handleEdit}
onManageFiles={shareManager.handleManageFiles}
onManageRecipients={shareManager.handleManageRecipients}
onSuccess={onSuccess}
onSuccess={handleShareSuccess}
onEditFile={fileManager.handleRename}
/>
<ShareDetailsModal shareId={shareToViewDetails?.id || null} onClose={onCloseViewDetails} />
<ShareDetailsModal
shareId={shareToViewDetails?.id || null}
onClose={onCloseViewDetails}
onUpdateName={shareManager.handleUpdateName}
onUpdateDescription={shareManager.handleUpdateDescription}
onGenerateLink={shareManager.handleGenerateLink}
onManageFiles={shareManager.setShareToManageFiles}
refreshTrigger={shareDetailsRefresh}
onSuccess={handleShareSuccess}
/>
<GenerateShareLinkModal
share={shareToGenerateLink}
shareId={shareToGenerateLink?.id || null}
onClose={onCloseGenerateLink}
onGenerate={shareManager.handleGenerateLink}
onSuccess={onSuccess}
onSuccess={handleShareSuccess}
/>
</>
);

View File

@@ -10,6 +10,7 @@ export function SharesTableContainer({ shares, onCopyLink, onCreateShare, shareM
onDelete={shareManager.setShareToDelete}
onEdit={shareManager.setShareToEdit}
onUpdateName={shareManager.handleUpdateName}
onUpdateDescription={shareManager.handleUpdateDescription}
onGenerateLink={shareManager.setShareToGenerateLink}
onManageFiles={shareManager.setShareToManageFiles}
onManageRecipients={shareManager.setShareToManageRecipients}

View File

@@ -6,6 +6,7 @@ import { Navbar } from "@/components/layout/navbar";
import { Card, CardContent } from "@/components/ui/card";
import { DefaultFooter } from "@/components/ui/default-footer";
import { useDisclosure } from "@/hooks/use-disclosure";
import { useFileManager } from "@/hooks/use-file-manager";
import { useShareManager } from "@/hooks/use-share-manager";
import { SharesHeader } from "./components/shares-header";
import { SharesModals } from "./components/shares-modals";
@@ -31,6 +32,7 @@ export default function SharesPage() {
const { isOpen: isCreateModalOpen, onOpen: onOpenCreateModal, onClose: onCloseCreateModal } = useDisclosure();
const shareManager = useShareManager(loadShares);
const fileManager = useFileManager(loadShares);
if (isLoading) {
return <LoadingScreen />;
@@ -67,6 +69,7 @@ export default function SharesPage() {
<SharesModals
isCreateModalOpen={isCreateModalOpen}
shareManager={shareManager}
fileManager={fileManager}
shareToGenerateLink={shareToGenerateLink}
shareToViewDetails={shareToViewDetails}
smtpEnabled={smtpEnabled}

View File

@@ -41,6 +41,7 @@ export interface SharesModalsProps {
shareToViewDetails: any;
shareToGenerateLink: any;
shareManager: any;
fileManager: any;
onSuccess: () => void;
onCloseViewDetails: () => void;
onCloseGenerateLink: () => void;

View File

@@ -50,6 +50,10 @@ export function RecentFiles({ files, fileManager, onOpenUploadModal }: RecentFil
onPreview={fileManager.setPreviewFile}
onRename={fileManager.setFileToRename}
onShare={fileManager.setFileToShare}
onBulkDelete={fileManager.handleBulkDelete}
onBulkShare={fileManager.handleBulkShare}
onBulkDownload={fileManager.handleBulkDownload}
setClearSelectionCallback={fileManager.setClearSelectionCallback}
onUpdateName={(fileId, newName) => {
const file = files.find((f) => f.id === fileId);
if (file) {

View File

@@ -51,6 +51,7 @@ export function RecentShares({ shares, shareManager, onOpenCreateModal, onCopyLi
onDelete={shareManager.setShareToDelete}
onEdit={shareManager.setShareToEdit}
onUpdateName={shareManager.handleUpdateName}
onUpdateDescription={shareManager.handleUpdateDescription}
onGenerateLink={shareManager.setShareToGenerateLink}
onManageFiles={shareManager.setShareToManageFiles}
onManageRecipients={shareManager.setShareToManageRecipients}

View File

@@ -1,14 +1,26 @@
import { useState } from "react";
import { BulkDownloadModal } from "@/components/modals/bulk-download-modal";
import { CreateShareModal } from "@/components/modals/create-share-modal";
import { DeleteConfirmationModal } from "@/components/modals/delete-confirmation-modal";
import { FileActionsModals } from "@/components/modals/file-actions-modals";
import { FilePreviewModal } from "@/components/modals/file-preview-modal";
import { GenerateShareLinkModal } from "@/components/modals/generate-share-link-modal";
import { ShareActionsModals } from "@/components/modals/share-actions-modals";
import { ShareDetailsModal } from "@/components/modals/share-details-modal";
import { ShareFileModal } from "@/components/modals/share-file-modal";
import { ShareMultipleFilesModal } from "@/components/modals/share-multiple-files-modal";
import { UploadFileModal } from "@/components/modals/upload-file-modal";
import { DashboardModalsProps } from "../types";
export function DashboardModals({ modals, fileManager, shareManager, onSuccess }: DashboardModalsProps) {
const [shareDetailsRefresh, setShareDetailsRefresh] = useState(0);
const handleShareSuccess = () => {
setShareDetailsRefresh((prev) => prev + 1);
onSuccess();
};
return (
<>
<UploadFileModal isOpen={modals.isUploadModalOpen} onClose={modals.onCloseUploadModal} onSuccess={onSuccess} />
@@ -35,6 +47,36 @@ export function DashboardModals({ modals, fileManager, shareManager, onSuccess }
onRename={fileManager.handleRename}
/>
<BulkDownloadModal
isOpen={fileManager.isBulkDownloadModalOpen}
onClose={() => fileManager.setBulkDownloadModalOpen(false)}
onDownload={(zipName) => {
if (fileManager.filesToDownload) {
fileManager.handleBulkDownloadWithZip(fileManager.filesToDownload, zipName);
}
}}
fileCount={fileManager.filesToDownload?.length || 0}
/>
<DeleteConfirmationModal
isOpen={!!fileManager.filesToDelete}
onClose={() => fileManager.setFilesToDelete(null)}
onConfirm={fileManager.handleDeleteBulk}
title="Excluir Arquivos Selecionados"
description={`Tem certeza que deseja excluir ${fileManager.filesToDelete?.length || 0} arquivo(s)? Esta ação não pode ser desfeita.`}
files={fileManager.filesToDelete?.map((f) => f.name) || []}
/>
<ShareMultipleFilesModal
files={fileManager.filesToShare}
isOpen={!!fileManager.filesToShare}
onClose={() => fileManager.setFilesToShare(null)}
onSuccess={() => {
fileManager.handleShareBulkSuccess();
onSuccess();
}}
/>
<CreateShareModal isOpen={modals.isCreateModalOpen} onClose={modals.onCloseCreateModal} onSuccess={onSuccess} />
<ShareActionsModals
@@ -50,12 +92,19 @@ export function DashboardModals({ modals, fileManager, shareManager, onSuccess }
onEdit={shareManager.handleEdit}
onManageFiles={shareManager.handleManageFiles}
onManageRecipients={shareManager.handleManageRecipients}
onSuccess={onSuccess}
onEditFile={fileManager.handleRename}
onSuccess={handleShareSuccess}
/>
<ShareDetailsModal
shareId={shareManager.shareToViewDetails?.id || null}
onClose={() => shareManager.setShareToViewDetails(null)}
onUpdateName={shareManager.handleUpdateName}
onUpdateDescription={shareManager.handleUpdateDescription}
onGenerateLink={shareManager.handleGenerateLink}
onManageFiles={shareManager.setShareToManageFiles}
refreshTrigger={shareDetailsRefresh}
onSuccess={onSuccess}
/>
<GenerateShareLinkModal

View File

@@ -26,6 +26,10 @@ export function FileList({ files, filteredFiles, fileManager, searchQuery, onSea
onPreview={fileManager.setPreviewFile}
onRename={fileManager.setFileToRename}
onShare={fileManager.setFileToShare}
onBulkDelete={fileManager.handleBulkDelete}
onBulkShare={fileManager.handleBulkShare}
onBulkDownload={fileManager.handleBulkDownload}
setClearSelectionCallback={fileManager.setClearSelectionCallback}
onUpdateName={(fileId, newName) => {
const file = filteredFiles.find((f) => f.id === fileId);
if (file) {

View File

@@ -13,6 +13,7 @@ export function useFiles() {
const [isLoading, setIsLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState("");
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const [clearSelectionCallback, setClearSelectionCallback] = useState<(() => void) | undefined>();
const loadFiles = async () => {
try {
@@ -31,7 +32,7 @@ export function useFiles() {
}
};
const fileManager = useFileManager(loadFiles);
const fileManager = useFileManager(loadFiles, clearSelectionCallback);
const filteredFiles = files.filter((file) => file.name.toLowerCase().includes(searchQuery.toLowerCase()));
useEffect(() => {
@@ -47,7 +48,10 @@ export function useFiles() {
onOpenUploadModal: () => setIsUploadModalOpen(true),
onCloseUploadModal: () => setIsUploadModalOpen(false),
},
fileManager,
fileManager: {
...fileManager,
setClearSelectionCallback,
} as typeof fileManager & { setClearSelectionCallback: typeof setClearSelectionCallback },
filteredFiles,
handleSearch: setSearchQuery,
loadFiles,

View File

@@ -1,10 +1,17 @@
import { useTranslations } from "next-intl";
import { BulkDownloadModal } from "@/components/modals/bulk-download-modal";
import { DeleteConfirmationModal } from "@/components/modals/delete-confirmation-modal";
import { FileActionsModals } from "@/components/modals/file-actions-modals";
import { FilePreviewModal } from "@/components/modals/file-preview-modal";
import { ShareFileModal } from "@/components/modals/share-file-modal";
import { ShareMultipleFilesModal } from "@/components/modals/share-multiple-files-modal";
import { UploadFileModal } from "@/components/modals/upload-file-modal";
import type { FilesModalsProps } from "../types";
export function FilesModals({ fileManager, modals, onSuccess }: FilesModalsProps) {
const t = useTranslations();
return (
<>
<UploadFileModal isOpen={modals.isUploadModalOpen} onClose={modals.onCloseUploadModal} onSuccess={onSuccess} />
@@ -30,6 +37,37 @@ export function FilesModals({ fileManager, modals, onSuccess }: FilesModalsProps
onDelete={fileManager.handleDelete}
onRename={fileManager.handleRename}
/>
{/* Bulk Actions Modals */}
<BulkDownloadModal
isOpen={fileManager.isBulkDownloadModalOpen}
onClose={() => fileManager.setBulkDownloadModalOpen(false)}
onDownload={(zipName) => {
if (fileManager.filesToDownload) {
fileManager.handleBulkDownloadWithZip(fileManager.filesToDownload, zipName);
}
}}
fileCount={fileManager.filesToDownload?.length || 0}
/>
<DeleteConfirmationModal
isOpen={!!fileManager.filesToDelete}
onClose={() => fileManager.setFilesToDelete(null)}
onConfirm={fileManager.handleDeleteBulk}
title="Excluir Arquivos Selecionados"
description={`Tem certeza que deseja excluir ${fileManager.filesToDelete?.length || 0} arquivo(s)? Esta ação não pode ser desfeita.`}
files={fileManager.filesToDelete?.map((f) => f.name) || []}
/>
<ShareMultipleFilesModal
files={fileManager.filesToShare}
isOpen={!!fileManager.filesToShare}
onClose={() => fileManager.setFilesToShare(null)}
onSuccess={() => {
fileManager.handleShareBulkSuccess();
onSuccess();
}}
/>
</>
);
}

View File

@@ -1,27 +1,34 @@
"use client";
import { useEffect, useState } from "react";
import { IconArrowLeft, IconArrowRight, IconFile } from "@tabler/icons-react";
import { IconCheck, IconEdit, IconEye, IconMinus, IconPlus, IconSearch } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { FileActionsModals } from "@/components/modals/file-actions-modals";
import { FilePreviewModal } from "@/components/modals/file-preview-modal";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { addFiles, listFiles, removeFiles } from "@/http/endpoints";
import { getFileIcon } from "@/utils/file-icons";
interface FileSelectorProps {
shareId: string;
selectedFiles: string[];
onSave: (files: string[]) => Promise<void>;
onEditFile?: (fileId: string, newName: string, description?: string) => Promise<void>;
}
export function FileSelector({ shareId, selectedFiles, onSave }: FileSelectorProps) {
export function FileSelector({ shareId, selectedFiles, onSave, onEditFile }: FileSelectorProps) {
const t = useTranslations();
const [availableFiles, setAvailableFiles] = useState<any[]>([]);
const [shareFiles, setShareFiles] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [availableFilter, setAvailableFilter] = useState("");
const [shareFilter, setShareFilter] = useState("");
const [searchFilter, setSearchFilter] = useState("");
const [shareSearchFilter, setShareSearchFilter] = useState("");
const [previewFile, setPreviewFile] = useState<any>(null);
const [fileToEdit, setFileToEdit] = useState<any>(null);
useEffect(() => {
loadFiles();
@@ -40,9 +47,8 @@ export function FileSelector({ shareId, selectedFiles, onSave }: FileSelectorPro
}
};
const moveToShare = (fileId: string) => {
const addToShare = (fileId: string) => {
const file = availableFiles.find((f) => f.id === fileId);
if (file) {
setShareFiles([...shareFiles, file]);
setAvailableFiles(availableFiles.filter((f) => f.id !== fileId));
@@ -51,7 +57,6 @@ export function FileSelector({ shareId, selectedFiles, onSave }: FileSelectorPro
const removeFromShare = (fileId: string) => {
const file = shareFiles.find((f) => f.id === fileId);
if (file) {
setAvailableFiles([...availableFiles, file]);
setShareFiles(shareFiles.filter((f) => f.id !== fileId));
@@ -63,7 +68,6 @@ export function FileSelector({ shareId, selectedFiles, onSave }: FileSelectorPro
setIsLoading(true);
const filesToAdd = shareFiles.filter((file) => !selectedFiles.includes(file.id)).map((file) => file.id);
const filesToRemove = selectedFiles.filter((fileId) => !shareFiles.find((f) => f.id === fileId));
if (filesToAdd.length > 0) {
@@ -75,7 +79,6 @@ export function FileSelector({ shareId, selectedFiles, onSave }: FileSelectorPro
}
await onSave(shareFiles.map((f) => f.id));
toast.success("Files updated successfully");
} catch (error) {
console.error(error);
toast.error("Failed to update files");
@@ -84,106 +87,219 @@ export function FileSelector({ shareId, selectedFiles, onSave }: FileSelectorPro
}
};
const handleEditFile = async (fileId: string, newName: string, description?: string) => {
if (onEditFile) {
await onEditFile(fileId, newName, description);
setFileToEdit(null);
// Recarregar arquivos para mostrar as mudanças
await loadFiles();
}
};
const filteredAvailableFiles = availableFiles.filter((file) =>
file.name.toLowerCase().includes(availableFilter.toLowerCase())
file.name.toLowerCase().includes(searchFilter.toLowerCase())
);
const filteredShareFiles = shareFiles.filter((file) => file.name.toLowerCase().includes(shareFilter.toLowerCase()));
const filteredShareFiles = shareFiles.filter((file) =>
file.name.toLowerCase().includes(shareSearchFilter.toLowerCase())
);
const formatFileSize = (bytes: number) => {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
};
const FileCard = ({ file, isInShare }: { file: any; isInShare: boolean }) => {
const { icon: FileIcon, color } = getFileIcon(file.name);
return (
<div className="flex items-center gap-3 p-3 bg-background rounded-lg border group hover:border-muted-foreground/20 transition-colors">
<FileIcon className={`h-5 w-5 ${color} flex-shrink-0`} />
<div className="flex-1 min-w-0">
<div className="font-medium text-sm truncate max-w-[260px]" title={file.name}>
{file.name}
</div>
{file.description && (
<div className="text-xs text-muted-foreground truncate max-w-[260px]" title={file.description}>
{file.description}
</div>
)}
<div className="text-xs text-muted-foreground">{formatFileSize(file.size)}</div>
</div>
<div className="flex items-center gap-2">
{/* Botões de ações secundárias */}
<div className="flex gap-1">
{onEditFile && (
<Button
size="icon"
variant="ghost"
className="h-7 w-7 hover:bg-muted transition-colors"
onClick={() => setFileToEdit(file)}
title="Editar arquivo"
>
<IconEdit className="h-4 w-4" />
</Button>
)}
<Button
size="icon"
variant="ghost"
className="h-7 w-7 hover:bg-muted transition-colors"
onClick={() => setPreviewFile(file)}
title="Visualizar arquivo"
>
<IconEye className="h-4 w-4" />
</Button>
</div>
{/* Botão de ação principal destacado */}
<div className="ml-1">
<Button
size="icon"
variant={isInShare ? "destructive" : "default"}
className="h-8 w-8 transition-all"
onClick={() => (isInShare ? removeFromShare(file.id) : addToShare(file.id))}
title={isInShare ? "Remover do compartilhamento" : "Adicionar ao compartilhamento"}
>
{isInShare ? <IconMinus className="h-4 w-4" /> : <IconPlus className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
);
};
return (
<div className="flex flex-col gap-4">
<div className="flex gap-4 h-[500px]">
<div className="flex-1 border rounded-lg">
<div className="p-4 border-b">
<h3 className="font-medium">
{t("fileSelector.availableFiles", { count: filteredAvailableFiles.length })}
</h3>
<Input
className="mt-2"
placeholder={t("fileSelector.searchPlaceholder")}
type="search"
value={availableFilter}
onChange={(e) => setAvailableFilter(e.target.value)}
/>
<>
<div className="space-y-6">
{/* Current Files in Share */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium">{t("fileSelector.shareFiles", { count: shareFiles.length })}</h3>
<p className="text-sm text-muted-foreground">Arquivos atualmente no compartilhamento</p>
</div>
<Badge variant="secondary" className="bg-blue-500/20 text-blue-700">
{shareFiles.length} {shareFiles.length === 1 ? "arquivo" : "arquivos"}
</Badge>
</div>
<div className="p-4 h-[calc(100%-115px)] overflow-y-auto">
{filteredAvailableFiles.length === 0 ? (
<div className="text-center py-8 text-gray-500">
{availableFilter ? t("fileSelector.noMatchingFiles") : t("fileSelector.noAvailableFiles")}
</div>
) : (
<div className="flex flex-col gap-2">
{filteredAvailableFiles.map((file) => (
<div
key={file.id}
className="flex items-center justify-between p-3 rounded-lg border border-transparent hover:border-primary-500 cursor-pointer"
onClick={() => moveToShare(file.id)}
>
<div className="flex items-center gap-2">
<IconFile className="text-gray-400" size={20} />
<span className="truncate max-w-[150px]" title={file.name}>
{file.name}
</span>
</div>
<IconArrowRight className="text-gray-400" size={20} />
</div>
))}
</div>
)}
</div>
</div>
<div className="flex-1 border rounded-lg">
<div className="p-4 border-b">
<h3 className="font-medium">{t("fileSelector.shareFiles", { count: filteredShareFiles.length })}</h3>
<Input
className="mt-2"
placeholder={t("fileSelector.searchPlaceholder")}
type="search"
value={shareFilter}
onChange={(e) => setShareFilter(e.target.value)}
/>
</div>
<div className="p-4 h-[calc(100%-115px)] overflow-y-auto">
{filteredShareFiles.length === 0 ? (
<div className="text-center py-8 text-gray-500">
{shareFilter ? t("fileSelector.noMatchingFiles") : t("fileSelector.noFilesInShare")}
</div>
) : (
<div className="flex flex-col gap-2">
{filteredShareFiles.map((file) => (
<div
key={file.id}
className="flex items-center justify-between p-3 rounded-lg border border-transparent hover:border-primary-500 cursor-pointer"
onClick={() => removeFromShare(file.id)}
>
<div className="flex items-center gap-2">
<IconFile className="text-gray-400" size={20} />
<span className="truncate max-w-[150px]" title={file.name}>
{file.name}
</span>
</div>
<IconArrowLeft className="text-gray-400" size={20} />
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* Search for selected files - only show if there are files */}
{shareFiles.length > 0 && (
<div className="relative">
<IconSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar nos arquivos selecionados..."
value={shareSearchFilter}
onChange={(e) => setShareSearchFilter(e.target.value)}
className="pl-10"
/>
</div>
)}
<div className="flex justify-end">
<Button variant="default" disabled={isLoading} onClick={handleSave}>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="animate-spin"></div>
{t("fileSelector.saveChanges")}
{shareFiles.length > 0 ? (
<div className="grid gap-2 max-h-40 overflow-y-auto border rounded-lg p-3 bg-muted/30">
{filteredShareFiles.map((file) => (
<FileCard key={file.id} file={file} isInShare={true} />
))}
{filteredShareFiles.length === 0 && shareSearchFilter && (
<div className="text-center py-4 text-muted-foreground">
<p className="text-sm">Nenhum arquivo encontrado com "{shareSearchFilter}"</p>
</div>
)}
</div>
) : (
t("fileSelector.saveChanges")
<div className="text-center py-8 text-muted-foreground border rounded-lg bg-muted/20">
<div className="text-4xl mb-2">📁</div>
<p className="font-medium">Nenhum arquivo no compartilhamento</p>
<p className="text-sm">Adicione arquivos da lista abaixo</p>
</div>
)}
</Button>
</div>
{/* Available Files to Add */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-medium">
{t("fileSelector.availableFiles", { count: filteredAvailableFiles.length })}
</h3>
<p className="text-sm text-muted-foreground">Selecione arquivos para adicionar ao compartilhamento</p>
</div>
</div>
{/* Search */}
<div className="relative">
<IconSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={t("fileSelector.searchPlaceholder")}
value={searchFilter}
onChange={(e) => setSearchFilter(e.target.value)}
className="pl-10"
/>
</div>
{/* Available Files Grid */}
{filteredAvailableFiles.length > 0 ? (
<div className="grid gap-2 max-h-60 overflow-y-auto border rounded-lg p-3 bg-muted/10">
{filteredAvailableFiles.map((file) => (
<FileCard key={file.id} file={file} isInShare={false} />
))}
</div>
) : searchFilter ? (
<div className="text-center py-8 text-muted-foreground border rounded-lg bg-muted/20">
<div className="text-4xl mb-2">🔍</div>
<p className="font-medium">Nenhum arquivo encontrado</p>
<p className="text-sm">Tente usar outros termos de busca</p>
</div>
) : (
<div className="text-center py-8 text-muted-foreground border rounded-lg bg-muted/20">
<div className="text-4xl mb-2">📄</div>
<p className="font-medium">Todos os arquivos estão no compartilhamento</p>
<p className="text-sm">Faça upload de novos arquivos para adicioná-los</p>
</div>
)}
</div>
{/* Actions */}
<div className="flex items-center justify-between pt-4 border-t">
<div className="text-sm text-muted-foreground">
{shareFiles.length} {shareFiles.length === 1 ? "arquivo" : "arquivos"} selecionado(s)
</div>
<Button onClick={handleSave} disabled={isLoading} className="gap-2">
{isLoading ? (
<>
<div className="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
Salvando...
</>
) : (
<>
<IconCheck className="h-4 w-4" />
{t("fileSelector.saveChanges")}
</>
)}
</Button>
</div>
</div>
</div>
{/* File Preview Dialog */}
<FilePreviewModal
isOpen={!!previewFile}
onClose={() => setPreviewFile(null)}
file={previewFile || { name: "", objectName: "" }}
/>
{/* File Edit Dialog */}
<FileActionsModals
fileToRename={fileToEdit}
fileToDelete={null}
onRename={handleEditFile}
onDelete={async () => {}}
onCloseRename={() => setFileToEdit(null)}
onCloseDelete={() => {}}
/>
</>
);
}

View File

@@ -0,0 +1,74 @@
"use client";
import { useState } from "react";
import { IconDownload, IconX } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
interface BulkDownloadModalProps {
isOpen: boolean;
onClose: () => void;
onDownload: (zipName: string) => void;
fileCount: number;
}
export function BulkDownloadModal({ isOpen, onClose, onDownload, fileCount }: BulkDownloadModalProps) {
const t = useTranslations();
const [zipName, setZipName] = useState("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (zipName.trim()) {
onDownload(zipName.trim());
handleClose();
}
};
const handleClose = () => {
onClose();
setZipName("");
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<IconDownload size={20} />
{t("bulkDownload.title")}
</DialogTitle>
</DialogHeader>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="zipName">{t("bulkDownload.zipNameLabel")}</Label>
<Input
id="zipName"
value={zipName}
onChange={(e) => setZipName(e.target.value)}
placeholder={t("bulkDownload.zipNamePlaceholder")}
className="w-full"
autoFocus
/>
<p className="text-sm text-muted-foreground">{t("bulkDownload.description", { count: fileCount })}</p>
</div>
</form>
<DialogFooter className="flex gap-2">
<Button variant="outline" onClick={handleClose}>
<IconX className="h-4 w-4 mr-2" />
{t("common.cancel")}
</Button>
<Button onClick={handleSubmit} disabled={!zipName.trim()}>
<IconDownload className="h-4 w-4 mr-2" />
{t("bulkDownload.download")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -22,6 +22,7 @@ export function CreateShareModal({ isOpen, onClose, onSuccess }: CreateShareModa
const t = useTranslations();
const [formData, setFormData] = useState({
name: "",
description: "",
password: "",
expiresAt: "",
isPasswordProtected: false,
@@ -34,6 +35,7 @@ export function CreateShareModal({ isOpen, onClose, onSuccess }: CreateShareModa
setIsLoading(true);
await createShare({
name: formData.name,
description: formData.description || undefined,
password: formData.isPasswordProtected ? formData.password : undefined,
expiration: formData.expiresAt ? new Date(formData.expiresAt).toISOString() : undefined,
maxViews: formData.maxViews ? parseInt(formData.maxViews) : undefined,
@@ -44,6 +46,7 @@ export function CreateShareModal({ isOpen, onClose, onSuccess }: CreateShareModa
onClose();
setFormData({
name: "",
description: "",
password: "",
expiresAt: "",
isPasswordProtected: false,
@@ -72,6 +75,15 @@ export function CreateShareModal({ isOpen, onClose, onSuccess }: CreateShareModa
<Input value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value })} />
</div>
<div className="space-y-2">
<Label>{t("createShare.descriptionLabel")}</Label>
<Input
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder={t("createShare.descriptionPlaceholder")}
/>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2">
<IconCalendar size={16} />

View File

@@ -0,0 +1,76 @@
"use client";
import { IconTrash, IconX } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
interface DeleteConfirmationModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
description: string;
files: string[];
}
export function DeleteConfirmationModal({
isOpen,
onClose,
onConfirm,
title,
description,
files,
}: DeleteConfirmationModalProps) {
const t = useTranslations();
const handleConfirm = () => {
onConfirm();
onClose();
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-destructive">
<IconTrash size={20} />
{title}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<p className="text-sm text-muted-foreground">{description}</p>
{files.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium">{t("deleteConfirmation.filesToDelete")}:</p>
<ScrollArea className="h-32 w-full rounded-md border p-2">
<div className="space-y-1">
{files.map((fileName, index) => (
<div key={index} className="text-sm text-muted-foreground truncate">
{fileName}
</div>
))}
</div>
</ScrollArea>
</div>
)}
</div>
<DialogFooter className="flex gap-2">
<Button variant="outline" onClick={onClose}>
<IconX className="h-4 w-4 mr-2" />
{t("common.cancel")}
</Button>
<Button variant="destructive" onClick={handleConfirm}>
<IconTrash className="h-4 w-4 mr-2" />
{t("common.delete")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -33,6 +33,7 @@ export interface ShareActionsModalsProps {
onEdit: (shareId: string, data: any) => Promise<void>;
onManageFiles: (shareId: string, files: string[]) => Promise<void>;
onManageRecipients: (shareId: string, recipients: string[]) => Promise<void>;
onEditFile?: (fileId: string, newName: string, description?: string) => Promise<void>;
onSuccess: () => void;
}
@@ -48,12 +49,14 @@ export function ShareActionsModals({
onDelete,
onEdit,
onManageFiles,
onEditFile,
onSuccess,
}: ShareActionsModalsProps) {
const t = useTranslations();
const [isLoading, setIsLoading] = useState(false);
const [editForm, setEditForm] = useState({
name: "",
description: "",
expiresAt: "",
isPasswordProtected: false,
password: "",
@@ -64,6 +67,7 @@ export function ShareActionsModals({
if (shareToEdit) {
setEditForm({
name: shareToEdit.name || "",
description: shareToEdit.description || "",
expiresAt: shareToEdit.expiration ? new Date(shareToEdit.expiration).toISOString().slice(0, 16) : "",
isPasswordProtected: Boolean(shareToEdit.security?.hasPassword),
password: "",
@@ -86,6 +90,7 @@ export function ShareActionsModals({
try {
const updateData = {
name: editForm.name,
description: editForm.description,
expiration: editForm.expiresAt ? new Date(editForm.expiresAt).toISOString() : undefined,
maxViews: editForm.maxViews ? parseInt(editForm.maxViews) : null,
};
@@ -138,6 +143,14 @@ export function ShareActionsModals({
<Label>{t("shareActions.nameLabel")}</Label>
<Input value={editForm.name} onChange={(e) => setEditForm({ ...editForm, name: e.target.value })} />
</div>
<div className="grid w-full items-center gap-1.5">
<Label>{t("shareActions.descriptionLabel")}</Label>
<Input
value={editForm.description}
onChange={(e) => setEditForm({ ...editForm, description: e.target.value })}
placeholder={t("shareActions.descriptionPlaceholder")}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label>{t("shareActions.expirationLabel")}</Label>
<Input
@@ -202,18 +215,21 @@ export function ShareActionsModals({
</Dialog>
<Dialog open={!!shareToManageFiles} onOpenChange={() => onCloseManageFiles()}>
<DialogContent className="sm:max-w-[425px] md:max-w-[700px]">
<DialogContent className="sm:max-w-[450px] md:max-w-[550px] lg:max-w-[650px] max-h-[80vh] overflow-hidden">
<DialogHeader>
<DialogTitle>{t("shareActions.manageFilesTitle")}</DialogTitle>
</DialogHeader>
<FileSelector
selectedFiles={shareToManageFiles?.files?.map((file: { id: string }) => file.id) || []}
shareId={shareToManageFiles?.id}
onSave={async (files) => {
await onManageFiles(shareToManageFiles?.id, files);
onSuccess();
}}
/>
<div className="overflow-y-auto max-h-[calc(80vh-120px)]">
<FileSelector
selectedFiles={shareToManageFiles?.files?.map((file: { id: string }) => file.id) || []}
shareId={shareToManageFiles?.id}
onSave={async (files) => {
await onManageFiles(shareToManageFiles?.id, files);
onSuccess();
}}
onEditFile={onEditFile}
/>
</div>
</DialogContent>
</Dialog>

View File

@@ -1,7 +1,16 @@
"use client";
import { useEffect, useState } from "react";
import { IconLock, IconLockOpen, IconMail } from "@tabler/icons-react";
import { useEffect, useRef, useState } from "react";
import {
IconCheck,
IconCopy,
IconEdit,
IconExternalLink,
IconLock,
IconLockOpen,
IconMail,
IconX,
} from "@tabler/icons-react";
import { format } from "date-fns";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
@@ -16,13 +25,21 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Loader } from "@/components/ui/loader";
import { getShare } from "@/http/endpoints";
import { getFileIcon } from "@/utils/file-icons";
import { GenerateShareLinkModal } from "./generate-share-link-modal";
interface ShareDetailsModalProps {
shareId: string | null;
onClose: () => void;
onUpdateName?: (shareId: string, newName: string) => Promise<void>;
onUpdateDescription?: (shareId: string, newDescription: string) => Promise<void>;
onGenerateLink?: (shareId: string, alias: string) => Promise<void>;
onManageFiles?: (share: any) => void;
refreshTrigger?: number;
onSuccess?: () => void;
}
interface ShareFile {
@@ -44,10 +61,24 @@ interface ShareRecipient {
updatedAt: string;
}
export function ShareDetailsModal({ shareId, onClose }: ShareDetailsModalProps) {
export function ShareDetailsModal({
shareId,
onClose,
onUpdateName,
onUpdateDescription,
onGenerateLink,
onManageFiles,
refreshTrigger,
onSuccess,
}: ShareDetailsModalProps) {
const t = useTranslations();
const [share, setShare] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [editingField, setEditingField] = useState<{ field: "name" | "description" } | null>(null);
const [editValue, setEditValue] = useState("");
const [pendingChanges, setPendingChanges] = useState<{ name?: string; description?: string }>({});
const [showLinkModal, setShowLinkModal] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (shareId) {
@@ -55,12 +86,30 @@ export function ShareDetailsModal({ shareId, onClose }: ShareDetailsModalProps)
}
}, [shareId]);
useEffect(() => {
if (editingField && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [editingField]);
// Clear pending changes when share is updated
useEffect(() => {
setPendingChanges({});
}, [share]);
// Refresh data when external update happens
useEffect(() => {
if (refreshTrigger) {
loadShareDetails();
}
}, [refreshTrigger]);
const loadShareDetails = async () => {
if (!shareId) return;
setIsLoading(true);
try {
const response = await getShare(shareId);
setShare(response.data.share);
} catch (error) {
console.error(error);
@@ -77,122 +126,421 @@ export function ShareDetailsModal({ shareId, onClose }: ShareDetailsModalProps)
} catch (error) {
console.error(error);
console.error("Invalid date:", dateString);
return t("shareDetails.invalidDate");
}
};
const startEdit = (field: "name" | "description", currentValue: string) => {
setEditingField({ field });
setEditValue(currentValue || "");
};
const saveEdit = async () => {
if (!editingField || !shareId) return;
const { field } = editingField;
// Update local state optimistically
setPendingChanges((prev) => ({
...prev,
[field]: editValue,
}));
try {
if (field === "name" && onUpdateName) {
await onUpdateName(shareId, editValue);
} else if (field === "description" && onUpdateDescription) {
await onUpdateDescription(shareId, editValue);
}
// Reload share details to get updated data
await loadShareDetails();
if (onSuccess) {
onSuccess();
}
} catch (error) {
console.error("Failed to update:", error);
// Revert optimistic update on error
setPendingChanges((prev) => {
const newState = { ...prev };
delete newState[field];
return newState;
});
}
setEditingField(null);
setEditValue("");
};
const cancelEdit = () => {
setEditingField(null);
setEditValue("");
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
saveEdit();
} else if (e.key === "Escape") {
cancelEdit();
}
};
const getDisplayValue = (field: "name" | "description") => {
const pendingChange = pendingChanges[field];
if (pendingChange !== undefined) {
return pendingChange;
}
return field === "name" ? share?.name : share?.description;
};
const handleCopyLink = () => {
if (share?.alias?.alias) {
const link = `${window.location.origin}/s/${share.alias.alias}`;
navigator.clipboard.writeText(link);
toast.success(t("shareDetails.linkCopied"));
}
};
const handleOpenLink = () => {
if (share?.alias?.alias) {
const link = `${window.location.origin}/s/${share.alias.alias}`;
window.open(link, "_blank");
}
};
const handleLinkGenerated = async () => {
setShowLinkModal(false);
await loadShareDetails();
if (onSuccess) {
onSuccess();
}
};
if (!share) return null;
const shareLink = share?.alias?.alias ? `${window.location.origin}/s/${share.alias.alias}` : null;
const isEditingName = editingField?.field === "name";
const isEditingDescription = editingField?.field === "description";
const displayName = getDisplayValue("name");
const displayDescription = getDisplayValue("description");
return (
<Dialog open={!!shareId} onOpenChange={() => onClose()}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>{t("shareDetails.title")}</DialogTitle>
<DialogDescription>{t("shareDetails.subtitle")}</DialogDescription>
</DialogHeader>
<div className="py-4">
{isLoading ? (
<div className="flex justify-center py-8">
<Loader size="lg" />
</div>
) : (
<div className="flex flex-col gap-6">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-4">
<div className="rounded-lg border p-4">
<h3 className="font-medium">{t("shareDetails.basicInfo")}</h3>
<div className="mt-3 space-y-3">
<div>
<span className="text-sm text-default-500">{t("shareDetails.name")}</span>
<p className="mt-1 font-medium">{share.name || t("shareDetails.untitled")}</p>
</div>
<div>
<span className="text-sm text-default-500">{t("shareDetails.views")}</span>
<p className="mt-1 font-medium">{share.views}</p>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div className="rounded-lg border p-4">
<h3 className="font-medium">{t("shareDetails.dates")}</h3>
<div className="mt-3 space-y-3">
<div>
<span className="text-sm text-default-500">{t("shareDetails.created")}</span>
<p className="mt-1 font-medium">{formatDate(share.createdAt)}</p>
</div>
<div>
<span className="text-sm text-default-500">{t("shareDetails.expires")}</span>
<p className="mt-1 font-medium">
{share.expiration ? formatDate(share.expiration) : t("shareDetails.never")}
</p>
</div>
</div>
</div>
</div>
<>
<Dialog open={!!shareId} onOpenChange={() => onClose()}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>{t("shareDetails.title")}</DialogTitle>
<DialogDescription>{t("shareDetails.subtitle")}</DialogDescription>
</DialogHeader>
<div className="py-4">
{isLoading ? (
<div className="flex justify-center py-8">
<Loader size="lg" />
</div>
) : (
<div className="space-y-4">
{/* Key metrics */}
<div className="grid grid-cols-3 gap-3">
<div className="text-center p-2 bg-muted/30 rounded-lg">
<p className="text-lg font-semibold text-green-600">{share.viewCount || 0}</p>
<p className="text-xs text-muted-foreground">{t("shareDetails.views")}</p>
</div>
<div className="text-center p-2 bg-muted/30 rounded-lg">
<p className="text-lg font-semibold text-green-600">{share.files?.length || 0}</p>
<p className="text-xs text-muted-foreground">{t("shareDetails.files")}</p>
</div>
<div className="text-center p-2 bg-muted/30 rounded-lg">
<p className="text-lg font-semibold text-green-600">{share.recipients?.length || 0}</p>
<p className="text-xs text-muted-foreground">{t("shareDetails.recipients")}</p>
</div>
</div>
<div className="rounded-lg border p-4">
<h3 className="font-medium">{t("shareDetails.security")}</h3>
<div className="mt-3 flex gap-2">
{share.security?.hasPassword ? (
<Badge variant="secondary">
<IconLock className="h-4 w-4" />
{t("shareDetails.passwordProtected")}
</Badge>
{/* Basic Information */}
<div className="space-y-3">
<div className="flex items-center gap-2 border-b pb-2">
<h3 className="text-base font-medium text-foreground">{t("shareDetails.basicInfo")}</h3>
</div>
{/* Name */}
<div>
<div className="flex items-center gap-2 mb-1">
<label className="text-sm font-medium text-muted-foreground">{t("shareDetails.name")}</label>
{onUpdateName && !isEditingName && (
<Button
size="icon"
variant="ghost"
className="h-5 w-5 text-muted-foreground hover:text-foreground"
onClick={() => startEdit("name", displayName || "")}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
{isEditingName ? (
<div className="flex items-center gap-2">
<Input
ref={inputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
className="h-8 flex-1 text-sm"
onClick={(e) => e.stopPropagation()}
/>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-green-600 hover:text-green-700"
onClick={saveEdit}
>
<IconCheck className="h-3 w-3" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-red-600 hover:text-red-700"
onClick={cancelEdit}
>
<IconX className="h-3 w-3" />
</Button>
</div>
) : (
<span className="text-sm font-medium block">{displayName || t("shareDetails.untitled")}</span>
)}
</div>
{/* Description */}
<div>
<div className="flex items-center gap-2 mb-1">
<label className="text-sm font-medium text-muted-foreground">
{t("shareDetails.description")}
</label>
{onUpdateDescription && !isEditingDescription && (
<Button
size="icon"
variant="ghost"
className="h-5 w-5 text-muted-foreground hover:text-foreground"
onClick={() => startEdit("description", displayDescription || "")}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
{isEditingDescription ? (
<div className="flex items-center gap-2">
<Input
ref={inputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
className="h-8 flex-1 text-sm"
placeholder={t("shareDetails.noDescription")}
onClick={(e) => e.stopPropagation()}
/>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-green-600 hover:text-green-700"
onClick={saveEdit}
>
<IconCheck className="h-3 w-3" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-red-600 hover:text-red-700"
onClick={cancelEdit}
>
<IconX className="h-3 w-3" />
</Button>
</div>
) : (
<span className="text-sm block">{displayDescription || t("shareDetails.noDescription")}</span>
)}
</div>
</div>
{/* Share Link Section */}
<div className="space-y-3">
<div className="flex items-center gap-2 border-b pb-2">
<h3 className="text-base font-medium text-foreground">{t("shareDetails.shareLink")}</h3>
</div>
{shareLink ? (
<div className="flex gap-2">
<Input value={shareLink} readOnly className="flex-1 bg-muted/30 text-sm h-8" />
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={handleCopyLink}
title={t("shareDetails.copyLink")}
>
<IconCopy className="h-3.5 w-3.5" />
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={handleOpenLink}
title={t("shareDetails.openLink")}
>
<IconExternalLink className="h-3.5 w-3.5" />
</Button>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => setShowLinkModal(true)}
title={t("shareDetails.editLink")}
>
<IconEdit className="h-3.5 w-3.5" />
</Button>
</div>
) : (
<Badge variant="secondary">
<IconLockOpen className="h-4 w-4" />
{t("shareDetails.publicAccess")}
</Badge>
)}
{share.security?.maxViews && (
<Badge variant="secondary">
{t("shareDetails.maxViews")} {share.security.maxViews}
</Badge>
<div className="flex items-center justify-between p-2 bg-muted/20 rounded-lg">
<p className="text-sm text-muted-foreground">{t("shareDetails.noLink")}</p>
<Button
variant="outline"
size="sm"
onClick={() => setShowLinkModal(true)}
className="gap-1 h-7 text-xs"
>
<IconEdit className="h-3 w-3" />
{t("shareDetails.generateLink")}
</Button>
</div>
)}
</div>
</div>
<div className="rounded-lg border p-4">
<h3 className="font-medium">
{t("shareDetails.files")} ({share.files?.length || 0})
</h3>
<div className="mt-3 flex flex-wrap gap-2">
{share.files?.map((file: ShareFile) => {
const { icon: FileIcon, color } = getFileIcon(file.name);
return (
<Badge key={file.id} variant="secondary">
<FileIcon className={`h-4 w-4 ${color}`} />
{file.name.length > 20 ? file.name.substring(0, 20) + "..." : file.name}
</Badge>
);
})}
</div>
</div>
{/* Dates and Security in Grid */}
<div className="grid grid-cols-2 gap-4">
{/* Dates */}
<div className="space-y-3">
<h3 className="text-base font-medium text-foreground border-b pb-2">{t("shareDetails.dates")}</h3>
<div className="space-y-2">
<div>
<div className="text-xs font-medium text-muted-foreground">{t("shareDetails.created")}</div>
<div className="text-sm">{formatDate(share.createdAt)}</div>
</div>
<div>
<div className="text-xs font-medium text-muted-foreground">{t("shareDetails.expires")}</div>
<div className="text-sm">
{share.expiration ? formatDate(share.expiration) : t("shareDetails.never")}
</div>
</div>
</div>
</div>
<div className="rounded-lg border p-4">
<h3 className="font-medium">
{t("shareDetails.recipients")} ({share.recipients?.length || 0})
</h3>
<div className="mt-3 flex flex-wrap gap-2">
{share.recipients?.map((recipient: ShareRecipient) => (
<Badge key={recipient.id} variant="secondary">
<IconMail className="h-4 w-4" />
{recipient.email}
</Badge>
))}
{/* Security */}
<div className="space-y-3">
<h3 className="text-base font-medium text-foreground border-b pb-2">
{t("shareDetails.security")}
</h3>
<div className="flex flex-col gap-2">
{share.security?.hasPassword ? (
<Badge variant="secondary" className="bg-yellow-500/20 text-yellow-700 border-yellow-200 w-fit">
<IconLock className="h-3 w-3 mr-1" />
{t("shareDetails.passwordProtected")}
</Badge>
) : (
<Badge variant="secondary" className="bg-green-500/20 text-green-700 border-green-200 w-fit">
<IconLockOpen className="h-3 w-3 mr-1" />
{t("shareDetails.publicAccess")}
</Badge>
)}
{share.security?.maxViews && (
<Badge variant="secondary" className="bg-blue-500/20 text-blue-700 border-blue-200 w-fit">
{t("shareDetails.maxViews")} {share.security.maxViews}
</Badge>
)}
</div>
</div>
</div>
{/* Files */}
{share.files && share.files.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2 border-b pb-2">
<h3 className="text-base font-medium text-foreground">{t("shareDetails.files")}</h3>
{onManageFiles && (
<Button
size="icon"
variant="ghost"
className="h-5 w-5 text-muted-foreground hover:text-foreground"
onClick={() => onManageFiles(share)}
title={t("sharesTable.actions.manageFiles")}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
<div className="border rounded-lg bg-muted/10 p-2">
<div className="grid gap-1 max-h-32 overflow-y-auto">
{share.files.map((file: ShareFile) => {
const { icon: FileIcon, color } = getFileIcon(file.name);
return (
<div
key={file.id}
className="flex items-center gap-2 p-2 bg-background rounded border mr-2"
>
<FileIcon className={`h-3.5 w-3.5 ${color} flex-shrink-0`} />
<div className="flex-1 min-w-0">
<div className="text-xs font-medium truncate max-w-[280px]" title={file.name}>
{file.name}
</div>
{file.description && (
<div
className="text-xs text-muted-foreground truncate max-w-[280px]"
title={file.description}
>
{file.description}
</div>
)}
</div>
</div>
);
})}
</div>
</div>
</div>
)}
{/* Recipients */}
{share.recipients && share.recipients.length > 0 && (
<div className="space-y-3">
<h3 className="text-base font-medium text-foreground border-b pb-2">
{t("shareDetails.recipients")}
</h3>
<div className="flex flex-wrap gap-1">
{share.recipients.map((recipient: ShareRecipient) => (
<Badge
key={recipient.id}
variant="secondary"
className="bg-blue-500/20 text-blue-700 border-blue-200 text-xs"
>
<IconMail className="h-3 w-3 mr-1" />
{recipient.email}
</Badge>
))}
</div>
</div>
)}
</div>
</div>
)}
</div>
<DialogFooter>
<Button onClick={onClose}>{t("common.close")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div>
<DialogFooter>
<Button onClick={onClose}>{t("common.close")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{showLinkModal && onGenerateLink && (
<GenerateShareLinkModal
shareId={shareId}
share={share}
onClose={() => setShowLinkModal(false)}
onGenerate={onGenerateLink}
onSuccess={handleLinkGenerated}
/>
)}
</>
);
}

View File

@@ -38,6 +38,7 @@ export function ShareFileModal({ isOpen, file, onClose, onSuccess }: ShareFileMo
const [shareId, setShareId] = useState<string | null>(null);
const [formData, setFormData] = useState({
name: "",
description: "",
password: "",
expiresAt: "",
isPasswordProtected: false,
@@ -51,6 +52,7 @@ export function ShareFileModal({ isOpen, file, onClose, onSuccess }: ShareFileMo
if (isOpen && file) {
setFormData({
name: `${file.name.split(".")[0]}`,
description: "",
password: "",
expiresAt: "",
isPasswordProtected: false,
@@ -72,6 +74,7 @@ export function ShareFileModal({ isOpen, file, onClose, onSuccess }: ShareFileMo
// Criar o compartilhamento
const shareResponse = await createShare({
name: formData.name,
description: formData.description || undefined,
password: formData.isPasswordProtected ? formData.password : undefined,
expiration: formData.expiresAt ? new Date(formData.expiresAt).toISOString() : undefined,
maxViews: formData.maxViews ? parseInt(formData.maxViews) : undefined,
@@ -95,6 +98,7 @@ export function ShareFileModal({ isOpen, file, onClose, onSuccess }: ShareFileMo
setFormData({
name: "",
description: "",
password: "",
expiresAt: "",
isPasswordProtected: false,
@@ -132,6 +136,7 @@ export function ShareFileModal({ isOpen, file, onClose, onSuccess }: ShareFileMo
setGeneratedLink("");
setFormData({
name: "",
description: "",
password: "",
expiresAt: "",
isPasswordProtected: false,
@@ -175,6 +180,15 @@ export function ShareFileModal({ isOpen, file, onClose, onSuccess }: ShareFileMo
/>
</div>
<div className="space-y-2">
<Label>{t("shareFile.descriptionLabel")}</Label>
<Input
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder={t("shareFile.descriptionPlaceholder")}
/>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2">
<IconCalendar size={16} />

View File

@@ -0,0 +1,341 @@
"use client";
import { useEffect, useState } from "react";
import { IconCalendar, IconCopy, IconEye, IconLink, IconLock, IconShare } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Switch } from "@/components/ui/switch";
import { addFiles, createShare, createShareAlias } from "@/http/endpoints";
import { customNanoid } from "@/lib/utils";
interface BulkFile {
id: string;
name: string;
description?: string;
size: number;
objectName: string;
createdAt: string;
updatedAt: string;
}
interface ShareMultipleFilesModalProps {
files: BulkFile[] | null;
isOpen: boolean;
onClose: () => void;
onSuccess: () => void;
}
const generateCustomId = () => customNanoid(10, "0123456789abcdefghijklmnopqrstuvwxyz");
export function ShareMultipleFilesModal({ files, isOpen, onClose, onSuccess }: ShareMultipleFilesModalProps) {
const t = useTranslations();
const [step, setStep] = useState<"create" | "link">("create");
const [shareId, setShareId] = useState<string | null>(null);
const [formData, setFormData] = useState({
name: "",
description: "",
password: "",
expiresAt: "",
isPasswordProtected: false,
maxViews: "",
});
const [alias, setAlias] = useState(() => generateCustomId());
const [generatedLink, setGeneratedLink] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (isOpen && files && files.length > 0) {
setFormData({
name: files.length === 1 ? `${files[0].name.split(".")[0]}` : `${files.length} arquivos compartilhados`,
description: "",
password: "",
expiresAt: "",
isPasswordProtected: false,
maxViews: "",
});
setAlias(generateCustomId());
setStep("create");
setShareId(null);
setGeneratedLink("");
}
}, [isOpen, files]);
const handleCreateShare = async () => {
if (!files || files.length === 0) return;
try {
setIsLoading(true);
// Criar o compartilhamento
const shareResponse = await createShare({
name: formData.name,
description: formData.description || undefined,
password: formData.isPasswordProtected ? formData.password : undefined,
expiration: formData.expiresAt ? new Date(formData.expiresAt).toISOString() : undefined,
maxViews: formData.maxViews ? parseInt(formData.maxViews) : undefined,
files: [],
});
const newShareId = shareResponse.data.share.id;
setShareId(newShareId);
// Adicionar todos os arquivos ao compartilhamento
await addFiles(newShareId, { files: files.map((f) => f.id) });
toast.success(t("createShare.success"));
setStep("link");
} catch (error) {
toast.error(t("createShare.error"));
console.error(error);
} finally {
setIsLoading(false);
}
};
const handleGenerateLink = async () => {
if (!shareId) return;
try {
setIsLoading(true);
await createShareAlias(shareId, { alias });
const link = `${window.location.origin}/s/${alias}`;
setGeneratedLink(link);
toast.success(t("generateShareLink.success"));
} catch (error) {
console.error(error);
toast.error(t("generateShareLink.error"));
} finally {
setIsLoading(false);
}
};
const handleCopyLink = () => {
navigator.clipboard.writeText(generatedLink);
toast.success(t("generateShareLink.copied"));
};
const handleClose = () => {
onClose();
setTimeout(() => {
setStep("create");
setShareId(null);
setGeneratedLink("");
setFormData({
name: "",
description: "",
password: "",
expiresAt: "",
isPasswordProtected: false,
maxViews: "",
});
}, 300);
};
const handleSuccess = () => {
onSuccess();
handleClose();
};
if (!files) return null;
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
const formatFileSize = (bytes: number) => {
const sizes = ["B", "KB", "MB", "GB"];
if (bytes === 0) return "0 B";
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${Math.round((bytes / Math.pow(1024, i)) * 100) / 100} ${sizes[i]}`;
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-md max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{step === "create" ? (
<>
<IconShare size={20} />
{t("shareMultipleFiles.title")}
</>
) : (
<>
<IconLink size={20} />
{t("shareFile.linkTitle")}
</>
)}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-auto">
{step === "create" && (
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("shareMultipleFiles.shareNameLabel")} *</Label>
<Input
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder={t("shareMultipleFiles.shareNamePlaceholder")}
required
/>
</div>
<div className="space-y-2">
<Label>{t("shareMultipleFiles.descriptionLabel")}</Label>
<Input
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
placeholder={t("shareMultipleFiles.descriptionPlaceholder")}
/>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2">
<IconCalendar size={16} />
{t("shareFile.expirationLabel")}
</Label>
<Input
placeholder={t("shareFile.expirationPlaceholder")}
type="datetime-local"
value={formData.expiresAt}
onChange={(e) => setFormData({ ...formData, expiresAt: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2">
<IconEye size={16} />
{t("shareFile.maxViewsLabel")}
</Label>
<Input
min="1"
placeholder={t("shareFile.maxViewsPlaceholder")}
type="number"
value={formData.maxViews}
onChange={(e) => setFormData({ ...formData, maxViews: e.target.value })}
/>
</div>
<div className="flex items-center gap-2">
<Switch
checked={formData.isPasswordProtected}
onCheckedChange={(checked) =>
setFormData({
...formData,
isPasswordProtected: checked,
password: "",
})
}
id="password-protection"
/>
<Label htmlFor="password-protection" className="flex items-center gap-2">
<IconLock size={16} />
{t("shareFile.passwordProtection")}
</Label>
</div>
{formData.isPasswordProtected && (
<div className="space-y-2">
<Label>{t("shareFile.passwordLabel")}</Label>
<Input
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder={t("shareFile.passwordPlaceholder")}
/>
</div>
)}
<div className="space-y-2">
<Label>
{t("shareMultipleFiles.filesToShare")} ({files.length} {t("shareMultipleFiles.files")})
</Label>
<ScrollArea className="h-32 w-full rounded-md border p-2 bg-muted/30">
<div className="space-y-1">
{files.map((file) => (
<div key={file.id} className="flex justify-between items-center text-sm">
<span className="truncate flex-1">{file.name}</span>
<span className="text-muted-foreground ml-2">{formatFileSize(file.size)}</span>
</div>
))}
</div>
</ScrollArea>
<p className="text-xs text-muted-foreground">
{t("shareMultipleFiles.totalSize")}: {formatFileSize(totalSize)}
</p>
</div>
</div>
)}
{step === "link" && (
<div className="space-y-4">
{!generatedLink ? (
<>
<p className="text-sm text-muted-foreground">{t("shareFile.linkDescription")}</p>
<div className="space-y-2">
<Label>{t("shareFile.aliasLabel")}</Label>
<Input
placeholder={t("shareFile.aliasPlaceholder")}
value={alias}
onChange={(e) => setAlias(e.target.value)}
/>
</div>
</>
) : (
<>
<p className="text-sm text-muted-foreground">{t("shareFile.linkReady")}</p>
<Input readOnly value={generatedLink} />
</>
)}
</div>
)}
</div>
<DialogFooter>
{step === "create" && (
<>
<Button variant="outline" onClick={handleClose}>
{t("common.cancel")}
</Button>
<Button
disabled={
isLoading || !formData.name.trim() || (formData.isPasswordProtected && !formData.password.trim())
}
onClick={handleCreateShare}
>
{isLoading ? <div className="animate-spin"></div> : t("shareMultipleFiles.create")}
</Button>
</>
)}
{step === "link" && !generatedLink && (
<>
<Button variant="outline" onClick={() => setStep("create")}>
{t("common.back")}
</Button>
<Button disabled={!alias || isLoading} onClick={handleGenerateLink}>
{isLoading ? <div className="animate-spin"></div> : t("shareFile.generateLink")}
</Button>
</>
)}
{step === "link" && generatedLink && (
<>
<Button variant="outline" onClick={handleSuccess}>
{t("common.close")}
</Button>
<Button onClick={handleCopyLink}>
<IconCopy className="h-4 w-4 mr-2" />
{t("shareFile.copyLink")}
</Button>
</>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from "react";
import {
IconCheck,
IconChevronDown,
IconDotsVertical,
IconDownload,
IconEdit,
@@ -12,6 +13,7 @@ import {
import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuContent,
@@ -42,6 +44,10 @@ interface FilesTableProps {
onDownload: (objectName: string, fileName: string) => void;
onShare: (file: File) => void;
onDelete: (file: File) => void;
onBulkDelete?: (files: File[]) => void;
onBulkShare?: (files: File[]) => void;
onBulkDownload?: (files: File[]) => void;
setClearSelectionCallback?: (callback: () => void) => void;
}
export function FilesTable({
@@ -53,6 +59,10 @@ export function FilesTable({
onDownload,
onShare,
onDelete,
onBulkDelete,
onBulkShare,
onBulkDownload,
setClearSelectionCallback,
}: FilesTableProps) {
const t = useTranslations();
const [editingField, setEditingField] = useState<{ fileId: string; field: "name" | "description" } | null>(null);
@@ -61,6 +71,7 @@ export function FilesTable({
const [pendingChanges, setPendingChanges] = useState<{ [fileId: string]: { name?: string; description?: string } }>(
{}
);
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
@@ -75,6 +86,17 @@ export function FilesTable({
setPendingChanges({});
}, [files]);
// Clear selected files when files array changes
useEffect(() => {
setSelectedFiles(new Set());
}, [files]);
// Register clearSelection callback with parent
useEffect(() => {
const clearSelection = () => setSelectedFiles(new Set());
setClearSelectionCallback?.(clearSelection);
}, [setClearSelectionCallback]);
const splitFileName = (fullName: string) => {
const lastDotIndex = fullName.lastIndexOf(".");
return lastDotIndex === -1
@@ -162,60 +184,239 @@ export function FilesTable({
return field === "name" ? file.name : file.description;
};
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedFiles(new Set(files.map((file) => file.id)));
} else {
setSelectedFiles(new Set());
}
};
const handleSelectFile = (fileId: string, checked: boolean) => {
const newSelected = new Set(selectedFiles);
if (checked) {
newSelected.add(fileId);
} else {
newSelected.delete(fileId);
}
setSelectedFiles(newSelected);
};
const getSelectedFiles = () => {
return files.filter((file) => selectedFiles.has(file.id));
};
const isAllSelected = files.length > 0 && selectedFiles.size === files.length;
const handleBulkAction = (action: "delete" | "share" | "download") => {
const selectedFileObjects = getSelectedFiles();
if (selectedFileObjects.length === 0) return;
switch (action) {
case "delete":
if (onBulkDelete) {
onBulkDelete(selectedFileObjects);
}
break;
case "share":
if (onBulkShare) {
onBulkShare(selectedFileObjects);
}
break;
case "download":
if (onBulkDownload) {
onBulkDownload(selectedFileObjects);
}
break;
}
// Don't clear selection here - let the individual handlers do it after their actions complete
};
const showBulkActions = selectedFiles.size > 0 && (onBulkDelete || onBulkShare || onBulkDownload);
return (
<div className="rounded-lg shadow-sm overflow-hidden border">
<Table>
<TableHeader>
<TableRow className="border-b-0">
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4 rounded-tl-lg">
{t("filesTable.columns.name")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("filesTable.columns.description")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("filesTable.columns.size")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("filesTable.columns.createdAt")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("filesTable.columns.updatedAt")}
</TableHead>
<TableHead className="h-10 w-[70px] text-xs font-bold text-muted-foreground bg-muted/50 px-4 rounded-tr-lg">
{t("filesTable.columns.actions")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{files.map((file) => {
const { icon: FileIcon, color } = getFileIcon(file.name);
const isEditingName = editingField?.fileId === file.id && editingField?.field === "name";
const isEditingDescription = editingField?.fileId === file.id && editingField?.field === "description";
const isHoveringName = hoveredField?.fileId === file.id && hoveredField?.field === "name";
const isHoveringDescription = hoveredField?.fileId === file.id && hoveredField?.field === "description";
<div className="space-y-4">
{showBulkActions && (
<div className="flex items-center justify-between p-4 bg-muted/30 border rounded-lg">
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-foreground">
{t("filesTable.bulkActions.selected", { count: selectedFiles.size })}
</span>
</div>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" size="sm" className="gap-2">
{t("filesTable.bulkActions.actions")}
<IconChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
{onBulkDownload && (
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => handleBulkAction("download")}>
<IconDownload className="mr-2 h-4 w-4" />
{t("filesTable.bulkActions.download")}
</DropdownMenuItem>
)}
{onBulkShare && (
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => handleBulkAction("share")}>
<IconShare className="mr-2 h-4 w-4" />
{t("filesTable.bulkActions.share")}
</DropdownMenuItem>
)}
{onBulkDelete && (
<DropdownMenuItem
onClick={() => handleBulkAction("delete")}
className="cursor-pointer py-2 text-destructive focus:text-destructive"
>
<IconTrash className="mr-2 h-4 w-4" />
{t("filesTable.bulkActions.delete")}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<Button variant="outline" size="sm" onClick={() => setSelectedFiles(new Set())}>
{t("common.cancel")}
</Button>
</div>
</div>
)}
const displayName = getDisplayValue(file, "name") || file.name;
const displayDescription = getDisplayValue(file, "description");
<div className="rounded-lg shadow-sm overflow-hidden border">
<Table>
<TableHeader>
<TableRow className="border-b-0">
<TableHead className="h-10 w-[50px] text-xs font-bold text-muted-foreground bg-muted/50 px-4 rounded-tl-lg">
<Checkbox
checked={isAllSelected}
onCheckedChange={handleSelectAll}
aria-label={t("filesTable.selectAll")}
/>
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("filesTable.columns.name")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("filesTable.columns.description")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("filesTable.columns.size")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("filesTable.columns.createdAt")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("filesTable.columns.updatedAt")}
</TableHead>
<TableHead className="h-10 w-[70px] text-xs font-bold text-muted-foreground bg-muted/50 px-4 rounded-tr-lg">
{t("filesTable.columns.actions")}
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{files.map((file) => {
const { icon: FileIcon, color } = getFileIcon(file.name);
const isEditingName = editingField?.fileId === file.id && editingField?.field === "name";
const isEditingDescription = editingField?.fileId === file.id && editingField?.field === "description";
const isHoveringName = hoveredField?.fileId === file.id && hoveredField?.field === "name";
const isHoveringDescription = hoveredField?.fileId === file.id && hoveredField?.field === "description";
const isSelected = selectedFiles.has(file.id);
return (
<TableRow key={file.id} className="hover:bg-muted/50 transition-colors border-0">
<TableCell className="h-12 px-4 border-0">
<div className="flex items-center gap-2">
<FileIcon className={`h-5 w-5 ${color}`} />
const displayName = getDisplayValue(file, "name") || file.name;
const displayDescription = getDisplayValue(file, "description");
return (
<TableRow key={file.id} className="hover:bg-muted/50 transition-colors border-0">
<TableCell className="h-12 px-4 border-0">
<Checkbox
checked={isSelected}
onCheckedChange={(checked: boolean) => handleSelectFile(file.id, checked)}
aria-label={t("filesTable.selectFile", { fileName: file.name })}
/>
</TableCell>
<TableCell className="h-12 px-4 border-0">
<div className="flex items-center gap-2">
<FileIcon className={`h-5 w-5 ${color}`} />
<div
className="flex items-center gap-1 min-w-0 flex-1"
onMouseEnter={() => setHoveredField({ fileId: file.id, field: "name" })}
onMouseLeave={() => setHoveredField(null)}
>
{isEditingName ? (
<div className="flex items-center gap-1 flex-1">
<Input
ref={inputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
className="h-8 text-sm font-medium"
onClick={(e) => e.stopPropagation()}
/>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-green-600 hover:text-green-700 flex-shrink-0"
onClick={(e) => {
e.stopPropagation();
saveEdit();
}}
>
<IconCheck className="h-3 w-3" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-red-600 hover:text-red-700 flex-shrink-0"
onClick={(e) => {
e.stopPropagation();
cancelEdit();
}}
>
<IconX className="h-3 w-3" />
</Button>
</div>
) : (
<div className="flex items-center gap-1 flex-1 min-w-0">
<span className="truncate max-w-[200px] font-medium" title={displayName}>
{displayName}
</span>
<div className="w-6 flex justify-center flex-shrink-0">
{isHoveringName && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-muted-foreground hover:text-foreground hidden sm:block"
onClick={(e) => {
e.stopPropagation();
startEdit(file.id, "name", displayName);
}}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
</div>
)}
</div>
</div>
</TableCell>
<TableCell className="h-12 px-4">
<div
className="flex items-center gap-1 min-w-0 flex-1"
onMouseEnter={() => setHoveredField({ fileId: file.id, field: "name" })}
className="flex items-center gap-1"
onMouseEnter={() => setHoveredField({ fileId: file.id, field: "description" })}
onMouseLeave={() => setHoveredField(null)}
>
{isEditingName ? (
{isEditingDescription ? (
<div className="flex items-center gap-1 flex-1">
<Input
ref={inputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
className="h-8 text-sm font-medium"
placeholder="Add description..."
className="h-8 text-sm"
onClick={(e) => e.stopPropagation()}
/>
<Button
@@ -243,18 +444,21 @@ export function FilesTable({
</div>
) : (
<div className="flex items-center gap-1 flex-1 min-w-0">
<span className="truncate max-w-[200px] font-medium" title={displayName}>
{displayName}
<span
className="text-muted-foreground truncate max-w-[150px]"
title={displayDescription || "-"}
>
{displayDescription || "-"}
</span>
<div className="w-6 flex justify-center flex-shrink-0">
{isHoveringName && (
{isHoveringDescription && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-muted-foreground hover:text-foreground hidden sm:block"
onClick={(e) => {
e.stopPropagation();
startEdit(file.id, "name", displayName);
startEdit(file.id, "description", displayDescription || "");
}}
>
<IconEdit className="h-3 w-3" />
@@ -264,116 +468,54 @@ export function FilesTable({
</div>
)}
</div>
</div>
</TableCell>
<TableCell className="h-12 px-4">
<div
className="flex items-center gap-1"
onMouseEnter={() => setHoveredField({ fileId: file.id, field: "description" })}
onMouseLeave={() => setHoveredField(null)}
>
{isEditingDescription ? (
<div className="flex items-center gap-1 flex-1">
<Input
ref={inputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Add description..."
className="h-8 text-sm"
onClick={(e) => e.stopPropagation()}
/>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-green-600 hover:text-green-700 flex-shrink-0"
onClick={(e) => {
e.stopPropagation();
saveEdit();
}}
>
<IconCheck className="h-3 w-3" />
</TableCell>
<TableCell className="h-12 px-4">{formatFileSize(file.size)}</TableCell>
<TableCell className="h-12 px-4">{formatDateTime(file.createdAt)}</TableCell>
<TableCell className="h-12 px-4">{formatDateTime(file.updatedAt || file.createdAt)}</TableCell>
<TableCell className="h-12 px-4 text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-muted cursor-pointer">
<IconDotsVertical className="h-4 w-4" />
<span className="sr-only">{t("filesTable.actions.menu")}</span>
</Button>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-red-600 hover:text-red-700 flex-shrink-0"
onClick={(e) => {
e.stopPropagation();
cancelEdit();
}}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onPreview(file)}>
<IconEye className="mr-2 h-4 w-4" />
{t("filesTable.actions.preview")}
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onRename(file)}>
<IconEdit className="mr-2 h-4 w-4" />
{t("filesTable.actions.edit")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer py-2"
onClick={() => onDownload(file.objectName, file.name)}
>
<IconX className="h-3 w-3" />
</Button>
</div>
) : (
<div className="flex items-center gap-1 flex-1 min-w-0">
<span className="flex-1 text-muted-foreground truncate">{displayDescription || "-"}</span>
<div className="w-6 flex justify-center flex-shrink-0">
{isHoveringDescription && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-muted-foreground hover:text-foreground hidden sm:block"
onClick={(e) => {
e.stopPropagation();
startEdit(file.id, "description", displayDescription || "");
}}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
</div>
)}
</div>
</TableCell>
<TableCell className="h-12 px-4">{formatFileSize(file.size)}</TableCell>
<TableCell className="h-12 px-4">{formatDateTime(file.createdAt)}</TableCell>
<TableCell className="h-12 px-4">{formatDateTime(file.updatedAt || file.createdAt)}</TableCell>
<TableCell className="h-12 px-4 text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 hover:bg-muted cursor-pointer">
<IconDotsVertical className="h-4 w-4" />
<span className="sr-only">{t("filesTable.actions.menu")}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onPreview(file)}>
<IconEye className="mr-2 h-4 w-4" />
{t("filesTable.actions.preview")}
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onRename(file)}>
<IconEdit className="mr-2 h-4 w-4" />
{t("filesTable.actions.edit")}
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer py-2"
onClick={() => onDownload(file.objectName, file.name)}
>
<IconDownload className="mr-2 h-4 w-4" />
{t("filesTable.actions.download")}
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onShare(file)}>
<IconShare className="mr-2 h-4 w-4" />
{t("filesTable.actions.share")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onDelete(file)}
className="cursor-pointer py-2 text-destructive focus:text-destructive"
>
<IconTrash className="mr-2 h-4 w-4" />
{t("filesTable.actions.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<IconDownload className="mr-2 h-4 w-4" />
{t("filesTable.actions.download")}
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onShare(file)}>
<IconShare className="mr-2 h-4 w-4" />
{t("filesTable.actions.share")}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onDelete(file)}
className="cursor-pointer py-2 text-destructive focus:text-destructive"
>
<IconTrash className="mr-2 h-4 w-4" />
{t("filesTable.actions.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -34,6 +34,7 @@ export interface SharesTableProps {
onDelete: (share: any) => void;
onEdit: (share: any) => void;
onUpdateName: (shareId: string, newName: string) => void;
onUpdateDescription: (shareId: string, newDescription: string) => void;
onManageFiles: (share: any) => void;
onManageRecipients: (share: any) => void;
onViewDetails: (share: any) => void;
@@ -47,6 +48,7 @@ export function SharesTable({
onDelete,
onEdit,
onUpdateName,
onUpdateDescription,
onManageFiles,
onManageRecipients,
onViewDetails,
@@ -56,45 +58,54 @@ export function SharesTable({
}: SharesTableProps) {
const t = useTranslations();
const { smtpEnabled } = useShareContext();
const [editingShareId, setEditingShareId] = useState<string | null>(null);
const [editingField, setEditingField] = useState<{ shareId: string; field: "name" | "description" } | null>(null);
const [editValue, setEditValue] = useState("");
const [hoveredShareId, setHoveredShareId] = useState<string | null>(null);
const [pendingChanges, setPendingChanges] = useState<{ [shareId: string]: { name?: string } }>({});
const [hoveredField, setHoveredField] = useState<{ shareId: string; field: "name" | "description" } | null>(null);
const [pendingChanges, setPendingChanges] = useState<{ [shareId: string]: { name?: string; description?: string } }>(
{}
);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (editingShareId && inputRef.current) {
if (editingField && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [editingShareId]);
}, [editingField]);
// Clear pending changes when shares are updated
useEffect(() => {
setPendingChanges({});
}, [shares]);
const startEdit = (shareId: string, currentName: string) => {
setEditingShareId(shareId);
setEditValue(currentName);
const startEdit = (shareId: string, field: "name" | "description", currentValue: string) => {
setEditingField({ shareId, field });
setEditValue(currentValue || "");
};
const saveEdit = () => {
if (!editingShareId) return;
if (!editingField) return;
const { shareId, field } = editingField;
// Update local state optimistically
setPendingChanges((prev) => ({
...prev,
[editingShareId]: { name: editValue },
[shareId]: { ...prev[shareId], [field]: editValue },
}));
onUpdateName(editingShareId, editValue);
setEditingShareId(null);
if (field === "name") {
onUpdateName(shareId, editValue);
} else {
onUpdateDescription(shareId, editValue);
}
setEditingField(null);
setEditValue("");
};
const cancelEdit = () => {
setEditingShareId(null);
setEditingField(null);
setEditValue("");
};
@@ -106,12 +117,12 @@ export function SharesTable({
}
};
const getDisplayName = (share: any) => {
const getDisplayValue = (share: any, field: "name" | "description") => {
const pendingChange = pendingChanges[share.id];
if (pendingChange && pendingChange.name !== undefined) {
return pendingChange.name;
if (pendingChange && pendingChange[field] !== undefined) {
return pendingChange[field];
}
return share.name;
return field === "name" ? share.name : share.description;
};
return (
@@ -122,6 +133,9 @@ export function SharesTable({
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("sharesTable.columns.name")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("sharesTable.columns.description")}
</TableHead>
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50 px-4">
{t("sharesTable.columns.createdAt")}
</TableHead>
@@ -147,65 +161,135 @@ export function SharesTable({
</TableHeader>
<TableBody>
{shares.map((share) => {
const isEditing = editingShareId === share.id;
const isHovering = hoveredShareId === share.id;
const displayName = getDisplayName(share);
const isEditingName = editingField?.shareId === share.id && editingField?.field === "name";
const isEditingDescription = editingField?.shareId === share.id && editingField?.field === "description";
const isHoveringName = hoveredField?.shareId === share.id && hoveredField?.field === "name";
const isHoveringDescription = hoveredField?.shareId === share.id && hoveredField?.field === "description";
const displayName = getDisplayValue(share, "name");
const displayDescription = getDisplayValue(share, "description");
return (
<TableRow key={share.id} className="hover:bg-muted/50 transition-colors border-0">
<TableCell className="h-12 px-4 border-0">
<div
className="flex items-center gap-1 min-w-0"
onMouseEnter={() => setHoveredShareId(share.id)}
onMouseLeave={() => setHoveredShareId(null)}
onMouseEnter={() => setHoveredField({ shareId: share.id, field: "name" })}
onMouseLeave={() => setHoveredField(null)}
>
{isEditing ? (
{isEditingName ? (
<div className="flex items-center gap-1 flex-1">
<Input
ref={inputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
className="h-8 text-sm font-medium"
className="h-8 text-sm font-medium min-w-[200px]"
onClick={(e) => e.stopPropagation()}
/>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-green-600 hover:text-green-700 flex-shrink-0"
className="h-8 w-8 text-green-600 hover:text-green-700 flex-shrink-0"
onClick={(e) => {
e.stopPropagation();
saveEdit();
}}
>
<IconCheck className="h-3 w-3" />
<IconCheck className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-red-600 hover:text-red-700 flex-shrink-0"
className="h-8 w-8 text-red-600 hover:text-red-700 flex-shrink-0"
onClick={(e) => {
e.stopPropagation();
cancelEdit();
}}
>
<IconX className="h-3 w-3" />
<IconX className="h-4 w-4" />
</Button>
</div>
) : (
<div className="flex items-center gap-1 flex-1 min-w-0">
<span className="truncate max-w-[200px] font-medium" title={displayName}>
<span className="truncate max-w-[120px] font-medium" title={displayName}>
{displayName}
</span>
<div className="w-6 flex justify-center flex-shrink-0">
{isHovering && (
{isHoveringName && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-muted-foreground hover:text-foreground hidden sm:block"
onClick={(e) => {
e.stopPropagation();
startEdit(share.id, displayName);
startEdit(share.id, "name", displayName);
}}
>
<IconEdit className="h-3 w-3" />
</Button>
)}
</div>
</div>
)}
</div>
</TableCell>
<TableCell className="h-12 px-4">
<div
className="flex items-center gap-1 min-w-0"
onMouseEnter={() => setHoveredField({ shareId: share.id, field: "description" })}
onMouseLeave={() => setHoveredField(null)}
>
{isEditingDescription ? (
<div className="flex items-center gap-1 flex-1">
<Input
ref={inputRef}
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
onKeyDown={handleKeyDown}
className="h-8 text-sm min-w-[250px]"
placeholder="Adicionar descrição..."
onClick={(e) => e.stopPropagation()}
/>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-green-600 hover:text-green-700 flex-shrink-0"
onClick={(e) => {
e.stopPropagation();
saveEdit();
}}
>
<IconCheck className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-red-600 hover:text-red-700 flex-shrink-0"
onClick={(e) => {
e.stopPropagation();
cancelEdit();
}}
>
<IconX className="h-4 w-4" />
</Button>
</div>
) : (
<div className="flex items-center gap-1 flex-1 min-w-0">
<span
className="text-muted-foreground truncate max-w-[100px]"
title={displayDescription || "-"}
>
{displayDescription || "-"}
</span>
<div className="w-6 flex justify-center flex-shrink-0">
{isHoveringDescription && (
<Button
size="icon"
variant="ghost"
className="h-6 w-6 text-muted-foreground hover:text-foreground hidden sm:block"
onClick={(e) => {
e.stopPropagation();
startEdit(share.id, "description", displayDescription || "");
}}
>
<IconEdit className="h-3 w-3" />

View File

@@ -0,0 +1,29 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { cn } from "@/lib/utils";
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"cursor-pointer peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };

View File

@@ -0,0 +1,23 @@
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
);
});
Textarea.displayName = "Textarea";
export { Textarea };

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useCallback, useState } from "react";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
@@ -30,33 +30,68 @@ interface FileToShare {
updatedAt: string;
}
interface BulkFile {
id: string;
name: string;
description?: string;
size: number;
objectName: string;
createdAt: string;
updatedAt: string;
}
export interface FileManagerHook {
previewFile: PreviewFile | null;
fileToDelete: any;
fileToRename: any;
fileToShare: FileToShare | null;
filesToDelete: BulkFile[] | null;
filesToShare: BulkFile[] | null;
filesToDownload: BulkFile[] | null;
isBulkDownloadModalOpen: boolean;
setFileToDelete: (file: any) => void;
setFileToRename: (file: any) => void;
setPreviewFile: (file: PreviewFile | null) => void;
setFileToShare: (file: FileToShare | null) => void;
setFilesToDelete: (files: BulkFile[] | null) => void;
setFilesToShare: (files: BulkFile[] | null) => void;
setFilesToDownload: (files: BulkFile[] | null) => void;
setBulkDownloadModalOpen: (open: boolean) => void;
handleDelete: (fileId: string) => Promise<void>;
handleDownload: (objectName: string, fileName: string) => Promise<void>;
handleRename: (fileId: string, newName: string, description?: string) => Promise<void>;
handleBulkDelete: (files: BulkFile[]) => void;
handleBulkShare: (files: BulkFile[]) => void;
handleBulkDownload: (files: BulkFile[]) => void;
handleBulkDownloadWithZip: (files: BulkFile[], zipName: string) => Promise<void>;
handleDeleteBulk: () => Promise<void>;
handleShareBulkSuccess: () => void;
clearSelection?: () => void;
setClearSelectionCallback?: (callback: () => void) => void;
}
export function useFileManager(onRefresh: () => Promise<void>) {
export function useFileManager(onRefresh: () => Promise<void>, clearSelection?: () => void) {
const t = useTranslations();
const [previewFile, setPreviewFile] = useState<PreviewFile | null>(null);
const [fileToRename, setFileToRename] = useState<FileToRename | null>(null);
const [fileToDelete, setFileToDelete] = useState<FileToDelete | null>(null);
const [fileToShare, setFileToShare] = useState<FileToShare | null>(null);
const [filesToDelete, setFilesToDelete] = useState<BulkFile[] | null>(null);
const [filesToShare, setFilesToShare] = useState<BulkFile[] | null>(null);
const [filesToDownload, setFilesToDownload] = useState<BulkFile[] | null>(null);
const [isBulkDownloadModalOpen, setBulkDownloadModalOpen] = useState(false);
const [clearSelectionCallback, setClearSelectionCallbackState] = useState<(() => void) | null>(null);
const setClearSelectionCallback = useCallback((callback: () => void) => {
setClearSelectionCallbackState(() => callback);
}, []);
const handleDownload = async (objectName: string, fileName: string) => {
try {
const encodedObjectName = encodeURIComponent(objectName);
const response = await getDownloadUrl(encodedObjectName);
const downloadUrl = response.data.url;
console.log(fileName);
const link = document.createElement("a");
link.href = downloadUrl;
link.download = fileName;
@@ -66,7 +101,7 @@ export function useFileManager(onRefresh: () => Promise<void>) {
toast.success(t("files.downloadStart"));
} catch (error) {
console.error(error);
toast.success(t("files.downloadError"));
toast.error(t("files.downloadError"));
}
};
@@ -97,6 +132,107 @@ export function useFileManager(onRefresh: () => Promise<void>) {
}
};
const handleBulkDelete = (files: BulkFile[]) => {
setFilesToDelete(files);
};
const handleBulkShare = (files: BulkFile[]) => {
setFilesToShare(files);
};
const handleShareBulkSuccess = () => {
setFilesToShare(null);
// Clear selection after successful share
if (clearSelectionCallback) {
clearSelectionCallback();
}
};
const handleBulkDownload = (files: BulkFile[]) => {
setFilesToDownload(files);
setBulkDownloadModalOpen(true);
};
const handleBulkDownloadWithZip = async (files: BulkFile[], zipName: string) => {
try {
toast.promise(
(async () => {
// Dynamically import JSZip
const JSZip = (await import("jszip")).default;
const zip = new JSZip();
// Download all files and add to zip
const downloadPromises = files.map(async (file) => {
try {
const encodedObjectName = encodeURIComponent(file.objectName);
const downloadResponse = await getDownloadUrl(encodedObjectName);
const downloadUrl = downloadResponse.data.url;
const response = await fetch(downloadUrl);
if (!response.ok) {
throw new Error(`Failed to download ${file.name}`);
}
const blob = await response.blob();
zip.file(file.name, blob);
} catch (error) {
console.error(`Error downloading file ${file.name}:`, error);
throw error;
}
});
await Promise.all(downloadPromises);
// Generate ZIP blob
const zipBlob = await zip.generateAsync({ type: "blob" });
// Download ZIP file
const url = URL.createObjectURL(zipBlob);
const a = document.createElement("a");
a.href = url;
a.download = zipName.endsWith(".zip") ? zipName : `${zipName}.zip`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// Clear selection after successful download
if (clearSelectionCallback) {
clearSelectionCallback();
}
})(),
{
loading: "Criando arquivo ZIP...",
success: "Arquivo ZIP baixado com sucesso!",
error: "Erro ao criar arquivo ZIP",
}
);
} catch (error) {
console.error("Error creating ZIP:", error);
}
};
const handleDeleteBulk = async () => {
if (!filesToDelete) return;
try {
const deletePromises = filesToDelete.map((file) => deleteFile(file.id));
await Promise.all(deletePromises);
toast.success(t("files.bulkDeleteSuccess", { count: filesToDelete.length }));
setFilesToDelete(null);
onRefresh();
// Clear selection after successful deletion
if (clearSelectionCallback) {
clearSelectionCallback();
}
} catch (error) {
console.error("Failed to delete files:", error);
toast.error(t("files.bulkDeleteError"));
}
};
return {
previewFile,
setPreviewFile,
@@ -106,8 +242,24 @@ export function useFileManager(onRefresh: () => Promise<void>) {
setFileToDelete,
fileToShare,
setFileToShare,
filesToDelete,
setFilesToDelete,
filesToShare,
setFilesToShare,
filesToDownload,
setFilesToDownload,
isBulkDownloadModalOpen,
setBulkDownloadModalOpen,
handleDownload,
handleRename,
handleDelete,
handleBulkDelete,
handleBulkShare,
handleBulkDownload,
handleBulkDownloadWithZip,
handleDeleteBulk,
handleShareBulkSuccess,
clearSelection,
setClearSelectionCallback: setClearSelectionCallback,
};
}

View File

@@ -30,6 +30,7 @@ export interface ShareManagerHook {
handleDelete: (shareId: string) => Promise<void>;
handleEdit: (shareId: string, data: any) => Promise<void>;
handleUpdateName: (shareId: string, newName: string) => Promise<void>;
handleUpdateDescription: (shareId: string, newDescription: string) => Promise<void>;
handleManageFiles: (shareId: string, files: any[]) => Promise<void>;
handleManageRecipients: (shareId: string, recipients: any[]) => Promise<void>;
handleGenerateLink: (shareId: string, alias: string) => Promise<void>;
@@ -80,6 +81,17 @@ export function useShareManager(onSuccess: () => void) {
}
};
const handleUpdateDescription = async (shareId: string, newDescription: string) => {
try {
await updateShare({ id: shareId, description: newDescription });
await onSuccess();
toast.success(t("shareManager.updateSuccess"));
} catch (error) {
toast.error(t("shareManager.updateError"));
console.error(error);
}
};
const handleManageFiles = async (shareId: string, files: string[]) => {
try {
await addFiles(shareId, { files });
@@ -147,6 +159,7 @@ export function useShareManager(onSuccess: () => void) {
handleDelete,
handleEdit,
handleUpdateName,
handleUpdateDescription,
handleManageFiles,
handleManageRecipients,
handleGenerateLink,