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
3 changes: 3 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -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/
76 changes: 76 additions & 0 deletions components/Activity/Card.tsx
Original file line number Diff line number Diff line change
@@ -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<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}>
<BadgeBar
list={(host as string[]).map(text => ({
text,
link: `/search/activity?keywords=${text}`,
}))}
/>
</Col>
<Col className="text-end" xs={4}>
<TimeDistance {...TimeOption} date={startTime as number} />
</Col>
</Row>
</Card.Body>
</Card>
);
6 changes: 5 additions & 1 deletion components/Navigator/MainNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand Down Expand Up @@ -107,7 +108,10 @@ export const MainNavigator: FC<MainNavigatorProps> = observer(({ menu }) => {
)}
</Nav>

<LanguageMenu />
<div className="d-flex justify-content-around gap-3">
<SearchBar />
<LanguageMenu />
</div>
</Navbar.Collapse>
</Container>
</Navbar>
Expand Down
10 changes: 4 additions & 6 deletions components/Navigator/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@ import { I18nContext } from '../../models/Translation';
import styles from './SearchBar.module.less';

export interface SearchBarProps
extends Omit<FormProps, 'onChange'>,
extends
Omit<FormProps, 'onChange'>,
Pick<InputGroupProps, 'size'>,
Pick<
FormControlProps,
'name' | 'placeholder' | 'defaultValue' | 'value' | 'onChange'
> {
Pick<FormControlProps, 'name' | 'placeholder' | 'defaultValue' | 'value' | 'onChange'> {
expanded?: boolean;
}

export const SearchBar: FC<SearchBarProps> = observer(
({
action = '/search',
action = '/search/activity',
size,
name = 'keywords',
placeholder,
Expand Down
4 changes: 2 additions & 2 deletions components/PageContent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<{}>;

Expand Down
16 changes: 16 additions & 0 deletions components/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TimeDistanceProps } from 'idea-react';

export const TimeOption: Pick<TimeDistanceProps, 'unitWords' | 'beforeWord' | 'afterWord'> = {
unitWords: {
ms: '毫秒',
s: '秒',
m: '分',
H: '时',
D: '日',
W: '周',
M: '月',
Y: '年',
},
beforeWord: '前',
afterWord: '后',
};
102 changes: 102 additions & 0 deletions models/Activity.ts
Original file line number Diff line number Diff line change
@@ -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<Activity>() {
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<Activity, 'id' | 'type' | 'alias' | 'link' | 'database'>) =>
database ? `/${type?.toString().toLowerCase() || 'activity'}/${alias || id}` : link + '';

extractFields({
id,
fields: { host, city, link, database, databaseSchema, ...fields },
}: TableRecord<Activity>) {
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<LarkPageData<TableRecord<Activity>>>(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<Activity>(ActivityModel) {
searchKeys = ['name', 'alias', 'type', 'tags', 'summary', 'city', 'location', 'host'];
}
5 changes: 5 additions & 0 deletions models/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading
Loading