Skip to content

Commit 89de8e8

Browse files
committed
feat: 발표 상세 페이지 구현
1 parent 9626058 commit 89de8e8

File tree

4 files changed

+137
-5
lines changed

4 files changed

+137
-5
lines changed
809 KB
Loading

apps/pyconkr/src/components/pages/presentation_detail.tsx

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,38 @@ import { ErrorBoundary, Suspense } from "@suspensive/react";
44
import * as React from "react";
55
import { Navigate, useParams } from "react-router-dom";
66

7+
import PyCon2025Logo from "../../assets/pyconkr2025_logo.png";
78
import { useAppContext } from "../../contexts/app_context";
89
import { PageLayout } from "../layout/PageLayout";
910

11+
const PROFILE_IMAGE_SIZE = "7rem";
12+
13+
type SimplifiedSpeakerSchema = {
14+
id: string;
15+
nickname: string;
16+
image: string | null;
17+
biography: string;
18+
};
19+
1020
const CenteredLoadingPage: React.FC = () => (
1121
<Common.Components.CenteredPage>
1222
<CircularProgress />
1323
</Common.Components.CenteredPage>
1424
);
1525

26+
const StyledPresentationImage = styled(Common.Components.FallbackImage)(({ theme }) => ({
27+
maxWidth: "75%",
28+
maxHeight: "480px",
29+
aspectRatio: "1",
30+
margin: theme.spacing(4, 0),
31+
borderRadius: "2rem",
32+
border: `1px solid ${theme.palette.divider}`,
33+
34+
[theme.breakpoints.down("lg")]: {
35+
maxWidth: "100%",
36+
},
37+
}));
38+
1639
const DescriptionBox = styled(Box)(({ theme }) => ({
1740
width: "100%",
1841
padding: theme.spacing(2, 4),
@@ -30,6 +53,84 @@ const DescriptionBox = styled(Box)(({ theme }) => ({
3053
},
3154
}));
3255

56+
const BiographyBox = styled(Box)(({ theme }) => ({
57+
width: "100%",
58+
59+
"& .markdown-body": {
60+
width: "100%",
61+
p: { margin: theme.spacing(2, 0) },
62+
},
63+
}));
64+
65+
const ProfileImageContainer = styled(Stack)({
66+
minWidth: PROFILE_IMAGE_SIZE,
67+
width: PROFILE_IMAGE_SIZE,
68+
maxWidth: PROFILE_IMAGE_SIZE,
69+
minHeight: PROFILE_IMAGE_SIZE,
70+
height: PROFILE_IMAGE_SIZE,
71+
maxHeight: PROFILE_IMAGE_SIZE,
72+
overflow: "hidden",
73+
borderRadius: "50%",
74+
border: `1px solid rgba(0, 0, 0, 0.12)`,
75+
});
76+
77+
const ProfileImageStyle: React.CSSProperties = {
78+
width: "100%",
79+
height: "100%",
80+
objectFit: "cover",
81+
};
82+
83+
const ProfileImage = styled(Common.Components.FallbackImage)(ProfileImageStyle);
84+
85+
const ProfileImageErrorFallback: React.FC = () => (
86+
<Stack alignItems="center" justifyContent="center" sx={{ ...ProfileImageStyle }}>
87+
<img src={PyCon2025Logo} alt="PyCon 2025 Logo" style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" }} />
88+
</Stack>
89+
);
90+
91+
const PresentationSpeakerItem: React.FC<{ speaker: SimplifiedSpeakerSchema }> = ({ speaker }) => {
92+
return (
93+
<>
94+
<Stack direction="row" spacing={4} sx={{ px: 2, py: 1 }}>
95+
<ProfileImageContainer sx={{ flexGrow: 0 }}>
96+
<ProfileImage alt="Speaker Image" src={speaker.image || ""} errorFallback={<ProfileImageErrorFallback />} />
97+
</ProfileImageContainer>
98+
<Stack alignItems="flex-start" justifyContent="center" sx={{ flexGrow: 1 }}>
99+
<Typography variant="h4" fontWeight="700" fontSize="2rem" children={speaker.nickname} />
100+
{speaker.biography ? (
101+
<BiographyBox children={<Common.Components.MDXRenderer text={speaker.biography || ""} format="md" />} />
102+
) : (
103+
<>
104+
<br />
105+
<br />
106+
</>
107+
)}
108+
</Stack>
109+
</Stack>
110+
<Divider flexItem />
111+
</>
112+
);
113+
};
114+
115+
const PresentationImageFallback: React.FC<{ language: "ko" | "en" }> = ({ language }) => {
116+
const message =
117+
language === "ko" ? (
118+
<>
119+
지금은 발표 사진을 불러올 수 없어요
120+
<br />
121+
잠시 후 다시 시도해 주세요.
122+
</>
123+
) : (
124+
<>
125+
Unable to load the presentation image at the moment.
126+
<br />
127+
Please try again later.
128+
</>
129+
);
130+
131+
return <Typography variant="caption" color="textSecondary" children={message} />;
132+
};
133+
33134
export const PresentationDetailPage: React.FC = ErrorBoundary.with(
34135
{ fallback: Common.Components.ErrorFallback },
35136
Suspense.with({ fallback: <CenteredLoadingPage /> }, () => {
@@ -42,6 +143,7 @@ export const PresentationDetailPage: React.FC = ErrorBoundary.with(
42143

43144
const descriptionFallback = language === "ko" ? "해당 발표의 설명은 준비 중이에요!" : "Description of the presentation is under preparation!";
44145
const categoriesStr = language === "ko" ? "카테고리" : "Categories";
146+
const speakersStr = language === "ko" ? "발표자" : "Speakers";
45147

46148
React.useEffect(() => {
47149
setAppContext((prev) => ({
@@ -53,10 +155,11 @@ export const PresentationDetailPage: React.FC = ErrorBoundary.with(
53155
}, [language, presentation, setAppContext]);
54156

55157
return (
56-
<PageLayout sx={{ maxWidth: "960px" }}>
57-
<Typography variant="h4" fontWeight="700" textAlign="start" sx={{ width: "100%", p: 2 }}>
58-
{presentation.title}
59-
</Typography>
158+
<PageLayout>
159+
<Typography variant="h4" fontWeight="700" textAlign="start" sx={{ width: "100%", px: 2, pt: 0, pb: 1 }} children={presentation.title} />
160+
{presentation.summary && (
161+
<Typography variant="h6" fontWeight="700" textAlign="start" sx={{ width: "100%", px: 2, pt: 1, pb: 3 }} children={presentation.summary} />
162+
)}
60163
<Divider flexItem />
61164
{presentation.categories.length ? (
62165
<>
@@ -71,9 +174,27 @@ export const PresentationDetailPage: React.FC = ErrorBoundary.with(
71174
<Divider flexItem />
72175
</>
73176
) : null}
177+
{presentation.image && (
178+
<StyledPresentationImage
179+
alt="Presentation Image"
180+
src={presentation.image}
181+
errorFallback={<PresentationImageFallback language={language} />}
182+
/>
183+
)}
74184
<DescriptionBox>
75185
<Common.Components.MDXRenderer text={presentation.description || descriptionFallback} format="md" />
76186
</DescriptionBox>
187+
<Divider flexItem />
188+
{presentation.speakers && (
189+
<>
190+
<Typography variant="h5" fontWeight="bold" sx={{ width: "100%", px: 2, py: 4 }} children={speakersStr} />
191+
<Stack spacing={2} sx={{ width: "100%", px: 3 }}>
192+
{presentation.speakers.map((speaker) => (
193+
<PresentationSpeakerItem key={speaker.id} speaker={speaker as SimplifiedSpeakerSchema} />
194+
))}
195+
</Stack>
196+
</>
197+
)}
77198
</PageLayout>
78199
);
79200
})

packages/common/src/components/mdx_components/session_list.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,17 @@ const SessionItem: React.FC<{ session: BackendAPISchemas.SessionSchema; enableLi
1515
{ fallback: <CircularProgress /> },
1616
({ session, enableLink }) => {
1717
const sessionTitle = session.title.replace("\\n", "\n");
18-
const speakerImgSrc = session.image || (R.isArray(session.speakers) && !R.isEmpty(session.speakers) && session.speakers[0].image) || "";
18+
19+
let speakerImgSrc = session.image || "";
20+
if (!speakerImgSrc && R.isArray(session.speakers) && !R.isEmpty(session.speakers)) {
21+
for (const speaker of session.speakers) {
22+
if (speaker.image) {
23+
speakerImgSrc = speaker.image;
24+
break;
25+
}
26+
}
27+
}
28+
1929
const urlSafeTitle = session.title
2030
.replace(/ /g, "-")
2131
.replace(/([.])/g, "_")

packages/common/src/schemas/backendAPI.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ namespace BackendAPISchemas {
7878
export type SessionSchema = {
7979
id: string;
8080
title: string;
81+
summary: string | null;
8182
description: string;
8283
image: string | null;
8384
categories: {

0 commit comments

Comments
 (0)