diff --git a/old_asset_type.ts b/old_asset_type.ts deleted file mode 100644 index 62bbf6c..0000000 Binary files a/old_asset_type.ts and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 3e77f32..9623c8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "ts-morph": "^28.0.0", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12" @@ -1931,6 +1932,18 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@ts-morph/common": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.29.0.tgz", + "integrity": "sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^10.0.1", + "path-browserify": "^1.0.1", + "tinyglobby": "^0.2.14" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", @@ -2591,6 +2604,13 @@ "node": ">=6" } }, + "node_modules/code-block-writer": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", + "dev": true, + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4106,6 +4126,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4647,6 +4674,17 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-morph": { + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-28.0.0.tgz", + "integrity": "sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.29.0", + "code-block-writer": "^13.0.3" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/package.json b/package.json index 5d37b22..60e2525 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", + "ts-morph": "^28.0.0", "typescript": "~6.0.2", "typescript-eslint": "^8.59.2", "vite": "^8.0.12" diff --git a/scripts/scaffold.cjs b/scripts/scaffold.cjs index 35f13ae..23c50df 100644 --- a/scripts/scaffold.cjs +++ b/scripts/scaffold.cjs @@ -2,7 +2,6 @@ const fs = require('fs'); const path = require('path'); const features = [ { name: 'attribute-groups', Name: 'AttributeGroup' }, - { name: 'scopes', Name: 'Scope' }, { name: 'assets', Name: 'Asset' }, { name: 'asset-types', Name: 'AssetType' }, { name: 'asset-families', Name: 'AssetFamily' }, diff --git a/src/App.tsx b/src/App.tsx index e01d92b..33df9a6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,37 +1,44 @@ +// src/App.tsx import { useEffect, useRef, useState } from 'react'; import AppRoutes from './routes/AppRoutes'; import { ToastContainer } from 'react-toastify'; import { LanguageProvider } from './contexts/LanguageContext'; -import { useAppDispatch } from './store'; import { HeaderProvider } from './contexts/HeaderContext'; - +import { Loader } from "./components/customs/Loader"; function App() { - const dispatch = useAppDispatch(); const bootstrappedRef = useRef(false); const [bootstrapped, setBootstrapped] = useState(false); + const [progress, setProgress] = useState(0); useEffect(() => { if (bootstrappedRef.current) return; bootstrappedRef.current = true; const bootstrap = async () => { - // Set default permissions for development - // dispatch(setPermissions({})); // Already set in initialState, but can override here + const steps = [15, 35, 55, 75, 92, 100]; + + for (const step of steps) { + await new Promise(resolve => setTimeout(resolve, 160)); + setProgress(step); + } + + await new Promise(resolve => setTimeout(resolve, 300)); setBootstrapped(true); }; bootstrap(); - }, [dispatch]); + }, []); if (!bootstrapped) { return ( -
-
-
-

Initializing PIM Platform...

-
-
+ ); } diff --git a/src/components/customs/ActionMenu.tsx b/src/components/customs/ActionMenu.tsx new file mode 100644 index 0000000..4ed058f --- /dev/null +++ b/src/components/customs/ActionMenu.tsx @@ -0,0 +1,58 @@ +// src/components/customs/ActionMenu.tsx +import { MoreHorizontal, Eye, Edit2, Trash2 } from "lucide-react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "./Dropdown"; + +export interface ActionMenuProps { + onView?: () => void; + onEdit?: () => void; + onDelete?: () => void; +} + +export function ActionMenu({ onView, onEdit, onDelete }: ActionMenuProps) { + return ( + + + + + + {onView && ( + { e.stopPropagation(); onView(); }} + className="cursor-pointer text-purple-900 hover:bg-purple-100 focus:bg-purple-100 focus:text-purple-900 rounded-lg px-3 py-2 flex items-center outline-none" + > + + View + + )} + {onEdit && ( + { e.stopPropagation(); onEdit(); }} + className="cursor-pointer text-purple-900 hover:bg-purple-100 focus:bg-purple-100 focus:text-purple-900 rounded-lg px-3 py-2 flex items-center outline-none mt-1" + > + + Edit + + )} + {onDelete && ( + { e.stopPropagation(); onDelete(); }} + className="cursor-pointer text-red-600 hover:bg-red-50 focus:bg-red-50 focus:text-red-700 rounded-lg px-3 py-2 flex items-center outline-none mt-1 border-t border-purple-100" + > + + Delete + + )} + + + ); +} \ No newline at end of file diff --git a/src/components/customs/DataTable.tsx b/src/components/customs/DataTable.tsx index b7f36ba..cf2e9c6 100644 --- a/src/components/customs/DataTable.tsx +++ b/src/components/customs/DataTable.tsx @@ -1,7 +1,9 @@ -import { useState, useMemo, type ReactNode } from "react"; -import { Search, X, ChevronsUpDown, ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Settings2 } from "lucide-react"; - -// ── Types ───────────────────────────────────────────────────────────────────── +import { useState, useMemo, useRef, useEffect, type ReactNode } from "react"; +import { + Search, X, ChevronsUpDown, ChevronUp, ChevronDown, + ChevronLeft, ChevronRight, Settings2, Check, Filter +} from "lucide-react"; +import { ActionMenu } from "./ActionMenu"; export interface DataTableColumn { key: string; @@ -21,7 +23,6 @@ export interface DataTableProps { onSelectionChange?: (ids: Set) => void; onRowClick?: (row: T) => void; rowIdKey?: keyof T; - actions?: (row: T) => ReactNode; pageSizeOptions?: number[]; resultLabel?: string; maxHeight?: string; @@ -30,47 +31,96 @@ export interface DataTableProps { draggable?: boolean; onReorder?: (newData: T[]) => void; searchPlaceholder?: string; -} + statusKey?: string; -// ── Component ───────────────────────────────────────────────────────────────── + actionConfig?: { + onView?: (row: T) => void; + onEdit?: (row: T) => void; + onDelete?: (row: T) => void; + }; +} export function DataTable = any>({ columns: initialColumns, data, - selectable = false, - selectedIds: _selectedIds = new Set(), - onSelectionChange: _onSelectionChange, + selectable: _selectable = true, + selectedIds: externalSelectedIds, + onSelectionChange, onRowClick, rowIdKey = "id" as keyof T, - actions, pageSizeOptions = [5, 10, 25, 50], resultLabel = "results", maxHeight = "520px", - toolbarRight, toolbarLeft, + toolbarRight, draggable = false, - onReorder: _onReorder, searchPlaceholder = "Search...", + statusKey = "status", + actionConfig, }: DataTableProps) { + + const [internalSelectedIds, setInternalSelectedIds] = useState>(new Set()); + const selectedIds = externalSelectedIds ?? internalSelectedIds; + + const handleSelectionChange = (ids: Set) => { + setInternalSelectedIds(ids); + onSelectionChange?.(ids); + }; + const [search, setSearch] = useState(""); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(pageSizeOptions[0] ?? 10); const [sortCol, setSortCol] = useState(null); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); - const [visibleColumns, _setVisibleColumns] = useState>( - initialColumns.reduce((acc, col) => ({ ...acc, [col.key]: col.visible !== false }), {}) + + const [visibleColumns, setVisibleColumns] = useState>( + initialColumns.reduce((acc, col) => ({ ...acc, [col.key]: col.visible !== false }), {} as Record) ); + const [selectedStatuses, setSelectedStatuses] = useState([]); + const [showStatusMenu, setShowStatusMenu] = useState(false); + const [showColumnMenu, setShowColumnMenu] = useState(false); + + const statusRef = useRef(null); + const columnRef = useRef(null); + + // Click Outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (statusRef.current && !statusRef.current.contains(event.target as Node)) setShowStatusMenu(false); + if (columnRef.current && !columnRef.current.contains(event.target as Node)) setShowColumnMenu(false); + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + useEffect(() => { if (showStatusMenu) setShowColumnMenu(false); }, [showStatusMenu]); + useEffect(() => { if (showColumnMenu) setShowStatusMenu(false); }, [showColumnMenu]); + const columns = useMemo(() => - initialColumns.filter(col => visibleColumns[col.key] !== false), + initialColumns.filter(col => visibleColumns[col.key] !== false), [initialColumns, visibleColumns] ); + const statusOptions = useMemo(() => { + const counts: Record = {}; + data.forEach(row => { + const status = String(row[statusKey] || "Unknown").trim(); + counts[status] = (counts[status] || 0) + 1; + }); + return Object.entries(counts).map(([status, count]) => ({ status, count })) + .sort((a, b) => a.status.localeCompare(b.status)); + }, [data, statusKey]); + + const toggleColumn = (key: string) => setVisibleColumns(prev => ({ ...prev, [key]: !prev[key] })); + const filteredData = useMemo(() => { + let result = [...data]; const q = search.trim().toLowerCase(); - if (!q) return data; - return data.filter(row => columns.some(col => String(row[col.key] ?? "").toLowerCase().includes(q))); - }, [data, search, columns]); + if (q) result = result.filter(row => columns.some(col => String(row[col.key] ?? "").toLowerCase().includes(q))); + if (selectedStatuses.length > 0) result = result.filter(row => selectedStatuses.includes(String(row[statusKey]))); + return result; + }, [data, search, columns, selectedStatuses, statusKey]); const sorted = useMemo(() => { if (!sortCol) return filteredData; @@ -78,8 +128,7 @@ export function DataTable = any>({ const av = a[sortCol], bv = b[sortCol]; if (av == null) return 1; if (bv == null) return -1; - if (typeof av === "number" && typeof bv === "number") - return sortDir === "asc" ? av - bv : bv - av; + if (typeof av === "number" && typeof bv === "number") return sortDir === "asc" ? av - bv : bv - av; return sortDir === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av)); }); }, [filteredData, sortCol, sortDir]); @@ -91,21 +140,42 @@ export function DataTable = any>({ const paged = useMemo(() => sorted.slice((page - 1) * pageSize, page * pageSize), [sorted, page, pageSize]); const resetPage = () => setPage(1); - const handleSearch = (v: string) => { setSearch(v); resetPage(); }; const handlePageSize = (s: number) => { setPageSize(s); resetPage(); }; + const handleSort = (key: string) => { if (sortCol === key) setSortDir(d => d === "asc" ? "desc" : "asc"); else { setSortCol(key); setSortDir("asc"); } resetPage(); }; - const colSpan = columns.length + (selectable ? 1 : 0) + (actions ? 1 : 0) + (draggable ? 1 : 0); + const colSpan = columns.length + 1 + (actionConfig ? 1 : 0) + (draggable ? 1 : 0); + + const pagedIds = paged.map(row => String(row[rowIdKey])); + const allPageSelected = pagedIds.length > 0 && pagedIds.every(id => selectedIds.has(id)); + const somePageSelected = pagedIds.some(id => selectedIds.has(id)); + + const toggleSelectAll = () => { + const next = new Set(selectedIds); + if (allPageSelected) { + pagedIds.forEach(id => next.delete(id)); + } else { + pagedIds.forEach(id => next.add(id)); + } + handleSelectionChange(next); + }; + + const toggleRow = (id: string, e: React.MouseEvent) => { + e.stopPropagation(); + const next = new Set(selectedIds); + if (next.has(id)) next.delete(id); else next.add(id); + handleSelectionChange(next); + }; return ( -
+
- {/* Toolbar - Tight spacing */} + {/* Toolbar */}
{toolbarLeft} @@ -116,7 +186,7 @@ export function DataTable = any>({ value={search} onChange={e => handleSearch(e.target.value)} placeholder={searchPlaceholder} - className="w-full pl-10 pr-4 h-9 text-sm bg-white border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-purple-500" + className="w-full pl-10 pr-4 h-9 text-sm bg-white border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500" /> {search && ( + + {/* Status Filter - Checkboxes */} +
+ + + {showStatusMenu && ( +
+
STATUS FILTER
+ + + + {statusOptions.map(({ status, count }) => ( + + ))} +
+ )} +
+ + {/* Columns Menu */} +
+ + + {showColumnMenu && ( +
+
SHOW COLUMNS
+ {initialColumns.map((col) => ( + + ))} +
+ )} +
- {/* Table - Compact & Clean */} + {/* Table */}
{draggable && {columns.map((col) => { - const align = col.align || "left"; + const align = col.align || "center"; return ( + {actionConfig && ( + )} {paged.length === 0 ? ( - - - + ) : ( paged.map((row) => { const id = String(row[rowIdKey]); return ( - onRowClick?.(row)} - className={`hover:bg-purple-50/70 transition-colors ${onRowClick ? "cursor-pointer" : ""}`} - > + onRowClick?.(row)} className={`hover:bg-purple-50/70 transition-colors ${onRowClick ? "cursor-pointer" : ""}`}> {draggable && } - {selectable && } + {columns.map(col => { - const align = col.align || "left"; + const align = col.align || "center"; return ( - ); })} - {actions && ( - )} @@ -223,11 +367,7 @@ export function DataTable = any>({
Rows per page - handlePageSize(Number(e.target.value))} className="border border-gray-200 rounded-lg px-3 py-1 focus:ring-purple-500"> {pageSizeOptions.map(s => )}
@@ -239,11 +379,7 @@ export function DataTable = any>({ {Array.from({ length: Math.min(7, totalPages) }, (_, i) => i + 1).map(p => ( - ))} @@ -254,4 +390,4 @@ export function DataTable = any>({
); -} +} \ No newline at end of file diff --git a/src/components/customs/Loader.tsx b/src/components/customs/Loader.tsx index 8ccc25d..302d03e 100644 --- a/src/components/customs/Loader.tsx +++ b/src/components/customs/Loader.tsx @@ -1,69 +1,68 @@ -import React from 'react'; -import { Loader2 } from 'lucide-react'; +// src/components/customs/Loader.tsx +import { Package } from "lucide-react"; +import { cn } from "../../lib/utils"; -interface LoaderProps { - size?: 'sm' | 'md' | 'lg' | 'xl'; - text?: string; +export interface LoaderProps { + size?: "sm" | "md" | "lg" | "xl"; + message?: string; + subMessage?: string; + progress?: number; + fullScreen?: boolean; className?: string; - fullScreen?: boolean; // New prop for full page loader } -/** - * Size mapping for consistent loader dimensions - */ -const sizeClasses = { - sm: 'w-4 h-4', - md: 'w-8 h-8', - lg: 'w-12 h-12', - xl: 'w-16 h-16', -}; - -/** - * Inline Loader - Used inside components, cards, buttons, etc. - */ -export const CustomLoader: React.FC = ({ - size = 'md', - text, - className = '', -}) => { - return ( -
- - {text && ( -

{text}

- )} -
- ); -}; - -/** - * Page Loader - Full screen overlay for initial loading or route transitions - */ -export const PageLoader: React.FC = ({ - size = 'lg', - text = 'Loading...', - className = '', -}) => { +export function Loader({ + size = "lg", + message = "Loading...", + subMessage, + progress, + fullScreen = false, + className, +}: LoaderProps) { return (
-
- +
+
- {/* Optional subtle ring effect */} -
+
+ +
+
+ +
+
- {text && ( -

- {text} -

+
+

{message}

+ {subMessage &&

{subMessage}

} +
+ + {typeof progress === "number" && ( +
+
+
)}
); -}; - -// Default export (most commonly used) -export default CustomLoader; \ No newline at end of file +} \ No newline at end of file diff --git a/src/components/customs/Table.tsx b/src/components/customs/Table.tsx deleted file mode 100644 index 477da1d..0000000 --- a/src/components/customs/Table.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import type { ReactNode } from "react"; -import { ChevronsUpDown, ChevronUp, ChevronDown } from "lucide-react"; -import { useState, useMemo } from "react"; - -export interface TableColumn { - key: string; - label: string; - sortable?: boolean; - width?: string; - render?: (value: any, row: T) => ReactNode; -} - -export interface TableProps { - columns: TableColumn[]; - data: T[]; - onRowClick?: (row: T) => void; - selectable?: boolean; - selectedIds?: Set; - onSelectionChange?: (selected: Set) => void; - actions?: (row: T) => ReactNode; - rowIdKey?: keyof T; - /** Rendered inside the toolbar row above the table (e.g. SearchBar) */ - header?: ReactNode; - /** Max height for the scrollable body. Defaults to 480px */ - maxHeight?: string; - variant?: "card" | "flat"; -} - -export function Table = any>({ - columns, - data, - onRowClick, - selectable = false, - selectedIds = new Set(), - onSelectionChange, - actions, - rowIdKey = "id" as keyof T, - header, - maxHeight = "480px", - variant = "flat", -}: TableProps) { - const [sortColumn, setSortColumn] = useState(null); - const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); - - const sortedData = useMemo(() => { - if (!sortColumn) return data; - return [...data].sort((a, b) => { - const aVal = a[sortColumn]; - const bVal = b[sortColumn]; - if (aVal == null) return 1; - if (bVal == null) return -1; - if (typeof aVal === "number" && typeof bVal === "number") - return sortDirection === "asc" ? aVal - bVal : bVal - aVal; - return sortDirection === "asc" - ? String(aVal).localeCompare(String(bVal)) - : String(bVal).localeCompare(String(aVal)); - }); - }, [data, sortColumn, sortDirection]); - - const handleSort = (key: string) => { - if (sortColumn === key) { - setSortDirection(prev => (prev === "asc" ? "desc" : "asc")); - } else { - setSortColumn(key); - setSortDirection("asc"); - } - }; - - const handleSelectRow = (id: string, e: React.MouseEvent) => { - e.stopPropagation(); - if (!onSelectionChange) return; - const next = new Set(selectedIds); - next.has(id) ? next.delete(id) : next.add(id); - onSelectionChange(next); - }; - - const handleSelectAll = (e: React.ChangeEvent) => { - if (!onSelectionChange) return; - e.target.checked - ? onSelectionChange(new Set(sortedData.map(r => String(r[rowIdKey])))) - : onSelectionChange(new Set()); - }; - - const isAllSelected = - sortedData.length > 0 && - sortedData.every(r => selectedIds.has(String(r[rowIdKey]))); - const isSomeSelected = - sortedData.some(r => selectedIds.has(String(r[rowIdKey]))) && !isAllSelected; - - const colCount = columns.length + (selectable ? 1 : 0) + (actions ? 1 : 0); - - return ( -
- {/* Toolbar */} - {header && ( -
- {header} -
- )} - - {/* Table wrapper: header is sticky, body scrolls */} -
-
-
} - {selectable && } + + { if (el) el.indeterminate = somePageSelected && !allPageSelected; }} + onChange={toggleSelectAll} + className="w-4 h-4 accent-purple-600 cursor-pointer" + title="Select all on this page" + /> + = any>({ ); })} - {actions && ( - - ACTION - ACTION
No records found
No records found
⋮⋮ toggleRow(id, e)}> + {}} + className="w-4 h-4 accent-purple-600 cursor-pointer" + /> + + {col.render ? col.render(row[col.key], row) : (row[col.key] ?? "—")} e.stopPropagation()}> - {actions(row)} + {actionConfig && ( + e.stopPropagation()}> + actionConfig.onView!(row) : undefined} + onEdit={actionConfig.onEdit ? () => actionConfig.onEdit!(row) : undefined} + onDelete={actionConfig.onDelete ? () => actionConfig.onDelete!(row) : undefined} + />
- {/* ── Sticky thead ── */} - - - {selectable && ( - - )} - {columns.map(col => ( - - ))} - {actions && ( - - )} - - - - {/* ── Scrollable tbody ── */} - - {sortedData.length === 0 ? ( - - - - ) : ( - sortedData.map(row => { - const id = String(row[rowIdKey]); - const isSelected = selectedIds.has(id); - return ( - onRowClick?.(row)} - className={`transition-colors ${onRowClick ? "cursor-pointer" : ""} ${isSelected ? "bg-purple-50/60" : "hover:bg-gray-50/70"}`} - > - {selectable && ( - - )} - {columns.map(col => ( - - ))} - {actions && ( - - )} - - ); - }) - )} - -
- { if (el) el.indeterminate = isSomeSelected; }} - onChange={handleSelectAll} - /> - col.sortable && handleSort(col.key)} - className={`px-4 py-3.5 text-left text-xs font-bold uppercase tracking-wider select-none ${col.sortable ? "cursor-pointer hover:text-purple-800" : ""} ${col.width ?? ""}`} - style={{ color: "#7C3AED" }} - > -
- {col.label} - {col.sortable && ( - sortColumn === col.key ? ( - sortDirection === "asc" - ? - : - ) : ( - - ) - )} -
-
- Action -
- No records found -
- handleSelectRow(id, e)} - onChange={() => {}} - /> - - {col.render - ? col.render(row[col.key], row) - : (row[col.key] ?? "—")} - e.stopPropagation()} - > - {actions(row)} -
-
-
-
- ); -} diff --git a/src/components/layouts/Sidebar.tsx b/src/components/layouts/Sidebar.tsx index e09b6d0..1953395 100644 --- a/src/components/layouts/Sidebar.tsx +++ b/src/components/layouts/Sidebar.tsx @@ -106,7 +106,7 @@ export function Sidebar() { } }} className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all group text-sm font-medium ${isActive || hasActiveChild - ? "bg-primary text-white shadow-sm" + ? "bg-primary/10 text-primary shadow-sm" : "text-gray-700 hover:bg-gray-50" }`} > diff --git a/src/features/asset-types/pages/AssetTypeList.tsx b/src/features/asset-types/pages/AssetTypeList.tsx index 53675f8..1a62aa1 100644 --- a/src/features/asset-types/pages/AssetTypeList.tsx +++ b/src/features/asset-types/pages/AssetTypeList.tsx @@ -1,11 +1,12 @@ import { useState } from "react"; -import { Plus, Edit2, Trash2, Eye, Search, Filter, Image as ImageIcon, Video, FileText, CheckCircle2, Layers, RefreshCw, LayoutGrid, BarChart2 } from "lucide-react"; +import { Plus, Image as ImageIcon, Video, FileText, CheckCircle2, Layers, RefreshCw, LayoutGrid, BarChart2 } from "lucide-react"; import { useNavigate } from "react-router-dom"; import { PageWrapper } from "../../../components/layouts/PageWrapper"; import { Button } from "../../../components/customs/Button"; import { DataTable } from "../../../components/customs/DataTable"; import { Breadcrumb } from "../../../components/layouts/Breadcrumb"; import { AssetTypeStatsCard } from "../components/AssetTypeStatsCard"; +import { ConfirmationModal } from "../../../components/modals/ConfirmationModal"; // Mock Data const MOCK_ASSET_TYPES = [ @@ -109,7 +110,7 @@ const MOCK_ASSET_TYPES = [ export default function AssetTypeList() { const navigate = useNavigate(); - const [searchQuery, setSearchQuery] = useState(""); + const [deleteModal, setDeleteModal] = useState({ isOpen: false, id: "", name: "" }); return ( @@ -160,35 +161,7 @@ export default function AssetTypeList() { icon={} />
- - {/* Table & Controls Container */} -
- {/* Toolbar */} -
-
-
- - setSearchQuery(e.target.value)} - className="w-full pl-9 pr-4 h-9 bg-white border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500" - /> -
-
- - All Categories -
-
- All Statuses -
-
-
- 12 of 12 types -
-
- + {/* Table */} ( -
- - - -
- )} + actionConfig={{ + onView: (row) => navigate(`/settings/asset-types/${row.id}`), + onEdit: (row) => navigate(`/settings/asset-types/${row.id}`), + onDelete: (row) => setDeleteModal({ isOpen: true, id: row.id, name: row.name }) + }} /> -
+ + setDeleteModal({ isOpen: false, id: "", name: "" })} + onCancel={() => setDeleteModal({ isOpen: false, id: "", name: "" })} + /> ); } diff --git a/src/features/assets/components/AssetCard.tsx b/src/features/assets/components/AssetCard.tsx new file mode 100644 index 0000000..ff73964 --- /dev/null +++ b/src/features/assets/components/AssetCard.tsx @@ -0,0 +1,60 @@ +import type { ReactNode } from "react"; + +interface AssetCardProps { + title: string; + value: string | number; + subtitle: string; + icon?: ReactNode; + color: "purple" | "green" | "blue" | "slate" | "indigo" | "red" | "orange"; +} + +const COLOR_MAP = { + purple: { border: "border-purple-200", iconText: "text-purple-600", bgGradient: "bg-purple-50/40" }, + green: { border: "border-green-200", iconText: "text-green-600", bgGradient: "bg-green-50/40" }, + blue: { border: "border-blue-200", iconText: "text-blue-600", bgGradient: "bg-blue-50/40" }, + slate: { border: "border-slate-200", iconText: "text-slate-500", bgGradient: "bg-slate-50/40" }, + indigo: { border: "border-indigo-200", iconText: "text-indigo-600", bgGradient: "bg-indigo-50/40" }, + red: { border: "border-red-200", iconText: "text-red-600", bgGradient: "bg-red-50/40" }, + orange: { border: "border-orange-200", iconText: "text-orange-600", bgGradient: "bg-orange-50/40" }, +}; + +export function AssetCard({ title, value, subtitle, icon, color }: AssetCardProps) { + const c = COLOR_MAP[color]; + + return ( +
+ {/* Header */} +
+ + {title} + + {icon && {icon}} +
+ + {/* Value */} +
+ {value} +
+ + {/* Subtitle */} +
+ {subtitle} +
+
+ ); +} \ No newline at end of file diff --git a/src/features/assets/pages/AssetList.tsx b/src/features/assets/pages/AssetList.tsx index 3c431af..aef9b99 100644 --- a/src/features/assets/pages/AssetList.tsx +++ b/src/features/assets/pages/AssetList.tsx @@ -1,26 +1,16 @@ import { useState } from "react"; -import { - Search, ChevronDown, Download, Archive, Upload, - Image as ImageIcon, Video, FileText, Eye, Edit2, List, Grid, - FileBadge, BookOpen, Presentation, PenTool, Package, Layers, - ChevronRight +import { useNavigate } from "react-router-dom"; +import { + Download, Archive, Upload, + Image as ImageIcon, Video, FileText, Grid, + FileBadge, Presentation, Package, Layers, + ChevronRight, TrendingUp, AlertTriangle, Clock } from "lucide-react"; import { Button } from "../../../components/customs/Button"; import { DataTable } from "../../../components/customs/DataTable"; - -// Mock Data -const ASSET_CATEGORIES = [ - { id: "all", name: "All Assets", count: 125, icon: Layers, active: true }, - { id: "product-images", name: "Product Images", count: 45, icon: ImageIcon }, - { id: "variant-images", name: "Variant Images", count: 32, icon: Package }, - { id: "videos", name: "Videos", count: 8, icon: Video }, - { id: "documents", name: "Documents", count: 15, icon: FileText }, - { id: "certificates", name: "Certificates", count: 12, icon: FileBadge }, - { id: "manuals", name: "User Manuals", count: 6, icon: BookOpen }, - { id: "marketing", name: "Marketing Assets", count: 18, icon: Presentation }, - { id: "drawings", name: "Technical Drawings", count: 4, icon: PenTool }, - { id: "packaging", name: "Packaging Assets", count: 10, icon: Package }, -]; +import { Breadcrumb } from "../../../components/layouts/Breadcrumb"; +import { PageWrapper } from "../../../components/layouts/PageWrapper"; +import { AssetCard } from "../components/AssetCard"; // ← Imported AssetCard const MOCK_ASSETS = [ { @@ -118,8 +108,9 @@ const MOCK_VARIANT_GROUPS = [ ]; export default function AssetList() { + const navigate = useNavigate(); const [activeTab, setActiveTab] = useState("Product Assets"); - + const getAssetIcon = (type: string, color: string) => { if (type === "Image") return ; if (type === "Video") return