feat: add total storage usage label in multiple languages and enhance storage usage component

- Introduced a new "total" label in various language translation files for storage usage.
- Updated the StorageUsage component to display the total storage size alongside the progress bar.
- Improved layout and styling in the QuickAccessCards component for better visual consistency.
This commit is contained in:
Daniel Luiz Alves
2025-07-03 14:55:53 -03:00
parent 3265f9d1a2
commit 4ea799ae80
23 changed files with 149 additions and 51 deletions

View File

@@ -1401,6 +1401,7 @@
"ariaLabel": "شريط تقدم استخدام التخزين",
"used": "المستخدمة",
"available": "متاح",
"total": "الإجمالي",
"loading": "جارٍ التحميل...",
"retry": "إعادة المحاولة",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "Fortschrittsbalken der Speichernutzung",
"used": "genutzt",
"available": "verfügbar",
"total": "Gesamt",
"loading": "Wird geladen...",
"retry": "Wiederholen",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "Storage usage progress bar",
"used": "used",
"available": "available",
"total": "Total",
"loading": "Loading...",
"retry": "Retry",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "Barra de progreso del uso de almacenamiento",
"used": "usados",
"available": "disponible",
"total": "Total",
"loading": "Cargando...",
"retry": "Reintentar",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "Barre de progression de l'utilisation du stockage",
"used": "utilisé",
"available": "disponible",
"total": "Total",
"loading": "Chargement...",
"retry": "Réessayer",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "स्टोरेज उपयोग प्रगति पट्टी",
"used": "उपयोग किया गया",
"available": "उपलब्ध",
"total": "कुल",
"loading": "लोड हो रहा है...",
"retry": "पुनः प्रयास करें",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "Barra di progresso utilizzo archiviazione",
"used": "utilizzato",
"available": "disponibile",
"total": "Totale",
"loading": "Caricamento...",
"retry": "Riprova",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "ストレージ使用状況のプログレスバー",
"used": "使用済み",
"available": "利用可能",
"total": "合計",
"loading": "読み込み中...",
"retry": "再試行",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "스토리지 사용량 진행 바",
"used": "사용됨",
"available": "사용 가능",
"total": "총계",
"loading": "로딩 중...",
"retry": "다시 시도",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "Opslaggebruik voortgangsbalk",
"used": "gebruikt",
"available": "beschikbaar",
"total": "Totaal",
"loading": "Laden...",
"retry": "Opnieuw proberen",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "Pasek postępu użycia pamięci",
"used": "użyte",
"available": "dostępne",
"total": "Razem",
"loading": "Ładowanie...",
"retry": "Spróbuj ponownie",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "Barra de progresso do uso de armazenamento",
"used": "usado",
"available": "disponível",
"total": "Total",
"loading": "Carregando...",
"retry": "Tentar novamente",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "Индикатор использования хранилища",
"used": "Использовано",
"available": "Доступно",
"total": "Всего",
"loading": "Загрузка...",
"retry": "Повторить",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "Depolama kullanım ilerleme çubuğu",
"used": "kullanıldı",
"available": "kullanılabilir",
"total": "Toplam",
"loading": "Yükleniyor...",
"retry": "Tekrar Dene",
"errors": {

View File

@@ -1399,6 +1399,7 @@
"ariaLabel": "存储使用进度条",
"used": "已使用:",
"available": "可用",
"total": "总计",
"loading": "加载中...",
"retry": "重试",
"errors": {

View File

@@ -11,45 +11,42 @@ export function QuickAccessCards() {
const QUICK_ACCESS_ITEMS = [
{
title: t("quickAccess.files.title"),
icon: <IconFoldersFilled size={28} />,
icon: <IconFoldersFilled size={24} />,
description: t("quickAccess.files.description"),
path: "/files",
color: "bg-primary",
},
{
title: t("quickAccess.shares.title"),
icon: <IconShare2 size={28} />,
icon: <IconShare2 size={24} />,
description: t("quickAccess.shares.description"),
path: "/shares",
color: "bg-orange-400",
},
{
title: t("quickAccess.reverseShares.title"),
icon: <IconDeviceDesktopDown size={28} />,
icon: <IconDeviceDesktopDown size={24} />,
description: t("quickAccess.reverseShares.description"),
path: "/reverse-shares",
color: "bg-blue-500",
},
] as const;
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{QUICK_ACCESS_ITEMS.map((card) => (
<Card
key={card.title}
className="cursor-pointer transform transition-all hover:bg-card/80"
className="cursor-pointer group transition-all duration-400 border-border/50 backdrop-blur-sm h-full hover:opacity-80 "
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`}
>
<CardContent className="h-full">
<div className="flex items-center gap-4 h-full">
<div className="dark:group-hover:bg-accent group-hover:bg-primary/10 w-12 h-12 rounded-lg flex items-center justify-center text-primary/80 group-hover:text-primary dark:bg-accent/60 bg-accent/50 border dark:border-none transition-all duration-400 flex-shrink-0">
{card.icon}
</div>
<div>
<h3 className="text-lg font-semibold">{card.title}</h3>
<p className="text-sm text-muted-foreground">{card.description}</p>
<div className="flex-1 min-w-0">
<h3 className="font-medium text-foreground mb-1 truncate">{card.title}</h3>
<p className="text-sm text-muted-foreground group-hover:text-muted-foreground leading-relaxed">
{card.description}
</p>
</div>
</div>
</CardContent>

View File

@@ -26,10 +26,13 @@ export function StorageUsage({ diskSpace, diskSpaceError, onRetry }: StorageUsag
<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 items-center justify-between">
<h2 className="text-xl font-semibold flex items-center gap-2">
<IconDatabaseCog className="text-gray-500" size={24} />
{t("storageUsage.title")}
</h2>
<span className="text-sm text-muted-foreground">{t("storageUsage.total")}: --</span>
</div>
<div className="flex flex-col gap-3 py-4">
<div className="flex items-center gap-2 text-amber-600 dark:text-amber-400">
<IconAlertCircle size={20} />
@@ -54,10 +57,13 @@ export function StorageUsage({ diskSpace, diskSpaceError, onRetry }: StorageUsag
<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 items-center justify-between">
<h2 className="text-xl font-semibold flex items-center gap-2">
<IconDatabaseCog className="text-gray-500" size={24} />
{t("storageUsage.title")}
</h2>
<span className="text-sm text-muted-foreground">{t("storageUsage.total")}: --</span>
</div>
<div className="flex flex-col gap-2">
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded animate-pulse" />
<div className="flex justify-between text-sm text-muted-foreground">
@@ -75,15 +81,20 @@ export function StorageUsage({ diskSpace, diskSpaceError, onRetry }: StorageUsag
<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 items-center justify-between">
<h2 className="text-xl font-semibold flex items-center gap-2">
<IconDatabaseCog className="text-gray-500" size={24} />
{t("storageUsage.title")}
</h2>
<span className="text-sm text-muted-foreground">
{t("storageUsage.total")}: {formatStorageSize(diskSpace?.diskSizeGB || 0)}
</span>
</div>
<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"
className="w-full h-2"
/>
<div className="flex justify-between text-sm">
<span>

View File

@@ -126,3 +126,25 @@
@apply bg-background text-foreground;
}
}
/* Line clamp utilities */
.line-clamp-1 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.line-clamp-3 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}

View File

@@ -206,7 +206,7 @@ export function GlobalDropZone({ onSuccess, children }: GlobalDropZoneProps) {
if (file) {
const timestamp = Date.now();
const extension = file.type.split("/")[1] || "png";
const fileName = `pasted-image-${timestamp}.${extension}`;
const fileName = `${timestamp}.${extension}`;
const renamedFile = new File([file], fileName, { type: file.type });

View File

@@ -17,6 +17,7 @@ interface FilePreviewModalProps {
objectName: string;
type?: string;
id?: string;
description?: string;
};
isReverseShare?: boolean;
}
@@ -48,6 +49,8 @@ export function FilePreviewModal({ isOpen, onClose, file, isReverseShare = false
pdfAsBlob={previewState.pdfAsBlob}
pdfLoadFailed={previewState.pdfLoadFailed}
onPdfLoadError={previewState.handlePdfLoadError}
description={file.description}
onDownload={previewState.handleDownload}
/>
</div>
<DialogFooter>

View File

@@ -16,6 +16,8 @@ interface FilePreviewRendererProps {
pdfAsBlob: boolean;
pdfLoadFailed: boolean;
onPdfLoadError: () => void;
description?: string;
onDownload?: () => void;
}
export function FilePreviewRenderer({
@@ -28,6 +30,8 @@ export function FilePreviewRenderer({
pdfAsBlob,
pdfLoadFailed,
onPdfLoadError,
description,
onDownload,
}: FilePreviewRendererProps) {
if (isLoading) {
return <DefaultPreview fileName={fileName} isLoading />;
@@ -63,7 +67,7 @@ export function FilePreviewRenderer({
return <TextPreview content={textContent} fileName={fileName} />;
case "image":
return <ImagePreview src={previewUrl!} alt={fileName} />;
return <ImagePreview src={previewUrl!} alt={fileName} description={description} onDownload={onDownload} />;
case "audio":
return <AudioPreview src={mediaUrl!} />;

View File

@@ -1,7 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { IconMaximize, IconX } from "@tabler/icons-react";
import { IconDownload, IconMaximize, IconX } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { createPortal } from "react-dom";
import { AspectRatio } from "@/components/ui/aspect-ratio";
@@ -10,10 +11,13 @@ import { Button } from "@/components/ui/button";
interface ImagePreviewProps {
src: string;
alt: string;
description?: string;
onDownload?: () => void;
}
export function ImagePreview({ src, alt }: ImagePreviewProps) {
export function ImagePreview({ src, alt, description, onDownload }: ImagePreviewProps) {
const [isFullscreen, setIsFullscreen] = useState(false);
const t = useTranslations();
const handleExpandClick = () => {
setIsFullscreen(true);
@@ -29,7 +33,19 @@ export function ImagePreview({ src, alt }: ImagePreviewProps) {
}
};
// Handle ESC key press
const handleDownload = () => {
if (onDownload) {
onDownload();
} else {
const link = document.createElement("a");
link.href = src;
link.download = alt;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape" && isFullscreen) {
@@ -39,7 +55,6 @@ export function ImagePreview({ src, alt }: ImagePreviewProps) {
if (isFullscreen) {
document.addEventListener("keydown", handleKeyDown);
// Prevent body scroll when fullscreen is open
document.body.style.overflow = "hidden";
}
@@ -76,26 +91,56 @@ export function ImagePreview({ src, alt }: ImagePreviewProps) {
typeof window !== "undefined" &&
createPortal(
<div
className="fixed inset-0 z-[99999] bg-black/95 backdrop-blur-sm flex items-center justify-center"
className="fixed inset-0 z-[99999] bg-black/95 backdrop-blur-sm"
onClick={handleBackdropClick}
style={{ margin: 0, padding: 0 }}
>
<Button
variant="outline"
size="icon"
className="cursor-pointer absolute top-6 right-6 z-10 bg-white/10 hover:bg-white/20 text-white border-white/20 h-10 w-10"
onClick={handleCloseFullscreen}
>
<IconX className="h-6 w-6 cursor-pointer" />
</Button>
<div className="fixed top-0 left-0 right-0 bg-transparent h-24 z-[100000] pointer-events-none">
<div className="absolute top-6 right-6 flex gap-2 pointer-events-auto">
<Button
variant="outline"
size="icon"
className="cursor-pointer bg-white/10 hover:bg-white/20 text-white border-white/20 h-10 w-10"
onClick={(e) => {
e.stopPropagation();
handleDownload();
}}
>
<IconDownload className="h-5 w-5" />
</Button>
<Button
variant="outline"
size="icon"
className="cursor-pointer bg-white/10 hover:bg-white/20 text-white border-white/20 h-10 w-10"
onClick={(e) => {
e.stopPropagation();
handleCloseFullscreen();
}}
>
<IconX className="h-6 w-6" />
</Button>
</div>
</div>
<img
src={src}
alt={alt}
className="max-w-screen max-h-screen w-auto h-auto object-contain p-4"
onClick={(e) => e.stopPropagation()}
style={{ maxWidth: "100vw", maxHeight: "100vh" }}
/>
<div className="fixed inset-0 flex items-center justify-center pt-6">
<div className="relative max-w-full max-h-full">
<img
src={src}
alt={alt}
className="max-w-full max-h-screen w-auto h-screen object-contain pb-12"
onClick={(e) => e.stopPropagation()}
/>
</div>
</div>
<div className="fixed bottom-0 left-0 right-0 z-[100000] pointer-events-none">
<div className="absolute bottom-0 left-0 right-0 p-6">
<div className="text-white/30">
<span className=" font-semibold mb-2 truncate">{alt}</span>
{description && <p className="text-sm text-gray-200/20 line-clamp-2">{description}</p>}
</div>
</div>
</div>
</div>,
document.body
)}

View File

@@ -18,6 +18,7 @@ interface FileToDelete {
interface PreviewFile {
name: string;
objectName: string;
description?: string;
}
interface FileToShare {