diff --git a/src/components/ClubEventList.tsx b/src/components/ClubEventList.tsx
index ea8865e..d5fa2ed 100644
--- a/src/components/ClubEventList.tsx
+++ b/src/components/ClubEventList.tsx
@@ -51,7 +51,7 @@ export const ClubEvenList = (props: Props) => {
return () => clearTimeout(id);
}, [props.clubId]);
const handleAddToCalendar = useCallback(() => {
- const url = `${window.location.origin}/api/club/${props.clubId}/calendar.ics?page=${page}`;
+ const url = `${window.location.origin}/api/club/${props.clubId}/calendar.ics`;
const uri = url.replace(/^http(s)?/, 'webcal');
console.debug(uri);
window.open(uri);
@@ -72,7 +72,7 @@ export const ClubEvenList = (props: Props) => {
icon={}
onClick={handleAddToCalendar}
>
- 本页全部添加到日历
+ 订阅该俱乐部比赛
{showAll ? (
{
navigate(`/event/${e.matchId}`);
}, [e]);
- const handleAddCalendar = useCallback(() => {
- const url = `${window.location.origin}/calendar/event/${e.matchId}/events.ics`;
- const uri = url.replace(/^http(s)?/, 'webcal');
- window.open(uri);
+ const messageFormat = useMemo(() => {
+ if (e.isFinished) {
+ return `已结束 DD 天`;
+ }
+ if (e.isProcessing) {
+ return '比赛进行中';
+ }
+ return `还有 DD 天 HH 时开始`;
}, [e]);
return (
}
- onClick={handleAddCalendar}
- >
- 加入日历
- ,
{e.info.map(e => (
diff --git a/src/index.tsx b/src/index.tsx
index 7729f83..c8e297b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -7,6 +7,12 @@ import { checkIsUserFav, favPlayer, listFavPlayers, unFavPlayer } from "./servic
import { BattleService } from "./services/BattleService";
import { KaiqiuService } from "./services/KaiqiuService";
import dayjs from "dayjs";
+import utc from 'dayjs/plugin/utc';
+import timezone from 'dayjs/plugin/timezone';
+import type { IEventInfo } from "./types";
+
+dayjs.extend(utc);
+dayjs.extend(timezone);
const server = serve({
port: process.env.PORT || 3000,
@@ -37,56 +43,35 @@ const server = serve({
return Response.json(data);
}
},
- // "/calendar/club/:id/events.ics": {
-
- // },
- "/calendar/event/:id/events.ics": {
- async GET(req) {
- const id = req.params.id;
- const info = await KaiqiuService.getEventInfo(id);
- const configs: ics.EventAttributes = {
- start: dayjs(info.startDate).format('YYYY-MM-DD-HH-mm').split('-').map(v => Number(v)) as any,
- duration: { hours: 6, minutes: 30 },
- title: info.title,
- // end: dayjs(event.startDate).add(6, 'h').add(30, 'minute').format('YYYY-MM-DD HH:mm'),
- // description: 'Annual 10-kilometer run in Boulder, Colorado',
- // location: 'Folsom Field, University of Colorado (finish line)',
- url: info.url,
- // categories: ['10k races', 'Memorial Day Weekend', 'Boulder CO'],
- // status: 'CONFIRMED',
- // busyStatus: 'BUSY',
- // organizer: { name: 'Admin', email: 'Race@BolderBOULDER.com' },
- // attendees: [
- // { name: 'Adam Gibbons', email: 'adam@example.com', rsvp: true, partstat: 'ACCEPTED', role: 'REQ-PARTICIPANT' },
- // { name: 'Brittany Seaton', email: 'brittany@example2.org', dir: 'https://linkedin.com/in/brittanyseaton', role: 'OPT-PARTICIPANT' }
- // ]
- };
- const data: string = await new Promise(resolve => ics.createEvent(configs, (err, data) => {
- if (err) {
- console.log(err);
- resolve('');
- }
- resolve(data);
- }));
- return new Response(data, { headers: {
- 'Content-Type': 'text/calendar; charset=utf-8',
- } });
- }
- },
"/api/club/:id/calendar.ics": {
async GET(req) {
const id = req.params.id;
- const page = Number(new URL(req.url).searchParams.get('page')) || 1;
- const events = await KaiqiuService.listClubEvents(id, page);
- const configs: ics.EventAttributes[] = events?.data?.filter(e => !e.isFinished).map(e => ({
- start: dayjs(e.startDate).format('YYYY-MM-DD-HH-mm').split('-').map(v => Number(v)) as any,
+ const clubInfo = await KaiqiuService.getClubInfo(id);
+ let allEvents: IEventInfo[] = [];
+ let page = 1;
+ let events = await KaiqiuService.listClubEvents(id, page);
+ allEvents = allEvents.concat(...events.data);
+ while (events.data.every(e => !e.isFinished)) {
+ page += 1;
+ events = await KaiqiuService.listClubEvents(id, page);
+ allEvents = allEvents.concat(...events.data);
+ }
+ const noGeo = !events.geo.lat && !events.geo.lng;
+ const geo = noGeo ? {} : { lat: events.geo.lat, lon: events.geo.lng };
+ const configs: ics.EventAttributes[] = allEvents?.filter(e => !e.isFinished)?.map(e => ({
+ ...geo,
+ start: dayjs.tz(e.startDate, 'Asia/Tokyo').format('YYYY-MM-DD-HH-mm').split('-').map(v => Number(v)) as any,
duration: { hours: 6, minutes: 30 },
title: e.title,
// end: dayjs(event.startDate).add(6, 'h').add(30, 'minute').format('YYYY-MM-DD HH:mm'),
// description: 'Annual 10-kilometer run in Boulder, Colorado',
- // location: 'Folsom Field, University of Colorado (finish line)',
- url: e.url,
- geo: { lat: events.location.lat, lon: events.location.lng },
+ location: e.location,
+ // url: e.url,
+ // geo: { lat: events.location.lat, lon: events.location.lng },
+ alarms: [
+ { action: 'display', summary: e.title, description: '距离比赛开始还有 2 小时', trigger: { before: true, hours: 2 } },
+ { action: 'audio', trigger: { before: true, hours: 2 } },
+ ],
// categories: ['10k races', 'Memorial Day Weekend', 'Boulder CO'],
// status: 'CONFIRMED',
// busyStatus: 'BUSY',
@@ -96,7 +81,9 @@ const server = serve({
// { name: 'Brittany Seaton', email: 'brittany@example2.org', dir: 'https://linkedin.com/in/brittanyseaton', role: 'OPT-PARTICIPANT' }
// ]
}));
- const data: string = await new Promise(resolve => ics.createEvents(configs, (err, data) => {
+ const data: string = await new Promise(resolve => ics.createEvents(configs, {
+ calName: clubInfo?.name ? `${clubInfo.name}的比赛` : '',
+ }, (err, data) => {
if (err) {
console.log(err);
resolve('');
diff --git a/src/services/KaiqiuService.ts b/src/services/KaiqiuService.ts
index 777345b..a50aa6e 100644
--- a/src/services/KaiqiuService.ts
+++ b/src/services/KaiqiuService.ts
@@ -2,6 +2,11 @@ import * as cheerio from "cheerio";
import { htmlRequestHeaders, redis, REDIS_CACHE_HOUR } from "../utils/server";
import type { ClubInfo, IEventInfo } from "../types";
import dayjs from "dayjs";
+import utc from 'dayjs/plugin/utc';
+import timezone from 'dayjs/plugin/timezone';
+
+dayjs.extend(utc);
+dayjs.extend(timezone);
export class KaiqiuService {
static #baseURL = 'https://kaiqiuwang.cc';
@@ -59,7 +64,7 @@ export class KaiqiuService {
if (!clubId) return {
data: [],
total: 0,
- location: {
+ geo: {
lat: 0,
lng: 0,
},
@@ -70,11 +75,11 @@ export class KaiqiuService {
html = await this.#fetchEventListHTML(clubId, page);
redis.setex(key, 60 * 60 * REDIS_CACHE_HOUR, html);
}
- const location = await this.#getClubLocation(clubId);
+ const geo = await this.#getClubLocation(clubId);
const data = await this.#parseEventList(html);
return {
...data,
- location,
+ geo,
}
}
@@ -89,14 +94,21 @@ export class KaiqiuService {
const eventPath = $(titleEl).find('a').attr('href') ?? '';
const eventURL = `${this.#baseURL}/home/${eventPath}`;
const matchId = /\S+-(\d+).html$/.exec(eventPath)?.[1] ?? '';
- const { startDate } = await this.getEventInfo(matchId);
+ const {
+ startDate,
+ isFinished,
+ isProcessing,
+ location,
+ } = await this.getEventInfo(matchId);
const event: IEventInfo = {
title,
info: [`比赛时间:${startDate}`, place],
url: eventURL,
startDate,
matchId,
- isFinished: dayjs(startDate).isBefore(),
+ isFinished,
+ isProcessing,
+ location,
}
list.push(event);
}
@@ -122,12 +134,13 @@ export class KaiqiuService {
}
public static async getEventInfo(eventId: string) {
- let eventPage = await redis.get(`my-kaiqiuwang:match:${eventId}`) ?? '';
+ const key = `my-kaiqiuwang:match:${eventId}`;
+ let eventPage = await redis.get(key) ?? '';
// https://kaiqiuwang.cc/home/space-event-id-175775.html
const eventURL = `${this.#baseURL}/home/space-event-id-${eventId}.html`;
if (!eventPage) {
eventPage = await fetch(eventURL, { headers: htmlRequestHeaders }).then(res => res.text() ?? '');
- await redis.setex(`my-kaiqiuwang:match:${eventId}`, 60 * 60 * 10, eventPage)
+ await redis.setex(key, 60 * 60 * 10, eventPage)
}
const $ = cheerio.load(eventPage);
const eventContent = $('.event_content').text().replace(/(\r|\n)/g, ',').split(',').filter(Boolean).join(' ');
@@ -135,11 +148,20 @@ export class KaiqiuService {
.exec(eventContent)?.groups ?? {};
const startDate = y ? `${y}-${M}-${D} ${H}:${m}` : '';
const title = $('#mainarea > h2 > a:nth-child(3)').text().trim();
+ const location = $('#content > div:nth-child(1) > div > div.event_content > dl > dd:nth-child(6) > a')
+ .text().split(' ')?.[1]?.trim() ?? '';
+ const startTime = dayjs.tz(startDate, 'Asia/Tokyo');
+ const overTime = startTime.add(6, 'hour');
+ const now = dayjs();
+ const isProcessing = now.isAfter(startTime) && now.isBefore(overTime);
+ const isFinished = now.isAfter(overTime);
return {
title,
startDate,
url: eventURL,
- isFinished: dayjs(startDate).isBefore(),
+ isProcessing,
+ isFinished,
+ location,
}
}
diff --git a/src/types/index.ts b/src/types/index.ts
index 1eddf41..03b43d7 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -10,6 +10,8 @@ export interface IEventInfo {
matchId: string;
startDate: string;
isFinished: boolean;
+ isProcessing: boolean;
+ location: string;
}
export interface MatchInfo {