From 7a33a00ab611c06ef7cd8f00f7a8215383242697 Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Fri, 23 May 2025 23:18:03 +0800 Subject: [PATCH 1/6] save --- .vscode/settings.json | 5 +- astro.config.mts | 15 +++-- cspell.json | 8 ++- package.json | 3 +- pnpm-lock.yaml | 31 +++++---- src/pages/calendar/Schedule.tsx | 114 ++++++++++++++++++++++++++++++++ src/pages/calendar/index.astro | 4 +- 7 files changed, 154 insertions(+), 26 deletions(-) create mode 100644 src/pages/calendar/Schedule.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index b0d60c1..ca847d7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,7 +8,10 @@ ], "eslint.experimental.useFlatConfig": true, "cSpell.words": [ + "ical", "Logto", - "qrcode" + "qrcode", + "Subcomponents", + "vevent" ], } diff --git a/astro.config.mts b/astro.config.mts index 38a9ef1..3ab3327 100644 --- a/astro.config.mts +++ b/astro.config.mts @@ -14,10 +14,10 @@ export default defineConfig({ syntaxHighlight: "shiki", shikiConfig: { themes: { - light: 'github-light', - dark: 'github-dark', + light: "github-light", + dark: "github-dark", }, - } + }, }, integrations: [ vue(), @@ -39,7 +39,10 @@ export default defineConfig({ target: "http://localhost:4000", rewrite: path => path.replace(/^\/saturday/, ""), }, - } - } - } + }, + }, + // optimizeDeps: { + // exclude: ["dayjs"], + // }, + }, }) diff --git a/cspell.json b/cspell.json index d16f8f1..efe88c3 100644 --- a/cspell.json +++ b/cspell.json @@ -21,6 +21,10 @@ "Rehype", "rehypePlugins", "shiki", - "tseslint" + "tseslint", + "ical", + "ICAL", + "vevents", + "getAllSubcomponents" ] -} +} \ No newline at end of file diff --git a/package.json b/package.json index 1f11ac5..45a6bf2 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,13 @@ "@fullcalendar/react": "^6.1.17", "@headlessui/vue": "^1.7.23", "@heroui/react": "2.7.6", + "@internationalized/date": "^3.8.0", "@logto/browser": "^2.2.18", "@stylistic/eslint-plugin": "^2.13.0", "astro": "^4.16.18", "dayjs": "^1.11.13", "framer-motion": "^11.18.2", - "ical.js": "^1.5.0", + "ical.js": "^2.1.0", "md5": "^2.3.0", "openapi-fetch": "^0.12.5", "qrcode": "^1.5.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 456a3bb..51883db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,7 +28,7 @@ importers: version: 6.1.17(@fullcalendar/core@6.1.17) '@fullcalendar/icalendar': specifier: ^6.1.17 - version: 6.1.17(@fullcalendar/core@6.1.17)(ical.js@1.5.0) + version: 6.1.17(@fullcalendar/core@6.1.17)(ical.js@2.1.0) '@fullcalendar/react': specifier: ^6.1.17 version: 6.1.17(@fullcalendar/core@6.1.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -38,6 +38,9 @@ importers: '@heroui/react': specifier: 2.7.6 version: 2.7.6(@types/react@18.3.20)(framer-motion@11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(tailwindcss@3.4.17) + '@internationalized/date': + specifier: ^3.8.0 + version: 3.8.0 '@logto/browser': specifier: ^2.2.18 version: 2.2.18 @@ -54,8 +57,8 @@ importers: specifier: ^11.18.2 version: 11.18.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) ical.js: - specifier: ^1.5.0 - version: 1.5.0 + specifier: ^2.1.0 + version: 2.1.0 md5: specifier: ^2.3.0 version: 2.3.0 @@ -3748,8 +3751,8 @@ packages: engines: {node: '>=18'} hasBin: true - ical.js@1.5.0: - resolution: {integrity: sha512-7ZxMkogUkkaCx810yp0ZGKvq1ZpRgJeornPttpoxe6nYZ3NLesZe1wWMXDdwTkj/b5NtXT+Y16Aakph/ao98ZQ==} + ical.js@2.1.0: + resolution: {integrity: sha512-BOVfrH55xQ6kpS3muGvIXIg2l7p+eoe12/oS7R5yrO3TL/j/bLsR0PR+tYQESFbyTbvGgPHn9zQ6tI4FWyuSaQ==} ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} @@ -6190,10 +6193,10 @@ snapshots: dependencies: '@fullcalendar/core': 6.1.17 - '@fullcalendar/icalendar@6.1.17(@fullcalendar/core@6.1.17)(ical.js@1.5.0)': + '@fullcalendar/icalendar@6.1.17(@fullcalendar/core@6.1.17)(ical.js@2.1.0)': dependencies: '@fullcalendar/core': 6.1.17 - ical.js: 1.5.0 + ical.js: 2.1.0 '@fullcalendar/react@6.1.17(@fullcalendar/core@6.1.17)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -7509,7 +7512,7 @@ snapshots: '@react-aria/calendar@3.7.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@internationalized/date': 3.7.0 + '@internationalized/date': 3.8.0 '@react-aria/i18n': 3.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-aria/interactions': 3.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@react-aria/live-announcer': 3.4.2 @@ -7561,7 +7564,7 @@ snapshots: '@react-aria/datepicker@3.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@internationalized/date': 3.7.0 + '@internationalized/date': 3.8.0 '@internationalized/number': 3.6.1 '@internationalized/string': 3.2.6 '@react-aria/focus': 3.20.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -8102,7 +8105,7 @@ snapshots: '@react-stately/calendar@3.7.1(react@18.3.1)': dependencies: - '@internationalized/date': 3.7.0 + '@internationalized/date': 3.8.0 '@react-stately/utils': 3.10.5(react@18.3.1) '@react-types/calendar': 3.6.1(react@18.3.1) '@react-types/shared': 3.28.0(react@18.3.1) @@ -8145,7 +8148,7 @@ snapshots: '@react-stately/datepicker@3.13.0(react@18.3.1)': dependencies: - '@internationalized/date': 3.7.0 + '@internationalized/date': 3.8.0 '@internationalized/string': 3.2.6 '@react-stately/form': 3.1.3(react@18.3.1) '@react-stately/overlays': 3.6.15(react@18.3.1) @@ -8381,7 +8384,7 @@ snapshots: '@react-types/calendar@3.6.1(react@18.3.1)': dependencies: - '@internationalized/date': 3.7.0 + '@internationalized/date': 3.8.0 '@react-types/shared': 3.28.0(react@18.3.1) react: 18.3.1 @@ -8408,7 +8411,7 @@ snapshots: '@react-types/datepicker@3.11.0(react@18.3.1)': dependencies: - '@internationalized/date': 3.7.0 + '@internationalized/date': 3.8.0 '@react-types/calendar': 3.7.0(react@18.3.1) '@react-types/overlays': 3.8.14(react@18.3.1) '@react-types/shared': 3.29.0(react@18.3.1) @@ -10447,7 +10450,7 @@ snapshots: husky@9.1.7: {} - ical.js@1.5.0: {} + ical.js@2.1.0: {} ignore@5.3.2: {} diff --git a/src/pages/calendar/Schedule.tsx b/src/pages/calendar/Schedule.tsx new file mode 100644 index 0000000..d4ec55d --- /dev/null +++ b/src/pages/calendar/Schedule.tsx @@ -0,0 +1,114 @@ +import ICAL from "ical.js" +import dayjs from "dayjs" +import { useEffect, useState } from "react" +import { Calendar } from "@heroui/react" +import { today, getLocalTimeZone } from "@internationalized/date" + +type ScheduleEvent = { + start: Date + end: Date + summary: string + description: string + recurrenceId?: string +} + +const parseCal = async (): Promise => { + const res = await fetch("https://ical.nbtca.space/").then(res => res.text()) + const jcalData = ICAL.parse(res) + return new ICAL.Component(jcalData) +} + +const expandEventOccurrences = ( + event: ICAL.Event, + rangeEnd: ICAL.Time, +): ScheduleEvent[] => { + const occurrences: ScheduleEvent[] = [] + const iterator = event.iterator() + let next: ICAL.Time | null = null + + while ((next = iterator.next())) { + if (next.compare(event.startDate) < 0) continue + if (next.compare(rangeEnd) > 0) break + const details = event.getOccurrenceDetails(next) + occurrences.push({ + start: details.startDate.toJSDate(), + end: details.endDate.toJSDate(), + summary: event.summary, + description: event.description, + recurrenceId: event.recurrenceId, + }) + } + + return occurrences +} + +const extractScheduleEvents = (icalComp: ICAL.Component): ScheduleEvent[] => { + const vevents = icalComp.getAllSubcomponents("vevent") + const rangeEnd = ICAL.Time.fromDateString("2026-01-01") + + return vevents + .flatMap((vevent) => { + const event = new ICAL.Event(vevent) + if (event.iterator().complete) { + return [{ + start: event.startDate.toJSDate(), + end: event.endDate.toJSDate(), + summary: event.summary, + description: event.description, + recurrenceId: "123", + }] + } + return expandEventOccurrences(event, rangeEnd) + }) + .sort((a, b) => b.start.getTime() - a.start.getTime()) +} + +const formatTimePair = (s: Date, e: Date): string => { + const start = dayjs(s) + const end = dayjs(e) + if (start.isSame(end, "day")) { + return `${start.format("YYYY-MM-DD HH:mm")} - ${end.format("HH:mm")}` + } + return `${start.format("YYYY-MM-DD")} - ${end.format("YYYY-MM-DD")}` +} + +export default function Schedule() { + const [scheduledEvents, setScheduledEvents] = useState([]) + + const defaultDate = today(getLocalTimeZone()) + const [focusedDate, setFocusedDate] = useState(defaultDate) + + useEffect(() => { + parseCal().then((icalComp) => { + setScheduledEvents(extractScheduleEvents(icalComp)) + }) + }, []) + + return ( +
+
日程
+
+
+ {scheduledEvents.map((event, index) => ( +
+
+
{event.summary}
+ { formatTimePair(event.start, event.end) } +
+

{event.description}

+

{event.recurrenceId}

+
+ ))} +
+
+ +
+
+
+ ) +} diff --git a/src/pages/calendar/index.astro b/src/pages/calendar/index.astro index 9716dd6..78e7fe8 100644 --- a/src/pages/calendar/index.astro +++ b/src/pages/calendar/index.astro @@ -1,8 +1,8 @@ --- -import FullCalendar from "./Calendar" +import Schedule from "./Schedule" import BaseLayout from "../../layouts/BaseLayout.astro" --- - + From c1467ef24da7986bdfe008f8b9e597d42fad8725 Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Fri, 30 May 2025 22:27:40 +0800 Subject: [PATCH 2/6] save --- src/pages/calendar/Schedule.tsx | 230 +++++++++++++++++++++++++++----- src/pages/join-us.astro | 2 +- src/styles/global.css | 4 +- 3 files changed, 202 insertions(+), 34 deletions(-) diff --git a/src/pages/calendar/Schedule.tsx b/src/pages/calendar/Schedule.tsx index d4ec55d..ed674ac 100644 --- a/src/pages/calendar/Schedule.tsx +++ b/src/pages/calendar/Schedule.tsx @@ -1,9 +1,13 @@ import ICAL from "ical.js" import dayjs from "dayjs" -import { useEffect, useState } from "react" -import { Calendar } from "@heroui/react" +import { useEffect, useMemo, useRef, useState } from "react" +import { Alert, Button, Calendar, Link, Spinner } from "@heroui/react" import { today, getLocalTimeZone } from "@internationalized/date" +import "dayjs/locale/zh-cn" + +dayjs.locale("zh-cn") + type ScheduleEvent = { start: Date end: Date @@ -18,27 +22,51 @@ const parseCal = async (): Promise => { return new ICAL.Component(jcalData) } +const extractScheduleEventsInRange = ( + icalComp: ICAL.Component, + rangeStart: ICAL.Time, + rangeEnd: ICAL.Time, +): ScheduleEvent[] => { + const vevents = icalComp.getAllSubcomponents("vevent") + return vevents + .flatMap((vevent) => { + const event = new ICAL.Event(vevent) + if (!event.isRecurring()) { + if (event.startDate.compare(rangeEnd) > 0 || event.endDate.compare(rangeStart) < 0) return [] + return [{ + start: event.startDate.toJSDate(), + end: event.endDate.toJSDate(), + summary: event.summary, + description: event.description, + recurrenceId: undefined, + }] + } + return expandEventOccurrences(event, rangeStart, rangeEnd) + }) + .sort((a, b) => b.start.getTime() - a.start.getTime()) +} + const expandEventOccurrences = ( event: ICAL.Event, + rangeStart: ICAL.Time, rangeEnd: ICAL.Time, ): ScheduleEvent[] => { const occurrences: ScheduleEvent[] = [] const iterator = event.iterator() let next: ICAL.Time | null = null - while ((next = iterator.next())) { - if (next.compare(event.startDate) < 0) continue + if (!next) break if (next.compare(rangeEnd) > 0) break + if (next.compare(rangeStart) < 0) continue const details = event.getOccurrenceDetails(next) occurrences.push({ start: details.startDate.toJSDate(), end: details.endDate.toJSDate(), summary: event.summary, description: event.description, - recurrenceId: event.recurrenceId, + recurrenceId: next.toString(), }) } - return occurrences } @@ -67,9 +95,9 @@ const formatTimePair = (s: Date, e: Date): string => { const start = dayjs(s) const end = dayjs(e) if (start.isSame(end, "day")) { - return `${start.format("YYYY-MM-DD HH:mm")} - ${end.format("HH:mm")}` + return `${start.format("HH:mm")} - ${end.format("HH:mm")}` } - return `${start.format("YYYY-MM-DD")} - ${end.format("YYYY-MM-DD")}` + return `${start.format("MM.DD")} - ${end.format("MM.DD")}` } export default function Schedule() { @@ -77,38 +105,178 @@ export default function Schedule() { const defaultDate = today(getLocalTimeZone()) const [focusedDate, setFocusedDate] = useState(defaultDate) + const dateRefs = useRef>({}) + const calendarRef = useRef(null) + const [loading, setLoading] = useState(true) useEffect(() => { + const rangeStart = ICAL.Time.fromJSDate(dayjs(focusedDate.toDate(getLocalTimeZone())).startOf("month").toDate()) + const rangeEnd = ICAL.Time.fromJSDate(dayjs(focusedDate.toDate(getLocalTimeZone())).endOf("month").toDate()) + if (!calendarRef.current) { + setLoading(true) + parseCal().then((icalComp) => { + calendarRef.current = icalComp + const events = extractScheduleEventsInRange( + icalComp, + rangeStart, + rangeEnd, + ) + setScheduledEvents(events) + setLoading(false) + }) + } + else { + const events = extractScheduleEventsInRange( + calendarRef.current, + rangeStart, + rangeEnd, + ) + setScheduledEvents(events) + } + }, [focusedDate]) + + useEffect(() => { + setLoading(true) parseCal().then((icalComp) => { setScheduledEvents(extractScheduleEvents(icalComp)) + setLoading(false) }) }, []) + const groupedEvents = useMemo(() => { + const grouped = new Map() + scheduledEvents.forEach((event) => { + const dateKey = dayjs(event.start).format("YYYY-MM-DD") + if (!grouped.has(dateKey)) { + grouped.set(dateKey, []) + } + grouped.get(dateKey)!.push(event) + }) + + // 可选:对每个日期内部事件排序(按开始时间升序) + for (const events of grouped.values()) { + events.sort((a, b) => a.start.getTime() - b.start.getTime()) + } + + // 可选:按照日期从早到晚排序 + return Array.from(grouped.entries()).sort(([a], [b]) => a.localeCompare(b)) + }, [scheduledEvents]) + + const isUserChangeRef = useRef(false) + + useEffect(() => { + if (!isUserChangeRef.current) return + isUserChangeRef.current = false + + const dateKey = dayjs(focusedDate.toDate(getLocalTimeZone())).format("YYYY-MM-DD") + const target = dateRefs.current[dateKey] + if (target) { + target.scrollIntoView({ behavior: "smooth", block: "start" }) + } + else { + // to nearest date if not found + const dates = Object.keys(dateRefs.current).map(d => dayjs(d)) + const closestDate = dates.reduce((prev, curr) => { + return Math.abs(curr.diff(focusedDate.toDate(getLocalTimeZone()), "day")) < Math.abs(prev.diff(focusedDate.toDate(getLocalTimeZone()), "day")) ? curr : prev + }, dates[0]) + const closestKey = closestDate.format("YYYY-MM-DD") + const closestTarget = dateRefs.current[closestKey] + if (closestTarget) { + closestTarget.scrollIntoView({ behavior: "smooth", block: "start" }) + } + } + }, [focusedDate, groupedEvents]) + return ( -
-
日程
-
-
- {scheduledEvents.map((event, index) => ( -
-
-
{event.summary}
- { formatTimePair(event.start, event.end) } -
-

{event.description}

-

{event.recurrenceId}

-
- ))} -
-
- +
+
+
日程
+ console.log(e)} + href="webcal://ical.nbtca.space" + target="_blank" + > + 订阅 + + )} /> +
+
+
+ { + dayjs(focusedDate.toDate()).format("MMMM YYYY") + } +
+
+ {loading && ( +
+ +
+ )} + {groupedEvents.length === 0 && !loading && ( +
本月暂无日程
+ )} + { + groupedEvents.map(([date, events]) => ( +
dateRefs.current[date] = el} + className="mb-6" + > +
+ { dayjs(date).format("MM.DD") } + { dayjs(date).format("ddd") } +
+ {events.map((event, index) => ( +
+
+
{event.summary}
+ {event.recurrenceId && ( + // 重复 + + + + + )} +
+ {formatTimePair(event.start, event.end)} + { + event.description && ( +

{event.description}

+ ) + } +
+ ))} +
+ )) + } +
+
+
+ { + isUserChangeRef.current = true + setFocusedDate(v) + }} + /> +
+
+
-
-
+
+ ) } diff --git a/src/pages/join-us.astro b/src/pages/join-us.astro index 41f13da..148c13f 100644 --- a/src/pages/join-us.astro +++ b/src/pages/join-us.astro @@ -121,7 +121,7 @@ import logoAnimated from "./_assets/nbtca.gif"; -webkit-font-smoothing: antialiased; direction: ltr; font-weight: 400; - font-size: 17px; + font-size: 16px; line-height: 1.82353; --body-container-width: 645px; text-align: center; diff --git a/src/styles/global.css b/src/styles/global.css index e05d037..c89fa63 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -149,12 +149,12 @@ button:disabled { html { font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Icons", "Helvetica Neue", "Helvetica", "Arial", sans-serif; - font-size: 106.25%; + /* font-size: 106.25%; */ quotes: "“" "”"; } body { - font-size: 17px; + /* font-size: 17px; */ line-height: 1.47059; font-weight: 400; letter-spacing: -0.022em; From 2a7e5bf861c2b1b4b79a86777e35e5349817493d Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Fri, 30 May 2025 22:47:12 +0800 Subject: [PATCH 3/6] change menu name --- src/components/header/HeaderNavigation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/header/HeaderNavigation.tsx b/src/components/header/HeaderNavigation.tsx index 079f728..2df2dc1 100644 --- a/src/components/header/HeaderNavigation.tsx +++ b/src/components/header/HeaderNavigation.tsx @@ -13,7 +13,7 @@ export default function App() { }, { link: "/calendar", - name: "日历", + name: "日程", }, { link: "/about", From 1bd9d9551b4c41d878e00732b42f51561c4df893 Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Sat, 31 May 2025 10:38:32 +0800 Subject: [PATCH 4/6] fix overflow --- src/pages/calendar/Schedule.tsx | 160 ++++++++++++++++---------------- 1 file changed, 79 insertions(+), 81 deletions(-) diff --git a/src/pages/calendar/Schedule.tsx b/src/pages/calendar/Schedule.tsx index ed674ac..dfba7fc 100644 --- a/src/pages/calendar/Schedule.tsx +++ b/src/pages/calendar/Schedule.tsx @@ -188,91 +188,89 @@ export default function Schedule() { }, [focusedDate, groupedEvents]) return ( -
-
-
-
日程
- console.log(e)} - href="webcal://ical.nbtca.space" - target="_blank" - > - 订阅 - - )} - /> -
-
-
- { - dayjs(focusedDate.toDate()).format("MMMM YYYY") - } -
-
- {loading && ( -
- -
- )} - {groupedEvents.length === 0 && !loading && ( -
本月暂无日程
- )} +
+
+
日程
+ console.log(e)} + href="webcal://ical.nbtca.space" + target="_blank" + > + 订阅 + + )} + /> +
+
+
{ - groupedEvents.map(([date, events]) => ( -
dateRefs.current[date] = el} - className="mb-6" - > -
- { dayjs(date).format("MM.DD") } - { dayjs(date).format("ddd") } -
- {events.map((event, index) => ( -
-
-
{event.summary}
- {event.recurrenceId && ( - // 重复 - - - - - )} -
- {formatTimePair(event.start, event.end)} - { - event.description && ( -

{event.description}

- ) - } -
- ))} -
- )) + dayjs(focusedDate.toDate()).format("MMMM YYYY") }
-
-
- { - isUserChangeRef.current = true - setFocusedDate(v) - }} - /> +
+ {loading && ( +
+
+ )} + {groupedEvents.length === 0 && !loading && ( +
本月暂无日程
+ )} + { + groupedEvents.map(([date, events]) => ( +
dateRefs.current[date] = el} + className="mb-6" + > +
+ { dayjs(date).format("MM.DD") } + { dayjs(date).format("ddd") } +
+ {events.map((event, index) => ( +
+
+
{event.summary}
+ {event.recurrenceId && ( + // 重复 + + + + + )} +
+ {formatTimePair(event.start, event.end)} + { + event.description && ( +

{event.description}

+ ) + } +
+ ))} +
+ )) + } +
+
+
+ { + isUserChangeRef.current = true + setFocusedDate(v) + }} + />
From 5d33c4040027acef9241964a97892b1ffece6822 Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Sat, 31 May 2025 10:41:45 +0800 Subject: [PATCH 5/6] fix style --- src/pages/calendar/Schedule.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/calendar/Schedule.tsx b/src/pages/calendar/Schedule.tsx index dfba7fc..85be941 100644 --- a/src/pages/calendar/Schedule.tsx +++ b/src/pages/calendar/Schedule.tsx @@ -238,7 +238,7 @@ export default function Schedule() {
{events.map((event, index) => (
-
+
{event.summary}
{event.recurrenceId && ( // 重复 From 1329a2792d2056c60ebaa09525fcebade253b9ec Mon Sep 17 00:00:00 2001 From: Clas Wen Date: Sat, 12 Jul 2025 15:02:10 +0800 Subject: [PATCH 6/6] Add mobile date selector for calendar page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add sticky mobile date selector with year and month dropdowns - Hide existing calendar on mobile screens - Display selected values with 年/月 suffixes - Improve mobile user experience for date navigation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/pages/calendar/Schedule.tsx | 59 +++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/pages/calendar/Schedule.tsx b/src/pages/calendar/Schedule.tsx index 85be941..52d758d 100644 --- a/src/pages/calendar/Schedule.tsx +++ b/src/pages/calendar/Schedule.tsx @@ -1,7 +1,7 @@ import ICAL from "ical.js" import dayjs from "dayjs" import { useEffect, useMemo, useRef, useState } from "react" -import { Alert, Button, Calendar, Link, Spinner } from "@heroui/react" +import { Alert, Button, Calendar, Link, Spinner, Select, SelectItem } from "@heroui/react" import { today, getLocalTimeZone } from "@internationalized/date" import "dayjs/locale/zh-cn" @@ -83,7 +83,8 @@ const extractScheduleEvents = (icalComp: ICAL.Component): ScheduleEvent[] => { end: event.endDate.toJSDate(), summary: event.summary, description: event.description, - recurrenceId: "123", + recurrenceId: event.uid || event.startDate.toString(), + }] } return expandEventOccurrences(event, rangeEnd) @@ -109,6 +110,12 @@ export default function Schedule() { const calendarRef = useRef(null) const [loading, setLoading] = useState(true) + const currentYear = focusedDate.year + const currentMonth = focusedDate.month + + const years = Array.from({ length: 10 }, (_, i) => currentYear - 5 + i) + const months = Array.from({ length: 12 }, (_, i) => i + 1) + useEffect(() => { const rangeStart = ICAL.Time.fromJSDate(dayjs(focusedDate.toDate(getLocalTimeZone())).startOf("month").toDate()) const rangeEnd = ICAL.Time.fromJSDate(dayjs(focusedDate.toDate(getLocalTimeZone())).endOf("month").toDate()) @@ -191,6 +198,54 @@ export default function Schedule() {
日程
+ + {/* Mobile Date Selector */} +
+
+ + +
+