このコミットが含まれているのは:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Helmet } from 'react-helmet-async'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useBeforeUnload, useBlocker, useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
import FieldError from '@/components/common/FieldError'
|
||||
import FormField from '@/components/common/FormField'
|
||||
@@ -21,7 +21,6 @@ import { apiPut } from '@/lib/api'
|
||||
import {
|
||||
applyClientAppearance,
|
||||
applyThemeAppearanceState,
|
||||
DEFAULT_USER_SETTINGS,
|
||||
fetchUserSettings,
|
||||
fetchUserThemeSlots,
|
||||
getClientActiveDarkThemeSlotNo,
|
||||
@@ -34,7 +33,6 @@ import {
|
||||
resolveDisplayedTheme,
|
||||
setCachedUserThemeSlots,
|
||||
setClientThemeAppearance,
|
||||
updateUserSettings,
|
||||
upsertUserThemeSlot,
|
||||
USER_THEME_SLOT_SELECTIONS,
|
||||
} from '@/lib/settings'
|
||||
@@ -51,7 +49,6 @@ import type {
|
||||
ClientTheme,
|
||||
ThemeTokens,
|
||||
ThemeTagColours,
|
||||
UserSettings,
|
||||
UserThemeSlotMap,
|
||||
UserThemeSlotNo,
|
||||
UserThemeSlotSelection,
|
||||
@@ -224,6 +221,7 @@ const parseTab = (search: string): SettingsTab => {
|
||||
|
||||
const tabButtonId = (tab: SettingsTab): string => `settings-tab-${ tab }`
|
||||
const tabPanelId = (tab: SettingsTab): string => `settings-panel-${ tab }`
|
||||
const SETTINGS_PATH = '/users/settings'
|
||||
|
||||
const themePreviewStyle = (tagColours: ThemeTagColours): CSSProperties =>
|
||||
Object.fromEntries (
|
||||
@@ -679,7 +677,6 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
const [loading, setLoading] = useState (true)
|
||||
const [name, setName] = useState ('')
|
||||
const [settingsError, setSettingsError] = useState<string | null> (null)
|
||||
const [, setDraftSettings] = useState<UserSettings> (DEFAULT_USER_SETTINGS)
|
||||
const [savedThemeMode, setSavedThemeMode] =
|
||||
useState<ClientThemeMode> (getClientThemeMode ())
|
||||
const [draftThemeMode, setDraftThemeMode] =
|
||||
@@ -719,45 +716,37 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
() => resolveDisplayedTheme ({
|
||||
themeMode: draftThemeMode,
|
||||
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
|
||||
}, draftThemeSlots),
|
||||
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo }, draftThemeSlots),
|
||||
[draftActiveDarkThemeSlotNo,
|
||||
draftActiveLightThemeSlotNo,
|
||||
draftThemeMode,
|
||||
draftThemeSlots],
|
||||
)
|
||||
draftThemeSlots])
|
||||
const selectedTheme = useMemo<ClientTheme> (
|
||||
() => {
|
||||
const currentSelection =
|
||||
themeSelections.find (selection => selection.id === editingThemeSlot)
|
||||
?? themeSelections[0]
|
||||
const themeSlot =
|
||||
draftThemeSlots[currentSelection.id]
|
||||
?? getDefaultUserThemeSlot (currentSelection.baseTheme, currentSelection.slotNo)
|
||||
return {
|
||||
id: currentSelection.baseTheme,
|
||||
name: themeLabelBySelection (currentSelection.id),
|
||||
builtin: false,
|
||||
baseTheme: currentSelection.baseTheme,
|
||||
tokens: themeSlot.tokens,
|
||||
}
|
||||
const currentSelection =
|
||||
themeSelections.find (selection => selection.id === editingThemeSlot)
|
||||
?? themeSelections[0]
|
||||
const themeSlot =
|
||||
draftThemeSlots[currentSelection.id]
|
||||
?? getDefaultUserThemeSlot (currentSelection.baseTheme, currentSelection.slotNo)
|
||||
return {
|
||||
id: currentSelection.baseTheme,
|
||||
name: themeLabelBySelection (currentSelection.id),
|
||||
builtin: false,
|
||||
baseTheme: currentSelection.baseTheme,
|
||||
tokens: themeSlot.tokens }
|
||||
},
|
||||
[draftThemeSlots, editingThemeSlot],
|
||||
)
|
||||
[draftThemeSlots, editingThemeSlot])
|
||||
const hasUnsavedThemeChanges = useMemo (
|
||||
() =>
|
||||
serialiseThemeDraft (
|
||||
draftThemeMode,
|
||||
draftActiveLightThemeSlotNo,
|
||||
draftActiveDarkThemeSlotNo,
|
||||
draftThemeSlots,
|
||||
)
|
||||
!== serialiseThemeDraft (
|
||||
savedThemeMode,
|
||||
savedActiveLightThemeSlotNo,
|
||||
savedActiveDarkThemeSlotNo,
|
||||
savedThemeSlots,
|
||||
),
|
||||
serialiseThemeDraft (draftThemeMode,
|
||||
draftActiveLightThemeSlotNo,
|
||||
draftActiveDarkThemeSlotNo,
|
||||
draftThemeSlots)
|
||||
!== (serialiseThemeDraft (savedThemeMode,
|
||||
savedActiveLightThemeSlotNo,
|
||||
savedActiveDarkThemeSlotNo,
|
||||
savedThemeSlots)),
|
||||
[draftActiveDarkThemeSlotNo,
|
||||
draftActiveLightThemeSlotNo,
|
||||
draftThemeMode,
|
||||
@@ -767,8 +756,10 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
savedActiveLightThemeSlotNo,
|
||||
savedEditingThemeSlot,
|
||||
savedThemeMode,
|
||||
savedThemeSlots],
|
||||
)
|
||||
savedThemeSlots])
|
||||
const savedName = user?.name ?? ''
|
||||
const hasUnsavedAccountChanges = name !== savedName
|
||||
const hasPageUnsavedChanges = Object.values (dirtyStates).some (state => state.dirty)
|
||||
|
||||
const applyActiveTab = (tab: SettingsTab) => {
|
||||
const params = new URLSearchParams (location.search)
|
||||
@@ -783,16 +774,16 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
state: SettingsTabDirtyState,
|
||||
) => {
|
||||
setDirtyStates (current => {
|
||||
const next = { ...current }
|
||||
const next = { ...current }
|
||||
|
||||
if (!(state.dirty))
|
||||
{
|
||||
delete next[tab]
|
||||
return next
|
||||
}
|
||||
if (!(state.dirty))
|
||||
{
|
||||
delete next[tab]
|
||||
return next
|
||||
}
|
||||
|
||||
next[tab] = state
|
||||
return next
|
||||
next[tab] = state
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
@@ -806,8 +797,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
description,
|
||||
confirmText,
|
||||
cancelText,
|
||||
variant: 'danger',
|
||||
})
|
||||
variant: 'danger' })
|
||||
|
||||
const settingsTabErrors = useMemo<Record<SettingsTab, boolean>> (
|
||||
() => (
|
||||
@@ -815,36 +805,35 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
behavior: false,
|
||||
theme: Boolean (settingsFieldErrors.theme?.length),
|
||||
keyboard: conflictActionIds.size > 0 }),
|
||||
[conflictActionIds.size, settingsFieldErrors.theme, userFieldErrors.name],
|
||||
)
|
||||
[conflictActionIds.size, settingsFieldErrors.theme, userFieldErrors.name])
|
||||
|
||||
const applyDraftThemeState = (
|
||||
{ themeMode = draftThemeMode,
|
||||
activeLightThemeSlotNo = draftActiveLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo = draftActiveDarkThemeSlotNo,
|
||||
themeSlots = draftThemeSlots,
|
||||
nextEditingThemeSlot = editingThemeSlot }: {
|
||||
themeMode?: ClientThemeMode
|
||||
activeLightThemeSlotNo?: UserThemeSlotNo
|
||||
activeDarkThemeSlotNo?: UserThemeSlotNo
|
||||
themeSlots?: UserThemeSlotMap
|
||||
nextEditingThemeSlot?: UserThemeSlotSelection },
|
||||
nextEditingThemeSlot = editingThemeSlot,
|
||||
}: { themeMode?: ClientThemeMode
|
||||
activeLightThemeSlotNo?: UserThemeSlotNo
|
||||
activeDarkThemeSlotNo?: UserThemeSlotNo
|
||||
themeSlots?: UserThemeSlotMap
|
||||
nextEditingThemeSlot?: UserThemeSlotSelection },
|
||||
) => {
|
||||
setDraftThemeMode (themeMode)
|
||||
setDraftActiveLightThemeSlotNo (activeLightThemeSlotNo)
|
||||
setDraftActiveDarkThemeSlotNo (activeDarkThemeSlotNo)
|
||||
setDraftThemeSlots (themeSlots)
|
||||
setEditingThemeSlot (nextEditingThemeSlot)
|
||||
setDraftSettings (current => ({
|
||||
...current,
|
||||
theme: themeMode,
|
||||
}))
|
||||
setCachedUserThemeSlots (themeSlots)
|
||||
applyThemeAppearanceState ({
|
||||
themeMode,
|
||||
activeLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo,
|
||||
}, themeSlots)
|
||||
activeDarkThemeSlotNo }, themeSlots)
|
||||
}
|
||||
|
||||
const discardAccountChanges = () => {
|
||||
setName (savedName)
|
||||
clearUserValidationErrors ()
|
||||
}
|
||||
|
||||
const currentThemeDraftState = (): ThemeDraftState => ({
|
||||
@@ -852,16 +841,14 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
|
||||
themeSlots: draftThemeSlots,
|
||||
editingThemeSlot,
|
||||
})
|
||||
editingThemeSlot })
|
||||
|
||||
const savedThemeDraftState = (): ThemeDraftState => ({
|
||||
themeMode: savedThemeMode,
|
||||
activeLightThemeSlotNo: savedActiveLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo: savedActiveDarkThemeSlotNo,
|
||||
themeSlots: savedThemeSlots,
|
||||
editingThemeSlot: savedEditingThemeSlot,
|
||||
})
|
||||
editingThemeSlot: savedEditingThemeSlot })
|
||||
|
||||
const discardThemeChanges = () => {
|
||||
setDraftThemeMode (savedThemeMode)
|
||||
@@ -873,11 +860,19 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
setClientThemeAppearance ({
|
||||
themeMode: savedThemeMode,
|
||||
activeLightThemeSlotNo: savedActiveLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo: savedActiveDarkThemeSlotNo,
|
||||
})
|
||||
activeDarkThemeSlotNo: savedActiveDarkThemeSlotNo })
|
||||
applyClientAppearance (savedThemeSlots)
|
||||
}
|
||||
|
||||
const discardCurrentTabChanges = async (): Promise<void> => {
|
||||
const currentDirtyState = dirtyStates[activeTab]
|
||||
|
||||
if (currentDirtyState?.dirty)
|
||||
await currentDirtyState.discard ()
|
||||
|
||||
setDirtyStates ({ })
|
||||
}
|
||||
|
||||
const requestActiveTab = async (
|
||||
nextTab: SettingsTab,
|
||||
): Promise<void> => {
|
||||
@@ -894,12 +889,11 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
const confirmed = await confirmDiscardChanges (
|
||||
'このタブを離れると、保存していない変更は失われます。',
|
||||
'このタブに残る',
|
||||
'変更を破棄して移動',
|
||||
)
|
||||
'変更を破棄して移動')
|
||||
if (!(confirmed))
|
||||
return
|
||||
|
||||
await currentDirtyState.discard ()
|
||||
await discardCurrentTabChanges ()
|
||||
applyActiveTab (nextTab)
|
||||
}
|
||||
|
||||
@@ -919,8 +913,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
const confirmed = await confirmDiscardChanges (
|
||||
description,
|
||||
cancelText,
|
||||
confirmText,
|
||||
)
|
||||
confirmText)
|
||||
if (!(confirmed))
|
||||
return
|
||||
}
|
||||
@@ -932,10 +925,52 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
activeLightThemeSlotNo: nextState.activeLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo: nextState.activeDarkThemeSlotNo,
|
||||
themeSlots: nextState.themeSlots,
|
||||
nextEditingThemeSlot: nextState.editingThemeSlot,
|
||||
})
|
||||
nextEditingThemeSlot: nextState.editingThemeSlot })
|
||||
}
|
||||
|
||||
const routeBlocker = useBlocker (({ currentLocation, nextLocation }) =>
|
||||
hasPageUnsavedChanges
|
||||
&& currentLocation.pathname === SETTINGS_PATH
|
||||
&& nextLocation.pathname !== currentLocation.pathname)
|
||||
|
||||
useEffect (() => {
|
||||
if (routeBlocker.state !== 'blocked')
|
||||
return
|
||||
|
||||
let active = true
|
||||
|
||||
void (async () => {
|
||||
const confirmed = await confirmDiscardChanges (
|
||||
'このまま移動すると、保存していない変更は失われます。',
|
||||
'このページに残る',
|
||||
'変更を破棄して移動',
|
||||
)
|
||||
if (!(active))
|
||||
return
|
||||
|
||||
if (!(confirmed))
|
||||
{
|
||||
routeBlocker.reset ()
|
||||
return
|
||||
}
|
||||
|
||||
await discardCurrentTabChanges ()
|
||||
routeBlocker.proceed ()
|
||||
}) ()
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [routeBlocker])
|
||||
|
||||
useBeforeUnload (event => {
|
||||
if (!(hasPageUnsavedChanges))
|
||||
return
|
||||
|
||||
event.preventDefault ()
|
||||
event.returnValue = ''
|
||||
}, { capture: true })
|
||||
|
||||
const updateDraftThemeSlot = (
|
||||
selection: UserThemeSlotSelection,
|
||||
updater: (tokens: ThemeTokens) => ThemeTokens,
|
||||
@@ -945,16 +980,9 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
?? ensureCompleteThemeSlots ({ })[selection]
|
||||
const nextThemeSlots = {
|
||||
...draftThemeSlots,
|
||||
[selection]: {
|
||||
...currentThemeSlot,
|
||||
tokens: updater (currentThemeSlot.tokens),
|
||||
},
|
||||
}
|
||||
[selection]: { ...currentThemeSlot, tokens: updater (currentThemeSlot.tokens) } }
|
||||
|
||||
applyDraftThemeState ({
|
||||
themeSlots: nextThemeSlots,
|
||||
nextEditingThemeSlot: selection,
|
||||
})
|
||||
applyDraftThemeState ({ themeSlots: nextThemeSlots, nextEditingThemeSlot: selection })
|
||||
}
|
||||
|
||||
const handleTabKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||
@@ -965,23 +993,23 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
case 'ArrowDown':
|
||||
case 'ArrowRight':
|
||||
event.preventDefault ()
|
||||
void requestActiveTab (tabs[(currentIndex + 1) % tabs.length].id)
|
||||
requestActiveTab (tabs[(currentIndex + 1) % tabs.length].id)
|
||||
break
|
||||
|
||||
case 'ArrowUp':
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault ()
|
||||
void requestActiveTab (tabs[(currentIndex - 1 + tabs.length) % tabs.length].id)
|
||||
requestActiveTab (tabs[(currentIndex - 1 + tabs.length) % tabs.length].id)
|
||||
break
|
||||
|
||||
case 'Home':
|
||||
event.preventDefault ()
|
||||
void requestActiveTab (tabs[0].id)
|
||||
requestActiveTab (tabs[0].id)
|
||||
break
|
||||
|
||||
case 'End':
|
||||
event.preventDefault ()
|
||||
void requestActiveTab (tabs[tabs.length - 1].id)
|
||||
requestActiveTab (tabs[tabs.length - 1].id)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -999,9 +1027,9 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
{
|
||||
const data = await apiPut<User> (
|
||||
`/users/${ user.id }`, formData,
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } },
|
||||
)
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
setUser (currentUser => ({ ...currentUser, ...data }))
|
||||
setName (data.name ?? '')
|
||||
toast ({ title: '表示名を更新しました.' })
|
||||
}
|
||||
catch (submitError)
|
||||
@@ -1026,11 +1054,9 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
selection === editingThemeSlot
|
||||
&& savedThemeSlot?.createdAt == null
|
||||
|
||||
if (
|
||||
JSON.stringify (draftThemeSlot?.tokens)
|
||||
=== JSON.stringify (savedThemeSlot?.tokens)
|
||||
&& !(requiresPersist)
|
||||
)
|
||||
if ((JSON.stringify (draftThemeSlot?.tokens)
|
||||
=== JSON.stringify (savedThemeSlot?.tokens))
|
||||
&& !(requiresPersist))
|
||||
continue
|
||||
|
||||
const persisted = await upsertUserThemeSlot (
|
||||
@@ -1038,19 +1064,12 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
draftThemeSlot?.slotNo ?? Number (selection.split (':')[2]) as UserThemeSlotNo,
|
||||
draftThemeSlot?.tokens ?? getDefaultUserThemeSlot (
|
||||
selection.split (':')[1] as BuiltinThemeId,
|
||||
Number (selection.split (':')[2]) as UserThemeSlotNo,
|
||||
).tokens,
|
||||
)
|
||||
nextSavedThemeSlots = {
|
||||
...nextSavedThemeSlots,
|
||||
[selection]: persisted,
|
||||
}
|
||||
Number (selection.split (':')[2]) as UserThemeSlotNo).tokens)
|
||||
nextSavedThemeSlots = { ...nextSavedThemeSlots, [selection]: persisted }
|
||||
}
|
||||
|
||||
const data = await updateUserSettings ({ theme: draftThemeMode })
|
||||
const completeThemeSlots = ensureCompleteThemeSlots (nextSavedThemeSlots)
|
||||
|
||||
setDraftSettings ({ ...data, theme: draftThemeMode })
|
||||
setSavedThemeMode (draftThemeMode)
|
||||
setSavedActiveLightThemeSlotNo (draftActiveLightThemeSlotNo)
|
||||
setSavedActiveDarkThemeSlotNo (draftActiveDarkThemeSlotNo)
|
||||
@@ -1058,11 +1077,11 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
setDraftThemeSlots (completeThemeSlots)
|
||||
setSavedEditingThemeSlot (editingThemeSlot)
|
||||
setEditingThemeSlot (editingThemeSlot)
|
||||
// Display mode and active slot numbers are browser-local state.
|
||||
setClientThemeAppearance ({
|
||||
themeMode: draftThemeMode,
|
||||
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
|
||||
})
|
||||
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo })
|
||||
setCachedUserThemeSlots (completeThemeSlots)
|
||||
applyClientAppearance (completeThemeSlots)
|
||||
toast ({ title: 'テーマ設定を保存しました.' })
|
||||
@@ -1096,10 +1115,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
|
||||
try
|
||||
{
|
||||
const [settings, themeSlots] = await Promise.all ([
|
||||
fetchUserSettings (),
|
||||
fetchUserThemeSlots (),
|
||||
])
|
||||
const [settings, themeSlots] = await Promise.all ([fetchUserSettings (),
|
||||
fetchUserThemeSlots ()])
|
||||
if (cancelled)
|
||||
return
|
||||
|
||||
@@ -1114,10 +1131,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
const initialEditingThemeSlot = resolveDisplayedTheme ({
|
||||
themeMode,
|
||||
activeLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo,
|
||||
}, completeThemeSlots).selection
|
||||
activeDarkThemeSlotNo }, completeThemeSlots).selection
|
||||
|
||||
setDraftSettings ({ ...settings, theme: themeMode })
|
||||
setSavedThemeMode (themeMode)
|
||||
setDraftThemeMode (themeMode)
|
||||
setSavedActiveLightThemeSlotNo (activeLightThemeSlotNo)
|
||||
@@ -1129,11 +1144,9 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
setSavedEditingThemeSlot (initialEditingThemeSlot)
|
||||
setEditingThemeSlot (initialEditingThemeSlot)
|
||||
setCachedUserThemeSlots (completeThemeSlots)
|
||||
applyThemeAppearanceState ({
|
||||
themeMode,
|
||||
activeLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo,
|
||||
}, completeThemeSlots)
|
||||
applyThemeAppearanceState ({ themeMode,
|
||||
activeLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo }, completeThemeSlots)
|
||||
setSettingsError (null)
|
||||
}
|
||||
catch
|
||||
@@ -1160,6 +1173,12 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
applyActiveTab ('account')
|
||||
}, [location.pathname, location.search, userFieldErrors.name])
|
||||
|
||||
useEffect (() => {
|
||||
updateTabDirtyState ('account', {
|
||||
dirty: hasUnsavedAccountChanges,
|
||||
discard: discardAccountChanges })
|
||||
}, [hasUnsavedAccountChanges, savedName])
|
||||
|
||||
useEffect (() => {
|
||||
if (settingsFieldErrors.theme?.length)
|
||||
applyActiveTab ('theme')
|
||||
@@ -1169,8 +1188,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
applyThemeAppearanceState ({
|
||||
themeMode: draftThemeMode,
|
||||
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
||||
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
|
||||
}, draftThemeSlots)
|
||||
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo }, draftThemeSlots)
|
||||
|
||||
return () => {
|
||||
setCachedUserThemeSlots (savedThemeSlots)
|
||||
@@ -1184,39 +1202,35 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
|
||||
const accountSectionProps: AccountSectionProps | null =
|
||||
user
|
||||
? {
|
||||
user,
|
||||
? { user,
|
||||
name,
|
||||
onNameChange: setName,
|
||||
onInheritOpen: () => setInheritVsbl (true),
|
||||
onUserCodeOpen: () => setUserCodeVsbl (true),
|
||||
onUserSubmit: handleUserSubmit,
|
||||
userBaseErrors,
|
||||
userFieldErrors,
|
||||
}
|
||||
userFieldErrors }
|
||||
: null
|
||||
|
||||
useEffect (() => {
|
||||
updateTabDirtyState ('theme', {
|
||||
dirty: hasUnsavedThemeChanges,
|
||||
discard: handleThemeDiscard,
|
||||
})
|
||||
discard: handleThemeDiscard })
|
||||
}, [hasUnsavedThemeChanges, savedThemeSlots])
|
||||
|
||||
useKeyboardShortcuts ({
|
||||
'settings.account': () => {
|
||||
void requestActiveTab ('account')
|
||||
['settings.account']: () => {
|
||||
requestActiveTab ('account')
|
||||
},
|
||||
'settings.behavior': () => {
|
||||
void requestActiveTab ('behavior')
|
||||
['settings.behavior']: () => {
|
||||
requestActiveTab ('behavior')
|
||||
},
|
||||
'settings.theme': () => {
|
||||
void requestActiveTab ('theme')
|
||||
['settings.theme']: () => {
|
||||
requestActiveTab ('theme')
|
||||
},
|
||||
'settings.keyboard': () => {
|
||||
void requestActiveTab ('keyboard')
|
||||
},
|
||||
})
|
||||
['settings.keyboard']: () => {
|
||||
requestActiveTab ('keyboard')
|
||||
} })
|
||||
|
||||
const themeSectionProps: ThemeSectionProps = {
|
||||
draftThemeMode,
|
||||
@@ -1230,62 +1244,43 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
||||
settingsBaseErrors,
|
||||
settingsFieldErrors,
|
||||
onSelectThemeMode: themeMode => {
|
||||
void requestThemeStateChange (baseState => ({
|
||||
...baseState,
|
||||
themeMode,
|
||||
}))
|
||||
requestThemeStateChange (baseState => ({ ...baseState, themeMode }))
|
||||
},
|
||||
onSelectLightThemeSlotNo: slotNo => {
|
||||
void requestThemeStateChange (baseState => ({
|
||||
...baseState,
|
||||
activeLightThemeSlotNo: slotNo,
|
||||
editingThemeSlot: getUserThemeSlotSelection ('light', slotNo),
|
||||
}))
|
||||
requestThemeStateChange (baseState => ({
|
||||
...baseState,
|
||||
activeLightThemeSlotNo: slotNo,
|
||||
editingThemeSlot: getUserThemeSlotSelection ('light', slotNo) }))
|
||||
},
|
||||
onSelectDarkThemeSlotNo: slotNo => {
|
||||
void requestThemeStateChange (baseState => ({
|
||||
...baseState,
|
||||
activeDarkThemeSlotNo: slotNo,
|
||||
editingThemeSlot: getUserThemeSlotSelection ('dark', slotNo),
|
||||
}))
|
||||
requestThemeStateChange (baseState => ({
|
||||
...baseState,
|
||||
activeDarkThemeSlotNo: slotNo,
|
||||
editingThemeSlot: getUserThemeSlotSelection ('dark', slotNo) }))
|
||||
},
|
||||
onSelectEditingThemeSlot: selection => {
|
||||
void requestThemeStateChange (baseState => ({
|
||||
...baseState,
|
||||
editingThemeSlot: selection,
|
||||
}))
|
||||
requestThemeStateChange (baseState => ({ ...baseState, editingThemeSlot: selection }))
|
||||
},
|
||||
onThemeTokenChange: (key, value) => {
|
||||
updateDraftThemeSlot (editingThemeSlot, tokens => ({
|
||||
...tokens,
|
||||
[key]: value,
|
||||
}))
|
||||
updateDraftThemeSlot (editingThemeSlot, tokens => ({ ...tokens, [key]: value }))
|
||||
},
|
||||
onTagColourChange: (category, colour) => {
|
||||
updateDraftThemeSlot (editingThemeSlot, tokens => ({
|
||||
...tokens,
|
||||
tagColours: {
|
||||
...tokens.tagColours,
|
||||
[category]: colour,
|
||||
},
|
||||
}))
|
||||
updateDraftThemeSlot (editingThemeSlot, tokens => ({
|
||||
...tokens, tagColours: { ...tokens.tagColours, [category]: colour } }))
|
||||
},
|
||||
onResetThemeSlot: selection => {
|
||||
const baseTheme =
|
||||
draftThemeSlots[selection]?.baseTheme
|
||||
?? selection.split (':')[1] as BuiltinThemeId
|
||||
updateDraftThemeSlot (
|
||||
selection,
|
||||
() => getDefaultUserThemeSlot (
|
||||
baseTheme,
|
||||
draftThemeSlots[selection]?.slotNo
|
||||
?? Number (selection.split (':')[2]) as UserThemeSlotNo,
|
||||
).tokens,
|
||||
)
|
||||
const baseTheme =
|
||||
draftThemeSlots[selection]?.baseTheme
|
||||
?? selection.split (':')[1] as BuiltinThemeId
|
||||
updateDraftThemeSlot (
|
||||
selection,
|
||||
() => getDefaultUserThemeSlot (
|
||||
baseTheme,
|
||||
(draftThemeSlots[selection]?.slotNo
|
||||
?? Number (selection.split (':')[2]) as UserThemeSlotNo)).tokens)
|
||||
},
|
||||
onThemeSubmit: handleThemeSubmit,
|
||||
onThemeDiscard: handleThemeDiscard,
|
||||
}
|
||||
onThemeDiscard: handleThemeDiscard }
|
||||
|
||||
return (
|
||||
<MainArea>
|
||||
|
||||
新しい課題から参照
ユーザをブロックする