Skip to content

Commit 4a83fd2

Browse files
committed
[add] Organization Charts component
[remove] useless Tag Nav component [fix] many GitHub copilot bugs
1 parent 56ffc8a commit 4a83fd2

15 files changed

Lines changed: 176 additions & 106 deletions

File tree

.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ NEXT_PUBLIC_LOGO = https://github.com/Open-Source-Bazaar.png
55
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
8+
9+
NEXT_PUBLIC_STRAPI_API_HOST = https://china-ngo-db.onrender.com/api/

components/Base/TagNav.tsx

Lines changed: 0 additions & 22 deletions
This file was deleted.

components/Navigator/MainNavigator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
5656
title: t('ngo'),
5757
subs: [
5858
{ href: '/ngo', title: t('China_NGO_Map') },
59-
{ href: '/ngo/landscape', title: t('china_public_interest_landscape') },
59+
{ href: '/ngo/landscape', title: t('China_NGO_Landscape') },
6060
{
6161
href: 'https://open-source-bazaar.feishu.cn/wiki/VGrMwiweVivWrHkTcvpcJTjjnoY',
6262
title: t('open_source_public_interest_plan')

components/Organization/Charts.tsx

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,43 @@
1-
import { FC } from 'react';
1+
import { BarSeries, PieSeries, SVGCharts, Title, Tooltip, XAxis, YAxis } from 'echarts-jsx';
2+
import { observer } from 'mobx-react';
3+
import { FC, useContext } from 'react';
24

3-
import { OrganizationStatistic } from '../../models/Organization';
5+
import { I18nContext } from '../../models/Translation';
6+
import { OrganizationStatistic, sortStatistic } from '../../models/Organization';
47

5-
// Placeholder for now - this will be implemented based on actual chart requirements
6-
const OrganizationCharts: FC<OrganizationStatistic> = () => (
7-
<div>Charts placeholder for organization statistics</div>
8-
);
8+
const OrganizationCharts: FC<OrganizationStatistic> = observer(
9+
({ entityType, serviceCategory, coverageArea }) => {
10+
const { t } = useContext(I18nContext);
11+
12+
const typeList = sortStatistic(entityType),
13+
categoryList = sortStatistic(serviceCategory),
14+
areaList = sortStatistic(coverageArea);
15+
16+
return (
17+
<div style={{ minHeight: '70vh' }}>
18+
<SVGCharts>
19+
<Title>NGO 地区分布</Title>
20+
<XAxis type="category" data={areaList.map(([key]) => key)} />
21+
<YAxis type="value" />
22+
<BarSeries data={areaList.map(([{}, value]) => value)} />
23+
<Tooltip />
24+
</SVGCharts>
925

10-
export default OrganizationCharts;
26+
<SVGCharts>
27+
<Title>NGO 服务分布</Title>
28+
<XAxis type="category" data={categoryList.map(([key]) => key)} />
29+
<YAxis type="value" />
30+
<BarSeries data={categoryList.map(([{}, value]) => value)} />
31+
<Tooltip />
32+
</SVGCharts>
33+
34+
<SVGCharts className="col-auto">
35+
<Title>NGO 类型分布</Title>
36+
<PieSeries data={typeList.map(([name, value]) => ({ name, value }))} />
37+
<Tooltip trigger="item" />
38+
</SVGCharts>
39+
</div>
40+
);
41+
},
42+
);
43+
export default OrganizationCharts;

components/Organization/ChinaPublicInterestLandscape.tsx

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@ import { Dialog } from 'idea-react';
22
import { observable } from 'mobx';
33
import { observer } from 'mobx-react';
44
import { Component } from 'react';
5-
import { Modal } from 'react-bootstrap';
5+
import { Image, Modal } from 'react-bootstrap';
66
import { splitArray } from 'web-utility';
77

8-
import { Organization, OrganizationModel } from '../../models/Organization';
8+
import { Organization } from '@open-source-bazaar/china-ngo-database';
99
import systemStore from '../../models/System';
10-
import { LarkImage } from '../LarkImage';
10+
import { OrganizationModel } from '../../models/Organization';
11+
1112
import { OrganizationCard } from './Card';
13+
import styles from './LandScape.module.less';
1214

13-
export type ChinaPublicInterestLandscapeProps = Pick<OrganizationModel, 'categoryMap'>;
15+
export type OpenCollaborationLandscapeProps = Pick<OrganizationModel, 'categoryMap'>;
1416

1517
@observer
16-
export class ChinaPublicInterestLandscape extends Component<ChinaPublicInterestLandscapeProps> {
18+
export class OpenCollaborationLandscape extends Component<OpenCollaborationLandscapeProps> {
1719
@observable
1820
accessor itemSize = 5;
1921

@@ -33,24 +35,22 @@ export class ChinaPublicInterestLandscape extends Component<ChinaPublicInterestL
3335

3436
if (!organization) return <></>;
3537

36-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3738
const { id, ...data } = organization;
3839

3940
return <OrganizationCard {...data} />;
4041
}
4142

42-
renderLogo = ({ name, logo }: Organization) => (
43+
renderLogo = ({ name }: Organization) => (
4344
<li
4445
key={name as string}
45-
className="border list-item"
46-
style={{ cursor: 'pointer' }}
46+
className={`border ${styles.listItem}`}
4747
onClick={() => this.modal.open({ name: name as string })}
4848
>
49-
<LarkImage
50-
className="object-fit-contain"
51-
style={{ width: this.itemSize + 'rem', height: this.itemSize + 'rem' }}
52-
src={logo?.data?.attributes?.url}
53-
/>
49+
<div style={{ fontSize: this.itemSize + 'rem' }}>
50+
{name.slice(0, 2)}
51+
<br />
52+
{name.slice(2, 4)}
53+
</div>
5454
</li>
5555
);
5656

@@ -60,16 +60,11 @@ export class ChinaPublicInterestLandscape extends Component<ChinaPublicInterestL
6060

6161
return (
6262
<>
63-
{rows.map((row, index) => (
64-
<ul
65-
key={index}
66-
className={`list-unstyled d-flex flex-${screenNarrow ? 'column' : 'row'} gap-2`}
67-
>
63+
{rows.map(row => (
64+
<ul className={`list-unstyled d-flex flex-${screenNarrow ? 'column' : 'row'} gap-2`}>
6865
{row.map(([name, list]) => (
6966
<li key={name} className="flex-fill">
70-
<h2 className="h5 p-2 text-white bg-primary">
71-
{name}
72-
</h2>
67+
<h2 className={`h5 p-2 text-white ${styles.groupTitle}`}>{name}</h2>
7368

7469
<ol className="list-unstyled d-flex flex-wrap gap-2">
7570
{list.map(this.renderLogo)}
@@ -82,4 +77,4 @@ export class ChinaPublicInterestLandscape extends Component<ChinaPublicInterestL
8277
</>
8378
);
8479
}
85-
}
80+
}

models/Base.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ import { TableCellAttachment, TableCellMedia, TableCellValue } from 'mobx-lark';
66
import { DataObject } from 'mobx-restful';
77
import { isEmpty } from 'web-utility';
88

9-
import { API_Host, GithubToken, isServer, ProxyBaseURL, LARK_API_HOST } from './configuration';
9+
import {
10+
API_Host,
11+
GithubToken,
12+
isServer,
13+
ProxyBaseURL,
14+
LARK_API_HOST,
15+
STRAPI_API_HOST,
16+
STRAPI_API_TOKEN,
17+
} from './configuration';
1018

1119
export const ownClient = new HTTPClient({
1220
baseURI: `${API_Host}/api/`,
@@ -61,10 +69,11 @@ export function fileURLOf(field: TableCellValue, cache = false) {
6169
}
6270

6371
export const strapiClient = new HTTPClient({
64-
baseURI: 'https://china-ngo-db.onrender.com/api/',
72+
baseURI: STRAPI_API_HOST,
6573
responseType: 'json',
6674
}).use(({ request }, next) => {
6775
request.headers = {
76+
Authorization: `Bearer ${STRAPI_API_TOKEN}`,
6877
...request.headers,
6978
'Strapi-Response-Format': 'v4',
7079
};

models/Organization.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
import { observable } from 'mobx';
22
import { Base, Searchable, SearchableFilter, StrapiListModel } from 'mobx-strapi';
3-
import { changeMonth, formatDate, groupBy } from 'web-utility';
3+
import { changeMonth, countBy, formatDate, groupBy } from 'web-utility';
44

55
import { Organization } from '@open-source-bazaar/china-ngo-database';
66
import { strapiClient } from './Base';
77

88
export type OrganizationStatistic = Record<
9-
'coverageArea' | 'locale' | 'entityType',
9+
'coverageArea' | 'locale' | 'entityType' | 'serviceCategory',
1010
Record<string, number>
1111
>;
1212

13+
export const sortStatistic = (data: Record<string, number>, sortValue = true) =>
14+
Object.entries(data)
15+
.map(([key, count]) => [key, count] as const)
16+
.sort(([kX, vX], [kY, vY]) => (sortValue ? vY - vX : kY.localeCompare(kX)));
17+
1318
export class OrganizationModel extends Searchable<Organization & Base>(StrapiListModel) {
1419
baseURI = 'organizations';
1520
client = strapiClient;
1621

1722
searchKeys = ['name', 'description', 'coverageArea'] as const;
1823

24+
@observable
25+
accessor statistic = {} as OrganizationStatistic;
26+
1927
@observable
2028
accessor categoryMap: Record<string, Organization[]> = {};
2129

@@ -42,12 +50,36 @@ export class OrganizationModel extends Searchable<Organization & Base>(StrapiLis
4250
return { ...meta, filters: { ...meta.filters, establishedDate: timeRangeFilter } };
4351
}
4452

45-
async groupAllByTags() {
46-
const allData = await this.getAll();
53+
async getStatistic(filter?: SearchableFilter<Organization & Base>) {
54+
const list = await this.getAll(filter);
55+
56+
const statistic = Object.fromEntries(
57+
(['coverageArea', 'locale', 'entityType'] as (keyof Organization)[]).map(key => [
58+
key,
59+
countBy(list, ({ [key]: value }) => value?.toString() || 'unknown'),
60+
]),
61+
);
62+
const serviceCategory = countBy(
63+
list,
64+
({ services }) =>
65+
services
66+
?.map(({ serviceCategory }) => serviceCategory!)
67+
.flat()
68+
.filter(Boolean) || [],
69+
);
70+
return (this.statistic = { ...statistic, serviceCategory } as OrganizationStatistic);
71+
}
72+
73+
async groupAllByTags(filter?: SearchableFilter<Organization & Base>) {
74+
const allData = await this.getAll(filter);
4775

4876
return (this.categoryMap = groupBy(
4977
allData,
50-
({ services }) => services?.flatMap(({ serviceCategory }) => serviceCategory!) || [],
78+
({ services }) =>
79+
services
80+
?.map(({ serviceCategory }) => serviceCategory!)
81+
.flat()
82+
.filter(Boolean) || [],
5183
));
5284
}
5385
}

models/configuration.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const Name = process.env.NEXT_PUBLIC_SITE_NAME,
66
Summary = process.env.NEXT_PUBLIC_SITE_SUMMARY,
77
DefaultImage = process.env.NEXT_PUBLIC_LOGO!;
88

9-
export const { VERCEL, VERCEL_URL } = process.env;
9+
export const { VERCEL, VERCEL_URL, STRAPI_API_TOKEN } = process.env;
1010

1111
export const API_Host = isServer()
1212
? VERCEL_URL
@@ -16,6 +16,8 @@ export const API_Host = isServer()
1616

1717
export const CACHE_HOST = process.env.NEXT_PUBLIC_CACHE_HOST!;
1818

19+
export const STRAPI_API_HOST = process.env.NEXT_PUBLIC_STRAPI_API_HOST!;
20+
1921
export const LARK_API_HOST = `${API_Host}/api/Lark/`;
2022

2123
export const ProxyBaseURL = 'https://bazaar.fcc-cd.dev/proxy';

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"@mdx-js/react": "^3.1.1",
2020
"@next/mdx": "^15.5.3",
2121
"core-js": "^3.45.1",
22+
"echarts-jsx": "^0.5.4",
2223
"file-type": "^21.0.0",
2324
"idea-react": "^2.0.0-rc.13",
2425
"koa": "^3.0.1",

pages/ngo/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import { PageHead } from '../../components/Layout/PageHead';
77
import { CityStatisticMap } from '../../components/Map';
88
import { SearchBar } from '../../components/Navigator/SearchBar';
99
import OrganizationCharts from '../../components/Organization/Charts';
10+
1011
import { OrganizationModel, OrganizationStatistic } from '../../models/Organization';
1112
import { I18nContext } from '../../models/Translation';
1213

1314
export const getStaticProps = async () => {
14-
const props = await new OrganizationModel().countAll(['coverageArea', 'locale', 'entityType'], {
15-
establishedDate: '2008',
16-
});
15+
const props = await new OrganizationModel().getStatistic({ establishedDate: '2008' });
16+
1717
return { props, revalidate: Day / Second };
1818
};
1919

0 commit comments

Comments
 (0)