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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCommonContext } from "@frontend/common/hooks/useCommonContext";
import { UserSignInAccount, UserSignInMethod } from "@frontend/shop/components/common";
import { useShopClient, useSignOutMutation, useUserStatus } from "@frontend/shop/hooks";
import { UserSignedInStatus } from "@frontend/shop/schemas";
import { AccountCircle, Login, Logout, ManageAccounts, Receipt } from "@mui/icons-material";
import { AccountCircle, CalendarMonth, Login, Logout, ManageAccounts, Receipt } from "@mui/icons-material";
import { Button, Divider, IconButton, ListItemIcon, ListItemText, Menu, MenuItem, styled, Typography } from "@mui/material";
import { ErrorBoundary, Suspense } from "@suspensive/react";
import { FC, MouseEvent, useState } from "react";
Expand Down Expand Up @@ -52,6 +52,7 @@ const InnerUserMenuButton: FC<InnerUserMenuButtonPropType> = ({ loading, user, o
const handleMenuClose = () => setAnchorEl(null);

const signInLabel = language === "ko" ? "로그인" : "Sign In";
const myTimetableLabel = language === "ko" ? "나의 세션 시간표 보기" : "View My Session Schedule";
const orderHistoryLabel = language === "ko" ? "결제 내역" : "Order History";
const manageAccountLabel = language === "ko" ? "계정 관리" : "Manage Account";
const signOutLabel = language === "ko" ? "로그아웃" : "Sign Out";
Expand Down Expand Up @@ -109,6 +110,12 @@ const InnerUserMenuButton: FC<InnerUserMenuButtonPropType> = ({ loading, user, o
</Typography>
</UserNameItem>,
<Divider key="divider" sx={{ my: 0.5 }} />,
<MenuItem key="my-timetable" component={RouterLink} to="/my-timetable" onClick={closeAll}>
<ListItemIcon>
<CalendarMonth fontSize="small" />
</ListItemIcon>
<ListItemText>{myTimetableLabel}</ListItemText>
</MenuItem>,
<MenuItem key="orders" component={RouterLink} to="/store/order-histories" onClick={closeAll}>
<ListItemIcon>
<Receipt fontSize="small" />
Expand Down
76 changes: 76 additions & 0 deletions apps/pyconkr-2026/src/components/pages/my-timetable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useBackendClient, useSessionsQuery } from "@frontend/common/hooks/useAPI";
import { getSessionDetailUrl } from "@frontend/common/utils";
import { useShopClient, useUserStatus } from "@frontend/shop/hooks";
import { Button, CircularProgress, Stack, Typography } from "@mui/material";
import { FC, useEffect, useMemo } from "react";
import { Link as RouterLink } from "react-router-dom";

import { PageLayout } from "@apps/pyconkr-2026/components/layout/PageLayout";
import { EVENT_NAME } from "@apps/pyconkr-2026/consts";
import { useAppContext } from "@apps/pyconkr-2026/contexts/app_context";
import { MyTimetableGrid, TimetablePlacement, TRACKS } from "@apps/pyconkr-2026/features/schedule/my_timetable_grid";
import { useMyScheduleQuery } from "@apps/pyconkr-2026/features/schedule/use_my_schedule";

export const MyTimetablePage: FC = () => {
const { language, setAppContext } = useAppContext();
const isKo = language === "ko";
const backendClient = useBackendClient();
const { data: user } = useUserStatus(useShopClient());
const isAuthenticated = user?.meta.is_authenticated === true;
const { data: sessions } = useSessionsQuery(backendClient, { event: EVENT_NAME, types: "세션" });
const eventId = sessions[0]?.presentation_type.event.id;
const { data: bookmarks, isLoading } = useMyScheduleQuery(backendClient, eventId, isAuthenticated);
const placements = useMemo(() => {
const selectedIds = new Set(bookmarks?.presentation_ids ?? []);
return sessions.flatMap((session) =>
selectedIds.has(session.id)
? (session.room_schedules.length === TRACKS.length ? session.room_schedules.slice(0, 1) : session.room_schedules).flatMap(
(schedule): TimetablePlacement[] => {
const track = TRACKS.find(({ roomOrder }) => roomOrder === schedule.room_order);
const startMs = Date.parse(schedule.start_at);
const endMs = Date.parse(schedule.end_at);
if (!track || !Number.isFinite(startMs) || !Number.isFinite(endMs) || startMs >= endMs) return [];
return [
{
key: `${session.id}:${schedule.id}`,
trackKey: track.key,
startMs,
endMs,
title: session.title,
speakers: session.speakers.map(({ nickname }) => nickname).join(", ") || undefined,
href: getSessionDetailUrl(session),
trackSpan: session.room_schedules.length === TRACKS.length ? TRACKS.length : undefined,
},
];
}
)
: []
);
}, [bookmarks, sessions]);

useEffect(() => {
setAppContext((prev) => ({
...prev,
title: isKo ? "내 시간표" : "My Schedule",
shouldShowTitleBanner: true,
shouldShowSponsorBanner: false,
}));
}, [isKo, setAppContext]);

if (!isAuthenticated) {
return (
<PageLayout>
<Stack alignItems="center" spacing={2} sx={{ py: 6, textAlign: "center" }}>
<Typography variant="h5" fontWeight={800} children={isKo ? "로그인이 필요해요" : "Sign in required"} />
<Typography
color="text.secondary"
children={isKo ? "나의 세션 시간표를 보려면 로그인해 주세요." : "Sign in to view your session schedule."}
/>
<Button component={RouterLink} to="/account/sign-in" variant="contained" children={isKo ? "로그인하기" : "Sign in"} />
</Stack>
</PageLayout>
);
}

return <PageLayout>{isLoading ? <CircularProgress /> : <MyTimetableGrid placements={placements} />}</PageLayout>;
};
13 changes: 11 additions & 2 deletions apps/pyconkr-2026/src/consts/mdx_components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
SessionTimeTableTransposed,
StyledFullWidthButton,
} from "@frontend/common/components/mdx_components";
import type { SessionSchema } from "@frontend/common/schemas/backendAPI";
import { PriceDisplay, ShopContextProvider, SignInGuard, UserSignInAccount, UserSignInMethod } from "@frontend/shop/components/common";
import { CartStatus, OrderList, PatronList, ProductImageCardList, ProductList, UserInfo } from "@frontend/shop/components/features";
import {
Expand Down Expand Up @@ -146,7 +147,9 @@ import {
Zoom,
} from "@mui/material";
import type { MDXComponents } from "mdx/types.js";
import { FC, createElement } from "react";
import { ComponentProps, FC, createElement } from "react";

import { SessionScheduleToggleButton } from "@apps/pyconkr-2026/features/schedule/toggle_schedule_button";
const MUIMDXComponents: MDXComponents = {
Mui__material__Accordion: Accordion,
Mui__material__AccordionActions: AccordionActions,
Expand Down Expand Up @@ -288,6 +291,12 @@ const PyConKR2025MobileCover: FC<object> = () =>
coverTitleSrc: PyCon2025MobileLogoTitle,
});

const PyConKR2026SessionList: FC<ComponentProps<typeof SessionList>> = (props) =>
createElement(SessionList, {
...props,
renderAction: (session: SessionSchema) => createElement(SessionScheduleToggleButton, { session }),
});

const PyConKRCommonMDXComponents: MDXComponents = {
Common__Components__Lottie: LottiePlayer,
Common__Components__NetworkLottie: NetworkLottiePlayer,
Expand All @@ -297,7 +306,7 @@ const PyConKRCommonMDXComponents: MDXComponents = {
Common__Components__MDX__Map: MDXMap,
Common__Components__MDX__FAQAccordion: FAQAccordion,
Common__Components__MDX__FullWidthStyledButton: StyledFullWidthButton,
Common__Components__Session__List: SessionList,
Common__Components__Session__List: PyConKR2026SessionList,
Common__Components__Session__TimeTable: SessionTimeTable,
Common__Components__Session__TimeTableTransposed: SessionTimeTableTransposed,
Common__Components__MDX__MobileAccordion: PyConKR2025MobileAccordion,
Expand Down
Loading