This commit is contained in:
Clas Wen 2025-05-23 23:18:03 +08:00
parent 53d6723dfb
commit 7a33a00ab6
7 changed files with 154 additions and 26 deletions

View file

@ -8,7 +8,10 @@
],
"eslint.experimental.useFlatConfig": true,
"cSpell.words": [
"ical",
"Logto",
"qrcode"
"qrcode",
"Subcomponents",
"vevent"
],
}

View file

@ -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"],
// },
},
})

View file

@ -21,6 +21,10 @@
"Rehype",
"rehypePlugins",
"shiki",
"tseslint"
"tseslint",
"ical",
"ICAL",
"vevents",
"getAllSubcomponents"
]
}

View file

@ -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",

View file

@ -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: {}

View file

@ -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<ICAL.Component> => {
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<ScheduleEvent>((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<ScheduleEvent[]>([])
const defaultDate = today(getLocalTimeZone())
const [focusedDate, setFocusedDate] = useState(defaultDate)
useEffect(() => {
parseCal().then((icalComp) => {
setScheduledEvents(extractScheduleEvents(icalComp))
})
}, [])
return (
<section className="w-full flex flex-col ">
<div className="section-content my-8 text-2xl font-bold"></div>
<div className="section-content grid grid-cols-5">
<div className="flex flex-col col-span-3">
{scheduledEvents.map((event, index) => (
<div key={index} className="p-4 m-2 border rounded-lg shadow">
<div className="flex items-center gap-2">
<div className="text-xl font-semibold">{event.summary}</div>
<span>{ formatTimePair(event.start, event.end) }</span>
</div>
<p>{event.description}</p>
<p>{event.recurrenceId}</p>
</div>
))}
</div>
<div className="col-span-2">
<Calendar
aria-label="Date (Controlled Focused Value)"
focusedValue={focusedDate}
value={defaultDate}
onFocusChange={setFocusedDate}
/>
</div>
</div>
</section>
)
}

View file

@ -1,8 +1,8 @@
---
import FullCalendar from "./Calendar"
import Schedule from "./Schedule"
import BaseLayout from "../../layouts/BaseLayout.astro"
---
<BaseLayout primaryTitle="日历">
<FullCalendar client:load />
<Schedule client:load />
</BaseLayout>