diff --git a/packages/common/src/assets/pyconkr2025_hostlogo_big.png b/apps/pyconkr/src/assets/pyconkr2025_hostlogo_big.png similarity index 100% rename from packages/common/src/assets/pyconkr2025_hostlogo_big.png rename to apps/pyconkr/src/assets/pyconkr2025_hostlogo_big.png diff --git a/packages/common/src/assets/pyconkr2025_hostlogo_small.png b/apps/pyconkr/src/assets/pyconkr2025_hostlogo_small.png similarity index 100% rename from packages/common/src/assets/pyconkr2025_hostlogo_small.png rename to apps/pyconkr/src/assets/pyconkr2025_hostlogo_small.png diff --git a/packages/common/src/assets/pyconkr2025_main_cover_image.png b/apps/pyconkr/src/assets/pyconkr2025_main_cover_image.png similarity index 100% rename from packages/common/src/assets/pyconkr2025_main_cover_image.png rename to apps/pyconkr/src/assets/pyconkr2025_main_cover_image.png diff --git a/packages/common/src/assets/pyconkr2025_main_cover_title.png b/apps/pyconkr/src/assets/pyconkr2025_main_cover_title.png similarity index 100% rename from packages/common/src/assets/pyconkr2025_main_cover_title.png rename to apps/pyconkr/src/assets/pyconkr2025_main_cover_title.png diff --git a/packages/common/src/components/mdx_components/mobile_accordion.tsx b/apps/pyconkr/src/components/mdx_components/mobile_accordion.tsx similarity index 98% rename from packages/common/src/components/mdx_components/mobile_accordion.tsx rename to apps/pyconkr/src/components/mdx_components/mobile_accordion.tsx index 151023b..6f21d6d 100644 --- a/packages/common/src/components/mdx_components/mobile_accordion.tsx +++ b/apps/pyconkr/src/components/mdx_components/mobile_accordion.tsx @@ -4,7 +4,7 @@ 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 { useAppContext } from "../../contexts/app_context"; import PyCon2025HostLogoBig from "../../assets/pyconkr2025_hostlogo_big.png"; import PyCon2025HostLogoSmall from "../../assets/pyconkr2025_hostlogo_small.png"; diff --git a/packages/common/src/components/mdx_components/mobile_cover.tsx b/apps/pyconkr/src/components/mdx_components/mobile_cover.tsx similarity index 95% rename from packages/common/src/components/mdx_components/mobile_cover.tsx rename to apps/pyconkr/src/components/mdx_components/mobile_cover.tsx index b2878c8..aa0999d 100644 --- a/packages/common/src/components/mdx_components/mobile_cover.tsx +++ b/apps/pyconkr/src/components/mdx_components/mobile_cover.tsx @@ -1,7 +1,7 @@ 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 { useAppContext } from "../../contexts/app_context"; import PyCon2025MobileLogoImage from "../../assets/pyconkr2025_main_cover_image.png"; import PyCon2025MobileLogoTitle from "../../assets/pyconkr2025_main_cover_title.png"; diff --git a/apps/pyconkr/src/consts/mdx_components.ts b/apps/pyconkr/src/consts/mdx_components.ts index ef65626..d25e5f5 100644 --- a/apps/pyconkr/src/consts/mdx_components.ts +++ b/apps/pyconkr/src/consts/mdx_components.ts @@ -3,6 +3,9 @@ import * as Common 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 PyCon2025Logo from "../assets/pyconkr2025_logo.png"; const MUIMDXComponents: MDXComponents = { Mui__material__Accordion: mui.Accordion, @@ -130,17 +133,71 @@ const MUIMDXComponents: MDXComponents = { Mui__material__Zoom: mui.Zoom, }; +const PyConKR2025SecondaryStyledDetails: React.FC> = (props) => { + const theme = mui.useTheme(); + return React.createElement(Common.Components.MDX.SecondaryStyledDetails, { ...props, paletteColor: theme.palette.highlight }); +}; + +const PyConKR2025SessionListFallbackImage = React.createElement("img", { + src: PyCon2025Logo, + alt: "PyCon 2025 Logo", + style: { width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" }, +}); + +const pyConKR2025SessionListStyles: Common.Components.MDX.SessionListStyles = { + categoryButton: { flexBasis: "14rem" }, + itemContainer: { padding: "0.5rem 1.5rem", gap: "1.5rem", minHeight: "9rem", mobilePadding: "0.5rem", mobileGap: "1rem" }, + imageContainer: { size: "6rem" }, + title: { fontSize: "1.5em", fontWeight: 600, lineHeight: 1.25 }, +}; + +const PyConKR2025SessionList: React.FC> = (props) => + React.createElement(Common.Components.MDX.SessionList, { ...props, fallbackImage: PyConKR2025SessionListFallbackImage, styles: pyConKR2025SessionListStyles }); + +const pyConKR2025SessionTimeTableStyles: Common.Components.MDX.SessionTimeTableStyles = { + layout: { tdHeight: 4, tdWidth: 15, tdWidthMobile: 20 }, + dateItemContainer: { padding: "1rem 3rem" }, + dateTitle: { fontSize: "2.25em", fontWeight: 600, lineHeight: 1.25 }, + dateSubTitle: { fontSize: "1em", fontWeight: 600, lineHeight: 1.25 }, + sessionTitle: { fontSize: "1.125em", fontWeight: 600, lineHeight: 1.25 }, + sessionBox: { padding: "0.25rem", borderRadius: "0.5rem", gap: "0.5rem" }, + tableCell: { padding: "0 0.5rem" }, + tableContainer: { gap: "1rem" }, +}; + +const PyConKR2025SessionTimeTable: React.FC> = (props) => + React.createElement(Common.Components.MDX.SessionTimeTable, { ...props, styles: pyConKR2025SessionTimeTableStyles }); + +const pyConKR2025FAQAccordionStyles: Common.Components.MDX.FAQAccordionStyles = { + summary: { padding: "10px 35px", minHeight: "60px", maxHeight: "60px" }, + number: { fontSize: "18px", fontWeight: 400 }, + question: { fontSize: "18px", fontWeight: 400, marginLeft: "60px" }, + details: { fontSize: "14px", fontWeight: 400, padding: "20px 0 20px calc(35px + 18px + 60px)" }, +}; + +const PyConKR2025FAQAccordion: React.FC> = (props) => + React.createElement(Common.Components.MDX.FAQAccordion, { ...props, styles: pyConKR2025FAQAccordionStyles }); + +const pyConKR2025StyledFullWidthButtonStyles: Common.Components.MDX.StyledFullWidthButtonStyles = { + borderRadius: "0.5rem", + textTransform: "none", + largeHeight: "3.5rem", +}; + +const PyConKR2025StyledFullWidthButton: React.FC> = (props) => + React.createElement(Common.Components.MDX.StyledFullWidthButton, { ...props, styles: pyConKR2025StyledFullWidthButtonStyles }); + const PyConKRCommonMDXComponents: MDXComponents = { Common__Components__Lottie: Common.Components.LottiePlayer, Common__Components__NetworkLottie: Common.Components.NetworkLottiePlayer, Common__Components__MDX__Confetti: Common.Components.MDX.Confetti, Common__Components__MDX__PrimaryStyledDetails: Common.Components.MDX.PrimaryStyledDetails, - Common__Components__MDX__SecondaryStyledDetails: Common.Components.MDX.SecondaryStyledDetails, + Common__Components__MDX__SecondaryStyledDetails: PyConKR2025SecondaryStyledDetails, Common__Components__MDX__Map: Common.Components.MDX.Map, - Common__Components__MDX__FAQAccordion: Common.Components.MDX.FAQAccordion, - Common__Components__MDX__FullWidthStyledButton: Common.Components.MDX.StyledFullWidthButton, - Common__Components__Session__List: Common.Components.MDX.SessionList, - Common__Components__Session__TimeTable: Common.Components.MDX.SessionTimeTable, + Common__Components__MDX__FAQAccordion: PyConKR2025FAQAccordion, + Common__Components__MDX__FullWidthStyledButton: PyConKR2025StyledFullWidthButton, + Common__Components__Session__List: PyConKR2025SessionList, + Common__Components__Session__TimeTable: PyConKR2025SessionTimeTable, }; const PythonKRShopMDXComponents: MDXComponents = { diff --git a/packages/common/src/assets/pyconkr2025_logo.png b/packages/common/src/assets/pyconkr2025_logo.png deleted file mode 100755 index a4b1f56..0000000 Binary files a/packages/common/src/assets/pyconkr2025_logo.png and /dev/null differ diff --git a/packages/common/src/components/index.ts b/packages/common/src/components/index.ts index 8b9914d..67ef9dc 100644 --- a/packages/common/src/components/index.ts +++ b/packages/common/src/components/index.ts @@ -17,18 +17,22 @@ import { Confetti as ConfettiComponent } from "./mdx_components/confetti"; import { FAQAccordion as FAQAccordionComponent, type FAQAccordionProps as FAQAccordionPropsType, + type FAQAccordionStyles as FAQAccordionStylesType, type FAQItem as FAQItemType, } from "./mdx_components/faq_accordion"; import type { MapPropType as MapComponentPropType } from "./mdx_components/map"; import { Map as MapComponent } from "./mdx_components/map"; 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"; +import { SessionList as SessionListComponent, type SessionListStyles as SessionListStylesType } from "./mdx_components/session_list"; +import { SessionTimeTable as SessionTimeTableComponent, type SessionTimeTableStyles as SessionTimeTableStylesType } from "./mdx_components/session_timetable"; import { PrimaryStyledDetails as PrimaryStyledDetailsComponent, HighlightedStyledDetails as SecondaryStyledDetailsComponent, } from "./mdx_components/styled_details"; -import { StyledFullWidthButton as StyledFullWidthButtonComponent } from "./mdx_components/styled_full_width_button"; +import { + StyledFullWidthButton as StyledFullWidthButtonComponent, + type StyledFullWidthButtonStyles as StyledFullWidthButtonStylesType, +} from "./mdx_components/styled_full_width_button"; import { MDXEditor as MDXEditorComponent } from "./mdx_editor"; import { PythonKorea as PythonKoreaComponent } from "./pythonkorea"; @@ -61,7 +65,11 @@ namespace Components { export const SessionTimeTable = SessionTimeTableComponent; export type MapPropType = MapComponentPropType; export type FAQAccordionProps = FAQAccordionPropsType; + export type FAQAccordionStyles = FAQAccordionStylesType; export type FAQItem = FAQItemType; + export type SessionListStyles = SessionListStylesType; + export type SessionTimeTableStyles = SessionTimeTableStylesType; + export type StyledFullWidthButtonStyles = StyledFullWidthButtonStylesType; } } diff --git a/packages/common/src/components/mdx_components/faq_accordion.tsx b/packages/common/src/components/mdx_components/faq_accordion.tsx index 39ba1e9..c051f44 100644 --- a/packages/common/src/components/mdx_components/faq_accordion.tsx +++ b/packages/common/src/components/mdx_components/faq_accordion.tsx @@ -9,21 +9,44 @@ export interface FAQItem { answer: string; } +export type FAQAccordionStyles = { + summary?: { + padding?: string; + minHeight?: string; + maxHeight?: string; + }; + number?: { + fontSize?: string; + fontWeight?: number | string; + }; + question?: { + fontSize?: string; + fontWeight?: number | string; + marginLeft?: string; + }; + details?: { + fontSize?: string; + fontWeight?: number | string; + padding?: string; + }; +}; + export interface FAQAccordionProps { items: FAQItem[]; + styles?: FAQAccordionStyles; } -export const FAQAccordion: React.FC = ({ items }) => { +export const FAQAccordion: React.FC = ({ items, styles }) => { return ( {items.map((faq, index) => ( - + } aria-controls={`panel${faq.id}-content`} id={`panel${faq.id}-header`}> - {faq.id} - {faq.question} + {faq.id} + {faq.question} - {faq.answer} + {faq.answer} {index !== items.length - 1 && } @@ -45,59 +68,54 @@ const Divider = styled.div` margin: 0; `; -const StyledAccordion = styled(MuiAccordion)` - box-shadow: none; - border-radius: 0; - - &:before { - display: none; - } - - &.MuiAccordion-root { - margin: 0; - - &:first-of-type { - border-top: none; - } - - &:last-of-type { - border-bottom: none; - } - } - - .MuiAccordionSummary-root { - padding: 10px 35px; - min-height: 60px; - max-height: 60px; - - .MuiAccordionSummary-content { - display: flex; - align-items: center; - margin: 0; - } - - &.Mui-expanded { - min-height: 60px; - max-height: 60px; - } - } -`; - -const Number = styled.span` - font-size: 18px; - font-weight: 400; -`; - -const Question = styled.span` - font-size: 18px; - font-weight: 400; - margin-left: 60px; -`; - -const StyledAccordionDetails = styled(AccordionDetails)` - background-color: ${({ theme }) => `${theme.palette.primary.light}26`}; // 15% opacity (26 in hex) - color: ${({ theme }) => theme.palette.primary.dark}; - font-size: 14px; - font-weight: 400; - padding: 20px 0 20px calc(35px + 18px + 60px); // top right bottom left -`; +const StyledAccordion = styled(MuiAccordion, { + shouldForwardProp: (prop) => prop !== "faqStyles", +})<{ faqStyles?: FAQAccordionStyles }>(({ faqStyles }) => ({ + boxShadow: "none", + borderRadius: 0, + "&:before": { display: "none" }, + "&.MuiAccordion-root": { + margin: 0, + "&:first-of-type": { borderTop: "none" }, + "&:last-of-type": { borderBottom: "none" }, + }, + ".MuiAccordionSummary-root": { + padding: faqStyles?.summary?.padding ?? "10px 35px", + minHeight: faqStyles?.summary?.minHeight ?? "60px", + maxHeight: faqStyles?.summary?.maxHeight ?? "60px", + ".MuiAccordionSummary-content": { + display: "flex", + alignItems: "center", + margin: 0, + }, + "&.Mui-expanded": { + minHeight: faqStyles?.summary?.minHeight ?? "60px", + maxHeight: faqStyles?.summary?.maxHeight ?? "60px", + }, + }, +})); + +const Number = styled("span", { + shouldForwardProp: (prop) => prop !== "faqStyles", +})<{ faqStyles?: FAQAccordionStyles }>(({ faqStyles }) => ({ + fontSize: faqStyles?.number?.fontSize ?? "18px", + fontWeight: faqStyles?.number?.fontWeight ?? 400, +})); + +const Question = styled("span", { + shouldForwardProp: (prop) => prop !== "faqStyles", +})<{ faqStyles?: FAQAccordionStyles }>(({ faqStyles }) => ({ + fontSize: faqStyles?.question?.fontSize ?? "18px", + fontWeight: faqStyles?.question?.fontWeight ?? 400, + marginLeft: faqStyles?.question?.marginLeft ?? "60px", +})); + +const StyledAccordionDetails = styled(AccordionDetails, { + shouldForwardProp: (prop) => prop !== "faqStyles", +})<{ faqStyles?: FAQAccordionStyles }>(({ theme, faqStyles }) => ({ + backgroundColor: `${theme.palette.primary.light}26`, + color: theme.palette.primary.dark, + fontSize: faqStyles?.details?.fontSize ?? "14px", + fontWeight: faqStyles?.details?.fontWeight ?? 400, + padding: faqStyles?.details?.padding ?? "20px 0 20px calc(35px + 18px + 60px)", +})); diff --git a/packages/common/src/components/mdx_components/session_list.tsx b/packages/common/src/components/mdx_components/session_list.tsx index e905e3f..f752b55 100644 --- a/packages/common/src/components/mdx_components/session_list.tsx +++ b/packages/common/src/components/mdx_components/session_list.tsx @@ -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 Hooks from "../../hooks"; import BackendAPISchemas from "../../schemas/backendAPI"; import { ErrorFallback } from "../error_handler"; @@ -13,9 +12,38 @@ import { StyledDivider } from "./styled_divider"; const EXCLUDE_CATEGORIES = ["후원사", "Sponsor"]; -const SessionItem: React.FC<{ session: BackendAPISchemas.SessionSchema; enableLink?: boolean }> = Suspense.with( +export type SessionListStyles = { + categoryButton?: { + flexBasis?: string; + }; + itemContainer?: { + padding?: string; + gap?: string; + minHeight?: string; + mobilePadding?: string; + mobileGap?: string; + }; + imageContainer?: { + size?: string; + }; + title?: { + fontSize?: string; + fontWeight?: number | string; + lineHeight?: number | string; + }; +}; + +type SessionItemProps = { + session: BackendAPISchemas.SessionSchema; + enableLink?: boolean; + fallbackImage?: React.ReactNode; + getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string; + styles?: SessionListStyles; +}; + +const SessionItem: React.FC = Suspense.with( { fallback: }, - ({ session, enableLink }) => { + ({ session, enableLink, fallbackImage, getSessionUrl, styles }) => { const sessionTitle = session.title.replace("\\n", "\n"); let speakerImgSrc = session.image || ""; @@ -32,14 +60,15 @@ const SessionItem: React.FC<{ session: BackendAPISchemas.SessionSchema; enableLi .replace(/ /g, "-") .replace(/([.])/g, "_") .replace(/(?![0-9A-Za-zㄱ-ㅣ가-힣-_])./g, ""); - const sessionDetailedUrl = `/presentations/${session.id}#${urlSafeTitle}`; + const sessionDetailedUrl = getSessionUrl ? getSessionUrl(session) : `/presentations/${session.id}#${urlSafeTitle}`; const result = ( - + } />} + listStyles={styles} + children={} />} /> - + {session.summary && } {session.speakers.map((speaker) => ( @@ -67,11 +96,14 @@ type SessionListPropType = { event?: string; types?: string | string[]; enableLink?: boolean; + fallbackImage?: React.ReactNode; + getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string; + styles?: SessionListStyles; }; export const SessionList: React.FC = ErrorBoundary.with( { fallback: ErrorFallback }, - Suspense.with({ fallback: }, ({ event, types, enableLink }) => { + Suspense.with({ fallback: }, ({ event, types, enableLink, fallbackImage, getSessionUrl, styles }) => { const { language } = Hooks.Common.useCommonContext(); const backendAPIClient = Hooks.BackendAPI.useBackendClient(); const params = { ...(event && { event }), ...(types && { types: R.isString(types) ? types : types.join(",") }) }; @@ -114,6 +146,7 @@ export const SessionList: React.FC = ErrorBoundary.with( onClick={() => toggleCategory(cat.id)} children={cat.name} selected={selectedCategoryIds.some((selectedCatId) => selectedCatId === cat.id)} + listStyles={styles} /> ))} @@ -122,17 +155,19 @@ export const SessionList: React.FC = ErrorBoundary.with( )} {filteredSessions.map((s) => ( - + ))} ); }) ); -const CategoryButtonStyle = styled(Button)<{ selected?: boolean }>(({ theme, selected }) => ({ +const CategoryButtonStyle = styled(Button, { + shouldForwardProp: (prop) => prop !== "selected" && prop !== "listStyles", +})<{ selected?: boolean; listStyles?: SessionListStyles }>(({ theme, selected, listStyles }) => ({ flexGrow: 0, flexShrink: 0, - flexBasis: "14rem", + flexBasis: listStyles?.categoryButton?.flexBasis ?? "14rem", wordBreak: "keep-all", whiteSpace: "nowrap", @@ -144,17 +179,19 @@ const CategoryButtonStyle = styled(Button)<{ selected?: boolean }>(({ theme, sel }, })); -const SessionItemContainer = styled(Stack)(({ theme }) => ({ +const SessionItemContainer = styled(Stack, { + shouldForwardProp: (prop) => prop !== "listStyles", +})<{ listStyles?: SessionListStyles }>(({ theme, listStyles }) => ({ alignItems: "center", justifyContent: "flex-start", - padding: "0.5rem 1.5rem", - gap: "1.5rem", - minHeight: "9rem", + padding: listStyles?.itemContainer?.padding ?? "0.5rem 1.5rem", + gap: listStyles?.itemContainer?.gap ?? "1.5rem", + minHeight: listStyles?.itemContainer?.minHeight ?? "9rem", [theme.breakpoints.down("md")]: { fontSize: "0.75rem", - padding: "0.5rem", - gap: "1rem", + padding: listStyles?.itemContainer?.mobilePadding ?? "0.5rem", + gap: listStyles?.itemContainer?.mobileGap ?? "1rem", "& .MuiChip-labelSmall": { fontSize: "0.75em", @@ -162,21 +199,24 @@ const SessionItemContainer = styled(Stack)(({ theme }) => ({ }, })); -const SessionImageContainer = styled(Stack)({ - alignItems: "center", - justifyContent: "center", - - width: "6rem", - minWidth: "6rem", - maxWidth: "6rem", - height: "6rem", - minHeight: "6rem", - maxHeight: "6rem", +const SessionImageContainer = styled(Stack, { + shouldForwardProp: (prop) => prop !== "listStyles", +})<{ listStyles?: SessionListStyles }>(({ listStyles }) => { + const size = listStyles?.imageContainer?.size ?? "6rem"; + return { + alignItems: "center", + justifyContent: "center", + width: size, + minWidth: size, + maxWidth: size, + height: size, + minHeight: size, + maxHeight: size, + }; }); const SessionImage = styled(FallbackImage)(({ theme }) => ({ border: `1px solid color-mix(in srgb, ${theme.palette.primary.light} 50%, transparent 50%)`, - width: "100%", height: "100%", borderRadius: "50%", @@ -188,22 +228,21 @@ const SessionImageErrorFallbackBox = styled(Box)(({ theme }) => ({ height: "100%", borderRadius: "50%", border: `1px solid color-mix(in srgb, ${theme.palette.primary.light} 50%, transparent 50%)`, - display: "flex", alignItems: "center", justifyContent: "center", })); -const SessionImageErrorFallback: React.FC = () => ( - - PyCon 2025 Logo - +const SessionImageErrorFallback: React.FC<{ fallbackImage?: React.ReactNode }> = ({ fallbackImage }) => ( + {fallbackImage} ); -const SessionTitle = styled(Typography)({ - fontSize: "1.5em", - fontWeight: 600, - lineHeight: 1.25, +const SessionTitle = styled(Typography, { + shouldForwardProp: (prop) => prop !== "listStyles", +})<{ listStyles?: SessionListStyles }>(({ listStyles }) => ({ + fontSize: listStyles?.title?.fontSize ?? "1.5em", + fontWeight: listStyles?.title?.fontWeight ?? 600, + lineHeight: listStyles?.title?.lineHeight ?? 1.25, textDecoration: "none", whiteSpace: "pre-wrap", -}); +})); diff --git a/packages/common/src/components/mdx_components/session_timetable.tsx b/packages/common/src/components/mdx_components/session_timetable.tsx index 9f02cf5..13f6c54 100644 --- a/packages/common/src/components/mdx_components/session_timetable.tsx +++ b/packages/common/src/components/mdx_components/session_timetable.tsx @@ -15,6 +15,43 @@ const TD_HEIGHT = 4; const TD_WIDTH = 15; const TD_WIDTH_MOBILE = 20; +export type SessionTimeTableStyles = { + layout?: { + tdHeight?: number; + tdWidth?: number; + tdWidthMobile?: number; + }; + dateItemContainer?: { + padding?: string; + }; + dateTitle?: { + fontSize?: string; + fontWeight?: number | string; + lineHeight?: number | string; + }; + dateSubTitle?: { + fontSize?: string; + fontWeight?: number | string; + lineHeight?: number | string; + }; + sessionTitle?: { + fontSize?: string; + fontWeight?: number | string; + lineHeight?: number | string; + }; + sessionBox?: { + padding?: string; + borderRadius?: string; + gap?: string; + }; + tableCell?: { + padding?: string; + }; + tableContainer?: { + gap?: string; + }; +}; + type TimeTableData = { [date: string]: { [time: string]: { @@ -107,23 +144,28 @@ const SessionColumn: React.FC<{ rowSpan: number; colSpan?: number; session: BackendAPISchemas.SessionSchema; -}> = ({ rowSpan, colSpan, session }) => { + getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string; + styles?: SessionTimeTableStyles; +}> = ({ rowSpan, colSpan, session, getSessionUrl, styles }) => { const clickable = R.isArray(session.speakers) && !R.isEmpty(session.speakers); // Firefox는 rowSpan된 td의 height를 계산할 때 rowSpan을 고려하지 않습니다. 따라서 직접 계산하여 height를 설정합니다. - const sessionBoxHeight = `${TD_HEIGHT * rowSpan}rem`; + const tdHeight = styles?.layout?.tdHeight ?? TD_HEIGHT; + const sessionBoxHeight = `${tdHeight * rowSpan}rem`; const urlSafeTitle = session.title .replace(/ /g, "-") .replace(/([.])/g, "_") .replace(/(?![.0-9A-Za-zㄱ-ㅣ가-힣-])./g, ""); + const sessionUrl = getSessionUrl ? getSessionUrl(session) : `/presentations/${session.id}#${urlSafeTitle}`; return ( - + {clickable ? ( - + - + {session.speakers.map((speaker) => ( @@ -133,9 +175,10 @@ const SessionColumn: React.FC<{ ) : ( - + {session.speakers.map((speaker) => ( @@ -155,11 +198,13 @@ const BreakTime: React.FC<{ language: "ko" | "en"; duration: number }> = ({ lang type SessionTimeTablePropType = { event?: string; types?: string | string[]; + getSessionUrl?: (session: BackendAPISchemas.SessionSchema) => string; + styles?: SessionTimeTableStyles; }; export const SessionTimeTable: React.FC = ErrorBoundary.with( { fallback: ErrorFallback }, - Suspense.with({ fallback: } /> }, ({ event, types }) => { + Suspense.with({ fallback: } /> }, ({ event, types, getSessionUrl, styles }) => { const [confDate, setConfDate] = React.useState(""); const { language } = Hooks.Common.useCommonContext(); @@ -192,21 +237,21 @@ export const SessionTimeTable: React.FC = ErrorBoundar const dateStr = DateTime.fromISO(date).setLocale(language).toLocaleString({ weekday: "long", month: "long", day: "numeric" }); return ( ); })} - - + + - + {sortedRoomList.map((room) => ( - + = ErrorBoundar ))} - } /> {/* dummy first row */} + } /> {/* dummy first row */} {Object.entries(selectedTableData).map(([time, roomData], i, a) => { const hasSession = Object.values(rooms).some((c) => c >= 1) || Object.values(roomData).some((room) => room !== undefined); @@ -232,13 +277,15 @@ export const SessionTimeTable: React.FC = ErrorBoundar breakCount += 1; } + const tdHeight = styles?.layout?.tdHeight ?? TD_HEIGHT; // I really hate this, but I can't think of a better way to do this. - const height = (TD_HEIGHT * breakCount) / (breakCount <= 2 ? 1 : 3); + const height = (tdHeight * breakCount) / (breakCount <= 2 ? 1 : 3); const isLast = i === a.length - 1; const duration = breakCount * 10; // 10 minutes per row return ( = ErrorBoundar {time} = ErrorBoundar Object.keys(rooms).forEach((room) => (rooms[room] = firstSessionInfo.rowSpan - 1)); return ( - - + + ); } return ( - + {sortedRoomList.map((room) => { const roomDatum = roomData[room]; if (roomDatum === undefined) { // 진행 중인 세션이 없는 경우, 해당 줄에서는 해당 room의 빈 column을 생성합니다. - if (rooms[room] <= 0) return ; + if (rooms[room] <= 0) return ; // 진행 중인 세션이 있는 경우, 이번 줄에서는 해당 세션들만큼 column을 생성하지 않습니다. rooms[room] -= 1; return null; } // 세션이 여러 줄에 걸쳐있는 경우, n-1 줄만큼 해당 room에 column을 생성하지 않도록 합니다. if (roomDatum.rowSpan > 1) rooms[room] = roomDatum.rowSpan - 1; - return ; + return ; })} ); @@ -306,82 +354,91 @@ export const SessionTimeTable: React.FC = ErrorBoundar }) ); -const SessionDateItemContainer = styled(Stack)({ +const SessionDateItemContainer = styled(Stack, { + shouldForwardProp: (prop) => prop !== "tableStyles", +})<{ tableStyles?: SessionTimeTableStyles }>(({ tableStyles }) => ({ alignItems: "center", justifyContent: "center", - padding: "1rem 3rem", -}); + padding: tableStyles?.dateItemContainer?.padding ?? "1rem 3rem", +})); -const SessionDateTitle = styled(Typography)<{ isSelected: boolean }>(({ theme, isSelected }) => ({ - fontSize: "2.25em", - fontWeight: 600, - lineHeight: 1.25, +const SessionDateTitle = styled(Typography, { + shouldForwardProp: (prop) => prop !== "isSelected" && prop !== "tableStyles", +})<{ isSelected: boolean; tableStyles?: SessionTimeTableStyles }>(({ theme, isSelected, tableStyles }) => ({ + fontSize: tableStyles?.dateTitle?.fontSize ?? "2.25em", + fontWeight: tableStyles?.dateTitle?.fontWeight ?? 600, + lineHeight: tableStyles?.dateTitle?.lineHeight ?? 1.25, textDecoration: "none", whiteSpace: "pre-wrap", color: isSelected ? theme.palette.primary.main : theme.palette.primary.light, })); -const SessionDateSubTitle = styled(Typography)<{ isSelected: boolean }>(({ theme, isSelected }) => ({ - fontSize: "1em", - fontWeight: 600, - lineHeight: 1.25, +const SessionDateSubTitle = styled(Typography, { + shouldForwardProp: (prop) => prop !== "isSelected" && prop !== "tableStyles", +})<{ isSelected: boolean; tableStyles?: SessionTimeTableStyles }>(({ theme, isSelected, tableStyles }) => ({ + fontSize: tableStyles?.dateSubTitle?.fontSize ?? "1em", + fontWeight: tableStyles?.dateSubTitle?.fontWeight ?? 600, + lineHeight: tableStyles?.dateSubTitle?.lineHeight ?? 1.25, textDecoration: "none", whiteSpace: "pre-wrap", color: isSelected ? theme.palette.primary.main : theme.palette.primary.light, })); -const SessionTitle = styled(Typography)({ - fontSize: "1.125em", - fontWeight: 600, - lineHeight: 1.25, +const SessionTitle = styled(Typography, { + shouldForwardProp: (prop) => prop !== "tableStyles", +})<{ tableStyles?: SessionTimeTableStyles }>(({ tableStyles }) => ({ + fontSize: tableStyles?.sessionTitle?.fontSize ?? "1.125em", + fontWeight: tableStyles?.sessionTitle?.fontWeight ?? 600, + lineHeight: tableStyles?.sessionTitle?.lineHeight ?? 1.25, textDecoration: "none", whiteSpace: "pre-wrap", -}); - -const SessionTable = styled(Table)({ - width: "100%", - maxWidth: "60rem", - alignItems: "center", - justifyContent: "center", - gap: "1rem", - flex: 1, - - "*": { - textAlign: "center", - }, - - "tbody > th": { - border: "unset", - }, +})); - "tr:first-child td": { - borderTop: "unset", - transform: "unset", - height: `${TD_HEIGHT / 2}rem`, - }, +const SessionTable = styled(Table, { + shouldForwardProp: (prop) => prop !== "tableStyles", +})<{ tableStyles?: SessionTimeTableStyles }>(({ tableStyles }) => { + const tdHeight = tableStyles?.layout?.tdHeight ?? TD_HEIGHT; + const tdWidth = tableStyles?.layout?.tdWidth ?? TD_WIDTH; + const tdWidthMobile = tableStyles?.layout?.tdWidthMobile ?? TD_WIDTH_MOBILE; + + return { + width: "100%", + maxWidth: "60rem", + alignItems: "center", + justifyContent: "center", + gap: "1rem", + flex: 1, + + "*": { textAlign: "center" }, + "tbody > th": { border: "unset" }, + + "tr:first-child td": { + borderTop: "unset", + transform: "unset", + height: `${tdHeight / 2}rem`, + }, - td: { - height: `${TD_HEIGHT}rem`, - }, + td: { height: `${tdHeight}rem` }, - "td:first-child": { - borderTop: "unset", - transform: `translateY(-${TD_HEIGHT / 2}rem)`, - width: "1.5rem", - }, - - "td:not(:first-child)": { - width: `${TD_WIDTH}vw`, - maxWidth: `${TD_WIDTH}vw`, - borderTop: `1px solid rgba(255, 255, 255, 0.1)`, - }, + "td:first-child": { + borderTop: "unset", + transform: `translateY(-${tdHeight / 2}rem)`, + width: "1.5rem", + }, - "@media only screen and (max-width: 810px)": { "td:not(:first-child)": { - width: `${TD_WIDTH_MOBILE}vw`, - maxWidth: `${TD_WIDTH_MOBILE}vw`, + width: `${tdWidth}vw`, + maxWidth: `${tdWidth}vw`, + borderTop: `1px solid rgba(255, 255, 255, 0.1)`, }, - }, + + "@media only screen and (max-width: 810px)": { + "td:not(:first-child)": { + width: `${tdWidthMobile}vw`, + maxWidth: `${tdWidthMobile}vw`, + }, + }, + }; }); const SessionTableBody = styled(TableBody)({ @@ -393,29 +450,30 @@ const SessionTableRow = styled(TableRow)({ justifyContent: "center", }); -const SessionTableCell = styled(TableCell)({ - padding: "0 0.5rem", +const SessionTableCell = styled(TableCell, { + shouldForwardProp: (prop) => prop !== "tableStyles", +})<{ tableStyles?: SessionTimeTableStyles }>(({ tableStyles }) => ({ + padding: tableStyles?.tableCell?.padding ?? "0 0.5rem", alignItems: "center", justifyContent: "center", border: "unset", -}); +})); -const SessionBox = styled(Stack)(({ theme }) => ({ +const SessionBox = styled(Stack, { + shouldForwardProp: (prop) => prop !== "tableStyles", +})<{ tableStyles?: SessionTimeTableStyles }>(({ theme, tableStyles }) => ({ height: "100%", - padding: "0.25rem", + padding: tableStyles?.sessionBox?.padding ?? "0.25rem", flexDirection: "column", justifyContent: "center", alignItems: "center", border: `1px solid color-mix(in srgb, ${theme.palette.primary.light} 50%, transparent 50%)`, - borderRadius: "0.5rem", - + borderRadius: tableStyles?.sessionBox?.borderRadius ?? "0.5rem", backgroundColor: `${theme.palette.primary.light}1A`, transition: "all 0.25s ease", - gap: "0.5rem", + gap: tableStyles?.sessionBox?.gap ?? "0.5rem", - "&.clickable": { - cursor: "pointer", - }, + "&.clickable": { cursor: "pointer" }, h6: { margin: 0, @@ -463,10 +521,12 @@ const SessionBox = styled(Stack)(({ theme }) => ({ }, })); -const SessionTableContainer = styled(Stack)({ +const SessionTableContainer = styled(Stack, { + shouldForwardProp: (prop) => prop !== "tableStyles", +})<{ tableStyles?: SessionTimeTableStyles }>(({ tableStyles }) => ({ flexDirection: "column", alignItems: "center", justifyContent: "center", - gap: "1rem", + gap: tableStyles?.tableContainer?.gap ?? "1rem", flex: 1, -}); +})); diff --git a/packages/common/src/components/mdx_components/styled_details.tsx b/packages/common/src/components/mdx_components/styled_details.tsx index be82e96..dedee4f 100644 --- a/packages/common/src/components/mdx_components/styled_details.tsx +++ b/packages/common/src/components/mdx_components/styled_details.tsx @@ -76,7 +76,7 @@ export const PrimaryStyledDetails: React.FC = (props) => { return ; }; -export const HighlightedStyledDetails: React.FC = (props) => { +export const HighlightedStyledDetails: React.FC = ({ paletteColor, ...props }) => { const { palette } = useTheme(); - return ; + return ; }; diff --git a/packages/common/src/components/mdx_components/styled_full_width_button.tsx b/packages/common/src/components/mdx_components/styled_full_width_button.tsx index 8272ce2..36049aa 100644 --- a/packages/common/src/components/mdx_components/styled_full_width_button.tsx +++ b/packages/common/src/components/mdx_components/styled_full_width_button.tsx @@ -4,13 +4,20 @@ import * as R from "remeda"; import { LinkHandler } from "../link_handler"; +export type StyledFullWidthButtonStyles = { + borderRadius?: string; + textTransform?: React.CSSProperties["textTransform"]; + largeHeight?: string; +}; + type StyledFullWidthButtonPropType = ButtonProps & { link?: string; setBackgroundColor?: boolean; transparency: number; + styles?: StyledFullWidthButtonStyles; }; -export const StyledFullWidthButton: React.FC = ({ link, setBackgroundColor, transparency, ...props }) => { +export const StyledFullWidthButton: React.FC = ({ link, setBackgroundColor, transparency, styles, ...props }) => { let children = props.children; if (React.isValidElement(children) && R.isString((children.props as { children: unknown }).children)) children = (children.props as { children: unknown }).children as string; @@ -21,8 +28,8 @@ export const StyledFullWidthButton: React.FC = ({ fullWidth variant="outlined" sx={({ palette }) => ({ - borderRadius: "0.5rem", - textTransform: "none", + borderRadius: styles?.borderRadius ?? "0.5rem", + textTransform: styles?.textTransform ?? "none", color: palette.primary.dark, borderColor: palette.primary.dark, backgroundColor: setBackgroundColor ? `color-mix(in srgb, ${palette.primary.light} ${transparency || 10}%, transparent)` : "transparent", @@ -31,7 +38,7 @@ export const StyledFullWidthButton: React.FC = ({ ? `color-mix(in srgb, ${palette.primary.light} ${transparency || 20}%, transparent)` : `color-mix(in srgb, ${palette.primary.light} ${transparency || 10}%, transparent)`, }, - "&.MuiButton-sizeLarge": { height: "3.5rem" }, + "&.MuiButton-sizeLarge": { height: styles?.largeHeight ?? "3.5rem" }, })} {...props} children={children}