Skip to content

Commit 12b0da1

Browse files
committed
feat: 후원사 상세 페이지 추가
1 parent 331db60 commit 12b0da1

File tree

11 files changed

+167
-59
lines changed

11 files changed

+167
-59
lines changed

apps/pyconkr-admin/src/components/layouts/admin_editor.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,17 @@ const MUIStyledFieldset = styled("fieldset")(({ theme }) => ({
154154
borderRadius: theme.shape.borderRadius,
155155
}));
156156

157+
const MDRendererContainer = styled(Box)(({ theme }) => ({
158+
width: "50%",
159+
maxWidth: "50%",
160+
backgroundColor: "#fff",
161+
162+
"& .markdown-body": {
163+
width: "100%",
164+
p: { margin: theme.spacing(2, 0) },
165+
},
166+
}));
167+
157168
const MDEditorField: Field = ErrorBoundary.with(
158169
{ fallback: Common.Components.ErrorFallback },
159170
({ disabled, formData, name, onChange: rawOnChange }) => {
@@ -169,9 +180,9 @@ const MDEditorField: Field = ErrorBoundary.with(
169180
<Box sx={{ width: "50%", maxWidth: "50%" }}>
170181
<Common.Components.MarkdownEditor disabled={disabled} name={name} value={valueState} onChange={onChange} extraCommands={[]} />
171182
</Box>
172-
<Box sx={{ width: "50%", maxWidth: "50%", backgroundColor: "#fff" }}>
183+
<MDRendererContainer>
173184
<Common.Components.MDXRenderer text={valueState || ""} format="md" />
174-
</Box>
185+
</MDRendererContainer>
175186
</Stack>
176187
</MUIStyledFieldset>
177188
);

apps/pyconkr/src/App.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import * as R from "remeda";
66
import MainLayout from "./components/layout/index.tsx";
77
import { PageIdParamRenderer, RouteRenderer } from "./components/pages/dynamic_route.tsx";
88
import { ShopSignInPage } from "./components/pages/sign_in.tsx";
9+
import { SponsorDetailPage } from "./components/pages/sponsor_detail.tsx";
910
import { Test } from "./components/pages/test.tsx";
1011
import { IS_DEBUG_ENV } from "./consts";
1112
import { useAppContext } from "./contexts/app_context";
1213
import BackendAPISchemas from "../../../packages/common/src/schemas/backendAPI";
1314

1415
export const App: React.FC = () => {
1516
const backendAPIClient = Common.Hooks.BackendAPI.useBackendClient();
17+
const { data: sponsorTiers } = Common.Hooks.BackendAPI.useSponsorQuery(backendAPIClient);
1618
const { data: flatSiteMap } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(backendAPIClient);
1719
const siteMapNode = Common.Utils.buildNestedSiteMap(flatSiteMap)?.[""];
1820

@@ -35,16 +37,17 @@ export const App: React.FC = () => {
3537
}
3638
}
3739

38-
setAppContext((ps) => ({ ...ps, siteMapNode, currentSiteMapDepth }));
40+
setAppContext((ps) => ({ ...ps, siteMapNode, sponsorTiers, currentSiteMapDepth }));
3941
})();
4042
// eslint-disable-next-line react-hooks/exhaustive-deps
41-
}, [location, language, flatSiteMap]);
43+
}, [location, language, flatSiteMap, sponsorTiers]);
4244

4345
return (
4446
<Routes>
4547
<Route element={<MainLayout />}>
4648
{IS_DEBUG_ENV && <Route path="/debug" element={<Test />} />}
4749
<Route path="/account/sign-in" element={<ShopSignInPage />} />
50+
<Route path="/sponsors/:id" element={<SponsorDetailPage />} />
4851
<Route path="/pages/:id" element={<PageIdParamRenderer />} />
4952
<Route path="*" element={<RouteRenderer />} />
5053
</Route>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Stack, styled } from "@mui/material";
2+
3+
export const PageLayout = styled(Stack)(({ theme }) => ({
4+
height: "75%",
5+
width: "100%",
6+
maxWidth: "1200px",
7+
8+
justifyContent: "flex-start",
9+
alignItems: "center",
10+
11+
paddingTop: theme.spacing(8),
12+
paddingBottom: theme.spacing(8),
13+
14+
paddingRight: theme.spacing(16),
15+
paddingLeft: theme.spacing(16),
16+
17+
[theme.breakpoints.down("lg")]: {
18+
padding: theme.spacing(4),
19+
},
20+
[theme.breakpoints.down("sm")]: {
21+
padding: theme.spacing(2),
22+
},
23+
}));

apps/pyconkr/src/components/layout/Sponsor/index.tsx

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as Common from "@frontend/common";
21
import { Badge, CircularProgress, Divider, Stack, Tooltip, Typography, TypographyProps, styled } from "@mui/material";
32
import { ErrorBoundary, Suspense } from "@suspensive/react";
43
import { Link } from "react-router-dom";
@@ -120,14 +119,8 @@ export const Sponsor: React.FC = ErrorBoundary.with(
120119
),
121120
},
122121
Suspense.with({ fallback: <CircularProgress /> }, () => {
123-
const { siteMapNode } = useAppContext();
124-
const backendAPIClient = Common.Hooks.BackendAPI.useBackendClient();
125-
const { data: sponsorData } = Common.Hooks.BackendAPI.useSponsorQuery(backendAPIClient);
126-
127-
if (!siteMapNode) return <CircularProgress />;
128-
129-
const flatSiteMap = Common.Utils.buildFlatSiteMap(siteMapNode);
130-
const flatSiteMapObj = flatSiteMap.reduce((a, i) => ({ ...a, [i.id]: i }), {} as Record<string, { route: string }>);
122+
const { sponsorTiers } = useAppContext();
123+
if (!sponsorTiers) return <CircularProgress />;
131124

132125
const textProps: TypographyProps = {
133126
textAlign: "center",
@@ -139,7 +132,7 @@ export const Sponsor: React.FC = ErrorBoundary.with(
139132
<SponsorSection aria-label="후원사 섹션">
140133
<Typography variant="h4" {...textProps} children="후원사 목록" area-level={4} />
141134
<Stack spacing={4} sx={{ my: 4 }} aria-label="후원사 목록 그리드">
142-
{sponsorData
135+
{sponsorTiers
143136
.filter((t) => t.sponsors.length)
144137
.map((sponsorTier, i, a) => (
145138
<Stack spacing={6} key={sponsorTier.id} aria-label={`후원사 티어: ${sponsorTier.name}`}>
@@ -148,21 +141,22 @@ export const Sponsor: React.FC = ErrorBoundary.with(
148141
{sponsorTier.sponsors.map((sponsor) => {
149142
const sponsorName = sponsor.name.replace(/\\n/g, "\n");
150143
const sponsorNameContent = <Typography variant="body1" {...textProps} children={sponsorName} sx={{ whiteSpace: "pre-wrap" }} />;
151-
const sponsorImg = (
152-
<Tooltip title={sponsorNameContent} arrow placement="top">
153-
<LogoImageEqualWidthContainer>
154-
<LogoBadgeContainer>
155-
{sponsor.tags.map((tag, i) => (
156-
<LogoBadge key={i} badgeContent={tag} />
157-
))}
158-
</LogoBadgeContainer>
159-
<LogoImageContainer>
160-
<LogoImage src={sponsor.logo} alt={sponsor.name} loading="lazy" />
161-
</LogoImageContainer>
162-
</LogoImageEqualWidthContainer>
163-
</Tooltip>
144+
return (
145+
<Link to={`/sponsors/${sponsor.id}`} key={sponsor.id} style={{ textDecoration: "none" }}>
146+
<Tooltip title={sponsorNameContent} arrow placement="top">
147+
<LogoImageEqualWidthContainer>
148+
<LogoBadgeContainer>
149+
{sponsor.tags.map((tag, i) => (
150+
<LogoBadge key={i} badgeContent={tag} />
151+
))}
152+
</LogoBadgeContainer>
153+
<LogoImageContainer>
154+
<LogoImage src={sponsor.logo} alt={sponsor.name} loading="lazy" />
155+
</LogoImageContainer>
156+
</LogoImageEqualWidthContainer>
157+
</Tooltip>
158+
</Link>
164159
);
165-
return sponsor.sitemap_id ? <Link to={flatSiteMapObj[sponsor.sitemap_id].route} children={sponsorImg} /> : sponsorImg;
166160
})}
167161
</SponsorStack>
168162
{i !== a.length - 1 && <Divider sx={{ m: "2rem" }} />}

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

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,13 @@
11
import * as Shop from "@frontend/shop";
22
import { AccountCircleOutlined, Google } from "@mui/icons-material";
3-
import { Backdrop, Button, ButtonProps, CircularProgress, Stack, styled, Typography } from "@mui/material";
3+
import { Backdrop, Button, ButtonProps, CircularProgress, Stack, Typography } from "@mui/material";
44
import { Suspense } from "@suspensive/react";
55
import { enqueueSnackbar, OptionsObject } from "notistack";
66
import * as React from "react";
77
import { useNavigate } from "react-router-dom";
88

99
import { useAppContext } from "../../contexts/app_context";
10-
11-
const SignInPageContainer = styled(Stack)(({ theme }) => ({
12-
height: "75%",
13-
width: "100%",
14-
maxWidth: "1200px",
15-
16-
justifyContent: "flex-start",
17-
alignItems: "center",
18-
19-
paddingTop: theme.spacing(8),
20-
paddingBottom: theme.spacing(8),
21-
22-
paddingRight: theme.spacing(16),
23-
paddingLeft: theme.spacing(16),
24-
25-
[theme.breakpoints.down("lg")]: {
26-
padding: theme.spacing(4),
27-
},
28-
[theme.breakpoints.down("sm")]: {
29-
padding: theme.spacing(2),
30-
},
31-
}));
10+
import { PageLayout } from "../layout/PageLayout";
3211

3312
type PageeStateType = {
3413
openBackdrop: boolean;
@@ -110,14 +89,14 @@ export const ShopSignInPage: React.FC = Suspense.with({ fallback: <CircularProgr
11089

11190
return (
11291
<>
113-
<SignInPageContainer spacing={6}>
92+
<PageLayout spacing={6}>
11493
<Typography variant="h4" sx={{ textAlign: "center", fontWeight: "bolder" }} children={signInTitleStr} />
11594
<Stack spacing={1} sx={{ width: "100%", maxWidth: "400px" }}>
11695
{btnProps.map((props, index) => (
11796
<Button key={index} {...commonBtnProps} {...props} />
11897
))}
11998
</Stack>
120-
</SignInPageContainer>
99+
</PageLayout>
121100
<Backdrop sx={({ zIndex }) => ({ zIndex: zIndex.drawer + 1 })} open={shouldOpenBackdrop} onClick={() => {}} />
122101
</>
123102
);
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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 { useParams } from "react-router-dom";
6+
import * as R from "remeda";
7+
8+
import BackendAPISchemas from "../../../../../packages/common/src/schemas/backendAPI";
9+
import { useAppContext } from "../../contexts/app_context";
10+
import { PageLayout } from "../layout/PageLayout";
11+
12+
const PageNotFound: React.FC = () => <>404 Not Found</>;
13+
const CenteredLoadingPage: React.FC = () => (
14+
<Common.Components.CenteredPage>
15+
<CircularProgress />
16+
</Common.Components.CenteredPage>
17+
);
18+
19+
const LogoImage = styled("img")(({ theme }) => ({
20+
maxWidth: "20rem",
21+
height: "auto",
22+
23+
padding: theme.spacing(8, 0),
24+
marginBottom: theme.spacing(4),
25+
26+
[theme.breakpoints.down("lg")]: {
27+
padding: theme.spacing(4),
28+
},
29+
[theme.breakpoints.down("sm")]: {
30+
padding: theme.spacing(2),
31+
},
32+
}));
33+
34+
const DescriptionBox = styled(Box)(({ theme }) => ({
35+
width: "100%",
36+
padding: theme.spacing(2, 4),
37+
38+
[theme.breakpoints.down("lg")]: {
39+
padding: theme.spacing(2),
40+
},
41+
[theme.breakpoints.down("sm")]: {
42+
padding: theme.spacing(1),
43+
},
44+
45+
"& .markdown-body": {
46+
width: "100%",
47+
p: { margin: theme.spacing(2, 0) },
48+
},
49+
}));
50+
51+
export const SponsorDetailPage: React.FC = ErrorBoundary.with(
52+
{ fallback: Common.Components.ErrorFallback },
53+
Suspense.with({ fallback: <CenteredLoadingPage /> }, () => {
54+
const { id } = useParams();
55+
const { language, sponsorTiers, setAppContext } = useAppContext();
56+
const sponsors = sponsorTiers?.reduce((acc, tier) => [...acc, ...tier.sponsors], [] as BackendAPISchemas.SponsorTierSchema["sponsors"]);
57+
const sponsor = sponsors?.find((s) => s.id === id);
58+
59+
const title = language === "ko" ? "후원사" : "Sponsor";
60+
const descriptionFallback = language === "ko" ? "해당 후원사의 설명은 준비 중이에요!" : "This sponsor's description is under preparation!";
61+
62+
React.useEffect(() => {
63+
setAppContext((prev) => ({
64+
...prev,
65+
title: `${title} - ${sponsor?.name || "Detail"}`,
66+
shouldShowTitleBanner: true,
67+
shouldShowSponsorBanner: !R.isNonNullish(sponsor),
68+
}));
69+
}, [sponsor, title, setAppContext]);
70+
71+
if (!id || !sponsorTiers) return <CenteredLoadingPage />;
72+
if (!sponsor) return <PageNotFound />;
73+
74+
return (
75+
<PageLayout sx={{ maxWidth: "960px" }}>
76+
<LogoImage src={sponsor.logo} alt={sponsor.name} loading="lazy" />
77+
<Divider flexItem />
78+
<Typography variant="h4" fontWeight="700" textAlign="start" sx={{ width: "100%", p: 2 }}>
79+
{sponsor.name.replace(/\\n/g, "\n")}
80+
{sponsor.tags.length ? (
81+
<Stack direction="row" spacing={1} sx={{ width: "100%", mt: 1 }} aria-label="후원사 태그 목록">
82+
{sponsor.tags.map((tag) => (
83+
<Chip key={tag} size="small" variant="outlined" color="primary" label={tag} />
84+
))}
85+
</Stack>
86+
) : null}
87+
</Typography>
88+
<Divider flexItem />
89+
<DescriptionBox>
90+
<Common.Components.MDXRenderer text={sponsor.description || descriptionFallback} format="md" />
91+
</DescriptionBox>
92+
</PageLayout>
93+
);
94+
})
95+
);

apps/pyconkr/src/contexts/app_context.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export type AppContextType = {
1010
shouldShowSponsorBanner: boolean;
1111

1212
siteMapNode?: BackendAPISchemas.NestedSiteMapSchema;
13-
sponsors: unknown;
13+
sponsorTiers?: BackendAPISchemas.SponsorTierSchema[];
1414
title: string;
1515
currentSiteMapDepth: (BackendAPISchemas.NestedSiteMapSchema | undefined)[];
1616

apps/pyconkr/src/main.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ const MainApp: React.FC = () => {
7373

7474
currentSiteMapDepth: [],
7575

76-
sponsors: null,
7776
title: "PyCon Korea 2025",
7877
});
7978

packages/common/src/apis/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace BackendAPIs {
55
export const BackendAPIClientError = _BackendAPIClientError;
66
export const listSiteMaps = (client: BackendAPIClient) => () => client.get<BackendAPISchemas.FlattenedSiteMapSchema[]>("v1/cms/sitemap/");
77
export const retrievePage = (client: BackendAPIClient) => (id: string) => client.get<BackendAPISchemas.PageSchema>(`v1/cms/page/${id}/`);
8-
export const listSponsors = (client: BackendAPIClient) => () => client.get<BackendAPISchemas.SponsorSchema[]>("v1/event/sponsor/");
8+
export const listSponsors = (client: BackendAPIClient) => () => client.get<BackendAPISchemas.SponsorTierSchema[]>("v1/event/sponsor/");
99
}
1010

1111
export default BackendAPIs;

packages/common/src/components/mdx.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ export const MDXRenderer: React.FC<MDXRendererPropType> = ({ text, resetKey, for
108108
try {
109109
// 원래 MDX는 각 줄의 마지막에 공백 2개가 있어야 줄바꿈이 되고, 또 연속 줄바꿈은 무시되지만,
110110
// 편의성을 위해 렌더러 단에서 공백 2개를 추가하고 연속 줄바꿈을 <br />로 변환합니다.
111-
const processedText = text.split("\n").map(lineFormatterForMDX).join("").replaceAll("\n\n", "\n<br />\n");
111+
let processedText = text.split("\n").map(lineFormatterForMDX).join("");
112+
113+
if (format === "md") processedText = processedText.replaceAll(/<br\s*\/?>/g, "\n");
114+
else processedText = processedText.replaceAll("\n\n", "\n<br />\n");
115+
112116
const { default: RenderResult } = await evaluate(processedText, {
113117
...runtime,
114118
...provider,

0 commit comments

Comments
 (0)