feat: search club
This commit is contained in:
parent
457fc8595d
commit
99fd5778df
@ -1,18 +1,20 @@
|
||||
import { Divider, Empty, Flex, Pagination, Skeleton, Space, Spin, Switch, Typography } from "antd";
|
||||
import { Button, Divider, Empty, Flex, Pagination, Skeleton, Space, Switch, Typography } from "antd";
|
||||
import type { IEventInfo } from "../types";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { EventCard } from "./EventCard";
|
||||
import { useRequest } from "ahooks";
|
||||
import { CalendarOutlined } from "@ant-design/icons";
|
||||
|
||||
interface Props {
|
||||
clubId: string;
|
||||
}
|
||||
export const ClubEvenList = (props: Props) => {
|
||||
const [showAllDisable, setShowAllDiable] = useState(false);
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const requestEvents = useRequest<{data: IEventInfo[]; total: number; }, [string, number]>(
|
||||
async (clubId: string, page = 1) => {
|
||||
if (!clubId) return [];
|
||||
return (await fetch(`/api/events/${clubId}?page=${page}`)).json()
|
||||
return (await fetch(`/api/club/${clubId}/events?page=${page}`)).json()
|
||||
}, { manual: true });
|
||||
const [page, setPage] = useState(1);
|
||||
const onPageChange = useCallback((page: number) => {
|
||||
@ -37,21 +39,40 @@ export const ClubEvenList = (props: Props) => {
|
||||
}
|
||||
}, [showAll, onPageChange]);
|
||||
useEffect(() => {
|
||||
const id = setTimeout(() => {
|
||||
requestEvents.run(props.clubId, 1);
|
||||
const id = setTimeout(async () => {
|
||||
const data = await requestEvents.runAsync(props.clubId, 1).then(res => res.data);
|
||||
setPage(1);
|
||||
if (data.length === 10) {
|
||||
setShowAll(data.every(e => !e.isFinished));
|
||||
setShowAllDiable(data.every(e => !e.isFinished));
|
||||
}
|
||||
}, 100);
|
||||
return () => clearTimeout(id);
|
||||
}, [props.clubId]);
|
||||
const handleAddToCalendar = useCallback(() => {
|
||||
const url = `${window.location.origin}/api/club/${props.clubId}/calendar.ics?page=${page}`;
|
||||
const uri = url.replace(/^http(s)?/, 'webcal');
|
||||
console.debug(uri);
|
||||
window.open(uri);
|
||||
}, [props.clubId, page]);
|
||||
return (
|
||||
<>
|
||||
<Divider>
|
||||
<Space>
|
||||
<Typography.Text type="secondary">显示已结束的活动</Typography.Text>
|
||||
<Switch checked={showAll} onChange={() => setShowAll(!showAll)} unCheckedChildren="隐藏" checkedChildren="显示" />
|
||||
</Space>
|
||||
{showAllDisable ? null : (
|
||||
<Space>
|
||||
<Typography.Text type="secondary">显示已结束的活动</Typography.Text>
|
||||
<Switch checked={showAll} onChange={() => setShowAll(!showAll)} unCheckedChildren="隐藏" checkedChildren="显示" />
|
||||
</Space>
|
||||
)}
|
||||
</Divider>
|
||||
<Flex wrap vertical gap={12} justify="center" align="center">
|
||||
<Button
|
||||
type="link"
|
||||
icon={<CalendarOutlined />}
|
||||
onClick={handleAddToCalendar}
|
||||
>
|
||||
本页全部添加到日历
|
||||
</Button>
|
||||
{showAll ? (
|
||||
<Pagination
|
||||
showQuickJumper
|
||||
|
||||
78
src/components/ClubSearchTable.tsx
Normal file
78
src/components/ClubSearchTable.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { useRequest } from "ahooks";
|
||||
import { Avatar, Button, Flex, Input, Space, Table } from "antd";
|
||||
import { useCallback, useState } from "react";
|
||||
import type { ClubInfo } from "../types";
|
||||
import { EyeOutlined, StarFilled, StarOutlined } from "@ant-design/icons";
|
||||
import { useClubStore } from "../store/useClubStore";
|
||||
|
||||
|
||||
type ClickType = 'VIEW' | 'FAV';
|
||||
|
||||
interface Props {
|
||||
handleClick?: (type: ClickType, club: ClubInfo) => void;
|
||||
}
|
||||
|
||||
export const ClubSearchTable = (props: Props) => {
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const searchClub = useRequest<{ clubs: ClubInfo[], total: number }, [string, number]>(async (searchKey: string, page = 1) => {
|
||||
const resp: { clubs: ClubInfo[], total: number } = await fetch(`/api/club/find?key=${searchKey}&page=${page}`).then(e => e.json());
|
||||
return resp;
|
||||
}, { manual: true });
|
||||
const [page, setPage] = useState(1);
|
||||
const handleSearch = useCallback((searchKey: string) => {
|
||||
searchClub.runAsync(searchKey, 1);
|
||||
}, [searchKey]);
|
||||
return (
|
||||
<Flex vertical gap={12} justify="center" align="center">
|
||||
<Input.Search
|
||||
allowClear
|
||||
onSearch={e => {
|
||||
setSearchKey(e);
|
||||
handleSearch(e);
|
||||
}}
|
||||
onPressEnter={() => handleSearch(searchKey)}
|
||||
value={searchKey}
|
||||
onChange={e => setSearchKey(e.target.value)}
|
||||
/>
|
||||
<Table
|
||||
rowKey={e => e.id}
|
||||
loading={searchClub.loading}
|
||||
dataSource={searchClub.data?.clubs}
|
||||
style={{ width: '100%' }}
|
||||
scroll={{ x: 400 }}
|
||||
pagination={{
|
||||
current: page,
|
||||
showSizeChanger: false,
|
||||
total: searchClub.data?.total,
|
||||
onChange: (page) => {
|
||||
setPage(page);
|
||||
searchClub.runAsync(searchKey, page);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Table.Column dataIndex="name" width={140} fixed="left" render={(name, { img }) => {
|
||||
return (
|
||||
<Space>
|
||||
<Avatar src={img} size={32} />
|
||||
<span>{name}</span>
|
||||
</Space>
|
||||
);
|
||||
}} />
|
||||
<Table.Column dataIndex="area" width={80} />
|
||||
<Table.Column dataIndex="members" width={80} render={num => `${num} 人`} />
|
||||
<Table.Column dataIndex="id" align="center" width={200} render={(id, record: ClubInfo) => {
|
||||
return (
|
||||
<Space.Compact size="small">
|
||||
<Button icon={
|
||||
useClubStore.getState().isFav(id)
|
||||
? <StarFilled style={{ color: 'yellow' }} />
|
||||
: <StarOutlined />
|
||||
} onClick={() => props.handleClick?.('FAV', record)}>关注</Button>
|
||||
<Button icon={<EyeOutlined />} onClick={() => props.handleClick?.('VIEW', record)}>查看</Button>
|
||||
</Space.Compact>
|
||||
);
|
||||
}} />
|
||||
</Table>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
import { Button, Card, Statistic, Typography } from "antd";
|
||||
import type { IEventInfo } from "../types";
|
||||
import dayjs from "dayjs";
|
||||
import { EyeOutlined } from "@ant-design/icons";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { CalendarOutlined, EyeOutlined } from "@ant-design/icons";
|
||||
import { useCallback } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
interface EventCardProps {
|
||||
@ -16,19 +16,31 @@ export function EventCard(props: EventCardProps) {
|
||||
const handleView = useCallback(() => {
|
||||
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);
|
||||
}, [e]);
|
||||
return (
|
||||
<Card
|
||||
key={e.matchId}
|
||||
title={e.title}
|
||||
style={{ width: '100%' }}
|
||||
actions={[
|
||||
<Button
|
||||
type="link"
|
||||
icon={<CalendarOutlined />}
|
||||
onClick={handleAddCalendar}
|
||||
>
|
||||
加入日历
|
||||
</Button>,
|
||||
<Button
|
||||
type="link"
|
||||
onClick={handleView}
|
||||
icon={<EyeOutlined />}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Typography.Text type={e.isFinished ? undefined : 'success'}>{e.title}</Typography.Text>
|
||||
|
||||
@ -1,30 +1,87 @@
|
||||
import { Flex, Select, Space } from 'antd';
|
||||
import { Button, Drawer, Flex, Select, Space } from 'antd';
|
||||
import type React from 'react';
|
||||
import { clubs } from './clubList';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { ClubEvenList } from '../ClubEventList';
|
||||
import { DeleteOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons';
|
||||
import { useClubStore } from '../../store/useClubStore';
|
||||
import { ClubSearchTable } from '../ClubSearchTable';
|
||||
|
||||
interface Props {
|
||||
}
|
||||
const defaultOptions = clubs.map(e => ({
|
||||
label: e.name,
|
||||
value: e.clubId,
|
||||
}));
|
||||
|
||||
export const GameSelector: React.FC<Props> = () => {
|
||||
const [options, setOptions] = useState<{ label: React.ReactNode, value: string }[]>(defaultOptions);
|
||||
const [clubId, setClubId] = useState<string>(clubs[0]?.clubId ?? '');
|
||||
const clubStore = useClubStore(store => store);
|
||||
const handleClubChange = useCallback(async (id: string) => {
|
||||
setClubId(id);
|
||||
}, []);
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
if (clubStore.clubs.length) {
|
||||
setOptions([...defaultOptions, ...clubStore.clubs.map(info => ({
|
||||
label: (
|
||||
<Flex align='center' justify='space-between'>
|
||||
<span>{info.name}</span>
|
||||
<Button type='link' icon={<DeleteOutlined />} onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
clubStore.unFav(info.id);
|
||||
}} />
|
||||
</Flex>
|
||||
),
|
||||
value: info.id
|
||||
}))]);
|
||||
return;
|
||||
}
|
||||
setOptions(defaultOptions);
|
||||
setClubId(defaultOptions[0]?.value ?? '');
|
||||
}, [clubStore]);
|
||||
return (
|
||||
<Space orientation='vertical' style={{ width: '100%' }}>
|
||||
<Flex vertical gap={12} justify='center'>
|
||||
<Flex gap={12} justify='center' align='center'>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
placeholder={'请选择俱乐部'}
|
||||
size='large'
|
||||
value={clubId}
|
||||
options={clubs.map(e => ({ label: e.name, value: e.clubId }))}
|
||||
options={options}
|
||||
onChange={handleClubChange}
|
||||
/>
|
||||
<Flex gap={12}>
|
||||
<Flex flex={1}>
|
||||
<Button block size='large' icon={<SearchOutlined />} onClick={() => setSearchOpen(true)}>查找</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<ClubEvenList clubId={clubId} />
|
||||
<Drawer
|
||||
open={searchOpen}
|
||||
placement='bottom'
|
||||
size={"80vh"}
|
||||
onClose={() => setSearchOpen(false)}
|
||||
>
|
||||
<ClubSearchTable
|
||||
handleClick={(type, record) => {
|
||||
switch (type) {
|
||||
case 'VIEW':
|
||||
window.open(`/club/${record.id}`);
|
||||
break;
|
||||
case 'FAV':
|
||||
if (clubStore.isFav(record.id)) {
|
||||
clubStore.unFav(record.id);
|
||||
} else {
|
||||
clubStore.add(record);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Drawer>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,8 +3,4 @@ export const clubs = [
|
||||
name: '东华',
|
||||
clubId: '47',
|
||||
},
|
||||
{
|
||||
name: '飞酷乒乓球',
|
||||
clubId: '7841',
|
||||
},
|
||||
];
|
||||
@ -1,10 +1,12 @@
|
||||
import { serve } from "bun";
|
||||
import { getMatchInfo, listEvent, xcxApi } from "./utils/server";
|
||||
import { getMatchInfo, xcxApi } from "./utils/server";
|
||||
import ics from 'ics';
|
||||
import index from "./index.html";
|
||||
import { getUidScore } from "./services/uidScoreStore";
|
||||
import { checkIsUserFav, favPlayer, listFavPlayers, unFavPlayer } from "./services/favPlayerService";
|
||||
import { BattleService } from "./services/BattleService";
|
||||
import { KaiqiuService } from "./services/KaiqiuService";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const server = serve({
|
||||
port: process.env.PORT || 3000,
|
||||
@ -28,13 +30,84 @@ const server = serve({
|
||||
return Response.json(data);
|
||||
}
|
||||
},
|
||||
"/api/events/:clubid": {
|
||||
"/api/club/:clubid/events": {
|
||||
async GET(req) {
|
||||
const page = Number(new URL(req.url).searchParams.get('page')) ?? 1;
|
||||
const data = await KaiqiuService.listClubEvents(req.params.clubid, page);
|
||||
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,
|
||||
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 },
|
||||
// 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.createEvents(configs, (err, data) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
resolve('');
|
||||
}
|
||||
resolve(data);
|
||||
}));
|
||||
return new Response(data, { headers: {
|
||||
'Content-Type': 'text/calendar; charset=utf-8',
|
||||
} });
|
||||
}
|
||||
},
|
||||
"/api/match/:matchId": {
|
||||
async GET(req) {
|
||||
const data = await getMatchInfo(req.params.matchId);
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import { Avatar, Button, Drawer, Typography } from "antd";
|
||||
import { useLoaderData } from "react-router";
|
||||
import type { ClubInfo, IEventInfo } from "../types";
|
||||
import { useCallback, useState } from "react";
|
||||
import type { ClubDetail, IEventInfo } from "../types";
|
||||
import { useState } from "react";
|
||||
import { ChangeBackground } from "../components/ChangeBackground";
|
||||
import { ClubEvenList } from "../components/ClubEventList";
|
||||
import { useRequest } from "ahooks";
|
||||
|
||||
export const ClubEventsPage = () => {
|
||||
const { info, events, total } = useLoaderData<{
|
||||
info: ClubInfo;
|
||||
const { info } = useLoaderData<{
|
||||
info: ClubDetail;
|
||||
events: IEventInfo[];
|
||||
total: number;
|
||||
}>();
|
||||
|
||||
@ -45,11 +45,10 @@ export const route = createBrowserRouter([
|
||||
HydrateFallback: () => <HydrateFallback />,
|
||||
loader: async ({ params }) => {
|
||||
const id = params.id;
|
||||
const [info, event] = await Promise.all([
|
||||
const [info] = await Promise.all([
|
||||
fetch(`/api/club/${id}`).then(r => r.json()),
|
||||
fetch(`/api/events/${id}`).then(r => r.json()),
|
||||
]);
|
||||
return { info, events: event.data, total: event.total };
|
||||
return { info };
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import * as cheerio from "cheerio";
|
||||
import { htmlRequestHeaders, redis, REDIS_CACHE_HOUR } from "../utils/server";
|
||||
import type { IEventInfo } from "../types";
|
||||
import type { ClubInfo, IEventInfo } from "../types";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export class KaiqiuService {
|
||||
@ -15,19 +15,22 @@ export class KaiqiuService {
|
||||
return this.parseSearchClubPage(html, Boolean(normalClub));
|
||||
}
|
||||
|
||||
private static parseSearchClubPage(html: string, normal: boolean) {
|
||||
private static parseSearchClubPage(html: string, normal: boolean): {
|
||||
clubs: ClubInfo[];
|
||||
total: number;
|
||||
} {
|
||||
const $ = cheerio.load(html);
|
||||
const infos = $(`#content > div:nth-of-type(${normal ? 2 : 1}) td[width="250"]`).toArray();
|
||||
const imgs = $(`#content > div:nth-of-type(${normal ? 2 : 1}) td[width="80"]`).toArray();
|
||||
const parseInfo = (info: typeof infos[number], index: number) => {
|
||||
const parseInfo = (info: typeof infos[number], index: number): ClubInfo => {
|
||||
const name = $(info).find('a').text().trim();
|
||||
const url = $(info).find('a').attr('href') ?? '';
|
||||
const clubId = /-(?<id>\d+).html$/.exec(url)?.groups?.id
|
||||
const clubId = /-(?<id>\d+).html$/.exec(url)?.groups?.id?.toString() ?? '';
|
||||
const members = parseInt($(info).find('span.num').text().trim());
|
||||
const area = $(info).find('div.gray:nth-of-type(1)').text().trim();
|
||||
const src = $(imgs[index]).find('.threadimg60 img').attr('src') ?? '';
|
||||
const img = src.startsWith('http') ? src : 'https://kaiqiuwang.cc/home/image/nologo.jpg';
|
||||
return { name, clubId, members, area, img, url: `${this.#baseURL}/home/${url}` };
|
||||
return { name, id: clubId, members, area, img, url: `${this.#baseURL}/home/${url}` };
|
||||
}
|
||||
const clubs = infos.map((e, i) => parseInfo(e, i));
|
||||
const total = parseInt($('#content > div:nth-of-type(1) div.page > em').text().trim()) || clubs.length;
|
||||
@ -56,6 +59,10 @@ export class KaiqiuService {
|
||||
if (!clubId) return {
|
||||
data: [],
|
||||
total: 0,
|
||||
location: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
};
|
||||
const key = `my-kaiqiuwang:club-events:${clubId}:page-${page}`;
|
||||
let html = await redis.get(key).catch(() => '');
|
||||
@ -63,10 +70,15 @@ export class KaiqiuService {
|
||||
html = await this.#fetchEventListHTML(clubId, page);
|
||||
redis.setex(key, 60 * 60 * REDIS_CACHE_HOUR, html);
|
||||
}
|
||||
return await this.#parseEventList(html);
|
||||
const location = await this.#getClubLocation(clubId);
|
||||
const data = await this.#parseEventList(html);
|
||||
return {
|
||||
...data,
|
||||
location,
|
||||
}
|
||||
}
|
||||
|
||||
static async #parseEventList(html: string) {
|
||||
static async #parseEventList(html: string) {
|
||||
const $ = cheerio.load(html);
|
||||
const blockList = $('div.event_list > ol > li');
|
||||
const list: IEventInfo[] = [];
|
||||
@ -95,6 +107,20 @@ export class KaiqiuService {
|
||||
};
|
||||
}
|
||||
|
||||
static async #getClubLocation(clubId: string) {
|
||||
const url = `${this.#baseURL}/home/space-mtag-tagid-${clubId}-view-map.html`;
|
||||
const key = `my-kaiqiuwang:location:${clubId}`;
|
||||
let html: string = await redis.get(key) || '';
|
||||
if (!html) {
|
||||
html = await fetch(url, { headers: htmlRequestHeaders }).then(res => res.text());
|
||||
redis.setex(key, 60 * 60 * 24, html);
|
||||
}
|
||||
const $ = cheerio.load(html);
|
||||
const lng = Number($('#lng').val());
|
||||
const lat = Number($('#lat').val());
|
||||
return { lng, lat };
|
||||
}
|
||||
|
||||
public static async getEventInfo(eventId: string) {
|
||||
let eventPage = await redis.get(`my-kaiqiuwang:match:${eventId}`) ?? '';
|
||||
// https://kaiqiuwang.cc/home/space-event-id-175775.html
|
||||
@ -112,6 +138,8 @@ export class KaiqiuService {
|
||||
return {
|
||||
title,
|
||||
startDate,
|
||||
url: eventURL,
|
||||
isFinished: dayjs(startDate).isBefore(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
28
src/store/useClubStore.ts
Normal file
28
src/store/useClubStore.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { create } from "zustand";
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import type { ClubInfo } from "../types";
|
||||
|
||||
interface Store {
|
||||
clubs: ClubInfo[];
|
||||
add(club: ClubInfo): void;
|
||||
unFav(id: string): void;
|
||||
isFav(id: string): boolean;
|
||||
}
|
||||
export const useClubStore = create(persist<Store>((set, get) => {
|
||||
return {
|
||||
clubs: [],
|
||||
add: (club: ClubInfo) => {
|
||||
set({ clubs: [...get().clubs, club] });
|
||||
},
|
||||
unFav: (id: string) => {
|
||||
const newClubs = get().clubs.filter(e => e.id !== id);
|
||||
set({ clubs: newClubs });
|
||||
},
|
||||
isFav: (id: string) => {
|
||||
return get().clubs.findIndex(e => e.id === id) > -1;
|
||||
},
|
||||
}
|
||||
}, {
|
||||
name: 'club-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}));
|
||||
@ -60,6 +60,15 @@ export interface XCXFindUserResp {
|
||||
}
|
||||
|
||||
export interface ClubInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
members: number;
|
||||
area: string;
|
||||
url: string;
|
||||
img: string;
|
||||
}
|
||||
|
||||
export interface ClubDetail {
|
||||
id: string;
|
||||
name: string;
|
||||
article: string;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user