Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 54 additions & 3 deletions apps/pyconkr/src/consts/mdx_components.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
// 후대의 개발자님께 : 컴포넌트 맨 첫글자가 대문자로 시작하지 않으면 JSX 컴포넌트가 아니라 일반 HTML 태그로 인식합니다. 제발 대문자로 시작해주세요.
import { Components } from "@frontend/common";
import { Components, Schemas } from "@frontend/common";
import * as Shop from "@frontend/shop";
import * as mui from "@mui/material";
import type { MDXComponents } from "mdx/types.js";
import * as React from "react";

import PyCon2025HostLogoBig from "../../../../packages/common/src/assets/pyconkr2025_hostlogo_big.png";
import PyCon2025HostLogoSmall from "../../../../packages/common/src/assets/pyconkr2025_hostlogo_small.png";
import PyCon2025MobileLogoImage from "../../../../packages/common/src/assets/pyconkr2025_main_cover_image.png";
import PyCon2025MobileLogoTitle from "../../../../packages/common/src/assets/pyconkr2025_main_cover_title.png";
import PyCon2025Logo from "../assets/pyconkr2025_logo.png";

const MUIMDXComponents: MDXComponents = {
Mui__material__Accordion: mui.Accordion,
Expand Down Expand Up @@ -130,6 +137,48 @@ const MUIMDXComponents: MDXComponents = {
Mui__material__Zoom: mui.Zoom,
};

const getPyConKR2025SessionUrl = (session: Schemas.BackendAPI.SessionSchema): string => {
const urlSafeTitle = session.title
.replace(/ /g, "-")
.replace(/([.])/g, "_")
.replace(/(?![.0-9A-Za-zㄱ-ㅣ가-힣-])./g, "");
return `/presentations/${session.id}#${urlSafeTitle}`;
};

const PyConKR2025FallbackImage = React.createElement("img", {
src: PyCon2025Logo,
alt: "PyCon 2025 Logo",
style: { width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" },
});

const PyConKR2025SessionList: React.FC<React.ComponentProps<typeof Components.MDX.SessionList>> = (props) =>
React.createElement(Components.MDX.SessionList, {
...props,
fallbackImage: PyConKR2025FallbackImage,
getSessionUrl: getPyConKR2025SessionUrl,
});

const PyConKR2025SessionTimeTable: React.FC<React.ComponentProps<typeof Components.MDX.SessionTimeTable>> = (props) =>
React.createElement(Components.MDX.SessionTimeTable, {
...props,
getSessionUrl: getPyConKR2025SessionUrl,
});

const PyConKR2025MobileAccordion: React.FC<object> = () =>
React.createElement(Components.MDX.MobileAccordion, {
marqueeText: "AUG 15 - 17",
marqueeLogoSrc: PyCon2025HostLogoSmall,
hostLogoBigSrc: PyCon2025HostLogoBig,
venueKo: "서울특별시 중구 필동로 1길 30 동국대학교 신공학관",
venueEnLines: ["New Engineering Building, Dongguk University", "Pildong-ro 1-gil, Jung-gu, Seoul, Republic of Korea"],
});

const PyConKR2025MobileCover: React.FC<object> = () =>
React.createElement(Components.MDX.MobileCover, {
coverImageSrc: PyCon2025MobileLogoImage,
coverTitleSrc: PyCon2025MobileLogoTitle,
});

const PyConKRCommonMDXComponents: MDXComponents = {
Common__Components__Lottie: Components.LottiePlayer,
Common__Components__NetworkLottie: Components.NetworkLottiePlayer,
Expand All @@ -139,8 +188,10 @@ const PyConKRCommonMDXComponents: MDXComponents = {
Common__Components__MDX__Map: Components.MDX.Map,
Common__Components__MDX__FAQAccordion: Components.MDX.FAQAccordion,
Common__Components__MDX__FullWidthStyledButton: Components.MDX.StyledFullWidthButton,
Common__Components__Session__List: Components.MDX.SessionList,
Common__Components__Session__TimeTable: Components.MDX.SessionTimeTable,
Common__Components__Session__List: PyConKR2025SessionList,
Common__Components__Session__TimeTable: PyConKR2025SessionTimeTable,
Common__Components__MDX__MobileAccordion: PyConKR2025MobileAccordion,
Common__Components__MDX__MobileCover: PyConKR2025MobileCover,
};

const PythonKRShopMDXComponents: MDXComponents = {
Expand Down
4 changes: 4 additions & 0 deletions packages/common/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
} from "./mdx_components/faq_accordion";
import type { MapPropType as MapComponentPropType } from "./mdx_components/map";
import { Map as MapComponent } from "./mdx_components/map";
import { MobileAccordion as MobileAccordionComponent } from "./mdx_components/mobile_accordion";
import { MobileCover as MobileCoverComponent } from "./mdx_components/mobile_cover";
import { OneDetailsOpener as OneDetailsOpenerComponent } from "./mdx_components/one_details_opener";
import { SessionList as SessionListComponent } from "./mdx_components/session_list";
import { SessionTimeTable as SessionTimeTableComponent } from "./mdx_components/session_timetable";
Expand Down Expand Up @@ -51,6 +53,8 @@ namespace Components {

export namespace MDX {
export const Confetti = ConfettiComponent;
export const MobileAccordion = MobileAccordionComponent;
export const MobileCover = MobileCoverComponent;
export const StyledFullWidthButton = StyledFullWidthButtonComponent;
export const PrimaryStyledDetails = PrimaryStyledDetailsComponent;
export const SecondaryStyledDetails = SecondaryStyledDetailsComponent;
Expand Down
41 changes: 23 additions & 18 deletions packages/common/src/components/mdx_components/mobile_accordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,33 @@ import { AccordionDetails, AccordionSummary, Accordion as MuiAccordion, Stack, T
import * as React from "react";
import Marquee from "react-fast-marquee";

import { useAppContext } from "../../../../../apps/pyconkr/src/contexts/app_context";
import PyCon2025HostLogoBig from "../../assets/pyconkr2025_hostlogo_big.png";
import PyCon2025HostLogoSmall from "../../assets/pyconkr2025_hostlogo_small.png";
import * as Hooks from "../../hooks";

const MarqueeAccordion: React.FC = () => {
const MarqueeAccordion: React.FC<{ marqueeText: string; marqueeLogoSrc: string }> = ({ marqueeText, marqueeLogoSrc }) => {
const marqueeWidth = window.innerWidth * 0.9;
const marqueeGradientWidth = window.innerWidth * 0.1;
const items = React.useMemo(() => {
return Array.from({ length: 100 }, () => (
<Stack direction="row" sx={{ gap: 0 }}>
<StyledTypography>AUG 15 - 17</StyledTypography>
<img alt="logo" src={PyCon2025HostLogoSmall} />
<StyledTypography>{marqueeText}</StyledTypography>
<img alt="logo" src={marqueeLogoSrc} />
</Stack>
));
}, []);
}, [marqueeText, marqueeLogoSrc]);

return <Marquee loop={0} gradient={true} gradientWidth={marqueeGradientWidth} speed={30} style={{ width: marqueeWidth }} children={items} />;
};

export const MobileAccordion: React.FC = () => {
const { language } = useAppContext();
type MobileAccordionProps = {
marqueeText: string;
marqueeLogoSrc: string;
hostLogoBigSrc: string;
venueKo: string;
venueEnLines: string[];
};

export const MobileAccordion: React.FC<MobileAccordionProps> = ({ marqueeText, marqueeLogoSrc, hostLogoBigSrc, venueKo, venueEnLines }) => {
const { language } = Hooks.Common.useCommonContext();
const [expanded, setExpanded] = React.useState<boolean>(false);

return (
Expand All @@ -44,27 +50,26 @@ export const MobileAccordion: React.FC = () => {
}
sx={{ margin: 0, padding: 0 }}
>
{expanded ? null : <MarqueeAccordion />}
{expanded ? null : <MarqueeAccordion marqueeText={marqueeText} marqueeLogoSrc={marqueeLogoSrc} />}
</AccordionSummary>
<StyledAccordionDetails>
<Stack>
<Stack sx={{ padding: "30px 0px", borderRadius: "16px", alignItems: "center", justifyContent: "center" }}>
<img src={PyCon2025HostLogoBig} alt="PyCon 2025 Host Logo" style={{ width: "90%", height: "90%" }} />
<img src={hostLogoBigSrc} alt="Host Logo" style={{ width: "90%", height: "90%" }} />
</Stack>
{language === "ko" ? (
<Stack direction="column" sx={{ transform: "translateY(-280%)" }}>
<Typography color="#938A85" textAlign="center" fontSize="11px" fontWeight={400}>
{"서울특별시 중구 필동로 1길 30 동국대학교 신공학관"}
{venueKo}
</Typography>
</Stack>
) : (
<Stack direction="column" sx={{ transform: "translateY(-180%)" }}>
<Typography color="#938A85" textAlign="center" fontSize="10px" fontWeight={400}>
{"New Engineering Building, Dongguk University"}
</Typography>
<Typography color="#938A85" textAlign="center" fontWeight={400} fontSize="10px">
{"Pildong-ro 1-gil, Jung-gu, Seoul, Republic of Korea"}
</Typography>
{venueEnLines.map((line, i) => (
<Typography key={i} color="#938A85" textAlign="center" fontWeight={400} fontSize="10px">
{line}
</Typography>
))}
</Stack>
)}
</Stack>
Expand Down
27 changes: 19 additions & 8 deletions packages/common/src/components/mdx_components/mobile_cover.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import { ButtonBase, Stack, Typography } from "@mui/material";
import * as React from "react";
import { useAppContext } from "../../../../../apps/pyconkr/src/contexts/app_context";
import PyCon2025MobileLogoImage from "../../assets/pyconkr2025_main_cover_image.png";
import PyCon2025MobileLogoTitle from "../../assets/pyconkr2025_main_cover_title.png";

export const MobileCover: React.FC = () => {
const { language } = useAppContext();
const buttonTitle = language === "ko" ? "티켓 구매하기" : "Buy Ticket";
import * as Hooks from "../../hooks";

type MobileCoverProps = {
coverImageSrc: string;
coverTitleSrc: string;
buttonTextKo?: string;
buttonTextEn?: string;
};

export const MobileCover: React.FC<MobileCoverProps> = ({
coverImageSrc,
coverTitleSrc,
buttonTextKo = "티켓 구매하기",
buttonTextEn = "Buy Ticket",
}) => {
const { language } = Hooks.Common.useCommonContext();
const buttonTitle = language === "ko" ? buttonTextKo : buttonTextEn;

return (
<Stack sx={{ display: "flex", flexDirection: "column", position: "relative", width: "100vw", height: "100vh", overflow: "hidden" }}>
<Stack sx={{ zIndex: 1, position: "absolute", top: 0, left: 0, flex: 1, display: "flex", width: "100%" }}>
<img src={PyCon2025MobileLogoImage} alt="Pycon 2025 Mobile Image" style={{ flex: 1, objectFit: "cover" }} />
<img src={coverImageSrc} alt="Mobile Cover Image" style={{ flex: 1, objectFit: "cover" }} />
</Stack>
<Stack sx={{ zIndex: 2, position: "absolute", top: 96, left: 46 }}>
<img src={PyCon2025MobileLogoTitle} alt="Pycon 2025 Mobile Title" style={{ objectFit: "contain" }} />
<img src={coverTitleSrc} alt="Mobile Cover Title" style={{ objectFit: "contain" }} />
</Stack>
<Stack sx={{ zIndex: 3, position: "absolute", top: 351, left: 48 }}>
<ButtonBase
Expand Down
108 changes: 56 additions & 52 deletions packages/common/src/components/mdx_components/session_list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import * as React from "react";
import { Link } from "react-router-dom";
import * as R from "remeda";

import PyCon2025Logo from "../../assets/pyconkr2025_logo.png";
import * as Hooks from "../../hooks";
import * as BackendAPISchemas from "../../schemas/backendAPI";
import { ErrorFallback } from "../error_handler";
Expand All @@ -13,65 +12,72 @@ import { StyledDivider } from "./styled_divider";

const EXCLUDE_CATEGORIES = ["후원사", "Sponsor"];

const SessionItem: React.FC<{ session: BackendAPISchemas.SessionSchema; enableLink?: boolean }> = Suspense.with(
{ fallback: <CircularProgress /> },
({ session, enableLink }) => {
const sessionTitle = session.title.replace("\\n", "\n");

let speakerImgSrc = session.image || "";
if (!speakerImgSrc && R.isArray(session.speakers) && !R.isEmpty(session.speakers)) {
for (const speaker of session.speakers) {
if (speaker.image) {
speakerImgSrc = speaker.image;
break;
}
const SessionItem: React.FC<{
session: BackendAPISchemas.SessionSchema;
enableLink?: boolean;
fallbackImage?: React.ReactNode;
getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string;
}> = Suspense.with({ fallback: <CircularProgress /> }, ({ session, enableLink, fallbackImage, getSessionUrl }) => {
const sessionTitle = session.title.replace("\\n", "\n");

let speakerImgSrc = session.image || "";
if (!speakerImgSrc && R.isArray(session.speakers) && !R.isEmpty(session.speakers)) {
for (const speaker of session.speakers) {
if (speaker.image) {
speakerImgSrc = speaker.image;
break;
}
}
}

const urlSafeTitle = session.title
.replace(/ /g, "-")
.replace(/([.])/g, "_")
.replace(/(?![0-9A-Za-zㄱ-ㅣ가-힣-_])./g, "");
const sessionDetailedUrl = `/presentations/${session.id}#${urlSafeTitle}`;
const result = (
<SessionItemContainer direction="row">
<SessionImageContainer
children={<SessionImage src={speakerImgSrc} alt="Session Image" loading="lazy" errorFallback={<SessionImageErrorFallback />} />}
/>
<Stack direction="column" sx={{ flexGrow: 1, py: 0.5, gap: 0.75 }}>
<SessionTitle children={sessionTitle} />
{session.summary && <Typography variant="subtitle1" sx={{ whiteSpace: "pre-wrap" }} children={session.summary} />}
<Stack direction="row" spacing={0.5}>
{session.speakers.map((speaker) => (
<Chip key={speaker.id} size="small" label={speaker.nickname} />
))}
</Stack>
<Stack direction="row" spacing={0.5}>
{session.categories.map((tag) => (
<Chip key={tag.id} variant="outlined" color="primary" size="small" label={tag.name} />
))}
</Stack>
const sessionDetailedUrl = getSessionUrl ? getSessionUrl(session) : undefined;
const result = (
<SessionItemContainer direction="row">
<SessionImageContainer
children={
<SessionImage
src={speakerImgSrc}
alt="Session Image"
loading="lazy"
errorFallback={<SessionImageErrorFallback>{fallbackImage}</SessionImageErrorFallback>}
/>
}
/>
<Stack direction="column" sx={{ flexGrow: 1, py: 0.5, gap: 0.75 }}>
<SessionTitle children={sessionTitle} />
{session.summary && <Typography variant="subtitle1" sx={{ whiteSpace: "pre-wrap" }} children={session.summary} />}
<Stack direction="row" spacing={0.5}>
{session.speakers.map((speaker) => (
<Chip key={speaker.id} size="small" label={speaker.nickname} />
))}
</Stack>
</SessionItemContainer>
);
return (
<>
{enableLink ? <Link to={sessionDetailedUrl} style={{ textDecoration: "none" }} children={result} /> : result}
<StyledDivider />
</>
);
}
);
<Stack direction="row" spacing={0.5}>
{session.categories.map((tag) => (
<Chip key={tag.id} variant="outlined" color="primary" size="small" label={tag.name} />
))}
</Stack>
</Stack>
</SessionItemContainer>
);
return (
<>
{enableLink && sessionDetailedUrl ? <Link to={sessionDetailedUrl} style={{ textDecoration: "none" }} children={result} /> : result}
<StyledDivider />
</>
);
});

type SessionListPropType = {
event?: string;
types?: string | string[];
enableLink?: boolean;
fallbackImage?: React.ReactNode;
getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string;
};

export const SessionList: React.FC<SessionListPropType> = ErrorBoundary.with(
{ fallback: ErrorFallback },
Suspense.with({ fallback: <CircularProgress /> }, ({ event, types, enableLink }) => {
Suspense.with({ fallback: <CircularProgress /> }, ({ event, types, enableLink, fallbackImage, getSessionUrl }) => {
const { language } = Hooks.Common.useCommonContext();
const backendAPIClient = Hooks.BackendAPI.useBackendClient();
const params = { ...(event && { event }), ...(types && { types: R.isString(types) ? types : types.join(",") }) };
Expand Down Expand Up @@ -122,7 +128,7 @@ export const SessionList: React.FC<SessionListPropType> = ErrorBoundary.with(
)}
</Box>
{filteredSessions.map((s) => (
<SessionItem key={s.id} session={s} enableLink={enableLink} />
<SessionItem key={s.id} session={s} enableLink={enableLink} fallbackImage={fallbackImage} getSessionUrl={getSessionUrl} />
))}
</Box>
);
Expand Down Expand Up @@ -194,10 +200,8 @@ const SessionImageErrorFallbackBox = styled(Box)(({ theme }) => ({
justifyContent: "center",
}));

const SessionImageErrorFallback: React.FC = () => (
<SessionImageErrorFallbackBox>
<img src={PyCon2025Logo} alt="PyCon 2025 Logo" style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" }} />
</SessionImageErrorFallbackBox>
const SessionImageErrorFallback: React.FC<{ children?: React.ReactNode }> = ({ children }) => (
<SessionImageErrorFallbackBox>{children}</SessionImageErrorFallbackBox>
);

const SessionTitle = styled(Typography)({
Expand Down
Loading