Skip to content

Commit ca30930

Browse files
CopilotTechQuery
andcommitted
Add China Public Interest Map foundation with mobx-strapi integration
Co-authored-by: TechQuery <19969570+TechQuery@users.noreply.github.com>
1 parent 234b008 commit ca30930

13 files changed

Lines changed: 481 additions & 63 deletions

File tree

.npmrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
auto-install-peers = false
1+
auto-install-peers = false
2+
@open-source-bazaar:registry=https://npm.pkg.github.com

components/LarkImage.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import { TableCellValue } from 'mobx-lark';
21
import { FC } from 'react';
32
import { Image, ImageProps } from 'react-bootstrap';
43

5-
import { fileURLOf } from '../models/Base';
64
import { DefaultImage } from '../models/configuration';
75

8-
export interface LarkImageProps extends Omit<ImageProps, 'src'> {
9-
src?: TableCellValue;
6+
export interface SimpleImageProps extends ImageProps {
7+
src?: string;
108
}
119

12-
export const LarkImage: FC<LarkImageProps> = ({
10+
export const LarkImage: FC<SimpleImageProps> = ({
1311
src = DefaultImage,
1412
alt,
1513
...props
@@ -18,18 +16,11 @@ export const LarkImage: FC<LarkImageProps> = ({
1816
fluid
1917
loading="lazy"
2018
{...props}
21-
src={fileURLOf(src, true)}
19+
src={src || DefaultImage}
2220
alt={alt}
2321
onError={({ currentTarget: image }) => {
24-
const path = fileURLOf(src),
25-
errorURL = decodeURI(image.src);
26-
27-
if (!path) return;
28-
29-
if (errorURL.endsWith(path)) {
30-
if (!alt) image.src = DefaultImage;
31-
} else if (!errorURL.endsWith(DefaultImage)) {
32-
image.src = path;
22+
if (image.src !== DefaultImage) {
23+
image.src = DefaultImage;
3324
}
3425
}}
3526
/>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { observer } from 'mobx-react';
2+
import { FC, useContext } from 'react';
3+
import { Badge,Card, Col, Row } from 'react-bootstrap';
4+
5+
import { Organization } from '../../models/Organization';
6+
import { I18nContext } from '../../models/Translation';
7+
8+
export interface ChinaPublicInterestLandscapeProps {
9+
tagMap: Record<string, Organization[]>;
10+
}
11+
12+
export const ChinaPublicInterestLandscape: FC<ChinaPublicInterestLandscapeProps> = observer(
13+
({ tagMap }) => {
14+
const { t } = useContext(I18nContext);
15+
16+
const tagEntries = Object.entries(tagMap).sort(([, a], [, b]) => b.length - a.length);
17+
18+
return (
19+
<div>
20+
{tagEntries.length === 0 && (
21+
<Card>
22+
<Card.Body className="text-center py-5">
23+
<Card.Title>{t('no_data_available')}</Card.Title>
24+
<Card.Text>{t('landscape_data_loading_message')}</Card.Text>
25+
</Card.Body>
26+
</Card>
27+
)}
28+
29+
{tagEntries.map(([tag, organizations]) => (
30+
<Card key={tag} className="mb-4">
31+
<Card.Header className="d-flex justify-content-between align-items-center">
32+
<h5 className="mb-0">{tag}</h5>
33+
<Badge bg="primary">{organizations.length} {t('organizations')}</Badge>
34+
</Card.Header>
35+
<Card.Body>
36+
<Row className="g-3">
37+
{organizations.map(org => (
38+
<Col key={org.id} md={6} lg={4}>
39+
<Card className="h-100">
40+
<Card.Body>
41+
<Card.Title className="h6">{org.name}</Card.Title>
42+
{org.description && (
43+
<Card.Text className="small text-muted">
44+
{org.description.length > 100
45+
? `${org.description.substring(0, 100)}...`
46+
: org.description}
47+
</Card.Text>
48+
)}
49+
<div className="mt-2">
50+
{org.city && (
51+
<Badge bg="secondary" className="me-1">
52+
{org.city}
53+
</Badge>
54+
)}
55+
{org.type && (
56+
<Badge bg="info" className="me-1">
57+
{org.type}
58+
</Badge>
59+
)}
60+
</div>
61+
{org.website && (
62+
<Card.Link
63+
href={org.website}
64+
target="_blank"
65+
className="small"
66+
>
67+
{t('visit_website')}
68+
</Card.Link>
69+
)}
70+
</Card.Body>
71+
</Card>
72+
</Col>
73+
))}
74+
</Row>
75+
</Card.Body>
76+
</Card>
77+
))}
78+
</div>
79+
);
80+
},
81+
);
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { observer } from 'mobx-react';
2+
import { FC, useContext } from 'react';
3+
import { Badge,Card, Col, Row } from 'react-bootstrap';
4+
5+
import { OrganizationModel, OrganizationStatistic } from '../../models/Organization';
6+
import { I18nContext } from '../../models/Translation';
7+
8+
export interface ChinaPublicInterestMapProps extends OrganizationStatistic {
9+
store: OrganizationModel;
10+
}
11+
12+
export const ChinaPublicInterestMap: FC<ChinaPublicInterestMapProps> = observer(
13+
({ store, year, city, type, tag }) => {
14+
const { t } = useContext(I18nContext);
15+
16+
return (
17+
<div>
18+
<Row className="g-4">
19+
<Col md={6} lg={3}>
20+
<Card>
21+
<Card.Body>
22+
<Card.Title>{t('by_year')}</Card.Title>
23+
<div>
24+
{year.slice(0, 5).map(item => (
25+
<Badge key={item.label} bg="primary" className="me-2 mb-2">
26+
{item.label}: {item.count}
27+
</Badge>
28+
))}
29+
</div>
30+
</Card.Body>
31+
</Card>
32+
</Col>
33+
34+
<Col md={6} lg={3}>
35+
<Card>
36+
<Card.Body>
37+
<Card.Title>{t('by_city')}</Card.Title>
38+
<div>
39+
{city.slice(0, 5).map(item => (
40+
<Badge key={item.label} bg="success" className="me-2 mb-2">
41+
{item.label}: {item.count}
42+
</Badge>
43+
))}
44+
</div>
45+
</Card.Body>
46+
</Card>
47+
</Col>
48+
49+
<Col md={6} lg={3}>
50+
<Card>
51+
<Card.Body>
52+
<Card.Title>{t('by_type')}</Card.Title>
53+
<div>
54+
{type.slice(0, 5).map(item => (
55+
<Badge key={item.label} bg="info" className="me-2 mb-2">
56+
{item.label}: {item.count}
57+
</Badge>
58+
))}
59+
</div>
60+
</Card.Body>
61+
</Card>
62+
</Col>
63+
64+
<Col md={6} lg={3}>
65+
<Card>
66+
<Card.Body>
67+
<Card.Title>{t('by_tag')}</Card.Title>
68+
<div>
69+
{tag.slice(0, 5).map(item => (
70+
<Badge key={item.label} bg="warning" className="me-2 mb-2">
71+
{item.label}: {item.count}
72+
</Badge>
73+
))}
74+
</div>
75+
</Card.Body>
76+
</Card>
77+
</Col>
78+
</Row>
79+
80+
<Card className="mt-4">
81+
<Card.Body>
82+
<Card.Title>{t('about_china_public_interest_map')}</Card.Title>
83+
<Card.Text>
84+
{t('china_public_interest_map_description')}
85+
</Card.Text>
86+
</Card.Body>
87+
</Card>
88+
</div>
89+
);
90+
},
91+
);

models/Base.ts

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import 'core-js/full/array/from-async';
22

33
import { HTTPClient } from 'koajax';
44
import { githubClient } from 'mobx-github';
5-
import { TableCellAttachment, TableCellMedia, TableCellValue } from 'mobx-lark';
65
import { DataObject } from 'mobx-restful';
76
import { isEmpty } from 'web-utility';
87

@@ -11,7 +10,6 @@ import {
1110
GithubToken,
1211
isServer,
1312
ProxyBaseURL,
14-
LARK_API_HOST,
1513
} from './configuration';
1614

1715
export const ownClient = new HTTPClient({
@@ -49,19 +47,8 @@ export const makeGithubSearchCondition = (queryMap: DataObject) =>
4947
.map(([key, value]) => `${key}:${value}`)
5048
.join(' ');
5149

52-
export const larkClient = new HTTPClient({
53-
baseURI: LARK_API_HOST,
50+
// Strapi client for China NGO Database
51+
export const strapiClient = new HTTPClient({
52+
baseURI: 'https://china-ngo-db.onrender.com/api/',
5453
responseType: 'json',
5554
});
56-
57-
export function fileURLOf(field: TableCellValue, cache = false) {
58-
if (!(field instanceof Array) || !field[0]) return field + '';
59-
60-
const file = field[0] as TableCellMedia | TableCellAttachment;
61-
62-
let URI = `/api/Lark/file/${'file_token' in file ? file.file_token : file.attachmentToken}/${file.name}`;
63-
64-
if (cache) URI += '?cache=1';
65-
66-
return URI;
67-
}

models/Organization.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { observable } from 'mobx';
2+
import { HTTPClient } from 'koajax';
3+
import { StrapiListModel, Base } from 'mobx-strapi';
4+
5+
// Define the organization data structure similar to China NGO database
6+
export interface Organization extends Base {
7+
name: string;
8+
description?: string;
9+
type?: string;
10+
city?: string;
11+
province?: string;
12+
tags?: string[];
13+
website?: string;
14+
logo?: {
15+
data?: {
16+
attributes: {
17+
url: string;
18+
};
19+
};
20+
};
21+
year?: number;
22+
}
23+
24+
export interface OrganizationStatistic {
25+
year: Array<{ label: string; count: number }>;
26+
city: Array<{ label: string; count: number }>;
27+
type: Array<{ label: string; count: number }>;
28+
tag: Array<{ label: string; count: number }>;
29+
}
30+
31+
// Strapi client configuration
32+
const strapiClient = new HTTPClient({
33+
baseURI: 'https://china-ngo-db.onrender.com/api/',
34+
responseType: 'json',
35+
});
36+
37+
export class OrganizationModel extends StrapiListModel<Organization> {
38+
baseURI = '/organizations';
39+
40+
constructor() {
41+
super();
42+
this.client = strapiClient;
43+
}
44+
45+
@observable
46+
accessor tagMap: Record<string, Organization[]> = {};
47+
48+
async groupAllByTags(): Promise<Record<string, Organization[]>> {
49+
try {
50+
const allData = await this.getAll();
51+
const tagMap: Record<string, Organization[]> = {};
52+
53+
for (const org of allData) {
54+
const tags = org.tags || [];
55+
for (const tag of tags) {
56+
if (!tagMap[tag]) {
57+
tagMap[tag] = [];
58+
}
59+
tagMap[tag].push(org);
60+
}
61+
}
62+
63+
this.tagMap = tagMap;
64+
return tagMap;
65+
} catch (error) {
66+
console.error('Failed to fetch organizations:', error);
67+
return {};
68+
}
69+
}
70+
}
71+
72+
export class OrganizationStatisticModel {
73+
private client: HTTPClient<any>;
74+
private collection: string;
75+
76+
constructor(baseId: string, collectionId: string) {
77+
this.client = new HTTPClient({
78+
baseURI: 'https://china-ngo-db.onrender.com/api/',
79+
responseType: 'json',
80+
});
81+
this.collection = collectionId;
82+
}
83+
84+
async countAll(): Promise<Array<{ label: string; count: number }>> {
85+
try {
86+
// This would need to be adapted based on the actual Strapi API structure
87+
const response = await this.client.get(`${this.collection}`);
88+
return response.body?.data || [];
89+
} catch (error) {
90+
console.error(`Failed to fetch statistics for ${this.collection}:`, error);
91+
return [];
92+
}
93+
}
94+
}
95+
96+
// Mock constants for now - these would be configured based on the actual Strapi setup
97+
export const COMMUNITY_BASE_ID = 'community';
98+
export const OSC_YEAR_STATISTIC_TABLE_ID = 'organization-year-stats';
99+
export const OSC_CITY_STATISTIC_TABLE_ID = 'organization-city-stats';
100+
export const OSC_TYPE_STATISTIC_TABLE_ID = 'organization-type-stats';
101+
export const OSC_TAG_STATISTIC_TABLE_ID = 'organization-tag-stats';

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"mobx": "^6.13.7",
2929
"mobx-github": "^0.5.1",
3030
"mobx-i18n": "^0.7.1",
31-
"mobx-lark": "^2.4.1",
31+
"mobx-strapi": "^0.7.0",
3232
"mobx-react": "^9.2.0",
3333
"mobx-react-helper": "^0.5.1",
3434
"mobx-restful": "^2.1.0",

0 commit comments

Comments
 (0)