diff --git a/.env b/.env index dc5c88f..6efe19b 100644 --- a/.env +++ b/.env @@ -6,4 +6,7 @@ NEXT_PUBLIC_LARK_API_HOST = https://open.feishu.cn/open-apis/ NEXT_PUBLIC_LARK_APP_ID = cli_a8094a652022900d NEXT_PUBLIC_LARK_WIKI_URL = https://open-source-bazaar.feishu.cn/wiki/space/7052192153363054596 +NEXT_PUBLIC_LARK_BITABLE_ID = PNOGbGqhPacsHOsvJqHctS77nje +NEXT_PUBLIC_ACTIVITY_TABLE_ID = tblREEMxDOECZZrK + NEXT_PUBLIC_STRAPI_API_HOST = https://china-ngo-db.onrender.com/api/ diff --git a/components/Activity/Card.tsx b/components/Activity/Card.tsx new file mode 100644 index 0000000..ee6b122 --- /dev/null +++ b/components/Activity/Card.tsx @@ -0,0 +1,76 @@ +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 '../LarkImage'; +import { TimeOption } from '../data'; +import { BadgeBar } from 'mobx-restful-table'; + +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} + + + + + + ({ + text, + link: `/search/activity?keywords=${text}`, + }))} + /> + + + + + + +
+); diff --git a/components/Navigator/MainNavigator.tsx b/components/Navigator/MainNavigator.tsx index 8894306..76b0824 100644 --- a/components/Navigator/MainNavigator.tsx +++ b/components/Navigator/MainNavigator.tsx @@ -6,6 +6,7 @@ import { Container, Image, Nav, Navbar, NavDropdown } from 'react-bootstrap'; import { DefaultImage } from '../../models/configuration'; import { i18n, I18nContext } from '../../models/Translation'; +import { SearchBar } from './SearchBar'; const LanguageMenu = dynamic(() => import('./LanguageMenu'), { ssr: false }); @@ -107,7 +108,10 @@ export const MainNavigator: FC = observer(({ menu }) => { )} - +
+ + +
diff --git a/components/Navigator/SearchBar.tsx b/components/Navigator/SearchBar.tsx index e6406aa..dc8727f 100644 --- a/components/Navigator/SearchBar.tsx +++ b/components/Navigator/SearchBar.tsx @@ -13,18 +13,16 @@ import { I18nContext } from '../../models/Translation'; import styles from './SearchBar.module.less'; export interface SearchBarProps - extends Omit, + extends + Omit, Pick, - Pick< - FormControlProps, - 'name' | 'placeholder' | 'defaultValue' | 'value' | 'onChange' - > { + Pick { expanded?: boolean; } export const SearchBar: FC = observer( ({ - action = '/search', + action = '/search/activity', size, name = 'keywords', placeholder, diff --git a/components/PageContent/PageContent.module.scss b/components/PageContent/PageContent.module.less similarity index 100% rename from components/PageContent/PageContent.module.scss rename to components/PageContent/PageContent.module.less diff --git a/components/PageContent/index.tsx b/components/PageContent/index.tsx index f3b4f1b..b9d68cd 100644 --- a/components/PageContent/index.tsx +++ b/components/PageContent/index.tsx @@ -2,8 +2,8 @@ import { MDXProvider } from '@mdx-js/react'; import type { FC, PropsWithChildren } from 'react'; import { Card, Container } from 'react-bootstrap'; -import styles from '../../styles/Home.module.scss'; -import pageContentStyles from './PageContent.module.scss'; +import styles from '../../styles/Home.module.less'; +import pageContentStyles from './PageContent.module.less'; export type PageContentProps = PropsWithChildren<{}>; diff --git a/components/data.ts b/components/data.ts new file mode 100644 index 0000000..748b3ae --- /dev/null +++ b/components/data.ts @@ -0,0 +1,16 @@ +import { TimeDistanceProps } from 'idea-react'; + +export const TimeOption: Pick = { + unitWords: { + ms: '毫秒', + s: '秒', + m: '分', + H: '时', + D: '日', + W: '周', + M: '月', + Y: '年', + }, + beforeWord: '前', + afterWord: '后', +}; diff --git a/models/Activity.ts b/models/Activity.ts new file mode 100644 index 0000000..e2d8864 --- /dev/null +++ b/models/Activity.ts @@ -0,0 +1,102 @@ +import { + BiDataQueryOptions, + BiDataTable, + BiSearch, + LarkPageData, + makeSimpleFilter, + normalizeText, + TableCellLink, + TableCellRelation, + TableCellValue, + TableRecord, +} from 'mobx-lark'; +import { toggle } from 'mobx-restful'; +import { HTTPError } from 'koajax'; +import { buildURLData } from 'web-utility'; + +import { LarkBase, larkClient } from './Base'; +import { ActivityTableId, LarkBitableId } from './configuration'; + +export type Activity = LarkBase & + Record< + | 'name' + | 'alias' + | 'type' + | 'tags' + | 'summary' + | 'image' + | 'cardImage' + | `${'start' | 'end'}Time` + | 'city' + | 'location' + | 'host' + | 'link' + | 'liveLink' + | `database${'' | 'Schema'}`, + TableCellValue + >; + +export class ActivityModel extends BiDataTable() { + client = larkClient; + + queryOptions: BiDataQueryOptions = { text_field_as_array: false }; + + constructor(appId = LarkBitableId, tableId = ActivityTableId) { + super(appId, tableId); + } + + static getLink = ({ + id, + type, + alias, + link, + database, + }: Pick) => + database ? `/${type?.toString().toLowerCase() || 'activity'}/${alias || id}` : link + ''; + + extractFields({ + id, + fields: { host, city, link, database, databaseSchema, ...fields }, + }: TableRecord) { + return { + ...fields, + id: id!, + host: (host as TableCellRelation[])?.map(normalizeText), + city: (city as TableCellRelation[])?.map(normalizeText), + link: (link as TableCellLink)?.link, + database: (database as TableCellLink)?.link, + databaseSchema: databaseSchema && JSON.parse(databaseSchema as string), + }; + } + + @toggle('downloading') + async getOneByAlias(alias: string) { + const path = `${this.baseURI}?${buildURLData({ filter: makeSimpleFilter({ alias }, '=') })}`; + + const { body } = await this.client.get>>(path); + + const [item] = body!.data!.items || []; + + if (!item) + throw new HTTPError( + `Activity "${alias}" is not found`, + { method: 'GET', path }, + { status: 404, statusText: 'Not found', headers: {} }, + ); + return (this.currentOne = this.extractFields(item)); + } + + @toggle('downloading') + async getOne(id: string) { + try { + await super.getOne(id); + } catch { + await this.getOneByAlias(id); + } + return this.currentOne; + } +} + +export class SearchActivityModel extends BiSearch(ActivityModel) { + searchKeys = ['name', 'alias', 'type', 'tags', 'summary', 'city', 'location', 'host']; +} diff --git a/models/Base.ts b/models/Base.ts index 1c5b56b..38ecbe4 100644 --- a/models/Base.ts +++ b/models/Base.ts @@ -51,6 +51,11 @@ export const makeGithubSearchCondition = (queryMap: DataObject) => .map(([key, value]) => `${key}:${value}`) .join(' '); +export type LarkBase = Record< + 'id' | `created${'At' | 'By'}` | `updated${'At' | 'By'}`, + TableCellValue +>; + export const larkClient = new HTTPClient({ baseURI: LARK_API_HOST, responseType: 'json', diff --git a/models/Hackathon.ts b/models/Hackathon.ts new file mode 100644 index 0000000..63c2048 --- /dev/null +++ b/models/Hackathon.ts @@ -0,0 +1,142 @@ +// Hackathon data types and mock data generator + +import { + BiDataQueryOptions, + BiDataTable, + normalizeText, + TableCellRelation, + TableCellText, + TableCellValue, + TableRecord, +} from 'mobx-lark'; + +import { LarkBase, larkClient } from './Base'; + +export type Agenda = LarkBase & + Record<'summary' | 'name' | 'type' | 'startedAt' | 'endedAt', TableCellValue>; + +export class AgendaModel extends BiDataTable() { + client = larkClient; + + queryOptions: BiDataQueryOptions = { text_field_as_array: false }; + + extractFields({ fields: { summary, ...fields }, ...meta }: TableRecord) { + return { + ...meta, + ...fields, + summary: normalizeText(summary as TableCellText), + }; + } +} + +export type Person = LarkBase & + Record< + | 'name' + | 'avatar' + | 'gender' + | 'age' + | 'address' + | 'organizations' + | 'skills' + | 'githubLink' + | 'githubAccount' + | 'createdBy', + TableCellValue + >; + +export class PersonModel extends BiDataTable() { + client = larkClient; + + queryOptions: BiDataQueryOptions = { text_field_as_array: false }; + + extractFields({ fields: { githubLink, ...fields }, ...meta }: TableRecord) { + return { + ...meta, + ...fields, + githubLink: normalizeText(githubLink as TableCellText), + }; + } +} + +export type Organization = LarkBase & + Record<'name' | 'logo' | 'link' | 'members' | 'prizes', TableCellValue>; + +export class OrganizationModel extends BiDataTable() { + client = larkClient; + + queryOptions: BiDataQueryOptions = { text_field_as_array: false }; + + extractFields({ fields: { link, ...fields }, ...meta }: TableRecord) { + return { + ...meta, + ...fields, + link: normalizeText(link as TableCellText), + }; + } +} + +export type Prize = LarkBase & + Record< + | 'summary' + | 'name' + | 'image' + | 'price' + | 'amount' + | 'level' + | `${'start' | 'end'}Rank` + | 'sponsor', + TableCellValue + >; + +export class PrizeModel extends BiDataTable() { + client = larkClient; + + queryOptions: BiDataQueryOptions = { text_field_as_array: false }; +} + +export type Template = LarkBase & + Record< + 'name' | 'languages' | 'tags' | 'summary' | `${'source' | 'preview'}Link` | 'products', + TableCellValue + >; + +export class TemplateModel extends BiDataTable