ai generated solutions to our ai generated problems
This commit is contained in:
+171
-210
@@ -3271,25 +3271,79 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
|
||||
setDraftColor('#e82517')
|
||||
}
|
||||
|
||||
const hasAnyCustomColor = customColor || Object.keys(customColors).length > 0
|
||||
|
||||
function resetAllColors() {
|
||||
onCustomColorChange('')
|
||||
setDraftColor('#e82517')
|
||||
onCustomColorsChange({})
|
||||
}
|
||||
|
||||
const allColorDefs = [
|
||||
{
|
||||
id: 'accent',
|
||||
label: 'Accent',
|
||||
desc: 'Links, highlights, interactive elements',
|
||||
getValue: () => draftColor,
|
||||
getDisplayVal: () => draftColor,
|
||||
isCustom: () => !!customColor,
|
||||
onChange: handleColorPickerChange,
|
||||
onReset: resetToDefault,
|
||||
},
|
||||
...advancedColorDefs.map(({ field, label, desc, defaults }) => ({
|
||||
id: field,
|
||||
label,
|
||||
desc,
|
||||
getValue: () => customColors[field] || '',
|
||||
getDisplayVal: () => customColors[field] || defaults[theme] || defaults.dark,
|
||||
isCustom: () => !!customColors[field],
|
||||
onChange: (val) => onCustomColorsChange({ ...customColors, [field]: val }),
|
||||
onReset: () => {
|
||||
const next = { ...customColors }
|
||||
delete next[field]
|
||||
onCustomColorsChange(next)
|
||||
},
|
||||
})),
|
||||
]
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-3xl pb-16 pt-24 sm:pt-28">
|
||||
<section className="mx-auto max-w-2xl pb-16 pt-24 sm:pt-28">
|
||||
<div className="border-b border-border pb-6">
|
||||
<p className="text-sm font-semibold uppercase tracking-wide text-fury-cyan">Preferences</p>
|
||||
<h1 className="mt-2 text-4xl font-bold">Settings</h1>
|
||||
<p className="mt-3 max-w-xl text-text-soft">
|
||||
Customise the look and feel of the site. Your choices are saved in cookies and local storage.
|
||||
<h1 className="text-3xl font-bold">Settings</h1>
|
||||
<p className="mt-2 text-sm text-text-soft">
|
||||
Your choices are saved locally and persist across visits.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Appearance */}
|
||||
<div className="mt-8 space-y-6">
|
||||
{/* Appearance */}
|
||||
<div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold">Appearance</h2>
|
||||
<p className="mt-1 text-sm text-text-soft">Choose a base theme and accent colour.</p>
|
||||
<h2 className="text-base font-semibold">Appearance</h2>
|
||||
|
||||
{/* Theme presets */}
|
||||
{/* Light / Dark toggle */}
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<span className="text-sm text-text-soft">Base mode</span>
|
||||
<div className="flex rounded-full border border-border bg-surface p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onThemeChange('light')}
|
||||
className={`rounded-full px-4 py-1 text-sm font-semibold transition ${theme === 'light' ? 'bg-text text-bg' : 'text-text-soft hover:text-text'}`}
|
||||
>
|
||||
☼ Light
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onThemeChange('dark')}
|
||||
className={`rounded-full px-4 py-1 text-sm font-semibold transition ${theme === 'dark' ? 'bg-text text-bg' : 'text-text-soft hover:text-text'}`}
|
||||
>
|
||||
☾ Dark
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Presets */}
|
||||
<div className="mt-5">
|
||||
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-text-muted">Theme presets</p>
|
||||
<p className="mb-2.5 text-xs font-semibold uppercase tracking-wide text-text-muted">Presets</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{accentPresets.map((preset) => {
|
||||
const isActive = activePresetId === preset.id
|
||||
@@ -3298,27 +3352,22 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => handlePreset(preset)}
|
||||
className={`flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm font-semibold transition ${isActive
|
||||
className={`flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold transition ${isActive
|
||||
? 'border-fury-cyan bg-fury-cyan text-bg'
|
||||
: 'border-border bg-surface text-text hover:border-ring hover:bg-surface-alt'
|
||||
}`}
|
||||
>
|
||||
{preset.color ? (
|
||||
<span
|
||||
className="h-3 w-3 rounded-full border border-white/20 shrink-0"
|
||||
style={{ background: preset.color }}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="h-3 w-3 rounded-full shrink-0"
|
||||
style={{
|
||||
background: preset.id === 'default-light'
|
||||
? 'linear-gradient(135deg, #fefde7 50%, #e82517 50%)'
|
||||
: 'linear-gradient(135deg, #130d08 50%, #ff6a5f 50%)',
|
||||
border: '1px solid rgba(255,255,255,0.2)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className="h-2.5 w-2.5 rounded-full shrink-0"
|
||||
style={{
|
||||
background: preset.color
|
||||
? preset.color
|
||||
: preset.id === 'default-light'
|
||||
? 'linear-gradient(135deg, #fefde7 50%, #e82517 50%)'
|
||||
: 'linear-gradient(135deg, #130d08 50%, #ff6a5f 50%)',
|
||||
border: '1px solid rgba(128,128,128,0.3)',
|
||||
}}
|
||||
/>
|
||||
{preset.label}
|
||||
</button>
|
||||
)
|
||||
@@ -3326,176 +3375,89 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Light / Dark toggle */}
|
||||
<div className="mt-5 flex items-center gap-3">
|
||||
<p className="text-sm font-semibold text-text-soft">Base mode:</p>
|
||||
<div className="flex rounded-full border border-border bg-surface p-0.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onThemeChange('light')}
|
||||
className={`rounded-full px-4 py-1.5 text-sm font-semibold transition ${theme === 'light' ? 'bg-text text-bg' : 'text-text-soft hover:text-text'
|
||||
}`}
|
||||
>
|
||||
☼ Light
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onThemeChange('dark')}
|
||||
className={`rounded-full px-4 py-1.5 text-sm font-semibold transition ${theme === 'dark' ? 'bg-text text-bg' : 'text-text-soft hover:text-text'
|
||||
}`}
|
||||
>
|
||||
☾ Dark
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom colour picker */}
|
||||
<div className="mt-6">
|
||||
<p className="mb-3 text-xs font-semibold uppercase tracking-wide text-text-muted">Custom accent colour</p>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
{/* Colour wheel trigger */}
|
||||
<label
|
||||
className="relative h-11 w-11 cursor-pointer overflow-hidden rounded-full border-2 border-border shadow-md transition hover:border-ring hover:scale-105 active:scale-95"
|
||||
style={{ background: draftColor }}
|
||||
title="Pick a custom colour"
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
value={draftColor}
|
||||
onChange={(e) => handleColorPickerChange(e.target.value)}
|
||||
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* Hex input */}
|
||||
<div className="flex items-center gap-1.5 rounded-md border border-border bg-surface px-3 py-2">
|
||||
<span className="text-xs font-semibold text-text-muted">#</span>
|
||||
<input
|
||||
type="text"
|
||||
value={draftColor.replace('#', '')}
|
||||
maxLength={6}
|
||||
onChange={(e) => handleColorInput('#' + e.target.value.replace(/[^0-9a-fA-F]/g, ''))}
|
||||
className="w-20 bg-transparent text-sm font-mono font-semibold text-text outline-none"
|
||||
placeholder="e82517"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Live preview swatch */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="h-8 rounded-md px-3 py-1.5 text-xs font-semibold text-white shadow"
|
||||
style={{ background: draftColor }}
|
||||
>
|
||||
Preview
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Reset button */}
|
||||
{customColor ? (
|
||||
{/* Colour grid */}
|
||||
<div className="mt-5">
|
||||
<div className="mb-2.5 flex items-center justify-between">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-text-muted">Colours</p>
|
||||
{hasAnyCustomColor && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={resetToDefault}
|
||||
className="rounded-md border border-border px-3 py-1.5 text-xs font-semibold text-text-soft transition hover:bg-surface hover:text-text"
|
||||
onClick={resetAllColors}
|
||||
className="text-xs text-text-muted transition hover:text-text"
|
||||
>
|
||||
Reset to default
|
||||
Reset all
|
||||
</button>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-text-muted">
|
||||
Changes all accent highlights, links, and interactive elements across the site.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced colours */}
|
||||
<div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold">Advanced colours</h2>
|
||||
<p className="mt-1 text-sm text-text-soft">Override individual colours for the current theme. Each resets independently.</p>
|
||||
|
||||
<div className="mt-5 space-y-4">
|
||||
{advancedColorDefs.map(({ field, label, desc, defaults }) => {
|
||||
const defaultVal = defaults[theme] ?? defaults.dark
|
||||
const currentVal = customColors[field] || ''
|
||||
const displayVal = currentVal || defaultVal
|
||||
return (
|
||||
<div key={field} className="flex flex-wrap items-center gap-3">
|
||||
<label
|
||||
className="relative h-9 w-9 shrink-0 cursor-pointer overflow-hidden rounded-full border-2 border-border shadow-sm transition hover:border-ring hover:scale-105 active:scale-95"
|
||||
style={{ background: displayVal }}
|
||||
title={`Pick ${label}`}
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{allColorDefs.map(({ id, label, desc, getDisplayVal, isCustom, onChange, onReset }) => {
|
||||
const displayVal = getDisplayVal()
|
||||
const customised = isCustom()
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className="group relative flex flex-col gap-2 rounded-lg border border-border bg-surface p-3"
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
value={displayVal}
|
||||
onChange={(e) => onCustomColorsChange({ ...customColors, [field]: e.target.value })}
|
||||
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="text-sm font-semibold text-text leading-none">{label}</span>
|
||||
<span className="text-xs text-text-muted mt-0.5">{desc}</span>
|
||||
</div>
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<div className="flex items-center gap-1 rounded-md border border-border bg-surface px-2 py-1.5">
|
||||
<span className="text-xs font-semibold text-text-muted">#</span>
|
||||
<div className="flex items-center justify-between">
|
||||
<label
|
||||
className="relative h-7 w-7 cursor-pointer overflow-hidden rounded-full border-2 border-border shadow-sm transition hover:scale-105 active:scale-95"
|
||||
style={{ background: displayVal }}
|
||||
title={`Pick ${label}`}
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
value={displayVal}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
||||
/>
|
||||
</label>
|
||||
{customised && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReset}
|
||||
className="text-xs text-text-muted opacity-0 transition group-hover:opacity-100 hover:text-text"
|
||||
title="Reset to default"
|
||||
>
|
||||
↺
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-text leading-none">
|
||||
{label}
|
||||
{customised && <span className="ml-1 text-fury-cyan">•</span>}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-text-muted leading-tight">{desc}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 rounded border border-border bg-bg px-1.5 py-1">
|
||||
<span className="text-xs text-text-muted">#</span>
|
||||
<input
|
||||
type="text"
|
||||
value={displayVal.replace('#', '')}
|
||||
maxLength={6}
|
||||
onChange={(e) => {
|
||||
const v = '#' + e.target.value.replace(/[^0-9a-fA-F]/g, '')
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(v)) onCustomColorsChange({ ...customColors, [field]: v })
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(v)) onChange(v)
|
||||
}}
|
||||
className="w-16 bg-transparent text-xs font-mono font-semibold text-text outline-none"
|
||||
className="w-full bg-transparent text-xs font-mono text-text outline-none"
|
||||
spellCheck={false}
|
||||
/>
|
||||
</div>
|
||||
{currentVal ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const next = { ...customColors }
|
||||
delete next[field]
|
||||
onCustomColorsChange(next)
|
||||
}}
|
||||
className="rounded-md border border-border px-2 py-1.5 text-xs font-semibold text-text-muted transition hover:bg-surface hover:text-text"
|
||||
title="Reset to theme default"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
) : (
|
||||
<span className="px-2 py-1.5 text-xs text-text-muted">Theme default</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{Object.keys(customColors).length > 0 ? (
|
||||
<div className="mt-5 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onCustomColorsChange({})}
|
||||
className="rounded-md border border-border px-3 py-1.5 text-xs font-semibold text-text-soft transition hover:bg-surface hover:text-text"
|
||||
>
|
||||
Reset all to theme defaults
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cookie & Analytics */}
|
||||
{/* Analytics */}
|
||||
<div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold">Cookie & analytics settings</h2>
|
||||
<p className="mt-1 text-sm leading-6 text-text-soft">
|
||||
We use a necessary cookie to remember these choices. You can also allow analytics for the public viewers page and choose which details are included.
|
||||
<h2 className="text-base font-semibold">Analytics & cookies</h2>
|
||||
<p className="mt-1 text-sm text-text-soft">
|
||||
A necessary cookie stores these preferences. You can allow analytics for the viewers page and control what's included.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
<div className="mt-4 space-y-2">
|
||||
<PreferenceToggle
|
||||
checked
|
||||
description="Stores your theme and consent choice so preferences persist across visits."
|
||||
@@ -3508,41 +3470,40 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
|
||||
label="Viewer analytics"
|
||||
onChange={(value) => updateDraft('analytics', value)}
|
||||
/>
|
||||
<PreferenceToggle
|
||||
checked={draft.device}
|
||||
description="Includes browser, operating system, and broad device type."
|
||||
disabled={!draft.analytics}
|
||||
label="Browser and device details"
|
||||
onChange={(value) => updateDraft('device', value)}
|
||||
/>
|
||||
<PreferenceToggle
|
||||
checked={draft.display}
|
||||
description="Includes screen size, viewport size, and colour depth."
|
||||
disabled={!draft.analytics}
|
||||
label="Screen details"
|
||||
onChange={(value) => updateDraft('display', value)}
|
||||
/>
|
||||
<PreferenceToggle
|
||||
checked={draft.locale}
|
||||
description="Includes browser language and timezone."
|
||||
disabled={!draft.analytics}
|
||||
label="Language and timezone"
|
||||
onChange={(value) => updateDraft('locale', value)}
|
||||
/>
|
||||
<PreferenceToggle
|
||||
checked={draft.referrer}
|
||||
description="Includes the page that linked you here when the browser provides it."
|
||||
disabled={!draft.analytics}
|
||||
label="Referrer"
|
||||
onChange={(value) => updateDraft('referrer', value)}
|
||||
/>
|
||||
<PreferenceToggle
|
||||
checked={draft.diagnostics}
|
||||
description="Includes network quality, privacy signals, touch support, CPU/memory hints, and other browser diagnostics."
|
||||
disabled={!draft.analytics}
|
||||
label="Technical diagnostics"
|
||||
onChange={(value) => updateDraft('diagnostics', value)}
|
||||
/>
|
||||
{draft.analytics && (
|
||||
<div className="ml-7 space-y-2">
|
||||
<PreferenceToggle
|
||||
checked={draft.device}
|
||||
description="Browser, OS, and broad device type."
|
||||
label="Browser and device"
|
||||
onChange={(value) => updateDraft('device', value)}
|
||||
/>
|
||||
<PreferenceToggle
|
||||
checked={draft.display}
|
||||
description="Screen size, viewport size, and colour depth."
|
||||
label="Screen details"
|
||||
onChange={(value) => updateDraft('display', value)}
|
||||
/>
|
||||
<PreferenceToggle
|
||||
checked={draft.locale}
|
||||
description="Browser language and timezone."
|
||||
label="Language and timezone"
|
||||
onChange={(value) => updateDraft('locale', value)}
|
||||
/>
|
||||
<PreferenceToggle
|
||||
checked={draft.referrer}
|
||||
description="The page that linked you here, when the browser provides it."
|
||||
label="Referrer"
|
||||
onChange={(value) => updateDraft('referrer', value)}
|
||||
/>
|
||||
<PreferenceToggle
|
||||
checked={draft.diagnostics}
|
||||
description="Network quality, privacy signals, touch support, CPU/memory hints."
|
||||
label="Technical diagnostics"
|
||||
onChange={(value) => updateDraft('diagnostics', value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||
@@ -3551,7 +3512,7 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
|
||||
onClick={() => onAnalyticsChoose({ ...defaultAnalyticsPreferences, chosen: true })}
|
||||
className="rounded-md border border-border px-4 py-2 text-sm font-semibold text-text-soft transition hover:bg-surface hover:text-text"
|
||||
>
|
||||
Decline all analytics
|
||||
Decline all
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
Reference in New Issue
Block a user