@@ -5,7 +5,6 @@ import { ErrorBoundary, Suspense } from "@suspensive/react";
55import { enqueueSnackbar , OptionsObject } from "notistack" ;
66import * as React from "react" ;
77import { Link , Navigate , useParams } from "react-router-dom" ;
8- import * as R from "remeda" ;
98
109import { useAppContext } from "../../contexts/app_context" ;
1110import { SubmitConfirmDialog } from "../dialogs/submit_confirm" ;
@@ -19,6 +18,7 @@ import { PrimaryTitle, SecondaryTitle } from "../elements/titles";
1918import { Page } from "../page" ;
2019
2120type SessionUpdateSchema = {
21+ id : string ;
2222 title_ko : string ;
2323 title_en : string ;
2424 summary_ko : string ;
@@ -35,42 +35,46 @@ type SessionUpdateSchema = {
3535 } [ ] ;
3636} ;
3737
38- type SessionEditorState = SessionUpdateSchema & {
39- openSubmitConfirmDialog : boolean ;
38+ type SessionSchema = SessionUpdateSchema & {
39+ speakers : ( SessionUpdateSchema [ "speakers" ] [ number ] & {
40+ user : {
41+ id : number ; // UUID of the user
42+ email : string ; // Email of the user
43+ nickname_ko : string | null ; // Nickname in Korean
44+ nickname_en : string | null ; // Nickname in English
45+ } ;
46+ } ) [ ] ;
4047} ;
4148
42- const DummySessionInfo : SessionUpdateSchema = {
43- title_ko : "" ,
44- title_en : "" ,
45- summary_ko : "" ,
46- summary_en : "" ,
47- description_ko : "" ,
48- description_en : "" ,
49- image : null ,
50-
51- speakers : [ ] ,
49+ type SessionEditorFormProps = {
50+ disabled ?: boolean ;
51+ showSubmitButton ?: boolean ;
52+ onSubmit ?: ( session : SessionUpdateSchema ) => void ;
53+ language : "ko" | "en" ;
54+ defaultValue : SessionSchema ;
5255} ;
5356
54- const InnerSessionEditor : React . FC = ( ) => {
55- const { sessionId } = useParams < { sessionId ?: string } > ( ) ;
56- const { language } = useAppContext ( ) ;
57- const participantPortalClient = Common . Hooks . BackendParticipantPortalAPI . useParticipantPortalClient ( ) ;
58- const updateSessionMutation = Common . Hooks . BackendParticipantPortalAPI . useUpdatePresentationMutation ( participantPortalClient ) ;
59- const { data : session } = Common . Hooks . BackendParticipantPortalAPI . useRetrievePresentationQuery ( participantPortalClient , sessionId || "" ) ;
60- const { data : profile } = Common . Hooks . BackendParticipantPortalAPI . useSignedInUserQuery ( participantPortalClient ) ;
61- const [ editorState , setEditorState ] = React . useState < SessionEditorState > ( {
62- openSubmitConfirmDialog : false ,
63- ...( session || DummySessionInfo ) ,
64- } ) ;
57+ type SessionEditorFormState = SessionSchema ;
6558
66- if ( ! sessionId || ! session || ! profile || ! ( R . isArray ( editorState . speakers ) && ! R . isEmpty ( editorState . speakers ) ) )
67- return < Navigate to = "/" replace /> ;
59+ export const SessionEditorForm : React . FC < SessionEditorFormProps > = ( { disabled, language, defaultValue, showSubmitButton, onSubmit } ) => {
60+ const [ formState , setFormState ] = React . useState < SessionEditorFormState > ( defaultValue ) ;
61+
62+ const setTitle = ( value : string | undefined , lang : "ko" | "en" ) => setFormState ( ( ps ) => ( { ...ps , [ `title_${ lang } ` ] : value } ) ) ;
63+ const setSummary = ( value : string | undefined , lang : "ko" | "en" ) => setFormState ( ( ps ) => ( { ...ps , [ `summary_${ lang } ` ] : value } ) ) ;
64+ const setDescription = ( value : string | undefined , lang : "ko" | "en" ) => setFormState ( ( ps ) => ( { ...ps , [ `description_${ lang } ` ] : value } ) ) ;
65+ const setImage = ( image : string | null ) => setFormState ( ( ps ) => ( { ...ps , image } ) ) ;
66+ const setSpeakerImage = ( image : string | null ) => setFormState ( ( ps ) => ( { ...ps , speakers : [ { ...speaker , image } ] } ) ) ;
67+ const setSpeakerBiography = ( value : string | undefined , lang : "ko" | "en" ) =>
68+ setFormState ( ( ps ) => ( { ...ps , speakers : [ { ...speaker , [ `biography_${ lang } ` ] : value } ] } ) ) ;
69+ const onImageSelectChange = ( e : SelectChangeEvent < string | null > ) => setImage ( e . target . value ) ;
70+ const onSpeakerImageSelectChange = ( e : SelectChangeEvent < string | null > ) => setSpeakerImage ( e . target . value ) ;
71+
72+ const onSubmitButtonClick = ( ) => onSubmit ?.( formState ) ;
6873
6974 // 유저는 하나의 세션에 발표자가 한번만 가능하고, 백엔드에서 본 유저의 세션 발표자 정보만 제공하므로, 첫 번째 발표자 정보를 사용해도 안전합니다.
70- const speaker = editorState . speakers [ 0 ] ;
75+ const speaker = formState . speakers [ 0 ] ;
7176
7277 const titleStr = language === "ko" ? "발표 정보 수정" : "Edit Session Information" ;
73- const submitStr = language === "ko" ? "제출" : "Submit" ;
7478 const sessionEditDescription =
7579 language === "ko" ? (
7680 < Typography variant = "body2" color = "textSecondary" >
@@ -106,6 +110,99 @@ const InnerSessionEditor: React.FC = () => {
106110 ) ;
107111 const sessionImageStr = language === "ko" ? "발표 이미지" : "Session Image" ;
108112 const speakerImageStr = language === "ko" ? "발표자 이미지" : "Speaker Image" ;
113+ const submitStr = language === "ko" ? "제출" : "Submit" ;
114+
115+ return (
116+ < >
117+ < PrimaryTitle variant = "h4" children = { titleStr } />
118+ < Box sx = { { width : "100%" , mb : 2 , textAlign : "start" } } children = { sessionEditDescription } />
119+ < Stack spacing = { 2 } sx = { { width : "100%" } } >
120+ < MultiLanguageField
121+ label = { { ko : "발표 제목" , en : "Session Title" } }
122+ value = { { ko : formState . title_ko , en : formState . title_en } }
123+ onChange = { setTitle }
124+ disabled = { disabled }
125+ fullWidth
126+ />
127+ < MultiLanguageField
128+ label = { { ko : "발표 요약" , en : "Session Summary" } }
129+ description = { { ko : "발표를 짧게 요약해주세요." , en : "Please enter the short session summary." } }
130+ value = { { ko : formState . summary_ko , en : formState . summary_en } }
131+ onChange = { setSummary }
132+ disabled = { disabled }
133+ multiline
134+ rows = { 4 }
135+ fullWidth
136+ />
137+ < MultiLanguageMarkdownField
138+ label = { { ko : "발표 내용" , en : "Session Description" } }
139+ description = { {
140+ ko : "발표의 상세 내용을 입력해주세요.\n상세 설명은 마크다운 문법을 지원합니다." ,
141+ en : "Please enter the description of the session.\nDetailed descriptions support Markdown syntax." ,
142+ } }
143+ value = { { ko : formState . description_ko , en : formState . description_en } }
144+ disabled = { disabled }
145+ onChange = { setDescription }
146+ />
147+ < PublicFileSelector label = { sessionImageStr } value = { formState . image } disabled = { disabled } onChange = { onImageSelectChange } />
148+ < Divider />
149+
150+ < SecondaryTitle variant = "h5" children = { titleStrForSpeaker } />
151+ < Box sx = { { width : "100%" , mb : 2 , textAlign : "start" } } children = { speakerEditDescription } />
152+ < MultiLanguageField
153+ label = { { ko : "발표자 별칭" , en : "Speaker Nickname" } }
154+ description = { {
155+ ko : (
156+ < Stack spacing = { 1 } >
157+ < Typography variant = "body2" color = "textSecondary" children = "발표자 별칭은 프로필 편집에서 변경할 수 있어요." />
158+ < Link to = "/user" children = { < Button size = "small" variant = "contained" children = "프로필 수정 페이지로 이동" /> } />
159+ </ Stack >
160+ ) ,
161+ en : (
162+ < Stack spacing = { 1 } >
163+ < Typography variant = "body2" color = "textSecondary" children = "You can change speaker nickname in the profile editor." />
164+ < Link to = "/user" children = { < Button size = "small" variant = "contained" children = "Go to Profile Editor" /> } />
165+ </ Stack >
166+ ) ,
167+ } }
168+ // value={{ ko: speaker.user.nickname_ko || "", en: speaker.user.nickname_en || "" }}
169+ disabled
170+ fullWidth
171+ />
172+ < MultiLanguageMarkdownField
173+ label = { { ko : "발표자 소개" , en : "Speaker Biography" } }
174+ value = { { ko : speaker . biography_ko || "" , en : speaker . biography_en || "" } }
175+ onChange = { setSpeakerBiography }
176+ disabled = { disabled }
177+ description = { {
178+ ko : "본인의 소개를 입력해주세요.\n본인 소개는 마크다운 문법을 지원합니다." ,
179+ en : "Please enter your biography.\nBiographies support Markdown syntax." ,
180+ } }
181+ />
182+ < PublicFileSelector label = { speakerImageStr } value = { speaker . image } disabled = { disabled } onChange = { onSpeakerImageSelectChange } />
183+ { showSubmitButton && (
184+ < Button variant = "contained" startIcon = { < SendAndArchive /> } onClick = { onSubmitButtonClick } disabled = { disabled } children = { submitStr } />
185+ ) }
186+ </ Stack >
187+ </ >
188+ ) ;
189+ } ;
190+
191+ type SessionEditorState = {
192+ openSubmitConfirmDialog : boolean ;
193+ formData ?: SessionUpdateSchema ;
194+ } ;
195+
196+ const InnerSessionEditor : React . FC = ( ) => {
197+ const { sessionId } = useParams < { sessionId ?: string } > ( ) ;
198+ const { language } = useAppContext ( ) ;
199+ const [ editorState , setEditorState ] = React . useState < SessionEditorState > ( { openSubmitConfirmDialog : false } ) ;
200+ const participantPortalClient = Common . Hooks . BackendParticipantPortalAPI . useParticipantPortalClient ( ) ;
201+ const updateSessionMutation = Common . Hooks . BackendParticipantPortalAPI . useUpdatePresentationMutation ( participantPortalClient ) ;
202+ const { data : session } = Common . Hooks . BackendParticipantPortalAPI . useRetrievePresentationQuery ( participantPortalClient , sessionId || "" ) ;
203+
204+ if ( ! sessionId || ! session ) return < Navigate to = "/" replace /> ;
205+
109206 const submitSucceedStr =
110207 language === "ko"
111208 ? "발표 정보 수정을 요청했어요. 검토 후 반영될 예정이에요."
@@ -114,48 +211,26 @@ const InnerSessionEditor: React.FC = () => {
114211 const addSnackbar = ( c : string | React . ReactNode , variant : OptionsObject [ "variant" ] ) =>
115212 enqueueSnackbar ( c , { variant, anchorOrigin : { vertical : "bottom" , horizontal : "center" } } ) ;
116213
117- const openSubmitConfirmDialog = ( ) => setEditorState ( ( ps ) => ( { ...ps , openSubmitConfirmDialog : true } ) ) ;
214+ const openSubmitConfirmDialog = ( formData : SessionUpdateSchema ) => setEditorState ( ( ps ) => ( { ...ps , openSubmitConfirmDialog : true , formData } ) ) ;
118215 const closeSubmitConfirmDialog = ( ) => setEditorState ( ( ps ) => ( { ...ps , openSubmitConfirmDialog : false } ) ) ;
119216
120- const setTitle = ( value : string | undefined , lang : "ko" | "en" ) => setEditorState ( ( ps ) => ( { ...ps , [ `title_${ lang } ` ] : value } ) ) ;
121- const setSummary = ( value : string | undefined , lang : "ko" | "en" ) => setEditorState ( ( ps ) => ( { ...ps , [ `summary_${ lang } ` ] : value } ) ) ;
122- const setDescription = ( value : string | undefined , lang : "ko" | "en" ) => setEditorState ( ( ps ) => ( { ...ps , [ `description_${ lang } ` ] : value } ) ) ;
123- const setImage = ( image : string | null ) => setEditorState ( ( ps ) => ( { ...ps , image } ) ) ;
124- const setSpeakerImage = ( image : string | null ) => setEditorState ( ( ps ) => ( { ...ps , speakers : [ { ...speaker , image } ] } ) ) ;
125- const setSpeakerBiography = ( value : string | undefined , lang : "ko" | "en" ) =>
126- setEditorState ( ( ps ) => ( { ...ps , speakers : [ { ...speaker , [ `biography_${ lang } ` ] : value } ] } ) ) ;
217+ const updateSession = ( ) => {
218+ if ( ! editorState . formData ) return ;
127219
128- const onImageSelectChange = ( e : SelectChangeEvent < string | null > ) => setImage ( e . target . value ) ;
129- const onSpeakerImageSelectChange = ( e : SelectChangeEvent < string | null > ) => setSpeakerImage ( e . target . value ) ;
220+ updateSessionMutation . mutate ( editorState . formData , {
221+ onSuccess : ( ) => {
222+ addSnackbar ( submitSucceedStr , "success" ) ;
223+ closeSubmitConfirmDialog ( ) ;
224+ } ,
225+ onError : ( error ) => {
226+ console . error ( "Updating session failed:" , error ) ;
130227
131- const updateSession = ( ) => {
132- updateSessionMutation . mutate (
133- {
134- id : sessionId ,
135- title_ko : editorState . title_ko ,
136- title_en : editorState . title_en ,
137- summary_ko : editorState . summary_ko ,
138- summary_en : editorState . summary_en ,
139- description_ko : editorState . description_ko ,
140- description_en : editorState . description_en ,
141- image : editorState . image || null ,
142- speakers : editorState . speakers ,
228+ let errorMessage = error instanceof Error ? error . message : "An unknown error occurred." ;
229+ if ( error instanceof Common . BackendAPIs . BackendAPIClientError ) errorMessage = error . message ;
230+
231+ addSnackbar ( errorMessage , "error" ) ;
143232 } ,
144- {
145- onSuccess : ( ) => {
146- addSnackbar ( submitSucceedStr , "success" ) ;
147- closeSubmitConfirmDialog ( ) ;
148- } ,
149- onError : ( error ) => {
150- console . error ( "Updating session failed:" , error ) ;
151-
152- let errorMessage = error instanceof Error ? error . message : "An unknown error occurred." ;
153- if ( error instanceof Common . BackendAPIs . BackendAPIClientError ) errorMessage = error . message ;
154-
155- addSnackbar ( errorMessage , "error" ) ;
156- } ,
157- }
158- ) ;
233+ } ) ;
159234 } ;
160235
161236 const modificationAuditId = session . requested_modification_audit_id || "" ;
@@ -166,84 +241,7 @@ const InnerSessionEditor: React.FC = () => {
166241 < SubmitConfirmDialog open = { editorState . openSubmitConfirmDialog } onClose = { closeSubmitConfirmDialog } onSubmit = { updateSession } />
167242 < Page >
168243 { session . has_requested_modification_audit && < CurrentlyModAuditInProgress language = { language } modificationAuditId = { modificationAuditId } /> }
169- < PrimaryTitle variant = "h4" children = { titleStr } />
170- < Box sx = { { width : "100%" , mb : 2 , textAlign : "start" } } children = { sessionEditDescription } />
171- < Stack spacing = { 2 } sx = { { width : "100%" } } >
172- < MultiLanguageField
173- label = { { ko : "발표 제목" , en : "Session Title" } }
174- value = { { ko : editorState . title_ko , en : editorState . title_en } }
175- onChange = { setTitle }
176- disabled = { formDisabled }
177- fullWidth
178- />
179- < MultiLanguageField
180- label = { { ko : "발표 요약" , en : "Session Summary" } }
181- description = { { ko : "발표를 짧게 요약해주세요." , en : "Please enter the short session summary." } }
182- value = { { ko : editorState . summary_ko , en : editorState . summary_en } }
183- onChange = { setSummary }
184- disabled = { formDisabled }
185- multiline
186- rows = { 4 }
187- fullWidth
188- />
189- < MultiLanguageMarkdownField
190- label = { { ko : "발표 내용" , en : "Session Description" } }
191- description = { {
192- ko : "발표의 상세 내용을 입력해주세요.\n상세 설명은 마크다운 문법을 지원합니다." ,
193- en : "Please enter the description of the session.\nDetailed descriptions support Markdown syntax." ,
194- } }
195- value = { { ko : editorState . description_ko , en : editorState . description_en } }
196- disabled = { formDisabled }
197- onChange = { setDescription }
198- />
199- < PublicFileSelector label = { sessionImageStr } value = { editorState . image } disabled = { formDisabled } onChange = { onImageSelectChange } />
200- < Divider />
201-
202- < SecondaryTitle variant = "h5" children = { titleStrForSpeaker } />
203- < Box sx = { { width : "100%" , mb : 2 , textAlign : "start" } } children = { speakerEditDescription } />
204- < MultiLanguageField
205- label = { { ko : "발표자 별칭" , en : "Speaker Nickname" } }
206- description = { {
207- ko : (
208- < Stack spacing = { 1 } >
209- < Typography variant = "body2" color = "textSecondary" children = "발표자 별칭은 프로필 편집에서 변경할 수 있어요." />
210- < Link to = "/user" children = { < Button size = "small" variant = "contained" children = "프로필 수정 페이지로 이동" /> } />
211- </ Stack >
212- ) ,
213- en : (
214- < Stack spacing = { 1 } >
215- < Typography variant = "body2" color = "textSecondary" children = "You can change speaker nickname in the profile editor." />
216- < Link to = "/user" children = { < Button size = "small" variant = "contained" children = "Go to Profile Editor" /> } />
217- </ Stack >
218- ) ,
219- } }
220- value = { { ko : profile . nickname_ko || "" , en : profile . nickname_en || "" } }
221- disabled
222- fullWidth
223- />
224- < MultiLanguageMarkdownField
225- label = { { ko : "발표자 소개" , en : "Speaker Biography" } }
226- value = { { ko : speaker . biography_ko || "" , en : speaker . biography_en || "" } }
227- onChange = { setSpeakerBiography }
228- disabled = { formDisabled }
229- description = { {
230- ko : "본인의 소개를 입력해주세요.\n본인 소개는 마크다운 문법을 지원합니다." ,
231- en : "Please enter your biography.\nBiographies support Markdown syntax." ,
232- } }
233- />
234- < PublicFileSelector label = { speakerImageStr } value = { speaker . image } disabled = { formDisabled } onChange = { onSpeakerImageSelectChange } />
235-
236- < Stack >
237- < Button
238- variant = "contained"
239- startIcon = { < SendAndArchive /> }
240- onClick = { openSubmitConfirmDialog }
241- loading = { updateSessionMutation . isPending }
242- disabled = { formDisabled }
243- children = { submitStr }
244- />
245- </ Stack >
246- </ Stack >
244+ < SessionEditorForm disabled = { formDisabled } language = { language } defaultValue = { session } onSubmit = { openSubmitConfirmDialog } showSubmitButton />
247245 </ Page >
248246 </ >
249247 ) ;
0 commit comments