'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, getDictionary, resolveLocale } 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, siteTitleEnv, }: SectionProps & { siteTitleEnv?: string | null }) { const confirm = useConfirm(); // What the site shows while the field is empty, resolved as getConfig does: // SITE_TITLE, else the site language's word for a gallery (QA A-15). const fallbackTitle = siteTitleEnv || getDictionary(resolveLocale(settings.lang)).common.gallery; // 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); /** Bumped after an upload or reset so the preview fetches the new icon. */ const [faviconVersion, setFaviconVersion] = useState(0); // 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={fallbackTitle} aria-describedby="general-site-title-hint" />

{settings.title ? 'Shown in the header, the browser tab and link previews.' : `Empty, so the site shows “${fallbackTitle}”${siteTitleEnv ? ', from SITE_TITLE' : ''}.`}

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="Photo zoom" description="Visitors zoom into a photo to full resolution (1:1) to check sharpness. Pages and albums can override this" checked={settings.zoom === true} onToggle={() => update('zoom', !settings.zoom)} />
{/* Shown even while proofing is off globally: a subpage can switch it on for itself (`proofing: true`), so the address still has to be settable from here. */}
update('proofing.email', e.target.value)} placeholder="Defaults to the footer contact email" />

Where “Email to photographer” sends a selection. Falls back to the footer contact email; when neither is set, the button is hidden.

} 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
{/* eslint-disable-next-line @next/next/no-img-element -- the site's own icon route, a 32px preview */} Current favicon {/* The native file input stays the control — focusable, keyboard operable, announced as a file picker — but is visually hidden; its label is drawn as the admin's secondary button, with the focus ring passed through (QA A-18). */} { 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}` }, ); if (res.ok) setFaviconVersion((v) => v + 1); } 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.
); }