Skip to content

feat: 참가자 포탈 App 추가 #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,24 @@ jobs:
BUMP_RULE: ${{ (github.event_name == 'workflow_dispatch' && inputs.WORKFLOW_PHASE || 'dev') == 'dev' && '--mode development' || '' }}
AWS_S3_PYCONKR_FRONTEND_BUCKET: ${{ (github.event_name == 'workflow_dispatch' && inputs.WORKFLOW_PHASE || 'dev') == 'dev' && secrets.AWS_S3_PYCONKR_FRONTEND_BUCKET_DEV || secrets.AWS_S3_PYCONKR_FRONTEND_BUCKET_PROD }}
AWS_S3_PYCONKR_ADMIN_BUCKET: ${{ (github.event_name == 'workflow_dispatch' && inputs.WORKFLOW_PHASE || 'dev') == 'dev' && secrets.AWS_S3_PYCONKR_ADMIN_BUCKET_DEV || secrets.AWS_S3_PYCONKR_ADMIN_BUCKET_PROD }}
AWS_S3_PYCONKR_PARTICIPANT_PORTAL_BUCKET: ${{ (github.event_name == 'workflow_dispatch' && inputs.WORKFLOW_PHASE || 'dev') == 'dev' && secrets.AWS_S3_PYCONKR_PARTICIPANT_PORTAL_BUCKET_DEV || secrets.AWS_S3_PYCONKR_PARTICIPANT_PORTAL_BUCKET_PROD }}
AWS_CLOUDFRONT_PYCONKR_FRONTEND_DISTRIBUTION_ID: ${{ (github.event_name == 'workflow_dispatch' && inputs.WORKFLOW_PHASE || 'dev') == 'dev' && secrets.AWS_CLOUDFRONT_PYCONKR_FRONTEND_DISTRIBUTION_ID_DEV || secrets.AWS_CLOUDFRONT_PYCONKR_FRONTEND_DISTRIBUTION_ID_PROD }}
AWS_CLOUDFRONT_PYCONKR_ADMIN_DISTRIBUTION_ID: ${{ (github.event_name == 'workflow_dispatch' && inputs.WORKFLOW_PHASE || 'dev') == 'dev' && secrets.AWS_CLOUDFRONT_PYCONKR_ADMIN_DISTRIBUTION_ID_DEV || secrets.AWS_CLOUDFRONT_PYCONKR_ADMIN_DISTRIBUTION_ID_PROD }}
AWS_CLOUDFRONT_PYCONKR_PARTICIPANT_PORTAL_DISTRIBUTION_ID: ${{ (github.event_name == 'workflow_dispatch' && inputs.WORKFLOW_PHASE || 'dev') == 'dev' && secrets.AWS_CLOUDFRONT_PYCONKR_PARTICIPANT_PORTAL_DISTRIBUTION_ID_DEV || secrets.AWS_CLOUDFRONT_PYCONKR_PARTICIPANT_PORTAL_DISTRIBUTION_ID_PROD }}

strategy:
matrix:
application: [pyconkr, pyconkr-admin]
application: [pyconkr, pyconkr-admin, pyconkr-participant-portal]
include:
- application: pyconkr
aws_s3_bucket_key: AWS_S3_PYCONKR_FRONTEND_BUCKET
aws_cloudfront_distribution_key: AWS_CLOUDFRONT_PYCONKR_FRONTEND_DISTRIBUTION_ID
- application: pyconkr-admin
aws_s3_bucket_key: AWS_S3_PYCONKR_ADMIN_BUCKET
aws_cloudfront_distribution_key: AWS_CLOUDFRONT_PYCONKR_ADMIN_DISTRIBUTION_ID
- application: pyconkr-participant-portal
aws_s3_bucket_key: AWS_S3_PYCONKR_PARTICIPANT_PORTAL_BUCKET
aws_cloudfront_distribution_key: AWS_CLOUDFRONT_PYCONKR_PARTICIPANT_PORTAL_DISTRIBUTION_ID

steps:
- uses: actions/checkout@master
Expand Down
50 changes: 50 additions & 0 deletions apps/pyconkr-participant-portal/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<meta charSet="UTF-8" />
<base href="/" />
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/favicon-180.png">

<meta name="theme-color" content="#fff" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#fff" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#fff" />

<meta name="msapplication-navbutton-color" content="#fff" />
<meta name="msapplication-TileColor" content="#fff" />
<meta name="msapplication-TileImage" content="/favicon-192.png" />
<meta name="application-name" content="PyCon KR" />
<meta name="apple-mobile-web-app-title" content="PyCon KR" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<!-- https://developers.google.com/web/fundamentals/web-app-manifest/ -->
<link rel="manifest" href="/site.webmanifest" />

<meta name="viewport" content="width=device-width,
height=device-height,
target-densitydpi=device-dpi,
initial-scale=1.0,
minimum-scale=1.0,
maximum-scale=1.0,
user-scalable=0,
user-scalable=no,
shrink-to-fit=no" />
<meta name="author" content="PyCon Korea Organizing Team" />
<meta name="description" content="PyCon Korea Participant Portal" />
<meta name="keywords" content="PyCon, Python, Conference, Korea" />
<meta name="google" content="notranslate" />
<meta name="googlebot" content="index, follow" />
<meta name="robots" content="index, follow" />

<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=d3945eccce7debf0942f885e90a71f97"></script>
<script src="https://cdn.iamport.kr/v1/iamport.js"></script>

<title>PyCon Korea Participant Portal</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/main.tsx"></script>
</body>
</html>
13 changes: 13 additions & 0 deletions apps/pyconkr-participant-portal/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "@apps/pyconkr-participant-portal",
"dependencies": {
"@frontend/common": "workspace:*",
"@frontend/shop": "workspace:*"
},
"devDependencies": {
"vite": "^6.3.5",
"vite-plugin-mdx": "^3.6.1",
"vite-plugin-mkcert": "^1.17.8",
"vite-plugin-svgr": "^4.3.0"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
1 change: 1 addition & 0 deletions apps/pyconkr-participant-portal/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions apps/pyconkr-participant-portal/public/site.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "PyCon Korea Participant Portal",
"icons": [
{
"src": "favicon-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "favicon-512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
},
{
"src": "favicon-512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"id": "/",
"start_url": "/",
"scope": "/",
"display": "standalone"
}
22 changes: 22 additions & 0 deletions apps/pyconkr-participant-portal/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from "react";
import { Navigate, Route, Routes } from "react-router-dom";

import { Layout } from "./components/layout.tsx";
import { LandingPage } from "./components/pages/home.tsx";
import { ProfileEditor } from "./components/pages/profile_editor.tsx";
import { SessionEditor } from "./components/pages/session_editor";
import { SignInPage } from "./components/pages/signin.tsx";
import { SponsorEditor } from "./components/pages/sponsor_editor";

export const App: React.FC = () => (
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<LandingPage />} />
<Route path="/signin" element={<SignInPage />} />
<Route path="/user" element={<ProfileEditor />} />
<Route path="/sponsor/:id" element={<SponsorEditor />} />
<Route path="/session/:id" element={<SessionEditor />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as Common from "@frontend/common";
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField } from "@mui/material";
import { enqueueSnackbar, OptionsObject } from "notistack";
import * as React from "react";

import { useAppContext } from "../../contexts/app_context";

type ChangePasswordDialogProps = {
open: boolean;
onClose: () => void;
};

type PasswordFormDataType = {
old_password: string;
new_password: string;
new_password_confirm: string;
};

export const ChangePasswordDialog: React.FC<ChangePasswordDialogProps> = ({ open, onClose }) => {
const formRef = React.useRef<HTMLFormElement>(null);
const { language } = useAppContext();
const participantPortalClient = Common.Hooks.BackendParticipantPortalAPI.useParticipantPortalClient();
const changePasswordMutation = Common.Hooks.BackendParticipantPortalAPI.useChangePasswordMutation(participantPortalClient);

const addSnackbar = (c: string | React.ReactNode, variant: OptionsObject["variant"]) =>
enqueueSnackbar(c, { variant, anchorOrigin: { vertical: "bottom", horizontal: "center" } });

const titleStr = language === "ko" ? "비밀번호 변경" : "Change Password";
const prevPasswordLabel = language === "ko" ? "이전 비밀번호" : "Previous Password";
const newPasswordLabel = language === "ko" ? "새 비밀번호" : "New Password";
const confirmPasswordLabel = language === "ko" ? "새 비밀번호 확인" : "Confirm New Password";
const cancelStr = language === "ko" ? "취소" : "Cancel";
const submitStr = language === "ko" ? "수정" : "Apply changes";
const passwordChangedStr = language === "ko" ? "비밀번호가 성공적으로 변경되었습니다." : "Password changed successfully.";

const handleSubmit = () => {
if (!Common.Utils.isFormValid(formRef.current)) return;

const formData = Common.Utils.getFormValue<PasswordFormDataType>({ form: formRef.current });
changePasswordMutation.mutate(formData, {
onSuccess: () => {
addSnackbar(passwordChangedStr, "success");
onClose();
},
onError: (error) => {
console.error("Change password failed:", error);

let errorMessage = error instanceof Error ? error.message : "An unknown error occurred.";
if (error instanceof Common.BackendAPIs.BackendAPIClientError) errorMessage = error.message;

addSnackbar(errorMessage, "error");
},
});
};

const disabled = changePasswordMutation.isPending;

return (
<Dialog open={open} maxWidth="sm" fullWidth>
<DialogTitle children={titleStr} />
<DialogContent>
<form ref={formRef}>
<Stack spacing={2} sx={{ my: 1 }}>
<TextField fullWidth disabled={disabled} type="password" name="old_password" label={prevPasswordLabel} />
<TextField fullWidth disabled={disabled} type="password" name="new_password" label={newPasswordLabel} />
<TextField fullWidth disabled={disabled} type="password" name="new_password_confirm" label={confirmPasswordLabel} />
</Stack>
</form>
</DialogContent>
<DialogActions>
<Button loading={disabled} onClick={onClose} color="error" children={cancelStr} />
<Button loading={disabled} onClick={handleSubmit} color="primary" variant="contained" children={submitStr} />
</DialogActions>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as Common from "@frontend/common";
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material";
import { enqueueSnackbar, OptionsObject } from "notistack";
import * as React from "react";

import { useAppContext } from "../../contexts/app_context";

type SetUploadedFileAsValueConfirmDialogProps = {
language: "ko" | "en";
open: boolean;
closeAll: () => void;
setValueAndCloseAll: () => void;
};

const SetUploadedFileAsValueConfirmDialog: React.FC<SetUploadedFileAsValueConfirmDialogProps> = ({
language,
open,
closeAll,
setValueAndCloseAll,
}) => {
const titleStr = language === "ko" ? "파일 업로드 완료" : "File Upload Completed";
const confirmStr =
language === "ko" ? "업로드한 파일을 현재 값으로 설정하시겠습니까?" : "Do you want to set the uploaded file as the current value?";
const yesStr = language === "ko" ? "네" : "Yes";
const noStr = language === "ko" ? "아니요" : "No";

return (
<Dialog open={open} maxWidth="xs" fullWidth>
<DialogTitle children={titleStr} />
<DialogContent children={confirmStr} />
<DialogActions>
<Button variant="contained" onClick={closeAll} color="error" children={noStr} />
<Button variant="contained" onClick={setValueAndCloseAll} children={yesStr} />
</DialogActions>
</Dialog>
);
};

type PublicFileUploadDialogProps = {
open: boolean;
onClose: () => void;
setFileIdAsValue?: (fileId: string | undefined) => void;
};

type PublicFileUploadDialogState = {
selectedFile?: File | null;
uploadedFileId?: string;
openSetValueDialog?: boolean;
};

export const PublicFileUploadDialog: React.FC<PublicFileUploadDialogProps> = ({ open, onClose, setFileIdAsValue }) => {
const { language } = useAppContext();
const [dialogState, setDialogState] = React.useState<PublicFileUploadDialogState>({});
const participantPortalClient = Common.Hooks.BackendParticipantPortalAPI.useParticipantPortalClient();
const uploadPublicFileMutation = Common.Hooks.BackendParticipantPortalAPI.useUploadPublicFileMutation(participantPortalClient);

const addSnackbar = (c: string | React.ReactNode, variant: OptionsObject["variant"]) =>
enqueueSnackbar(c, { variant, anchorOrigin: { vertical: "bottom", horizontal: "center" } });

const titleStr = language === "ko" ? "파일 업로드" : "Upload File";
const cancelStr = language === "ko" ? "취소" : "Cancel";
const uploadStr = language === "ko" ? "업로드" : "Upload";
const fileNotSelectedStr = language === "ko" ? "파일이 선택되지 않았습니다." : "No file selected.";
const failedToUploadStr = language === "ko" ? "파일 업로드에 실패했습니다." : "Failed to upload file.";
const loading = uploadPublicFileMutation.isPending;

const openSetValueDialog = () => setDialogState((ps) => ({ ...ps, openSetValueDialog: true }));
const cleanUpDialogState = () => setDialogState({});
const setFile = (selectedFile?: File | null) => setDialogState((ps) => ({ ...ps, selectedFile }));
const setFileId = (uploadedFileId?: string) => setDialogState((ps) => ({ ...ps, uploadedFileId }));

const uploadFile = async () => {
if (!dialogState.selectedFile) {
addSnackbar(fileNotSelectedStr, "error");
return;
}

uploadPublicFileMutation.mutate(dialogState.selectedFile, {
onSuccess: (data) => {
setFileId(data.id);
openSetValueDialog();
},
onError: (error) => {
console.error("Uploading file failed:", error);

let errorMessage = error instanceof Error ? error.message : "An unknown error occurred.";
if (error instanceof Common.BackendAPIs.BackendAPIClientError) errorMessage = error.message;

addSnackbar(`${failedToUploadStr}\n${errorMessage}`, "error");
},
});
};
const closeAllDialogs = () => {
cleanUpDialogState();
onClose();
};
const setValueAndCloseAllDialogs = () => {
setFileIdAsValue?.(dialogState.uploadedFileId);
closeAllDialogs();
};

return (
<>
<SetUploadedFileAsValueConfirmDialog
language={language}
open={!!dialogState.openSetValueDialog}
closeAll={closeAllDialogs}
setValueAndCloseAll={setValueAndCloseAllDialogs}
/>
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle children={titleStr} />
<DialogContent>
<Common.Components.DndFileInput onFileChange={setFile} />
</DialogContent>
<DialogActions>
<Button variant="contained" loading={loading} children={cancelStr} color="error" onClick={onClose} />
<Button variant="contained" loading={loading} children={uploadStr} disabled={!dialogState.selectedFile} onClick={uploadFile} />
</DialogActions>
</Dialog>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from "@mui/material";
import * as React from "react";

import { useAppContext } from "../../contexts/app_context";

type SubmitConfirmDialogProps = {
open: boolean;
onClose: () => void;
onSubmit: () => void;
};

export const SubmitConfirmDialog: React.FC<SubmitConfirmDialogProps> = ({ open, onClose, onSubmit }) => {
const { language } = useAppContext();

const titleStr = language === "ko" ? "제출 확인" : "Confirm Submission";
const content =
language === "ko" ? (
<Typography variant="body1" gutterBottom>
제출하시면 파이콘 준비 위원회에서 검토 후 결과를 알려드립니다.
<br />
제출 후에는 수정 심사를 철회 후 다시 수정하셔야 하오니, 내용을 한번 더 확인해 주세요.
<br />
계속하시려면 <Chip label="제출" color="primary" size="small" /> 버튼을 클릭해 주세요.
</Typography>
) : (
<Typography>
Once you submit, the PyCon Korea organizing committee will review your submission and notify you of the results.
<br />
Please double-check your content as you will need to withdraw and resubmit if you wish to make changes after submission.
<br />
To continue, please click the <Chip label="Submit" color="primary" size="small" /> button below.
</Typography>
);
const submitStr = language === "ko" ? "제출" : "Submit";
const cancelStr = language === "ko" ? "취소" : "Cancel";

return (
<Dialog open={open} maxWidth="sm" fullWidth>
<DialogTitle children={titleStr} />
<DialogContent children={content} />
<DialogActions>
<Button onClick={onClose} color="error" children={cancelStr} />
<Button onClick={onSubmit} color="primary" variant="contained" children={submitStr} />
</DialogActions>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { styled } from "@mui/material";

export const BlockQuote = styled("blockquote")(({ theme }) => ({
margin: 0,
paddingLeft: theme.spacing(1.5),
borderLeft: `4px solid ${theme.palette.grey[700]}`,
color: theme.palette.text.secondary,
}));
Loading