このコミットが含まれているのは:
@@ -362,6 +362,20 @@ const value =
|
|||||||
- Do not use a leading semicolon for expression statements such as
|
- Do not use a leading semicolon for expression statements such as
|
||||||
`;([...]).forEach(...)`; rewrite the expression to avoid ASI hazards
|
`;([...]).forEach(...)`; rewrite the expression to avoid ASI hazards
|
||||||
explicitly, for example with `void`.
|
explicitly, for example with `void`.
|
||||||
|
- Do not insert `void` mechanically before ordinary event-handler or callback
|
||||||
|
calls merely because the callee returns a `Promise`. If the surrounding code
|
||||||
|
already uses a plain expression statement such as
|
||||||
|
`onClick={() => requestActiveTab ('theme')}`, preserve that style unless a
|
||||||
|
local rule or type requirement truly forces `void`. Adding unnecessary
|
||||||
|
`void` is a medium-severity style violation.
|
||||||
|
- When fixing formatting, never change operator binding, evaluation order,
|
||||||
|
tuple/comma behaviour, or any other expression structure. A formatting pass
|
||||||
|
that changes how `!==`, `??`, `?:`, `&&`, `||`, or `,` binds is a capital
|
||||||
|
offence.
|
||||||
|
- In TypeScript and TSX, do not mechanically verticalise short call tails,
|
||||||
|
short object literals, or short callback bodies that already fit the local
|
||||||
|
line-length and delimiter rules. Expanding compact local code without a
|
||||||
|
readability need is a minor style violation.
|
||||||
- Use correct British English spelling for new identifiers, filenames,
|
- Use correct British English spelling for new identifiers, filenames,
|
||||||
component names, helper names, comments, and developer-facing prose unless
|
component names, helper names, comments, and developer-facing prose unless
|
||||||
editing an already established American-English API that must keep its
|
editing an already established American-English API that must keep its
|
||||||
@@ -380,6 +394,82 @@ const value =
|
|||||||
- For this repository, prefer names such as `BehaviourSettingsSection.tsx`, not
|
- For this repository, prefer names such as `BehaviourSettingsSection.tsx`, not
|
||||||
`BehaviorSettingsSection.tsx`.
|
`BehaviorSettingsSection.tsx`.
|
||||||
|
|
||||||
|
### Additional style offences and penalties
|
||||||
|
|
||||||
|
Use this section for recurrent failure modes that are easy to miss when
|
||||||
|
applying the delimiter rules mechanically.
|
||||||
|
|
||||||
|
#### Unnecessary `void` before ordinary callbacks
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
onClick={() => void requestActiveTab ('theme')}
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
onClick={() => requestActiveTab ('theme')}
|
||||||
|
```
|
||||||
|
|
||||||
|
Penalty: medium-severity offence. The code still works, but it pollutes the
|
||||||
|
local style and usually signals that the edit was made mechanically rather than
|
||||||
|
by reading the surrounding code.
|
||||||
|
|
||||||
|
#### Formatting that changes expression structure
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const hasUnsavedChanges =
|
||||||
|
serialiseThemeDraft (draftThemeSlots)
|
||||||
|
!== (serialiseThemeDraft (savedThemeSlots),
|
||||||
|
draftActiveThemeMode !== savedThemeMode)
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const hasUnsavedChanges =
|
||||||
|
serialiseThemeDraft (draftThemeSlots)
|
||||||
|
!== serialiseThemeDraft (savedThemeSlots)
|
||||||
|
|| draftThemeMode !== savedThemeMode
|
||||||
|
```
|
||||||
|
|
||||||
|
Penalty: capital offence. Formatting must never corrupt the meaning of an
|
||||||
|
expression.
|
||||||
|
|
||||||
|
#### Mechanical vertical expansion of compact local code
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const nextState = {
|
||||||
|
open,
|
||||||
|
activeTab }
|
||||||
|
|
||||||
|
setDirtyStates (
|
||||||
|
current => ({
|
||||||
|
current,
|
||||||
|
key: value }),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const nextState = { open, activeTab }
|
||||||
|
|
||||||
|
setDirtyStates (
|
||||||
|
current => ({
|
||||||
|
current,
|
||||||
|
key: value }))
|
||||||
|
```
|
||||||
|
|
||||||
|
Penalty: minor offence when semantics stay intact. It is not necessarily
|
||||||
|
broken, but it ignores the repository's compact associative style.
|
||||||
|
|
||||||
### Frontend delimiter decision table
|
### Frontend delimiter decision table
|
||||||
|
|
||||||
Use this table before accepting any edited TypeScript or TSX hunk. The table is
|
Use this table before accepting any edited TypeScript or TSX hunk. The table is
|
||||||
@@ -412,6 +502,7 @@ import { Button, Card } from '@/components/ui'
|
|||||||
Rule: named-binding `}` is associative-array-style syntax. It must not be alone
|
Rule: named-binding `}` is associative-array-style syntax. It must not be alone
|
||||||
at the beginning of a line. Prefer keeping `{` with the first binding and `}`
|
at the beginning of a line. Prefer keeping `{` with the first binding and `}`
|
||||||
with the final binding when this fits the line limit.
|
with the final binding when this fits the line limit.
|
||||||
|
Penalty: capital offence. This is one of the core associative-brace rules.
|
||||||
|
|
||||||
#### Type literals
|
#### Type literals
|
||||||
|
|
||||||
@@ -434,6 +525,8 @@ type Props = {
|
|||||||
|
|
||||||
Rule: type-literal `}` is not a block close. It must stay on the same line as
|
Rule: type-literal `}` is not a block close. It must stay on the same line as
|
||||||
the final property unless that would break the hard line limit.
|
the final property unless that would break the hard line limit.
|
||||||
|
Penalty: capital offence. Mistaking a type-literal `}` for a block `}` is one
|
||||||
|
of the recurring death-class errors.
|
||||||
|
|
||||||
#### Object literals
|
#### Object literals
|
||||||
|
|
||||||
@@ -474,6 +567,8 @@ const value = useMemo (() => ({
|
|||||||
Rule: object-literal `}` is associative-array-style syntax. It must not be on a
|
Rule: object-literal `}` is associative-array-style syntax. It must not be on a
|
||||||
line by itself. Keep it with the final property, and keep call `)` off the
|
line by itself. Keep it with the final property, and keep call `)` off the
|
||||||
beginning of a line.
|
beginning of a line.
|
||||||
|
Penalty: capital offence. Wrong object-literal `}` placement and wrong call
|
||||||
|
`)` placement are both death-class violations here.
|
||||||
|
|
||||||
#### Destructuring parameters
|
#### Destructuring parameters
|
||||||
|
|
||||||
@@ -504,6 +599,8 @@ that spans multiple lines, do not use the Prettier-style `= ({ ... }) =>`
|
|||||||
shape. Put the function parameter list on its own lines. The destructuring `}`
|
shape. Put the function parameter list on its own lines. The destructuring `}`
|
||||||
stays with the final binding. The parameter-list `)` is then allowed and
|
stays with the final binding. The parameter-list `)` is then allowed and
|
||||||
required at the beginning of its own line.
|
required at the beginning of its own line.
|
||||||
|
Penalty: capital offence. This pattern is a primary source of repeated TSX
|
||||||
|
style regressions.
|
||||||
|
|
||||||
#### Inline typed destructuring parameter
|
#### Inline typed destructuring parameter
|
||||||
|
|
||||||
@@ -531,6 +628,8 @@ const RouteTransitionWrapper = ({ user, setUser }: {
|
|||||||
Rule: this is not a separately split parameter-list block. The line break is
|
Rule: this is not a separately split parameter-list block. The line break is
|
||||||
inside the inline type. Keep the type-literal `}` and parameter-list `)` on the
|
inside the inline type. Keep the type-literal `}` and parameter-list `)` on the
|
||||||
same line as the final type property.
|
same line as the final type property.
|
||||||
|
Penalty: capital offence. Misclassifying this case means the delimiter review
|
||||||
|
has failed at the syntax-role level.
|
||||||
|
|
||||||
#### Multi-line normal parameter list
|
#### Multi-line normal parameter list
|
||||||
|
|
||||||
@@ -558,6 +657,8 @@ const updateDraft = <Key extends keyof Settings,> (
|
|||||||
Rule: when the parameter list itself is split across multiple parameter lines,
|
Rule: when the parameter list itself is split across multiple parameter lines,
|
||||||
the closing parameter `)` goes at the beginning of its own line before `=>` or
|
the closing parameter `)` goes at the beginning of its own line before `=>` or
|
||||||
the return type.
|
the return type.
|
||||||
|
Penalty: capital offence. This is the main allowed exception for a leading `)`,
|
||||||
|
so getting it wrong destroys the whole rule set.
|
||||||
|
|
||||||
#### Function and callback blocks
|
#### Function and callback blocks
|
||||||
|
|
||||||
@@ -586,6 +687,8 @@ const handleSave = () => {
|
|||||||
Rule: block `}` closes executable code, not associative data. Multi-line
|
Rule: block `}` closes executable code, not associative data. Multi-line
|
||||||
function, lambda, callback, `if`, `for`, `switch`, and similar block braces
|
function, lambda, callback, `if`, `for`, `switch`, and similar block braces
|
||||||
belong on their own line.
|
belong on their own line.
|
||||||
|
Penalty: capital offence. Confusing block `}` with associative `}` is a
|
||||||
|
death-class delimiter error.
|
||||||
|
|
||||||
#### Function and method calls
|
#### Function and method calls
|
||||||
|
|
||||||
@@ -609,6 +712,8 @@ const value = compute (
|
|||||||
Rule: call-expression `)` must not be at the beginning of a line. The exception
|
Rule: call-expression `)` must not be at the beginning of a line. The exception
|
||||||
for leading `)` applies only to function declaration parameter lists, never to
|
for leading `)` applies only to function declaration parameter lists, never to
|
||||||
calls.
|
calls.
|
||||||
|
Penalty: capital offence. A leading call `)` is one of the clearest death-class
|
||||||
|
violations in this codebase.
|
||||||
|
|
||||||
#### JSX closing markers
|
#### JSX closing markers
|
||||||
|
|
||||||
@@ -653,6 +758,8 @@ Good:
|
|||||||
|
|
||||||
Rule: keep `>` or `/>` with the final prop, and keep JSX closing parentheses in
|
Rule: keep `>` or `/>` with the final prop, and keep JSX closing parentheses in
|
||||||
the local compact form such as `</div>)`.
|
the local compact form such as `</div>)`.
|
||||||
|
Penalty: medium-severity offence. It is usually not semantically broken, but it
|
||||||
|
is a conspicuous TSX-style failure.
|
||||||
|
|
||||||
#### Arrays and tuples
|
#### Arrays and tuples
|
||||||
|
|
||||||
@@ -681,6 +788,8 @@ const items = [first, second]
|
|||||||
|
|
||||||
Rule: array and tuple `]` must not be at the beginning of a line. Keep it with
|
Rule: array and tuple `]` must not be at the beginning of a line. Keep it with
|
||||||
the final element unless that would break the hard line limit.
|
the final element unless that would break the hard line limit.
|
||||||
|
Penalty: capital offence. A leading `]` is forbidden by the same class of hard
|
||||||
|
delimiter rule as leading associative `}` and leading call `)`.
|
||||||
|
|
||||||
#### Single-line braces
|
#### Single-line braces
|
||||||
|
|
||||||
@@ -705,6 +814,8 @@ Good:
|
|||||||
|
|
||||||
Rule: JavaScript object braces on one line get one inner space. JSX expression
|
Rule: JavaScript object braces on one line get one inner space. JSX expression
|
||||||
braces do not get inner spaces.
|
braces do not get inner spaces.
|
||||||
|
Penalty: minor offence. This is not normally semantic breakage, but it still
|
||||||
|
counts as a style miss.
|
||||||
|
|
||||||
#### Final TypeScript/TSX self-review checklist
|
#### Final TypeScript/TSX self-review checklist
|
||||||
|
|
||||||
|
|||||||
生成ファイル
-6
@@ -371,12 +371,6 @@ ActiveRecord::Schema[8.0].define(version: 2026_07_05_000000) do
|
|||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.string "theme", default: "system", null: false
|
t.string "theme", default: "system", null: false
|
||||||
t.string "display_density", default: "comfortable", null: false
|
|
||||||
t.string "font_size", default: "normal", null: false
|
|
||||||
t.integer "post_list_limit", default: 50, null: false
|
|
||||||
t.string "post_list_order", default: "created_at_desc", null: false
|
|
||||||
t.string "viewed_post_display", default: "show", null: false
|
|
||||||
t.boolean "tag_autocomplete_nico", default: true, null: false
|
|
||||||
t.string "auto_fetch_title", default: "manual", null: false
|
t.string "auto_fetch_title", default: "manual", null: false
|
||||||
t.string "auto_fetch_thumbnail", default: "manual", null: false
|
t.string "auto_fetch_thumbnail", default: "manual", null: false
|
||||||
t.string "wiki_editor_mode", default: "split", null: false
|
t.string "wiki_editor_mode", default: "split", null: false
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
|
||||||
|
|
||||||
import {
|
|
||||||
applyClientAppearance,
|
|
||||||
DEFAULT_USER_SETTINGS,
|
|
||||||
fetchUserSettings,
|
|
||||||
} from '@/lib/settings'
|
|
||||||
|
|
||||||
import type { Dispatch, FC, ReactNode, SetStateAction } from 'react'
|
|
||||||
|
|
||||||
import type { User } from '@/types'
|
|
||||||
import type { UserSettings } from '@/lib/settings'
|
|
||||||
|
|
||||||
type ContextValue = {
|
|
||||||
loaded: boolean
|
|
||||||
error: string | null
|
|
||||||
settings: UserSettings
|
|
||||||
setSettings: Dispatch<SetStateAction<UserSettings>> }
|
|
||||||
|
|
||||||
const UserSettingsContext = createContext<ContextValue | null> (null)
|
|
||||||
|
|
||||||
|
|
||||||
export const UserSettingsProvider: FC<{
|
|
||||||
children: ReactNode
|
|
||||||
user: User | null }> = ({ children, user }) => {
|
|
||||||
const [error, setError] = useState<string | null> (null)
|
|
||||||
const [loaded, setLoaded] = useState (false)
|
|
||||||
const [settings, setSettings] = useState<UserSettings> (DEFAULT_USER_SETTINGS)
|
|
||||||
|
|
||||||
useEffect (() => {
|
|
||||||
let cancelled = false
|
|
||||||
|
|
||||||
if (!(user))
|
|
||||||
{
|
|
||||||
setError (null)
|
|
||||||
setSettings (DEFAULT_USER_SETTINGS)
|
|
||||||
setLoaded (true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoaded (false)
|
|
||||||
setError (null)
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const next = await fetchUserSettings ()
|
|
||||||
if (!(cancelled))
|
|
||||||
{
|
|
||||||
setError (null)
|
|
||||||
setSettings (next)
|
|
||||||
setLoaded (true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
if (!(cancelled))
|
|
||||||
{
|
|
||||||
setError ('設定を読み込めませんでした.既定値で表示しています.')
|
|
||||||
setSettings (DEFAULT_USER_SETTINGS)
|
|
||||||
setLoaded (true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) ()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [user])
|
|
||||||
|
|
||||||
useEffect (() => {
|
|
||||||
void settings.theme
|
|
||||||
applyClientAppearance ()
|
|
||||||
}, [settings.theme])
|
|
||||||
|
|
||||||
const value = useMemo<ContextValue> (() => ({
|
|
||||||
error,
|
|
||||||
loaded,
|
|
||||||
settings,
|
|
||||||
setSettings,
|
|
||||||
}), [error, loaded, settings])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UserSettingsContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</UserSettingsContext.Provider>)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const useUserSettings = (): ContextValue => {
|
|
||||||
const value = useContext (UserSettingsContext)
|
|
||||||
|
|
||||||
if (value == null)
|
|
||||||
throw new Error ('UserSettingsProvider is missing')
|
|
||||||
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
+1
-24
@@ -105,7 +105,7 @@
|
|||||||
}
|
}
|
||||||
a:hover
|
a:hover
|
||||||
{
|
{
|
||||||
opacity: .85;
|
color: color-mix(in oklab, hsl(var(--theme-link)), white 18%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,29 +125,6 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root[data-font-size='small']
|
|
||||||
{
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-font-size='normal']
|
|
||||||
{
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-font-size='large']
|
|
||||||
{
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-display-density='compact'] input,
|
|
||||||
:root[data-display-density='compact'] textarea,
|
|
||||||
:root[data-display-density='compact'] select,
|
|
||||||
:root[data-display-density='compact'] button
|
|
||||||
{
|
|
||||||
line-height: 1.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-animation='reduced'] *,
|
:root[data-animation='reduced'] *,
|
||||||
:root[data-animation='off'] *
|
:root[data-animation='off'] *
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -721,6 +721,10 @@ const mixHexColour = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Legacy appearance-model compatibility.
|
||||||
|
// The current settings UI uses `themeMode` plus the light/dark slot numbers.
|
||||||
|
// The helpers below remain only to read older localStorage shapes.
|
||||||
|
|
||||||
const legacyThemeSlotSelection = (): ThemeSlotSelection => {
|
const legacyThemeSlotSelection = (): ThemeSlotSelection => {
|
||||||
const appearance = appearanceFromSettings ()
|
const appearance = appearanceFromSettings ()
|
||||||
const customThemes = getClientCustomThemes ()
|
const customThemes = getClientCustomThemes ()
|
||||||
@@ -774,6 +778,7 @@ export const getClientActiveThemeSlot = (): ThemeSlotSelection =>
|
|||||||
?? legacyThemeSlotSelection ()
|
?? legacyThemeSlotSelection ()
|
||||||
|
|
||||||
|
|
||||||
|
// Legacy write path. New code should use `setClientThemeAppearance`.
|
||||||
export const setClientActiveThemeSlot = (
|
export const setClientActiveThemeSlot = (
|
||||||
activeThemeSlot: ThemeSlotSelection,
|
activeThemeSlot: ThemeSlotSelection,
|
||||||
): void => {
|
): void => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
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 FieldError from '@/components/common/FieldError'
|
||||||
import FormField from '@/components/common/FormField'
|
import FormField from '@/components/common/FormField'
|
||||||
@@ -21,7 +21,6 @@ import { apiPut } from '@/lib/api'
|
|||||||
import {
|
import {
|
||||||
applyClientAppearance,
|
applyClientAppearance,
|
||||||
applyThemeAppearanceState,
|
applyThemeAppearanceState,
|
||||||
DEFAULT_USER_SETTINGS,
|
|
||||||
fetchUserSettings,
|
fetchUserSettings,
|
||||||
fetchUserThemeSlots,
|
fetchUserThemeSlots,
|
||||||
getClientActiveDarkThemeSlotNo,
|
getClientActiveDarkThemeSlotNo,
|
||||||
@@ -34,7 +33,6 @@ import {
|
|||||||
resolveDisplayedTheme,
|
resolveDisplayedTheme,
|
||||||
setCachedUserThemeSlots,
|
setCachedUserThemeSlots,
|
||||||
setClientThemeAppearance,
|
setClientThemeAppearance,
|
||||||
updateUserSettings,
|
|
||||||
upsertUserThemeSlot,
|
upsertUserThemeSlot,
|
||||||
USER_THEME_SLOT_SELECTIONS,
|
USER_THEME_SLOT_SELECTIONS,
|
||||||
} from '@/lib/settings'
|
} from '@/lib/settings'
|
||||||
@@ -51,7 +49,6 @@ import type {
|
|||||||
ClientTheme,
|
ClientTheme,
|
||||||
ThemeTokens,
|
ThemeTokens,
|
||||||
ThemeTagColours,
|
ThemeTagColours,
|
||||||
UserSettings,
|
|
||||||
UserThemeSlotMap,
|
UserThemeSlotMap,
|
||||||
UserThemeSlotNo,
|
UserThemeSlotNo,
|
||||||
UserThemeSlotSelection,
|
UserThemeSlotSelection,
|
||||||
@@ -224,6 +221,7 @@ const parseTab = (search: string): SettingsTab => {
|
|||||||
|
|
||||||
const tabButtonId = (tab: SettingsTab): string => `settings-tab-${ tab }`
|
const tabButtonId = (tab: SettingsTab): string => `settings-tab-${ tab }`
|
||||||
const tabPanelId = (tab: SettingsTab): string => `settings-panel-${ tab }`
|
const tabPanelId = (tab: SettingsTab): string => `settings-panel-${ tab }`
|
||||||
|
const SETTINGS_PATH = '/users/settings'
|
||||||
|
|
||||||
const themePreviewStyle = (tagColours: ThemeTagColours): CSSProperties =>
|
const themePreviewStyle = (tagColours: ThemeTagColours): CSSProperties =>
|
||||||
Object.fromEntries (
|
Object.fromEntries (
|
||||||
@@ -679,7 +677,6 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
const [loading, setLoading] = useState (true)
|
const [loading, setLoading] = useState (true)
|
||||||
const [name, setName] = useState ('')
|
const [name, setName] = useState ('')
|
||||||
const [settingsError, setSettingsError] = useState<string | null> (null)
|
const [settingsError, setSettingsError] = useState<string | null> (null)
|
||||||
const [, setDraftSettings] = useState<UserSettings> (DEFAULT_USER_SETTINGS)
|
|
||||||
const [savedThemeMode, setSavedThemeMode] =
|
const [savedThemeMode, setSavedThemeMode] =
|
||||||
useState<ClientThemeMode> (getClientThemeMode ())
|
useState<ClientThemeMode> (getClientThemeMode ())
|
||||||
const [draftThemeMode, setDraftThemeMode] =
|
const [draftThemeMode, setDraftThemeMode] =
|
||||||
@@ -719,13 +716,11 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
() => resolveDisplayedTheme ({
|
() => resolveDisplayedTheme ({
|
||||||
themeMode: draftThemeMode,
|
themeMode: draftThemeMode,
|
||||||
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
||||||
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
|
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo }, draftThemeSlots),
|
||||||
}, draftThemeSlots),
|
|
||||||
[draftActiveDarkThemeSlotNo,
|
[draftActiveDarkThemeSlotNo,
|
||||||
draftActiveLightThemeSlotNo,
|
draftActiveLightThemeSlotNo,
|
||||||
draftThemeMode,
|
draftThemeMode,
|
||||||
draftThemeSlots],
|
draftThemeSlots])
|
||||||
)
|
|
||||||
const selectedTheme = useMemo<ClientTheme> (
|
const selectedTheme = useMemo<ClientTheme> (
|
||||||
() => {
|
() => {
|
||||||
const currentSelection =
|
const currentSelection =
|
||||||
@@ -739,25 +734,19 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
name: themeLabelBySelection (currentSelection.id),
|
name: themeLabelBySelection (currentSelection.id),
|
||||||
builtin: false,
|
builtin: false,
|
||||||
baseTheme: currentSelection.baseTheme,
|
baseTheme: currentSelection.baseTheme,
|
||||||
tokens: themeSlot.tokens,
|
tokens: themeSlot.tokens }
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[draftThemeSlots, editingThemeSlot],
|
[draftThemeSlots, editingThemeSlot])
|
||||||
)
|
|
||||||
const hasUnsavedThemeChanges = useMemo (
|
const hasUnsavedThemeChanges = useMemo (
|
||||||
() =>
|
() =>
|
||||||
serialiseThemeDraft (
|
serialiseThemeDraft (draftThemeMode,
|
||||||
draftThemeMode,
|
|
||||||
draftActiveLightThemeSlotNo,
|
draftActiveLightThemeSlotNo,
|
||||||
draftActiveDarkThemeSlotNo,
|
draftActiveDarkThemeSlotNo,
|
||||||
draftThemeSlots,
|
draftThemeSlots)
|
||||||
)
|
!== (serialiseThemeDraft (savedThemeMode,
|
||||||
!== serialiseThemeDraft (
|
|
||||||
savedThemeMode,
|
|
||||||
savedActiveLightThemeSlotNo,
|
savedActiveLightThemeSlotNo,
|
||||||
savedActiveDarkThemeSlotNo,
|
savedActiveDarkThemeSlotNo,
|
||||||
savedThemeSlots,
|
savedThemeSlots)),
|
||||||
),
|
|
||||||
[draftActiveDarkThemeSlotNo,
|
[draftActiveDarkThemeSlotNo,
|
||||||
draftActiveLightThemeSlotNo,
|
draftActiveLightThemeSlotNo,
|
||||||
draftThemeMode,
|
draftThemeMode,
|
||||||
@@ -767,8 +756,10 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
savedActiveLightThemeSlotNo,
|
savedActiveLightThemeSlotNo,
|
||||||
savedEditingThemeSlot,
|
savedEditingThemeSlot,
|
||||||
savedThemeMode,
|
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 applyActiveTab = (tab: SettingsTab) => {
|
||||||
const params = new URLSearchParams (location.search)
|
const params = new URLSearchParams (location.search)
|
||||||
@@ -806,8 +797,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
description,
|
description,
|
||||||
confirmText,
|
confirmText,
|
||||||
cancelText,
|
cancelText,
|
||||||
variant: 'danger',
|
variant: 'danger' })
|
||||||
})
|
|
||||||
|
|
||||||
const settingsTabErrors = useMemo<Record<SettingsTab, boolean>> (
|
const settingsTabErrors = useMemo<Record<SettingsTab, boolean>> (
|
||||||
() => (
|
() => (
|
||||||
@@ -815,16 +805,15 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
behavior: false,
|
behavior: false,
|
||||||
theme: Boolean (settingsFieldErrors.theme?.length),
|
theme: Boolean (settingsFieldErrors.theme?.length),
|
||||||
keyboard: conflictActionIds.size > 0 }),
|
keyboard: conflictActionIds.size > 0 }),
|
||||||
[conflictActionIds.size, settingsFieldErrors.theme, userFieldErrors.name],
|
[conflictActionIds.size, settingsFieldErrors.theme, userFieldErrors.name])
|
||||||
)
|
|
||||||
|
|
||||||
const applyDraftThemeState = (
|
const applyDraftThemeState = (
|
||||||
{ themeMode = draftThemeMode,
|
{ themeMode = draftThemeMode,
|
||||||
activeLightThemeSlotNo = draftActiveLightThemeSlotNo,
|
activeLightThemeSlotNo = draftActiveLightThemeSlotNo,
|
||||||
activeDarkThemeSlotNo = draftActiveDarkThemeSlotNo,
|
activeDarkThemeSlotNo = draftActiveDarkThemeSlotNo,
|
||||||
themeSlots = draftThemeSlots,
|
themeSlots = draftThemeSlots,
|
||||||
nextEditingThemeSlot = editingThemeSlot }: {
|
nextEditingThemeSlot = editingThemeSlot,
|
||||||
themeMode?: ClientThemeMode
|
}: { themeMode?: ClientThemeMode
|
||||||
activeLightThemeSlotNo?: UserThemeSlotNo
|
activeLightThemeSlotNo?: UserThemeSlotNo
|
||||||
activeDarkThemeSlotNo?: UserThemeSlotNo
|
activeDarkThemeSlotNo?: UserThemeSlotNo
|
||||||
themeSlots?: UserThemeSlotMap
|
themeSlots?: UserThemeSlotMap
|
||||||
@@ -835,16 +824,16 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
setDraftActiveDarkThemeSlotNo (activeDarkThemeSlotNo)
|
setDraftActiveDarkThemeSlotNo (activeDarkThemeSlotNo)
|
||||||
setDraftThemeSlots (themeSlots)
|
setDraftThemeSlots (themeSlots)
|
||||||
setEditingThemeSlot (nextEditingThemeSlot)
|
setEditingThemeSlot (nextEditingThemeSlot)
|
||||||
setDraftSettings (current => ({
|
|
||||||
...current,
|
|
||||||
theme: themeMode,
|
|
||||||
}))
|
|
||||||
setCachedUserThemeSlots (themeSlots)
|
setCachedUserThemeSlots (themeSlots)
|
||||||
applyThemeAppearanceState ({
|
applyThemeAppearanceState ({
|
||||||
themeMode,
|
themeMode,
|
||||||
activeLightThemeSlotNo,
|
activeLightThemeSlotNo,
|
||||||
activeDarkThemeSlotNo,
|
activeDarkThemeSlotNo }, themeSlots)
|
||||||
}, themeSlots)
|
}
|
||||||
|
|
||||||
|
const discardAccountChanges = () => {
|
||||||
|
setName (savedName)
|
||||||
|
clearUserValidationErrors ()
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentThemeDraftState = (): ThemeDraftState => ({
|
const currentThemeDraftState = (): ThemeDraftState => ({
|
||||||
@@ -852,16 +841,14 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
||||||
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
|
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
|
||||||
themeSlots: draftThemeSlots,
|
themeSlots: draftThemeSlots,
|
||||||
editingThemeSlot,
|
editingThemeSlot })
|
||||||
})
|
|
||||||
|
|
||||||
const savedThemeDraftState = (): ThemeDraftState => ({
|
const savedThemeDraftState = (): ThemeDraftState => ({
|
||||||
themeMode: savedThemeMode,
|
themeMode: savedThemeMode,
|
||||||
activeLightThemeSlotNo: savedActiveLightThemeSlotNo,
|
activeLightThemeSlotNo: savedActiveLightThemeSlotNo,
|
||||||
activeDarkThemeSlotNo: savedActiveDarkThemeSlotNo,
|
activeDarkThemeSlotNo: savedActiveDarkThemeSlotNo,
|
||||||
themeSlots: savedThemeSlots,
|
themeSlots: savedThemeSlots,
|
||||||
editingThemeSlot: savedEditingThemeSlot,
|
editingThemeSlot: savedEditingThemeSlot })
|
||||||
})
|
|
||||||
|
|
||||||
const discardThemeChanges = () => {
|
const discardThemeChanges = () => {
|
||||||
setDraftThemeMode (savedThemeMode)
|
setDraftThemeMode (savedThemeMode)
|
||||||
@@ -873,11 +860,19 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
setClientThemeAppearance ({
|
setClientThemeAppearance ({
|
||||||
themeMode: savedThemeMode,
|
themeMode: savedThemeMode,
|
||||||
activeLightThemeSlotNo: savedActiveLightThemeSlotNo,
|
activeLightThemeSlotNo: savedActiveLightThemeSlotNo,
|
||||||
activeDarkThemeSlotNo: savedActiveDarkThemeSlotNo,
|
activeDarkThemeSlotNo: savedActiveDarkThemeSlotNo })
|
||||||
})
|
|
||||||
applyClientAppearance (savedThemeSlots)
|
applyClientAppearance (savedThemeSlots)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const discardCurrentTabChanges = async (): Promise<void> => {
|
||||||
|
const currentDirtyState = dirtyStates[activeTab]
|
||||||
|
|
||||||
|
if (currentDirtyState?.dirty)
|
||||||
|
await currentDirtyState.discard ()
|
||||||
|
|
||||||
|
setDirtyStates ({ })
|
||||||
|
}
|
||||||
|
|
||||||
const requestActiveTab = async (
|
const requestActiveTab = async (
|
||||||
nextTab: SettingsTab,
|
nextTab: SettingsTab,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
@@ -894,12 +889,11 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
const confirmed = await confirmDiscardChanges (
|
const confirmed = await confirmDiscardChanges (
|
||||||
'このタブを離れると、保存していない変更は失われます。',
|
'このタブを離れると、保存していない変更は失われます。',
|
||||||
'このタブに残る',
|
'このタブに残る',
|
||||||
'変更を破棄して移動',
|
'変更を破棄して移動')
|
||||||
)
|
|
||||||
if (!(confirmed))
|
if (!(confirmed))
|
||||||
return
|
return
|
||||||
|
|
||||||
await currentDirtyState.discard ()
|
await discardCurrentTabChanges ()
|
||||||
applyActiveTab (nextTab)
|
applyActiveTab (nextTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -919,8 +913,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
const confirmed = await confirmDiscardChanges (
|
const confirmed = await confirmDiscardChanges (
|
||||||
description,
|
description,
|
||||||
cancelText,
|
cancelText,
|
||||||
confirmText,
|
confirmText)
|
||||||
)
|
|
||||||
if (!(confirmed))
|
if (!(confirmed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -932,10 +925,52 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
activeLightThemeSlotNo: nextState.activeLightThemeSlotNo,
|
activeLightThemeSlotNo: nextState.activeLightThemeSlotNo,
|
||||||
activeDarkThemeSlotNo: nextState.activeDarkThemeSlotNo,
|
activeDarkThemeSlotNo: nextState.activeDarkThemeSlotNo,
|
||||||
themeSlots: nextState.themeSlots,
|
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 = (
|
const updateDraftThemeSlot = (
|
||||||
selection: UserThemeSlotSelection,
|
selection: UserThemeSlotSelection,
|
||||||
updater: (tokens: ThemeTokens) => ThemeTokens,
|
updater: (tokens: ThemeTokens) => ThemeTokens,
|
||||||
@@ -945,16 +980,9 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
?? ensureCompleteThemeSlots ({ })[selection]
|
?? ensureCompleteThemeSlots ({ })[selection]
|
||||||
const nextThemeSlots = {
|
const nextThemeSlots = {
|
||||||
...draftThemeSlots,
|
...draftThemeSlots,
|
||||||
[selection]: {
|
[selection]: { ...currentThemeSlot, tokens: updater (currentThemeSlot.tokens) } }
|
||||||
...currentThemeSlot,
|
|
||||||
tokens: updater (currentThemeSlot.tokens),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
applyDraftThemeState ({
|
applyDraftThemeState ({ themeSlots: nextThemeSlots, nextEditingThemeSlot: selection })
|
||||||
themeSlots: nextThemeSlots,
|
|
||||||
nextEditingThemeSlot: selection,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleTabKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
const handleTabKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
||||||
@@ -965,23 +993,23 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
event.preventDefault ()
|
event.preventDefault ()
|
||||||
void requestActiveTab (tabs[(currentIndex + 1) % tabs.length].id)
|
requestActiveTab (tabs[(currentIndex + 1) % tabs.length].id)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
event.preventDefault ()
|
event.preventDefault ()
|
||||||
void requestActiveTab (tabs[(currentIndex - 1 + tabs.length) % tabs.length].id)
|
requestActiveTab (tabs[(currentIndex - 1 + tabs.length) % tabs.length].id)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'Home':
|
case 'Home':
|
||||||
event.preventDefault ()
|
event.preventDefault ()
|
||||||
void requestActiveTab (tabs[0].id)
|
requestActiveTab (tabs[0].id)
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'End':
|
case 'End':
|
||||||
event.preventDefault ()
|
event.preventDefault ()
|
||||||
void requestActiveTab (tabs[tabs.length - 1].id)
|
requestActiveTab (tabs[tabs.length - 1].id)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -999,9 +1027,9 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
{
|
{
|
||||||
const data = await apiPut<User> (
|
const data = await apiPut<User> (
|
||||||
`/users/${ user.id }`, formData,
|
`/users/${ user.id }`, formData,
|
||||||
{ headers: { 'Content-Type': 'multipart/form-data' } },
|
{ headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
)
|
|
||||||
setUser (currentUser => ({ ...currentUser, ...data }))
|
setUser (currentUser => ({ ...currentUser, ...data }))
|
||||||
|
setName (data.name ?? '')
|
||||||
toast ({ title: '表示名を更新しました.' })
|
toast ({ title: '表示名を更新しました.' })
|
||||||
}
|
}
|
||||||
catch (submitError)
|
catch (submitError)
|
||||||
@@ -1026,11 +1054,9 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
selection === editingThemeSlot
|
selection === editingThemeSlot
|
||||||
&& savedThemeSlot?.createdAt == null
|
&& savedThemeSlot?.createdAt == null
|
||||||
|
|
||||||
if (
|
if ((JSON.stringify (draftThemeSlot?.tokens)
|
||||||
JSON.stringify (draftThemeSlot?.tokens)
|
=== JSON.stringify (savedThemeSlot?.tokens))
|
||||||
=== JSON.stringify (savedThemeSlot?.tokens)
|
&& !(requiresPersist))
|
||||||
&& !(requiresPersist)
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
const persisted = await upsertUserThemeSlot (
|
const persisted = await upsertUserThemeSlot (
|
||||||
@@ -1038,19 +1064,12 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
draftThemeSlot?.slotNo ?? Number (selection.split (':')[2]) as UserThemeSlotNo,
|
draftThemeSlot?.slotNo ?? Number (selection.split (':')[2]) as UserThemeSlotNo,
|
||||||
draftThemeSlot?.tokens ?? getDefaultUserThemeSlot (
|
draftThemeSlot?.tokens ?? getDefaultUserThemeSlot (
|
||||||
selection.split (':')[1] as BuiltinThemeId,
|
selection.split (':')[1] as BuiltinThemeId,
|
||||||
Number (selection.split (':')[2]) as UserThemeSlotNo,
|
Number (selection.split (':')[2]) as UserThemeSlotNo).tokens)
|
||||||
).tokens,
|
nextSavedThemeSlots = { ...nextSavedThemeSlots, [selection]: persisted }
|
||||||
)
|
|
||||||
nextSavedThemeSlots = {
|
|
||||||
...nextSavedThemeSlots,
|
|
||||||
[selection]: persisted,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await updateUserSettings ({ theme: draftThemeMode })
|
|
||||||
const completeThemeSlots = ensureCompleteThemeSlots (nextSavedThemeSlots)
|
const completeThemeSlots = ensureCompleteThemeSlots (nextSavedThemeSlots)
|
||||||
|
|
||||||
setDraftSettings ({ ...data, theme: draftThemeMode })
|
|
||||||
setSavedThemeMode (draftThemeMode)
|
setSavedThemeMode (draftThemeMode)
|
||||||
setSavedActiveLightThemeSlotNo (draftActiveLightThemeSlotNo)
|
setSavedActiveLightThemeSlotNo (draftActiveLightThemeSlotNo)
|
||||||
setSavedActiveDarkThemeSlotNo (draftActiveDarkThemeSlotNo)
|
setSavedActiveDarkThemeSlotNo (draftActiveDarkThemeSlotNo)
|
||||||
@@ -1058,11 +1077,11 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
setDraftThemeSlots (completeThemeSlots)
|
setDraftThemeSlots (completeThemeSlots)
|
||||||
setSavedEditingThemeSlot (editingThemeSlot)
|
setSavedEditingThemeSlot (editingThemeSlot)
|
||||||
setEditingThemeSlot (editingThemeSlot)
|
setEditingThemeSlot (editingThemeSlot)
|
||||||
|
// Display mode and active slot numbers are browser-local state.
|
||||||
setClientThemeAppearance ({
|
setClientThemeAppearance ({
|
||||||
themeMode: draftThemeMode,
|
themeMode: draftThemeMode,
|
||||||
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
||||||
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
|
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo })
|
||||||
})
|
|
||||||
setCachedUserThemeSlots (completeThemeSlots)
|
setCachedUserThemeSlots (completeThemeSlots)
|
||||||
applyClientAppearance (completeThemeSlots)
|
applyClientAppearance (completeThemeSlots)
|
||||||
toast ({ title: 'テーマ設定を保存しました.' })
|
toast ({ title: 'テーマ設定を保存しました.' })
|
||||||
@@ -1096,10 +1115,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const [settings, themeSlots] = await Promise.all ([
|
const [settings, themeSlots] = await Promise.all ([fetchUserSettings (),
|
||||||
fetchUserSettings (),
|
fetchUserThemeSlots ()])
|
||||||
fetchUserThemeSlots (),
|
|
||||||
])
|
|
||||||
if (cancelled)
|
if (cancelled)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1114,10 +1131,8 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
const initialEditingThemeSlot = resolveDisplayedTheme ({
|
const initialEditingThemeSlot = resolveDisplayedTheme ({
|
||||||
themeMode,
|
themeMode,
|
||||||
activeLightThemeSlotNo,
|
activeLightThemeSlotNo,
|
||||||
activeDarkThemeSlotNo,
|
activeDarkThemeSlotNo }, completeThemeSlots).selection
|
||||||
}, completeThemeSlots).selection
|
|
||||||
|
|
||||||
setDraftSettings ({ ...settings, theme: themeMode })
|
|
||||||
setSavedThemeMode (themeMode)
|
setSavedThemeMode (themeMode)
|
||||||
setDraftThemeMode (themeMode)
|
setDraftThemeMode (themeMode)
|
||||||
setSavedActiveLightThemeSlotNo (activeLightThemeSlotNo)
|
setSavedActiveLightThemeSlotNo (activeLightThemeSlotNo)
|
||||||
@@ -1129,11 +1144,9 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
setSavedEditingThemeSlot (initialEditingThemeSlot)
|
setSavedEditingThemeSlot (initialEditingThemeSlot)
|
||||||
setEditingThemeSlot (initialEditingThemeSlot)
|
setEditingThemeSlot (initialEditingThemeSlot)
|
||||||
setCachedUserThemeSlots (completeThemeSlots)
|
setCachedUserThemeSlots (completeThemeSlots)
|
||||||
applyThemeAppearanceState ({
|
applyThemeAppearanceState ({ themeMode,
|
||||||
themeMode,
|
|
||||||
activeLightThemeSlotNo,
|
activeLightThemeSlotNo,
|
||||||
activeDarkThemeSlotNo,
|
activeDarkThemeSlotNo }, completeThemeSlots)
|
||||||
}, completeThemeSlots)
|
|
||||||
setSettingsError (null)
|
setSettingsError (null)
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -1160,6 +1173,12 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
applyActiveTab ('account')
|
applyActiveTab ('account')
|
||||||
}, [location.pathname, location.search, userFieldErrors.name])
|
}, [location.pathname, location.search, userFieldErrors.name])
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
updateTabDirtyState ('account', {
|
||||||
|
dirty: hasUnsavedAccountChanges,
|
||||||
|
discard: discardAccountChanges })
|
||||||
|
}, [hasUnsavedAccountChanges, savedName])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
if (settingsFieldErrors.theme?.length)
|
if (settingsFieldErrors.theme?.length)
|
||||||
applyActiveTab ('theme')
|
applyActiveTab ('theme')
|
||||||
@@ -1169,8 +1188,7 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
applyThemeAppearanceState ({
|
applyThemeAppearanceState ({
|
||||||
themeMode: draftThemeMode,
|
themeMode: draftThemeMode,
|
||||||
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
activeLightThemeSlotNo: draftActiveLightThemeSlotNo,
|
||||||
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo,
|
activeDarkThemeSlotNo: draftActiveDarkThemeSlotNo }, draftThemeSlots)
|
||||||
}, draftThemeSlots)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
setCachedUserThemeSlots (savedThemeSlots)
|
setCachedUserThemeSlots (savedThemeSlots)
|
||||||
@@ -1184,39 +1202,35 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
|
|
||||||
const accountSectionProps: AccountSectionProps | null =
|
const accountSectionProps: AccountSectionProps | null =
|
||||||
user
|
user
|
||||||
? {
|
? { user,
|
||||||
user,
|
|
||||||
name,
|
name,
|
||||||
onNameChange: setName,
|
onNameChange: setName,
|
||||||
onInheritOpen: () => setInheritVsbl (true),
|
onInheritOpen: () => setInheritVsbl (true),
|
||||||
onUserCodeOpen: () => setUserCodeVsbl (true),
|
onUserCodeOpen: () => setUserCodeVsbl (true),
|
||||||
onUserSubmit: handleUserSubmit,
|
onUserSubmit: handleUserSubmit,
|
||||||
userBaseErrors,
|
userBaseErrors,
|
||||||
userFieldErrors,
|
userFieldErrors }
|
||||||
}
|
|
||||||
: null
|
: null
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
updateTabDirtyState ('theme', {
|
updateTabDirtyState ('theme', {
|
||||||
dirty: hasUnsavedThemeChanges,
|
dirty: hasUnsavedThemeChanges,
|
||||||
discard: handleThemeDiscard,
|
discard: handleThemeDiscard })
|
||||||
})
|
|
||||||
}, [hasUnsavedThemeChanges, savedThemeSlots])
|
}, [hasUnsavedThemeChanges, savedThemeSlots])
|
||||||
|
|
||||||
useKeyboardShortcuts ({
|
useKeyboardShortcuts ({
|
||||||
'settings.account': () => {
|
['settings.account']: () => {
|
||||||
void requestActiveTab ('account')
|
requestActiveTab ('account')
|
||||||
},
|
},
|
||||||
'settings.behavior': () => {
|
['settings.behavior']: () => {
|
||||||
void requestActiveTab ('behavior')
|
requestActiveTab ('behavior')
|
||||||
},
|
},
|
||||||
'settings.theme': () => {
|
['settings.theme']: () => {
|
||||||
void requestActiveTab ('theme')
|
requestActiveTab ('theme')
|
||||||
},
|
},
|
||||||
'settings.keyboard': () => {
|
['settings.keyboard']: () => {
|
||||||
void requestActiveTab ('keyboard')
|
requestActiveTab ('keyboard')
|
||||||
},
|
} })
|
||||||
})
|
|
||||||
|
|
||||||
const themeSectionProps: ThemeSectionProps = {
|
const themeSectionProps: ThemeSectionProps = {
|
||||||
draftThemeMode,
|
draftThemeMode,
|
||||||
@@ -1230,45 +1244,29 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
settingsBaseErrors,
|
settingsBaseErrors,
|
||||||
settingsFieldErrors,
|
settingsFieldErrors,
|
||||||
onSelectThemeMode: themeMode => {
|
onSelectThemeMode: themeMode => {
|
||||||
void requestThemeStateChange (baseState => ({
|
requestThemeStateChange (baseState => ({ ...baseState, themeMode }))
|
||||||
...baseState,
|
|
||||||
themeMode,
|
|
||||||
}))
|
|
||||||
},
|
},
|
||||||
onSelectLightThemeSlotNo: slotNo => {
|
onSelectLightThemeSlotNo: slotNo => {
|
||||||
void requestThemeStateChange (baseState => ({
|
requestThemeStateChange (baseState => ({
|
||||||
...baseState,
|
...baseState,
|
||||||
activeLightThemeSlotNo: slotNo,
|
activeLightThemeSlotNo: slotNo,
|
||||||
editingThemeSlot: getUserThemeSlotSelection ('light', slotNo),
|
editingThemeSlot: getUserThemeSlotSelection ('light', slotNo) }))
|
||||||
}))
|
|
||||||
},
|
},
|
||||||
onSelectDarkThemeSlotNo: slotNo => {
|
onSelectDarkThemeSlotNo: slotNo => {
|
||||||
void requestThemeStateChange (baseState => ({
|
requestThemeStateChange (baseState => ({
|
||||||
...baseState,
|
...baseState,
|
||||||
activeDarkThemeSlotNo: slotNo,
|
activeDarkThemeSlotNo: slotNo,
|
||||||
editingThemeSlot: getUserThemeSlotSelection ('dark', slotNo),
|
editingThemeSlot: getUserThemeSlotSelection ('dark', slotNo) }))
|
||||||
}))
|
|
||||||
},
|
},
|
||||||
onSelectEditingThemeSlot: selection => {
|
onSelectEditingThemeSlot: selection => {
|
||||||
void requestThemeStateChange (baseState => ({
|
requestThemeStateChange (baseState => ({ ...baseState, editingThemeSlot: selection }))
|
||||||
...baseState,
|
|
||||||
editingThemeSlot: selection,
|
|
||||||
}))
|
|
||||||
},
|
},
|
||||||
onThemeTokenChange: (key, value) => {
|
onThemeTokenChange: (key, value) => {
|
||||||
updateDraftThemeSlot (editingThemeSlot, tokens => ({
|
updateDraftThemeSlot (editingThemeSlot, tokens => ({ ...tokens, [key]: value }))
|
||||||
...tokens,
|
|
||||||
[key]: value,
|
|
||||||
}))
|
|
||||||
},
|
},
|
||||||
onTagColourChange: (category, colour) => {
|
onTagColourChange: (category, colour) => {
|
||||||
updateDraftThemeSlot (editingThemeSlot, tokens => ({
|
updateDraftThemeSlot (editingThemeSlot, tokens => ({
|
||||||
...tokens,
|
...tokens, tagColours: { ...tokens.tagColours, [category]: colour } }))
|
||||||
tagColours: {
|
|
||||||
...tokens.tagColours,
|
|
||||||
[category]: colour,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
},
|
},
|
||||||
onResetThemeSlot: selection => {
|
onResetThemeSlot: selection => {
|
||||||
const baseTheme =
|
const baseTheme =
|
||||||
@@ -1278,14 +1276,11 @@ const SettingPage: FC<Props> = ({ user, setUser }) => {
|
|||||||
selection,
|
selection,
|
||||||
() => getDefaultUserThemeSlot (
|
() => getDefaultUserThemeSlot (
|
||||||
baseTheme,
|
baseTheme,
|
||||||
draftThemeSlots[selection]?.slotNo
|
(draftThemeSlots[selection]?.slotNo
|
||||||
?? Number (selection.split (':')[2]) as UserThemeSlotNo,
|
?? Number (selection.split (':')[2]) as UserThemeSlotNo)).tokens)
|
||||||
).tokens,
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onThemeSubmit: handleThemeSubmit,
|
onThemeSubmit: handleThemeSubmit,
|
||||||
onThemeDiscard: handleThemeDiscard,
|
onThemeDiscard: handleThemeDiscard }
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainArea>
|
<MainArea>
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする