d170058509
- add optional `categoryIcon` flag to icon metadata helper in add-remove.js and business.js - mark Add01Icon and ChartLineData01Icon as category representative icons - use categoryIcon flag in IconsPage to display the most representative icon per category - add `cursor-pointer` to category sidebar buttons and widen sidebar from 200px to 250px
152 lines
7.0 KiB
JavaScript
152 lines
7.0 KiB
JavaScript
'use client';
|
||
|
||
import { useState, useMemo } from 'react';
|
||
import * as Icons from '@zen/core/shared/icons';
|
||
import { useToast } from '@zen/core/toast';
|
||
import AdminHeader from '../components/AdminHeader.js';
|
||
|
||
const ALL_ICONS = Object.entries(Icons);
|
||
|
||
export default function IconsPage() {
|
||
const [query, setQuery] = useState('');
|
||
const [selectedCategory, setSelectedCategory] = useState(null);
|
||
const toast = useToast();
|
||
|
||
const categories = useMemo(() => {
|
||
const map = new Map();
|
||
for (const [, Icon] of ALL_ICONS) {
|
||
const cat = Icon.category;
|
||
if (!cat) continue;
|
||
if (!map.has(cat)) map.set(cat, { count: 0, FirstIcon: Icon });
|
||
const entry = map.get(cat);
|
||
entry.count += 1;
|
||
if (Icon.categoryIcon) entry.FirstIcon = Icon;
|
||
}
|
||
return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
|
||
}, []);
|
||
|
||
const filtered = useMemo(() => {
|
||
let list = ALL_ICONS;
|
||
if (selectedCategory) list = list.filter(([, Icon]) => Icon.category === selectedCategory);
|
||
if (!query.trim()) return list;
|
||
const q = query.trim().toLowerCase();
|
||
return list.filter(([name, Icon]) =>
|
||
name.toLowerCase().includes(q) ||
|
||
Icon.keywords?.some(k => k.toLowerCase().includes(q))
|
||
);
|
||
}, [query, selectedCategory]);
|
||
|
||
const handleCopy = (name, e) => {
|
||
if (e.shiftKey) {
|
||
navigator.clipboard.writeText(`<${name} className="w-5 h-5" />`);
|
||
toast.success(`JSX de ${name} copié`);
|
||
} else {
|
||
navigator.clipboard.writeText(name);
|
||
toast.success(`${name} copié`);
|
||
}
|
||
};
|
||
|
||
const hasSidebar = categories.length > 0;
|
||
|
||
return (
|
||
<div className="flex flex-col gap-6">
|
||
<AdminHeader
|
||
title="Icônes"
|
||
description={`${ALL_ICONS.length} icônes disponibles`}
|
||
/>
|
||
|
||
<div className={`flex gap-4 items-start ${hasSidebar ? 'flex-col sm:flex-row' : ''}`}>
|
||
<div className="flex-1 min-w-0 flex flex-col gap-6">
|
||
<div className="relative">
|
||
<input
|
||
type="text"
|
||
value={query}
|
||
onChange={e => setQuery(e.target.value)}
|
||
placeholder="Rechercher une icône..."
|
||
className="w-full rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-4 py-2.5 text-sm text-neutral-900 dark:text-white placeholder-neutral-400 dark:placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-blue-700/40"
|
||
/>
|
||
{query && (
|
||
<button
|
||
onClick={() => setQuery('')}
|
||
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-200 text-lg leading-none"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{filtered.length === 0 ? (
|
||
<p className="text-sm text-neutral-500 dark:text-neutral-400 text-center py-12">
|
||
Aucune icône trouvée pour “{query}”
|
||
</p>
|
||
) : (
|
||
<div className={`grid gap-2 ${
|
||
hasSidebar
|
||
? 'grid-cols-4 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 2xl:grid-cols-[repeat(13,minmax(0,1fr))]'
|
||
: 'grid-cols-4 sm:grid-cols-6 md:grid-cols-8 lg:grid-cols-10 xl:grid-cols-12 2xl:grid-cols-[repeat(16,minmax(0,1fr))]'
|
||
}`}>
|
||
{filtered.map(([name, IconComponent]) => (
|
||
<button
|
||
key={name}
|
||
onClick={(e) => handleCopy(name, e)}
|
||
title={`${name.replace('Icon', '')} · Shift: JSX`}
|
||
className="aspect-square flex flex-col items-center justify-between rounded-lg border border-neutral-200 dark:border-neutral-800 bg-neutral-100 dark:bg-neutral-900 hover:border-blue-500 dark:hover:border-blue-500 transition-colors duration-100 group cursor-pointer px-1 py-2 overflow-hidden"
|
||
>
|
||
<div className="flex-1 flex items-center justify-center w-full">
|
||
<IconComponent className="w-7 h-7 text-black dark:text-white" />
|
||
</div>
|
||
<span className="shrink-0 text-[9px] text-neutral-500 dark:text-neutral-400 leading-none text-center truncate group-hover:text-neutral-600 dark:group-hover:text-neutral-300 w-full px-1">
|
||
{name.replace('Icon', '')}
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{hasSidebar && (
|
||
<div className="w-full sm:w-[250px] sm:shrink-0 sm:sticky sm:top-4 rounded-lg border border-neutral-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 overflow-hidden order-first sm:order-none">
|
||
<div className="px-3 py-2.5 border-b border-neutral-200 dark:border-neutral-800">
|
||
<span className="text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wide">Catégories</span>
|
||
</div>
|
||
<div className="py-1 flex sm:flex-col flex-row flex-wrap sm:flex-nowrap gap-0">
|
||
<button
|
||
onClick={() => setSelectedCategory(null)}
|
||
className={`cursor-pointer flex items-center justify-between px-3 py-1.5 text-sm rounded-md m-1 sm:mx-1 sm:my-0 sm:w-[calc(100%-8px)] transition-colors ${
|
||
selectedCategory === null
|
||
? 'bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-white'
|
||
: 'text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:text-neutral-900 dark:hover:text-white'
|
||
}`}
|
||
>
|
||
<span className="truncate">Tout</span>
|
||
<span className="text-xs tabular-nums shrink-0 ml-2 text-neutral-400 dark:text-neutral-500">
|
||
{ALL_ICONS.length}
|
||
</span>
|
||
</button>
|
||
{categories.map(([cat, { count, FirstIcon }]) => (
|
||
<button
|
||
key={cat}
|
||
onClick={() => setSelectedCategory(cat)}
|
||
className={`cursor-pointer flex items-center justify-between px-3 py-1.5 text-sm rounded-md m-1 sm:mx-1 sm:my-0 sm:w-[calc(100%-8px)] transition-colors ${
|
||
selectedCategory === cat
|
||
? 'bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-white'
|
||
: 'text-neutral-600 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:text-neutral-900 dark:hover:text-white'
|
||
}`}
|
||
>
|
||
<span className="flex items-center gap-2 min-w-0">
|
||
<FirstIcon className="w-4 h-4 shrink-0 text-neutral-500 dark:text-neutral-400" />
|
||
<span className="truncate">{cat}</span>
|
||
</span>
|
||
<span className="text-xs tabular-nums shrink-0 ml-2 text-neutral-400 dark:text-neutral-500">
|
||
{count}
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|