Skip to content

Commit b44ea57

Browse files
CopilotTechQuery
andauthored
[add] Hackathon home page with Lark BI Table API (#44)
Co-authored-by: TechQuery <[email protected]>
1 parent 250884f commit b44ea57

File tree

25 files changed

+1289
-243
lines changed

25 files changed

+1289
-243
lines changed

.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ NEXT_PUBLIC_LARK_API_HOST = https://open.feishu.cn/open-apis/
66
NEXT_PUBLIC_LARK_APP_ID = cli_a8094a652022900d
77
NEXT_PUBLIC_LARK_WIKI_URL = https://open-source-bazaar.feishu.cn/wiki/space/7052192153363054596
88

9+
NEXT_PUBLIC_LARK_BITABLE_ID = PNOGbGqhPacsHOsvJqHctS77nje
10+
NEXT_PUBLIC_ACTIVITY_TABLE_ID = tblREEMxDOECZZrK
11+
912
NEXT_PUBLIC_STRAPI_API_HOST = https://china-ngo-db.onrender.com/api/

components/Activity/Card.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 '../LarkImage';
8+
import { TimeOption } from '../data';
9+
import { BadgeBar } from 'mobx-restful-table';
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+
<BadgeBar
64+
list={(host as string[]).map(text => ({
65+
text,
66+
link: `/search/activity?keywords=${text}`,
67+
}))}
68+
/>
69+
</Col>
70+
<Col className="text-end" xs={4}>
71+
<TimeDistance {...TimeOption} date={startTime as number} />
72+
</Col>
73+
</Row>
74+
</Card.Body>
75+
</Card>
76+
);

components/Navigator/MainNavigator.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Container, Image, Nav, Navbar, NavDropdown } from 'react-bootstrap';
66

77
import { DefaultImage } from '../../models/configuration';
88
import { i18n, I18nContext } from '../../models/Translation';
9+
import { SearchBar } from './SearchBar';
910

1011
const LanguageMenu = dynamic(() => import('./LanguageMenu'), { ssr: false });
1112

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

110-
<LanguageMenu />
111+
<div className="d-flex justify-content-around gap-3">
112+
<SearchBar />
113+
<LanguageMenu />
114+
</div>
111115
</Navbar.Collapse>
112116
</Container>
113117
</Navbar>

components/Navigator/SearchBar.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,16 @@ import { I18nContext } from '../../models/Translation';
1313
import styles from './SearchBar.module.less';
1414

1515
export interface SearchBarProps
16-
extends Omit<FormProps, 'onChange'>,
16+
extends
17+
Omit<FormProps, 'onChange'>,
1718
Pick<InputGroupProps, 'size'>,
18-
Pick<
19-
FormControlProps,
20-
'name' | 'placeholder' | 'defaultValue' | 'value' | 'onChange'
21-
> {
19+
Pick<FormControlProps, 'name' | 'placeholder' | 'defaultValue' | 'value' | 'onChange'> {
2220
expanded?: boolean;
2321
}
2422

2523
export const SearchBar: FC<SearchBarProps> = observer(
2624
({
27-
action = '/search',
25+
action = '/search/activity',
2826
size,
2927
name = 'keywords',
3028
placeholder,
File renamed without changes.

components/PageContent/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { MDXProvider } from '@mdx-js/react';
22
import type { FC, PropsWithChildren } from 'react';
33
import { Card, Container } from 'react-bootstrap';
44

5-
import styles from '../../styles/Home.module.scss';
6-
import pageContentStyles from './PageContent.module.scss';
5+
import styles from '../../styles/Home.module.less';
6+
import pageContentStyles from './PageContent.module.less';
77

88
export type PageContentProps = PropsWithChildren<{}>;
99

components/data.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TimeDistanceProps } from 'idea-react';
2+
3+
export const TimeOption: Pick<TimeDistanceProps, 'unitWords' | 'beforeWord' | 'afterWord'> = {
4+
unitWords: {
5+
ms: '毫秒',
6+
s: '秒',
7+
m: '分',
8+
H: '时',
9+
D: '日',
10+
W: '周',
11+
M: '月',
12+
Y: '年',
13+
},
14+
beforeWord: '前',
15+
afterWord: '后',
16+
};

models/Activity.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import {
2+
BiDataQueryOptions,
3+
BiDataTable,
4+
BiSearch,
5+
LarkPageData,
6+
makeSimpleFilter,
7+
normalizeText,
8+
TableCellLink,
9+
TableCellRelation,
10+
TableCellValue,
11+
TableRecord,
12+
} from 'mobx-lark';
13+
import { toggle } from 'mobx-restful';
14+
import { HTTPError } from 'koajax';
15+
import { buildURLData } from 'web-utility';
16+
17+
import { LarkBase, larkClient } from './Base';
18+
import { ActivityTableId, LarkBitableId } from './configuration';
19+
20+
export type Activity = LarkBase &
21+
Record<
22+
| 'name'
23+
| 'alias'
24+
| 'type'
25+
| 'tags'
26+
| 'summary'
27+
| 'image'
28+
| 'cardImage'
29+
| `${'start' | 'end'}Time`
30+
| 'city'
31+
| 'location'
32+
| 'host'
33+
| 'link'
34+
| 'liveLink'
35+
| `database${'' | 'Schema'}`,
36+
TableCellValue
37+
>;
38+
39+
export class ActivityModel extends BiDataTable<Activity>() {
40+
client = larkClient;
41+
42+
queryOptions: BiDataQueryOptions = { text_field_as_array: false };
43+
44+
constructor(appId = LarkBitableId, tableId = ActivityTableId) {
45+
super(appId, tableId);
46+
}
47+
48+
static getLink = ({
49+
id,
50+
type,
51+
alias,
52+
link,
53+
database,
54+
}: Pick<Activity, 'id' | 'type' | 'alias' | 'link' | 'database'>) =>
55+
database ? `/${type?.toString().toLowerCase() || 'activity'}/${alias || id}` : link + '';
56+
57+
extractFields({
58+
id,
59+
fields: { host, city, link, database, databaseSchema, ...fields },
60+
}: TableRecord<Activity>) {
61+
return {
62+
...fields,
63+
id: id!,
64+
host: (host as TableCellRelation[])?.map(normalizeText),
65+
city: (city as TableCellRelation[])?.map(normalizeText),
66+
link: (link as TableCellLink)?.link,
67+
database: (database as TableCellLink)?.link,
68+
databaseSchema: databaseSchema && JSON.parse(databaseSchema as string),
69+
};
70+
}
71+
72+
@toggle('downloading')
73+
async getOneByAlias(alias: string) {
74+
const path = `${this.baseURI}?${buildURLData({ filter: makeSimpleFilter({ alias }, '=') })}`;
75+
76+
const { body } = await this.client.get<LarkPageData<TableRecord<Activity>>>(path);
77+
78+
const [item] = body!.data!.items || [];
79+
80+
if (!item)
81+
throw new HTTPError(
82+
`Activity "${alias}" is not found`,
83+
{ method: 'GET', path },
84+
{ status: 404, statusText: 'Not found', headers: {} },
85+
);
86+
return (this.currentOne = this.extractFields(item));
87+
}
88+
89+
@toggle('downloading')
90+
async getOne(id: string) {
91+
try {
92+
await super.getOne(id);
93+
} catch {
94+
await this.getOneByAlias(id);
95+
}
96+
return this.currentOne;
97+
}
98+
}
99+
100+
export class SearchActivityModel extends BiSearch<Activity>(ActivityModel) {
101+
searchKeys = ['name', 'alias', 'type', 'tags', 'summary', 'city', 'location', 'host'];
102+
}

models/Base.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ export const makeGithubSearchCondition = (queryMap: DataObject) =>
5151
.map(([key, value]) => `${key}:${value}`)
5252
.join(' ');
5353

54+
export type LarkBase = Record<
55+
'id' | `created${'At' | 'By'}` | `updated${'At' | 'By'}`,
56+
TableCellValue
57+
>;
58+
5459
export const larkClient = new HTTPClient({
5560
baseURI: LARK_API_HOST,
5661
responseType: 'json',

0 commit comments

Comments
 (0)