ai generated solutions to our ai generated problems
This commit is contained in:
+127
-166
@@ -3271,25 +3271,79 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
|
|||||||
setDraftColor('#e82517')
|
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 (
|
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">
|
<div className="border-b border-border pb-6">
|
||||||
<p className="text-sm font-semibold uppercase tracking-wide text-fury-cyan">Preferences</p>
|
<h1 className="text-3xl font-bold">Settings</h1>
|
||||||
<h1 className="mt-2 text-4xl font-bold">Settings</h1>
|
<p className="mt-2 text-sm text-text-soft">
|
||||||
<p className="mt-3 max-w-xl text-text-soft">
|
Your choices are saved locally and persist across visits.
|
||||||
Customise the look and feel of the site. Your choices are saved in cookies and local storage.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Appearance */}
|
|
||||||
<div className="mt-8 space-y-6">
|
<div className="mt-8 space-y-6">
|
||||||
|
{/* Appearance */}
|
||||||
<div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm">
|
<div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm">
|
||||||
<h2 className="text-lg font-semibold">Appearance</h2>
|
<h2 className="text-base font-semibold">Appearance</h2>
|
||||||
<p className="mt-1 text-sm text-text-soft">Choose a base theme and accent colour.</p>
|
|
||||||
|
|
||||||
{/* 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">
|
<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">
|
<div className="flex flex-wrap gap-2">
|
||||||
{accentPresets.map((preset) => {
|
{accentPresets.map((preset) => {
|
||||||
const isActive = activePresetId === preset.id
|
const isActive = activePresetId === preset.id
|
||||||
@@ -3298,27 +3352,22 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
|
|||||||
key={preset.id}
|
key={preset.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handlePreset(preset)}
|
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-fury-cyan bg-fury-cyan text-bg'
|
||||||
: 'border-border bg-surface text-text hover:border-ring hover:bg-surface-alt'
|
: 'border-border bg-surface text-text hover:border-ring hover:bg-surface-alt'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{preset.color ? (
|
|
||||||
<span
|
<span
|
||||||
className="h-3 w-3 rounded-full border border-white/20 shrink-0"
|
className="h-2.5 w-2.5 rounded-full shrink-0"
|
||||||
style={{ background: preset.color }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
className="h-3 w-3 rounded-full shrink-0"
|
|
||||||
style={{
|
style={{
|
||||||
background: preset.id === 'default-light'
|
background: preset.color
|
||||||
|
? preset.color
|
||||||
|
: preset.id === 'default-light'
|
||||||
? 'linear-gradient(135deg, #fefde7 50%, #e82517 50%)'
|
? 'linear-gradient(135deg, #fefde7 50%, #e82517 50%)'
|
||||||
: 'linear-gradient(135deg, #130d08 50%, #ff6a5f 50%)',
|
: 'linear-gradient(135deg, #130d08 50%, #ff6a5f 50%)',
|
||||||
border: '1px solid rgba(255,255,255,0.2)',
|
border: '1px solid rgba(128,128,128,0.3)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{preset.label}
|
{preset.label}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
@@ -3326,176 +3375,89 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Light / Dark toggle */}
|
{/* Colour grid */}
|
||||||
<div className="mt-5 flex items-center gap-3">
|
<div className="mt-5">
|
||||||
<p className="text-sm font-semibold text-text-soft">Base mode:</p>
|
<div className="mb-2.5 flex items-center justify-between">
|
||||||
<div className="flex rounded-full border border-border bg-surface p-0.5">
|
<p className="text-xs font-semibold uppercase tracking-wide text-text-muted">Colours</p>
|
||||||
|
{hasAnyCustomColor && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onThemeChange('light')}
|
onClick={resetAllColors}
|
||||||
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'
|
className="text-xs text-text-muted transition hover:text-text"
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
☼ Light
|
Reset all
|
||||||
</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>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<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 }) => {
|
||||||
{/* Custom colour picker */}
|
const displayVal = getDisplayVal()
|
||||||
<div className="mt-6">
|
const customised = isCustom()
|
||||||
<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 ? (
|
|
||||||
<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"
|
|
||||||
>
|
|
||||||
Reset to default
|
|
||||||
</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 (
|
return (
|
||||||
<div key={field} className="flex flex-wrap items-center gap-3">
|
<div
|
||||||
|
key={id}
|
||||||
|
className="group relative flex flex-col gap-2 rounded-lg border border-border bg-surface p-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<label
|
<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"
|
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 }}
|
style={{ background: displayVal }}
|
||||||
title={`Pick ${label}`}
|
title={`Pick ${label}`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
value={displayVal}
|
value={displayVal}
|
||||||
onChange={(e) => onCustomColorsChange({ ...customColors, [field]: e.target.value })}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
{customised && (
|
||||||
<div className="flex min-w-0 flex-col">
|
<button
|
||||||
<span className="text-sm font-semibold text-text leading-none">{label}</span>
|
type="button"
|
||||||
<span className="text-xs text-text-muted mt-0.5">{desc}</span>
|
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>
|
||||||
|
<div>
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<p className="text-xs font-semibold text-text leading-none">
|
||||||
<div className="flex items-center gap-1 rounded-md border border-border bg-surface px-2 py-1.5">
|
{label}
|
||||||
<span className="text-xs font-semibold text-text-muted">#</span>
|
{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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={displayVal.replace('#', '')}
|
value={displayVal.replace('#', '')}
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const v = '#' + e.target.value.replace(/[^0-9a-fA-F]/g, '')
|
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}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</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>
|
</div>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cookie & Analytics */}
|
{/* Analytics */}
|
||||||
<div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm">
|
<div className="rounded-xl border border-border bg-fury-white p-6 shadow-sm">
|
||||||
<h2 className="text-lg font-semibold">Cookie & analytics settings</h2>
|
<h2 className="text-base font-semibold">Analytics & cookies</h2>
|
||||||
<p className="mt-1 text-sm leading-6 text-text-soft">
|
<p className="mt-1 text-sm 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.
|
A necessary cookie stores these preferences. You can allow analytics for the viewers page and control what's included.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-4 space-y-2">
|
||||||
<PreferenceToggle
|
<PreferenceToggle
|
||||||
checked
|
checked
|
||||||
description="Stores your theme and consent choice so preferences persist across visits."
|
description="Stores your theme and consent choice so preferences persist across visits."
|
||||||
@@ -3508,42 +3470,41 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
|
|||||||
label="Viewer analytics"
|
label="Viewer analytics"
|
||||||
onChange={(value) => updateDraft('analytics', value)}
|
onChange={(value) => updateDraft('analytics', value)}
|
||||||
/>
|
/>
|
||||||
|
{draft.analytics && (
|
||||||
|
<div className="ml-7 space-y-2">
|
||||||
<PreferenceToggle
|
<PreferenceToggle
|
||||||
checked={draft.device}
|
checked={draft.device}
|
||||||
description="Includes browser, operating system, and broad device type."
|
description="Browser, OS, and broad device type."
|
||||||
disabled={!draft.analytics}
|
label="Browser and device"
|
||||||
label="Browser and device details"
|
|
||||||
onChange={(value) => updateDraft('device', value)}
|
onChange={(value) => updateDraft('device', value)}
|
||||||
/>
|
/>
|
||||||
<PreferenceToggle
|
<PreferenceToggle
|
||||||
checked={draft.display}
|
checked={draft.display}
|
||||||
description="Includes screen size, viewport size, and colour depth."
|
description="Screen size, viewport size, and colour depth."
|
||||||
disabled={!draft.analytics}
|
|
||||||
label="Screen details"
|
label="Screen details"
|
||||||
onChange={(value) => updateDraft('display', value)}
|
onChange={(value) => updateDraft('display', value)}
|
||||||
/>
|
/>
|
||||||
<PreferenceToggle
|
<PreferenceToggle
|
||||||
checked={draft.locale}
|
checked={draft.locale}
|
||||||
description="Includes browser language and timezone."
|
description="Browser language and timezone."
|
||||||
disabled={!draft.analytics}
|
|
||||||
label="Language and timezone"
|
label="Language and timezone"
|
||||||
onChange={(value) => updateDraft('locale', value)}
|
onChange={(value) => updateDraft('locale', value)}
|
||||||
/>
|
/>
|
||||||
<PreferenceToggle
|
<PreferenceToggle
|
||||||
checked={draft.referrer}
|
checked={draft.referrer}
|
||||||
description="Includes the page that linked you here when the browser provides it."
|
description="The page that linked you here, when the browser provides it."
|
||||||
disabled={!draft.analytics}
|
|
||||||
label="Referrer"
|
label="Referrer"
|
||||||
onChange={(value) => updateDraft('referrer', value)}
|
onChange={(value) => updateDraft('referrer', value)}
|
||||||
/>
|
/>
|
||||||
<PreferenceToggle
|
<PreferenceToggle
|
||||||
checked={draft.diagnostics}
|
checked={draft.diagnostics}
|
||||||
description="Includes network quality, privacy signals, touch support, CPU/memory hints, and other browser diagnostics."
|
description="Network quality, privacy signals, touch support, CPU/memory hints."
|
||||||
disabled={!draft.analytics}
|
|
||||||
label="Technical diagnostics"
|
label="Technical diagnostics"
|
||||||
onChange={(value) => updateDraft('diagnostics', value)}
|
onChange={(value) => updateDraft('diagnostics', value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
<div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||||
<button
|
<button
|
||||||
@@ -3551,7 +3512,7 @@ function SettingsPage({ theme, onThemeChange, customColor, onCustomColorChange,
|
|||||||
onClick={() => onAnalyticsChoose({ ...defaultAnalyticsPreferences, chosen: true })}
|
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"
|
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>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user