224 lines
7.1 KiB
TypeScript
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;
|