Skip to content

Commit 50de9be

Browse files
committed
feat: 발표 상세 페이지 초안 추가
1 parent 2449b9f commit 50de9be

File tree

5 files changed

+116
-19
lines changed

5 files changed

+116
-19
lines changed

apps/pyconkr/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as R from "remeda";
55

66
import MainLayout from "./components/layout/index.tsx";
77
import { PageIdParamRenderer, RouteRenderer } from "./components/pages/dynamic_route.tsx";
8+
import { PresentationDetailPage } from "./components/pages/presentation_detail.tsx";
89
import { ShopSignInPage } from "./components/pages/sign_in.tsx";
910
import { SponsorDetailPage } from "./components/pages/sponsor_detail.tsx";
1011
import { Test } from "./components/pages/test.tsx";
@@ -48,6 +49,7 @@ export const App: React.FC = () => {
4849
{IS_DEBUG_ENV && <Route path="/debug" element={<Test />} />}
4950
<Route path="/account/sign-in" element={<ShopSignInPage />} />
5051
<Route path="/sponsors/:id" element={<SponsorDetailPage />} />
52+
<Route path="/presentations/:id" element={<PresentationDetailPage />} />
5153
<Route path="/pages/:id" element={<PageIdParamRenderer />} />
5254
<Route path="*" element={<RouteRenderer />} />
5355
</Route>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as Common from "@frontend/common";
2+
import { Box, Chip, CircularProgress, Divider, Stack, styled, Typography } from "@mui/material";
3+
import { ErrorBoundary, Suspense } from "@suspensive/react";
4+
import * as React from "react";
5+
import { Navigate, useParams } from "react-router-dom";
6+
7+
import { useAppContext } from "../../contexts/app_context";
8+
import { PageLayout } from "../layout/PageLayout";
9+
10+
const CenteredLoadingPage: React.FC = () => (
11+
<Common.Components.CenteredPage>
12+
<CircularProgress />
13+
</Common.Components.CenteredPage>
14+
);
15+
16+
const DescriptionBox = styled(Box)(({ theme }) => ({
17+
width: "100%",
18+
padding: theme.spacing(2, 4),
19+
20+
[theme.breakpoints.down("lg")]: {
21+
padding: theme.spacing(2),
22+
},
23+
[theme.breakpoints.down("sm")]: {
24+
padding: theme.spacing(1),
25+
},
26+
27+
"& .markdown-body": {
28+
width: "100%",
29+
p: { margin: theme.spacing(2, 0) },
30+
},
31+
}));
32+
33+
export const PresentationDetailPage: React.FC = ErrorBoundary.with(
34+
{ fallback: Common.Components.ErrorFallback },
35+
Suspense.with({ fallback: <CenteredLoadingPage /> }, () => {
36+
const { id } = useParams();
37+
const { language, setAppContext } = useAppContext();
38+
const backendClient = Common.Hooks.BackendAPI.useBackendClient();
39+
const { data: presentation } = Common.Hooks.BackendAPI.useSessionQuery(backendClient, id || "");
40+
41+
if (!id || !presentation) return <Navigate to="/" replace />;
42+
43+
const descriptionFallback = language === "ko" ? "해당 발표의 설명은 준비 중이에요!" : "Description of the presentation is under preparation!";
44+
const categoriesStr = language === "ko" ? "카테고리" : "Categories";
45+
46+
React.useEffect(() => {
47+
setAppContext((prev) => ({
48+
...prev,
49+
title: language === "ko" ? "발표 상세" : "Presentation Detail",
50+
shouldShowTitleBanner: true,
51+
shouldShowSponsorBanner: true,
52+
}));
53+
}, [language, presentation, setAppContext]);
54+
55+
return (
56+
<PageLayout sx={{ maxWidth: "960px" }}>
57+
<Typography variant="h4" fontWeight="700" textAlign="start" sx={{ width: "100%", p: 2 }}>
58+
{presentation.title}
59+
</Typography>
60+
<Divider flexItem />
61+
{presentation.categories.length ? (
62+
<>
63+
<Stack direction="row" alignItems="center" justifyContent="flex-start" sx={{ width: "100%", gap: 1, p: 2 }}>
64+
<Typography variant="subtitle1" fontWeight="bold" children={categoriesStr} />
65+
<Stack direction="row" spacing={1} sx={{ width: "100%" }}>
66+
{presentation.categories.map((c) => (
67+
<Chip key={c.id} size="small" variant="outlined" color="primary" label={c.name} />
68+
))}
69+
</Stack>
70+
</Stack>
71+
<Divider flexItem />
72+
</>
73+
) : null}
74+
<DescriptionBox>
75+
<Common.Components.MDXRenderer text={presentation.description || descriptionFallback} format="md" />
76+
</DescriptionBox>
77+
</PageLayout>
78+
);
79+
})
80+
);

packages/common/src/apis/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ namespace BackendAPIs {
88
export const listSponsors = (client: BackendAPIClient) => () => client.get<BackendAPISchemas.SponsorTierSchema[]>("v1/event/sponsor/");
99
export const listSessions = (client: BackendAPIClient, params?: BackendAPISchemas.SessionQueryParameterSchema) => () =>
1010
client.get<BackendAPISchemas.SessionSchema[]>("v1/event/presentation/", { params });
11+
export const retrieveSession = (client: BackendAPIClient) => (id: string) => {
12+
if (!id) return Promise.resolve(null);
13+
return client.get<BackendAPISchemas.SessionSchema>(`v1/event/presentation/${id}/`);
14+
};
1115
}
1216

1317
export default BackendAPIs;

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

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Box, Button, Chip, CircularProgress, Stack, styled, Typography } from "@mui/material";
22
import { ErrorBoundary, Suspense } from "@suspensive/react";
33
import * as React from "react";
4+
import { Link } from "react-router-dom";
45
import * as R from "remeda";
56

67
import PyCon2025Logo from "../../assets/pyconkr2025_logo.png";
@@ -10,18 +11,17 @@ import { ErrorFallback } from "../error_handler";
1011
import { FallbackImage } from "../fallback_image";
1112
import { StyledDivider } from "./styled_divider";
1213

13-
const SessionItem: React.FC<{ session: BackendAPISchemas.SessionSchema }> = Suspense.with({ fallback: <CircularProgress /> }, ({ session }) => {
14-
const sessionTitle = session.title.replace("\\n", "\n");
15-
const speakerImgSrc = session.image || (R.isArray(session.speakers) && !R.isEmpty(session.speakers) && session.speakers[0].image) || "";
16-
// const urlSafeTitle = session.title
17-
// .replace(/ /g, "-")
18-
// .replace(/([.])/g, "_")
19-
// .replace(/(?![0-9A-Za-zㄱ-ㅣ가-힣-_])./g, "");
20-
// const sessionDetailedUrl = `/session/${session.id}#${urlSafeTitle}`;
21-
22-
return (
23-
<>
24-
{/* <Link to={sessionDetailedUrl} style={{ textDecoration: "none" }}> */}
14+
const SessionItem: React.FC<{ session: BackendAPISchemas.SessionSchema; enableLink?: boolean }> = Suspense.with(
15+
{ fallback: <CircularProgress /> },
16+
({ session, enableLink }) => {
17+
const sessionTitle = session.title.replace("\\n", "\n");
18+
const speakerImgSrc = session.image || (R.isArray(session.speakers) && !R.isEmpty(session.speakers) && session.speakers[0].image) || "";
19+
const urlSafeTitle = session.title
20+
.replace(/ /g, "-")
21+
.replace(/([.])/g, "_")
22+
.replace(/(?![0-9A-Za-z---_])./g, "");
23+
const sessionDetailedUrl = `/presentations/${session.id}#${urlSafeTitle}`;
24+
const result = (
2525
<SessionItemContainer direction="row">
2626
<SessionImageContainer
2727
children={<SessionImage src={speakerImgSrc} alt="Session Image" loading="lazy" errorFallback={<SessionImageErrorFallback />} />}
@@ -40,20 +40,25 @@ const SessionItem: React.FC<{ session: BackendAPISchemas.SessionSchema }> = Susp
4040
</Stack>
4141
</Stack>
4242
</SessionItemContainer>
43-
{/* </Link> */}
44-
<StyledDivider />
45-
</>
46-
);
47-
});
43+
);
44+
return (
45+
<>
46+
{enableLink ? <Link to={sessionDetailedUrl} style={{ textDecoration: "none" }} children={result} /> : result}
47+
<StyledDivider />
48+
</>
49+
);
50+
}
51+
);
4852

4953
type SessionListPropType = {
5054
event?: string;
5155
types?: string | string[];
56+
enableLink?: boolean;
5257
};
5358

5459
export const SessionList: React.FC<SessionListPropType> = ErrorBoundary.with(
5560
{ fallback: ErrorFallback },
56-
Suspense.with({ fallback: <CircularProgress /> }, ({ event, types }) => {
61+
Suspense.with({ fallback: <CircularProgress /> }, ({ event, types, enableLink }) => {
5762
const { language } = Hooks.Common.useCommonContext();
5863
const backendAPIClient = Hooks.BackendAPI.useBackendClient();
5964
const params = { ...(event && { event }), ...(types && { types: R.isString(types) ? types : types.join(",") }) };
@@ -103,7 +108,7 @@ export const SessionList: React.FC<SessionListPropType> = ErrorBoundary.with(
103108
)}
104109
</Box>
105110
{filteredSessions.map((s) => (
106-
<SessionItem key={s.id} session={s} />
111+
<SessionItem key={s.id} session={s} enableLink={enableLink} />
107112
))}
108113
</Box>
109114
);

packages/common/src/hooks/useAPI.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ namespace BackendAPIHooks {
4848
queryKey: [...QUERY_KEYS.SESSION_LIST, client.language, ...(params ? [JSON.stringify(params)] : [])],
4949
queryFn: BackendAPIs.listSessions(client, params),
5050
});
51+
52+
export const useSessionQuery = (client: BackendAPIClient, id: string) =>
53+
useSuspenseQuery({
54+
queryKey: [...QUERY_KEYS.SESSION_LIST, id, client.language],
55+
queryFn: () => BackendAPIs.retrieveSession(client)(id),
56+
});
5157
}
5258

5359
export default BackendAPIHooks;

0 commit comments

Comments
 (0)