From d3b7fe04ed6c75c86f9237d79b21e1c11cb86426 Mon Sep 17 00:00:00 2001 From: Daniel Luiz Alves Date: Sun, 1 Jun 2025 00:29:35 -0300 Subject: [PATCH] feat: implement file size input component and update settings configuration - Added a new FileSizeInput component to handle file size configurations in a user-friendly manner. - Updated SettingsInput to integrate the FileSizeInput for maxFileSize and maxTotalStoragePerUser settings. - Revised environment schema to remove MAX_FILESIZE validation, ensuring flexibility in configuration. - Adjusted localization files to remove byte references in descriptions for clarity across multiple languages. --- apps/server/prisma/seed.js | 2 +- apps/server/src/env.ts | 1 - apps/web/messages/ar-SA.json | 4 +- apps/web/messages/de-DE.json | 4 +- apps/web/messages/en-US.json | 4 +- apps/web/messages/es-ES.json | 4 +- apps/web/messages/fr-FR.json | 4 +- apps/web/messages/hi-IN.json | 4 +- apps/web/messages/it-IT.json | 4 +- apps/web/messages/ja-JP.json | 4 +- apps/web/messages/ko-KR.json | 4 +- apps/web/messages/nl-NL.json | 4 +- apps/web/messages/pt-BR.json | 4 +- apps/web/messages/ru-RU.json | 4 +- apps/web/messages/tr-TR.json | 4 +- apps/web/messages/zh-CN.json | 4 +- .../settings/components/file-size-input.tsx | 142 ++++++++++++++++++ .../settings/components/settings-group.tsx | 1 + .../settings/components/settings-input.tsx | 30 +++- apps/web/src/app/settings/types/index.ts | 1 + 20 files changed, 198 insertions(+), 35 deletions(-) create mode 100644 apps/web/src/app/settings/components/file-size-input.tsx diff --git a/apps/server/prisma/seed.js b/apps/server/prisma/seed.js index e36c224..437205c 100644 --- a/apps/server/prisma/seed.js +++ b/apps/server/prisma/seed.js @@ -39,7 +39,7 @@ const defaultConfigs = [ // Storage Configurations { key: "maxFileSize", - value: process.env.MAX_FILESIZE || "1073741824", // default 1GiB in bytes + value: "1073741824", // default 1GiB in bytes type: "bigint", group: "storage", }, diff --git a/apps/server/src/env.ts b/apps/server/src/env.ts index 2076cab..687201a 100644 --- a/apps/server/src/env.ts +++ b/apps/server/src/env.ts @@ -11,7 +11,6 @@ const envSchema = z.object({ S3_REGION: z.string().optional(), S3_BUCKET_NAME: z.string().optional(), S3_FORCE_PATH_STYLE: z.union([z.literal("true"), z.literal("false")]).default("false"), - MAX_FILESIZE: z.string().min(1), }); export const env = envSchema.parse(process.env); diff --git a/apps/web/messages/ar-SA.json b/apps/web/messages/ar-SA.json index 93d86d9..55c7b3f 100644 --- a/apps/web/messages/ar-SA.json +++ b/apps/web/messages/ar-SA.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "أقصى حجم للملف", - "description": "الحد الأقصى لحجم الملف المسموح به للرفع (بالبايت)" + "description": "الحد الأقصى لحجم الملف المسموح به للرفع " }, "maxTotalStoragePerUser": { "title": "أقصى تخزين لكل مستخدم", - "description": "الحد الإجمالي للتخزين لكل مستخدم (بالبايت)" + "description": "الحد الإجمالي للتخزين لكل مستخدم " } }, "buttons": { diff --git a/apps/web/messages/de-DE.json b/apps/web/messages/de-DE.json index 2e3cc0a..29d0518 100644 --- a/apps/web/messages/de-DE.json +++ b/apps/web/messages/de-DE.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "Maximale Dateigröße", - "description": "Maximal erlaubte Dateigröße für Uploads (in Bytes)" + "description": "Maximal erlaubte Dateigröße für Uploads" }, "maxTotalStoragePerUser": { "title": "Maximaler Speicher pro Benutzer", - "description": "Gesamtspeicherlimit pro Benutzer (in Bytes)" + "description": "Gesamtspeicherlimit pro Benutzer" } }, "buttons": { diff --git a/apps/web/messages/en-US.json b/apps/web/messages/en-US.json index 296c6e8..a18948e 100644 --- a/apps/web/messages/en-US.json +++ b/apps/web/messages/en-US.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "Maximum File Size", - "description": "Maximum allowed file size for uploads (in bytes)" + "description": "Maximum allowed file size for uploads" }, "maxTotalStoragePerUser": { "title": "Maximum Storage Per User", - "description": "Total storage limit per user (in bytes)" + "description": "Total storage limit per user" } }, "buttons": { diff --git a/apps/web/messages/es-ES.json b/apps/web/messages/es-ES.json index 43da1de..e4f8a6f 100644 --- a/apps/web/messages/es-ES.json +++ b/apps/web/messages/es-ES.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "Tamaño máximo de archivo", - "description": "Tamaño máximo permitido para subir archivos (en bytes)" + "description": "Tamaño máximo permitido para subir archivos" }, "maxTotalStoragePerUser": { "title": "Almacenamiento máximo por usuario", - "description": "Límite total de almacenamiento por usuario (en bytes)" + "description": "Límite total de almacenamiento por usuario" } }, "buttons": { diff --git a/apps/web/messages/fr-FR.json b/apps/web/messages/fr-FR.json index 275ea17..c4bc419 100644 --- a/apps/web/messages/fr-FR.json +++ b/apps/web/messages/fr-FR.json @@ -402,11 +402,11 @@ }, "maxFileSize": { "title": "Taille Maximale de Fichier", - "description": "Taille maximale autorisée pour les fichiers (en octets)" + "description": "Taille maximale autorisée pour les fichiers" }, "maxTotalStoragePerUser": { "title": "Stockage Maximum par Utilisateur", - "description": "Limite totale de stockage par utilisateur (en octets)" + "description": "Limite totale de stockage par utilisateur" } }, "buttons": { diff --git a/apps/web/messages/hi-IN.json b/apps/web/messages/hi-IN.json index 0106b1b..17e5211 100644 --- a/apps/web/messages/hi-IN.json +++ b/apps/web/messages/hi-IN.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "अधिकतम फाइल आकार", - "description": "अपलोड के लिए अधिकतम अनुमत फाइल आकार (बाइट में)" + "description": "अपलोड के लिए अधिकतम अनुमत फाइल आकार" }, "maxTotalStoragePerUser": { "title": "प्रति उपयोगकर्ता अधिकतम स्टोरेज", - "description": "प्रति उपयोगकर्ता कुल स्टोरेज सीमा (बाइट में)" + "description": "प्रति उपयोगकर्ता कुल स्टोरेज सीमा" } }, "buttons": { diff --git a/apps/web/messages/it-IT.json b/apps/web/messages/it-IT.json index 148ca93..c959f9f 100644 --- a/apps/web/messages/it-IT.json +++ b/apps/web/messages/it-IT.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "Dimensione Massima File", - "description": "Dimensione massima consentita per i caricamenti (in byte)" + "description": "Dimensione massima consentita per i caricamenti" }, "maxTotalStoragePerUser": { "title": "Archiviazione Massima per Utente", - "description": "Limite totale di archiviazione per utente (in byte)" + "description": "Limite totale di archiviazione per utente" } }, "buttons": { diff --git a/apps/web/messages/ja-JP.json b/apps/web/messages/ja-JP.json index edaf09b..b520d38 100644 --- a/apps/web/messages/ja-JP.json +++ b/apps/web/messages/ja-JP.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "最大ファイルサイズ", - "description": "アップロード可能な最大ファイルサイズ(バイト単位)" + "description": "アップロード可能な最大ファイルサイズ" }, "maxTotalStoragePerUser": { "title": "ユーザーごとの最大ストレージ", - "description": "ユーザーごとの総ストレージ制限(バイト単位)" + "description": "ユーザーごとの総ストレージ制限" } }, "buttons": { diff --git a/apps/web/messages/ko-KR.json b/apps/web/messages/ko-KR.json index 5df3703..6b35178 100644 --- a/apps/web/messages/ko-KR.json +++ b/apps/web/messages/ko-KR.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "최대 파일 크기", - "description": "업로드 가능한 최대 파일 크기(바이트 단위)" + "description": "업로드 가능한 최대 파일 크기" }, "maxTotalStoragePerUser": { "title": "사용자당 최대 스토리지", - "description": "사용자당 총 스토리지 제한(바이트 단위)" + "description": "사용자당 총 스토리지 제한" } }, "buttons": { diff --git a/apps/web/messages/nl-NL.json b/apps/web/messages/nl-NL.json index 9daad02..47d8f40 100644 --- a/apps/web/messages/nl-NL.json +++ b/apps/web/messages/nl-NL.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "Maximum Bestandsgrootte", - "description": "Maximum toegestane bestandsgrootte voor uploads (in bytes)" + "description": "Maximum toegestane bestandsgrootte voor uploads" }, "maxTotalStoragePerUser": { "title": "Maximum Opslag Per Gebruiker", - "description": "Totale opslaglimiet per gebruiker (in bytes)" + "description": "Totale opslaglimiet per gebruiker" } }, "buttons": { diff --git a/apps/web/messages/pt-BR.json b/apps/web/messages/pt-BR.json index 2017796..f6bf0a4 100644 --- a/apps/web/messages/pt-BR.json +++ b/apps/web/messages/pt-BR.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "Tamanho Máximo do Arquivo", - "description": "Tamanho máximo permitido para uploads (em bytes)" + "description": "Tamanho máximo permitido para uploads" }, "maxTotalStoragePerUser": { "title": "Armazenamento Máximo por Usuário", - "description": "Limite total de armazenamento por usuário (em bytes)" + "description": "Limite total de armazenamento por usuário" } }, "buttons": { diff --git a/apps/web/messages/ru-RU.json b/apps/web/messages/ru-RU.json index f7c03d8..5c76f7b 100644 --- a/apps/web/messages/ru-RU.json +++ b/apps/web/messages/ru-RU.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "Максимальный размер файла", - "description": "Максимальный размер файла для загрузки (в байтах)" + "description": "Максимальный размер файла для загрузки" }, "maxTotalStoragePerUser": { "title": "Максимальное хранилище для пользователя", - "description": "Общий лимит хранилища для каждого пользователя (в байтах)" + "description": "Общий лимит хранилища для каждого пользователя" } }, "buttons": { diff --git a/apps/web/messages/tr-TR.json b/apps/web/messages/tr-TR.json index afc2ebf..53f89be 100644 --- a/apps/web/messages/tr-TR.json +++ b/apps/web/messages/tr-TR.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "Maksimum Dosya Boyutu", - "description": "Yüklemeye izin verilen maksimum dosya boyutu (bayt cinsinden)" + "description": "Yüklemeye izin verilen maksimum dosya boyutu" }, "maxTotalStoragePerUser": { "title": "Kullanıcı Başına Maksimum Depolama", - "description": "Kullanıcı başına toplam depolama sınırı (bayt cinsinden)" + "description": "Kullanıcı başına toplam depolama sınırı" } }, "buttons": { diff --git a/apps/web/messages/zh-CN.json b/apps/web/messages/zh-CN.json index bbc2572..13e24e0 100644 --- a/apps/web/messages/zh-CN.json +++ b/apps/web/messages/zh-CN.json @@ -399,11 +399,11 @@ }, "maxFileSize": { "title": "最大文件大小", - "description": "允许上传的最大文件大小(以字节为单位)" + "description": "允许上传的最大文件大小" }, "maxTotalStoragePerUser": { "title": "每个用户的最大存储量", - "description": "每个用户的总存储限制(以字节为单位)" + "description": "每个用户的总存储限制" } }, "buttons": { diff --git a/apps/web/src/app/settings/components/file-size-input.tsx b/apps/web/src/app/settings/components/file-size-input.tsx new file mode 100644 index 0000000..a4f0095 --- /dev/null +++ b/apps/web/src/app/settings/components/file-size-input.tsx @@ -0,0 +1,142 @@ +import { useEffect, useState } from "react"; + +import { Input } from "@/components/ui/input"; + +export interface FileSizeInputProps { + value: string; // valor em bytes + onChange: (value: string) => void; + disabled?: boolean; + error?: any; +} + +type Unit = "MB" | "GB" | "TB"; + +const UNIT_MULTIPLIERS: Record = { + MB: 1024 * 1024, + GB: 1024 * 1024 * 1024, + TB: 1024 * 1024 * 1024 * 1024, +}; + +function bytesToHumanReadable(bytes: string): { value: string; unit: Unit } { + const numBytes = parseInt(bytes, 10); + + // Se for 0 ou inválido, retorna 0 GB + if (!numBytes || numBytes <= 0) { + return { value: "0", unit: "GB" }; + } + + // Verifica TB (com tolerância para valores próximos) + if (numBytes >= UNIT_MULTIPLIERS.TB) { + const tbValue = numBytes / UNIT_MULTIPLIERS.TB; + if (tbValue === Math.floor(tbValue)) { + return { + value: tbValue.toString(), + unit: "TB", + }; + } + } + + // Verifica GB (com tolerância para valores próximos) + if (numBytes >= UNIT_MULTIPLIERS.GB) { + const gbValue = numBytes / UNIT_MULTIPLIERS.GB; + if (gbValue === Math.floor(gbValue)) { + return { + value: gbValue.toString(), + unit: "GB", + }; + } + } + + // Verifica MB + if (numBytes >= UNIT_MULTIPLIERS.MB) { + const mbValue = numBytes / UNIT_MULTIPLIERS.MB; + return { + value: mbValue === Math.floor(mbValue) ? mbValue.toString() : mbValue.toFixed(2), + unit: "MB", + }; + } + + // Para valores menores que 1MB, converte para MB com decimais + const mbValue = numBytes / UNIT_MULTIPLIERS.MB; + return { + value: mbValue.toFixed(3), + unit: "MB", + }; +} + +function humanReadableToBytes(value: string, unit: Unit): string { + const numValue = parseFloat(value); + if (isNaN(numValue) || numValue <= 0) { + return "0"; + } + + return Math.floor(numValue * UNIT_MULTIPLIERS[unit]).toString(); +} + +export function FileSizeInput({ value, onChange, disabled = false, error }: FileSizeInputProps) { + const [displayValue, setDisplayValue] = useState(""); + const [selectedUnit, setSelectedUnit] = useState("GB"); + + // Inicializar os valores quando o componente monta ou o value muda + useEffect(() => { + if (value && value !== "0") { + const { value: humanValue, unit } = bytesToHumanReadable(value); + setDisplayValue(humanValue); + setSelectedUnit(unit); + } else { + setDisplayValue(""); + setSelectedUnit("GB"); + } + }, [value]); + + const handleValueChange = (newValue: string) => { + // Só permitir números e ponto decimal + const sanitizedValue = newValue.replace(/[^0-9.]/g, ""); + + // Prevenir múltiplos pontos decimais + const parts = sanitizedValue.split("."); + const finalValue = parts.length > 2 ? parts[0] + "." + parts.slice(1).join("") : sanitizedValue; + + setDisplayValue(finalValue); + + if (finalValue === "" || finalValue === "0") { + onChange("0"); + } else { + const bytesValue = humanReadableToBytes(finalValue, selectedUnit); + onChange(bytesValue); + } + }; + + const handleUnitChange = (newUnit: Unit) => { + setSelectedUnit(newUnit); + + if (displayValue && displayValue !== "0") { + const bytesValue = humanReadableToBytes(displayValue, newUnit); + onChange(bytesValue); + } + }; + + return ( +
+ handleValueChange(e.target.value)} + placeholder="0" + className="flex-1" + disabled={disabled} + aria-invalid={!!error} + /> + +
+ ); +} diff --git a/apps/web/src/app/settings/components/settings-group.tsx b/apps/web/src/app/settings/components/settings-group.tsx index f7a11d7..7dfc2f4 100644 --- a/apps/web/src/app/settings/components/settings-group.tsx +++ b/apps/web/src/app/settings/components/settings-group.tsx @@ -52,6 +52,7 @@ export function SettingsGroup({ group, configs, form, isCollapsed, onToggleColla config={config} error={form.formState.errors.configs?.[config.key]} register={form.register} + setValue={form.setValue} smtpEnabled={form.watch("configs.smtpEnabled")} watch={form.watch} /> diff --git a/apps/web/src/app/settings/components/settings-input.tsx b/apps/web/src/app/settings/components/settings-input.tsx index 9b00b03..8061cd4 100644 --- a/apps/web/src/app/settings/components/settings-input.tsx +++ b/apps/web/src/app/settings/components/settings-input.tsx @@ -1,20 +1,22 @@ import { useTranslations } from "next-intl"; -import { UseFormRegister, UseFormWatch } from "react-hook-form"; +import { UseFormRegister, UseFormSetValue, UseFormWatch } from "react-hook-form"; import { Input } from "@/components/ui/input"; import { createFieldTitles } from "../constants"; import { Config } from "../types"; +import { FileSizeInput } from "./file-size-input"; import { LogoInput } from "./logo-input"; export interface ConfigInputProps { config: Config; register: UseFormRegister; watch: UseFormWatch; + setValue: UseFormSetValue; error?: any; smtpEnabled?: string; } -export function SettingsInput({ config, register, watch, error, smtpEnabled }: ConfigInputProps) { +export function SettingsInput({ config, register, watch, setValue, error, smtpEnabled }: ConfigInputProps) { const t = useTranslations(); const FIELD_TITLES = createFieldTitles(t); const isSmtpField = config.group === "email" && config.key !== "smtpEnabled"; @@ -31,9 +33,7 @@ export function SettingsInput({ config, register, watch, error, smtpEnabled }: C isDisabled={isDisabled} value={value} onChange={(value) => { - register(`configs.${config.key}`).onChange({ - target: { value }, - }); + setValue(`configs.${config.key}`, value, { shouldDirty: true }); }} /> {error &&

{error.message}

} @@ -41,6 +41,26 @@ export function SettingsInput({ config, register, watch, error, smtpEnabled }: C ); } + // Special input for file size configurations + if (config.key === "maxFileSize" || config.key === "maxTotalStoragePerUser") { + const value = watch(`configs.${config.key}`); + + return ( +
+ + { + setValue(`configs.${config.key}`, value, { shouldDirty: true }); + }} + disabled={isDisabled} + error={error} + /> + {error &&

{error.message}

} +
+ ); + } + switch (config.type) { case "boolean": return ( diff --git a/apps/web/src/app/settings/types/index.ts b/apps/web/src/app/settings/types/index.ts index b3cd9c8..926facd 100644 --- a/apps/web/src/app/settings/types/index.ts +++ b/apps/web/src/app/settings/types/index.ts @@ -24,6 +24,7 @@ export interface SettingsGroupProps { export interface ConfigInputProps { config: Config; register: UseFormReturn["register"]; + setValue: UseFormReturn["setValue"]; error?: any; smtpEnabled?: string; }