diff --git a/apps/pyconkr/src/consts/mdx_components.ts b/apps/pyconkr/src/consts/mdx_components.ts index 8b2b767..3276981 100644 --- a/apps/pyconkr/src/consts/mdx_components.ts +++ b/apps/pyconkr/src/consts/mdx_components.ts @@ -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, @@ -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> = (props) => + React.createElement(Components.MDX.SessionList, { + ...props, + fallbackImage: PyConKR2025FallbackImage, + getSessionUrl: getPyConKR2025SessionUrl, + }); + +const PyConKR2025SessionTimeTable: React.FC> = (props) => + React.createElement(Components.MDX.SessionTimeTable, { + ...props, + getSessionUrl: getPyConKR2025SessionUrl, + }); + +const PyConKR2025MobileAccordion: React.FC = () => + 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 = () => + React.createElement(Components.MDX.MobileCover, { + coverImageSrc: PyCon2025MobileLogoImage, + coverTitleSrc: PyCon2025MobileLogoTitle, + }); + const PyConKRCommonMDXComponents: MDXComponents = { Common__Components__Lottie: Components.LottiePlayer, Common__Components__NetworkLottie: Components.NetworkLottiePlayer, @@ -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 = { diff --git a/packages/common/src/components/index.ts b/packages/common/src/components/index.ts index 8b9914d..24751bc 100644 --- a/packages/common/src/components/index.ts +++ b/packages/common/src/components/index.ts @@ -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"; @@ -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; diff --git a/packages/common/src/components/mdx_components/mobile_accordion.tsx b/packages/common/src/components/mdx_components/mobile_accordion.tsx index 151023b..9940a56 100644 --- a/packages/common/src/components/mdx_components/mobile_accordion.tsx +++ b/packages/common/src/components/mdx_components/mobile_accordion.tsx @@ -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 }, () => ( - AUG 15 - 17 - logo + {marqueeText} + logo )); - }, []); + }, [marqueeText, marqueeLogoSrc]); return ; }; -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 = ({ marqueeText, marqueeLogoSrc, hostLogoBigSrc, venueKo, venueEnLines }) => { + const { language } = Hooks.Common.useCommonContext(); const [expanded, setExpanded] = React.useState(false); return ( @@ -44,27 +50,26 @@ export const MobileAccordion: React.FC = () => { } sx={{ margin: 0, padding: 0 }} > - {expanded ? null : } + {expanded ? null : } - PyCon 2025 Host Logo + Host Logo {language === "ko" ? ( - {"서울특별시 중구 필동로 1길 30 동국대학교 신공학관"} + {venueKo} ) : ( - - {"New Engineering Building, Dongguk University"} - - - {"Pildong-ro 1-gil, Jung-gu, Seoul, Republic of Korea"} - + {venueEnLines.map((line, i) => ( + + {line} + + ))} )} diff --git a/packages/common/src/components/mdx_components/mobile_cover.tsx b/packages/common/src/components/mdx_components/mobile_cover.tsx index b2878c8..cb37c6d 100644 --- a/packages/common/src/components/mdx_components/mobile_cover.tsx +++ b/packages/common/src/components/mdx_components/mobile_cover.tsx @@ -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 = ({ + coverImageSrc, + coverTitleSrc, + buttonTextKo = "티켓 구매하기", + buttonTextEn = "Buy Ticket", +}) => { + const { language } = Hooks.Common.useCommonContext(); + const buttonTitle = language === "ko" ? buttonTextKo : buttonTextEn; return ( - Pycon 2025 Mobile Image + Mobile Cover Image - Pycon 2025 Mobile Title + Mobile Cover Title = Suspense.with( - { fallback: }, - ({ 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: }, ({ 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 = ( - - } />} - /> - - - {session.summary && } - - {session.speakers.map((speaker) => ( - - ))} - - - {session.categories.map((tag) => ( - - ))} - + const sessionDetailedUrl = getSessionUrl ? getSessionUrl(session) : undefined; + const result = ( + + {fallbackImage}} + /> + } + /> + + + {session.summary && } + + {session.speakers.map((speaker) => ( + + ))} - - ); - return ( - <> - {enableLink ? : result} - - - ); - } -); + + {session.categories.map((tag) => ( + + ))} + + + + ); + return ( + <> + {enableLink && sessionDetailedUrl ? : result} + + + ); +}); type SessionListPropType = { event?: string; types?: string | string[]; enableLink?: boolean; + fallbackImage?: React.ReactNode; + getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string; }; export const SessionList: React.FC = ErrorBoundary.with( { fallback: ErrorFallback }, - Suspense.with({ fallback: }, ({ event, types, enableLink }) => { + Suspense.with({ fallback: }, ({ 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(",") }) }; @@ -122,7 +128,7 @@ export const SessionList: React.FC = ErrorBoundary.with( )} {filteredSessions.map((s) => ( - + ))} ); @@ -194,10 +200,8 @@ const SessionImageErrorFallbackBox = styled(Box)(({ theme }) => ({ justifyContent: "center", })); -const SessionImageErrorFallback: React.FC = () => ( - - PyCon 2025 Logo - +const SessionImageErrorFallback: React.FC<{ children?: React.ReactNode }> = ({ children }) => ( + {children} ); const SessionTitle = styled(Typography)({ diff --git a/packages/common/src/components/mdx_components/session_timetable.tsx b/packages/common/src/components/mdx_components/session_timetable.tsx index a646c0a..1453fd5 100644 --- a/packages/common/src/components/mdx_components/session_timetable.tsx +++ b/packages/common/src/components/mdx_components/session_timetable.tsx @@ -107,18 +107,16 @@ const SessionColumn: React.FC<{ rowSpan: number; colSpan?: number; session: BackendAPISchemas.SessionSchema; -}> = ({ rowSpan, colSpan, session }) => { - const clickable = R.isArray(session.speakers) && !R.isEmpty(session.speakers); + getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string; +}> = ({ rowSpan, colSpan, session, getSessionUrl }) => { + const sessionUrl = getSessionUrl ? getSessionUrl(session) : undefined; + const clickable = R.isArray(session.speakers) && !R.isEmpty(session.speakers) && !!sessionUrl; // Firefox는 rowSpan된 td의 height를 계산할 때 rowSpan을 고려하지 않습니다. 따라서 직접 계산하여 height를 설정합니다. const sessionBoxHeight = `${TD_HEIGHT * rowSpan}rem`; - const urlSafeTitle = session.title - .replace(/ /g, "-") - .replace(/([.])/g, "_") - .replace(/(?![.0-9A-Za-zㄱ-ㅣ가-힣-])./g, ""); return ( {clickable ? ( - + = ({ lang type SessionTimeTablePropType = { event?: string; types?: string | string[]; + getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string; }; export const SessionTimeTable: React.FC = ErrorBoundary.with( { fallback: ErrorFallback }, - Suspense.with({ fallback: } /> }, ({ event, types }) => { + Suspense.with({ fallback: } /> }, ({ event, types, getSessionUrl }) => { const [confDate, setConfDate] = React.useState(""); const { language } = Hooks.Common.useCommonContext(); @@ -274,7 +273,7 @@ export const SessionTimeTable: React.FC = ErrorBoundar return ( - + ); } @@ -293,7 +292,7 @@ export const SessionTimeTable: React.FC = ErrorBoundar } // 세션이 여러 줄에 걸쳐있는 경우, n-1 줄만큼 해당 room에 column을 생성하지 않도록 합니다. if (roomDatum.rowSpan > 1) rooms[room] = roomDatum.rowSpan - 1; - return ; + return ; })} );