Skip to content

Commit 1706753

Browse files
authored
Merge pull request #29 from pythonkr/feature/add-lottie-component
feat: 전반적인 여백 재조정 및 `LottiePlayer` & `NetworkLottiePlayer` 추가
2 parents 249956c + c5ad511 commit 1706753

File tree

10 files changed

+154
-40
lines changed

10 files changed

+154
-40
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ const ReadOnlyValueField: React.FC<{
105105
)}
106106
{fieldState.blob.type.startsWith("application/json") && fieldState.blobText && (
107107
<Box sx={{ maxWidth: "600px", overflow: "auto" }}>
108-
<Common.Components.LottieDebugPanel animationData={JSON.parse(fieldState.blobText)} />
108+
<Common.Components.LottieDebugPanel data={JSON.parse(fieldState.blobText)} />
109109
</Box>
110110
)}
111111
<a href={value as string}>링크</a>

apps/pyconkr-admin/src/consts/mdx_components.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ const MUIMDXComponents: MDXComponents = {
131131
};
132132

133133
const PyConKRCommonMDXComponents: MDXComponents = {
134+
Common__Components__Lottie: Common.Components.LottiePlayer,
135+
Common__Components__NetworkLottie: Common.Components.NetworkLottiePlayer,
134136
Common__Components__MDX__PrimaryStyledDetails: Common.Components.MDX.PrimaryStyledDetails,
135137
Common__Components__MDX__SecondaryStyledDetails: Common.Components.MDX.SecondaryStyledDetails,
136138
Common__Components__MDX__Map: Common.Components.MDX.Map,

apps/pyconkr/src/consts/mdx_components.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ const MUIMDXComponents: MDXComponents = {
131131
};
132132

133133
const PyConKRCommonMDXComponents: MDXComponents = {
134+
Common__Components__Lottie: Common.Components.LottiePlayer,
135+
Common__Components__NetworkLottie: Common.Components.NetworkLottiePlayer,
134136
Common__Components__MDX__PrimaryStyledDetails: Common.Components.MDX.PrimaryStyledDetails,
135137
Common__Components__MDX__SecondaryStyledDetails: Common.Components.MDX.SecondaryStyledDetails,
136138
Common__Components__MDX__Map: Common.Components.MDX.Map,

packages/common/src/components/dynamic_route.tsx

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Box, CircularProgress, Stack, Theme } from "@mui/material";
1+
import { CircularProgress, Stack, Theme } from "@mui/material";
22
import { ErrorBoundary, Suspense } from "@suspensive/react";
33
import { AxiosError, AxiosResponse } from "axios";
44
import * as React from "react";
@@ -15,18 +15,49 @@ import { MDXRenderer } from "./mdx";
1515
const initialPageStyle: (additionalStyle: React.CSSProperties) => (theme: Theme) => React.CSSProperties =
1616
(additionalStyle) => (theme) => ({
1717
width: "100%",
18-
marginTop: theme.spacing(8),
1918
display: "flex",
20-
justifyContent: "center",
19+
justifyContent: "flex-start",
2120
alignItems: "center",
2221
flexDirection: "column",
22+
23+
marginTop: theme.spacing(8),
24+
25+
...(additionalStyle
26+
? additionalStyle
27+
: {
28+
[theme.breakpoints.down("md")]: {
29+
marginTop: theme.spacing(4),
30+
},
31+
[theme.breakpoints.down("sm")]: {
32+
marginTop: theme.spacing(2),
33+
},
34+
}),
2335
...additionalStyle,
2436
});
2537

2638
const initialSectionStyle: (additionalStyle: React.CSSProperties) => (theme: Theme) => React.CSSProperties =
27-
(additionalStyle) => () => ({
39+
(additionalStyle) => (theme) => ({
2840
width: "100%",
29-
...additionalStyle,
41+
maxWidth: "1000px",
42+
display: "flex",
43+
justifyContent: "flex-start",
44+
alignItems: "center",
45+
paddingRight: "2rem",
46+
paddingLeft: "2rem",
47+
48+
"& .markdown-body": { width: "100%" },
49+
...(additionalStyle
50+
? additionalStyle
51+
: {
52+
[theme.breakpoints.down("md")]: {
53+
paddingRight: "1rem",
54+
paddingLeft: "1rem",
55+
},
56+
[theme.breakpoints.down("sm")]: {
57+
paddingRight: "0.5rem",
58+
paddingLeft: "0.5rem",
59+
},
60+
}),
3061
});
3162

3263
const LoginRequired: React.FC = () => <>401 Login Required</>;
@@ -62,18 +93,12 @@ export const PageRenderer: React.FC<{ id?: string }> = ErrorBoundary.with(
6293
Suspense.with({ fallback: <CircularProgress /> }, ({ id }) => {
6394
const backendClient = Hooks.BackendAPI.useBackendClient();
6495
const { data } = Hooks.BackendAPI.usePageQuery(backendClient, id || "");
65-
const commonStackStyle = {
66-
justifyContent: "flex-start",
67-
alignItems: "center",
68-
};
6996

7097
return (
71-
<Stack {...commonStackStyle} sx={initialPageStyle(Utils.parseCss(data.css))}>
98+
<Stack sx={initialPageStyle(Utils.parseCss(data.css))}>
7299
{data.sections.map((s) => (
73-
<Stack {...commonStackStyle} sx={initialSectionStyle(Utils.parseCss(s.css))} key={s.id}>
74-
<Box sx={{ maxWidth: "1000px" }}>
75-
<MDXRenderer text={s.body} />
76-
</Box>
100+
<Stack sx={initialSectionStyle(Utils.parseCss(s.css))} key={s.id}>
101+
<MDXRenderer text={s.body} />
77102
</Stack>
78103
))}
79104
</Stack>

packages/common/src/components/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import {
66
RouteRenderer as RouteRendererComponent,
77
} from "./dynamic_route";
88
import { ErrorFallback as ErrorFallbackComponent } from "./error_handler";
9-
import { LottieDebugPanel as LottieDebugPanelComponent } from "./lottie";
9+
import {
10+
LottieDebugPanel as LottieDebugPanelComponent,
11+
LottiePlayer as LottiePlayerComponent,
12+
NetworkLottiePlayer as NetworkLottiePlayerComponent,
13+
} from "./lottie";
1014
import { MDXRenderer as MDXRendererComponent } from "./mdx";
1115
import type { MapPropType as MapComponentPropType } from "./mdx_components/map";
1216
import { Map as MapComponent } from "./mdx_components/map";
@@ -27,6 +31,8 @@ namespace Components {
2731
export const MDXRenderer = MDXRendererComponent;
2832
export const PythonKorea = PythonKoreaComponent;
2933
export const LottieDebugPanel = LottieDebugPanelComponent;
34+
export const LottiePlayer = LottiePlayerComponent;
35+
export const NetworkLottiePlayer = NetworkLottiePlayerComponent;
3036
export const ErrorFallback = ErrorFallbackComponent;
3137

3238
export namespace MDX {
Lines changed: 88 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,120 @@
11
import { Pause, PlayArrow, Stop } from "@mui/icons-material";
2-
import { Box, FormControlLabel, IconButton, Stack, Switch } from "@mui/material";
2+
import { Box, CircularProgress, FormControlLabel, IconButton, Stack, Switch } from "@mui/material";
3+
import { ErrorBoundary } from "@suspensive/react";
34
import * as React from "react";
4-
import Lottie from "react-lottie";
5+
import Lottie, { Options } from "react-lottie";
6+
7+
import { ErrorFallback } from "./error_handler";
8+
import { isValidHttpUrl } from "../utils/string";
59

610
type PlayState = "playing" | "paused" | "stopped";
711

12+
type LottiePlayerProps = {
13+
data: unknown;
14+
playState?: PlayState;
15+
disableLoop?: boolean;
16+
renderSettings?: Options["rendererSettings"];
17+
style?: React.CSSProperties;
18+
};
19+
820
type LottiePlayerStateType = {
21+
playState: PlayState;
22+
};
23+
24+
type LottieDebugPanelStateType = LottiePlayerStateType & {
925
loop: boolean;
10-
isStopped: boolean;
11-
isPaused: boolean;
1226
};
1327

14-
export const LottieDebugPanel: React.FC<{ animationData: unknown }> = ({ animationData }) => {
15-
const [playerState, setPlayerState] = React.useState<LottiePlayerStateType>({
16-
loop: true,
17-
isStopped: false,
18-
isPaused: false,
28+
const playStateToLottiePlayerState = (playState: PlayState): { isStopped: boolean; isPaused: boolean } => {
29+
if (playState === "playing") return { isStopped: false, isPaused: false };
30+
if (playState === "paused") return { isStopped: false, isPaused: true };
31+
return { isStopped: true, isPaused: true };
32+
};
33+
34+
export const LottieDebugPanel: React.FC<LottiePlayerProps> = ({
35+
data,
36+
playState = "playing",
37+
disableLoop = false,
38+
renderSettings = {},
39+
style,
40+
}) => {
41+
const [playerState, setPlayerState] = React.useState<LottieDebugPanelStateType>({
42+
playState,
43+
loop: !disableLoop,
1944
});
45+
const isPlaying = playerState.playState === "playing";
2046

2147
const toggleLoop = () => setPlayerState((ps) => ({ ...ps, loop: !ps.loop }));
22-
const setPlayState = (playState: PlayState) => {
23-
if (playState === "playing") setPlayerState((ps) => ({ ...ps, isStopped: false, isPaused: false }));
24-
if (playState === "paused") setPlayerState((ps) => ({ ...ps, isStopped: false, isPaused: true }));
25-
if (playState === "stopped") setPlayerState((ps) => ({ ...ps, isStopped: true, isPaused: true }));
26-
};
48+
const setPlayState = (playState: PlayState) => setPlayerState((ps) => ({ ...ps, playState }));
2749

2850
const stop = () => setPlayState("stopped");
29-
const togglePause = () => setPlayState(playerState.isPaused ? "playing" : "paused");
51+
const togglePause = () => setPlayState(!isPlaying ? "playing" : "paused");
3052

3153
return (
3254
<Stack direction="column">
3355
<Box>
3456
<Lottie
35-
isStopped={playerState.isStopped}
36-
isPaused={playerState.isPaused}
57+
{...playStateToLottiePlayerState(playerState.playState)}
3758
options={{
38-
animationData,
59+
animationData: data,
3960
loop: playerState.loop,
4061
autoplay: true,
41-
rendererSettings: { preserveAspectRatio: "xMidYMid slice" },
62+
rendererSettings: { preserveAspectRatio: "xMidYMid slice", ...renderSettings },
4263
}}
64+
style={style}
4365
/>
4466
</Box>
4567
<Stack direction="row" spacing={2}>
46-
<IconButton onClick={togglePause} children={playerState.isPaused ? <PlayArrow /> : <Pause />} />
68+
<IconButton onClick={togglePause} children={!isPlaying ? <PlayArrow /> : <Pause />} />
4769
<IconButton onClick={stop} children={<Stop />} />
4870
<FormControlLabel control={<Switch checked={playerState.loop} onChange={toggleLoop} />} label="반복 재생" />
4971
</Stack>
5072
</Stack>
5173
);
5274
};
75+
76+
export const LottiePlayer: React.FC<LottiePlayerProps> = ({
77+
data,
78+
playState = "playing",
79+
disableLoop = false,
80+
renderSettings = {},
81+
style,
82+
}) => (
83+
<Lottie
84+
{...playStateToLottiePlayerState(playState)}
85+
options={{
86+
animationData: data,
87+
loop: !disableLoop,
88+
autoplay: playState === "playing",
89+
rendererSettings: { preserveAspectRatio: "xMidYMid slice", ...renderSettings },
90+
}}
91+
style={style}
92+
/>
93+
);
94+
95+
type NetworkLottiePlayerProps = Omit<LottiePlayerProps, "data"> & {
96+
url: string;
97+
fetchOptions?: RequestInit;
98+
};
99+
100+
type NetworkLottiePlayerStateType = {
101+
data?: unknown | null;
102+
};
103+
104+
export const NetworkLottiePlayer: React.FC<NetworkLottiePlayerProps> = ErrorBoundary.with(
105+
{ fallback: ErrorFallback },
106+
(props) => {
107+
const [playerState, setPlayerState] = React.useState<NetworkLottiePlayerStateType>({});
108+
109+
React.useEffect(() => {
110+
(async () => {
111+
if (!isValidHttpUrl(props.url)) throw new Error("Invalid URL for NetworkLottiePlayer: " + props.url);
112+
113+
const data = JSON.parse(await (await fetch(props.url, props.fetchOptions)).text());
114+
setPlayerState((ps) => ({ ...ps, data }));
115+
})();
116+
}, [props.url, props.fetchOptions]);
117+
118+
return playerState.data === undefined ? <CircularProgress /> : <LottiePlayer {...props} data={playerState.data} />;
119+
}
120+
);

packages/common/src/components/mdx.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const CustomMDXComponents: MDXComponents = {
5555
h6: (props) => <h6 style={{ margin: 0 }} {...props} />,
5656
strong: (props) => <strong {...props} />,
5757
hr: (props) => <StyledDivider {...props} />,
58+
img: (props) => <img style={{ maxWidth: "100%" }} alt="" {...props} />,
5859
em: (props) => <em {...props} />,
5960
ul: (props) => <ul {...props} />,
6061
ol: (props) => <ol {...props} />,

packages/common/src/utils/api.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,8 @@ export const findSiteMapUsingRoute = (
6161
export const parseCss = (t: unknown): React.CSSProperties => {
6262
try {
6363
if (R.isString(t) && !R.isEmpty(t)) return JSON.parse(t);
64-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
65-
} catch (_) {
66-
// Ignore parsing errors
64+
} catch (e) {
65+
console.warn("Failed to parse CSS string:", t, e);
6766
}
6867
return {} as React.CSSProperties;
6968
};

packages/common/src/utils/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
filterReadOnlyPropertiesInJsonSchema as _filterReadOnlyPropertiesInJsonSchema,
1111
filterWritablePropertiesInJsonSchema as _filterWritablePropertiesInJsonSchema,
1212
} from "./json_schema";
13-
import { isFilledString as _isFilledString, rtrim as _rtrim } from "./string";
13+
import { isFilledString as _isFilledString, isValidHttpUrl as _isValidHttpUrl, rtrim as _rtrim } from "./string";
1414

1515
namespace Utils {
1616
export const buildNestedSiteMap = _buildNestedSiteMap;
@@ -20,6 +20,7 @@ namespace Utils {
2020
export const isFormValid = _isFormValid;
2121
export const getFormValue = _getFormValue;
2222
export const isFilledString = _isFilledString;
23+
export const isValidHttpUrl = _isValidHttpUrl;
2324
export const rtrim = _rtrim;
2425
export const filterWritablePropertiesInJsonSchema = _filterWritablePropertiesInJsonSchema;
2526
export const filterReadOnlyPropertiesInJsonSchema = _filterReadOnlyPropertiesInJsonSchema;

packages/common/src/utils/string.ts

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

33
export const isFilledString = (obj: unknown): obj is string => R.isString(obj) && !R.isEmpty(obj);
44

5+
export const isValidHttpUrl = (obj: unknown): obj is string => {
6+
try {
7+
const url = new URL(obj as string);
8+
return url.protocol === "http:" || url.protocol === "https:";
9+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
10+
} catch (_) {
11+
return false;
12+
}
13+
};
14+
515
// Remove whitespace from the right side of the input string.
616
export const rtrim = (x: string): string => x.replace(/\s+$/gm, "");

0 commit comments

Comments
 (0)