diff --git a/.env b/.env index 3baba80..dc5c88f 100644 --- a/.env +++ b/.env @@ -5,3 +5,5 @@ NEXT_PUBLIC_LOGO = https://github.com/Open-Source-Bazaar.png NEXT_PUBLIC_LARK_API_HOST = https://open.feishu.cn/open-apis/ NEXT_PUBLIC_LARK_APP_ID = cli_a8094a652022900d NEXT_PUBLIC_LARK_WIKI_URL = https://open-source-bazaar.feishu.cn/wiki/space/7052192153363054596 + +NEXT_PUBLIC_STRAPI_API_HOST = https://china-ngo-db.onrender.com/api/ diff --git a/.gitignore b/.gitignore index 3abe835..d663663 100644 --- a/.gitignore +++ b/.gitignore @@ -26,10 +26,7 @@ yarn-debug.log* yarn-error.log* # local env files -.env.local -.env.development.local -.env.test.local -.env.production.local +.env*local # vercel .vercel diff --git a/.husky/pre-commit b/.husky/pre-commit index 255814b..18de984 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1 +1 @@ -npm test +npm test \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push index da7be55..10da9ff 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1 +1 @@ -npm run build +npm run build \ No newline at end of file diff --git a/.npmrc b/.npmrc index f048ded..beaab3f 100644 --- a/.npmrc +++ b/.npmrc @@ -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 diff --git a/components/Base/ZodiacBar.tsx b/components/Base/ZodiacBar.tsx new file mode 100644 index 0000000..b54dd49 --- /dev/null +++ b/components/Base/ZodiacBar.tsx @@ -0,0 +1,34 @@ +import Link from 'next/link'; +import { FC, ReactNode } from 'react'; + +export const ZodiacSigns = ['🐵', '🐔', '🐶', '🐷', '🐭', '🐮', '🐯', '🐰', '🐲', '🐍', '🐴', '🐐']; + +export interface ZodiacBarProps { + startYear: number; + endYear?: number; + itemOf?: (year: number, zodiac: string) => { link?: string; title?: ReactNode }; +} + +export const ZodiacBar: FC = ({ + startYear, + endYear = new Date().getFullYear(), + itemOf, +}) => ( +
    + {Array.from({ length: endYear - startYear + 1 }, (_, index) => { + const year = endYear - index; + const zodiac = ZodiacSigns[year % 12]; + const { link = '#', title } = itemOf?.(year, zodiac) || {}; + + return ( +
  1. + +
    {zodiac}
    + + {title} + +
  2. + ); + })} +
+); diff --git a/components/LarkImage.tsx b/components/LarkImage.tsx index e0cfbac..3a47ee7 100644 --- a/components/LarkImage.tsx +++ b/components/LarkImage.tsx @@ -9,11 +9,7 @@ export interface LarkImageProps extends Omit { src?: TableCellValue; } -export const LarkImage: FC = ({ - src = DefaultImage, - alt, - ...props -}) => ( +export const LarkImage: FC = ({ src = DefaultImage, alt, ...props }) => ( = props => ( + } + /> +); +export default ChinaMap; \ No newline at end of file diff --git a/components/Map/CityStatisticMap.tsx b/components/Map/CityStatisticMap.tsx new file mode 100644 index 0000000..af47a3e --- /dev/null +++ b/components/Map/CityStatisticMap.tsx @@ -0,0 +1,63 @@ +import { computed } from 'mobx'; +import { observer } from 'mobx-react'; +import { ObservedComponent } from 'mobx-react-helper'; +import dynamic from 'next/dynamic'; +import { MarkerMeta, OpenReactMapProps } from 'open-react-map'; + +import { OrganizationStatistic } from '../../models/Organization'; +import systemStore from '../../models/System'; + +const ChinaMap = dynamic(() => import('./ChinaMap'), { ssr: false }); + +export interface CityStatisticMapProps { + data: OrganizationStatistic['coverageArea']; + onChange?: (city: string) => any; +} + +@observer +export class CityStatisticMap extends ObservedComponent { + componentDidMount() { + systemStore.getCityCoordinate(); + } + + @computed + get markers() { + const { cityCoordinate } = systemStore, + { data } = this.observedProps; + + return Object.entries(data) + .map(([city, count]) => { + const point = cityCoordinate[city]; + + if (point) + return { + tooltip: `${city} ${count}`, + position: [point[1], point[0]], + }; + }) + .filter(Boolean) as MarkerMeta[]; + } + + handleChange: OpenReactMapProps['onMarkerClick'] = ({ latlng: { lat, lng } }) => { + const { markers } = this; + const { tooltip } = + markers.find(({ position: p }) => p instanceof Array && lat === p[0] && lng === p[1]) || {}; + const [city] = tooltip?.split(/\s+/) || []; + + this.props.onChange?.(city); + }; + + render() { + const { markers } = this; + + return ( + + ); + } +} diff --git a/components/Navigator/MainNavigator.tsx b/components/Navigator/MainNavigator.tsx index 8aaeba4..58b8d41 100644 --- a/components/Navigator/MainNavigator.tsx +++ b/components/Navigator/MainNavigator.tsx @@ -45,6 +45,16 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [ { href: '/license-filter', title: t('license_filter') }, ], }, + { + title: t('NGO'), + subs: [ + { href: '/NGO', title: t('China_NGO_DB') }, + { + href: 'https://open-source-bazaar.feishu.cn/wiki/VGrMwiweVivWrHkTcvpcJTjjnoY', + title: t('Open_Source_NGO_plan'), + }, + ], + }, { title: t('wiki'), subs: [ diff --git a/components/Organization/Card.tsx b/components/Organization/Card.tsx new file mode 100644 index 0000000..1009612 --- /dev/null +++ b/components/Organization/Card.tsx @@ -0,0 +1,110 @@ +import { Organization } from '@open-source-bazaar/china-ngo-database'; +import { Icon } from 'idea-react'; +import { observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { ObservedComponent } from 'mobx-react-helper'; +import { BadgeBar } from 'mobx-restful-table'; +import { HTMLAttributes } from 'react'; +import { Button, Card, CardProps, Image } from 'react-bootstrap'; + +import { i18n, I18nContext } from '../../models/Translation'; + +export interface OrganizationCardProps + extends Pick, 'className' | 'style'>, + Omit, + CardProps { + onSwitch?: (filter: Partial>) => any; +} + +@observer +export class OrganizationCard extends ObservedComponent { + static contextType = I18nContext; + + @observable + accessor showQRC = false; + + renderIcon() { + const { website, wechatPublic } = this.observedProps.internetContact || {}; + + return ( +
+ {/* {email && ( + + )} */} + {website && ( + + )} + {wechatPublic && ( + + )} +
+ ); + } + + render() { + const { t } = this.observedContext, + { name, entityType, services, description, internetContact, onSwitch, ...props } = this.props; + const { wechatPublic } = internetContact || {}; + + return ( + + {/* */} + + + {name} + + + + {services && ( + ({ text: serviceCategory! }))} + /> + )} + + {description} + + + + + {this.renderIcon()} + + {this.showQRC && ( + {wechatPublic} + )} + + + ); + } +} diff --git a/components/Organization/Charts.tsx b/components/Organization/Charts.tsx new file mode 100644 index 0000000..ce3e15e --- /dev/null +++ b/components/Organization/Charts.tsx @@ -0,0 +1,43 @@ +import { BarSeries, PieSeries, SVGCharts, Title, Tooltip, XAxis, YAxis } from 'echarts-jsx'; +import { observer } from 'mobx-react'; +import { FC, useContext } from 'react'; + +import { OrganizationStatistic, sortStatistic } from '../../models/Organization'; +import { I18nContext } from '../../models/Translation'; + +const OrganizationCharts: FC = observer( + ({ entityType, serviceCategory, coverageArea }) => { + const { t } = useContext(I18nContext); + + const typeList = sortStatistic(entityType), + categoryList = sortStatistic(serviceCategory), + areaList = sortStatistic(coverageArea); + + return ( +
+ + {t('NGO_area_distribution')} + key)} /> + + value)} /> + + + + + {t('NGO_service_distribution')} + key)} /> + + value)} /> + + + + + {t('NGO_type_distribution')} + ({ name, value }))} /> + + +
+ ); + }, +); +export default OrganizationCharts; diff --git a/components/Organization/LandScape.module.less b/components/Organization/LandScape.module.less new file mode 100644 index 0000000..d81c912 --- /dev/null +++ b/components/Organization/LandScape.module.less @@ -0,0 +1,10 @@ +.groupTitle { + background-color: rgb(52, 112, 159); +} +.listItem { + transition: 0.25ms; + &:hover { + cursor: pointer; + box-shadow: 1px 1px 5px 1px rgba(0, 0, 0, 0.2); + } +} diff --git a/components/Organization/Landscape.tsx b/components/Organization/Landscape.tsx new file mode 100644 index 0000000..2eb373e --- /dev/null +++ b/components/Organization/Landscape.tsx @@ -0,0 +1,79 @@ +import { Organization } from '@open-source-bazaar/china-ngo-database'; +import { Dialog } from 'idea-react'; +import { observable } from 'mobx'; +import { observer } from 'mobx-react'; +import { Component } from 'react'; +import { Modal } from 'react-bootstrap'; +import { splitArray } from 'web-utility'; + +import { OrganizationModel } from '../../models/Organization'; +import systemStore from '../../models/System'; +import { OrganizationCard } from './Card'; +import styles from './LandScape.module.less'; + +export type OpenCollaborationLandscapeProps = Pick; + +@observer +export class OpenCollaborationLandscape extends Component { + @observable + accessor itemSize = 2; + + modal = new Dialog<{ name?: string }>(({ defer, name }) => ( + defer?.resolve()}> + + {name} + + {this.renderCard(name!)} + + )); + + renderCard(name: string) { + const organization = Object.values(this.props.typeMap) + .flat() + .find(({ name: n }) => n === name); + + if (!organization) return <>; + + const { id, ...data } = organization; + + return ; + } + + renderLogo = ({ name }: Organization) => ( +
  • this.modal.open({ name: name as string })} + > +
    + {name.slice(0, 2)} +
    + {name.slice(2, 4)} +
    +
  • + ); + + render() { + const { screenNarrow } = systemStore; + const rows = splitArray(Object.entries(this.props.typeMap), 2); + + return ( + <> + {rows.map(row => ( +
      + {row.map(([name, list]) => ( +
    • +

      {name}

      + +
        + {list.map(this.renderLogo)} +
      +
    • + ))} +
    + ))} + + + ); + } +} diff --git a/models/Base.ts b/models/Base.ts index 03935e1..1c5b56b 100644 --- a/models/Base.ts +++ b/models/Base.ts @@ -12,6 +12,8 @@ import { isServer, ProxyBaseURL, LARK_API_HOST, + STRAPI_API_HOST, + STRAPI_API_TOKEN, } from './configuration'; export const ownClient = new HTTPClient({ @@ -65,3 +67,15 @@ export function fileURLOf(field: TableCellValue, cache = false) { return URI; } + +export const strapiClient = new HTTPClient({ + baseURI: STRAPI_API_HOST, + responseType: 'json', +}).use(({ request }, next) => { + request.headers = { + Authorization: `Bearer ${STRAPI_API_TOKEN}`, + ...request.headers, + 'Strapi-Response-Format': 'v4', + }; + return next(); +}); diff --git a/models/Organization.ts b/models/Organization.ts new file mode 100644 index 0000000..b723145 --- /dev/null +++ b/models/Organization.ts @@ -0,0 +1,75 @@ +import { observable } from 'mobx'; +import { toggle } from 'mobx-restful'; +import { Base, Searchable, SearchableFilter, StrapiListModel } from 'mobx-strapi'; +import { countBy, groupBy } from 'web-utility'; + +import { Organization } from '@open-source-bazaar/china-ngo-database'; +import { strapiClient } from './Base'; + +export type OrganizationStatistic = Record< + 'coverageArea' | 'locale' | 'entityType' | 'serviceCategory', + Record +>; + +export const sortStatistic = (data: Record, sortValue = true) => + Object.entries(data) + .map(([key, count]) => [key, count] as const) + .sort(([kX, vX], [kY, vY]) => (sortValue ? vY - vX : kY.localeCompare(kX))); + +export class OrganizationModel extends Searchable(StrapiListModel) { + baseURI = 'organizations'; + client = strapiClient; + + sort = { establishedDate: 'asc' } as const; + + dateKeys = ['establishedDate'] as const; + + searchKeys = ['name', 'description', 'coverageArea'] as const; + + @observable + accessor statistic = {} as OrganizationStatistic; + + @observable + accessor typeMap: Record = {}; + + @toggle('downloading') + async getYearRange() { + const now = Date.now(), + organizationStore = new OrganizationModel(); + + const [{ establishedDate: start } = {}] = await organizationStore.getList({}, 1, 1); + + Object.assign(organizationStore, { sort: { establishedDate: 'desc' } }); + + const [{ establishedDate: end } = {}] = await organizationStore.getList({}, 1, 1); + + const startYear = new Date(start || now).getFullYear(), + endYear = new Date(end || now).getFullYear(); + + return [startYear, endYear] as const; + } + + async getStatistic(filter?: SearchableFilter) { + const list = await this.getAll(filter); + + const statistic = Object.fromEntries( + (['coverageArea', 'locale', 'entityType'] as (keyof Organization)[]).map(key => [ + key, + countBy(list, ({ [key]: value }) => value?.toString() || 'unknown'), + ]), + ); + const serviceCategory = countBy( + list, + ({ services }) => + services?.map(({ serviceCategory }) => serviceCategory!).filter(Boolean) || [], + ); + return (this.statistic = { ...statistic, serviceCategory } as OrganizationStatistic); + } + + async groupAllByType(filter?: SearchableFilter) { + const allData = await this.getAll(filter); + + return (this.typeMap = groupBy(allData, 'entityType')); + } +} +export default new OrganizationModel(); diff --git a/models/System.ts b/models/System.ts index 559a99d..d5cbc59 100644 --- a/models/System.ts +++ b/models/System.ts @@ -1,13 +1,50 @@ +import { observable } from 'mobx'; import { BiSearchModelClass } from 'mobx-lark'; -import { BaseModel } from 'mobx-restful'; +import { BaseModel, DataObject, ListModel, toggle } from 'mobx-restful'; +import { Base, SearchableFilter } from 'mobx-strapi'; +import { Constructor } from 'web-utility'; + +import { ownClient } from './Base'; +import { OrganizationModel } from './Organization'; + +export type SearchModel = ListModel>; export type SearchPageMeta = Pick< InstanceType, 'pageIndex' | 'currentPage' | 'pageCount' >; +export type CityCoordinateMap = Record; + export class SystemModel extends BaseModel { - searchMap: Record = {}; + searchMap = { + NGO: OrganizationModel, + } as Record>>; + + @observable + accessor screenNarrow = false; + + @observable + accessor cityCoordinate: CityCoordinateMap = {}; + + constructor() { + super(); + + this.updateScreen(); + globalThis.addEventListener?.('resize', this.updateScreen); + } + + updateScreen = () => + (this.screenNarrow = + globalThis.innerWidth < globalThis.innerHeight || globalThis.innerWidth < 992); + + @toggle('downloading') + async getCityCoordinate() { + const { body } = await ownClient.get( + 'https://idea2app.github.io/public-meta-data/china-city-coordinate.json', + ); + return (this.cityCoordinate = body!); + } } export default new SystemModel(); diff --git a/models/Translation.ts b/models/Translation.ts index 90c22cf..2fdc196 100644 --- a/models/Translation.ts +++ b/models/Translation.ts @@ -1,7 +1,8 @@ -import { loadLanguageMapFrom, parseCookie, TranslationMap, TranslationModel } from 'mobx-i18n'; +import { loadLanguageMapFrom, TranslationMap, TranslationModel } from 'mobx-i18n'; import { DataObject } from 'mobx-restful'; import { NextPageContext } from 'next'; import { createContext } from 'react'; +import { parseCookie } from 'web-utility'; import zhCN from '../translation/zh-CN'; diff --git a/models/configuration.ts b/models/configuration.ts index 49ed225..2eb27e4 100644 --- a/models/configuration.ts +++ b/models/configuration.ts @@ -1,4 +1,4 @@ -import { parseCookie } from 'mobx-i18n'; +import { parseCookie } from 'web-utility'; export const isServer = () => typeof window === 'undefined'; @@ -6,7 +6,7 @@ export const Name = process.env.NEXT_PUBLIC_SITE_NAME, Summary = process.env.NEXT_PUBLIC_SITE_SUMMARY, DefaultImage = process.env.NEXT_PUBLIC_LOGO!; -export const { VERCEL, VERCEL_URL } = process.env; +export const { VERCEL, VERCEL_URL, STRAPI_API_TOKEN } = process.env; export const API_Host = isServer() ? VERCEL_URL @@ -16,6 +16,8 @@ export const API_Host = isServer() export const CACHE_HOST = process.env.NEXT_PUBLIC_CACHE_HOST!; +export const STRAPI_API_HOST = process.env.NEXT_PUBLIC_STRAPI_API_HOST!; + export const LARK_API_HOST = `${API_Host}/api/Lark/`; export const ProxyBaseURL = 'https://bazaar.fcc-cd.dev/proxy'; diff --git a/package.json b/package.json index a633917..9fb48f2 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Open Source Bazaar web-site", "private": true, "scripts": { + "e": "pnpx @dotenvx/dotenvx run -f .env.personal.local -- pnpm", "prepare": "husky", "install": "pnpx git-utility download https://github.com/Open-Source-Bazaar/key-vault main Open-Source-Bazaar.github.io || true", "dev": "next dev", @@ -16,34 +17,37 @@ "@koa/router": "^14.0.0", "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", - "@next/mdx": "^15.5.2", + "@next/mdx": "^15.5.3", "core-js": "^3.45.1", + "echarts-jsx": "^0.5.4", "file-type": "^21.0.0", "idea-react": "^2.0.0-rc.13", "koa": "^3.0.1", "koajax": "^3.1.2", "license-filter": "^0.2.5", - "marked": "^16.2.1", - "mime": "^4.0.7", + "marked": "^16.3.0", + "mime": "^4.1.0", "mobx": "^6.13.7", "mobx-github": "^0.5.1", - "mobx-i18n": "^0.7.1", - "mobx-lark": "^2.4.1", + "mobx-i18n": "^0.7.2", + "mobx-lark": "^2.4.2", "mobx-react": "^9.2.0", "mobx-react-helper": "^0.5.1", - "mobx-restful": "^2.1.0", - "mobx-restful-table": "^2.5.3", - "next": "^15.5.2", + "mobx-restful": "^2.1.1", + "mobx-restful-table": "^2.5.4", + "mobx-strapi": "^0.7.4", + "next": "^15.5.3", "next-pwa": "^5.6.0", "next-ssr-middleware": "^1.0.3", + "open-react-map": "^0.9.1", "react": "^19.1.1", "react-bootstrap": "^2.10.10", "react-dom": "^19.1.1", "react-typed-component": "^1.0.6", "remark-frontmatter": "^5.0.0", "remark-mdx-frontmatter": "^5.2.0", - "undici": "^7.15.0", - "web-utility": "^4.5.3", + "undici": "^7.16.0", + "web-utility": "^4.6.1", "yaml": "^2.8.1" }, "devDependencies": { @@ -52,21 +56,22 @@ "@babel/preset-react": "^7.27.1", "@cspell/eslint-plugin": "^9.2.1", "@eslint/js": "^9.35.0", - "@next/eslint-plugin-next": "^15.5.2", + "@next/eslint-plugin-next": "^15.5.3", + "@open-source-bazaar/china-ngo-database": "^0.6.0", "@softonus/prettier-plugin-duplicate-remover": "^1.1.2", "@stylistic/eslint-plugin": "^5.3.1", "@types/eslint-config-prettier": "^6.11.3", "@types/koa": "^3.0.0", "@types/next-pwa": "^5.6.9", - "@types/node": "^22.18.1", - "@types/react": "^19.1.12", + "@types/node": "^22.18.5", + "@types/react": "^19.1.13", "@types/react-dom": "^19.1.9", "eslint": "^9.35.0", - "eslint-config-next": "^15.5.2", + "eslint-config-next": "^15.5.3", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.5", "eslint-plugin-simple-import-sort": "^12.1.1", - "globals": "^16.3.0", + "globals": "^16.4.0", "husky": "^9.1.7", "jiti": "^2.5.1", "less": "^4.4.1", @@ -77,9 +82,10 @@ "prettier-plugin-css-order": "^2.1.2", "sass": "^1.92.1", "typescript": "~5.9.2", - "typescript-eslint": "^8.43.0" + "typescript-eslint": "^8.44.0" }, "resolutions": { + "mobx-react-helper": "$mobx-react-helper", "next": "$next" }, "pnpm": { diff --git a/pages/NGO/[year]/index.tsx b/pages/NGO/[year]/index.tsx new file mode 100644 index 0000000..f486979 --- /dev/null +++ b/pages/NGO/[year]/index.tsx @@ -0,0 +1,52 @@ +import { observer } from 'mobx-react'; +import { cache, compose, errorLogger } from 'next-ssr-middleware'; +import { FC, useContext } from 'react'; +import { Button, Container } from 'react-bootstrap'; + +import { PageHead } from '../../../components/Layout/PageHead'; +import { CityStatisticMap } from '../../../components/Map/CityStatisticMap'; +import { SearchBar } from '../../../components/Navigator/SearchBar'; +import OrganizationCharts from '../../../components/Organization/Charts'; +import { OrganizationModel, OrganizationStatistic } from '../../../models/Organization'; +import { I18nContext } from '../../../models/Translation'; + +interface OrganizationPageProps { + year: string; + statistic: OrganizationStatistic; +} + +export const getServerSideProps = compose<{ year: string }, OrganizationPageProps>( + cache(), + errorLogger, + async ({ params }) => { + const statistic = await new OrganizationModel().getStatistic({ establishedDate: params!.year }); + + return { props: { year: params!.year, statistic } }; + }, +); + +const OrganizationPage: FC = observer(({ year, statistic }) => { + const { t } = useContext(I18nContext); + + return ( + + + +
    +

    {t('China_NGO_Map')}

    +
    + +
    + + +
    + + + + +
    + ); +}); +export default OrganizationPage; diff --git a/pages/NGO/[year]/landscape.tsx b/pages/NGO/[year]/landscape.tsx new file mode 100644 index 0000000..296171d --- /dev/null +++ b/pages/NGO/[year]/landscape.tsx @@ -0,0 +1,39 @@ +import { observer } from 'mobx-react'; +import { cache, compose, errorLogger } from 'next-ssr-middleware'; +import { FC, useContext } from 'react'; +import { Container } from 'react-bootstrap'; + +import { PageHead } from '../../../components/Layout/PageHead'; +import { + OpenCollaborationLandscape, + OpenCollaborationLandscapeProps, +} from '../../../components/Organization/Landscape'; +import { OrganizationModel } from '../../../models/Organization'; +import { I18nContext } from '../../../models/Translation'; + +export const getServerSideProps = compose<{ year: string }, Pick>( + cache(), + errorLogger, + async ({ params }) => { + const typeMap = await new OrganizationModel().groupAllByType({ + establishedDate: params!.year, + }); + + return { props: JSON.parse(JSON.stringify({ typeMap })) }; + }, +); + +const LandscapePage: FC = observer(props => { + const { t } = useContext(I18nContext); + + return ( + + + +

    {t('China_NGO_Landscape')}

    + + +
    + ); +}); +export default LandscapePage; diff --git a/pages/NGO/index.tsx b/pages/NGO/index.tsx new file mode 100644 index 0000000..0ce5287 --- /dev/null +++ b/pages/NGO/index.tsx @@ -0,0 +1,32 @@ +import { observer } from 'mobx-react'; +import { InferGetServerSidePropsType } from 'next'; +import { cache, compose, errorLogger } from 'next-ssr-middleware'; +import { FC, useContext } from 'react'; +import { Container } from 'react-bootstrap'; + +import { ZodiacBar } from '../../components/Base/ZodiacBar'; +import { PageHead } from '../../components/Layout/PageHead'; +import { OrganizationModel } from '../../models/Organization'; +import { I18nContext } from '../../models/Translation'; + +export const getServerSideProps = compose(cache(), errorLogger, async () => { + const [startYear, endYear] = await new OrganizationModel().getYearRange(); + + return { props: { startYear, endYear } }; +}); + +const OrganizationHomePage: FC> = observer( + props => { + const { t } = useContext(I18nContext); + + return ( + + +

    {t('China_NGO_DB')} 2.0

    + + ({ title: year, link: `/NGO/${year}` })} /> +
    + ); + }, +); +export default OrganizationHomePage; diff --git a/pages/_document.tsx b/pages/_document.tsx index 50b0597..f4d7b6a 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,10 +1,4 @@ -import Document, { - DocumentContext, - Head, - Html, - Main, - NextScript, -} from 'next/document'; +import Document, { DocumentContext, Head, Html, Main, NextScript } from 'next/document'; import { LanguageCode, parseSSRContext } from '../models/Translation'; @@ -34,7 +28,7 @@ export default class CustomDocument extends Document { { rel="stylesheet" href="https://unpkg.com/github-markdown-css@5.8.1/github-markdown.css" /> + + diff --git a/pages/index.tsx b/pages/index.tsx index e468328..ab4c84c 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,11 +1,17 @@ +import { observer } from 'mobx-react'; +import { FC, useContext } from 'react'; import { Card, Col, Row } from 'react-bootstrap'; import { renderToStaticMarkup } from 'react-dom/server'; import ReactTyped from 'react-typed-component'; import { PageHead } from '../components/Layout/PageHead'; +import { I18nContext } from '../models/Translation'; import styles from '../styles/Home.module.scss'; -const HomePage = () => ( +const HomePage: FC = observer(() => { + const { t } = useContext(I18nContext); + + return ( <> @@ -38,7 +44,7 @@ const HomePage = () => ( ), renderToStaticMarkup( <> - 欢迎参与开放式协作 + {t('welcome_open_collaboration')}{t('open_collaboration')} , ), renderToStaticMarkup( @@ -54,11 +60,11 @@ const HomePage = () => (
    -

    参与

    +

    {t('participate')}

    -
    代码工作
    +
    {t('code_work')}