Skip to content

Commit 190bdc1

Browse files
committed
feat: 참가자 포탈 추가 구현
1 parent 7a3f1ed commit 190bdc1

File tree

12 files changed

+321
-37
lines changed

12 files changed

+321
-37
lines changed

apps/pyconkr-participant-portal/index.html

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@
3838
<meta name="googlebot" content="index, follow" />
3939
<meta name="robots" content="index, follow" />
4040

41-
<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=d3945eccce7debf0942f885e90a71f97"></script>
42-
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>
43-
4441
<title>PyCon Korea Participant Portal</title>
4542
</head>
4643
<body>

apps/pyconkr-participant-portal/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const App: React.FC = () => (
1515
<Route path="/signin" element={<SignInPage />} />
1616
<Route path="/user" element={<ProfileEditor />} />
1717
<Route path="/sponsor/:id" element={<SponsorEditor />} />
18-
<Route path="/session/:id" element={<SessionEditor />} />
18+
<Route path="/session/:sessionId" element={<SessionEditor />} />
1919
<Route path="*" element={<Navigate to="/" replace />} />
2020
</Route>
2121
</Routes>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as Common from "@frontend/common";
2+
import { Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material";
3+
import { enqueueSnackbar, OptionsObject } from "notistack";
4+
import * as React from "react";
5+
6+
import { useAppContext } from "../../contexts/app_context";
7+
8+
type ModificationAuditCancelConfirmDialogProps = {
9+
modificationAuditId: string;
10+
open: boolean;
11+
onClose: () => void;
12+
};
13+
14+
export const ModificationAuditCancelConfirmDialog: React.FC<ModificationAuditCancelConfirmDialogProps> = ({ open, onClose, modificationAuditId }) => {
15+
const reasonInputRef = React.useRef<HTMLInputElement>(null);
16+
const { language } = useAppContext();
17+
const participantPortalClient = Common.Hooks.BackendParticipantPortalAPI.useParticipantPortalClient();
18+
const cancelModificationAuditMutation = Common.Hooks.BackendParticipantPortalAPI.useCancelModificationAuditMutation(participantPortalClient);
19+
20+
const addSnackbar = (c: string | React.ReactNode, variant: OptionsObject["variant"]) =>
21+
enqueueSnackbar(c, { variant, anchorOrigin: { vertical: "bottom", horizontal: "center" } });
22+
23+
const titleStr = language === "ko" ? "수정 요청 철회 확인" : "Confirm Withdrawal of Modification Request";
24+
const content =
25+
language === "ko" ? (
26+
<Typography variant="body1" gutterBottom>
27+
제출하신 수정 요청을 철회하시겠습니까?
28+
<br />
29+
철회 후에는 다시 수정 요청을 하셔야 합니다.
30+
<br />
31+
계속하시려면 <Chip label="철회" color="error" size="small" /> 버튼을 클릭해 주세요.
32+
</Typography>
33+
) : (
34+
<Typography>
35+
Are you sure you want to withdraw your modification request?
36+
<br />
37+
After withdrawal, you will need to submit a new modification request.
38+
<br />
39+
To continue, please click the <Chip label="Cancel" color="error" size="small" /> button below.
40+
</Typography>
41+
);
42+
// const reasonStr = language === "ko" ? "철회 사유 (선택)" : "Reason for Withdrawal (Optional)";
43+
const submitStr = language === "ko" ? "철회" : "Withdraw Request";
44+
const cancelStr = language === "ko" ? "취소" : "Cancel";
45+
const successStr = language === "ko" ? "수정 요청이 철회되었습니다." : "Modification request has been canceled.";
46+
47+
const onClick = () => {
48+
cancelModificationAuditMutation.mutate(
49+
{
50+
id: modificationAuditId,
51+
reason: reasonInputRef.current?.value || "",
52+
},
53+
{
54+
onSuccess: () => {
55+
addSnackbar(successStr, "success");
56+
onClose();
57+
},
58+
onError: (error) => {
59+
console.error("Canceling ModAudit failed:", error);
60+
61+
let errorMessage = error instanceof Error ? error.message : "An unknown error occurred.";
62+
if (error instanceof Common.BackendAPIs.BackendAPIClientError) errorMessage = error.message;
63+
64+
addSnackbar(errorMessage, "error");
65+
},
66+
}
67+
);
68+
};
69+
70+
const disabled = cancelModificationAuditMutation.isPending;
71+
72+
return (
73+
<Dialog open={open} maxWidth="sm" fullWidth>
74+
<DialogTitle children={titleStr} />
75+
<DialogContent>
76+
{content}
77+
{/* <br />
78+
<TextField ref={reasonInputRef} disabled={disabled} label={reasonStr} fullWidth variant="filled" /> */}
79+
</DialogContent>
80+
<DialogActions>
81+
<Button disabled={disabled} onClick={onClose} color="error" children={cancelStr} />
82+
<Button disabled={disabled} onClick={onClick} color="error" children={submitStr} variant="contained" />
83+
</DialogActions>
84+
</Dialog>
85+
);
86+
};

apps/pyconkr-participant-portal/src/components/elements/multilang_field.tsx

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const ButtonWidth: React.CSSProperties["width"] = "4.5rem";
1111

1212
const FieldContainer = styled(Stack)(({ theme }) => ({
1313
flexDirection: "row",
14-
alignItems: "flex-start",
14+
alignItems: "stretch",
1515

1616
[theme.breakpoints.down("sm")]: {
1717
flexDirection: "column",
@@ -115,12 +115,13 @@ type MultiLanguageMarkdownFieldProps = {
115115
onChange?: (value: string | undefined, language: "ko" | "en") => void;
116116
} & MultiLanguageCommonProps;
117117

118-
const MDRendererContainer = styled(Box)(({ theme }) => ({
119-
width: "50%",
120-
maxWidth: "50%",
118+
const MDRendererContainer = styled(Box)<{ fullWidth?: boolean }>(({ theme, fullWidth }) => ({
119+
width: fullWidth ? "100%" : "50%",
120+
maxWidth: fullWidth ? "100%" : "50%",
121121
backgroundColor: "#fff",
122122

123123
"& .markdown-body": {
124+
padding: theme.spacing(1, 2),
124125
width: "100%",
125126
p: { margin: theme.spacing(2, 0) },
126127
},
@@ -132,6 +133,7 @@ export const MultiLanguageMarkdownField: React.FC<MultiLanguageMarkdownFieldProp
132133
defaultValue,
133134
value,
134135
onChange,
136+
disabled,
135137
...props
136138
}) => {
137139
const { language } = useAppContext();
@@ -156,17 +158,19 @@ export const MultiLanguageMarkdownField: React.FC<MultiLanguageMarkdownFieldProp
156158
<SmallTab value="ko" sx={{ textTransform: "none" }} label={koreanStr} />
157159
<SmallTab value="en" sx={{ textTransform: "none" }} label={englishStr} />
158160
</SmallTabs>
159-
<Stack direction="row" spacing={2} sx={{ width: "100%", height: "100%", minHeight: "100%", maxHeight: "100%", flexGrow: 1 }}>
160-
<Box sx={{ width: "50%", maxWidth: "50%" }}>
161-
<Common.Components.MarkdownEditor
162-
defaultValue={inputDefaultValue}
163-
value={inputValue}
164-
onChange={handleChange}
165-
extraCommands={[]}
166-
{...props}
167-
/>
168-
</Box>
169-
<MDRendererContainer>
161+
<Stack direction="row" spacing={2} sx={{ width: "100%", flexGrow: 1 }}>
162+
{!disabled && (
163+
<Box sx={{ width: "50%", maxWidth: "50%" }}>
164+
<Common.Components.MarkdownEditor
165+
defaultValue={inputDefaultValue}
166+
value={inputValue}
167+
onChange={handleChange}
168+
extraCommands={[]}
169+
{...props}
170+
/>
171+
</Box>
172+
)}
173+
<MDRendererContainer fullWidth={disabled}>
170174
<Common.Components.MDXRenderer text={inputValue || ""} format="md" />
171175
</MDRendererContainer>
172176
</Stack>

apps/pyconkr-participant-portal/src/components/elements/public_file_selector.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export const PublicFileSelector: React.FC<PublicFileSelectorProps> = ErrorBounda
5555
<Button
5656
variant="contained"
5757
size="small"
58+
disabled={props.disabled}
5859
onClick={openUploadDialog}
5960
startIcon={<PermMedia />}
6061
fullWidth={isMobile}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Button, Card, CardContent, Stack, styled, Typography } from "@mui/material";
2+
import * as React from "react";
3+
4+
import { ModificationAuditCancelConfirmDialog } from "../dialogs/modification_audit_cancel_confirm";
5+
6+
const StyledAlertCard = styled(Card)(({ theme }) => ({
7+
width: "100%",
8+
backgroundColor: theme.palette.info.light,
9+
color: theme.palette.info.contrastText,
10+
marginBottom: theme.spacing(2),
11+
fontWeight: 500,
12+
}));
13+
14+
export const CurrentlyModAuditInProgress: React.FC<{ language: "ko" | "en"; modificationAuditId: string }> = ({ language, modificationAuditId }) => {
15+
const [cardState, setCardState] = React.useState<{ openCancelConfirmDialog: boolean }>({ openCancelConfirmDialog: false });
16+
const openCancelConfirmDialog = () => setCardState((ps) => ({ ...ps, openCancelConfirmDialog: true }));
17+
const closeCancelConfirmDialog = () => setCardState((ps) => ({ ...ps, openCancelConfirmDialog: false }));
18+
19+
const cancelModAuditStr = language === "ko" ? "수정 요청 취소하기" : "Cancel Request";
20+
const sessionModAuditInProgress =
21+
language === "ko" ? (
22+
<Typography variant="body1" sx={{ flexGrow: 1 }}>
23+
현재 정보 수정 요청이 진행 중이에요. 수정 요청이 완료되기 전까지는 정보를 변경할 수 없어요.
24+
<br />
25+
만약 수정할 내용이 있다면, 수정 요청을 취소하고 다시 수정 요청을 해주세요.
26+
</Typography>
27+
) : (
28+
<Typography variant="body1" sx={{ flexGrow: 1 }}>
29+
A modification request is currently in progress.
30+
<br />
31+
You cannot change the information until the modification request is completed.
32+
<br />
33+
If you have changes to make, please cancel the modification request and submit a new one.
34+
</Typography>
35+
);
36+
37+
return (
38+
<>
39+
<ModificationAuditCancelConfirmDialog
40+
open={cardState.openCancelConfirmDialog}
41+
onClose={closeCancelConfirmDialog}
42+
modificationAuditId={modificationAuditId}
43+
/>
44+
<StyledAlertCard>
45+
<CardContent>
46+
<Stack direction="row">
47+
{sessionModAuditInProgress}
48+
<Button variant="outlined" color="inherit" onClick={openCancelConfirmDialog} children={cancelModAuditStr} />
49+
</Stack>
50+
</CardContent>
51+
</StyledAlertCard>
52+
</>
53+
);
54+
};

apps/pyconkr-participant-portal/src/components/pages/home.tsx

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const InnerLandingPage: React.FC = () => {
6969
const isMobile = useMediaQuery((theme) => theme.breakpoints.down("sm"));
7070
const participantPortalAPIClient = Common.Hooks.BackendParticipantPortalAPI.useParticipantPortalClient();
7171
const { data: profile } = Common.Hooks.BackendParticipantPortalAPI.useSignedInUserQuery(participantPortalAPIClient);
72+
const { data: modificationAudits } = Common.Hooks.BackendParticipantPortalAPI.useModificationAuditsQuery(participantPortalAPIClient);
7273
const { data: sessions } = Common.Hooks.BackendParticipantPortalAPI.useListPresentationsQuery(participantPortalAPIClient);
7374

7475
if (!profile) {
@@ -90,6 +91,7 @@ const InnerLandingPage: React.FC = () => {
9091
const nickNameStr = language === "ko" ? `별칭 : ${profile.nickname}` : `Nickname : ${profile.nickname}`;
9192
const emailStr = language === "ko" ? `이메일 : ${profile.email}` : `Email : ${profile.email}`;
9293
const editProfileStr = language === "ko" ? "프로필 수정" : "Edit Profile";
94+
const auditEmptyStr = language === "ko" ? "수정 요청이 없습니다." : "No audit requests.";
9395

9496
return (
9597
<Page>
@@ -115,18 +117,33 @@ const InnerLandingPage: React.FC = () => {
115117
<FieldsetContainer>
116118
<ProperWidthFieldset legend={auditStr}>
117119
<List>
118-
<ListItem>준비 중입니다.</ListItem>
119-
</List>
120-
</ProperWidthFieldset>
121-
<ProperWidthFieldset legend={sessionsStr}>
122-
<List>
123-
{sessions?.map((s) => (
124-
<ListItem key={s.id} disablePadding sx={{ cursor: "pointer", border: "1px solid #ccc" }}>
125-
<ListItemButton children={<ListItemText primary={s.title} />} onClick={() => navigate(`/session/${s.id}`)} />
120+
{modificationAudits.length > 0 ? (
121+
modificationAudits.map((audit) => (
122+
<ListItem key={audit.id} disablePadding sx={{ cursor: "pointer", border: "1px solid #ccc" }}>
123+
<ListItemButton
124+
children={<ListItemText primary={audit.str_repr} secondary={audit.status} />}
125+
onClick={() => navigate(`/session/${audit.instance_id}/modification-audit/${audit.id}`)}
126+
/>
127+
</ListItem>
128+
))
129+
) : (
130+
<ListItem disablePadding sx={{ cursor: "pointer", border: "1px solid #ccc" }}>
131+
<ListItemButton children={<ListItemText primary={auditEmptyStr} />} />
126132
</ListItem>
127-
))}
133+
)}
128134
</List>
129135
</ProperWidthFieldset>
136+
{sessions && (
137+
<ProperWidthFieldset legend={sessionsStr}>
138+
<List>
139+
{sessions.map((s) => (
140+
<ListItem key={s.id} disablePadding sx={{ cursor: "pointer", border: "1px solid #ccc" }}>
141+
<ListItemButton children={<ListItemText primary={s.title} />} onClick={() => navigate(`/session/${s.id}`)} />
142+
</ListItem>
143+
))}
144+
</List>
145+
</ProperWidthFieldset>
146+
)}
130147
{/* <ProperWidthFieldset legend={sponsorsStr}>
131148
<List></List>
132149
</ProperWidthFieldset> */}

apps/pyconkr-participant-portal/src/components/pages/profile_editor.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { ErrorPage } from "../elements/error_page";
1212
import { LoadingPage } from "../elements/loading_page";
1313
import { MultiLanguageField } from "../elements/multilang_field";
1414
import { PublicFileSelector } from "../elements/public_file_selector";
15+
import { CurrentlyModAuditInProgress } from "../elements/requested_modification_audit_available_header";
1516
import { SignInGuard } from "../elements/signin_guard";
1617
import { PrimaryTitle } from "../elements/titles";
1718
import { Page } from "../page";
@@ -93,11 +94,15 @@ const InnerProfileEditor: React.FC = () => {
9394
);
9495
};
9596

97+
const modificationAuditId = profile?.requested_modification_audit_id || "";
98+
const formDisabled = profile?.has_requested_modification_audit || updateMeMutation.isPending;
99+
96100
return (
97101
<>
98102
<ChangePasswordDialog open={editorState.openChangePasswordDialog} onClose={closeChangePasswordDialog} />
99103
<SubmitConfirmDialog open={editorState.openSubmitConfirmDialog} onClose={closeSubmitConfirmDialog} onSubmit={updateMe} />
100104
<Page>
105+
{profile?.has_requested_modification_audit && <CurrentlyModAuditInProgress language={language} modificationAuditId={modificationAuditId} />}
101106
<PrimaryTitle variant="h4" children={titleStr} />
102107
<Stack spacing={2} sx={{ width: "100%", flexGrow: 1 }}>
103108
<PublicFileSelector label={speakerImageStr} value={editorState.image} onChange={onImageSelectChange} setFileIdAsValue={setImageId} />
@@ -107,6 +112,7 @@ const InnerProfileEditor: React.FC = () => {
107112
ko: editorState.nickname_ko || "",
108113
en: editorState.nickname_en || "",
109114
}}
115+
disabled={formDisabled}
110116
onChange={setNickname}
111117
description={{
112118
ko: "닉네임은 발표자 소개에 사용됩니다. 청중이 기억하기 쉬운 이름을 입력해주세요.",
@@ -116,8 +122,23 @@ const InnerProfileEditor: React.FC = () => {
116122
fullWidth
117123
/>
118124
<Stack direction="row" spacing={2} sx={{ width: "100%" }}>
119-
<Button variant="contained" fullWidth startIcon={<Key />} color="error" onClick={openChangePasswordDialog} children={changePasswordStr} />
120-
<Button variant="contained" fullWidth startIcon={<SendAndArchive />} onClick={openSubmitConfirmDialog} children={submitStr} />
125+
<Button
126+
variant="contained"
127+
fullWidth
128+
startIcon={<Key />}
129+
color="error"
130+
onClick={openChangePasswordDialog}
131+
children={changePasswordStr}
132+
disabled={formDisabled}
133+
/>
134+
<Button
135+
variant="contained"
136+
fullWidth
137+
startIcon={<SendAndArchive />}
138+
onClick={openSubmitConfirmDialog}
139+
children={submitStr}
140+
disabled={formDisabled}
141+
/>
121142
</Stack>
122143
</Stack>
123144
</Page>

0 commit comments

Comments
 (0)