'use client'; import { useState } from 'react'; import * as Icons from '../Icons'; import type { SaveStatus } from '../SaveBar'; import { resolveExifDisplay } from '@/lib/config/schema'; import { SUPPORTED_LOCALES } from '@/lib/i18n'; import { FeatureGroup, FeatureRow, SettingRow } from './fields'; import type { SectionProps } from './types'; import { useConfirm } from '../ConfirmDialog'; export default function GeneralSection({ settings, update, updateMany }: SectionProps) { const confirm = useConfirm(); // Collapsed by default: the four metadata switches are a detail of one // decision, and showing them permanently is what made the section a wall (#510). const [metadataOpen, setMetadataOpen] = useState(false); const [faviconUploading, setFaviconUploading] = useState(false); const [faviconStatus, setFaviconStatus] = useState(null); // Resolved the same way the site resolves it, so the switches show what a // visitor actually sees — including a config that only ever set the older // `exifOnHover`. const exif = resolveExifDisplay(settings.exif, settings.exifOnHover); // The master switch has no key of its own and needs none: "off" is all four // groups off, a state the site already acts on — with nothing left to show, // the lightbox withdraws its info button and the `i` key entirely (#506). const anyMetadata = exif.camera || exif.settings || exif.location || exif.caption; const metadataSummary = [ exif.camera && 'Camera & lens', exif.settings && 'Exposure', exif.location && 'Location', exif.caption && 'Description', ] .filter(Boolean) .join(' · ') || 'Nothing published — the lightbox hides its info panel'; /** Turning it back on selects everything; the details below narrow it again. */ const toggleMetadata = () => updateMany({ 'exif.camera': !anyMetadata, 'exif.settings': !anyMetadata, 'exif.location': !anyMetadata, 'exif.caption': !anyMetadata, }); return (

General Site Settings

Configure basic site identity, language, and core feature toggles.

update('title', e.target.value)} placeholder="My Portfolio" />
update('subtitle', e.target.value)} placeholder="A visual journal" />

The visitor-facing interface is translated for {SUPPORTED_LOCALES.join(', ')}. Other languages still set <html lang> and date formatting, but show the English interface. The admin panel is always English.

Portfolio Features & Modules

Optional pages, and what the site reveals about your photos and counts about visits.

} title="Modules" description="Each one adds a page or a control visitors can use." >
} title="About page" description="Portrait, bio and gear at /about" checked={settings.about?.enabled !== false} onToggle={() => update('about.enabled', settings.about?.enabled === false)} href="/admin/settings/about" /> } title="Map" description="Photo locations on a world map at /map" checked={settings.map === true} onToggle={() => update('map', !settings.map)} /> } title="Client proofing" description="Visitors heart, filter and export a selection of photos" checked={settings.proofing?.enabled !== false} onToggle={() => update('proofing.enabled', settings.proofing?.enabled === false)} />
} title="What visitors learn about you" description="What each photo reveals, and what is counted about a visit." chip="visible to visitors" >
{metadataOpen && (
update('exif.camera', !exif.camera)} /> update('exif.settings', !exif.settings)} /> update('exif.location', !exif.location)} /> update('exif.caption', !exif.caption)} /> {/* Dependent, not a peer: this only controls the grid overlay, and the overlay carries camera and lens. With those off it has nothing to show, which used to leave it switched on and silently doing nothing (#510). */} update('exifOnHover', settings.exifOnHover === false)} />
)}
} title="Analytics" description="Cookieless view counts, stored on your server only" checked={settings.analytics !== false} onToggle={() => update('analytics', settings.analytics === false)} href="/admin/analytics" />
Favicon
{ const file = e.target.files?.[0]; if (!file) return; setFaviconStatus(null); setFaviconUploading(true); const form = new FormData(); form.append('file', file); try { const res = await fetch('/api/admin/favicon', { method: 'PUT', body: form, }); const data = await res.json(); setFaviconStatus( res.ok ? { kind: 'success', message: data.message } : { kind: 'error', message: `Error: ${data.error}` }, ); } catch { setFaviconStatus({ kind: 'error', message: 'Error: Upload failed' }); } finally { setFaviconUploading(false); e.target.value = ''; } }} /> {faviconUploading &&
}
{faviconStatus && (

{faviconStatus.message}

)} SVG, PNG, ICO, or JPEG — max 512 kB. Stored in the content volume. Reset restores the bundled default.
); }