Public Access
feat: implement tweaks context for user preferences and remove TweaksPanel component
This commit is contained in:
1 parent
92c1de650a
commit
50efba6f34
8 files changed
+212
-147
No files matched your search
@@ -353,6 +353,28 @@ export default function ExamRunner({
|
||||
<div style={{ fontSize: 14 }}>{exam.author}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="meta-block" style={{ marginTop: 'auto' }}>
|
||||
<div className="meta-label">{t('tweaks.feedback')}</div>
|
||||
<div className="tweak-control">
|
||||
<button
|
||||
className={`tweak-seg${feedbackMode === 'immediate' ? ' active' : ''}`}
|
||||
onClick={() =>
|
||||
setState((s) => ({ ...s, feedbackMode: 'immediate' }))
|
||||
}
|
||||
>
|
||||
{t('tweaks.immediate')}
|
||||
</button>
|
||||
<button
|
||||
className={`tweak-seg${feedbackMode === 'end' ? ' active' : ''}`}
|
||||
onClick={() =>
|
||||
setState((s) => ({ ...s, feedbackMode: 'end' }))
|
||||
}
|
||||
>
|
||||
{t('tweaks.end')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { useTweaks } from '../contexts/TweaksContext'
|
||||
import * as authApi from '../api/authApi'
|
||||
import type { ThemeMode, FontSize, FeedbackTweak } from '../types'
|
||||
|
||||
function Seg<K extends string>({ value, options, onChange }: {
|
||||
value: K
|
||||
options: { value: K; label: string }[]
|
||||
onChange: (v: K) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="tweak-control">
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
className={`tweak-seg${value === opt.value ? ' active' : ''}`}
|
||||
onClick={() => onChange(opt.value)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Settings() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { user, refreshUser } = useAuth()
|
||||
const { tweaks, updateTweak } = useTweaks()
|
||||
|
||||
const [displayName, setDisplayName] = useState(user?.displayName ?? '')
|
||||
const [savingName, setSavingName] = useState(false)
|
||||
const [nameMsg, setNameMsg] = useState<{ ok: boolean; text: string } | null>(null)
|
||||
@@ -188,14 +212,14 @@ export default function Settings() {
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => i18n.changeLanguage('en')}
|
||||
onClick={() => void i18n.changeLanguage('en')}
|
||||
style={{ fontWeight: i18n.language === 'en' ? 700 : 400, fontSize: 13 }}
|
||||
>
|
||||
English
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={() => i18n.changeLanguage('nl')}
|
||||
onClick={() => void i18n.changeLanguage('nl')}
|
||||
style={{ fontWeight: i18n.language === 'nl' ? 700 : 400, fontSize: 13 }}
|
||||
>
|
||||
Nederlands
|
||||
@@ -204,6 +228,66 @@ export default function Settings() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workspace-panel">
|
||||
<div className="workspace-panel-header">
|
||||
<h3>{t('workspace.appearance')}</h3>
|
||||
</div>
|
||||
<div className="workspace-panel-body workspace-settings-form">
|
||||
<div className="workspace-setting-row">
|
||||
<label className="workspace-setting-label">
|
||||
<span className="workspace-setting-title">{t('workspace.themeLabel')}</span>
|
||||
<span className="workspace-setting-desc">{t('workspace.themeDesc')}</span>
|
||||
</label>
|
||||
<Seg
|
||||
value={tweaks.theme}
|
||||
options={[
|
||||
{ value: 'light' as ThemeMode, label: t('tweaks.light') },
|
||||
{ value: 'dark' as ThemeMode, label: t('tweaks.dark') },
|
||||
]}
|
||||
onChange={(v) => updateTweak('theme', v)}
|
||||
/>
|
||||
</div>
|
||||
<div className="workspace-setting-row">
|
||||
<label className="workspace-setting-label">
|
||||
<span className="workspace-setting-title">{t('workspace.textSizeLabel')}</span>
|
||||
<span className="workspace-setting-desc">{t('workspace.textSizeDesc')}</span>
|
||||
</label>
|
||||
<Seg
|
||||
value={tweaks.fontSize}
|
||||
options={[
|
||||
{ value: 'small' as FontSize, label: t('tweaks.small') },
|
||||
{ value: 'medium' as FontSize, label: t('tweaks.medium') },
|
||||
{ value: 'large' as FontSize, label: t('tweaks.large') },
|
||||
]}
|
||||
onChange={(v) => updateTweak('fontSize', v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="workspace-panel">
|
||||
<div className="workspace-panel-header">
|
||||
<h3>{t('workspace.defaultFeedback')}</h3>
|
||||
</div>
|
||||
<div className="workspace-panel-body workspace-settings-form">
|
||||
<div className="workspace-setting-row">
|
||||
<label className="workspace-setting-label">
|
||||
<span className="workspace-setting-title">{t('workspace.defaultFeedbackLabel')}</span>
|
||||
<span className="workspace-setting-desc">{t('workspace.defaultFeedbackDesc')}</span>
|
||||
</label>
|
||||
<Seg
|
||||
value={tweaks.feedbackMode}
|
||||
options={[
|
||||
{ value: 'auto' as FeedbackTweak, label: t('tweaks.auto') },
|
||||
{ value: 'immediate' as FeedbackTweak, label: t('tweaks.immediate') },
|
||||
{ value: 'end' as FeedbackTweak, label: t('tweaks.end') },
|
||||
]}
|
||||
onChange={(v) => updateTweak('feedbackMode', v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Tweaks, ThemeMode, FontSize, FeedbackTweak } from '../types'
|
||||
|
||||
interface Props {
|
||||
tweaks: Tweaks
|
||||
update: <K extends keyof Tweaks>(key: K, value: Tweaks[K]) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function Seg({ tkey, options, tweaks, update }: {
|
||||
tkey: keyof Tweaks
|
||||
options: { value: string; label: string }[]
|
||||
tweaks: Tweaks
|
||||
update: Props['update']
|
||||
}) {
|
||||
return (
|
||||
<div className="tweak-control">
|
||||
{options.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
className={`tweak-seg${tweaks[tkey] === opt.value ? ' active' : ''}`}
|
||||
onClick={() => update(tkey, opt.value as never)}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function TweaksPanel({ tweaks, update, onClose }: Props) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className="tweaks-panel">
|
||||
<div className="tweaks-header">
|
||||
<span>{t('tweaks.title')}</span>
|
||||
<button className="btn btn-ghost btn-sm" onClick={onClose} style={{ padding: '2px 6px' }}>×</button>
|
||||
</div>
|
||||
<div className="tweaks-body">
|
||||
<div className="tweak-row">
|
||||
<div className="tweak-label">{t('tweaks.theme')}</div>
|
||||
<Seg tkey="theme" options={[{ value: 'light' as ThemeMode, label: t('tweaks.light') }, { value: 'dark' as ThemeMode, label: t('tweaks.dark') }]} tweaks={tweaks} update={update} />
|
||||
</div>
|
||||
<div className="tweak-row">
|
||||
<div className="tweak-label">{t('tweaks.textSize')}</div>
|
||||
<Seg tkey="fontSize" options={[{ value: 'small' as FontSize, label: t('tweaks.small') }, { value: 'medium' as FontSize, label: t('tweaks.medium') }, { value: 'large' as FontSize, label: t('tweaks.large') }]} tweaks={tweaks} update={update} />
|
||||
</div>
|
||||
<div className="tweak-row">
|
||||
<div className="tweak-label">{t('tweaks.feedback')}</div>
|
||||
<Seg tkey="feedbackMode" options={[
|
||||
{ value: 'auto' as FeedbackTweak, label: t('tweaks.auto') },
|
||||
{ value: 'immediate' as FeedbackTweak, label: t('tweaks.immediate') },
|
||||
{ value: 'end' as FeedbackTweak, label: t('tweaks.end') },
|
||||
]} tweaks={tweaks} update={update} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user