Skip to content

Commit 68bd527

Browse files
committed
[refactor] load NGO data by year
[add] Zodiac Bar component [optimize] update Upstream packages
1 parent e07c205 commit 68bd527

File tree

10 files changed

+242
-180
lines changed

10 files changed

+242
-180
lines changed

components/Base/ZodiacBar.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Link from 'next/link';
2+
import { FC, ReactNode } from 'react';
3+
4+
export const ZodiacSigns = ['🐵', '🐔', '🐶', '🐷', '🐭', '🐮', '🐯', '🐰', '🐲', '🐍', '🐴', '🐐'];
5+
6+
export interface ZodiacBarProps {
7+
startYear: number;
8+
endYear?: number;
9+
itemOf?: (year: number, zodiac: string) => { link?: string; title?: ReactNode };
10+
}
11+
12+
export const ZodiacBar: FC<ZodiacBarProps> = ({
13+
startYear,
14+
endYear = new Date().getFullYear(),
15+
itemOf,
16+
}) => (
17+
<ol className="list-inline d-flex flex-wrap justify-content-center gap-3">
18+
{Array.from({ length: endYear - startYear + 1 }, (_, index) => {
19+
const year = endYear - index;
20+
const zodiac = ZodiacSigns[year % 12];
21+
const { link = '#', title } = itemOf?.(year, zodiac) || {};
22+
23+
return (
24+
<li key={index} className="list-inline-item border rounded">
25+
<Link className="d-inline-block p-3 text-decoration-none text-center" href={link}>
26+
<div className="fs-1">{zodiac}</div>
27+
28+
{title}
29+
</Link>
30+
</li>
31+
);
32+
})}
33+
</ol>
34+
);

components/Organization/Landscape.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import systemStore from '../../models/System';
1111
import { OrganizationCard } from './Card';
1212
import styles from './LandScape.module.less';
1313

14-
export type OpenCollaborationLandscapeProps = Pick<OrganizationModel, 'categoryMap'>;
14+
export type OpenCollaborationLandscapeProps = Pick<OrganizationModel, 'typeMap'>;
1515

1616
@observer
1717
export class OpenCollaborationLandscape extends Component<OpenCollaborationLandscapeProps> {
1818
@observable
19-
accessor itemSize = 5;
19+
accessor itemSize = 2;
2020

2121
modal = new Dialog<{ name?: string }>(({ defer, name }) => (
2222
<Modal show={!!defer} onHide={() => defer?.resolve()}>
@@ -28,7 +28,7 @@ export class OpenCollaborationLandscape extends Component<OpenCollaborationLands
2828
));
2929

3030
renderCard(name: string) {
31-
const organization = Object.values(this.props.categoryMap)
31+
const organization = Object.values(this.props.typeMap)
3232
.flat()
3333
.find(({ name: n }) => n === name);
3434

@@ -45,7 +45,7 @@ export class OpenCollaborationLandscape extends Component<OpenCollaborationLands
4545
className={`border ${styles.listItem}`}
4646
onClick={() => this.modal.open({ name: name as string })}
4747
>
48-
<div style={{ fontSize: this.itemSize + 'rem' }}>
48+
<div className="text-nowrap" style={{ fontSize: this.itemSize + 'rem' }}>
4949
{name.slice(0, 2)}
5050
<br />
5151
{name.slice(2, 4)}
@@ -55,7 +55,7 @@ export class OpenCollaborationLandscape extends Component<OpenCollaborationLands
5555

5656
render() {
5757
const { screenNarrow } = systemStore;
58-
const rows = splitArray(Object.entries(this.props.categoryMap), 2);
58+
const rows = splitArray(Object.entries(this.props.typeMap), 2);
5959

6060
return (
6161
<>

models/Organization.ts

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { observable } from 'mobx';
2+
import { toggle } from 'mobx-restful';
23
import { Base, Searchable, SearchableFilter, StrapiListModel } from 'mobx-strapi';
3-
import { changeMonth, countBy, formatDate, groupBy } from 'web-utility';
4+
import { countBy, groupBy } from 'web-utility';
45

56
import { Organization } from '@open-source-bazaar/china-ngo-database';
67
import { strapiClient } from './Base';
@@ -19,35 +20,33 @@ export class OrganizationModel extends Searchable<Organization & Base>(StrapiLis
1920
baseURI = 'organizations';
2021
client = strapiClient;
2122

23+
sort = { establishedDate: 'asc' } as const;
24+
25+
dateKeys = ['establishedDate'] as const;
26+
2227
searchKeys = ['name', 'description', 'coverageArea'] as const;
2328

2429
@observable
2530
accessor statistic = {} as OrganizationStatistic;
2631

2732
@observable
28-
accessor categoryMap: Record<string, Organization[]> = {};
29-
30-
makeFilter(
31-
pageIndex: number,
32-
pageSize: number,
33-
{ keywords, ...filter }: SearchableFilter<Organization & Base>,
34-
) {
35-
if (keywords) return super.makeFilter(pageIndex, pageSize, { keywords, ...filter });
36-
37-
const meta = super.makeFilter(pageIndex, pageSize, filter);
38-
39-
const { establishedDate } = filter;
40-
41-
const timeRangeFilter =
42-
establishedDate?.length === 4
43-
? { $gte: `${establishedDate}-01-01`, $lt: `${+establishedDate + 1}-01-01` }
44-
: establishedDate?.length === 7
45-
? {
46-
$gte: `${establishedDate}-01`,
47-
$lte: `${formatDate(changeMonth(establishedDate, 1), 'YYYY-MM')}-01`,
48-
}
49-
: {};
50-
return { ...meta, filters: { ...meta.filters, establishedDate: timeRangeFilter } };
33+
accessor typeMap: Record<string, Organization[]> = {};
34+
35+
@toggle('downloading')
36+
async getYearRange() {
37+
const now = Date.now(),
38+
organizationStore = new OrganizationModel();
39+
40+
const [{ establishedDate: start } = {}] = await organizationStore.getList({}, 1, 1);
41+
42+
Object.assign(organizationStore, { sort: { establishedDate: 'desc' } });
43+
44+
const [{ establishedDate: end } = {}] = await organizationStore.getList({}, 1, 1);
45+
46+
const startYear = new Date(start || now).getFullYear(),
47+
endYear = new Date(end || now).getFullYear();
48+
49+
return [startYear, endYear] as const;
5150
}
5251

5352
async getStatistic(filter?: SearchableFilter<Organization & Base>) {
@@ -67,14 +66,10 @@ export class OrganizationModel extends Searchable<Organization & Base>(StrapiLis
6766
return (this.statistic = { ...statistic, serviceCategory } as OrganizationStatistic);
6867
}
6968

70-
async groupAllByTags(filter?: SearchableFilter<Organization & Base>) {
69+
async groupAllByType(filter?: SearchableFilter<Organization & Base>) {
7170
const allData = await this.getAll(filter);
7271

73-
return (this.categoryMap = groupBy(
74-
allData,
75-
({ services }) =>
76-
services?.map(({ serviceCategory }) => serviceCategory!).filter(Boolean) || [],
77-
));
72+
return (this.typeMap = groupBy(allData, 'entityType'));
7873
}
7974
}
8075
export default new OrganizationModel();

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@
3333
"mobx-lark": "^2.4.2",
3434
"mobx-react": "^9.2.0",
3535
"mobx-react-helper": "^0.5.1",
36-
"mobx-restful": "^2.1.0",
36+
"mobx-restful": "^2.1.1",
3737
"mobx-restful-table": "^2.5.4",
38-
"mobx-strapi": "^0.7.2",
38+
"mobx-strapi": "^0.7.4",
3939
"next": "^15.5.3",
4040
"next-pwa": "^5.6.0",
4141
"next-ssr-middleware": "^1.0.3",
@@ -47,7 +47,7 @@
4747
"remark-frontmatter": "^5.0.0",
4848
"remark-mdx-frontmatter": "^5.2.0",
4949
"undici": "^7.16.0",
50-
"web-utility": "^4.6.0",
50+
"web-utility": "^4.6.1",
5151
"yaml": "^2.8.1"
5252
},
5353
"devDependencies": {
@@ -63,7 +63,7 @@
6363
"@types/eslint-config-prettier": "^6.11.3",
6464
"@types/koa": "^3.0.0",
6565
"@types/next-pwa": "^5.6.9",
66-
"@types/node": "^22.18.4",
66+
"@types/node": "^22.18.5",
6767
"@types/react": "^19.1.13",
6868
"@types/react-dom": "^19.1.9",
6969
"eslint": "^9.35.0",

pages/ngo/[year]/index.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { observer } from 'mobx-react';
2+
import { cache, compose, errorLogger } from 'next-ssr-middleware';
3+
import { FC, useContext } from 'react';
4+
import { Button, Container } from 'react-bootstrap';
5+
6+
import { PageHead } from '../../../components/Layout/PageHead';
7+
import { CityStatisticMap } from '../../../components/Map/CityStatisticMap';
8+
import { SearchBar } from '../../../components/Navigator/SearchBar';
9+
import OrganizationCharts from '../../../components/Organization/Charts';
10+
import { OrganizationModel, OrganizationStatistic } from '../../../models/Organization';
11+
import { I18nContext } from '../../../models/Translation';
12+
13+
interface OrganizationPageProps {
14+
year: string;
15+
statistic: OrganizationStatistic;
16+
}
17+
18+
export const getServerSideProps = compose<{ year: string }, OrganizationPageProps>(
19+
cache(),
20+
errorLogger,
21+
async ({ params }) => {
22+
const statistic = await new OrganizationModel().getStatistic({ establishedDate: params!.year });
23+
24+
return { props: { year: params!.year, statistic } };
25+
},
26+
);
27+
28+
const OrganizationPage: FC<OrganizationPageProps> = observer(({ year, statistic }) => {
29+
const { t } = useContext(I18nContext);
30+
31+
return (
32+
<Container>
33+
<PageHead title={t('China_NGO_Map')} />
34+
35+
<header className="d-flex flex-wrap justify-content-around align-items-center my-4">
36+
<h1 className="my-4">{t('China_NGO_Map')}</h1>
37+
<div>
38+
<Button className="me-2" size="sm" href={`/NGO/${year}/landscape`}>
39+
{t('landscape')}
40+
</Button>
41+
</div>
42+
43+
<SearchBar action="/search/NGO" />
44+
</header>
45+
46+
<CityStatisticMap data={statistic.coverageArea} />
47+
48+
<OrganizationCharts {...statistic} />
49+
</Container>
50+
);
51+
});
52+
export default OrganizationPage;

pages/ngo/[year]/landscape.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { observer } from 'mobx-react';
2+
import { cache, compose, errorLogger } from 'next-ssr-middleware';
3+
import { FC, useContext } from 'react';
4+
import { Container } from 'react-bootstrap';
5+
6+
import { PageHead } from '../../../components/Layout/PageHead';
7+
import {
8+
OpenCollaborationLandscape,
9+
OpenCollaborationLandscapeProps,
10+
} from '../../../components/Organization/Landscape';
11+
import { OrganizationModel } from '../../../models/Organization';
12+
import { I18nContext } from '../../../models/Translation';
13+
14+
export const getServerSideProps = compose<{ year: string }, Pick<OrganizationModel, 'typeMap'>>(
15+
cache(),
16+
errorLogger,
17+
async ({ params }) => {
18+
const typeMap = await new OrganizationModel().groupAllByType({
19+
establishedDate: params!.year,
20+
});
21+
22+
return { props: JSON.parse(JSON.stringify({ typeMap })) };
23+
},
24+
);
25+
26+
const LandscapePage: FC<OpenCollaborationLandscapeProps> = observer(props => {
27+
const { t } = useContext(I18nContext);
28+
29+
return (
30+
<Container className="mb-5">
31+
<PageHead title={t('China_NGO_Landscape')} />
32+
33+
<h1 className="my-5 text-center">{t('China_NGO_Landscape')}</h1>
34+
35+
<OpenCollaborationLandscape {...props} />
36+
</Container>
37+
);
38+
});
39+
export default LandscapePage;

pages/ngo/index.tsx

Lines changed: 10 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,16 @@
1-
import { observer } from 'mobx-react';
2-
import { FC, useContext } from 'react';
3-
import { Button, Container } from 'react-bootstrap';
4-
import { Day, Second } from 'web-utility';
1+
import { InferGetStaticPropsType } from 'next';
52

6-
import { PageHead } from '../../components/Layout/PageHead';
7-
import { CityStatisticMap } from '../../components/Map/CityStatisticMap';
8-
import { SearchBar } from '../../components/Navigator/SearchBar';
9-
import OrganizationCharts from '../../components/Organization/Charts';
10-
import { OrganizationModel, OrganizationStatistic } from '../../models/Organization';
11-
import { I18nContext } from '../../models/Translation';
3+
import { ZodiacBar } from '../../components/Base/ZodiacBar';
4+
import { OrganizationModel } from '../../models/Organization';
125

136
export const getStaticProps = async () => {
14-
const props = await new OrganizationModel().getStatistic({ establishedDate: '2008' });
7+
const [startYear, endYear] = await new OrganizationModel().getYearRange();
158

16-
return { props, revalidate: Day / Second };
9+
return { props: { startYear, endYear } };
1710
};
1811

19-
const OrganizationPage: FC<OrganizationStatistic> = observer(props => {
20-
const { t } = useContext(I18nContext);
21-
22-
return (
23-
<Container>
24-
<PageHead title={t('China_NGO_Map')} />
25-
26-
<header className="d-flex flex-wrap justify-content-around align-items-center my-4">
27-
<h1 className="my-4">{t('China_NGO_Map')}</h1>
28-
<div>
29-
<Button className="me-2" size="sm" href="/NGO/landscape">
30-
{t('landscape')}
31-
</Button>
32-
</div>
33-
34-
<SearchBar action="/search/NGO" />
35-
</header>
36-
37-
<CityStatisticMap data={props.coverageArea} />
38-
39-
<OrganizationCharts {...props} />
40-
</Container>
41-
);
42-
});
43-
export default OrganizationPage;
12+
export default function OrganizationHomePage(
13+
props: InferGetStaticPropsType<typeof getStaticProps>,
14+
) {
15+
return <ZodiacBar {...props} itemOf={year => ({ title: year, link: `/NGO/${year}` })} />;
16+
}

pages/ngo/landscape.tsx

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

pages/search/[model]/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { buildURLData } from 'web-utility';
99
import { CardPage, CardPageProps } from '../../../components/Layout/CardPage';
1010
import { PageHead } from '../../../components/Layout/PageHead';
1111
import { SearchBar } from '../../../components/Navigator/SearchBar';
12+
import { OrganizationCard } from '../../../components/Organization/Card';
1213
import systemStore, { SearchPageMeta } from '../../../models/System';
1314
import { i18n, I18nContext } from '../../../models/Translation';
1415

@@ -42,7 +43,7 @@ const SearchNameMap = ({ t }: typeof i18n): Record<string, string> => ({
4243
});
4344

4445
const SearchCardMap: Record<string, CardPageProps['Card']> = {
45-
NGO: ({ name }) => <div>{name}</div>,
46+
NGO: OrganizationCard,
4647
};
4748

4849
const SearchModelPage: FC<SearchModelPageProps> = observer(

0 commit comments

Comments
 (0)