feat: enhance settings page with tab icons and grouping
feat(files-settings): improve file settings preview and layout feat(locales): update translations for tab groups and add preview labels
This commit is contained in:
@@ -1,9 +1,173 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Folder, FolderOpen, FileText, FileCode, ImageIcon, FileAudio, File, Home, ChevronRight, ChevronDown } from "lucide-react";
|
||||
import { SettingsSection, SettingItem, ToggleSwitch, RadioGroup } from "./settings-section";
|
||||
import { loadFilesSettings, saveFilesSettings, type FilesSettings, type FolderLayout } from "@/components/files/files-settings-dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SampleFile {
|
||||
name: string;
|
||||
isFolder: boolean;
|
||||
size: number;
|
||||
modified: string;
|
||||
hidden?: boolean;
|
||||
thumbnailUrl?: string;
|
||||
}
|
||||
|
||||
const SAMPLE_FILES: SampleFile[] = [
|
||||
{ name: "Documents", isFolder: true, size: 0, modified: "2026-03-10" },
|
||||
{ name: "Photos", isFolder: true, size: 0, modified: "2026-03-14" },
|
||||
{ name: "report.pdf", isFolder: false, size: 245000, modified: "2026-03-15" },
|
||||
{ name: "notes.md", isFolder: false, size: 1200, modified: "2026-03-12" },
|
||||
{ name: "vacation.jpg", isFolder: false, size: 3400000, modified: "2026-03-08", thumbnailUrl: "/branding/Bulwark_Logo_Color.png" },
|
||||
{ name: "song.mp3", isFolder: false, size: 5200000, modified: "2026-03-01" },
|
||||
{ name: ".config", isFolder: false, size: 340, modified: "2026-02-20", hidden: true },
|
||||
];
|
||||
|
||||
function getPreviewIcon(file: SampleFile, colored: boolean, size: "sm" | "lg") {
|
||||
const cls = size === "sm" ? "w-4 h-4 flex-shrink-0" : "w-8 h-8 flex-shrink-0";
|
||||
|
||||
if (file.isFolder) {
|
||||
return <Folder className={cn(cls, colored ? "text-blue-500" : "text-muted-foreground")} />;
|
||||
}
|
||||
|
||||
const ext = file.name.split(".").pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case "jpg": case "png": case "gif":
|
||||
return <ImageIcon className={cn(cls, colored ? "text-emerald-500" : "text-muted-foreground")} />;
|
||||
case "mp3": case "wav":
|
||||
return <FileAudio className={cn(cls, colored ? "text-purple-500" : "text-muted-foreground")} />;
|
||||
case "pdf":
|
||||
return <FileText className={cn(cls, colored ? "text-red-600" : "text-muted-foreground")} />;
|
||||
case "md": case "json": case "js": case "ts":
|
||||
return <FileCode className={cn(cls, colored ? "text-yellow-600" : "text-muted-foreground")} />;
|
||||
default:
|
||||
return <File className={cn(cls, "text-muted-foreground")} />;
|
||||
}
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes === 0) return "—";
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
function FilesSettingsPreview({ settings }: { settings: FilesSettings }) {
|
||||
const sortedFiles = useMemo(() => {
|
||||
let files = SAMPLE_FILES.filter((f) => {
|
||||
if (!settings.showHiddenFiles && f.hidden) return false;
|
||||
if (settings.folderLayout === "sidebar" && f.isFolder) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
files.sort((a, b) => {
|
||||
// Folders first
|
||||
if (a.isFolder !== b.isFolder) return a.isFolder ? -1 : 1;
|
||||
|
||||
let cmp = 0;
|
||||
switch (settings.defaultSortKey) {
|
||||
case "name": cmp = a.name.localeCompare(b.name); break;
|
||||
case "size": cmp = a.size - b.size; break;
|
||||
case "modified": cmp = a.modified.localeCompare(b.modified); break;
|
||||
}
|
||||
return settings.defaultSortDir === "desc" ? -cmp : cmp;
|
||||
});
|
||||
|
||||
return files;
|
||||
}, [settings.showHiddenFiles, settings.folderLayout, settings.defaultSortKey, settings.defaultSortDir]);
|
||||
|
||||
const listView = (
|
||||
<div className="flex-1 min-w-0 overflow-hidden">
|
||||
<div className="flex items-center gap-3 px-2 py-1 text-[10px] font-medium text-muted-foreground border-b border-border bg-muted/50">
|
||||
<span className="flex-1 min-w-0">Name</span>
|
||||
<span className="w-14 text-right">Size</span>
|
||||
<span className="w-16 text-right">Modified</span>
|
||||
</div>
|
||||
{sortedFiles.map((file) => (
|
||||
<div
|
||||
key={file.name}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-2 py-1.5 border-b border-border last:border-b-0 transition-colors hover:bg-muted/50",
|
||||
file.hidden && "opacity-50"
|
||||
)}
|
||||
>
|
||||
{settings.showThumbnails && file.thumbnailUrl ? (
|
||||
<img src={file.thumbnailUrl} alt="" className="w-4 h-4 rounded object-cover flex-shrink-0" />
|
||||
) : settings.showIcons ? (
|
||||
getPreviewIcon(file, settings.coloredIcons, "sm")
|
||||
) : null}
|
||||
<span className={cn("flex-1 min-w-0 truncate text-[11px]", file.isFolder && "font-medium")}>
|
||||
{file.name}
|
||||
</span>
|
||||
<span className="w-14 text-right text-[10px] text-muted-foreground tabular-nums">
|
||||
{formatSize(file.size)}
|
||||
</span>
|
||||
<span className="w-16 text-right text-[10px] text-muted-foreground tabular-nums">
|
||||
{file.modified.slice(5)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const gridView = (
|
||||
<div className="flex-1 min-w-0 p-2">
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(4.5rem,1fr))] gap-1.5">
|
||||
{sortedFiles.map((file) => (
|
||||
<div
|
||||
key={file.name}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1 p-2 rounded-md transition-colors hover:bg-muted/50",
|
||||
file.hidden && "opacity-50"
|
||||
)}
|
||||
>
|
||||
{settings.showThumbnails && file.thumbnailUrl ? (
|
||||
<img src={file.thumbnailUrl} alt="" className="w-8 h-8 rounded object-cover flex-shrink-0" />
|
||||
) : settings.showIcons ? (
|
||||
getPreviewIcon(file, settings.coloredIcons, "lg")
|
||||
) : (
|
||||
<div className="w-8 h-8" />
|
||||
)}
|
||||
<span className={cn("text-[9px] truncate w-full text-center", file.isFolder && "font-medium")}>
|
||||
{file.name}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const sidebar = settings.folderLayout === "sidebar" && (
|
||||
<div className="w-24 border-r border-border bg-muted/30 py-1.5 flex-shrink-0">
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium text-foreground">
|
||||
<Home className="w-3 h-3 flex-shrink-0" />
|
||||
<span className="truncate">Files</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 text-[10px] text-foreground bg-accent rounded-sm mx-1">
|
||||
<ChevronDown className="w-2.5 h-2.5 flex-shrink-0" />
|
||||
<FolderOpen className="w-3 h-3 flex-shrink-0 text-blue-500" />
|
||||
<span className="truncate">Documents</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 px-2 py-0.5 text-[10px] text-muted-foreground" style={{ paddingLeft: "1.25rem" }}>
|
||||
<ChevronRight className="w-2.5 h-2.5 flex-shrink-0" />
|
||||
<Folder className="w-3 h-3 flex-shrink-0 text-blue-500" />
|
||||
<span className="truncate">Photos</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-4 rounded-lg border border-border overflow-hidden bg-background text-xs select-none">
|
||||
<div className="flex" style={{ minHeight: "10rem" }}>
|
||||
{sidebar}
|
||||
{settings.defaultViewMode === "grid" ? gridView : listView}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FilesSettingsComponent() {
|
||||
const t = useTranslations("settings.files");
|
||||
@@ -29,8 +193,14 @@ export function FilesSettingsComponent() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<SettingsSection title={t("display.title")} description={t("display.description")}>
|
||||
<div>
|
||||
<div className="sticky top-0 z-10 bg-background pb-4 -mx-4 px-4 -mt-4 pt-4 lg:-mx-6 lg:px-6 lg:-mt-6 lg:pt-6 border-b border-border mb-6">
|
||||
<p className="text-sm font-medium text-foreground mb-1">{t("preview.label")}</p>
|
||||
<FilesSettingsPreview settings={settings} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
<SettingsSection title={t("display.title")} description={t("display.description")}>
|
||||
<SettingItem label={t("folder_layout.label")} description={t("folder_layout.description")}>
|
||||
<RadioGroup
|
||||
value={settings.folderLayout}
|
||||
@@ -104,6 +274,7 @@ export function FilesSettingsComponent() {
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user