Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 3 additions & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
auto-install-peers = false
auto-install-peers = false
//npm.pkg.github.com/:_authToken=${GH_PAT}
@open-source-bazaar:registry=https://npm.pkg.github.com
21 changes: 6 additions & 15 deletions components/LarkImage.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { TableCellValue } from 'mobx-lark';
import { FC } from 'react';
import { Image, ImageProps } from 'react-bootstrap';

import { fileURLOf } from '../models/Base';
import { DefaultImage } from '../models/configuration';

export interface LarkImageProps extends Omit<ImageProps, 'src'> {
src?: TableCellValue;
export interface SimpleImageProps extends ImageProps {
src?: string;
}

export const LarkImage: FC<LarkImageProps> = ({
export const LarkImage: FC<SimpleImageProps> = ({
src = DefaultImage,
alt,
...props
Expand All @@ -18,18 +16,11 @@ export const LarkImage: FC<LarkImageProps> = ({
fluid
loading="lazy"
{...props}
src={fileURLOf(src, true)}
src={src || DefaultImage}
alt={alt}
onError={({ currentTarget: image }) => {
const path = fileURLOf(src),
errorURL = decodeURI(image.src);

if (!path) return;

if (errorURL.endsWith(path)) {
if (!alt) image.src = DefaultImage;
} else if (!errorURL.endsWith(DefaultImage)) {
image.src = path;
if (image.src !== DefaultImage) {
image.src = DefaultImage;
}
}}
/>
Expand Down
67 changes: 67 additions & 0 deletions components/Map/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { FC, useEffect, useRef } from 'react';
import { Card, Col, Container, Row, Spinner } from 'react-bootstrap';

export interface MapProps {
data?: { name: string; city?: string; province?: string; latitude?: number; longitude?: number }[];
loading?: boolean;
}

export const Map: FC<MapProps> = ({ data = [], loading = false }) => {
const mapRef = useRef<HTMLDivElement>(null);

useEffect(() => {
// Placeholder for actual map implementation
// This would integrate with a mapping library like Leaflet, AMap, or MapBox
}, []);

if (loading) {
return (
<Container className="d-flex justify-content-center align-items-center" style={{ minHeight: '400px' }}>
<Spinner animation="border" role="status">
<span className="visually-hidden">Loading...</span>
</Spinner>
</Container>
);
}

return (
<Container>
<Row>
<Col>
<Card>
<Card.Body>
<div
ref={mapRef}
style={{
width: '100%',
height: '500px',
backgroundColor: '#f8f9fa',
border: '1px solid #dee2e6',
borderRadius: '0.375rem',
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div className="text-center text-muted">
<h5>Interactive Map Coming Soon</h5>
<p>Geographic visualization of {data.length} organizations</p>
{data.length > 0 && (
<small>
Data includes organizations from{' '}
{[...new Set(data.map(item => item.province || item.city).filter(Boolean))].length}{' '}
locations
</small>
)}
</div>
</div>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
);
};

export default Map;
11 changes: 11 additions & 0 deletions components/Navigator/MainNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
{ href: '/policy', title: t('policy') },
],
},
{
title: t('ngo'),
subs: [
{ href: '/ngo', title: t('china_public_interest_map') },
{ href: '/ngo/landscape', title: t('china_public_interest_landscape') },
{
href: 'https://open-source-bazaar.feishu.cn/wiki/VGrMwiweVivWrHkTcvpcJTjjnoY',
title: t('open_source_public_interest_plan')
},
],
},
];

export interface MainNavigatorProps {
Expand Down
117 changes: 117 additions & 0 deletions components/Organization/ChinaPublicInterestLandscape.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { observer } from 'mobx-react';
import { FC, useContext, useEffect, useState } from 'react';
import { Badge, Card, Col, Container, Row } from 'react-bootstrap';

import { Organization, OrganizationModel } from '../../models/Organization';
import { I18nContext } from '../../models/Translation';

const organizationModel = new OrganizationModel();

export const ChinaPublicInterestLandscape: FC = observer(() => {
const { t } = useContext(I18nContext);
const [tagMap, setTagMap] = useState<Record<string, Organization[]>>({});
const [loading, setLoading] = useState(false);

useEffect(() => {
const loadData = async () => {
setLoading(true);
try {
const groupedData = await organizationModel.groupAllByTags();
setTagMap(groupedData);
} finally {
setLoading(false);
}
};

loadData();
}, []);

const tagEntries = Object.entries(tagMap).sort(([, a], [, b]) => b.length - a.length);

return (
<Container className="py-4">
<Row className="mb-4">
<Col>
<h1>{t('china_public_interest_landscape')}</h1>
</Col>
</Row>

{loading && (
<Row>
<Col className="text-center">
<div className="spinner-border" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</Col>
</Row>
)}

{!loading && tagEntries.length === 0 && (
<Row>
<Col>
<Card>
<Card.Body className="text-center py-5">
<Card.Title>{t('no_data_available')}</Card.Title>
<Card.Text>{t('landscape_data_loading_message')}</Card.Text>
</Card.Body>
</Card>
</Col>
</Row>
)}

{!loading && tagEntries.map(([tag, organizations]) => (
<Row key={tag} className="mb-4">
<Col>
<Card>
<Card.Header className="d-flex justify-content-between align-items-center">
<h5 className="mb-0">{tag}</h5>
<Badge bg="primary">{organizations.length} {t('organizations')}</Badge>
</Card.Header>
<Card.Body>
<Row className="g-3">
{organizations.map(org => (
<Col key={org.id} md={6} lg={4}>
<Card className="h-100">
<Card.Body>
<Card.Title className="h6">{org.name}</Card.Title>
{org.description && (
<Card.Text className="small text-muted">
{org.description.length > 100
? `${org.description.substring(0, 100)}...`
: org.description}
</Card.Text>
)}
<div className="mt-2">
{org.city && (
<Badge bg="secondary" className="me-1">
{org.city}
</Badge>
)}
{org.type && (
<Badge bg="info" className="me-1">
{org.type}
</Badge>
)}
</div>
{org.website && (
<Card.Link
href={org.website}
target="_blank"
className="small"
>
{t('visit_website')}
</Card.Link>
)}
</Card.Body>
</Card>
</Col>
))}
</Row>
</Card.Body>
</Card>
</Col>
</Row>
))}
</Container>
);
});
129 changes: 129 additions & 0 deletions components/Organization/ChinaPublicInterestMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { observer } from 'mobx-react';
import { FC, useContext, useEffect, useState } from 'react';
import { Card, Col, Container, Nav, Row } from 'react-bootstrap';

import { Organization, OrganizationModel } from '../../models/Organization';
import { I18nContext } from '../../models/Translation';
import { Map } from '../Map';

const organizationModel = new OrganizationModel();

export const ChinaPublicInterestMap: FC = observer(() => {
const { t } = useContext(I18nContext);
const [loading, setLoading] = useState(false);
const [organizations, setOrganizations] = useState<Organization[]>([]);

useEffect(() => {
const loadData = async () => {
setLoading(true);
try {
const orgs = await organizationModel.getAll();
setOrganizations(orgs);
} catch (error) {
console.error('Failed to load organizations:', error);
} finally {
setLoading(false);
}
};

loadData();
}, []);

return (
<Container className="py-4">
<Row className="mb-4">
<Col>
<h1>{t('china_public_interest_map')}</h1>
<Nav className="mb-3">
<Nav.Link href="/ngo/landscape">{t('landscape')}</Nav.Link>
<Nav.Link href="/ngo">{t('join_the_public_interest_map')}</Nav.Link>
</Nav>
</Col>
</Row>

<Row className="mb-4">
<Col>
<Map data={organizations} loading={loading} />
</Col>
</Row>

<Row>
<Col md={3}>
<Card>
<Card.Header>{t('by_year')}</Card.Header>
<Card.Body>
{loading ? (
<div className="text-center">
<div className="spinner-border spinner-border-sm" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : (
<p className="text-muted">{t('no_data_available')}</p>
)}
</Card.Body>
</Card>
</Col>
<Col md={3}>
<Card>
<Card.Header>{t('by_city')}</Card.Header>
<Card.Body>
{loading ? (
<div className="text-center">
<div className="spinner-border spinner-border-sm" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : (
<p className="text-muted">{t('no_data_available')}</p>
)}
</Card.Body>
</Card>
</Col>
<Col md={3}>
<Card>
<Card.Header>{t('by_type')}</Card.Header>
<Card.Body>
{loading ? (
<div className="text-center">
<div className="spinner-border spinner-border-sm" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : (
<p className="text-muted">{t('no_data_available')}</p>
)}
</Card.Body>
</Card>
</Col>
<Col md={3}>
<Card>
<Card.Header>{t('by_tag')}</Card.Header>
<Card.Body>
{loading ? (
<div className="text-center">
<div className="spinner-border spinner-border-sm" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
) : (
<p className="text-muted">{t('no_data_available')}</p>
)}
</Card.Body>
</Card>
</Col>
</Row>

<Row className="mt-4">
<Col>
<Card>
<Card.Body>
<h5>{t('about_china_public_interest_map')}</h5>
<p>{t('china_public_interest_map_description')}</p>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
);
});
6 changes: 6 additions & 0 deletions models/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,9 @@ export function fileURLOf(field: TableCellValue, cache = false) {

return URI;
}

// Strapi client for China NGO Database
export const strapiClient = new HTTPClient({
baseURI: 'https://china-ngo-db.onrender.com/api/',
responseType: 'json',
});
Loading