Skip to content

Commit 7e0ec30

Browse files
committed
[add] Activity models & components
[optimize] move Utility functions to an independent folder
1 parent 415f00c commit 7e0ec30

46 files changed

Lines changed: 1958 additions & 85 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { HorizontalMarqueeBox, text2color } from 'idea-react';
2+
import { TableCellAttachment } from 'mobx-lark';
3+
import { observer } from 'mobx-react';
4+
import dynamic from 'next/dynamic';
5+
import { Component } from 'react';
6+
import { Badge, Carousel, Col, Container, Row } from 'react-bootstrap';
7+
8+
import { LarkImage } from '../../Base/LarkImage';
9+
import { ScoreBar } from '../../Base/ScoreBar';
10+
import { TimeRange } from '../../Base/TimeRange';
11+
import { ActivityPeople } from '../People';
12+
import { AgendaToolbarProps } from './Toolbar';
13+
14+
const AgendaToolbar = dynamic(() => import('./Toolbar'), { ssr: false });
15+
16+
@observer
17+
export class AgendaCard extends Component<AgendaToolbarProps> {
18+
renderCardImage = (file: TableCellAttachment) => (
19+
<LarkImage
20+
key={file.attachmentToken}
21+
className="m-auto object-fit-cover"
22+
style={{ width: '6rem', height: '6rem' }}
23+
src={[file]}
24+
roundedCircle
25+
/>
26+
);
27+
28+
renderAvatarImages() {
29+
const { type, mentors, mentorAvatars, organizationLogos } = this.props;
30+
const images = (type === 'Booth' && organizationLogos) || mentorAvatars;
31+
32+
return (mentors as string[])?.[1] ? (
33+
<Carousel indicators={false}>
34+
{(images as TableCellAttachment[])?.map(file => (
35+
<Carousel.Item key={file.attachmentToken}>{this.renderCardImage(file)}</Carousel.Item>
36+
))}
37+
</Carousel>
38+
) : (
39+
<ActivityPeople size={5} avatars={images} />
40+
);
41+
}
42+
43+
render() {
44+
const {
45+
activityId,
46+
id,
47+
type,
48+
title,
49+
mentors,
50+
mentorOrganizations,
51+
mentorPositions,
52+
startTime,
53+
endTime,
54+
score,
55+
} = this.props;
56+
57+
return (
58+
<Container
59+
className="h-100"
60+
style={{ contentVisibility: 'auto', containIntrinsicHeight: '13rem' }}
61+
>
62+
<Row className="border shadow-sm rounded h-100">
63+
<Col xs={4} className="d-flex flex-column justify-content-around align-items-center">
64+
<Badge bg={text2color(type + '', ['light'])}>
65+
<HorizontalMarqueeBox maxWidth="80px" height="12px">
66+
{type + ''}
67+
</HorizontalMarqueeBox>
68+
</Badge>
69+
70+
{this.renderAvatarImages()}
71+
</Col>
72+
73+
<Col xs={8} className="d-flex flex-column py-3">
74+
<h3 className="fs-5">
75+
<a
76+
className="text-decoration-none text-secondary"
77+
href={`/activity/${activityId}/agenda/${id}`}
78+
title={title as string}
79+
>
80+
<HorizontalMarqueeBox duration="20s" maxWidth="330px" height="24px">
81+
{title as string}
82+
</HorizontalMarqueeBox>
83+
</a>
84+
</h3>
85+
<ul className="list-unstyled flex-fill d-flex flex-column justify-content-between gap-2">
86+
<li>
87+
<TimeRange {...{ startTime, endTime }} />
88+
</li>
89+
{(mentors as string[])?.map((name, index) => (
90+
<li key={name}>
91+
{name} {(mentorOrganizations as string[])?.[index]}{' '}
92+
{(mentorPositions as string[])?.[index]}
93+
</li>
94+
))}
95+
{score && (
96+
<li>
97+
<ScoreBar value={score + ''} />
98+
</li>
99+
)}
100+
<AgendaToolbar
101+
as="li"
102+
className="justify-content-end"
103+
{...{ ...this.props, activityId }}
104+
/>
105+
</ul>
106+
</Col>
107+
</Row>
108+
</Container>
109+
);
110+
}
111+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { text2color } from 'idea-react';
2+
import { TableCellAttachment } from 'mobx-lark';
3+
import { observer } from 'mobx-react';
4+
import { FilePreview } from 'mobx-restful-table';
5+
import { FC, useContext } from 'react';
6+
import { Badge } from 'react-bootstrap';
7+
8+
import { I18nContext } from '../../../models/Base/Translation';
9+
10+
export const FileList: FC<{ data: TableCellAttachment[] }> = observer(({ data }) => {
11+
const { t } = useContext(I18nContext);
12+
13+
return (
14+
<section>
15+
<h2>{t('file_download')}</h2>
16+
<ol className="mt-3 mb-5">
17+
{data.map(({ id, name, mimeType, attachmentToken }) => (
18+
<li key={id + ''}>
19+
<FilePreview type={mimeType} path={`/api/lark/file/${attachmentToken}`} />
20+
21+
<Badge bg={text2color(name, ['light'])}>{name}</Badge>
22+
</li>
23+
))}
24+
</ol>
25+
</section>
26+
);
27+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { observer } from 'mobx-react';
2+
import { FC, useContext } from 'react';
3+
import { Button, Stack, StackProps } from 'react-bootstrap';
4+
import ICalendarLink from 'react-icalendar-link';
5+
import { TimeData } from 'web-utility';
6+
7+
import { Agenda } from '../../../models/Activity/Agenda';
8+
import { I18nContext } from '../../../models/Base/Translation';
9+
import { isServer } from '../../../utility/configuration';
10+
11+
export interface AgendaToolbarProps extends Omit<StackProps, 'id' | 'title'>, Agenda {
12+
activityId: string;
13+
}
14+
15+
const AgendaToolbar: FC<AgendaToolbarProps> = observer(
16+
({
17+
activityId,
18+
location,
19+
id,
20+
title,
21+
summary,
22+
startTime,
23+
endTime,
24+
mentors,
25+
children,
26+
...props
27+
}) => {
28+
const { t } = useContext(I18nContext);
29+
30+
return (
31+
<Stack direction="horizontal" gap={3} {...props}>
32+
<Button
33+
size="sm"
34+
variant="success"
35+
href={`/activity/${activityId}/agenda/${id}/invitation`}
36+
>
37+
{t('share')}
38+
</Button>
39+
40+
{!isServer() && (
41+
// @ts-expect-error https://github.com/josephj/react-icalendar-link/issues/41#issuecomment-1584173370
42+
<ICalendarLink
43+
className="btn btn-primary btn-sm"
44+
filename={`${title}.ics`}
45+
event={{
46+
title: title as string,
47+
description: summary as string,
48+
startTime: new Date(startTime as TimeData).toJSON(),
49+
endTime: new Date(endTime as TimeData).toJSON(),
50+
location: location as string,
51+
attendees: mentors as string[],
52+
url: `https://kaiyuanshe.cn/activity/${activityId}/agenda/${id}`,
53+
}}
54+
>
55+
{t('calendar')}
56+
</ICalendarLink>
57+
)}
58+
59+
{children}
60+
</Stack>
61+
);
62+
},
63+
);
64+
export default AgendaToolbar;

components/Activity/Card.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { TimeDistance } from 'idea-react';
2+
import { TableCellLocation } from 'mobx-lark';
3+
import type { FC } from 'react';
4+
import { Card, Col, Row } from 'react-bootstrap';
5+
6+
import { type Activity, ActivityModel } from '../../models/Activity';
7+
import { LarkImage } from '../Base/LarkImage';
8+
import { TagNav } from '../Base/TagNav';
9+
import { TimeOption } from '../data';
10+
11+
export interface ActivityCardProps extends Activity {
12+
className?: string;
13+
}
14+
15+
export const ActivityCard: FC<ActivityCardProps> = ({
16+
className = '',
17+
id,
18+
host,
19+
name,
20+
startTime,
21+
city,
22+
location,
23+
image,
24+
...activity
25+
}) => (
26+
<Card
27+
className={`shadow-sm ${className}`}
28+
style={{ contentVisibility: 'auto', containIntrinsicHeight: '23rem' }}
29+
>
30+
<div className="position-relative w-100" style={{ paddingBottom: '56%' }}>
31+
<div className="position-absolute top-0 left-0 w-100 h-100">
32+
<LarkImage
33+
className="card-img-top h-100 object-fit-cover"
34+
style={{ objectPosition: 'top left' }}
35+
src={image}
36+
/>
37+
</div>
38+
</div>
39+
<Card.Body className="d-flex flex-column">
40+
<Card.Title as="h3" className="h5 flex-fill">
41+
<a
42+
className="text-decoration-none text-secondary text-truncate-lines"
43+
href={ActivityModel.getLink({ id, ...activity })}
44+
>
45+
{name as string}
46+
</a>
47+
</Card.Title>
48+
49+
<Row className="mt-2 flex-fill">
50+
<Col className="text-start">
51+
<Card.Text
52+
className="mt-1 text-truncate"
53+
title={(location as TableCellLocation)?.full_address}
54+
>
55+
<span className="me-1">{city as string}</span>
56+
57+
{(location as TableCellLocation)?.full_address}
58+
</Card.Text>
59+
</Col>
60+
</Row>
61+
<Row as="footer" className="flex-fill small mt-1">
62+
<Col xs={8}>
63+
<TagNav
64+
list={host as string[]}
65+
linkOf={organizer => `/search/activity?keywords=${organizer}`}
66+
/>
67+
</Col>
68+
<Col className="text-end" xs={4}>
69+
<TimeDistance {...TimeOption} date={startTime as number} />
70+
</Col>
71+
</Row>
72+
</Card.Body>
73+
</Card>
74+
);

components/Activity/Charts.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { BarSeries, SVGCharts, Title, Tooltip, XAxis, YAxis } from 'echarts-jsx';
2+
import { observer } from 'mobx-react';
3+
import { FC, useContext } from 'react';
4+
5+
import { AgendaModel } from '../../models/Activity/Agenda';
6+
import { I18nContext } from '../../models/Base/Translation';
7+
8+
type ActivityDataProps = Awaited<ReturnType<AgendaModel['getStatistics']>>;
9+
10+
const ActivityCharts: FC<ActivityDataProps> = observer(
11+
({ keynoteSpeechCounts, mentorOrganizationCounts }) => {
12+
const { t } = useContext(I18nContext);
13+
14+
const keynoteSpeechList = Object.entries(keynoteSpeechCounts).sort((a, b) => b[1] - a[1]),
15+
mentorOrganizationList = Object.entries(mentorOrganizationCounts).sort((a, b) => b[1] - a[1]);
16+
17+
return (
18+
<>
19+
<SVGCharts>
20+
<Title>{t('distribution_of_activity_topics_by_heat')}</Title>
21+
<XAxis type="category" data={keynoteSpeechList.map(([key]) => key)} />
22+
<YAxis type="value" />
23+
<BarSeries data={keynoteSpeechList.map(([{}, value]) => value)} />
24+
<Tooltip />
25+
</SVGCharts>
26+
27+
<SVGCharts>
28+
<Title>{t('distribution_of_mentor_organizations_by_topics')}</Title>
29+
<XAxis type="category" data={mentorOrganizationList.map(([key]) => key)} />
30+
<YAxis type="value" />
31+
<BarSeries data={mentorOrganizationList.map(([{}, value]) => value)} />
32+
<Tooltip />
33+
</SVGCharts>
34+
</>
35+
);
36+
},
37+
);
38+
export default ActivityCharts;

components/Activity/DrawerNav.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Icon, PageNav } from 'idea-react';
2+
import { observable } from 'mobx';
3+
import { observer } from 'mobx-react';
4+
import { Component } from 'react';
5+
import { Button, Offcanvas } from 'react-bootstrap';
6+
import { sleep } from 'web-utility';
7+
8+
@observer
9+
export class DrawerNav extends Component {
10+
@observable
11+
accessor drawerShown = false;
12+
13+
closeDrawer = async () => {
14+
let { scrollTop } = document.scrollingElement || {};
15+
16+
do {
17+
await sleep(0.1);
18+
19+
if (scrollTop === document.scrollingElement?.scrollTop) {
20+
this.drawerShown = false;
21+
break;
22+
}
23+
scrollTop = document.scrollingElement?.scrollTop;
24+
// eslint-disable-next-line no-constant-condition
25+
} while (true);
26+
};
27+
28+
render() {
29+
const { drawerShown, closeDrawer } = this;
30+
31+
return (
32+
<>
33+
<div className="fixed-bottom p-3">
34+
<Button onClick={() => (this.drawerShown = true)}>
35+
<Icon name="layout-text-sidebar" />
36+
</Button>
37+
</div>
38+
39+
<Offcanvas style={{ width: 'max-content' }} show={drawerShown} onHide={closeDrawer}>
40+
<Offcanvas.Body>
41+
<PageNav depth={2} onItemClick={this.closeDrawer} />
42+
</Offcanvas.Body>
43+
</Offcanvas>
44+
</>
45+
);
46+
}
47+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.gift {
2+
&.disabled {
3+
filter: grayscale(1);
4+
cursor: not-allowed;
5+
}
6+
img {
7+
width: 10rem;
8+
height: 10rem;
9+
}
10+
}

0 commit comments

Comments
 (0)