| @@ -11,7 +11,7 @@ function StyledSelect<T extends string>({ | |||
| }: { | |||
| value: T; | |||
| onChange: (v: T) => void; | |||
| options: { value: T; label: string }[]; | |||
| options: { value: T; label: string; style?: React.CSSProperties }[]; | |||
| }) { | |||
| const [open, setOpen] = useState(false); | |||
| const ref = useRef<HTMLDivElement>(null); | |||
| @@ -45,7 +45,7 @@ function StyledSelect<T extends string>({ | |||
| onClick={() => setOpen(o => !o)} | |||
| className={`${inputBase} text-left flex items-center justify-between cursor-pointer`} | |||
| > | |||
| <span className={displayLabel ? '' : 'text-gray-400'}>{displayLabel || 'Seleziona…'}</span> | |||
| <span className={displayLabel ? '' : 'text-gray-400'} style={current?.style}>{displayLabel || 'Seleziona…'}</span> | |||
| <span className={`text-gray-500 transition-transform ${open ? 'rotate-180' : ''}`}>▾</span> | |||
| </button> | |||
| {open && ( | |||
| @@ -56,6 +56,7 @@ function StyledSelect<T extends string>({ | |||
| type="button" | |||
| onClick={() => { onChange(o.value); setOpen(false); }} | |||
| className={`w-full text-left px-3 py-2.5 hover:bg-blue-50 transition-colors ${o.value === value ? 'bg-blue-100 font-semibold text-blue-700' : 'text-gray-800'}`} | |||
| style={o.style} | |||
| > | |||
| {o.label} | |||
| </button> | |||
| @@ -79,6 +80,14 @@ const isVideoFile = (file: File) => | |||
| const isPlayableVideoFile = (file: File) => | |||
| new RegExp(`\\.(${PLAYBACK_SUPPORTED_VIDEO})$`, 'i').test(file.name); | |||
| const previewFontFamily = (filename: string): string => | |||
| `PortalPreview-${filename.replace(/[^A-Za-z0-9]/g, '_')}`; | |||
| const fontFormatFromName = (filename: string): string => { | |||
| const ext = filename.match(/\.([^.]+)$/)?.[1].toLowerCase() ?? 'woff2'; | |||
| return ({ woff2: 'woff2', woff: 'woff', ttf: 'truetype', otf: 'opentype' } as Record<string, string>)[ext] ?? 'woff2'; | |||
| }; | |||
| const extractFileName = (url: string): string => { | |||
| const match = url.match(/[?&]name=([^&]+)/); | |||
| if (match) return decodeURIComponent(match[1]); | |||
| @@ -538,12 +547,22 @@ export default function AdminDashboard() { | |||
| <div> | |||
| <label className="block text-sm font-semibold text-gray-700 mb-1">Font del portale</label> | |||
| <style dangerouslySetInnerHTML={{ __html: availableFonts.map(f => ` | |||
| @font-face { | |||
| font-family: '${previewFontFamily(f)}'; | |||
| src: url('/api/fonts?name=${encodeURIComponent(f)}') format('${fontFormatFromName(f)}'); | |||
| font-display: swap; | |||
| }`).join('') }} /> | |||
| <StyledSelect<string> | |||
| value={portal.fontFamily ?? ''} | |||
| onChange={(v) => setPortal({ ...portal, fontFamily: v })} | |||
| options={[ | |||
| { value: '', label: 'Sistema (Arial)' }, | |||
| ...availableFonts.map(f => ({ value: f, label: f.replace(/\.(woff2?|ttf|otf)$/i, '') })), | |||
| ...availableFonts.map(f => ({ | |||
| value: f, | |||
| label: f.replace(/\.(woff2?|ttf|otf)$/i, ''), | |||
| style: { fontFamily: `'${previewFontFamily(f)}', Arial, Helvetica, sans-serif` }, | |||
| })), | |||
| ]} | |||
| /> | |||
| </div> | |||