Files
saas_frontend/src/application/theme/components/AllPalettes.tsx
T
2026-08-31 20:40:20 -04:00

224 lines
7.1 KiB
TypeScript

import React, { useCallback, useEffect, useState, useMemo } from "react";
import { Plus, Edit2, Trash2 } from "lucide-react";
import { CustomButton } from "../../../components/custom";
import DataTable, { type ColumnDef } from "../../../components/custom/CustomTable";
import { CustomActionMenu, CustomActionItem, CustomStatus, CustomConfirmationModal } from "../../../components/custom";
import { paletteApi } from "../PaletteApi";
import type { ColorPalette, ColorPalettePayload } from "../ThemeTypes";
import PaletteForm from "./PaletteForm";
import { useTheme } from "../../../context/ThemeContext";
import { useTranslation } from "react-i18next";
const AllPalettes: React.FC = () => {
const { t } = useTranslation(['theme', 'common']);
const [palettes, setPalettes] = useState<ColorPalette[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingPalette, setEditingPalette] = useState<ColorPalette | null>(null);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deleteId, setDeleteId] = useState<string | null>(null);
const { refreshTheme } = useTheme();
const fetchPalettes = useCallback(async () => {
setIsLoading(true);
setErrorMessage("");
try {
const data = await paletteApi.getAllPalettes();
setPalettes(data);
} catch (error) {
const message = error instanceof Error ? error.message : t('theme:errors.fetchFailed');
setErrorMessage(message);
console.error("Failed to fetch palettes", error);
} finally {
setIsLoading(false);
}
}, [t]);
useEffect(() => {
fetchPalettes();
}, [fetchPalettes]);
const handleCreate = () => {
setEditingPalette(null);
setIsModalOpen(true);
};
const handleEdit = (palette: ColorPalette) => {
setEditingPalette(palette);
setIsModalOpen(true);
};
const handleDelete = (id: string, isDefault: boolean) => {
if (isDefault) return;
setDeleteId(id);
setIsDeleteModalOpen(true);
};
const handleConfirmDelete = async () => {
if (!deleteId) return;
try {
await paletteApi.deletePalette(deleteId);
fetchPalettes();
} catch (error) {
console.error("Failed to delete palette", error);
} finally {
setIsDeleteModalOpen(false);
setDeleteId(null);
}
};
const handleSubmit = async (data: ColorPalettePayload) => {
setIsLoading(true);
try {
if (editingPalette) {
await paletteApi.updatePalette(editingPalette.id, data);
} else {
await paletteApi.createPalette(data);
}
setIsModalOpen(false);
fetchPalettes();
refreshTheme();
} finally {
setIsLoading(false);
}
};
const columns = useMemo<ColumnDef<ColorPalette>[]>(
() => [
{
key: "name",
header: t('theme:table.name'),
searchable: true,
render: (row) => (
<div>
<div className="font-medium text-(--text-primary)">{row.name}</div>
{row.description && (
<div className="text-xs text-(--text-secondary)">{row.description}</div>
)}
</div>
),
},
{
key: "is_default",
header: t('theme:table.default'),
render: (row) => (
row.is_default ? (
<CustomStatus status={t('common:status.default')} variant="success" />
) : null
),
},
{
key: "colors",
header: t('theme:table.preview'),
render: (row) => (
<div className="flex items-center gap-1">
<div
className="w-6 h-6 rounded-full border border-(--card-border) shadow-sm"
style={{ backgroundColor: row.colors.primary }}
title={t('theme:colors.primary')}
/>
<div
className="w-6 h-6 rounded-full border border-(--card-border) shadow-sm"
style={{ backgroundColor: row.colors.sidebar_bg }}
title={t('theme:colors.sidebar')}
/>
<div
className="w-6 h-6 rounded-full border border-(--card-border) shadow-sm"
style={{ backgroundColor: row.colors.header_bg }}
title={t('theme:colors.header')}
/>
</div>
),
},
{
key: "actions",
header: t('common:columns.actions'),
render: (row) => (
<CustomActionMenu>
<CustomActionItem onClick={() => handleEdit(row)}>
<Edit2 size={16} className="mr-2" /> {t('theme:actions.edit')}
</CustomActionItem>
<CustomActionItem
onClick={() => handleDelete(row.id, row.is_default)}
className={`${row.is_default ? "opacity-50 cursor-not-allowed" : "text-red-600 hover:text-red-700 hover:bg-red-50"}`}
>
<div
className="flex items-center w-full"
onClick={(e) => {
if (row.is_default) {
e.stopPropagation();
return;
}
}}
>
<Trash2 size={16} className="mr-2" /> {t('theme:actions.delete')}
</div>
</CustomActionItem>
</CustomActionMenu>
),
},
],
[t]
);
return (
<>
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold text-(--text-primary)">{t('theme:title')}</h1>
<p className="text-sm text-(--text-secondary)">
Manage system color themes and default palettes.
</p>
</div>
<CustomButton onClick={handleCreate} leftIcon={<Plus size={18} />}>
{t('theme:actions.create')}
</CustomButton>
</div>
{errorMessage ? (
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
{errorMessage}
</div>
) : (
<DataTable
isLoading={isLoading}
data={palettes}
columns={columns}
columnVisibilityEnabled
columnVisibilityStorageKey="palettes-table-columns"
exportEnabled={true}
enableSearchDropdown={true}
search="name"
buildSuggestionLabel={(row) => row.name}
/>
)}
<PaletteForm
isOpen={isModalOpen}
initialData={editingPalette}
onSubmit={handleSubmit}
onCancel={() => setIsModalOpen(false)}
isLoading={isLoading}
/>
</div>
<CustomConfirmationModal
isOpen={isDeleteModalOpen}
onConfirm={handleConfirmDelete}
onClose={() => setIsDeleteModalOpen(false)}
title={t('theme:deleteModal.title')}
description={t('theme:deleteModal.message')}
confirmText={t('theme:deleteModal.confirm')}
cancelText={t('theme:deleteModal.cancel')}
isLoading={isLoading}
/>
</>
);
};
export default AllPalettes;