Files
core/src/features/admin/devkit/IconsPage.client.js
T

145 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 });
map.get(cat).count += 1;
}
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) => {
navigator.clipboard.writeText(`<${name} className="w-5 h-5" />`);
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 &ldquo;{query}&rdquo;
</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={() => handleCopy(name)}
title={name.replace('Icon', '')}
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-[200px] 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={`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={`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>
);
}