x
This commit is contained in:
1 parent
c2269ea090
commit
14d3282a81
8 files changed
+226
-39
No files matched your search
@@ -37,6 +37,8 @@ export default function ProductsPage() {
|
|||||||
const activeCategory = searchParams.get('category') ? Number(searchParams.get('category')) : null
|
const activeCategory = searchParams.get('category') ? Number(searchParams.get('category')) : null
|
||||||
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null)
|
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null)
|
||||||
const [isGalleryOpen, setIsGalleryOpen] = useState(false)
|
const [isGalleryOpen, setIsGalleryOpen] = useState(false)
|
||||||
|
const [selectedIds, setSelectedIds] = useState<number[]>([])
|
||||||
|
const [isBulkDeleting, setIsBulkDeleting] = useState(false)
|
||||||
|
|
||||||
// Pagination State
|
// Pagination State
|
||||||
const page = searchParams.get('page') ? Number(searchParams.get('page')) : 1
|
const page = searchParams.get('page') ? Number(searchParams.get('page')) : 1
|
||||||
@@ -188,11 +190,31 @@ export default function ProductsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isAllSelected = products.length > 0 && products.every(p => selectedIds.includes(p.id))
|
||||||
|
const isSomeSelected = products.some(p => selectedIds.includes(p.id)) && !isAllSelected
|
||||||
|
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
if (isAllSelected) {
|
||||||
|
const pageIds = new Set(products.map(p => p.id))
|
||||||
|
setSelectedIds(prev => prev.filter(id => !pageIds.has(id)))
|
||||||
|
} else {
|
||||||
|
const pageIds = products.map(p => p.id)
|
||||||
|
setSelectedIds(prev => Array.from(new Set([...prev, ...pageIds])))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleSelect = (id: number) => {
|
||||||
|
setSelectedIds(prev =>
|
||||||
|
prev.includes(id) ? prev.filter(item => item !== id) : [...prev, id]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const handleDelete = async (id: number) => {
|
const handleDelete = async (id: number) => {
|
||||||
if (!confirm(t('common.confirmDelete', 'Are you sure you want to delete this product?'))) return
|
if (!confirm(t('common.confirmDelete', 'Are you sure you want to delete this product?'))) return
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE}/products/${id}`, { method: 'DELETE' })
|
const res = await fetch(`${API_BASE}/products/${id}`, { method: 'DELETE' })
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
setSelectedIds(prev => prev.filter(item => item !== id))
|
||||||
fetchData()
|
fetchData()
|
||||||
} else {
|
} else {
|
||||||
const errorData = await res.json().catch(() => null)
|
const errorData = await res.json().catch(() => null)
|
||||||
@@ -203,6 +225,34 @@ export default function ProductsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBulkDelete = async () => {
|
||||||
|
if (selectedIds.length === 0) return
|
||||||
|
const count = selectedIds.length
|
||||||
|
if (!confirm(t('products.confirmBulkDelete', `Are you sure you want to delete ${count} selected products?`, { count }))) return
|
||||||
|
|
||||||
|
setIsBulkDeleting(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE}/products/bulk-delete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ ids: selectedIds })
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setSelectedIds([])
|
||||||
|
fetchData()
|
||||||
|
} else {
|
||||||
|
const errorData = await res.json().catch(() => null)
|
||||||
|
alert(errorData?.message || 'Failed to delete selected products')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Bulk delete error:', err)
|
||||||
|
alert('Failed to delete selected products')
|
||||||
|
} finally {
|
||||||
|
setIsBulkDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const setPage = (newOrUpdaterData: number | ((p: number) => number)) => {
|
const setPage = (newOrUpdaterData: number | ((p: number) => number)) => {
|
||||||
const nextUrlPage = typeof newOrUpdaterData === 'function' ? newOrUpdaterData(page) : newOrUpdaterData
|
const nextUrlPage = typeof newOrUpdaterData === 'function' ? newOrUpdaterData(page) : newOrUpdaterData
|
||||||
setSearchParams(prev => {
|
setSearchParams(prev => {
|
||||||
@@ -245,7 +295,7 @@ export default function ProductsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8 animate-fade-in">
|
<div className="space-y-8 animate-fade-in relative pb-16">
|
||||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="text-3xl font-bold">{t('products.title', 'Products Management')}</h1>
|
<h1 className="text-3xl font-bold">{t('products.title', 'Products Management')}</h1>
|
||||||
@@ -300,6 +350,16 @@ export default function ProductsPage() {
|
|||||||
<table className="w-full text-left border-collapse">
|
<table className="w-full text-left border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-white/[0.02] border-b border-white/5">
|
<tr className="bg-white/[0.02] border-b border-white/5">
|
||||||
|
<th className="w-12 px-4 py-4 text-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isAllSelected}
|
||||||
|
ref={el => { if (el) el.indeterminate = isSomeSelected }}
|
||||||
|
onChange={handleSelectAll}
|
||||||
|
className="w-4 h-4 rounded border-white/20 bg-night-950 text-cyan-glow focus:ring-cyan-glow/30 accent-cyan-glow cursor-pointer"
|
||||||
|
title={t('common.selectAll', 'Select all on this page')}
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">{t('products.name', 'Product')}</th>
|
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">{t('products.name', 'Product')}</th>
|
||||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">{t('products.category', 'Category')}</th>
|
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">{t('products.category', 'Category')}</th>
|
||||||
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">{t('products.variants', 'Variants')}</th>
|
<th className="px-6 py-4 text-[10px] uppercase font-bold tracking-widest text-slate-500">{t('products.variants', 'Variants')}</th>
|
||||||
@@ -311,14 +371,14 @@ export default function ProductsPage() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
Array.from({ length: 5 }).map((_, i) => (
|
Array.from({ length: 5 }).map((_, i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
<td colSpan={5} className="px-6 py-8">
|
<td colSpan={6} className="px-6 py-8">
|
||||||
<div className="h-10 w-full bg-white/5 animate-pulse rounded-lg" />
|
<div className="h-10 w-full bg-white/5 animate-pulse rounded-lg" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
) : products.length === 0 ? (
|
) : products.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} className="px-6 py-20 text-center space-y-3">
|
<td colSpan={6} className="px-6 py-20 text-center space-y-3">
|
||||||
<Package className="w-10 h-10 text-slate-700 mx-auto" />
|
<Package className="w-10 h-10 text-slate-700 mx-auto" />
|
||||||
<p className="text-slate-500 font-medium italic">{t('common.noData', 'No products matched your search.')}</p>
|
<p className="text-slate-500 font-medium italic">{t('common.noData', 'No products matched your search.')}</p>
|
||||||
</td>
|
</td>
|
||||||
@@ -330,9 +390,21 @@ export default function ProductsPage() {
|
|||||||
const localizedCategory = typeof product.category.name === 'string'
|
const localizedCategory = typeof product.category.name === 'string'
|
||||||
? product.category.name
|
? product.category.name
|
||||||
: (product.category.name?.[i18n.language] || (product.category.name as any)?.th || (product.category.name as any)?.en || '')
|
: (product.category.name?.[i18n.language] || (product.category.name as any)?.th || (product.category.name as any)?.en || '')
|
||||||
|
const isSelected = selectedIds.includes(product.id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={product.id} className="hover:bg-white/[0.01] transition-colors group">
|
<tr
|
||||||
|
key={product.id}
|
||||||
|
className={`transition-colors group ${isSelected ? 'bg-cyan-glow/[0.04] hover:bg-cyan-glow/[0.07]' : 'hover:bg-white/[0.01]'}`}
|
||||||
|
>
|
||||||
|
<td className="w-12 px-4 py-4 text-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => handleToggleSelect(product.id)}
|
||||||
|
className="w-4 h-4 rounded border-white/20 bg-night-950 text-cyan-glow focus:ring-cyan-glow/30 accent-cyan-glow cursor-pointer"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="w-12 h-12 rounded-lg overflow-hidden border border-white/5 bg-night-950/50 p-1 flex-shrink-0 flex items-center justify-center">
|
<div className="w-12 h-12 rounded-lg overflow-hidden border border-white/5 bg-night-950/50 p-1 flex-shrink-0 flex items-center justify-center">
|
||||||
@@ -376,8 +448,8 @@ export default function ProductsPage() {
|
|||||||
onClick={() => { setSelectedProduct(product); setIsGalleryOpen(true); }}
|
onClick={() => { setSelectedProduct(product); setIsGalleryOpen(true); }}
|
||||||
className={`p-2 rounded-lg transition-all cursor-pointer ${
|
className={`p-2 rounded-lg transition-all cursor-pointer ${
|
||||||
product.images && product.images.length > 0
|
product.images && product.images.length > 0
|
||||||
? 'bg-cyan-glow/10 text-cyan-glow hover:bg-cyan-glow/20'
|
? 'bg-cyan-glow/10 text-cyan-glow hover:bg-cyan-glow/20'
|
||||||
: 'hover:bg-white/5 text-slate-600'
|
: 'hover:bg-white/5 text-slate-600'
|
||||||
}`}
|
}`}
|
||||||
title={product.images && product.images.length > 0 ? "View Gallery" : "No Images"}
|
title={product.images && product.images.length > 0 ? "View Gallery" : "No Images"}
|
||||||
>
|
>
|
||||||
@@ -578,6 +650,40 @@ export default function ProductsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Floating Bulk Actions Bar */}
|
||||||
|
{selectedIds.length > 0 && (
|
||||||
|
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40 bg-night-900/95 border border-cyan-glow/30 backdrop-blur-xl shadow-2xl rounded-2xl px-6 py-3.5 flex items-center gap-6 animate-slide-up">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<span className="w-2.5 h-2.5 rounded-full bg-cyan-glow animate-pulse" />
|
||||||
|
<span className="text-sm font-semibold text-white">
|
||||||
|
{t('products.selectedCount', `เลือกอยู่ ${selectedIds.length} รายการ`, { count: selectedIds.length })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-4 w-px bg-white/10" />
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleBulkDelete}
|
||||||
|
disabled={isBulkDeleting}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-rose-500/20 hover:bg-rose-500/30 text-rose-400 border border-rose-500/30 hover:border-rose-500/50 rounded-xl text-xs font-bold transition-all disabled:opacity-50 cursor-pointer shadow-lg shadow-rose-950/40"
|
||||||
|
>
|
||||||
|
{isBulkDeleting ? (
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
)}
|
||||||
|
<span>{t('products.deleteSelected', `ลบรายการที่เลือก (${selectedIds.length})`, { count: selectedIds.length })}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedIds([])}
|
||||||
|
className="p-2 hover:bg-white/5 text-slate-400 hover:text-white rounded-xl transition-all cursor-pointer"
|
||||||
|
title={t('common.cancel', 'ยกเลิกการเลือก')}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "isDeleted" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
ALTER TABLE "Product" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX IF NOT EXISTS "Product_isDeleted_idx" ON "Product"("isDeleted");
|
||||||
|
CREATE INDEX IF NOT EXISTS "Product_categoryId_isDeleted_idx" ON "Product"("categoryId", "isDeleted");
|
||||||
@@ -72,8 +72,13 @@ model Product {
|
|||||||
reviews Review[]
|
reviews Review[]
|
||||||
detailSections ProductDetailSection[]
|
detailSections ProductDetailSection[]
|
||||||
translations ProductTranslation[]
|
translations ProductTranslation[]
|
||||||
|
isDeleted Boolean @default(false)
|
||||||
|
deletedAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([isDeleted])
|
||||||
|
@@index([categoryId, isDeleted])
|
||||||
}
|
}
|
||||||
|
|
||||||
model ProductTranslation {
|
model ProductTranslation {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
|
|||||||
include: {
|
include: {
|
||||||
translations: true,
|
translations: true,
|
||||||
products: {
|
products: {
|
||||||
|
where: { isDeleted: false },
|
||||||
include: {
|
include: {
|
||||||
brand: true,
|
brand: true,
|
||||||
category: { include: { translations: true } },
|
category: { include: { translations: true } },
|
||||||
@@ -62,11 +63,11 @@ export const categoriesRoute = new Elysia({ prefix: "/categories" })
|
|||||||
take: limit,
|
take: limit,
|
||||||
},
|
},
|
||||||
_count: {
|
_count: {
|
||||||
select: { products: true }
|
select: { products: { where: { isDeleted: false } } }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.product.count({ where: { categoryId: id } }),
|
prisma.product.count({ where: { categoryId: id, isDeleted: false } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!category) return status(404, { message: "Category not found" });
|
if (!category) return status(404, { message: "Category not found" });
|
||||||
|
|||||||
@@ -1242,7 +1242,7 @@ async function resolveProductsForSection(payload: any, lang: string) {
|
|||||||
|
|
||||||
if (payload.type === "featured") {
|
if (payload.type === "featured") {
|
||||||
products = await prisma.product.findMany({
|
products = await prisma.product.findMany({
|
||||||
where: { categoryId: Number(payload.categoryId) || 1 },
|
where: { categoryId: Number(payload.categoryId) || 1, isDeleted: false },
|
||||||
include: productInclude,
|
include: productInclude,
|
||||||
take: limit,
|
take: limit,
|
||||||
});
|
});
|
||||||
@@ -1251,7 +1251,7 @@ async function resolveProductsForSection(payload: any, lang: string) {
|
|||||||
(payload.scentId || payload.brandId)
|
(payload.scentId || payload.brandId)
|
||||||
) {
|
) {
|
||||||
products = await prisma.product.findMany({
|
products = await prisma.product.findMany({
|
||||||
where: { brandId: Number(payload.brandId || payload.scentId) },
|
where: { brandId: Number(payload.brandId || payload.scentId), isDeleted: false },
|
||||||
include: productInclude,
|
include: productInclude,
|
||||||
take: limit,
|
take: limit,
|
||||||
});
|
});
|
||||||
@@ -1259,13 +1259,14 @@ async function resolveProductsForSection(payload: any, lang: string) {
|
|||||||
const ids = payload.productIds.map(Number);
|
const ids = payload.productIds.map(Number);
|
||||||
if (ids.length > 0) {
|
if (ids.length > 0) {
|
||||||
products = await prisma.product.findMany({
|
products = await prisma.product.findMany({
|
||||||
where: { id: { in: ids } },
|
where: { id: { in: ids }, isDeleted: false },
|
||||||
include: productInclude,
|
include: productInclude,
|
||||||
});
|
});
|
||||||
products.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
|
products.sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
products = await prisma.product.findMany({
|
products = await prisma.product.findMany({
|
||||||
|
where: { isDeleted: false },
|
||||||
include: productInclude,
|
include: productInclude,
|
||||||
take: limit,
|
take: limit,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -122,7 +122,10 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
|||||||
const limit = query.limit ? parseInt(query.limit) : 12;
|
const limit = query.limit ? parseInt(query.limit) : 12;
|
||||||
const skip = (page - 1) * limit;
|
const skip = (page - 1) * limit;
|
||||||
|
|
||||||
|
const includeDeleted = query.includeDeleted === 'true';
|
||||||
|
|
||||||
const where = {
|
const where = {
|
||||||
|
...(!includeDeleted && { isDeleted: false }),
|
||||||
...(categoryId && { categoryId }),
|
...(categoryId && { categoryId }),
|
||||||
...(brandId && { brandId }),
|
...(brandId && { brandId }),
|
||||||
...(search && {
|
...(search && {
|
||||||
@@ -255,7 +258,8 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!product) return status(404, { message: "Product not found" });
|
const includeDeleted = query.includeDeleted === 'true';
|
||||||
|
if (!product || (!includeDeleted && product.isDeleted)) return status(404, { message: "Product not found" });
|
||||||
|
|
||||||
if (lang === 'all') {
|
if (lang === 'all') {
|
||||||
const nameObj: any = {};
|
const nameObj: any = {};
|
||||||
@@ -1011,4 +1015,73 @@ export const productsRoute = new Elysia({ prefix: "/products" })
|
|||||||
tags: ["products"],
|
tags: ["products"],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.delete(
|
||||||
|
"/:id",
|
||||||
|
async ({ params, status }) => {
|
||||||
|
const id = parseInt(params.id);
|
||||||
|
if (isNaN(id)) return status(400, { success: false, message: "Invalid product ID" });
|
||||||
|
|
||||||
|
const existing = await prisma.product.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { id: true, isDeleted: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
return status(404, { success: false, message: "Product not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.product.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
isDeleted: true,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true, message: "Product deleted successfully" };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
summary: "Soft Delete Product",
|
||||||
|
tags: ["products"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/bulk-delete",
|
||||||
|
async ({ body, status }) => {
|
||||||
|
const { ids } = body as { ids?: number[] };
|
||||||
|
if (!Array.isArray(ids) || ids.length === 0) {
|
||||||
|
return status(400, { success: false, message: "Invalid or empty IDs array" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const validIds = ids.map(id => Number(id)).filter(id => !isNaN(id));
|
||||||
|
if (validIds.length === 0) {
|
||||||
|
return status(400, { success: false, message: "No valid IDs provided" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await prisma.product.updateMany({
|
||||||
|
where: { id: { in: validIds } },
|
||||||
|
data: {
|
||||||
|
isDeleted: true,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
count: result.count,
|
||||||
|
message: `Successfully deleted ${result.count} products`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
ids: t.Array(t.Numeric()),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Bulk Soft Delete Products",
|
||||||
|
tags: ["products"],
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
@@ -109,6 +109,8 @@ export interface Product {
|
|||||||
variants: Variant[]
|
variants: Variant[]
|
||||||
images: ProductImage[]
|
images: ProductImage[]
|
||||||
attributes?: ProductAttribute[]
|
attributes?: ProductAttribute[]
|
||||||
|
isDeleted?: boolean
|
||||||
|
deletedAt?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_DEV_API_PORT = 3051
|
const DEFAULT_DEV_API_PORT = 3051
|
||||||
|
|||||||
@@ -1,38 +1,30 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"themeTokens": {
|
"themeTokens": {
|
||||||
"brandPrimary": "#B8956A",
|
"brandPrimary": "#A100FF",
|
||||||
"brandPrimaryHover": "#A07D3A",
|
"brandPrimaryHover": "#C23BFF",
|
||||||
"brandAccent": "#D4B97A",
|
"brandAccent": "#FF7A1A",
|
||||||
"brandWarm": "#D4785A",
|
"brandWarm": "#FF8F45",
|
||||||
"brandCool": "#EDE6DD",
|
"brandCool": "#E7EBF5",
|
||||||
"brandSuccess": "#5B9A4D",
|
"brandSuccess": "#46D36A",
|
||||||
"brandSoft": "#DABE7E",
|
"brandSoft": "#D16BFF",
|
||||||
|
"surfacePage": "#0A0D14",
|
||||||
"surfacePage": "#F5F0EB",
|
"surfaceNavRest": "#0D1018",
|
||||||
"surfaceNavRest": "#14171C",
|
"surfaceCard": "#151A24",
|
||||||
"surfaceCard": "#FFFFFF",
|
"textMain": "#F5F7FF",
|
||||||
|
"mutedText": "#A7AFBF",
|
||||||
"textMain": "#EDE7E1",
|
"linePrimary": "#A100FF",
|
||||||
"mutedText": "#BDB7B0",
|
"linePrimaryHover": "#FF7A1A",
|
||||||
|
"captionText": "#7D8598",
|
||||||
"linePrimary": "#B8956A",
|
"borderColor": "#242A38",
|
||||||
"linePrimaryHover": "#D4B97A",
|
"surfaceMuted": "#111622",
|
||||||
|
|
||||||
"captionText": "#A9A39D",
|
|
||||||
"borderColor": "#2A2E34",
|
|
||||||
"surfaceMuted": "#1B1F24",
|
|
||||||
|
|
||||||
"fontBody": "",
|
"fontBody": "",
|
||||||
"fontDisplay": "",
|
"fontDisplay": "",
|
||||||
|
"shadowCard": "0 2px 8px rgba(0,0,0,0.30)",
|
||||||
"shadowCard": "0 1px 4px rgba(0,0,0,0.12)",
|
"shadowCardHover": "0 8px 24px rgba(0,0,0,0.40)",
|
||||||
"shadowCardHover": "0 4px 12px rgba(0,0,0,0.18)",
|
|
||||||
|
|
||||||
"radiusBase": "8px",
|
"radiusBase": "8px",
|
||||||
"radiusCard": "16px"
|
"radiusCard": "16px"
|
||||||
},
|
},
|
||||||
|
|
||||||
"scrollRevealEnabled": true,
|
"scrollRevealEnabled": true,
|
||||||
"scrollRevealStyle": "fade-up",
|
"scrollRevealStyle": "fade-up",
|
||||||
"scrollRevealDuration": 700,
|
"scrollRevealDuration": 700,
|
||||||
|
|||||||
Reference in new issue
Block a user