Skip to content

Commit 5a6e580

Browse files
committed
chore: AppContext 추가
1 parent 71cee4a commit 5a6e580

File tree

9 files changed

+256
-227
lines changed

9 files changed

+256
-227
lines changed

apps/pyconkr-admin/src/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const queryClient = new QueryClient({
3232

3333
const CommonOptions: Common.Contexts.ContextOptions = {
3434
debug: true,
35+
language: "ko",
3536
baseUrl: ".",
3637
frontendDomain: import.meta.env.VITE_PYCONKR_FRONTEND_DOMAIN,
3738
backendApiDomain: import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN,

apps/pyconkr/src/App.tsx

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,48 @@
11
import * as Common from "@frontend/common";
2-
import React from "react";
3-
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";
2+
import * as React from "react";
3+
import { Route, Routes, useLocation } from "react-router-dom";
4+
import * as R from "remeda";
45

5-
import MainLayout from "./components/layout";
6-
import { Test } from "./components/pages/test";
7-
import { IS_DEBUG_ENV } from "./consts/index.ts";
8-
import { SponsorProvider } from "./contexts/SponsorContext";
6+
import MainLayout from "./components/layout/index.tsx";
7+
import { PageIdParamRenderer, RouteRenderer } from "./components/pages/dynamic_route.tsx";
8+
import { Test } from "./components/pages/test.tsx";
9+
import { IS_DEBUG_ENV } from "./consts";
10+
import { useAppContext } from "./contexts/app_context";
11+
import BackendAPISchemas from "../../../packages/common/src/schemas/backendAPI";
912

10-
// 스폰서를 표시할 페이지 경로 설정
11-
const SPONSOR_VISIBLE_PATHS = ["/"];
13+
export const App: React.FC = () => {
14+
const backendAPIClient = Common.Hooks.BackendAPI.useBackendClient();
15+
const { data: flatSiteMap } = Common.Hooks.BackendAPI.useFlattenSiteMapQuery(backendAPIClient);
16+
const siteMapNode = Common.Utils.buildNestedSiteMap(flatSiteMap)?.[""];
1217

13-
const AppContent = () => {
1418
const location = useLocation();
15-
const shouldShowSponsor = SPONSOR_VISIBLE_PATHS.includes(location.pathname);
19+
const { setAppContext, language } = useAppContext();
1620

17-
return (
18-
<SponsorProvider initialVisibility={shouldShowSponsor}>
19-
<Routes>
20-
<Route element={<MainLayout />}>
21-
{IS_DEBUG_ENV && <Route path="/debug" element={<Test />} />}
22-
<Route path="/pages/:id" element={<Common.Components.PageIdParamRenderer />} />
23-
<Route path="*" element={<Common.Components.RouteRenderer />} />
24-
</Route>
25-
</Routes>
26-
</SponsorProvider>
27-
);
28-
};
21+
React.useEffect(() => {
22+
(async () => {
23+
const currentRouteCodes = ["", ...location.pathname.split("/").filter((code) => !R.isEmpty(code))];
24+
const currentSiteMapDepth: (BackendAPISchemas.NestedSiteMapSchema | undefined)[] = [siteMapNode];
25+
26+
for (const routeCode of currentRouteCodes.splice(1)) {
27+
currentSiteMapDepth.push(currentSiteMapDepth.at(-1)?.children[routeCode]);
28+
if (R.isNullish(currentSiteMapDepth.at(-1))) {
29+
console.warn(`Route not found in site map: ${routeCode}`);
30+
break;
31+
}
32+
}
33+
34+
setAppContext((ps) => ({ ...ps, siteMapNode, currentSiteMapDepth }));
35+
})();
36+
// eslint-disable-next-line react-hooks/exhaustive-deps
37+
}, [location, language, flatSiteMap]);
2938

30-
export const App: React.FC = () => {
3139
return (
32-
<BrowserRouter>
33-
<AppContent />
34-
</BrowserRouter>
40+
<Routes>
41+
<Route element={<MainLayout />}>
42+
{IS_DEBUG_ENV && <Route path="/debug" element={<Test />} />}
43+
<Route path="/pages/:id" element={<PageIdParamRenderer />} />
44+
<Route path="*" element={<RouteRenderer />} />
45+
</Route>
46+
</Routes>
3547
);
3648
};
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import * as Common from "@frontend/common";
2+
import { CircularProgress, Stack, Theme } from "@mui/material";
3+
import { ErrorBoundary, Suspense } from "@suspensive/react";
4+
import { AxiosError, AxiosResponse } from "axios";
5+
import * as React from "react";
6+
import { useLocation, useParams } from "react-router-dom";
7+
import * as R from "remeda";
8+
9+
import { useAppContext } from "../../contexts/app_context";
10+
11+
const initialPageStyle: (additionalStyle: React.CSSProperties) => (theme: Theme) => React.CSSProperties =
12+
(additionalStyle) => (theme) => ({
13+
width: "100%",
14+
display: "flex",
15+
justifyContent: "flex-start",
16+
alignItems: "center",
17+
flexDirection: "column",
18+
19+
marginTop: theme.spacing(4),
20+
21+
...(!R.isEmpty(additionalStyle)
22+
? additionalStyle
23+
: {
24+
[theme.breakpoints.down("lg")]: {
25+
marginTop: theme.spacing(2),
26+
},
27+
[theme.breakpoints.down("sm")]: {
28+
marginTop: theme.spacing(1),
29+
},
30+
}),
31+
});
32+
33+
const initialSectionStyle: (additionalStyle: React.CSSProperties) => (theme: Theme) => React.CSSProperties =
34+
(additionalStyle) => (theme) => ({
35+
width: "100%",
36+
maxWidth: "1200px",
37+
display: "flex",
38+
justifyContent: "flex-start",
39+
alignItems: "center",
40+
paddingRight: theme.spacing(16),
41+
paddingLeft: theme.spacing(16),
42+
43+
"& .markdown-body": { width: "100%" },
44+
...(!R.isEmpty(additionalStyle)
45+
? additionalStyle
46+
: {
47+
[theme.breakpoints.down("lg")]: {
48+
paddingRight: theme.spacing(4),
49+
paddingLeft: theme.spacing(4),
50+
},
51+
[theme.breakpoints.down("sm")]: {
52+
paddingRight: theme.spacing(2),
53+
paddingLeft: theme.spacing(2),
54+
},
55+
}),
56+
});
57+
58+
const LoginRequired: React.FC = () => <>401 Login Required</>;
59+
const PermissionDenied: React.FC = () => <>403 Permission Denied</>;
60+
const PageNotFound: React.FC = () => <>404 Not Found</>;
61+
const CenteredLoadingPage: React.FC = () => (
62+
<Common.Components.CenteredPage>
63+
<CircularProgress />
64+
</Common.Components.CenteredPage>
65+
);
66+
67+
const throwPageNotFound: (message: string) => never = (message) => {
68+
const errorStr = `RouteRenderer: ${message}`;
69+
const axiosError = new AxiosError(errorStr, errorStr, undefined, undefined, {
70+
status: 404,
71+
} as AxiosResponse);
72+
throw new Common.BackendAPIs.BackendAPIClientError(axiosError);
73+
};
74+
75+
const RouteErrorFallback: React.FC<{ error: Error; reset: () => void }> = ({ error, reset }) => {
76+
if (error instanceof Common.BackendAPIs.BackendAPIClientError) {
77+
switch (error.status) {
78+
case 401:
79+
return <LoginRequired />;
80+
case 403:
81+
return <PermissionDenied />;
82+
case 404:
83+
return <PageNotFound />;
84+
default:
85+
return <Common.Components.ErrorFallback error={error} reset={reset} />;
86+
}
87+
}
88+
return <Common.Components.ErrorFallback error={error} reset={reset} />;
89+
};
90+
91+
export const PageRenderer: React.FC<{ id: string }> = ErrorBoundary.with(
92+
{ fallback: RouteErrorFallback },
93+
Suspense.with({ fallback: <CenteredLoadingPage /> }, ({ id }) => {
94+
const { setAppContext } = useAppContext();
95+
const backendClient = Common.Hooks.BackendAPI.useBackendClient();
96+
const { data } = Common.Hooks.BackendAPI.usePageQuery(backendClient, id);
97+
98+
React.useEffect(() => {
99+
setAppContext((prev) => ({
100+
...prev,
101+
title: data.title,
102+
shouldShowTitleBanner: data.show_top_title_banner,
103+
shouldShowSponsorBanner: data.show_bottom_sponsor_banner,
104+
}));
105+
}, [data, setAppContext]);
106+
107+
return (
108+
<Stack sx={initialPageStyle(Common.Utils.parseCss(data.css))}>
109+
{data.sections.map((s) => (
110+
<Stack sx={initialSectionStyle(Common.Utils.parseCss(s.css))} key={s.id}>
111+
<Common.Components.MDXRenderer text={s.body} />
112+
</Stack>
113+
))}
114+
</Stack>
115+
);
116+
})
117+
);
118+
119+
export const RouteRenderer: React.FC = ErrorBoundary.with(
120+
{ fallback: RouteErrorFallback },
121+
Suspense.with({ fallback: <CenteredLoadingPage /> }, () => {
122+
const location = useLocation();
123+
const { siteMapNode, currentSiteMapDepth } = useAppContext();
124+
125+
if (!siteMapNode) return <CenteredLoadingPage />;
126+
127+
const routeInfo = !R.isEmpty(currentSiteMapDepth) && currentSiteMapDepth[currentSiteMapDepth.length - 1];
128+
if (!routeInfo) throwPageNotFound(`Route ${location} not found`);
129+
130+
return <PageRenderer id={routeInfo.page} />;
131+
})
132+
);
133+
134+
export const PageIdParamRenderer: React.FC = Suspense.with({ fallback: <CenteredLoadingPage /> }, () => {
135+
const { id } = useParams();
136+
if (!id) throwPageNotFound("Page ID is required");
137+
return <PageRenderer id={id} />;
138+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const LOCAL_STORAGE_LANGUAGE_KEY = "language";

apps/pyconkr/src/contexts/SponsorContext.tsx

Lines changed: 0 additions & 27 deletions
This file was deleted.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as React from "react";
2+
3+
import BackendAPISchemas from "../../../../packages/common/src/schemas/backendAPI";
4+
5+
type LanguageType = "ko" | "en";
6+
7+
export type AppContextType = {
8+
language: LanguageType;
9+
shouldShowTitleBanner: boolean;
10+
shouldShowSponsorBanner: boolean;
11+
12+
siteMapNode?: BackendAPISchemas.NestedSiteMapSchema;
13+
sponsors: unknown;
14+
title: string;
15+
currentSiteMapDepth: (BackendAPISchemas.NestedSiteMapSchema | undefined)[];
16+
17+
setAppContext: React.Dispatch<React.SetStateAction<Omit<AppContextType, "setAppContext">>>;
18+
};
19+
20+
export const AppContext = React.createContext<AppContextType | undefined>(undefined);
21+
22+
export const useAppContext = (): AppContextType => {
23+
const context = React.useContext(AppContext);
24+
if (!context) {
25+
throw new Error("useAppContext must be used within an AppContextProvider");
26+
}
27+
return context;
28+
};

apps/pyconkr/src/main.tsx

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
88
import { SnackbarProvider } from "notistack";
99
import * as React from "react";
1010
import * as ReactDom from "react-dom/client";
11+
import { BrowserRouter } from "react-router-dom";
1112

1213
import { App } from "./App.tsx";
1314
import { IS_DEBUG_ENV } from "./consts";
15+
import { LOCAL_STORAGE_LANGUAGE_KEY } from "./consts/local_stroage.ts";
1416
import { PyConKRMDXComponents } from "./consts/mdx_components.ts";
17+
import { AppContext, AppContextType } from "./contexts/app_context.tsx";
1518
import { globalStyles, muiTheme } from "./styles/globalStyles.ts";
1619

1720
const queryClient = new QueryClient({
@@ -32,6 +35,7 @@ const queryClient = new QueryClient({
3235
});
3336

3437
const CommonOptions: Common.Contexts.ContextOptions = {
38+
language: "ko",
3539
debug: IS_DEBUG_ENV,
3640
baseUrl: ".",
3741
backendApiDomain: import.meta.env.VITE_PYCONKR_BACKEND_API_DOMAIN,
@@ -46,37 +50,50 @@ const ShopOptions: Shop.Contexts.ContextOptions = {
4650
shopImpAccountId: import.meta.env.VITE_PYCONKR_SHOP_IMP_ACCOUNT_ID,
4751
};
4852

49-
ReactDom.createRoot(document.getElementById("root")!).render(
50-
<React.StrictMode>
51-
<QueryClientProvider client={queryClient}>
52-
<ReactQueryDevtools initialIsOpen={false} />
53-
<Common.Components.CommonContextProvider options={CommonOptions}>
54-
<Shop.Components.Common.ShopContextProvider options={ShopOptions}>
55-
<ThemeProvider theme={muiTheme}>
56-
<SnackbarProvider>
57-
<CssBaseline />
58-
<Global styles={globalStyles} />
59-
<ErrorBoundary
60-
fallback={
61-
<Common.Components.CenteredPage>
62-
문제가 발생했습니다, 새로고침을 해주세요.
63-
</Common.Components.CenteredPage>
64-
}
65-
>
66-
<Suspense
67-
fallback={
68-
<Common.Components.CenteredPage>
69-
<CircularProgress />
70-
</Common.Components.CenteredPage>
71-
}
72-
>
73-
<App />
74-
</Suspense>
75-
</ErrorBoundary>
76-
</SnackbarProvider>
77-
</ThemeProvider>
78-
</Shop.Components.Common.ShopContextProvider>
79-
</Common.Components.CommonContextProvider>
80-
</QueryClientProvider>
81-
</React.StrictMode>
53+
const SuspenseFallback = (
54+
<Common.Components.CenteredPage>
55+
<CircularProgress />
56+
</Common.Components.CenteredPage>
8257
);
58+
59+
const MainApp: React.FC = () => {
60+
const [appState, setAppContext] = React.useState<Omit<AppContextType, "setAppContext">>({
61+
language: (localStorage.getItem(LOCAL_STORAGE_LANGUAGE_KEY) as "ko" | "en" | null) ?? "ko",
62+
shouldShowTitleBanner: true,
63+
shouldShowSponsorBanner: false,
64+
65+
currentSiteMapDepth: [],
66+
67+
sponsors: null,
68+
title: "PyCon Korea 2025",
69+
});
70+
71+
return (
72+
<React.StrictMode>
73+
<QueryClientProvider client={queryClient}>
74+
<ReactQueryDevtools initialIsOpen={false} />
75+
<SnackbarProvider>
76+
<BrowserRouter>
77+
<AppContext.Provider value={{ ...appState, setAppContext }}>
78+
<Common.Components.CommonContextProvider options={{ ...CommonOptions, language: appState.language }}>
79+
<Shop.Components.Common.ShopContextProvider options={ShopOptions}>
80+
<ErrorBoundary fallback={Common.Components.ErrorFallback}>
81+
<Suspense fallback={SuspenseFallback}>
82+
<ThemeProvider theme={muiTheme}>
83+
<CssBaseline />
84+
<Global styles={globalStyles} />
85+
<App />
86+
</ThemeProvider>
87+
</Suspense>
88+
</ErrorBoundary>
89+
</Shop.Components.Common.ShopContextProvider>
90+
</Common.Components.CommonContextProvider>
91+
</AppContext.Provider>
92+
</BrowserRouter>
93+
</SnackbarProvider>
94+
</QueryClientProvider>
95+
</React.StrictMode>
96+
);
97+
};
98+
99+
ReactDom.createRoot(document.getElementById("root")!).render(<MainApp />);

0 commit comments

Comments
 (0)