|
= {
threeColumns: '3 列',
tagsBottom: '2 列 A 型',
@@ -228,32 +231,20 @@ const TheatreDetailPage: FC = ({ user }: Props) => {
const [post, setPost] = useState (null)
const [videoLength, setVideoLength] = useState (0)
const [weights, setWeights] = useState (INITIAL_WEIGHTS)
- const [layoutMode, setLayoutMode] = useState (() => {
- const stored = localStorage.getItem (LAYOUT_STORAGE_KEY)
- return (
- ((['threeColumns', 'tagsBottom', 'commentsBottom'] as TheatreLayoutMode[])
- .includes (stored as TheatreLayoutMode))
- ? (stored as TheatreLayoutMode)
- : 'threeColumns')
- })
- const [tagFlow, setTagFlow] = useState (() => {
- const stored = localStorage.getItem (TAG_FLOW_STORAGE_KEY)
- return (
- (['vertical', 'horizontal'] as TagFlow[]).includes (stored as TagFlow)
- ? (stored as TagFlow)
- : 'vertical')
- })
+ const [layoutMode, setLayoutMode] =
+ useState (() => getClientTheatreLayoutMode ())
+ const [tagFlow, setTagFlow] = useState (() => getClientTheatreTagFlow ())
const { fieldErrors, clearValidationErrors, applyValidationError } =
useValidationErrors ()
const changeLayoutMode = (mode: TheatreLayoutMode) => {
setLayoutMode (mode)
- localStorage.setItem (LAYOUT_STORAGE_KEY, mode)
+ setClientTheatreLayoutMode (mode)
}
const changeTagFlow = (flow: TagFlow) => {
setTagFlow (flow)
- localStorage.setItem (TAG_FLOW_STORAGE_KEY, flow)
+ setClientTheatreTagFlow (flow)
}
const applyTheatreInfo = useCallback ((nextInfo: TheatreInfo) => {
diff --git a/frontend/src/pages/users/SettingPage.tsx b/frontend/src/pages/users/SettingPage.tsx
index a271da9..44f0158 100644
--- a/frontend/src/pages/users/SettingPage.tsx
+++ b/frontend/src/pages/users/SettingPage.tsx
@@ -1,4 +1,4 @@
-import type { FC } from 'react'
+import type { Dispatch, FC, SetStateAction } from 'react'
import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet-async'
@@ -9,35 +9,115 @@ import FormField from '@/components/common/FormField'
import Label from '@/components/common/Label'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea'
+import { useUserSettings } from '@/components/users/UserSettingsProvider'
import InheritDialogue from '@/components/users/InheritDialogue'
import UserCodeDialogue from '@/components/users/UserCodeDialogue'
import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { apiPut } from '@/lib/api'
+import {
+ AUTO_FETCH_OPTIONS,
+ DEFAULT_USER_SETTINGS,
+ DISPLAY_DENSITY_OPTIONS,
+ FONT_SIZE_OPTIONS,
+ POST_LIST_LIMIT_OPTIONS,
+ POST_LIST_ORDER_OPTIONS,
+ THEME_OPTIONS,
+ VIEWED_POST_DISPLAY_OPTIONS,
+ WIKI_EDITOR_MODE_OPTIONS,
+ updateUserSettings,
+} from '@/lib/settings'
import { inputClass } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors'
import type { User } from '@/types'
+import type { UserSettings } from '@/lib/settings'
type Props = { user: User | null
- setUser: React.Dispatch> }
+ setUser: Dispatch> }
type UserFormField = 'name'
+type SettingsFormField =
+ | 'theme'
+ | 'displayDensity'
+ | 'fontSize'
+ | 'postListLimit'
+ | 'postListOrder'
+ | 'viewedPostDisplay'
+ | 'tagAutocompleteNico'
+ | 'autoFetchTitle'
+ | 'autoFetchThumbnail'
+ | 'wikiEditorMode'
+
+const sectionClassName = 'space-y-4 rounded-xl border border-border bg-background/80 p-4'
+
+const themeLabel: Record = {
+ system: 'システム設定に従う',
+ light: '明',
+ dark: '暗' }
+
+const densityLabel: Record = {
+ comfortable: '標準',
+ compact: '詰める' }
+
+const fontSizeLabel: Record = {
+ small: '小',
+ normal: '標準',
+ large: '大' }
+
+const postListOrderLabel: Record = {
+ title_asc: 'タイトル昇順',
+ title_desc: 'タイトル降順',
+ url_asc: 'URL 昇順',
+ url_desc: 'URL 降順',
+ original_created_at_asc: 'オリジナル投稿日時 昇順',
+ original_created_at_desc: 'オリジナル投稿日時 降順',
+ created_at_asc: '投稿日 昇順',
+ created_at_desc: '投稿日 降順',
+ updated_at_asc: '更新日 昇順',
+ updated_at_desc: '更新日 降順' }
+
+const viewedPostDisplayLabel: Record = {
+ show: '通常表示',
+ dim: '薄く表示',
+ hide: '隠す' }
+
+const autoFetchLabel: Record = {
+ auto: '自動',
+ manual: '手動',
+ off: '無効' }
+
+const wikiEditorModeLabel: Record = {
+ split: '左右表示',
+ write: '本文のみ',
+ preview: 'プレビューのみ' }
const SettingPage: FC = ({ user, setUser }) => {
+ const [draftSettings, setDraftSettings] = useState (DEFAULT_USER_SETTINGS)
const [name, setName] = useState ('')
const [userCodeVsbl, setUserCodeVsbl] = useState (false)
const [inheritVsbl, setInheritVsbl] = useState (false)
- const { baseErrors, fieldErrors, clearValidationErrors, applyValidationError } =
- useValidationErrors ()
+ const { loaded, settings, setSettings } = useUserSettings ()
+ const {
+ baseErrors: nameBaseErrors,
+ fieldErrors: nameFieldErrors,
+ clearValidationErrors: clearNameValidationErrors,
+ applyValidationError: applyNameValidationError,
+ } = useValidationErrors ()
+ const {
+ baseErrors: settingsBaseErrors,
+ fieldErrors: settingsFieldErrors,
+ clearValidationErrors: clearSettingsValidationErrors,
+ applyValidationError: applySettingsValidationError,
+ } = useValidationErrors ()
- const handleSubmit = async () => {
+ const handleUserSubmit = async () => {
if (!(user))
return
- clearValidationErrors ()
+ clearNameValidationErrors ()
const formData = new FormData
formData.append ('name', name)
@@ -47,13 +127,41 @@ const SettingPage: FC = ({ user, setUser }) => {
const data = await apiPut (
`/users/${ user.id }`, formData,
{ headers: { 'Content-Type': 'multipart/form-data' } })
- setUser (user => ({ ...user, ...data }))
- toast ({ title: '設定を更新しました.' })
+ setUser (currentUser => ({ ...currentUser, ...data }))
+ toast ({ title: '表示名を更新しました.' })
}
- catch (e)
+ catch (error)
{
- applyValidationError (e)
- toast ({ title: 'しっぱい……' })
+ applyNameValidationError (error)
+ toast ({ title: '表示名を更新できませんでした.' })
+ }
+ }
+
+ const handleSettingsSubmit = async () => {
+ clearSettingsValidationErrors ()
+
+ try
+ {
+ const data = await updateUserSettings ({
+ theme: draftSettings.theme,
+ display_density: draftSettings.displayDensity,
+ font_size: draftSettings.fontSize,
+ post_list_limit: draftSettings.postListLimit,
+ post_list_order: draftSettings.postListOrder,
+ viewed_post_display: draftSettings.viewedPostDisplay,
+ tag_autocomplete_nico: draftSettings.tagAutocompleteNico,
+ auto_fetch_title: draftSettings.autoFetchTitle,
+ auto_fetch_thumbnail: draftSettings.autoFetchThumbnail,
+ wiki_editor_mode: draftSettings.wikiEditorMode,
+ })
+ setDraftSettings (data)
+ setSettings (data)
+ toast ({ title: '設定を保存しました.' })
+ }
+ catch (error)
+ {
+ applySettingsValidationError (error)
+ toast ({ title: '設定を保存できませんでした.' })
}
}
@@ -64,6 +172,10 @@ const SettingPage: FC = ({ user, setUser }) => {
setName (user.name ?? '')
}, [user])
+ useEffect (() => {
+ setDraftSettings (settings)
+ }, [settings])
+
return (
@@ -74,48 +186,234 @@ const SettingPage: FC = ({ user, setUser }) => {
diff --git a/frontend/src/pages/wiki/WikiEditPage.tsx b/frontend/src/pages/wiki/WikiEditPage.tsx
index dcbe12c..e7bf99d 100644
--- a/frontend/src/pages/wiki/WikiEditPage.tsx
+++ b/frontend/src/pages/wiki/WikiEditPage.tsx
@@ -8,6 +8,7 @@ import { useParams, useNavigate } from 'react-router-dom'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import MainArea from '@/components/layout/MainArea'
+import { useUserSettings } from '@/components/users/UserSettingsProvider'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { apiGet, apiPut } from '@/lib/api'
@@ -25,6 +26,27 @@ import type { User, WikiPage } from '@/types'
const mdParser = new MarkdownIt
+type EditorView = {
+ menu: boolean
+ md: boolean
+ html: boolean }
+
+const editorView = (
+ mode: 'split' | 'write' | 'preview',
+): EditorView => {
+ switch (mode)
+ {
+ case 'write':
+ return { menu: true, md: true, html: false }
+
+ case 'preview':
+ return { menu: true, md: false, html: true }
+
+ default:
+ return { menu: true, md: true, html: true }
+ }
+}
+
type Props = { user: User | null }
type WikiFormField = 'title' | 'body'
@@ -32,6 +54,7 @@ type WikiFormField = 'title' | 'body'
const WikiEditPage: FC = ({ user }) => {
const editable = canEditContent (user)
+ const { settings } = useUserSettings ()
const { id } = useParams ()
@@ -114,6 +137,7 @@ const WikiEditPage: FC = ({ user }) => {
{() => (
mdParser.render (text)}
onChange={({ text }) => setBody (text)}/>)}
diff --git a/frontend/src/pages/wiki/WikiNewPage.tsx b/frontend/src/pages/wiki/WikiNewPage.tsx
index fffb690..b4d5d6a 100644
--- a/frontend/src/pages/wiki/WikiNewPage.tsx
+++ b/frontend/src/pages/wiki/WikiNewPage.tsx
@@ -9,6 +9,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
import FieldError from '@/components/common/FieldError'
import FormField from '@/components/common/FormField'
import MainArea from '@/components/layout/MainArea'
+import { useUserSettings } from '@/components/users/UserSettingsProvider'
import { toast } from '@/components/ui/use-toast'
import { SITE_TITLE } from '@/config'
import { apiPost } from '@/lib/api'
@@ -23,6 +24,27 @@ import type { User, WikiPage } from '@/types'
const mdParser = new MarkdownIt
+type EditorView = {
+ menu: boolean
+ md: boolean
+ html: boolean }
+
+const editorView = (
+ mode: 'split' | 'write' | 'preview',
+): EditorView => {
+ switch (mode)
+ {
+ case 'write':
+ return { menu: true, md: true, html: false }
+
+ case 'preview':
+ return { menu: true, md: false, html: true }
+
+ default:
+ return { menu: true, md: true, html: true }
+ }
+}
+
type Props = { user: User | null }
type WikiFormField = 'title' | 'body'
@@ -30,6 +52,7 @@ type WikiFormField = 'title' | 'body'
const WikiNewPage: FC = ({ user }) => {
const editable = canEditContent (user)
+ const { settings } = useUserSettings ()
const location = useLocation ()
const navigate = useNavigate ()
@@ -92,6 +115,7 @@ const WikiNewPage: FC = ({ user }) => {
{() => (
mdParser.render (text)}
onChange={({ text }) => setBody (text)}/>)}
|