Files
mintel.me/apps/web/src/payload/components/IconSelector/index.tsx
Marc Mintel 6864903cff
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 2m24s
Build & Deploy / 🏗️ Build (push) Failing after 3m40s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
fix(web): remove redundant prop-types and unblock lint pipeline
2026-02-24 11:38:43 +01:00

192 lines
4.5 KiB
TypeScript

"use client";
import React, { useState } from "react";
import { useField } from "@payloadcms/ui";
import * as LucideIcons from "lucide-react";
const COMMON_ICONS = [
"Check",
"X",
"AlertTriangle",
"Info",
"ArrowRight",
"ArrowUpRight",
"ChevronRight",
"Settings",
"Tool",
"Terminal",
"Code",
"Database",
"Server",
"Cpu",
"Zap",
"Shield",
"Lock",
"Key",
"Eye",
"Search",
"Filter",
"BarChart",
"LineChart",
"PieChart",
"TrendingUp",
"TrendingDown",
"Users",
"User",
"Briefcase",
"Building",
"Globe",
"Mail",
"FileText",
"File",
"Folder",
"Image",
"Video",
"MessageSquare",
"Clock",
"Calendar",
"CheckCircle",
"XCircle",
"Play",
"Pause",
"Activity",
"Box",
"Layers",
"Layout",
"Monitor",
"Smartphone",
"Tablet",
"Wifi",
"Cloud",
"Crosshair",
"Target",
"Trophy",
"Star",
"Heart",
];
export default function IconSelectorField({ path }: { path: string }) {
const { value, setValue } = useField<string>({ path });
const [searchTerm, setSearchTerm] = useState("");
const filteredIcons = COMMON_ICONS.filter((name) =>
name.toLowerCase().includes(searchTerm.toLowerCase()),
);
const handleIconClick = (iconName: string) => {
// Toggle off if clicking the current value
if (value === iconName) {
setValue(null);
} else {
setValue(iconName);
}
};
return (
<div className="field-type" style={{ marginBottom: "1.5rem" }}>
<label
className="field-label"
style={{
display: "block",
marginBottom: "8px",
fontSize: "0.875rem",
fontWeight: 500,
}}
>
Icon Selection (Lucide)
</label>
<input
type="text"
placeholder="Search icons..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
style={{
padding: "8px 12px",
border: "1px solid var(--theme-elevation-200)",
borderRadius: "4px",
background: "var(--theme-elevation-50)",
color: "var(--theme-text)",
fontSize: "0.875rem",
width: "100%",
marginBottom: "12px",
boxSizing: "border-box",
}}
/>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(40px, 1fr))",
gap: "8px",
maxHeight: "200px",
overflowY: "auto",
padding: "4px",
}}
>
{filteredIcons.map((iconName) => {
const Icon = (LucideIcons as any)[iconName];
if (!Icon) return null;
const isSelected = value === iconName;
return (
<button
key={iconName}
type="button"
title={iconName}
onClick={() => handleIconClick(iconName)}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "40px",
height: "40px",
borderRadius: "6px",
background: isSelected
? "var(--theme-elevation-200)"
: "transparent",
border: isSelected
? "1px solid var(--theme-elevation-400)"
: "1px solid var(--theme-elevation-150)",
color: isSelected
? "var(--theme-text)"
: "var(--theme-elevation-500)",
cursor: "pointer",
transition: "all 0.1s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = isSelected
? "var(--theme-elevation-200)"
: "var(--theme-elevation-100)";
e.currentTarget.style.color = "var(--theme-text)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = isSelected
? "var(--theme-elevation-200)"
: "transparent";
e.currentTarget.style.color = isSelected
? "var(--theme-text)"
: "var(--theme-elevation-500)";
}}
>
<Icon size={20} strokeWidth={1.5} />
</button>
);
})}
</div>
{filteredIcons.length === 0 && (
<div
style={{
fontSize: "0.875rem",
color: "var(--theme-elevation-500)",
marginTop: "8px",
}}
>
No matching icons found.
</div>
)}
</div>
);
}