Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:

- name: Lark notification
uses: foxundermoon/feishu-action@v2
if: ${{ env.VERCEL_TOKEN && env.VERCEL_ORG_ID && env.VERCEL_PROJECT_ID }}
with:
url: ${{ secrets.LARK_CHATBOT_HOOK_URL }}
msg_type: post
Expand Down
111 changes: 111 additions & 0 deletions components/Activity/Agenda/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { HorizontalMarqueeBox, text2color } from 'idea-react';
import { TableCellAttachment } from 'mobx-lark';
import { observer } from 'mobx-react';
import dynamic from 'next/dynamic';
import { Component } from 'react';
import { Badge, Carousel, Col, Container, Row } from 'react-bootstrap';

import { LarkImage } from '../../Base/LarkImage';
import { ScoreBar } from '../../Base/ScoreBar';
import { TimeRange } from '../../Base/TimeRange';
import { ActivityPeople } from '../People';
import { AgendaToolbarProps } from './Toolbar';

const AgendaToolbar = dynamic(() => import('./Toolbar'), { ssr: false });

@observer
export class AgendaCard extends Component<AgendaToolbarProps> {
renderCardImage = (file: TableCellAttachment) => (
<LarkImage
key={file.attachmentToken}
className="m-auto object-fit-cover"
style={{ width: '6rem', height: '6rem' }}
src={[file]}
roundedCircle
/>
);

renderAvatarImages() {
const { type, mentors, mentorAvatars, organizationLogos } = this.props;
const images = (type === 'Booth' && organizationLogos) || mentorAvatars;

return (mentors as string[])?.[1] ? (
<Carousel indicators={false}>
{(images as TableCellAttachment[])?.map(file => (
<Carousel.Item key={file.attachmentToken}>{this.renderCardImage(file)}</Carousel.Item>
))}
</Carousel>
) : (
<ActivityPeople size={5} avatars={images} />
);
}

render() {
const {
activityId,
id,
type,
title,
mentors,
mentorOrganizations,
mentorPositions,
startTime,
endTime,
score,
} = this.props;

return (
<Container
className="h-100"
style={{ contentVisibility: 'auto', containIntrinsicHeight: '13rem' }}
>
<Row className="border shadow-sm rounded h-100">
<Col xs={4} className="d-flex flex-column justify-content-around align-items-center">
<Badge bg={text2color(type + '', ['light'])}>
<HorizontalMarqueeBox maxWidth="80px" height="12px">
{type + ''}
</HorizontalMarqueeBox>
</Badge>

{this.renderAvatarImages()}
</Col>

<Col xs={8} className="d-flex flex-column py-3">
<h3 className="fs-5">
<a
className="text-decoration-none text-secondary"
href={`/activity/${activityId}/agenda/${id}`}
title={title as string}
>
<HorizontalMarqueeBox duration="20s" maxWidth="330px" height="24px">
{title as string}
</HorizontalMarqueeBox>
</a>
</h3>
<ul className="list-unstyled flex-fill d-flex flex-column justify-content-between gap-2">
<li>
<TimeRange {...{ startTime, endTime }} />
</li>
{(mentors as string[])?.map((name, index) => (
<li key={name}>
{name} {(mentorOrganizations as string[])?.[index]}{' '}
{(mentorPositions as string[])?.[index]}
</li>
))}
{score && (
<li>
<ScoreBar value={score + ''} />
</li>
)}
<AgendaToolbar
as="li"
className="justify-content-end"
{...{ ...this.props, activityId }}
/>
</ul>
</Col>
</Row>
</Container>
);
}
}
27 changes: 27 additions & 0 deletions components/Activity/Agenda/FileList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { text2color } from 'idea-react';
import { TableCellAttachment } from 'mobx-lark';
import { observer } from 'mobx-react';
import { FilePreview } from 'mobx-restful-table';
import { FC, useContext } from 'react';
import { Badge } from 'react-bootstrap';

import { I18nContext } from '../../../models/Base/Translation';

export const FileList: FC<{ data: TableCellAttachment[] }> = observer(({ data }) => {
const { t } = useContext(I18nContext);

return (
<section>
<h2>{t('file_download')}</h2>
<ol className="mt-3 mb-5">
{data.map(({ id, name, mimeType, attachmentToken }) => (
<li key={id + ''}>
<FilePreview type={mimeType} path={`/api/lark/file/${attachmentToken}`} />

<Badge bg={text2color(name, ['light'])}>{name}</Badge>
</li>
))}
</ol>
</section>
);
});
64 changes: 64 additions & 0 deletions components/Activity/Agenda/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { observer } from 'mobx-react';
import { FC, useContext } from 'react';
import { Button, Stack, StackProps } from 'react-bootstrap';
import ICalendarLink from 'react-icalendar-link';
import { TimeData } from 'web-utility';

import { Agenda } from '../../../models/Activity/Agenda';
import { I18nContext } from '../../../models/Base/Translation';
import { isServer } from '../../../utility/configuration';

export interface AgendaToolbarProps extends Omit<StackProps, 'id' | 'title'>, Agenda {
activityId: string;
}

const AgendaToolbar: FC<AgendaToolbarProps> = observer(
({
activityId,
location,
id,
title,
summary,
startTime,
endTime,
mentors,
children,
...props
}) => {
const { t } = useContext(I18nContext);

return (
<Stack direction="horizontal" gap={3} {...props}>
<Button
size="sm"
variant="success"
href={`/activity/${activityId}/agenda/${id}/invitation`}
>
{t('share')}
</Button>

{!isServer() && (
// @ts-expect-error https://github.com/josephj/react-icalendar-link/issues/41#issuecomment-1584173370
<ICalendarLink
className="btn btn-primary btn-sm"
filename={`${title}.ics`}
event={{
title: title as string,
description: summary as string,
startTime: new Date(startTime as TimeData).toJSON(),
endTime: new Date(endTime as TimeData).toJSON(),
location: location as string,
attendees: mentors as string[],
url: `https://kaiyuanshe.cn/activity/${activityId}/agenda/${id}`,
}}
>
{t('calendar')}
</ICalendarLink>
)}

{children}
</Stack>
);
},
);
export default AgendaToolbar;
74 changes: 74 additions & 0 deletions components/Activity/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { TimeDistance } from 'idea-react';
import { TableCellLocation } from 'mobx-lark';
import type { FC } from 'react';
import { Card, Col, Row } from 'react-bootstrap';

import { type Activity, ActivityModel } from '../../models/Activity';
import { LarkImage } from '../Base/LarkImage';
import { TagNav } from '../Base/TagNav';
import { TimeOption } from '../data';

export interface ActivityCardProps extends Activity {
className?: string;
}

export const ActivityCard: FC<ActivityCardProps> = ({
className = '',
id,
host,
name,
startTime,
city,
location,
image,
...activity
}) => (
<Card
className={`shadow-sm ${className}`}
style={{ contentVisibility: 'auto', containIntrinsicHeight: '23rem' }}
>
<div className="position-relative w-100" style={{ paddingBottom: '56%' }}>
<div className="position-absolute top-0 left-0 w-100 h-100">
<LarkImage
className="card-img-top h-100 object-fit-cover"
style={{ objectPosition: 'top left' }}
src={image}
/>
</div>
</div>
<Card.Body className="d-flex flex-column">
<Card.Title as="h3" className="h5 flex-fill">
<a
className="text-decoration-none text-secondary text-truncate-lines"
href={ActivityModel.getLink({ id, ...activity })}
>
{name as string}
</a>
</Card.Title>

<Row className="mt-2 flex-fill">
<Col className="text-start">
<Card.Text
className="mt-1 text-truncate"
title={(location as TableCellLocation)?.full_address}
>
<span className="me-1">{city as string}</span>

{(location as TableCellLocation)?.full_address}
</Card.Text>
</Col>
</Row>
<Row as="footer" className="flex-fill small mt-1">
<Col xs={8}>
<TagNav
list={host as string[]}
linkOf={organizer => `/search/activity?keywords=${organizer}`}
/>
</Col>
<Col className="text-end" xs={4}>
<TimeDistance {...TimeOption} date={startTime as number} />
</Col>
</Row>
</Card.Body>
</Card>
);
38 changes: 38 additions & 0 deletions components/Activity/Charts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { BarSeries, SVGCharts, Title, Tooltip, XAxis, YAxis } from 'echarts-jsx';
import { observer } from 'mobx-react';
import { FC, useContext } from 'react';

import { AgendaModel } from '../../models/Activity/Agenda';
import { I18nContext } from '../../models/Base/Translation';

type ActivityDataProps = Awaited<ReturnType<AgendaModel['getStatistics']>>;

const ActivityCharts: FC<ActivityDataProps> = observer(
({ keynoteSpeechCounts, mentorOrganizationCounts }) => {
const { t } = useContext(I18nContext);

const keynoteSpeechList = Object.entries(keynoteSpeechCounts).sort((a, b) => b[1] - a[1]),
mentorOrganizationList = Object.entries(mentorOrganizationCounts).sort((a, b) => b[1] - a[1]);

return (
<>
<SVGCharts>
<Title>{t('distribution_of_activity_topics_by_heat')}</Title>
<XAxis type="category" data={keynoteSpeechList.map(([key]) => key)} />
<YAxis type="value" />
<BarSeries data={keynoteSpeechList.map(([{}, value]) => value)} />
<Tooltip />
</SVGCharts>

<SVGCharts>
<Title>{t('distribution_of_mentor_organizations_by_topics')}</Title>
<XAxis type="category" data={mentorOrganizationList.map(([key]) => key)} />
<YAxis type="value" />
<BarSeries data={mentorOrganizationList.map(([{}, value]) => value)} />
<Tooltip />
</SVGCharts>
</>
);
},
);
export default ActivityCharts;
47 changes: 47 additions & 0 deletions components/Activity/DrawerNav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Icon, PageNav } from 'idea-react';
import { observable } from 'mobx';
import { observer } from 'mobx-react';
import { Component } from 'react';
import { Button, Offcanvas } from 'react-bootstrap';
import { sleep } from 'web-utility';

@observer
export class DrawerNav extends Component {
@observable
accessor drawerShown = false;

closeDrawer = async () => {
let { scrollTop } = document.scrollingElement || {};

do {
await sleep(0.1);

if (scrollTop === document.scrollingElement?.scrollTop) {
this.drawerShown = false;
break;
}
scrollTop = document.scrollingElement?.scrollTop;
// eslint-disable-next-line no-constant-condition
} while (true);
};

render() {
const { drawerShown, closeDrawer } = this;

return (
<>
<div className="fixed-bottom p-3">
<Button onClick={() => (this.drawerShown = true)}>
<Icon name="layout-text-sidebar" />
</Button>
</div>

<Offcanvas style={{ width: 'max-content' }} show={drawerShown} onHide={closeDrawer}>
<Offcanvas.Body>
<PageNav depth={2} onItemClick={this.closeDrawer} />
</Offcanvas.Body>
</Offcanvas>
</>
);
}
}
10 changes: 10 additions & 0 deletions components/Activity/GiftCard.module.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.gift {
&.disabled {
filter: grayscale(1);
cursor: not-allowed;
}
img {
width: 10rem;
height: 10rem;
}
}
Loading