mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
feat(dashboard): add dashboard components and utilities
This commit introduces new components and utilities for the dashboard, including storage usage, quick access cards, recent files, and recent shares. It also adds file and share management hooks, along with new UI components like progress bars, separators, and avatars. The changes enhance the dashboard's functionality and improve user experience by providing quick access to essential features and better visual feedback. The commit includes: - New components for storage usage, quick access, recent files, and shares - File and share management hooks for CRUD operations - Utility functions for file size formatting and file icons - New UI components like progress bars, separators, and avatars - Updated translations and styles for consistency
This commit is contained in:
@@ -203,8 +203,8 @@
|
||||
"protected": "محمي",
|
||||
"public": "عام"
|
||||
},
|
||||
"filesCount": "{{count}} ملف",
|
||||
"recipientsCount": "{{count}} مستلم",
|
||||
"filesCount": "ملف",
|
||||
"recipientsCount": "مستلم",
|
||||
"actions": {
|
||||
"menu": "قائمة إجراءات المشاركة",
|
||||
"edit": "تعديل",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "استخدام التخزين",
|
||||
"ariaLabel": "شريط تقدم استخدام التخزين",
|
||||
"used": "المستخدمة: {{size}}",
|
||||
"available": "المتاحة: {{size}}"
|
||||
"used": "المستخدمة",
|
||||
"available": "المتاحة"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "فشل في تحميل بيانات لوحة التحكم",
|
||||
|
@@ -203,8 +203,8 @@
|
||||
"protected": "Geschützt",
|
||||
"public": "Öffentlich"
|
||||
},
|
||||
"filesCount": "{{count}} Dateien",
|
||||
"recipientsCount": "{{count}} Empfänger",
|
||||
"filesCount": "Dateien",
|
||||
"recipientsCount": "Empfänger",
|
||||
"actions": {
|
||||
"menu": "Freigabeaktionsmenü",
|
||||
"edit": "Bearbeiten",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "Speichernutzung",
|
||||
"ariaLabel": "Fortschrittsbalken der Speichernutzung",
|
||||
"used": "{{size}} genutzt",
|
||||
"available": "{{size}} verfügbar"
|
||||
"used": "genutzt",
|
||||
"available": "verfügbar"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Fehler beim Laden der Dashboard-Daten",
|
||||
|
@@ -203,8 +203,8 @@
|
||||
"protected": "Protected",
|
||||
"public": "Public"
|
||||
},
|
||||
"filesCount": "{{count}} files",
|
||||
"recipientsCount": "{{count}} recipients",
|
||||
"filesCount": "files",
|
||||
"recipientsCount": "recipients",
|
||||
"actions": {
|
||||
"menu": "Share actions menu",
|
||||
"edit": "Edit",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "Storage Usage",
|
||||
"ariaLabel": "Storage usage progress bar",
|
||||
"used": "{{size}} used",
|
||||
"available": "{{size}} available"
|
||||
"used": "used",
|
||||
"available": "available"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Failed to load dashboard data",
|
||||
|
@@ -203,8 +203,8 @@
|
||||
"protected": "Protegida",
|
||||
"public": "Pública"
|
||||
},
|
||||
"filesCount": "{{count}} archivos",
|
||||
"recipientsCount": "{{count}} destinatarios",
|
||||
"filesCount": "archivos",
|
||||
"recipientsCount": "destinatarios",
|
||||
"actions": {
|
||||
"menu": "Menú de acciones de la compartición",
|
||||
"edit": "Editar",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "Uso de almacenamiento",
|
||||
"ariaLabel": "Barra de progreso del uso de almacenamiento",
|
||||
"used": "{{size}} usados",
|
||||
"available": "{{size}} disponibles"
|
||||
"used": "usados",
|
||||
"available": "disponibles"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Error al cargar los datos del tablero",
|
||||
|
@@ -203,8 +203,8 @@
|
||||
"protected": "Protégé",
|
||||
"public": "Public"
|
||||
},
|
||||
"filesCount": "{{count}} fichiers",
|
||||
"recipientsCount": "{{count}} destinataires",
|
||||
"filesCount": "fichiers",
|
||||
"recipientsCount": "destinataires",
|
||||
"actions": {
|
||||
"menu": "Menu d'actions du partage",
|
||||
"edit": "Modifier",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "Utilisation du Stockage",
|
||||
"ariaLabel": "Barre de progression de l'utilisation du stockage",
|
||||
"used": "{{size}} utilisé",
|
||||
"available": "{{size}} disponible"
|
||||
"used": "utilisé",
|
||||
"available": "disponible"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Échec du chargement des données du tableau de bord",
|
||||
|
@@ -203,8 +203,8 @@
|
||||
"protected": "संरक्षित",
|
||||
"public": "सार्वजनिक"
|
||||
},
|
||||
"filesCount": "{{count}} फाइलें",
|
||||
"recipientsCount": "{{count}} प्राप्तकर्ता",
|
||||
"filesCount": "फाइलें",
|
||||
"recipientsCount": "प्राप्तकर्ता",
|
||||
"actions": {
|
||||
"menu": "साझाकरण क्रिया मेनू",
|
||||
"edit": "संपादित करें",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "स्टोरेज उपयोग",
|
||||
"ariaLabel": "स्टोरेज उपयोग प्रगति पट्टी",
|
||||
"used": "{{size}} उपयोग किया गया",
|
||||
"available": "{{size}} उपलब्ध"
|
||||
"used": "उपयोग किया गया",
|
||||
"available": "उपलब्ध"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "डैशबोर्ड डेटा लोड करने में त्रुटि",
|
||||
|
@@ -203,8 +203,8 @@
|
||||
"protected": "保護済み",
|
||||
"public": "公開"
|
||||
},
|
||||
"filesCount": "{{count}} ファイル",
|
||||
"recipientsCount": "{{count}} 受信者",
|
||||
"filesCount": "ファイル",
|
||||
"recipientsCount": "受信者",
|
||||
"actions": {
|
||||
"menu": "共有操作メニュー",
|
||||
"edit": "編集",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "ストレージ使用量",
|
||||
"ariaLabel": "ストレージ使用状況のプログレスバー",
|
||||
"used": "{{size}} 使用済み",
|
||||
"available": "{{size}} 利用可能"
|
||||
"used": "使用済み",
|
||||
"available": "利用可能"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "ダッシュボードデータの読み込みに失敗しました",
|
||||
|
@@ -203,8 +203,8 @@
|
||||
"protected": "보호됨",
|
||||
"public": "공개"
|
||||
},
|
||||
"filesCount": "{{count}}개의 파일",
|
||||
"recipientsCount": "{{count}}명의 수신자",
|
||||
"filesCount": "개의 파일",
|
||||
"recipientsCount": "명의 수신자",
|
||||
"actions": {
|
||||
"menu": "공유 작업 메뉴",
|
||||
"edit": "편집",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "스토리지 사용량",
|
||||
"ariaLabel": "스토리지 사용량 진행 바",
|
||||
"used": "{{size}} 사용됨",
|
||||
"available": "{{size}} 사용 가능"
|
||||
"used": "사용됨",
|
||||
"available": "사용 가능"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "대시보드 데이터를 불러오는데 실패했습니다",
|
||||
|
@@ -203,8 +203,8 @@
|
||||
"protected": "Protegido",
|
||||
"public": "Público"
|
||||
},
|
||||
"filesCount": "{{count}} arquivos",
|
||||
"recipientsCount": "{{count}} destinatários",
|
||||
"filesCount": "arquivos",
|
||||
"recipientsCount": "destinatários",
|
||||
"actions": {
|
||||
"menu": "Menu de ações do compartilhamento",
|
||||
"edit": "Editar",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "Uso de Armazenamento",
|
||||
"ariaLabel": "Barra de progresso do uso de armazenamento",
|
||||
"used": "{{size}} usado",
|
||||
"available": "{{size}} disponível"
|
||||
"used": "usado",
|
||||
"available": "disponível"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Falha ao carregar dados do painel",
|
||||
|
@@ -203,8 +203,8 @@
|
||||
"protected": "Защищено",
|
||||
"public": "Публично"
|
||||
},
|
||||
"filesCount": "{{count}} файлов",
|
||||
"recipientsCount": "{{count}} получателей",
|
||||
"filesCount": "файлов",
|
||||
"recipientsCount": "получателей",
|
||||
"actions": {
|
||||
"menu": "Меню действий общего доступа",
|
||||
"edit": "Редактировать",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "Использование хранилища",
|
||||
"ariaLabel": "Индикатор использования хранилища",
|
||||
"used": "Использовано: {{size}}",
|
||||
"available": "Доступно: {{size}}"
|
||||
"used": "Использовано",
|
||||
"available": "Доступно"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Ошибка загрузки данных панели управления",
|
||||
|
@@ -203,8 +203,8 @@
|
||||
"protected": "Korunuyor",
|
||||
"public": "Genel"
|
||||
},
|
||||
"filesCount": "{{count}} dosya",
|
||||
"recipientsCount": "{{count}} alıcı",
|
||||
"filesCount": "dosya",
|
||||
"recipientsCount": "alıcı",
|
||||
"actions": {
|
||||
"menu": "Paylaşım İşlem Menüsü",
|
||||
"edit": "Düzenle",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "Depolama Kullanımı",
|
||||
"ariaLabel": "Depolama kullanım ilerleme çubuğu",
|
||||
"used": "{{size}} kullanıldı",
|
||||
"available": "{{size}} kullanılabilir"
|
||||
"used": "kullanıldı",
|
||||
"available": "kullanılabilir"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Gösterge paneli verileri yüklenemedi",
|
||||
|
@@ -203,8 +203,8 @@
|
||||
"protected": "受保护",
|
||||
"public": "公开"
|
||||
},
|
||||
"filesCount": "{{count}} 个文件",
|
||||
"recipientsCount": "{{count}} 个收件人",
|
||||
"filesCount": "个文件",
|
||||
"recipientsCount": "个收件人",
|
||||
"actions": {
|
||||
"menu": "共享操作菜单",
|
||||
"edit": "编辑",
|
||||
@@ -270,8 +270,8 @@
|
||||
"storageUsage": {
|
||||
"title": "存储使用情况",
|
||||
"ariaLabel": "存储使用进度条",
|
||||
"used": "已使用:{{size}}",
|
||||
"available": "可用:{{size}}"
|
||||
"used": "已使用:",
|
||||
"available": "可用:"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "加载仪表盘数据失败",
|
||||
|
@@ -13,14 +13,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-avatar": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-progress": "^1.1.3",
|
||||
"@radix-ui/react-separator": "^1.1.3",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"axios": "^1.8.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^12.6.3",
|
||||
"lucide-react": "^0.487.0",
|
||||
"next": "15.2.4",
|
||||
@@ -31,6 +35,7 @@
|
||||
"react-country-flag": "^3.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.55.0",
|
||||
"sonner": "^2.0.3",
|
||||
"tailwind-merge": "^3.1.0",
|
||||
"tw-animate-css": "^1.2.5",
|
||||
"zod": "^3.24.2",
|
||||
|
199
apps/app/pnpm-lock.yaml
generated
199
apps/app/pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
||||
'@hookform/resolvers':
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(react-hook-form@7.55.0(react@19.1.0))
|
||||
'@radix-ui/react-avatar':
|
||||
specifier: ^1.1.4
|
||||
version: 1.1.4(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-dialog':
|
||||
specifier: ^1.1.6
|
||||
version: 1.1.6(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@@ -20,6 +23,12 @@ importers:
|
||||
'@radix-ui/react-label':
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-progress':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-separator':
|
||||
specifier: ^1.1.3
|
||||
version: 1.1.3(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/react@19.1.0)(react@19.1.0)
|
||||
@@ -35,6 +44,9 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
framer-motion:
|
||||
specifier: ^12.6.3
|
||||
version: 12.6.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
@@ -65,6 +77,9 @@ importers:
|
||||
react-hook-form:
|
||||
specifier: ^7.55.0
|
||||
version: 7.55.0(react@19.1.0)
|
||||
sonner:
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
tailwind-merge:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0
|
||||
@@ -503,6 +518,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-avatar@1.1.4':
|
||||
resolution: {integrity: sha512-+kBesLBzwqyDiYCtYFK+6Ktf+N7+Y6QOTUueLGLIbLZ/YeyFW6bsBGDsN+5HxHpM55C90u5fxsg0ErxzXTcwKA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-collection@1.1.2':
|
||||
resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==}
|
||||
peerDependencies:
|
||||
@@ -525,6 +553,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2':
|
||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-context@1.1.1':
|
||||
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
|
||||
peerDependencies:
|
||||
@@ -534,6 +571,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-context@1.1.2':
|
||||
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-dialog@1.1.6':
|
||||
resolution: {integrity: sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==}
|
||||
peerDependencies:
|
||||
@@ -691,6 +737,32 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-primitive@2.0.3':
|
||||
resolution: {integrity: sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-progress@1.1.3':
|
||||
resolution: {integrity: sha512-F56aZPGTPb4qJQ/vDjnAq63oTu/DRoIG/Asb5XKOWj8rpefNLtUllR969j5QDN2sRrTk9VXIqQDRj5VvAuquaw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.2':
|
||||
resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==}
|
||||
peerDependencies:
|
||||
@@ -704,6 +776,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-separator@1.1.3':
|
||||
resolution: {integrity: sha512-2omrWKJvxR0U/tkIXezcc1nFMwtLU0+b/rDK40gnzJqTLWQ/TD/D5IYVefp9sC3QWfeQbpSbEA6op9MQKyaALQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.1.2':
|
||||
resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==}
|
||||
peerDependencies:
|
||||
@@ -713,6 +798,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.2.0':
|
||||
resolution: {integrity: sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.0':
|
||||
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
|
||||
peerDependencies:
|
||||
@@ -722,6 +816,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1':
|
||||
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-controllable-state@1.1.0':
|
||||
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
|
||||
peerDependencies:
|
||||
@@ -749,6 +852,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.1':
|
||||
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.0':
|
||||
resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==}
|
||||
peerDependencies:
|
||||
@@ -1199,6 +1311,9 @@ packages:
|
||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
debug@3.2.7:
|
||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||
peerDependencies:
|
||||
@@ -2217,6 +2332,12 @@ packages:
|
||||
simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
|
||||
sonner@2.0.3:
|
||||
resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2775,6 +2896,18 @@ snapshots:
|
||||
'@types/react': 19.1.0
|
||||
'@types/react-dom': 19.1.1(@types/react@19.1.0)
|
||||
|
||||
'@radix-ui/react-avatar@1.1.4(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.0)(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.0)(react@19.1.0)
|
||||
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.0)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
'@types/react-dom': 19.1.1(@types/react@19.1.0)
|
||||
|
||||
'@radix-ui/react-collection@1.1.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0)
|
||||
@@ -2793,12 +2926,24 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.0)(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
'@radix-ui/react-context@1.1.1(@types/react@19.1.0)(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
'@radix-ui/react-context@1.1.2(@types/react@19.1.0)(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
'@radix-ui/react-dialog@1.1.6(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
@@ -2961,6 +3106,25 @@ snapshots:
|
||||
'@types/react': 19.1.0
|
||||
'@types/react-dom': 19.1.1(@types/react@19.1.0)
|
||||
|
||||
'@radix-ui/react-primitive@2.0.3(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-slot': 1.2.0(@types/react@19.1.0)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
'@types/react-dom': 19.1.1(@types/react@19.1.0)
|
||||
|
||||
'@radix-ui/react-progress@1.1.3(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.1.0)(react@19.1.0)
|
||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
'@types/react-dom': 19.1.1(@types/react@19.1.0)
|
||||
|
||||
'@radix-ui/react-roving-focus@1.1.2(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
@@ -2978,6 +3142,15 @@ snapshots:
|
||||
'@types/react': 19.1.0
|
||||
'@types/react-dom': 19.1.1(@types/react@19.1.0)
|
||||
|
||||
'@radix-ui/react-separator@1.1.3(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.0.3(@types/react-dom@19.1.1(@types/react@19.1.0))(@types/react@19.1.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
'@types/react-dom': 19.1.1(@types/react@19.1.0)
|
||||
|
||||
'@radix-ui/react-slot@1.1.2(@types/react@19.1.0)(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.1(@types/react@19.1.0)(react@19.1.0)
|
||||
@@ -2985,12 +3158,25 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
'@radix-ui/react-slot@1.2.0(@types/react@19.1.0)(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.0)(react@19.1.0)
|
||||
react: 19.1.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@19.1.0)(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.0)(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
'@radix-ui/react-use-controllable-state@1.1.0(@types/react@19.1.0)(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.1.0)(react@19.1.0)
|
||||
@@ -3011,6 +3197,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.0)(react@19.1.0)':
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
optionalDependencies:
|
||||
'@types/react': 19.1.0
|
||||
|
||||
'@radix-ui/react-use-rect@1.1.0(@types/react@19.1.0)(react@19.1.0)':
|
||||
dependencies:
|
||||
'@radix-ui/rect': 1.1.0
|
||||
@@ -3476,6 +3668,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-data-view: 1.0.2
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
debug@3.2.7:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -4647,6 +4841,11 @@ snapshots:
|
||||
is-arrayish: 0.3.2
|
||||
optional: true
|
||||
|
||||
sonner@2.0.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
react-dom: 19.1.0(react@19.1.0)
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
stable-hash@0.0.5: {}
|
||||
|
@@ -6,7 +6,7 @@ import Link from "next/link";
|
||||
import { IconHeart, IconMenu2 } from "@tabler/icons-react";
|
||||
|
||||
import { LanguageSwitcher } from "@/components/general/language-switcher";
|
||||
import { ModeToggle } from "@/components/mode-toggle";
|
||||
import { ModeToggle } from "@/components/general/mode-toggle";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { siteConfig } from "@/config/site";
|
||||
|
18
apps/app/src/app/dashboard/components/empty-file-state.tsx
Normal file
18
apps/app/src/app/dashboard/components/empty-file-state.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { IconCloudUpload, IconFolderOpen } from "@tabler/icons-react";
|
||||
|
||||
export function EmptyFilesState({ onUpload }: { onUpload: () => void; }) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="text-center py-6 flex flex-col items-center gap-2">
|
||||
<IconFolderOpen className="h-10 w-10 text-gray-500" />
|
||||
<p className="text-gray-500">{t("recentFiles.noFiles")}</p>
|
||||
<Button variant="secondary" size="sm" onClick={onUpload}>
|
||||
<IconCloudUpload className="mr-2 h-4 w-4" />
|
||||
{t("recentFiles.uploadFile")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
20
apps/app/src/app/dashboard/components/empty-shares-state.tsx
Normal file
20
apps/app/src/app/dashboard/components/empty-shares-state.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { IconShare, IconPlus } from "@tabler/icons-react";
|
||||
|
||||
export function EmptySharesState({ onCreate }: { onCreate: () => void; }) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-8 gap-4">
|
||||
<IconShare className="h-10 w-10 text-gray-500" />
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500 mb-4">{t("recentShares.noShares")}</p>
|
||||
<Button variant="outline" size="sm" onClick={onCreate}>
|
||||
<IconPlus className="h-4 w-4" />
|
||||
{t("recentShares.createFirst")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
50
apps/app/src/app/dashboard/components/quick-access-cards.tsx
Normal file
50
apps/app/src/app/dashboard/components/quick-access-cards.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { IconFoldersFilled, IconShare2 } from "@tabler/icons-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function QuickAccessCards() {
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
|
||||
const QUICK_ACCESS_ITEMS = [
|
||||
{
|
||||
title: t("quickAccess.files.title"),
|
||||
icon: <IconFoldersFilled size={28} />,
|
||||
description: t("quickAccess.files.description"),
|
||||
path: "/files",
|
||||
color: "bg-primary",
|
||||
},
|
||||
{
|
||||
title: t("quickAccess.shares.title"),
|
||||
icon: <IconShare2 size={28} />,
|
||||
description: t("quickAccess.shares.description"),
|
||||
path: "/shares",
|
||||
color: "bg-orange-400",
|
||||
},
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{QUICK_ACCESS_ITEMS.map((card) => (
|
||||
<Card
|
||||
key={card.title}
|
||||
className="cursor-pointer transform transition-all hover:scale-102"
|
||||
onClick={() => router.push(card.path)}
|
||||
>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className={`${card.color} w-12 h-12 rounded-lg flex items-center justify-center text-white shadow-sm`}>
|
||||
{card.icon}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{card.title}</h3>
|
||||
<p className="text-sm text-gray-500">{card.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
61
apps/app/src/app/dashboard/components/recent-files.tsx
Normal file
61
apps/app/src/app/dashboard/components/recent-files.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { RecentFilesProps } from "../types";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { EmptyFilesState } from "./empty-file-state";
|
||||
import { IconCloudUpload, IconFolderOpen } from "@tabler/icons-react";
|
||||
import { FilesTable } from "@/components/tables/files-table";
|
||||
|
||||
export function RecentFiles({ files, fileManager, onOpenUploadModal }: RecentFilesProps) {
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<IconCloudUpload className="text-xl text-gray-500" />
|
||||
{t("recentFiles.title")}
|
||||
</h2>
|
||||
{files.length >= 5 ? (
|
||||
<Button
|
||||
className="font-semibold text-sm cursor-pointer"
|
||||
variant="outline"
|
||||
size="default"
|
||||
onClick={() => router.push("/files")}
|
||||
>
|
||||
<IconFolderOpen className="h-4 w-4" />
|
||||
{t("recentFiles.viewAll")}
|
||||
</Button>
|
||||
) : files.length === 0 ? null : (
|
||||
<Button
|
||||
className="font-semibold text-sm cursor-pointer"
|
||||
variant="outline"
|
||||
size="default"
|
||||
onClick={onOpenUploadModal}
|
||||
>
|
||||
<IconCloudUpload className="h-4 w-4" />
|
||||
{t("recentFiles.uploadFile")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{files.length > 0 ? (
|
||||
<FilesTable
|
||||
files={files}
|
||||
onDelete={fileManager.setFileToDelete}
|
||||
onDownload={fileManager.handleDownload}
|
||||
onPreview={fileManager.setPreviewFile}
|
||||
onRename={fileManager.setFileToRename}
|
||||
/>
|
||||
) : (
|
||||
<EmptyFilesState onUpload={onOpenUploadModal} />
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
67
apps/app/src/app/dashboard/components/recent-shares.tsx
Normal file
67
apps/app/src/app/dashboard/components/recent-shares.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { RecentSharesProps } from "../types";
|
||||
import { SharesTable } from "@/components/tables/shares-table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { IconShare, IconPlus } from "@tabler/icons-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { EmptySharesState } from "./empty-shares-state";
|
||||
|
||||
export function RecentShares({ shares, shareManager, onOpenCreateModal, onCopyLink }: RecentSharesProps) {
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<IconShare className="text-xl text-gray-500" />
|
||||
{t("recentShares.title")}
|
||||
</h2>
|
||||
{shares.length >= 5 ? (
|
||||
<Button
|
||||
className="font-semibold text-sm cursor-pointer"
|
||||
variant="outline"
|
||||
size="default"
|
||||
onClick={() => router.push("/shares")}
|
||||
>
|
||||
<IconShare className="h-4 w-4" />
|
||||
{t("recentShares.viewAll")}
|
||||
</Button>
|
||||
) : shares.length === 0 ? null : (
|
||||
<Button
|
||||
className="font-semibold text-sm cursor-pointer"
|
||||
variant="outline"
|
||||
size="default"
|
||||
onClick={onOpenCreateModal}
|
||||
>
|
||||
<IconPlus className="h-4 w-4" />
|
||||
{t("recentShares.createShare")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shares.length > 0 ? (
|
||||
<SharesTable
|
||||
shares={shares}
|
||||
onCopyLink={onCopyLink}
|
||||
onDelete={shareManager.setShareToDelete}
|
||||
onEdit={shareManager.setShareToEdit}
|
||||
onGenerateLink={shareManager.setShareToGenerateLink}
|
||||
onManageFiles={shareManager.setShareToManageFiles}
|
||||
onManageRecipients={shareManager.setShareToManageRecipients}
|
||||
onNotifyRecipients={shareManager.handleNotifyRecipients}
|
||||
onViewDetails={shareManager.setShareToViewDetails}
|
||||
/>
|
||||
) : (
|
||||
<EmptySharesState onCreate={onOpenCreateModal} />
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
35
apps/app/src/app/dashboard/components/storage-usage.tsx
Normal file
35
apps/app/src/app/dashboard/components/storage-usage.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { StorageUsageProps } from "../types";
|
||||
import { formatStorageSize } from "../utils/format-storage-size";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { IconDatabaseCog } from "@tabler/icons-react";
|
||||
|
||||
export function StorageUsage({ diskSpace }: StorageUsageProps) {
|
||||
const t = useTranslations();
|
||||
console.log('diskSpace:', diskSpace);
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
<CardContent className="">
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2 className="text-xl font-semibold flex items-center gap-2">
|
||||
<IconDatabaseCog className="text-gray-500" size={24}/>
|
||||
{t("storageUsage.title")}
|
||||
</h2>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Progress
|
||||
aria-label={t("storageUsage.ariaLabel")}
|
||||
value={((diskSpace?.diskUsedGB || 0) / (diskSpace?.diskSizeGB || 1)) * 100}
|
||||
className="w-full h-3"
|
||||
/>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span> {formatStorageSize(diskSpace?.diskUsedGB || 0)} {t("storageUsage.used")}</span>
|
||||
<span> {formatStorageSize(diskSpace?.diskAvailableGB || 0)} {t("storageUsage.available")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
102
apps/app/src/app/dashboard/hooks/use-dashboard.ts
Normal file
102
apps/app/src/app/dashboard/hooks/use-dashboard.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client"
|
||||
|
||||
import { useFileManager } from "@/hooks/use-file-manager";
|
||||
import { useShareManager } from "@/hooks/use-share-manager";
|
||||
import { getDiskSpace, listFiles, listUserShares, getAllConfigs } from "@/http/endpoints";
|
||||
import { ListUserShares200SharesItem } from "@/http/models/listUserShares200SharesItem";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function useDashboard() {
|
||||
const t = useTranslations();
|
||||
const [diskSpace, setDiskSpace] = useState<{
|
||||
diskSizeGB: number;
|
||||
diskUsedGB: number;
|
||||
diskAvailableGB: number;
|
||||
uploadAllowed: boolean;
|
||||
} | null>(null);
|
||||
const [recentFiles, setRecentFiles] = useState<any[]>([]);
|
||||
const [recentShares, setRecentShares] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [smtpEnabled, setSmtpEnabled] = useState("false");
|
||||
|
||||
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
||||
const onOpenUploadModal = () => setIsUploadModalOpen(true);
|
||||
const onCloseUploadModal = () => setIsUploadModalOpen(false);
|
||||
const onOpenCreateModal = () => setIsCreateModalOpen(true);
|
||||
const onCloseCreateModal = () => setIsCreateModalOpen(false);
|
||||
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
const [diskSpaceRes, filesRes, sharesRes, configsRes] = await Promise.all([
|
||||
getDiskSpace(),
|
||||
listFiles(),
|
||||
listUserShares(),
|
||||
getAllConfigs(),
|
||||
]);
|
||||
|
||||
setDiskSpace(diskSpaceRes.data);
|
||||
|
||||
const allFiles = filesRes.data.files || [];
|
||||
const sortedFiles = [...allFiles].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
|
||||
setRecentFiles(sortedFiles.slice(0, 5));
|
||||
|
||||
const allShares = sharesRes.data.shares || [];
|
||||
const sortedShares = [...allShares].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
|
||||
setRecentShares(sortedShares.slice(0, 5));
|
||||
|
||||
const smtpConfig = configsRes.data.configs.find((config: any) => config.key === "smtpEnabled");
|
||||
|
||||
setSmtpEnabled(smtpConfig?.value === "true" ? "true" : "false");
|
||||
} catch (error) {
|
||||
toast.error(t("dashboard.loadError"));
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fileManager = useFileManager(loadDashboardData);
|
||||
const shareManager = useShareManager(loadDashboardData);
|
||||
|
||||
const handleCopyLink = (share: ListUserShares200SharesItem) => {
|
||||
if (!share.alias?.alias) return;
|
||||
const link = `${window.location.origin}/s/${share.alias.alias}`;
|
||||
|
||||
navigator.clipboard.writeText(link);
|
||||
toast.success(t("dashboard.linkCopied"));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboardData();
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
diskSpace,
|
||||
recentFiles,
|
||||
recentShares,
|
||||
modals: {
|
||||
isUploadModalOpen,
|
||||
isCreateModalOpen,
|
||||
onOpenUploadModal,
|
||||
onCloseUploadModal,
|
||||
onOpenCreateModal,
|
||||
onCloseCreateModal,
|
||||
},
|
||||
fileManager,
|
||||
shareManager,
|
||||
handleCopyLink,
|
||||
loadDashboardData,
|
||||
smtpEnabled,
|
||||
};
|
||||
}
|
62
apps/app/src/app/dashboard/modals/dashboard-modals.tsx
Normal file
62
apps/app/src/app/dashboard/modals/dashboard-modals.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { DashboardModalsProps } from "../types";
|
||||
import { CreateShareModal } from "@/components/modals/create-share-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 { UploadFileModal } from "@/components/modals/upload-file-modal";
|
||||
|
||||
export function DashboardModals({ modals, fileManager, shareManager, onSuccess }: DashboardModalsProps) {
|
||||
return (
|
||||
<>
|
||||
<UploadFileModal isOpen={modals.isUploadModalOpen} onClose={modals.onCloseUploadModal} onSuccess={onSuccess} />
|
||||
|
||||
<FilePreviewModal
|
||||
file={fileManager.previewFile || { name: "", objectName: "" }}
|
||||
isOpen={!!fileManager.previewFile}
|
||||
onClose={() => fileManager.setPreviewFile(null)}
|
||||
/>
|
||||
|
||||
<FileActionsModals
|
||||
fileToDelete={fileManager.fileToDelete}
|
||||
fileToRename={fileManager.fileToRename}
|
||||
onCloseDelete={() => fileManager.setFileToDelete(null)}
|
||||
onCloseRename={() => fileManager.setFileToRename(null)}
|
||||
onDelete={fileManager.handleDelete}
|
||||
onRename={fileManager.handleRename}
|
||||
/>
|
||||
|
||||
<CreateShareModal isOpen={modals.isCreateModalOpen} onClose={modals.onCloseCreateModal} onSuccess={onSuccess} />
|
||||
|
||||
<ShareActionsModals
|
||||
shareToDelete={shareManager.shareToDelete}
|
||||
shareToEdit={shareManager.shareToEdit}
|
||||
shareToManageFiles={shareManager.shareToManageFiles}
|
||||
shareToManageRecipients={shareManager.shareToManageRecipients}
|
||||
onCloseDelete={() => shareManager.setShareToDelete(null)}
|
||||
onCloseEdit={() => shareManager.setShareToEdit(null)}
|
||||
onCloseManageFiles={() => shareManager.setShareToManageFiles(null)}
|
||||
onCloseManageRecipients={() => shareManager.setShareToManageRecipients(null)}
|
||||
onDelete={shareManager.handleDelete}
|
||||
onEdit={shareManager.handleEdit}
|
||||
onManageFiles={shareManager.handleManageFiles}
|
||||
onManageRecipients={shareManager.handleManageRecipients}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
|
||||
<ShareDetailsModal
|
||||
shareId={shareManager.shareToViewDetails?.id || null}
|
||||
onClose={() => shareManager.setShareToViewDetails(null)}
|
||||
/>
|
||||
|
||||
<GenerateShareLinkModal
|
||||
share={shareManager.shareToGenerateLink || null}
|
||||
shareId={shareManager.shareToGenerateLink?.id || null}
|
||||
onClose={() => shareManager.setShareToGenerateLink(null)}
|
||||
onGenerate={shareManager.handleGenerateLink}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -1,16 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { ProtectedRoute } from "@/components/auth/protected-route";
|
||||
import { IconLayoutDashboardFilled } from "@tabler/icons-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { ProtectedRoute } from "@/components/auth/protected-route";
|
||||
import { FileManagerLayout } from "@/components/layout/file-manager-layout";
|
||||
import { LoadingScreen } from "@/components/layout/loading-screen";
|
||||
import { QuickAccessCards } from "./components/quick-access-cards";
|
||||
import { RecentFiles } from "./components/recent-files";
|
||||
import { RecentShares } from "./components/recent-shares";
|
||||
import { StorageUsage } from "./components/storage-usage";
|
||||
import { useDashboard } from "./hooks/use-dashboard";
|
||||
import { DashboardModals } from "./modals/dashboard-modals";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const t = useTranslations();
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
diskSpace,
|
||||
recentFiles,
|
||||
recentShares,
|
||||
modals,
|
||||
fileManager,
|
||||
shareManager,
|
||||
handleCopyLink,
|
||||
loadDashboardData,
|
||||
} = useDashboard();
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingScreen />;
|
||||
}
|
||||
|
||||
function DashboardPage() {
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
{/* Your dashboard content */}
|
||||
</div>
|
||||
<FileManagerLayout
|
||||
breadcrumbLabel={t("dashboard.breadcrumb")}
|
||||
icon={<IconLayoutDashboardFilled className="text-xl" />}
|
||||
showBreadcrumb={false}
|
||||
title={t("dashboard.pageTitle")}
|
||||
>
|
||||
<StorageUsage diskSpace={diskSpace} />
|
||||
<QuickAccessCards />
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<RecentFiles
|
||||
fileManager={fileManager}
|
||||
files={recentFiles}
|
||||
isUploadModalOpen={modals.isUploadModalOpen}
|
||||
onOpenUploadModal={modals.onOpenUploadModal}
|
||||
/>
|
||||
|
||||
<RecentShares
|
||||
isCreateModalOpen={modals.isCreateModalOpen}
|
||||
shareManager={shareManager}
|
||||
shares={recentShares}
|
||||
onCopyLink={handleCopyLink}
|
||||
onOpenCreateModal={modals.onOpenCreateModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* <DashboardModals
|
||||
fileManager={fileManager}
|
||||
modals={modals}
|
||||
shareManager={shareManager}
|
||||
onSuccess={loadDashboardData}
|
||||
/> */}
|
||||
</FileManagerLayout>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
40
apps/app/src/app/dashboard/types/index.ts
Normal file
40
apps/app/src/app/dashboard/types/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { FileManagerHook } from "@/hooks/use-file-manager";
|
||||
import { ShareManagerHook } from "@/hooks/use-share-manager";
|
||||
import { ListUserShares200SharesItem } from "@/http/models/listUserShares200SharesItem";
|
||||
|
||||
export interface RecentFilesProps {
|
||||
files: any[];
|
||||
fileManager: FileManagerHook;
|
||||
isUploadModalOpen: boolean;
|
||||
onOpenUploadModal: () => void;
|
||||
}
|
||||
|
||||
export interface RecentSharesProps {
|
||||
shares: ListUserShares200SharesItem[];
|
||||
shareManager: ShareManagerHook;
|
||||
isCreateModalOpen: boolean;
|
||||
onOpenCreateModal: () => void;
|
||||
onCopyLink: (share: ListUserShares200SharesItem) => void;
|
||||
}
|
||||
|
||||
export interface StorageUsageProps {
|
||||
diskSpace: {
|
||||
diskSizeGB: number;
|
||||
diskUsedGB: number;
|
||||
diskAvailableGB: number;
|
||||
uploadAllowed: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface DashboardModalsProps {
|
||||
modals: {
|
||||
isUploadModalOpen: boolean;
|
||||
isCreateModalOpen: boolean;
|
||||
onCloseUploadModal: () => void;
|
||||
onCloseCreateModal: () => void;
|
||||
};
|
||||
fileManager: FileManagerHook;
|
||||
shareManager: ShareManagerHook;
|
||||
onSuccess: () => Promise<void>;
|
||||
smtpEnabled?: string;
|
||||
}
|
21
apps/app/src/app/dashboard/utils/format-storage-size.ts
Normal file
21
apps/app/src/app/dashboard/utils/format-storage-size.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export const formatStorageSize = (sizeInGB: number) => {
|
||||
if (sizeInGB >= 1) {
|
||||
return `${sizeInGB.toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
const sizeInMB = sizeInGB * 1024;
|
||||
|
||||
if (sizeInMB >= 1) {
|
||||
return `${sizeInMB.toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
const sizeInKB = sizeInMB * 1024;
|
||||
|
||||
if (sizeInKB >= 1) {
|
||||
return `${sizeInKB.toFixed(2)} KB`;
|
||||
}
|
||||
|
||||
const sizeInB = sizeInKB * 1024;
|
||||
|
||||
return `${Math.round(sizeInB)} B`;
|
||||
};
|
@@ -79,13 +79,13 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--background: oklch(0 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.696 0.17 162.48);
|
||||
--primary: oklch(0.723 0.219 149.579);
|
||||
--primary-foreground: oklch(0.393 0.095 152.535);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
@@ -94,17 +94,17 @@
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--border: oklch(1 0 0 / 15%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.527 0.154 150.069);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-2: oklch(0.723 0.219 149.579);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.696 0.17 162.48);
|
||||
--sidebar-primary: oklch(0.723 0.219 149.579);
|
||||
--sidebar-primary-foreground: oklch(0.393 0.095 152.535);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
@@ -120,3 +120,4 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { IconLanguage } from "@tabler/icons-react";
|
||||
import { useLocale } from "next-intl";
|
||||
|
65
apps/app/src/components/layout/file-manager-layout.tsx
Normal file
65
apps/app/src/components/layout/file-manager-layout.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
import { DefaultFooter } from "@/components/ui/default-footer";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { IconLayoutDashboard } from "@tabler/icons-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface FileManagerLayoutProps {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
icon: ReactNode;
|
||||
breadcrumbLabel?: string;
|
||||
showBreadcrumb?: boolean;
|
||||
}
|
||||
|
||||
export function FileManagerLayout({
|
||||
children,
|
||||
title,
|
||||
icon,
|
||||
breadcrumbLabel,
|
||||
showBreadcrumb = true,
|
||||
}: FileManagerLayoutProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="w-full min-h-screen flex flex-col">
|
||||
<Navbar />
|
||||
<div className="flex-1 max-w-7xl mx-auto w-full p-6 py-8">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
{icon}
|
||||
<h1 className="text-2xl font-bold">{title}</h1>
|
||||
</div>
|
||||
<Separator />
|
||||
{showBreadcrumb && breadcrumbLabel && (
|
||||
<nav className="flex" aria-label="Breadcrumb">
|
||||
<ol className="flex items-center gap-2">
|
||||
<li className="flex items-center">
|
||||
<Link
|
||||
href="/dashboard"
|
||||
className="flex items-center text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<IconLayoutDashboard className="h-4 w-4 mr-1" />
|
||||
{t("navigation.dashboard")}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="flex items-center before:content-['/'] before:mx-2 before:text-muted-foreground">
|
||||
<span className="flex items-center">
|
||||
{icon} {breadcrumbLabel}
|
||||
</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
<DefaultFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
106
apps/app/src/components/layout/navbar.tsx
Normal file
106
apps/app/src/components/layout/navbar.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
IconSettings,
|
||||
IconLogout,
|
||||
IconUser,
|
||||
IconUsers,
|
||||
} from "@tabler/icons-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import { LanguageSwitcher } from "@/components/general/language-switcher";
|
||||
import { ModeToggle } from "@/components/general/mode-toggle";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useAppInfo } from "@/contexts/app-info-context";
|
||||
import { useAuth } from "@/contexts/auth-context";
|
||||
import { logout as logoutAPI } from "@/http/endpoints";
|
||||
|
||||
export function Navbar() {
|
||||
const t = useTranslations();
|
||||
const router = useRouter();
|
||||
const { user, isAdmin, logout } = useAuth();
|
||||
const { appName, appLogo } = useAppInfo();
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logoutAPI();
|
||||
logout();
|
||||
router.push("/login");
|
||||
} catch (err) {
|
||||
console.error("Error logging out:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-40 w-full border-b border-border/50 bg-background/70 backdrop-blur-sm px-6">
|
||||
<div className="container flex h-16 max-w-screen-xl items-center mx-auto lg:px-6">
|
||||
<div className="flex flex-1 items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/dashboard" className="flex items-center gap-2 cursor-pointer">
|
||||
{appLogo && <img alt={t("navbar.logoAlt")} className="h-8 w-8 object-contain" src={appLogo} />}
|
||||
<p className="font-bold text-2xl">{appName}</p>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 cursor-pointer">
|
||||
<LanguageSwitcher />
|
||||
<ModeToggle />
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger className="rounded-full">
|
||||
<Avatar className="cursor-pointer h-10 w-10 rounded-full" >
|
||||
<AvatarImage src={user?.image as string | undefined} />
|
||||
<AvatarFallback>{user?.firstName?.[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<div className="flex flex-col px-2 py-1.5 gap-0.5">
|
||||
<p className="font-semibold text-sm">
|
||||
{user?.firstName} {user?.lastName}
|
||||
</p>
|
||||
<p className="font-semibold text-xs text-muted-foreground">{user?.email}</p>
|
||||
</div>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/profile" className="flex items-center gap-2 cursor-pointer">
|
||||
<IconUser className="h-4 w-4" />
|
||||
{t("navbar.profile")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/settings" className="flex items-center gap-2 cursor-pointer">
|
||||
<IconSettings className="h-4 w-4" />
|
||||
{t("navbar.settings")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/admin" className="flex items-center gap-2 cursor-pointer">
|
||||
<IconUsers className="h-4 w-4" />
|
||||
{t("navbar.usersManagement")}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
className="text-destructive focus:text-destructive cursor-pointer"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<IconLogout className="h-4 w-4 mr-2 text-destructive" />
|
||||
{t("navbar.logout")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
146
apps/app/src/components/tables/files-table.tsx
Normal file
146
apps/app/src/components/tables/files-table.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { getFileIcon } from "@/utils/file-icons";
|
||||
import { formatFileSize } from "@/utils/format-file-size";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
IconEye,
|
||||
IconEdit,
|
||||
IconDownload,
|
||||
IconTrash,
|
||||
IconDotsVertical,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
interface File {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
size: number;
|
||||
objectName: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface FilesTableProps {
|
||||
files: File[];
|
||||
onPreview: (file: File) => void;
|
||||
onRename: (file: File) => void;
|
||||
onDownload: (objectName: string, fileName: string) => void;
|
||||
onDelete: (file: File) => void;
|
||||
}
|
||||
|
||||
export function FilesTable({ files, onPreview, onRename, onDownload, onDelete }: FilesTableProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const formatDateTime = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
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 rounded-tl-lg">
|
||||
{t("filesTable.columns.name")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50">
|
||||
{t("filesTable.columns.description")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50">
|
||||
{t("filesTable.columns.size")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50">
|
||||
{t("filesTable.columns.createdAt")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50">
|
||||
{t("filesTable.columns.updatedAt")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 w-[70px] text-xs font-bold text-muted-foreground bg-muted/50 rounded-tr-lg">
|
||||
{t("filesTable.columns.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody >
|
||||
{files.map((file) => {
|
||||
const { icon: FileIcon, color } = getFileIcon(file.name);
|
||||
|
||||
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}`} />
|
||||
<span className="truncate max-w-[250px] font-medium" title={file.name}>
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="h-12 px-4">{file.description || "-"}</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"
|
||||
>
|
||||
<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
|
||||
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>
|
||||
);
|
||||
}
|
192
apps/app/src/components/tables/shares-table.tsx
Normal file
192
apps/app/src/components/tables/shares-table.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { useShareContext } from "../../contexts/share-context";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableHead,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import { format } from "date-fns";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
import {
|
||||
IconEdit,
|
||||
IconTrash,
|
||||
IconUsers,
|
||||
IconFolder,
|
||||
IconDotsVertical,
|
||||
IconLock,
|
||||
IconLockOpen,
|
||||
IconEye,
|
||||
IconCopy,
|
||||
IconLink,
|
||||
IconMail,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
export interface SharesTableProps {
|
||||
shares: any[];
|
||||
onDelete: (share: any) => void;
|
||||
onEdit: (share: any) => void;
|
||||
onManageFiles: (share: any) => void;
|
||||
onManageRecipients: (share: any) => void;
|
||||
onViewDetails: (share: any) => void;
|
||||
onGenerateLink: (share: any) => void;
|
||||
onCopyLink: (share: any) => void;
|
||||
onNotifyRecipients: (share: any) => void;
|
||||
}
|
||||
|
||||
export function SharesTable({
|
||||
shares,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onManageFiles,
|
||||
onManageRecipients,
|
||||
onViewDetails,
|
||||
onGenerateLink,
|
||||
onCopyLink,
|
||||
onNotifyRecipients,
|
||||
}: SharesTableProps) {
|
||||
const t = useTranslations();
|
||||
const { smtpEnabled } = useShareContext();
|
||||
|
||||
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">
|
||||
{t("sharesTable.columns.name")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50">
|
||||
{t("sharesTable.columns.createdAt")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50">
|
||||
{t("sharesTable.columns.expiresAt")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50">
|
||||
{t("sharesTable.columns.status")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50">
|
||||
{t("sharesTable.columns.security")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50">
|
||||
{t("sharesTable.columns.files")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 text-xs font-bold text-muted-foreground bg-muted/50">
|
||||
{t("sharesTable.columns.recipients")}
|
||||
</TableHead>
|
||||
<TableHead className="h-10 w-[70px] text-xs font-bold text-muted-foreground bg-muted/50">
|
||||
{t("sharesTable.columns.actions")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{shares.map((share) => (
|
||||
<TableRow key={share.id} className="hover:bg-muted/50 transition-colors border-0">
|
||||
<TableCell className="h-12 border-0">{share.name}</TableCell>
|
||||
<TableCell className="h-12 ">{format(new Date(share.createdAt), "MM/dd/yyyy HH:mm")}</TableCell>
|
||||
<TableCell className="h-12 ">
|
||||
{share.expiration ? format(new Date(share.expiration), "MM/dd/yyyy HH:mm") : t("sharesTable.never")}
|
||||
</TableCell>
|
||||
<TableCell className="h-12 ">
|
||||
<Badge variant="secondary" className={
|
||||
!share.expiration || new Date(share.expiration) > new Date()
|
||||
? "bg-green-500/20 hover:bg-green-500/30 text-green-500"
|
||||
: "bg-red-500/20 hover:bg-red-500/30 text-red-500"
|
||||
}>
|
||||
{!share.expiration
|
||||
? t("sharesTable.status.neverExpires")
|
||||
: new Date(share.expiration) > new Date()
|
||||
? t("sharesTable.status.active")
|
||||
: t("sharesTable.status.expired")}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="h-12 ">
|
||||
<Badge variant="secondary" className={`flex items-center gap-1 ${
|
||||
share.security.hasPassword
|
||||
? "bg-yellow-500/20 hover:bg-yellow-500/30 text-yellow-500"
|
||||
: "bg-green-500/20 hover:bg-green-500/30 text-green-500"
|
||||
}`}>
|
||||
{share.security.hasPassword ? (
|
||||
<IconLock className="h-4 w-4" />
|
||||
) : (
|
||||
<IconLockOpen className="h-4 w-4" />
|
||||
)}
|
||||
{share.security.hasPassword
|
||||
? t("sharesTable.security.protected")
|
||||
: t("sharesTable.security.public")}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="h-12">{ share.files?.length || 0 } {t("sharesTable.filesCount")}</TableCell>
|
||||
<TableCell className="h-12">{ share.recipients?.length || 0 } {t("sharesTable.recipientsCount")}</TableCell>
|
||||
<TableCell className="h-12 text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 hover:bg-muted"
|
||||
>
|
||||
<IconDotsVertical className="h-4 w-4" />
|
||||
<span className="sr-only">{t("sharesTable.actions.menu")}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onEdit(share)}>
|
||||
<IconEdit className="mr-2 h-4 w-4" />
|
||||
{t("sharesTable.actions.edit")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onManageFiles(share)}>
|
||||
<IconFolder className="mr-2 h-4 w-4" />
|
||||
{t("sharesTable.actions.manageFiles")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onManageRecipients(share)}>
|
||||
<IconUsers className="mr-2 h-4 w-4" />
|
||||
{t("sharesTable.actions.manageRecipients")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onViewDetails(share)}>
|
||||
<IconEye className="mr-2 h-4 w-4" />
|
||||
{t("sharesTable.actions.viewDetails")}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onGenerateLink(share)}>
|
||||
<IconLink className="mr-2 h-4 w-4" />
|
||||
{share.alias ? t("sharesTable.actions.editLink") : t("sharesTable.actions.generateLink")}
|
||||
</DropdownMenuItem>
|
||||
{share.alias && (
|
||||
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onCopyLink(share)}>
|
||||
<IconCopy className="mr-2 h-4 w-4" />
|
||||
{t("sharesTable.actions.copyLink")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{share.recipients?.length > 0 && share.alias && smtpEnabled === "true" && (
|
||||
<DropdownMenuItem className="cursor-pointer py-2" onClick={() => onNotifyRecipients(share)}>
|
||||
<IconMail className="mr-2 h-4 w-4" />
|
||||
{t("sharesTable.actions.notifyRecipients")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => onDelete(share)}
|
||||
className="cursor-pointer py-2 text-destructive focus:text-destructive"
|
||||
>
|
||||
<IconTrash className="mr-2 h-4 w-4" />
|
||||
{t("sharesTable.actions.delete")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
53
apps/app/src/components/ui/avatar.tsx
Normal file
53
apps/app/src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
46
apps/app/src/components/ui/badge.tsx
Normal file
46
apps/app/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
92
apps/app/src/components/ui/card.tsx
Normal file
92
apps/app/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border dark:border-none shadow-lg py-6 dark:shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
31
apps/app/src/components/ui/progress.tsx
Normal file
31
apps/app/src/components/ui/progress.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Progress }
|
28
apps/app/src/components/ui/separator.tsx
Normal file
28
apps/app/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator-root"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
116
apps/app/src/components/ui/table.tsx
Normal file
116
apps/app/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
41
apps/app/src/contexts/share-context.tsx
Normal file
41
apps/app/src/contexts/share-context.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { getAllConfigs } from "@/http/endpoints";
|
||||
import { createContext, useContext, useState, useEffect } from "react";
|
||||
|
||||
interface ShareContextType {
|
||||
smtpEnabled: string;
|
||||
refreshShareContext: () => Promise<void>;
|
||||
}
|
||||
|
||||
const ShareContext = createContext<ShareContextType>({
|
||||
smtpEnabled: "false",
|
||||
refreshShareContext: async () => {},
|
||||
});
|
||||
|
||||
export function ShareProvider({ children }: { children: React.ReactNode }) {
|
||||
const [smtpEnabled, setSmtpEnabled] = useState("false");
|
||||
|
||||
const loadConfigs = async () => {
|
||||
try {
|
||||
const response = await getAllConfigs();
|
||||
const smtpConfig = response.data.configs.find((config: any) => config.key === "smtpEnabled");
|
||||
|
||||
setSmtpEnabled(smtpConfig?.value || "false");
|
||||
} catch (error) {
|
||||
console.error("Failed to load SMTP config:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshShareContext = async () => {
|
||||
await loadConfigs();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadConfigs();
|
||||
}, []);
|
||||
|
||||
return <ShareContext.Provider value={{ smtpEnabled, refreshShareContext }}>{children}</ShareContext.Provider>;
|
||||
}
|
||||
|
||||
export const useShareContext = () => useContext(ShareContext);
|
101
apps/app/src/hooks/use-file-manager.ts
Normal file
101
apps/app/src/hooks/use-file-manager.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { getDownloadUrl, updateFile, deleteFile } from "@/http/endpoints";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface FileToRename {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface FileToDelete {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface PreviewFile {
|
||||
name: string;
|
||||
objectName: string;
|
||||
}
|
||||
|
||||
export interface FileManagerHook {
|
||||
previewFile: PreviewFile | null;
|
||||
fileToDelete: any;
|
||||
fileToRename: any;
|
||||
setFileToDelete: (file: any) => void;
|
||||
setFileToRename: (file: any) => void;
|
||||
setPreviewFile: (file: PreviewFile | null) => void;
|
||||
handleDelete: (fileId: string) => Promise<void>;
|
||||
handleDownload: (objectName: string, fileName: string) => Promise<void>;
|
||||
handleRename: (fileId: string, newName: string, description?: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useFileManager(onRefresh: () => Promise<void>) {
|
||||
const [previewFile, setPreviewFile] = useState<PreviewFile | null>(null);
|
||||
const [fileToRename, setFileToRename] = useState<FileToRename | null>(null);
|
||||
const [fileToDelete, setFileToDelete] = useState<FileToDelete | null>(null);
|
||||
|
||||
const handleDownload = async (objectName: string, fileName: string) => {
|
||||
try {
|
||||
const encodedObjectName = encodeURIComponent(objectName);
|
||||
const response = await getDownloadUrl(encodedObjectName);
|
||||
const downloadUrl = response.data.url;
|
||||
|
||||
const fileResponse = await fetch(downloadUrl);
|
||||
const blob = await fileResponse.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Failed to download file");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRename = async (fileId: string, newName: string, description?: string) => {
|
||||
try {
|
||||
await updateFile(fileId, {
|
||||
name: newName,
|
||||
description: description || null,
|
||||
});
|
||||
await onRefresh();
|
||||
toast.success("File updated successfully");
|
||||
setFileToRename(null);
|
||||
} catch (error) {
|
||||
console.error("Failed to update file:", error);
|
||||
toast.error("Failed to update file");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (fileId: string) => {
|
||||
try {
|
||||
await deleteFile(fileId);
|
||||
await onRefresh();
|
||||
toast.success("File deleted successfully");
|
||||
setFileToDelete(null);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete file:", error);
|
||||
toast.error("Failed to delete file");
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
previewFile,
|
||||
setPreviewFile,
|
||||
fileToRename,
|
||||
setFileToRename,
|
||||
fileToDelete,
|
||||
setFileToDelete,
|
||||
handleDownload,
|
||||
handleRename,
|
||||
handleDelete,
|
||||
};
|
||||
}
|
135
apps/app/src/hooks/use-share-manager.ts
Normal file
135
apps/app/src/hooks/use-share-manager.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { deleteShare, notifyRecipients, updateShare } from "@/http/endpoints";
|
||||
import { addFiles, addRecipients } from "@/http/endpoints";
|
||||
import { createShareAlias } from "@/http/endpoints";
|
||||
import { ListUserShares200SharesItem } from "@/http/models/listUserShares200SharesItem";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
||||
import { toast } from "sonner";
|
||||
|
||||
export interface ShareManagerHook {
|
||||
shareToDelete: ListUserShares200SharesItem | null;
|
||||
shareToEdit: ListUserShares200SharesItem | null;
|
||||
shareToManageFiles: ListUserShares200SharesItem | null;
|
||||
shareToManageRecipients: ListUserShares200SharesItem | null;
|
||||
shareToViewDetails: ListUserShares200SharesItem | null;
|
||||
shareToGenerateLink: ListUserShares200SharesItem | null;
|
||||
setShareToDelete: (share: ListUserShares200SharesItem | null) => void;
|
||||
setShareToEdit: (share: ListUserShares200SharesItem | null) => void;
|
||||
setShareToManageFiles: (share: ListUserShares200SharesItem | null) => void;
|
||||
setShareToManageRecipients: (share: ListUserShares200SharesItem | null) => void;
|
||||
setShareToViewDetails: (share: ListUserShares200SharesItem | null) => void;
|
||||
setShareToGenerateLink: (share: ListUserShares200SharesItem | null) => void;
|
||||
handleDelete: (shareId: string) => Promise<void>;
|
||||
handleEdit: (shareId: string, data: any) => Promise<void>;
|
||||
handleManageFiles: (shareId: string, files: any[]) => Promise<void>;
|
||||
handleManageRecipients: (shareId: string, recipients: any[]) => Promise<void>;
|
||||
handleGenerateLink: (shareId: string, alias: string) => Promise<void>;
|
||||
handleNotifyRecipients: (share: ListUserShares200SharesItem) => Promise<void>;
|
||||
}
|
||||
|
||||
export function useShareManager(onSuccess: () => void) {
|
||||
const t = useTranslations();
|
||||
const [shareToDelete, setShareToDelete] = useState<ListUserShares200SharesItem | null>(null);
|
||||
const [shareToEdit, setShareToEdit] = useState<ListUserShares200SharesItem | null>(null);
|
||||
const [shareToManageFiles, setShareToManageFiles] = useState<ListUserShares200SharesItem | null>(null);
|
||||
const [shareToManageRecipients, setShareToManageRecipients] = useState<ListUserShares200SharesItem | null>(null);
|
||||
const [shareToViewDetails, setShareToViewDetails] = useState<ListUserShares200SharesItem | null>(null);
|
||||
const [shareToGenerateLink, setShareToGenerateLink] = useState<ListUserShares200SharesItem | null>(null);
|
||||
|
||||
const handleDelete = async (shareId: string) => {
|
||||
try {
|
||||
await deleteShare(shareId);
|
||||
toast.success(t("shareManager.deleteSuccess"));
|
||||
onSuccess();
|
||||
setShareToDelete(null);
|
||||
} catch (error) {
|
||||
toast.error(t("shareManager.deleteError"));
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (shareId: string, data: any) => {
|
||||
try {
|
||||
await updateShare({ id: shareId, ...data });
|
||||
toast.success(t("shareManager.updateSuccess"));
|
||||
onSuccess();
|
||||
setShareToEdit(null);
|
||||
} catch (error) {
|
||||
toast.error(t("shareManager.updateError"));
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleManageFiles = async (shareId: string, files: string[]) => {
|
||||
try {
|
||||
await addFiles(shareId, { files });
|
||||
toast.success(t("shareManager.filesUpdateSuccess"));
|
||||
onSuccess();
|
||||
setShareToManageFiles(null);
|
||||
} catch (error) {
|
||||
toast.error(t("shareManager.filesUpdateError"));
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleManageRecipients = async (shareId: string, recipients: string[]) => {
|
||||
try {
|
||||
await addRecipients(shareId, { emails: recipients });
|
||||
toast.success(t("shareManager.recipientsUpdateSuccess"));
|
||||
onSuccess();
|
||||
setShareToManageRecipients(null);
|
||||
} catch (error) {
|
||||
toast.error(t("shareManager.recipientsUpdateError"));
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerateLink = async (shareId: string, alias: string) => {
|
||||
try {
|
||||
await createShareAlias(shareId, { alias });
|
||||
toast.success(t("shareManager.linkGenerateSuccess"));
|
||||
onSuccess();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(t("shareManager.linkGenerateError"));
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotifyRecipients = async (share: ListUserShares200SharesItem) => {
|
||||
const link = `${window.location.origin}/s/${share.alias?.alias}`;
|
||||
const loadingToast = toast.loading(t("shareManager.notifyLoading"));
|
||||
|
||||
try {
|
||||
await notifyRecipients(share.id, { shareLink: link });
|
||||
toast.dismiss(loadingToast);
|
||||
toast.success(t("shareManager.notifySuccess"));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.dismiss(loadingToast);
|
||||
toast.error(t("shareManager.notifyError"));
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
shareToDelete,
|
||||
shareToEdit,
|
||||
shareToManageFiles,
|
||||
shareToManageRecipients,
|
||||
shareToViewDetails,
|
||||
shareToGenerateLink,
|
||||
setShareToDelete,
|
||||
setShareToEdit,
|
||||
setShareToManageFiles,
|
||||
setShareToManageRecipients,
|
||||
setShareToViewDetails,
|
||||
setShareToGenerateLink,
|
||||
handleDelete,
|
||||
handleEdit,
|
||||
handleManageFiles,
|
||||
handleManageRecipients,
|
||||
handleGenerateLink,
|
||||
handleNotifyRecipients,
|
||||
};
|
||||
}
|
81
apps/app/src/utils/file-icons.tsx
Normal file
81
apps/app/src/utils/file-icons.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Icon } from "@tabler/icons-react";
|
||||
import {
|
||||
IconFile,
|
||||
IconPhoto,
|
||||
IconFileTypePdf,
|
||||
IconFileText,
|
||||
IconFileSpreadsheet,
|
||||
IconPresentation,
|
||||
IconFileMusic,
|
||||
IconVideo,
|
||||
IconFileZip,
|
||||
IconFileCode,
|
||||
IconFileDescription,
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
interface FileIconMapping {
|
||||
extensions: string[];
|
||||
icon: Icon;
|
||||
color: string;
|
||||
}
|
||||
|
||||
const fileIcons: FileIconMapping[] = [
|
||||
{
|
||||
extensions: ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"],
|
||||
icon: IconPhoto,
|
||||
color: "text-blue-500",
|
||||
},
|
||||
{
|
||||
extensions: ["pdf"],
|
||||
icon: IconFileTypePdf,
|
||||
color: "text-red-500",
|
||||
},
|
||||
{
|
||||
extensions: ["doc", "docx"],
|
||||
icon: IconFileText,
|
||||
color: "text-blue-600",
|
||||
},
|
||||
{
|
||||
extensions: ["xls", "xlsx", "csv"],
|
||||
icon: IconFileSpreadsheet,
|
||||
color: "text-green-600",
|
||||
},
|
||||
{
|
||||
extensions: ["ppt", "pptx"],
|
||||
icon: IconPresentation,
|
||||
color: "text-orange-500",
|
||||
},
|
||||
{
|
||||
extensions: ["mp3", "wav", "ogg", "m4a"],
|
||||
icon: IconFileMusic,
|
||||
color: "text-purple-500",
|
||||
},
|
||||
{
|
||||
extensions: ["mp4", "avi", "mov", "wmv", "mkv"],
|
||||
icon: IconVideo,
|
||||
color: "text-pink-500",
|
||||
},
|
||||
{
|
||||
extensions: ["zip", "rar", "7z", "tar", "gz"],
|
||||
icon: IconFileZip,
|
||||
color: "text-yellow-600",
|
||||
},
|
||||
{
|
||||
extensions: ["html", "css", "js", "ts", "jsx", "tsx", "json", "xml"],
|
||||
icon: IconFileCode,
|
||||
color: "text-gray-600",
|
||||
},
|
||||
{
|
||||
extensions: ["txt", "md", "rtf"],
|
||||
icon: IconFileDescription,
|
||||
color: "text-gray-500",
|
||||
},
|
||||
];
|
||||
|
||||
export function getFileIcon(filename: string): { icon: Icon; color: string } {
|
||||
const extension = filename.split(".").pop()?.toLowerCase() || "";
|
||||
|
||||
const mapping = fileIcons.find((type) => type.extensions.includes(extension));
|
||||
|
||||
return mapping || { icon: IconFile, color: "text-gray-400" };
|
||||
}
|
13
apps/app/src/utils/format-file-size.ts
Normal file
13
apps/app/src/utils/format-file-size.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function formatFileSize(bytes: number): string {
|
||||
const KB = 1024;
|
||||
const MB = KB * 1024;
|
||||
const GB = MB * 1024;
|
||||
|
||||
if (bytes < MB) {
|
||||
return `${(bytes / KB).toFixed(2)} KB`;
|
||||
} else if (bytes < GB) {
|
||||
return `${(bytes / MB).toFixed(2)} MB`;
|
||||
} else {
|
||||
return `${(bytes / GB).toFixed(2)} GB`;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user