mirror of
				https://github.com/kyantech/Palmr.git
				synced 2025-11-04 05:53:23 +00:00 
			
		
		
		
	feat: implement global drop zone for file uploads across the application
- Introduced a new GlobalDropZone component to handle file drag-and-drop uploads, enhancing user experience. - Updated dashboard and files pages to utilize the GlobalDropZone, allowing users to easily upload files by dragging them into designated areas. - Added support for pasting images directly into the application, with success notifications for completed uploads. - Enhanced localization by adding relevant messages for various languages in the translation files.
This commit is contained in:
		@@ -1442,7 +1442,12 @@
 | 
				
			|||||||
      "warning": "إذا أغلقت الآن، سيتم إلغاء الرفع وسيفقد أي تقدم.",
 | 
					      "warning": "إذا أغلقت الآن، سيتم إلغاء الرفع وسيفقد أي تقدم.",
 | 
				
			||||||
      "continue": "مواصلة الرفع",
 | 
					      "continue": "مواصلة الرفع",
 | 
				
			||||||
      "cancel": "إلغاء الرفع"
 | 
					      "cancel": "إلغاء الرفع"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "إفلات الملفات للرفع",
 | 
				
			||||||
 | 
					      "description": "حرر للرفع ملفاتك"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {تم لصق الصورة ورفعها بنجاح} other {تم لصق # صور ورفعها بنجاح}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1528,4 +1533,4 @@
 | 
				
			|||||||
    "nameRequired": "الاسم مطلوب",
 | 
					    "nameRequired": "الاسم مطلوب",
 | 
				
			||||||
    "required": "هذا الحقل مطلوب"
 | 
					    "required": "هذا الحقل مطلوب"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
      "warning": "Wenn Sie jetzt schließen, werden die Uploads abgebrochen und jeder Fortschritt geht verloren.",
 | 
					      "warning": "Wenn Sie jetzt schließen, werden die Uploads abgebrochen und jeder Fortschritt geht verloren.",
 | 
				
			||||||
      "continue": "Uploads Fortsetzen",
 | 
					      "continue": "Uploads Fortsetzen",
 | 
				
			||||||
      "cancel": "Uploads Abbrechen"
 | 
					      "cancel": "Uploads Abbrechen"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "Dateien zum Hochladen ablegen",
 | 
				
			||||||
 | 
					      "description": "Loslassen, um Ihre Dateien hochzuladen"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {Bild erfolgreich eingefügt und hochgeladen} other {# Bilder erfolgreich eingefügt und hochgeladen}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "Name ist erforderlich",
 | 
					    "nameRequired": "Name ist erforderlich",
 | 
				
			||||||
    "required": "Dieses Feld ist erforderlich"
 | 
					    "required": "Dieses Feld ist erforderlich"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1433,6 +1433,10 @@
 | 
				
			|||||||
    "fileSizeExceeded": "File size exceeds the limit of {maxsizemb}MB.",
 | 
					    "fileSizeExceeded": "File size exceeds the limit of {maxsizemb}MB.",
 | 
				
			||||||
    "insufficientStorage": "Insufficient storage space. You have {availablespace}MB available.",
 | 
					    "insufficientStorage": "Insufficient storage space. You have {availablespace}MB available.",
 | 
				
			||||||
    "unauthorized": "Unauthorized: a valid token is required to access this resource.",
 | 
					    "unauthorized": "Unauthorized: a valid token is required to access this resource.",
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "Drop files to upload",
 | 
				
			||||||
 | 
					      "description": "Release to upload your files"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "confirmCancel": {
 | 
					    "confirmCancel": {
 | 
				
			||||||
      "title": "Cancel Uploads",
 | 
					      "title": "Cancel Uploads",
 | 
				
			||||||
      "messageSingle": "There is one upload in progress.",
 | 
					      "messageSingle": "There is one upload in progress.",
 | 
				
			||||||
@@ -1440,7 +1444,8 @@
 | 
				
			|||||||
      "warning": "If you close now, uploads will be cancelled and any progress will be lost.",
 | 
					      "warning": "If you close now, uploads will be cancelled and any progress will be lost.",
 | 
				
			||||||
      "continue": "Continue Uploads",
 | 
					      "continue": "Continue Uploads",
 | 
				
			||||||
      "cancel": "Cancel Uploads"
 | 
					      "cancel": "Cancel Uploads"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {Image pasted and uploaded successfully} other {# images pasted and uploaded successfully}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "Name is required",
 | 
					    "nameRequired": "Name is required",
 | 
				
			||||||
    "required": "This field is required"
 | 
					    "required": "This field is required"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
      "warning": "Si cierra ahora, las subidas serán canceladas y se perderá cualquier progreso.",
 | 
					      "warning": "Si cierra ahora, las subidas serán canceladas y se perderá cualquier progreso.",
 | 
				
			||||||
      "continue": "Continuar Subidas",
 | 
					      "continue": "Continuar Subidas",
 | 
				
			||||||
      "cancel": "Cancelar Subidas"
 | 
					      "cancel": "Cancelar Subidas"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "Suelta archivos para subir",
 | 
				
			||||||
 | 
					      "description": "Suelta para subir tus archivos"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {Imagen pegada y subida exitosamente} other {# imágenes pegadas y subidas exitosamente}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "El nombre es obligatorio",
 | 
					    "nameRequired": "El nombre es obligatorio",
 | 
				
			||||||
    "required": "Este campo es obligatorio"
 | 
					    "required": "Este campo es obligatorio"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
      "warning": "Si vous fermez maintenant, les téléchargements seront annulés et tout progrès sera perdu.",
 | 
					      "warning": "Si vous fermez maintenant, les téléchargements seront annulés et tout progrès sera perdu.",
 | 
				
			||||||
      "continue": "Continuer les Téléchargements",
 | 
					      "continue": "Continuer les Téléchargements",
 | 
				
			||||||
      "cancel": "Annuler les Téléchargements"
 | 
					      "cancel": "Annuler les Téléchargements"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "Déposer des fichiers pour télécharger",
 | 
				
			||||||
 | 
					      "description": "Relâchez pour télécharger vos fichiers"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {Image collée et téléchargée avec succès} other {# images collées et téléchargées avec succès}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "Nome é obrigatório",
 | 
					    "nameRequired": "Nome é obrigatório",
 | 
				
			||||||
    "required": "Este campo é obrigatório"
 | 
					    "required": "Este campo é obrigatório"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
      "warning": "यदि आप अभी बंद करते हैं, तो अपलोड रद्द हो जाएंगे और कोई भी प्रगति खो जाएगी।",
 | 
					      "warning": "यदि आप अभी बंद करते हैं, तो अपलोड रद्द हो जाएंगे और कोई भी प्रगति खो जाएगी।",
 | 
				
			||||||
      "continue": "अपलोड जारी रखें",
 | 
					      "continue": "अपलोड जारी रखें",
 | 
				
			||||||
      "cancel": "अपलोड रद्द करें"
 | 
					      "cancel": "अपलोड रद्द करें"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "अपलोड करने के लिए फ़ाइलें छोड़ें",
 | 
				
			||||||
 | 
					      "description": "अपनी फ़ाइलें अपलोड करने के लिए छोड़ें"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {छवि सफलतापूर्वक चिपकाई और अपलोड की गई} other {# छवियाँ सफलतापूर्वक चिपकाई और अपलोड की गईं}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "नाम आवश्यक है",
 | 
					    "nameRequired": "नाम आवश्यक है",
 | 
				
			||||||
    "required": "यह फ़ील्ड आवश्यक है"
 | 
					    "required": "यह फ़ील्ड आवश्यक है"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
      "warning": "Se chiudi ora, i caricamenti saranno annullati e qualsiasi progresso sarà perso.",
 | 
					      "warning": "Se chiudi ora, i caricamenti saranno annullati e qualsiasi progresso sarà perso.",
 | 
				
			||||||
      "continue": "Continua Caricamenti",
 | 
					      "continue": "Continua Caricamenti",
 | 
				
			||||||
      "cancel": "Annulla Caricamenti"
 | 
					      "cancel": "Annulla Caricamenti"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "Rilascia i file per caricarli",
 | 
				
			||||||
 | 
					      "description": "Rilascia per caricare i tuoi file"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {Immagine incollata e caricata con successo} other {# immagini incollate e caricate con successo}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "Il nome è obbligatorio",
 | 
					    "nameRequired": "Il nome è obbligatorio",
 | 
				
			||||||
    "required": "Questo campo è obbligatorio"
 | 
					    "required": "Questo campo è obbligatorio"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
    "retry": "再試行",
 | 
					    "retry": "再試行",
 | 
				
			||||||
    "allSuccess": "{count, plural, =1 {ファイルがアップロードされました} other {#個のファイルがアップロードされました}}",
 | 
					    "allSuccess": "{count, plural, =1 {ファイルがアップロードされました} other {#個のファイルがアップロードされました}}",
 | 
				
			||||||
    "partialSuccess": "{success}個のファイルがアップロードされ、{error}個が失敗しました",
 | 
					    "partialSuccess": "{success}個のファイルがアップロードされ、{error}個が失敗しました",
 | 
				
			||||||
    "dragAndDrop": "またはここにファイルをドラッグ&ドロップ"
 | 
					    "dragAndDrop": "またはここにファイルをドラッグ&ドロップ",
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "アップロードするファイルをドロップ",
 | 
				
			||||||
 | 
					      "description": "ファイルをアップロードするにはリリースしてください"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {画像が貼り付けられ、正常にアップロードされました} other {#個の画像が貼り付けられ、正常にアップロードされました}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "名前は必須です",
 | 
					    "nameRequired": "名前は必須です",
 | 
				
			||||||
    "required": "このフィールドは必須です"
 | 
					    "required": "このフィールドは必須です"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
      "warning": "지금 닫으면 업로드가 취소되고 모든 진행 상황이 손실됩니다.",
 | 
					      "warning": "지금 닫으면 업로드가 취소되고 모든 진행 상황이 손실됩니다.",
 | 
				
			||||||
      "continue": "업로드 계속",
 | 
					      "continue": "업로드 계속",
 | 
				
			||||||
      "cancel": "업로드 취소"
 | 
					      "cancel": "업로드 취소"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "업로드할 파일 드롭",
 | 
				
			||||||
 | 
					      "description": "파일을 업로드하려면 놓으세요"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {이미지가 성공적으로 붙여넣기 및 업로드되었습니다} other {# 개의 이미지가 성공적으로 붙여넣기 및 업로드되었습니다}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "이름은 필수입니다",
 | 
					    "nameRequired": "이름은 필수입니다",
 | 
				
			||||||
    "required": "이 필드는 필수입니다"
 | 
					    "required": "이 필드는 필수입니다"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
      "warning": "Als u nu sluit, worden de uploads geannuleerd en gaat alle voortgang verloren.",
 | 
					      "warning": "Als u nu sluit, worden de uploads geannuleerd en gaat alle voortgang verloren.",
 | 
				
			||||||
      "continue": "Uploads Voortzetten",
 | 
					      "continue": "Uploads Voortzetten",
 | 
				
			||||||
      "cancel": "Uploads Annuleren"
 | 
					      "cancel": "Uploads Annuleren"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "Sleep bestanden om te uploaden",
 | 
				
			||||||
 | 
					      "description": "Laat los om je bestanden te uploaden"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {Afbeelding geplakt en succesvol geüpload} other {# afbeeldingen geplakt en succesvol geüpload}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "Naam is verplicht",
 | 
					    "nameRequired": "Naam is verplicht",
 | 
				
			||||||
    "required": "Dit veld is verplicht"
 | 
					    "required": "Dit veld is verplicht"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
      "warning": "Jeśli teraz zamkniesz, przesyłanie zostanie anulowane, a wszelki postęp zostanie utracony.",
 | 
					      "warning": "Jeśli teraz zamkniesz, przesyłanie zostanie anulowane, a wszelki postęp zostanie utracony.",
 | 
				
			||||||
      "continue": "Kontynuuj przesyłanie",
 | 
					      "continue": "Kontynuuj przesyłanie",
 | 
				
			||||||
      "cancel": "Anuluj przesyłanie"
 | 
					      "cancel": "Anuluj przesyłanie"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "Upuść pliki, aby przesłać",
 | 
				
			||||||
 | 
					      "description": "Zwolnij, aby przesłać pliki"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {Obraz wklejony i przesłany pomyślnie} other {# obrazy wklejone i przesłane pomyślnie}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "Nazwa jest wymagana",
 | 
					    "nameRequired": "Nazwa jest wymagana",
 | 
				
			||||||
    "required": "To pole jest wymagane"
 | 
					    "required": "To pole jest wymagane"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1433,6 +1433,10 @@
 | 
				
			|||||||
    "fileSizeExceeded": "O tamanho do arquivo excede o limite de {maxsizemb}MB.",
 | 
					    "fileSizeExceeded": "O tamanho do arquivo excede o limite de {maxsizemb}MB.",
 | 
				
			||||||
    "insufficientStorage": "Espaço de armazenamento insuficiente. Você tem {availablespace}MB disponíveis.",
 | 
					    "insufficientStorage": "Espaço de armazenamento insuficiente. Você tem {availablespace}MB disponíveis.",
 | 
				
			||||||
    "unauthorized": "Não autorizado: um token válido é necessário para acessar este recurso.",
 | 
					    "unauthorized": "Não autorizado: um token válido é necessário para acessar este recurso.",
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "Solte arquivos para enviar",
 | 
				
			||||||
 | 
					      "description": "Solte para enviar seus arquivos"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "confirmCancel": {
 | 
					    "confirmCancel": {
 | 
				
			||||||
      "title": "Cancelar Uploads",
 | 
					      "title": "Cancelar Uploads",
 | 
				
			||||||
      "messageSingle": "Há um upload em andamento.",
 | 
					      "messageSingle": "Há um upload em andamento.",
 | 
				
			||||||
@@ -1440,7 +1444,8 @@
 | 
				
			|||||||
      "warning": "Se você fechar agora, os uploads serão cancelados e qualquer progresso será perdido.",
 | 
					      "warning": "Se você fechar agora, os uploads serão cancelados e qualquer progresso será perdido.",
 | 
				
			||||||
      "continue": "Continuar Uploads",
 | 
					      "continue": "Continuar Uploads",
 | 
				
			||||||
      "cancel": "Cancelar Uploads"
 | 
					      "cancel": "Cancelar Uploads"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {Imagem colada e enviada com sucesso} other {# imagens coladas e enviadas com sucesso}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
      "warning": "Если вы закроете сейчас, загрузки будут отменены и весь прогресс будет потерян.",
 | 
					      "warning": "Если вы закроете сейчас, загрузки будут отменены и весь прогресс будет потерян.",
 | 
				
			||||||
      "continue": "Продолжить Загрузку",
 | 
					      "continue": "Продолжить Загрузку",
 | 
				
			||||||
      "cancel": "Отменить Загрузку"
 | 
					      "cancel": "Отменить Загрузку"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "Перетащите файлы для загрузки",
 | 
				
			||||||
 | 
					      "description": "Отпустите, чтобы загрузить файлы"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {Изображение вставлено и успешно загружено} other {# изображений вставлено и успешно загружено}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "Требуется имя",
 | 
					    "nameRequired": "Требуется имя",
 | 
				
			||||||
    "required": "Это поле обязательно"
 | 
					    "required": "Это поле обязательно"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
      "warning": "Şimdi kapatırsanız, yüklemeler iptal olacak ve tüm ilerleme kaybolacak.",
 | 
					      "warning": "Şimdi kapatırsanız, yüklemeler iptal olacak ve tüm ilerleme kaybolacak.",
 | 
				
			||||||
      "continue": "Yüklemeleri Sürdür",
 | 
					      "continue": "Yüklemeleri Sürdür",
 | 
				
			||||||
      "cancel": "Yüklemeleri İptal Et"
 | 
					      "cancel": "Yüklemeleri İptal Et"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "Yüklemek için dosyaları bırakın",
 | 
				
			||||||
 | 
					      "description": "Dosyalarınızı yüklemek için bırakın"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {Görüntü yapıştırıldı ve başarıyla yüklendi} other {# görüntü yapıştırıldı ve başarıyla yüklendi}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "İsim gereklidir",
 | 
					    "nameRequired": "İsim gereklidir",
 | 
				
			||||||
    "required": "Bu alan zorunludur"
 | 
					    "required": "Bu alan zorunludur"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1440,7 +1440,12 @@
 | 
				
			|||||||
      "warning": "如果您现在关闭,上传将被取消,任何进度都将丢失。",
 | 
					      "warning": "如果您现在关闭,上传将被取消,任何进度都将丢失。",
 | 
				
			||||||
      "continue": "继续上传",
 | 
					      "continue": "继续上传",
 | 
				
			||||||
      "cancel": "取消上传"
 | 
					      "cancel": "取消上传"
 | 
				
			||||||
    }
 | 
					    },
 | 
				
			||||||
 | 
					    "globalDrop": {
 | 
				
			||||||
 | 
					      "title": "拖放文件以上传",
 | 
				
			||||||
 | 
					      "description": "松开以上传您的文件"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "pasteSuccess": "{count, plural, =1 {图片已成功粘贴并上传} other {# 张图片已成功粘贴并上传}}"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "users": {
 | 
					  "users": {
 | 
				
			||||||
    "modes": {
 | 
					    "modes": {
 | 
				
			||||||
@@ -1526,4 +1531,4 @@
 | 
				
			|||||||
    "nameRequired": "名称为必填项",
 | 
					    "nameRequired": "名称为必填项",
 | 
				
			||||||
    "required": "此字段为必填项"
 | 
					    "required": "此字段为必填项"
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -4,6 +4,7 @@ import { IconLayoutDashboardFilled } from "@tabler/icons-react";
 | 
				
			|||||||
import { useTranslations } from "next-intl";
 | 
					import { useTranslations } from "next-intl";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ProtectedRoute } from "@/components/auth/protected-route";
 | 
					import { ProtectedRoute } from "@/components/auth/protected-route";
 | 
				
			||||||
 | 
					import { GlobalDropZone } from "@/components/general/global-drop-zone";
 | 
				
			||||||
import { FileManagerLayout } from "@/components/layout/file-manager-layout";
 | 
					import { FileManagerLayout } from "@/components/layout/file-manager-layout";
 | 
				
			||||||
import { LoadingScreen } from "@/components/layout/loading-screen";
 | 
					import { LoadingScreen } from "@/components/layout/loading-screen";
 | 
				
			||||||
import { QuickAccessCards } from "./components/quick-access-cards";
 | 
					import { QuickAccessCards } from "./components/quick-access-cards";
 | 
				
			||||||
@@ -39,39 +40,41 @@ export default function DashboardPage() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <ProtectedRoute>
 | 
					    <ProtectedRoute>
 | 
				
			||||||
      <FileManagerLayout
 | 
					      <GlobalDropZone onSuccess={loadDashboardData}>
 | 
				
			||||||
        breadcrumbLabel={t("dashboard.breadcrumb")}
 | 
					        <FileManagerLayout
 | 
				
			||||||
        icon={<IconLayoutDashboardFilled className="text-xl" />}
 | 
					          breadcrumbLabel={t("dashboard.breadcrumb")}
 | 
				
			||||||
        showBreadcrumb={false}
 | 
					          icon={<IconLayoutDashboardFilled className="text-xl" />}
 | 
				
			||||||
        title={t("dashboard.pageTitle")}
 | 
					          showBreadcrumb={false}
 | 
				
			||||||
      >
 | 
					          title={t("dashboard.pageTitle")}
 | 
				
			||||||
        <StorageUsage diskSpace={diskSpace} diskSpaceError={diskSpaceError} onRetry={handleRetryDiskSpace} />
 | 
					        >
 | 
				
			||||||
        <QuickAccessCards />
 | 
					          <StorageUsage diskSpace={diskSpace} diskSpaceError={diskSpaceError} onRetry={handleRetryDiskSpace} />
 | 
				
			||||||
 | 
					          <QuickAccessCards />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className="flex flex-col gap-6">
 | 
					          <div className="flex flex-col gap-6">
 | 
				
			||||||
          <RecentFiles
 | 
					            <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}
 | 
					            fileManager={fileManager}
 | 
				
			||||||
            files={recentFiles}
 | 
					            modals={modals}
 | 
				
			||||||
            isUploadModalOpen={modals.isUploadModalOpen}
 | 
					 | 
				
			||||||
            onOpenUploadModal={modals.onOpenUploadModal}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <RecentShares
 | 
					 | 
				
			||||||
            isCreateModalOpen={modals.isCreateModalOpen}
 | 
					 | 
				
			||||||
            shareManager={shareManager}
 | 
					            shareManager={shareManager}
 | 
				
			||||||
            shares={recentShares}
 | 
					            onSuccess={loadDashboardData}
 | 
				
			||||||
            onCopyLink={handleCopyLink}
 | 
					 | 
				
			||||||
            onOpenCreateModal={modals.onOpenCreateModal}
 | 
					 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        </div>
 | 
					        </FileManagerLayout>
 | 
				
			||||||
 | 
					      </GlobalDropZone>
 | 
				
			||||||
        <DashboardModals
 | 
					 | 
				
			||||||
          fileManager={fileManager}
 | 
					 | 
				
			||||||
          modals={modals}
 | 
					 | 
				
			||||||
          shareManager={shareManager}
 | 
					 | 
				
			||||||
          onSuccess={loadDashboardData}
 | 
					 | 
				
			||||||
        />
 | 
					 | 
				
			||||||
      </FileManagerLayout>
 | 
					 | 
				
			||||||
    </ProtectedRoute>
 | 
					    </ProtectedRoute>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import { IconFolderOpen } from "@tabler/icons-react";
 | 
				
			|||||||
import { useTranslations } from "next-intl";
 | 
					import { useTranslations } from "next-intl";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { ProtectedRoute } from "@/components/auth/protected-route";
 | 
					import { ProtectedRoute } from "@/components/auth/protected-route";
 | 
				
			||||||
 | 
					import { GlobalDropZone } from "@/components/general/global-drop-zone";
 | 
				
			||||||
import { FileManagerLayout } from "@/components/layout/file-manager-layout";
 | 
					import { FileManagerLayout } from "@/components/layout/file-manager-layout";
 | 
				
			||||||
import { LoadingScreen } from "@/components/layout/loading-screen";
 | 
					import { LoadingScreen } from "@/components/layout/loading-screen";
 | 
				
			||||||
import { Card, CardContent } from "@/components/ui/card";
 | 
					import { Card, CardContent } from "@/components/ui/card";
 | 
				
			||||||
@@ -23,47 +24,49 @@ export default function FilesPage() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <ProtectedRoute>
 | 
					    <ProtectedRoute>
 | 
				
			||||||
      <FileManagerLayout
 | 
					      <GlobalDropZone onSuccess={loadFiles}>
 | 
				
			||||||
        breadcrumbLabel={t("files.breadcrumb")}
 | 
					        <FileManagerLayout
 | 
				
			||||||
        icon={<IconFolderOpen size={20} />}
 | 
					          breadcrumbLabel={t("files.breadcrumb")}
 | 
				
			||||||
        title={t("files.pageTitle")}
 | 
					          icon={<IconFolderOpen size={20} />}
 | 
				
			||||||
      >
 | 
					          title={t("files.pageTitle")}
 | 
				
			||||||
        <Card>
 | 
					        >
 | 
				
			||||||
          <CardContent>
 | 
					          <Card>
 | 
				
			||||||
            <div className="flex flex-col gap-6">
 | 
					            <CardContent>
 | 
				
			||||||
              <Header onUpload={modals.onOpenUploadModal} />
 | 
					              <div className="flex flex-col gap-6">
 | 
				
			||||||
              <FilesViewManager
 | 
					                <Header onUpload={modals.onOpenUploadModal} />
 | 
				
			||||||
                files={filteredFiles}
 | 
					                <FilesViewManager
 | 
				
			||||||
                searchQuery={searchQuery}
 | 
					                  files={filteredFiles}
 | 
				
			||||||
                onSearch={handleSearch}
 | 
					                  searchQuery={searchQuery}
 | 
				
			||||||
                onDelete={fileManager.setFileToDelete}
 | 
					                  onSearch={handleSearch}
 | 
				
			||||||
                onDownload={fileManager.handleDownload}
 | 
					                  onDelete={fileManager.setFileToDelete}
 | 
				
			||||||
                onPreview={fileManager.setPreviewFile}
 | 
					                  onDownload={fileManager.handleDownload}
 | 
				
			||||||
                onRename={fileManager.setFileToRename}
 | 
					                  onPreview={fileManager.setPreviewFile}
 | 
				
			||||||
                onShare={fileManager.setFileToShare}
 | 
					                  onRename={fileManager.setFileToRename}
 | 
				
			||||||
                onBulkDelete={fileManager.handleBulkDelete}
 | 
					                  onShare={fileManager.setFileToShare}
 | 
				
			||||||
                onBulkShare={fileManager.handleBulkShare}
 | 
					                  onBulkDelete={fileManager.handleBulkDelete}
 | 
				
			||||||
                onBulkDownload={fileManager.handleBulkDownload}
 | 
					                  onBulkShare={fileManager.handleBulkShare}
 | 
				
			||||||
                setClearSelectionCallback={fileManager.setClearSelectionCallback}
 | 
					                  onBulkDownload={fileManager.handleBulkDownload}
 | 
				
			||||||
                onUpdateName={(fileId, newName) => {
 | 
					                  setClearSelectionCallback={fileManager.setClearSelectionCallback}
 | 
				
			||||||
                  const file = filteredFiles.find((f) => f.id === fileId);
 | 
					                  onUpdateName={(fileId, newName) => {
 | 
				
			||||||
                  if (file) {
 | 
					                    const file = filteredFiles.find((f) => f.id === fileId);
 | 
				
			||||||
                    fileManager.handleRename(fileId, newName, file.description);
 | 
					                    if (file) {
 | 
				
			||||||
                  }
 | 
					                      fileManager.handleRename(fileId, newName, file.description);
 | 
				
			||||||
                }}
 | 
					                    }
 | 
				
			||||||
                onUpdateDescription={(fileId, newDescription) => {
 | 
					                  }}
 | 
				
			||||||
                  const file = filteredFiles.find((f) => f.id === fileId);
 | 
					                  onUpdateDescription={(fileId, newDescription) => {
 | 
				
			||||||
                  if (file) {
 | 
					                    const file = filteredFiles.find((f) => f.id === fileId);
 | 
				
			||||||
                    fileManager.handleRename(fileId, file.name, newDescription);
 | 
					                    if (file) {
 | 
				
			||||||
                  }
 | 
					                      fileManager.handleRename(fileId, file.name, newDescription);
 | 
				
			||||||
                }}
 | 
					                    }
 | 
				
			||||||
              />
 | 
					                  }}
 | 
				
			||||||
            </div>
 | 
					                />
 | 
				
			||||||
          </CardContent>
 | 
					              </div>
 | 
				
			||||||
        </Card>
 | 
					            </CardContent>
 | 
				
			||||||
 | 
					          </Card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <FilesModals fileManager={fileManager} modals={modals} onSuccess={loadFiles} />
 | 
					          <FilesModals fileManager={fileManager} modals={modals} onSuccess={loadFiles} />
 | 
				
			||||||
      </FileManagerLayout>
 | 
					        </FileManagerLayout>
 | 
				
			||||||
 | 
					      </GlobalDropZone>
 | 
				
			||||||
    </ProtectedRoute>
 | 
					    </ProtectedRoute>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,9 +4,8 @@ import { getLocale } from "next-intl/server";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import "./globals.css";
 | 
					import "./globals.css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Toaster } from "sonner";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { Favicon } from "@/components/layout/favicon";
 | 
					import { Favicon } from "@/components/layout/favicon";
 | 
				
			||||||
 | 
					import { DynamicToaster } from "@/components/ui/dynamic-toaster";
 | 
				
			||||||
import { useAppInfo } from "@/contexts/app-info-context";
 | 
					import { useAppInfo } from "@/contexts/app-info-context";
 | 
				
			||||||
import { AuthProvider } from "@/contexts/auth-context";
 | 
					import { AuthProvider } from "@/contexts/auth-context";
 | 
				
			||||||
import { ShareProvider } from "@/contexts/share-context";
 | 
					import { ShareProvider } from "@/contexts/share-context";
 | 
				
			||||||
@@ -42,8 +41,8 @@ export default async function RootLayout({
 | 
				
			|||||||
            <AuthProvider>
 | 
					            <AuthProvider>
 | 
				
			||||||
              <ShareProvider>{children}</ShareProvider>
 | 
					              <ShareProvider>{children}</ShareProvider>
 | 
				
			||||||
            </AuthProvider>
 | 
					            </AuthProvider>
 | 
				
			||||||
 | 
					            <DynamicToaster />
 | 
				
			||||||
          </ThemeProvider>
 | 
					          </ThemeProvider>
 | 
				
			||||||
          <Toaster position="bottom-right" expand={false} richColors={false} closeButton={false} />
 | 
					 | 
				
			||||||
        </NextIntlClientProvider>
 | 
					        </NextIntlClientProvider>
 | 
				
			||||||
      </body>
 | 
					      </body>
 | 
				
			||||||
    </html>
 | 
					    </html>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										373
									
								
								apps/web/src/components/general/global-drop-zone.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										373
									
								
								apps/web/src/components/general/global-drop-zone.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,373 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useCallback, useEffect, useState } from "react";
 | 
				
			||||||
 | 
					import { IconCloudUpload, IconLoader, IconX } from "@tabler/icons-react";
 | 
				
			||||||
 | 
					import axios from "axios";
 | 
				
			||||||
 | 
					import { useTranslations } from "next-intl";
 | 
				
			||||||
 | 
					import { toast } from "sonner";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Button } from "@/components/ui/button";
 | 
				
			||||||
 | 
					import { Progress } from "@/components/ui/progress";
 | 
				
			||||||
 | 
					import { checkFile, getPresignedUrl, registerFile } from "@/http/endpoints";
 | 
				
			||||||
 | 
					import { getFileIcon } from "@/utils/file-icons";
 | 
				
			||||||
 | 
					import { generateSafeFileName } from "@/utils/file-utils";
 | 
				
			||||||
 | 
					import { formatFileSize } from "@/utils/format-file-size";
 | 
				
			||||||
 | 
					import getErrorData from "@/utils/getErrorData";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface GlobalDropZoneProps {
 | 
				
			||||||
 | 
					  onSuccess?: () => void;
 | 
				
			||||||
 | 
					  children: React.ReactNode;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum UploadStatus {
 | 
				
			||||||
 | 
					  PENDING = "pending",
 | 
				
			||||||
 | 
					  UPLOADING = "uploading",
 | 
				
			||||||
 | 
					  SUCCESS = "success",
 | 
				
			||||||
 | 
					  ERROR = "error",
 | 
				
			||||||
 | 
					  CANCELLED = "cancelled",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FileUpload {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  file: File;
 | 
				
			||||||
 | 
					  status: UploadStatus;
 | 
				
			||||||
 | 
					  progress: number;
 | 
				
			||||||
 | 
					  error?: string;
 | 
				
			||||||
 | 
					  abortController?: AbortController;
 | 
				
			||||||
 | 
					  objectName?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function GlobalDropZone({ onSuccess, children }: GlobalDropZoneProps) {
 | 
				
			||||||
 | 
					  const t = useTranslations();
 | 
				
			||||||
 | 
					  const [isDragOver, setIsDragOver] = useState(false);
 | 
				
			||||||
 | 
					  const [fileUploads, setFileUploads] = useState<FileUpload[]>([]);
 | 
				
			||||||
 | 
					  const [hasShownSuccessToast, setHasShownSuccessToast] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const generateFileId = () => {
 | 
				
			||||||
 | 
					    return Date.now().toString() + Math.random().toString(36).substr(2, 9);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const createFileUpload = (file: File): FileUpload => {
 | 
				
			||||||
 | 
					    const id = generateFileId();
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      id,
 | 
				
			||||||
 | 
					      file,
 | 
				
			||||||
 | 
					      status: UploadStatus.PENDING,
 | 
				
			||||||
 | 
					      progress: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDragOver = useCallback((event: DragEvent) => {
 | 
				
			||||||
 | 
					    event.preventDefault();
 | 
				
			||||||
 | 
					    event.stopPropagation();
 | 
				
			||||||
 | 
					    setIsDragOver(true);
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDragLeave = useCallback((event: DragEvent) => {
 | 
				
			||||||
 | 
					    event.preventDefault();
 | 
				
			||||||
 | 
					    event.stopPropagation();
 | 
				
			||||||
 | 
					    setIsDragOver(false);
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const uploadFile = useCallback(
 | 
				
			||||||
 | 
					    async (fileUpload: FileUpload) => {
 | 
				
			||||||
 | 
					      const { file, id } = fileUpload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const fileName = file.name;
 | 
				
			||||||
 | 
					        const extension = fileName.split(".").pop() || "";
 | 
				
			||||||
 | 
					        const safeObjectName = generateSafeFileName(fileName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          await checkFile({
 | 
				
			||||||
 | 
					            name: fileName,
 | 
				
			||||||
 | 
					            objectName: "checkFile",
 | 
				
			||||||
 | 
					            size: file.size,
 | 
				
			||||||
 | 
					            extension: extension,
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        } catch (error) {
 | 
				
			||||||
 | 
					          console.error("File check failed:", error);
 | 
				
			||||||
 | 
					          const errorData = getErrorData(error);
 | 
				
			||||||
 | 
					          let errorMessage = t("uploadFile.error");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (errorData.code === "fileSizeExceeded") {
 | 
				
			||||||
 | 
					            errorMessage = t(`uploadFile.${errorData.code}`, { maxsizemb: t(`${errorData.details}`) });
 | 
				
			||||||
 | 
					          } else if (errorData.code === "insufficientStorage") {
 | 
				
			||||||
 | 
					            errorMessage = t(`uploadFile.${errorData.code}`, { availablespace: t(`${errorData.details}`) });
 | 
				
			||||||
 | 
					          } else if (errorData.code) {
 | 
				
			||||||
 | 
					            errorMessage = t(`uploadFile.${errorData.code}`);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          setFileUploads((prev) =>
 | 
				
			||||||
 | 
					            prev.map((u) => (u.id === id ? { ...u, status: UploadStatus.ERROR, error: errorMessage } : u))
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setFileUploads((prev) =>
 | 
				
			||||||
 | 
					          prev.map((u) => (u.id === id ? { ...u, status: UploadStatus.UPLOADING, progress: 0 } : u))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const presignedResponse = await getPresignedUrl({
 | 
				
			||||||
 | 
					          filename: safeObjectName.replace(`.${extension}`, ""),
 | 
				
			||||||
 | 
					          extension: extension,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const { url, objectName } = presignedResponse.data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setFileUploads((prev) => prev.map((u) => (u.id === id ? { ...u, objectName } : u)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const abortController = new AbortController();
 | 
				
			||||||
 | 
					        setFileUploads((prev) => prev.map((u) => (u.id === id ? { ...u, abortController } : u)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await axios.put(url, file, {
 | 
				
			||||||
 | 
					          headers: {
 | 
				
			||||||
 | 
					            "Content-Type": file.type,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          signal: abortController.signal,
 | 
				
			||||||
 | 
					          onUploadProgress: (progressEvent: any) => {
 | 
				
			||||||
 | 
					            const progress = (progressEvent.loaded / (progressEvent.total || file.size)) * 100;
 | 
				
			||||||
 | 
					            setFileUploads((prev) => prev.map((u) => (u.id === id ? { ...u, progress: Math.round(progress) } : u)));
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await registerFile({
 | 
				
			||||||
 | 
					          name: fileName,
 | 
				
			||||||
 | 
					          objectName: objectName,
 | 
				
			||||||
 | 
					          size: file.size,
 | 
				
			||||||
 | 
					          extension: extension,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setFileUploads((prev) =>
 | 
				
			||||||
 | 
					          prev.map((u) =>
 | 
				
			||||||
 | 
					            u.id === id ? { ...u, status: UploadStatus.SUCCESS, progress: 100, abortController: undefined } : u
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      } catch (error: any) {
 | 
				
			||||||
 | 
					        if (error.name === "AbortError" || error.code === "ERR_CANCELED") {
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.error("Upload failed:", error);
 | 
				
			||||||
 | 
					        const errorData = getErrorData(error);
 | 
				
			||||||
 | 
					        let errorMessage = t("uploadFile.error");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (errorData.code && errorData.code !== "error") {
 | 
				
			||||||
 | 
					          errorMessage = t(`uploadFile.${errorData.code}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setFileUploads((prev) =>
 | 
				
			||||||
 | 
					          prev.map((u) =>
 | 
				
			||||||
 | 
					            u.id === id ? { ...u, status: UploadStatus.ERROR, error: errorMessage, abortController: undefined } : u
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [t]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleDrop = useCallback(
 | 
				
			||||||
 | 
					    (event: DragEvent) => {
 | 
				
			||||||
 | 
					      event.preventDefault();
 | 
				
			||||||
 | 
					      event.stopPropagation();
 | 
				
			||||||
 | 
					      setIsDragOver(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const files = event.dataTransfer?.files;
 | 
				
			||||||
 | 
					      if (!files || files.length === 0) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const newUploads = Array.from(files).map(createFileUpload);
 | 
				
			||||||
 | 
					      setFileUploads((prev) => [...prev, ...newUploads]);
 | 
				
			||||||
 | 
					      setHasShownSuccessToast(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      newUploads.forEach((upload) => uploadFile(upload));
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [uploadFile]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handlePaste = useCallback(
 | 
				
			||||||
 | 
					    (event: ClipboardEvent) => {
 | 
				
			||||||
 | 
					      event.preventDefault();
 | 
				
			||||||
 | 
					      event.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const items = event.clipboardData?.items;
 | 
				
			||||||
 | 
					      if (!items) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const imageItems = Array.from(items).filter((item) => item.type.startsWith("image/"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (imageItems.length === 0) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const newUploads: FileUpload[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      imageItems.forEach((item) => {
 | 
				
			||||||
 | 
					        const file = item.getAsFile();
 | 
				
			||||||
 | 
					        if (file) {
 | 
				
			||||||
 | 
					          const timestamp = Date.now();
 | 
				
			||||||
 | 
					          const extension = file.type.split("/")[1] || "png";
 | 
				
			||||||
 | 
					          const fileName = `pasted-image-${timestamp}.${extension}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const renamedFile = new File([file], fileName, { type: file.type });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          newUploads.push(createFileUpload(renamedFile));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (newUploads.length > 0) {
 | 
				
			||||||
 | 
					        setFileUploads((prev) => [...prev, ...newUploads]);
 | 
				
			||||||
 | 
					        setHasShownSuccessToast(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        newUploads.forEach((upload) => uploadFile(upload));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        toast.success(t("uploadFile.pasteSuccess", { count: newUploads.length }));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [uploadFile, t]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    document.addEventListener("dragover", handleDragOver);
 | 
				
			||||||
 | 
					    document.addEventListener("dragleave", handleDragLeave);
 | 
				
			||||||
 | 
					    document.addEventListener("drop", handleDrop);
 | 
				
			||||||
 | 
					    document.addEventListener("paste", handlePaste);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      document.removeEventListener("dragover", handleDragOver);
 | 
				
			||||||
 | 
					      document.removeEventListener("dragleave", handleDragLeave);
 | 
				
			||||||
 | 
					      document.removeEventListener("drop", handleDrop);
 | 
				
			||||||
 | 
					      document.removeEventListener("paste", handlePaste);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, [handleDragOver, handleDragLeave, handleDrop, handlePaste]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const removeFile = (fileId: string) => {
 | 
				
			||||||
 | 
					    setFileUploads((prev) => {
 | 
				
			||||||
 | 
					      const upload = prev.find((u) => u.id === fileId);
 | 
				
			||||||
 | 
					      if (upload?.abortController) {
 | 
				
			||||||
 | 
					        upload.abortController.abort();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return prev.filter((u) => u.id !== fileId);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const retryUpload = (fileId: string) => {
 | 
				
			||||||
 | 
					    const upload = fileUploads.find((u) => u.id === fileId);
 | 
				
			||||||
 | 
					    if (upload) {
 | 
				
			||||||
 | 
					      setFileUploads((prev) =>
 | 
				
			||||||
 | 
					        prev.map((u) => (u.id === fileId ? { ...u, status: UploadStatus.PENDING, error: undefined, progress: 0 } : u))
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      uploadFile({ ...upload, status: UploadStatus.PENDING, error: undefined, progress: 0 });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const renderFileIcon = (fileName: string) => {
 | 
				
			||||||
 | 
					    const { icon: FileIcon, color } = getFileIcon(fileName);
 | 
				
			||||||
 | 
					    return <FileIcon size={16} className={color} />;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getStatusIcon = (status: UploadStatus) => {
 | 
				
			||||||
 | 
					    switch (status) {
 | 
				
			||||||
 | 
					      case UploadStatus.UPLOADING:
 | 
				
			||||||
 | 
					        return <IconLoader size={14} className="animate-spin text-blue-500" />;
 | 
				
			||||||
 | 
					      case UploadStatus.SUCCESS:
 | 
				
			||||||
 | 
					        return <IconCloudUpload size={14} className="text-green-500" />;
 | 
				
			||||||
 | 
					      case UploadStatus.ERROR:
 | 
				
			||||||
 | 
					        return <IconX size={14} className="text-red-500" />;
 | 
				
			||||||
 | 
					      default:
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (fileUploads.length > 0) {
 | 
				
			||||||
 | 
					      const allComplete = fileUploads.every(
 | 
				
			||||||
 | 
					        (u) => u.status === UploadStatus.SUCCESS || u.status === UploadStatus.ERROR
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (allComplete && !hasShownSuccessToast) {
 | 
				
			||||||
 | 
					        const successCount = fileUploads.filter((u) => u.status === UploadStatus.SUCCESS).length;
 | 
				
			||||||
 | 
					        const errorCount = fileUploads.filter((u) => u.status === UploadStatus.ERROR).length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (successCount > 0) {
 | 
				
			||||||
 | 
					          toast.success(
 | 
				
			||||||
 | 
					            errorCount > 0
 | 
				
			||||||
 | 
					              ? t("uploadFile.partialSuccess", { success: successCount, error: errorCount })
 | 
				
			||||||
 | 
					              : t("uploadFile.allSuccess", { count: successCount })
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          setHasShownSuccessToast(true);
 | 
				
			||||||
 | 
					          onSuccess?.();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setTimeout(() => {
 | 
				
			||||||
 | 
					          setFileUploads([]);
 | 
				
			||||||
 | 
					          setHasShownSuccessToast(false);
 | 
				
			||||||
 | 
					        }, 3000);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [fileUploads, hasShownSuccessToast, onSuccess, t]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      {children}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {isDragOver && (
 | 
				
			||||||
 | 
					        <div className="fixed inset-0 z-50 dark:bg-black/80 bg-white/90 border-2 border-dashed dark:border-primary/50 border-primary/90 rounded-lg m-1 flex items-center justify-center">
 | 
				
			||||||
 | 
					          <div className="text-center">
 | 
				
			||||||
 | 
					            <IconCloudUpload size={64} className="text-primary mx-auto mb-4" />
 | 
				
			||||||
 | 
					            <h3 className="text-2xl font-bold text-primary mb-2">{t("uploadFile.globalDrop.title")}</h3>
 | 
				
			||||||
 | 
					            <p className="text-lg dark:text-muted-foreground text-black">{t("uploadFile.globalDrop.description")}</p>
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      {fileUploads.length > 0 && (
 | 
				
			||||||
 | 
					        <div className="fixed bottom-4 right-4 z-50 max-w-sm w-full space-y-2">
 | 
				
			||||||
 | 
					          {fileUploads.map((upload) => (
 | 
				
			||||||
 | 
					            <div key={upload.id} className="bg-background border rounded-lg shadow-lg p-3 flex items-center gap-3">
 | 
				
			||||||
 | 
					              <div className="flex-shrink-0">{renderFileIcon(upload.file.name)}</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div className="flex-1 min-w-0">
 | 
				
			||||||
 | 
					                <div className="flex items-center gap-2">
 | 
				
			||||||
 | 
					                  <p className="text-sm font-medium truncate">{upload.file.name}</p>
 | 
				
			||||||
 | 
					                  {getStatusIcon(upload.status)}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <p className="text-xs text-muted-foreground">{formatFileSize(upload.file.size)}</p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                {upload.status === UploadStatus.UPLOADING && (
 | 
				
			||||||
 | 
					                  <div className="mt-1">
 | 
				
			||||||
 | 
					                    <Progress value={upload.progress} className="h-1" />
 | 
				
			||||||
 | 
					                    <p className="text-xs text-muted-foreground mt-1">{upload.progress}%</p>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                {upload.status === UploadStatus.ERROR && upload.error && (
 | 
				
			||||||
 | 
					                  <p className="text-xs text-destructive mt-1">{upload.error}</p>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              <div className="flex-shrink-0">
 | 
				
			||||||
 | 
					                {upload.status === UploadStatus.ERROR ? (
 | 
				
			||||||
 | 
					                  <div className="flex gap-1">
 | 
				
			||||||
 | 
					                    <Button
 | 
				
			||||||
 | 
					                      variant="ghost"
 | 
				
			||||||
 | 
					                      size="sm"
 | 
				
			||||||
 | 
					                      onClick={() => retryUpload(upload.id)}
 | 
				
			||||||
 | 
					                      className="h-6 w-6 p-0"
 | 
				
			||||||
 | 
					                      title={t("uploadFile.retry")}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                      <IconLoader size={12} />
 | 
				
			||||||
 | 
					                    </Button>
 | 
				
			||||||
 | 
					                    <Button variant="ghost" size="sm" onClick={() => removeFile(upload.id)} className="h-6 w-6 p-0">
 | 
				
			||||||
 | 
					                      <IconX size={12} />
 | 
				
			||||||
 | 
					                    </Button>
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                ) : upload.status === UploadStatus.SUCCESS ? null : (
 | 
				
			||||||
 | 
					                  <Button variant="ghost" size="sm" onClick={() => removeFile(upload.id)} className="h-6 w-6 p-0">
 | 
				
			||||||
 | 
					                    <IconX size={12} />
 | 
				
			||||||
 | 
					                  </Button>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          ))}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										20
									
								
								apps/web/src/components/ui/dynamic-toaster.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/web/src/components/ui/dynamic-toaster.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useTheme } from "next-themes";
 | 
				
			||||||
 | 
					import { Toaster } from "sonner";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function DynamicToaster() {
 | 
				
			||||||
 | 
					  const { theme, resolvedTheme } = useTheme();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const currentTheme = resolvedTheme || theme;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <Toaster
 | 
				
			||||||
 | 
					      position="top-right"
 | 
				
			||||||
 | 
					      expand={false}
 | 
				
			||||||
 | 
					      richColors={theme === "dark" ? true : false}
 | 
				
			||||||
 | 
					      closeButton={false}
 | 
				
			||||||
 | 
					      theme={currentTheme as "light" | "dark"}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user