diff --git a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx
index 3e4a8a2..a03fe82 100644
--- a/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx
+++ b/apps/pyconkr-admin/src/components/layouts/admin_editor.tsx
@@ -105,7 +105,7 @@ const ReadOnlyValueField: React.FC<{
)}
{fieldState.blob.type.startsWith("application/json") && fieldState.blobText && (
-
+
)}
링크
diff --git a/apps/pyconkr-admin/src/consts/mdx_components.ts b/apps/pyconkr-admin/src/consts/mdx_components.ts
index 8cb9f19..b8db684 100644
--- a/apps/pyconkr-admin/src/consts/mdx_components.ts
+++ b/apps/pyconkr-admin/src/consts/mdx_components.ts
@@ -131,6 +131,8 @@ const MUIMDXComponents: MDXComponents = {
};
const PyConKRCommonMDXComponents: MDXComponents = {
+ Common__Components__Lottie: Common.Components.LottiePlayer,
+ Common__Components__NetworkLottie: Common.Components.NetworkLottiePlayer,
Common__Components__MDX__PrimaryStyledDetails: Common.Components.MDX.PrimaryStyledDetails,
Common__Components__MDX__SecondaryStyledDetails: Common.Components.MDX.SecondaryStyledDetails,
Common__Components__MDX__Map: Common.Components.MDX.Map,
diff --git a/apps/pyconkr/src/consts/mdx_components.ts b/apps/pyconkr/src/consts/mdx_components.ts
index 8cb9f19..b8db684 100644
--- a/apps/pyconkr/src/consts/mdx_components.ts
+++ b/apps/pyconkr/src/consts/mdx_components.ts
@@ -131,6 +131,8 @@ const MUIMDXComponents: MDXComponents = {
};
const PyConKRCommonMDXComponents: MDXComponents = {
+ Common__Components__Lottie: Common.Components.LottiePlayer,
+ Common__Components__NetworkLottie: Common.Components.NetworkLottiePlayer,
Common__Components__MDX__PrimaryStyledDetails: Common.Components.MDX.PrimaryStyledDetails,
Common__Components__MDX__SecondaryStyledDetails: Common.Components.MDX.SecondaryStyledDetails,
Common__Components__MDX__Map: Common.Components.MDX.Map,
diff --git a/packages/common/src/components/dynamic_route.tsx b/packages/common/src/components/dynamic_route.tsx
index 2678df0..1ab3397 100644
--- a/packages/common/src/components/dynamic_route.tsx
+++ b/packages/common/src/components/dynamic_route.tsx
@@ -1,4 +1,4 @@
-import { Box, CircularProgress, Stack, Theme } from "@mui/material";
+import { CircularProgress, Stack, Theme } from "@mui/material";
import { ErrorBoundary, Suspense } from "@suspensive/react";
import { AxiosError, AxiosResponse } from "axios";
import * as React from "react";
@@ -15,18 +15,49 @@ import { MDXRenderer } from "./mdx";
const initialPageStyle: (additionalStyle: React.CSSProperties) => (theme: Theme) => React.CSSProperties =
(additionalStyle) => (theme) => ({
width: "100%",
- marginTop: theme.spacing(8),
display: "flex",
- justifyContent: "center",
+ justifyContent: "flex-start",
alignItems: "center",
flexDirection: "column",
+
+ marginTop: theme.spacing(8),
+
+ ...(additionalStyle
+ ? additionalStyle
+ : {
+ [theme.breakpoints.down("md")]: {
+ marginTop: theme.spacing(4),
+ },
+ [theme.breakpoints.down("sm")]: {
+ marginTop: theme.spacing(2),
+ },
+ }),
...additionalStyle,
});
const initialSectionStyle: (additionalStyle: React.CSSProperties) => (theme: Theme) => React.CSSProperties =
- (additionalStyle) => () => ({
+ (additionalStyle) => (theme) => ({
width: "100%",
- ...additionalStyle,
+ maxWidth: "1000px",
+ display: "flex",
+ justifyContent: "flex-start",
+ alignItems: "center",
+ paddingRight: "2rem",
+ paddingLeft: "2rem",
+
+ "& .markdown-body": { width: "100%" },
+ ...(additionalStyle
+ ? additionalStyle
+ : {
+ [theme.breakpoints.down("md")]: {
+ paddingRight: "1rem",
+ paddingLeft: "1rem",
+ },
+ [theme.breakpoints.down("sm")]: {
+ paddingRight: "0.5rem",
+ paddingLeft: "0.5rem",
+ },
+ }),
});
const LoginRequired: React.FC = () => <>401 Login Required>;
@@ -62,18 +93,12 @@ export const PageRenderer: React.FC<{ id?: string }> = ErrorBoundary.with(
Suspense.with({ fallback: }, ({ id }) => {
const backendClient = Hooks.BackendAPI.useBackendClient();
const { data } = Hooks.BackendAPI.usePageQuery(backendClient, id || "");
- const commonStackStyle = {
- justifyContent: "flex-start",
- alignItems: "center",
- };
return (
-
+
{data.sections.map((s) => (
-
-
-
-
+
+
))}
diff --git a/packages/common/src/components/index.ts b/packages/common/src/components/index.ts
index 6433931..b409d89 100644
--- a/packages/common/src/components/index.ts
+++ b/packages/common/src/components/index.ts
@@ -6,7 +6,11 @@ import {
RouteRenderer as RouteRendererComponent,
} from "./dynamic_route";
import { ErrorFallback as ErrorFallbackComponent } from "./error_handler";
-import { LottieDebugPanel as LottieDebugPanelComponent } from "./lottie";
+import {
+ LottieDebugPanel as LottieDebugPanelComponent,
+ LottiePlayer as LottiePlayerComponent,
+ NetworkLottiePlayer as NetworkLottiePlayerComponent,
+} from "./lottie";
import { MDXRenderer as MDXRendererComponent } from "./mdx";
import type { MapPropType as MapComponentPropType } from "./mdx_components/map";
import { Map as MapComponent } from "./mdx_components/map";
@@ -27,6 +31,8 @@ namespace Components {
export const MDXRenderer = MDXRendererComponent;
export const PythonKorea = PythonKoreaComponent;
export const LottieDebugPanel = LottieDebugPanelComponent;
+ export const LottiePlayer = LottiePlayerComponent;
+ export const NetworkLottiePlayer = NetworkLottiePlayerComponent;
export const ErrorFallback = ErrorFallbackComponent;
export namespace MDX {
diff --git a/packages/common/src/components/lottie.tsx b/packages/common/src/components/lottie.tsx
index 71d8062..20f28cf 100644
--- a/packages/common/src/components/lottie.tsx
+++ b/packages/common/src/components/lottie.tsx
@@ -1,52 +1,120 @@
import { Pause, PlayArrow, Stop } from "@mui/icons-material";
-import { Box, FormControlLabel, IconButton, Stack, Switch } from "@mui/material";
+import { Box, CircularProgress, FormControlLabel, IconButton, Stack, Switch } from "@mui/material";
+import { ErrorBoundary } from "@suspensive/react";
import * as React from "react";
-import Lottie from "react-lottie";
+import Lottie, { Options } from "react-lottie";
+
+import { ErrorFallback } from "./error_handler";
+import { isValidHttpUrl } from "../utils/string";
type PlayState = "playing" | "paused" | "stopped";
+type LottiePlayerProps = {
+ data: unknown;
+ playState?: PlayState;
+ disableLoop?: boolean;
+ renderSettings?: Options["rendererSettings"];
+ style?: React.CSSProperties;
+};
+
type LottiePlayerStateType = {
+ playState: PlayState;
+};
+
+type LottieDebugPanelStateType = LottiePlayerStateType & {
loop: boolean;
- isStopped: boolean;
- isPaused: boolean;
};
-export const LottieDebugPanel: React.FC<{ animationData: unknown }> = ({ animationData }) => {
- const [playerState, setPlayerState] = React.useState({
- loop: true,
- isStopped: false,
- isPaused: false,
+const playStateToLottiePlayerState = (playState: PlayState): { isStopped: boolean; isPaused: boolean } => {
+ if (playState === "playing") return { isStopped: false, isPaused: false };
+ if (playState === "paused") return { isStopped: false, isPaused: true };
+ return { isStopped: true, isPaused: true };
+};
+
+export const LottieDebugPanel: React.FC = ({
+ data,
+ playState = "playing",
+ disableLoop = false,
+ renderSettings = {},
+ style,
+}) => {
+ const [playerState, setPlayerState] = React.useState({
+ playState,
+ loop: !disableLoop,
});
+ const isPlaying = playerState.playState === "playing";
const toggleLoop = () => setPlayerState((ps) => ({ ...ps, loop: !ps.loop }));
- const setPlayState = (playState: PlayState) => {
- if (playState === "playing") setPlayerState((ps) => ({ ...ps, isStopped: false, isPaused: false }));
- if (playState === "paused") setPlayerState((ps) => ({ ...ps, isStopped: false, isPaused: true }));
- if (playState === "stopped") setPlayerState((ps) => ({ ...ps, isStopped: true, isPaused: true }));
- };
+ const setPlayState = (playState: PlayState) => setPlayerState((ps) => ({ ...ps, playState }));
const stop = () => setPlayState("stopped");
- const togglePause = () => setPlayState(playerState.isPaused ? "playing" : "paused");
+ const togglePause = () => setPlayState(!isPlaying ? "playing" : "paused");
return (
- : } />
+ : } />
} />
} label="반복 재생" />
);
};
+
+export const LottiePlayer: React.FC = ({
+ data,
+ playState = "playing",
+ disableLoop = false,
+ renderSettings = {},
+ style,
+}) => (
+
+);
+
+type NetworkLottiePlayerProps = Omit & {
+ url: string;
+ fetchOptions?: RequestInit;
+};
+
+type NetworkLottiePlayerStateType = {
+ data?: unknown | null;
+};
+
+export const NetworkLottiePlayer: React.FC = ErrorBoundary.with(
+ { fallback: ErrorFallback },
+ (props) => {
+ const [playerState, setPlayerState] = React.useState({});
+
+ React.useEffect(() => {
+ (async () => {
+ if (!isValidHttpUrl(props.url)) throw new Error("Invalid URL for NetworkLottiePlayer: " + props.url);
+
+ const data = JSON.parse(await (await fetch(props.url, props.fetchOptions)).text());
+ setPlayerState((ps) => ({ ...ps, data }));
+ })();
+ }, [props.url, props.fetchOptions]);
+
+ return playerState.data === undefined ? : ;
+ }
+);
diff --git a/packages/common/src/components/mdx.tsx b/packages/common/src/components/mdx.tsx
index fbc7360..774f68e 100644
--- a/packages/common/src/components/mdx.tsx
+++ b/packages/common/src/components/mdx.tsx
@@ -55,6 +55,7 @@ const CustomMDXComponents: MDXComponents = {
h6: (props) => ,
strong: (props) => ,
hr: (props) => ,
+ img: (props) =>
,
em: (props) => ,
ul: (props) => ,
ol: (props) =>
,
diff --git a/packages/common/src/utils/api.ts b/packages/common/src/utils/api.ts
index bfac3e0..c7cceb4 100644
--- a/packages/common/src/utils/api.ts
+++ b/packages/common/src/utils/api.ts
@@ -61,9 +61,8 @@ export const findSiteMapUsingRoute = (
export const parseCss = (t: unknown): React.CSSProperties => {
try {
if (R.isString(t) && !R.isEmpty(t)) return JSON.parse(t);
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- } catch (_) {
- // Ignore parsing errors
+ } catch (e) {
+ console.warn("Failed to parse CSS string:", t, e);
}
return {} as React.CSSProperties;
};
diff --git a/packages/common/src/utils/index.ts b/packages/common/src/utils/index.ts
index 48f8e33..574c828 100644
--- a/packages/common/src/utils/index.ts
+++ b/packages/common/src/utils/index.ts
@@ -10,7 +10,7 @@ import {
filterReadOnlyPropertiesInJsonSchema as _filterReadOnlyPropertiesInJsonSchema,
filterWritablePropertiesInJsonSchema as _filterWritablePropertiesInJsonSchema,
} from "./json_schema";
-import { isFilledString as _isFilledString, rtrim as _rtrim } from "./string";
+import { isFilledString as _isFilledString, isValidHttpUrl as _isValidHttpUrl, rtrim as _rtrim } from "./string";
namespace Utils {
export const buildNestedSiteMap = _buildNestedSiteMap;
@@ -20,6 +20,7 @@ namespace Utils {
export const isFormValid = _isFormValid;
export const getFormValue = _getFormValue;
export const isFilledString = _isFilledString;
+ export const isValidHttpUrl = _isValidHttpUrl;
export const rtrim = _rtrim;
export const filterWritablePropertiesInJsonSchema = _filterWritablePropertiesInJsonSchema;
export const filterReadOnlyPropertiesInJsonSchema = _filterReadOnlyPropertiesInJsonSchema;
diff --git a/packages/common/src/utils/string.ts b/packages/common/src/utils/string.ts
index b44c6f3..4bf656e 100644
--- a/packages/common/src/utils/string.ts
+++ b/packages/common/src/utils/string.ts
@@ -2,5 +2,15 @@ import * as R from "remeda";
export const isFilledString = (obj: unknown): obj is string => R.isString(obj) && !R.isEmpty(obj);
+export const isValidHttpUrl = (obj: unknown): obj is string => {
+ try {
+ const url = new URL(obj as string);
+ return url.protocol === "http:" || url.protocol === "https:";
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (_) {
+ return false;
+ }
+};
+
// Remove whitespace from the right side of the input string.
export const rtrim = (x: string): string => x.replace(/\s+$/gm, "");