diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e7eebb4..13ec060 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/components/Activity/Agenda/Card.tsx b/components/Activity/Agenda/Card.tsx new file mode 100644 index 0000000..3e72397 --- /dev/null +++ b/components/Activity/Agenda/Card.tsx @@ -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 { + renderCardImage = (file: TableCellAttachment) => ( + + ); + + renderAvatarImages() { + const { type, mentors, mentorAvatars, organizationLogos } = this.props; + const images = (type === 'Booth' && organizationLogos) || mentorAvatars; + + return (mentors as string[])?.[1] ? ( + + {(images as TableCellAttachment[])?.map(file => ( + {this.renderCardImage(file)} + ))} + + ) : ( + + ); + } + + render() { + const { + activityId, + id, + type, + title, + mentors, + mentorOrganizations, + mentorPositions, + startTime, + endTime, + score, + } = this.props; + + return ( + + + + + + {type + ''} + + + + {this.renderAvatarImages()} + + + +

+ + + {title as string} + + +

+
    +
  • + +
  • + {(mentors as string[])?.map((name, index) => ( +
  • + {name} {(mentorOrganizations as string[])?.[index]}{' '} + {(mentorPositions as string[])?.[index]} +
  • + ))} + {score && ( +
  • + +
  • + )} + +
+ +
+
+ ); + } +} diff --git a/components/Activity/Agenda/FileList.tsx b/components/Activity/Agenda/FileList.tsx new file mode 100644 index 0000000..e7625ad --- /dev/null +++ b/components/Activity/Agenda/FileList.tsx @@ -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 ( +
+

{t('file_download')}

+
    + {data.map(({ id, name, mimeType, attachmentToken }) => ( +
  1. + + + {name} +
  2. + ))} +
+
+ ); +}); diff --git a/components/Activity/Agenda/Toolbar.tsx b/components/Activity/Agenda/Toolbar.tsx new file mode 100644 index 0000000..e00b384 --- /dev/null +++ b/components/Activity/Agenda/Toolbar.tsx @@ -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, Agenda { + activityId: string; +} + +const AgendaToolbar: FC = observer( + ({ + activityId, + location, + id, + title, + summary, + startTime, + endTime, + mentors, + children, + ...props + }) => { + const { t } = useContext(I18nContext); + + return ( + + + + {!isServer() && ( + // @ts-expect-error https://github.com/josephj/react-icalendar-link/issues/41#issuecomment-1584173370 + + {t('calendar')} + + )} + + {children} + + ); + }, +); +export default AgendaToolbar; diff --git a/components/Activity/Card.tsx b/components/Activity/Card.tsx new file mode 100644 index 0000000..e1cb326 --- /dev/null +++ b/components/Activity/Card.tsx @@ -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 = ({ + className = '', + id, + host, + name, + startTime, + city, + location, + image, + ...activity +}) => ( + +
+
+ +
+
+ + + + {name as string} + + + + + + + {city as string} + + {(location as TableCellLocation)?.full_address} + + + + + + `/search/activity?keywords=${organizer}`} + /> + + + + + + +
+); diff --git a/components/Activity/Charts.tsx b/components/Activity/Charts.tsx new file mode 100644 index 0000000..878fe7c --- /dev/null +++ b/components/Activity/Charts.tsx @@ -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>; + +const ActivityCharts: FC = 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 ( + <> + + {t('distribution_of_activity_topics_by_heat')} + key)} /> + + value)} /> + + + + + {t('distribution_of_mentor_organizations_by_topics')} + key)} /> + + value)} /> + + + + ); + }, +); +export default ActivityCharts; diff --git a/components/Activity/DrawerNav.tsx b/components/Activity/DrawerNav.tsx new file mode 100644 index 0000000..9ef02b0 --- /dev/null +++ b/components/Activity/DrawerNav.tsx @@ -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 ( + <> +
+ +
+ + + + + + + + ); + } +} diff --git a/components/Activity/GiftCard.module.less b/components/Activity/GiftCard.module.less new file mode 100644 index 0000000..bb63d09 --- /dev/null +++ b/components/Activity/GiftCard.module.less @@ -0,0 +1,10 @@ +.gift { + &.disabled { + filter: grayscale(1); + cursor: not-allowed; + } + img { + width: 10rem; + height: 10rem; + } +} diff --git a/components/Activity/GiftCard.tsx b/components/Activity/GiftCard.tsx new file mode 100644 index 0000000..5c99c82 --- /dev/null +++ b/components/Activity/GiftCard.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; +import { Badge } from 'react-bootstrap'; + +import { Gift } from '../../models/Activity/Gift'; +import { LarkImage } from '../Base/LarkImage'; +import style from './GiftCard.module.less'; + +export interface GiftCardProps extends Gift { + disabled?: boolean; +} + +export const GiftCard: FC = ({ name, photo, stock, disabled = !stock }) => ( + <> +
+ + +
+ + {stock as number} + +
+
+ + {name as string} + +); diff --git a/components/Activity/List.tsx b/components/Activity/List.tsx new file mode 100644 index 0000000..0d1387e --- /dev/null +++ b/components/Activity/List.tsx @@ -0,0 +1,15 @@ +import { FC } from 'react'; +import { Col, Row } from 'react-bootstrap'; + +import { Activity } from '../../models/Activity'; +import { ActivityCard } from './Card'; + +export const ActivityListLayout: FC<{ defaultData: Activity[] }> = ({ defaultData }) => ( + + {defaultData.map(item => ( + + + + ))} + +); diff --git a/components/Activity/People.tsx b/components/Activity/People.tsx new file mode 100644 index 0000000..be7983a --- /dev/null +++ b/components/Activity/People.tsx @@ -0,0 +1,46 @@ +import { AvatarProps } from 'idea-react'; +import { TableCellAttachment, TableCellMedia, TableCellValue } from 'mobx-lark'; +import { FC } from 'react'; + +import { LarkImage } from '../Base/LarkImage'; + +type TableCellFile = TableCellMedia | TableCellAttachment; + +export interface ActivityPeopleProps + extends Pick, + Partial> { + avatars?: TableCellValue; +} + +export const ActivityPeople: FC = ({ + size = 3, + names, + avatars, + positions, + organizations, + summaries, +}) => ( +
    + {(avatars as TableCellFile[])?.map((avatar, index) => ( +
  • + +
      +
    • {names?.[index]}
    • +
    • {organizations?.[index]}
    • +
    • {positions?.[index]}
    • +
    • {summaries?.[index]}
    • +
    +
  • + ))} +
+); diff --git a/components/Activity/index.ts b/components/Activity/index.ts new file mode 100644 index 0000000..45e2d93 --- /dev/null +++ b/components/Activity/index.ts @@ -0,0 +1,8 @@ +export * from './Agenda/Card'; +export * from './Agenda/FileList'; +export { default as AgendaToolbar, type AgendaToolbarProps } from './Agenda/Toolbar'; +export * from './Card'; +export * from './DrawerNav'; +export * from './GiftCard'; +export * from './List'; +export * from './People'; diff --git a/components/Base/CommentBox.tsx b/components/Base/CommentBox.tsx new file mode 100644 index 0000000..e7553bf --- /dev/null +++ b/components/Base/CommentBox.tsx @@ -0,0 +1,23 @@ +import Giscus, { GiscusProps } from '@giscus/react'; +import { observer } from 'mobx-react'; +import { FC } from 'react'; + +import { i18n } from '../../models/Base/Translation'; + +export const CommentBox: FC> = observer(props => { + const { currentLanguage } = i18n; + + return ( + + ); +}); diff --git a/components/Base/LanguageMenu.tsx b/components/Base/LanguageMenu.tsx new file mode 100644 index 0000000..ae96d22 --- /dev/null +++ b/components/Base/LanguageMenu.tsx @@ -0,0 +1,24 @@ +import { Option, Select } from 'idea-react'; +import { observer } from 'mobx-react'; +import { FC, useContext } from 'react'; + +import { I18nContext, LanguageName } from '../../models/Base/Translation'; + +const LanguageMenu: FC = observer(() => { + const i18n = useContext(I18nContext); + const { currentLanguage } = i18n; + + return ( + + ); +}); +export default LanguageMenu; diff --git a/components/LarkImage.tsx b/components/Base/LarkImage.tsx similarity index 76% rename from components/LarkImage.tsx rename to components/Base/LarkImage.tsx index e0cfbac..121709a 100644 --- a/components/LarkImage.tsx +++ b/components/Base/LarkImage.tsx @@ -2,18 +2,14 @@ import { TableCellValue } from 'mobx-lark'; import { FC } from 'react'; import { Image, ImageProps } from 'react-bootstrap'; -import { fileURLOf } from '../models/Base'; -import { DefaultImage } from '../models/configuration'; +import { DefaultImage } from '../../utility/configuration'; +import { fileURLOf } from '../../utility/Lark'; export interface LarkImageProps extends Omit { src?: TableCellValue; } -export const LarkImage: FC = ({ - src = DefaultImage, - alt, - ...props -}) => ( +export const LarkImage: FC = ({ src = DefaultImage, alt, ...props }) => ( = ({ direction = 'row', list }) => ( + +
+ + + {list.map(({ key, content }) => ( + + {content} + + ))} + +
+
+); diff --git a/components/Base/QRCodeButton.tsx b/components/Base/QRCodeButton.tsx new file mode 100644 index 0000000..c9901cb --- /dev/null +++ b/components/Base/QRCodeButton.tsx @@ -0,0 +1,41 @@ +import { Dialog, DialogClose } from 'idea-react'; +import { QRCodeCanvas } from 'qrcode.react'; +import { FC } from 'react'; +import { Button, ButtonProps, Modal, ModalProps } from 'react-bootstrap'; + +type DialogQRCProps = Pick< + ModalProps, + 'size' | 'fullscreen' | 'centered' | 'animation' | 'scrollable' +> & + Pick; + +const dialogQRC = new Dialog(({ defer, title, value, ...props }) => ( + defer?.reject(new DialogClose())}> + + {title} + + + + + +)); + +export type QRCodeButtonProps = DialogQRCProps & ButtonProps; + +export const QRCodeButton: FC = ({ + title, + value = '', + size = 'sm', + variant = 'danger', + disabled, + children, + ...props +}) => ( + <> + + + + +); diff --git a/components/Base/ScoreBar.tsx b/components/Base/ScoreBar.tsx new file mode 100644 index 0000000..38643fd --- /dev/null +++ b/components/Base/ScoreBar.tsx @@ -0,0 +1,6 @@ +import { RangeInput, RangeInputProps } from 'mobx-restful-table'; +import { FC } from 'react'; + +export const ScoreBar: FC = ({ value }) => ( + (value > 0.5 ? '❤' : '🤍')} value={value} readOnly /> +); diff --git a/components/Base/SearchBar.module.less b/components/Base/SearchBar.module.less new file mode 100644 index 0000000..fb76ecf --- /dev/null +++ b/components/Base/SearchBar.module.less @@ -0,0 +1,7 @@ +.input { + transition: 0.25s; + width: 3rem !important; + &:focus { + width: 10rem !important; + } +} diff --git a/components/Base/SearchBar.tsx b/components/Base/SearchBar.tsx new file mode 100644 index 0000000..47c58c2 --- /dev/null +++ b/components/Base/SearchBar.tsx @@ -0,0 +1,47 @@ +import { observer } from 'mobx-react'; +import { FC } from 'react'; +import { + Button, + Form, + FormControlProps, + FormProps, + InputGroup, + InputGroupProps, +} from 'react-bootstrap'; + +import { i18n } from '../../models/Base/Translation'; +import styles from './SearchBar.module.less'; + +export interface SearchBarProps + extends Omit, + Pick, + Pick { + expanded?: boolean; +} + +export const SearchBar: FC = observer( + ({ + action = '/search/article', + size, + name = 'keywords', + placeholder = i18n.t('keyword'), + expanded = true, + defaultValue, + value, + onChange, + ...props + }) => ( +
+ + + + +
+ ), +); diff --git a/components/Base/TagNav.tsx b/components/Base/TagNav.tsx new file mode 100644 index 0000000..72d1f3c --- /dev/null +++ b/components/Base/TagNav.tsx @@ -0,0 +1,26 @@ +import { text2color } from 'idea-react'; +import { FC, HTMLAttributes } from 'react'; +import { Badge } from 'react-bootstrap'; + +export interface TagNavProps extends HTMLAttributes { + linkOf?: (value: string) => string; + list: string[]; + onCheck?: (value: string) => any; +} + +export const TagNav: FC = ({ className = '', list, linkOf, onCheck, ...props }) => ( + +); diff --git a/components/Base/TimeRange.tsx b/components/Base/TimeRange.tsx new file mode 100644 index 0000000..384e0c8 --- /dev/null +++ b/components/Base/TimeRange.tsx @@ -0,0 +1,15 @@ +import { Time } from 'idea-react'; +import { TableCellValue } from 'mobx-lark'; +import { FC } from 'react'; + +export const TimeRange: FC> = ({ + startTime, + endTime, +}) => + startTime && + endTime && ( + <> + 🕒