Skip to content

Commit 887d543

Browse files
authored
Merge pull request #60 from DigitalCommons/create-about-panel
29 Get AboutPanel working
2 parents cc520ce + 02c665e commit 887d543

21 files changed

+279
-1142
lines changed

apps/back-end/src/routes.ts

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { contract } from "@mykomap/common";
44
import { FastifyPluginOptions } from "fastify";
55
import fs from "node:fs";
66
import {
7+
getDatasetAbout,
78
getDatasetConfig,
89
getDatasetItem,
910
getDatasetLocations,
@@ -109,6 +110,12 @@ export function MykomapRouter(
109110
return { status: 200, body: config };
110111
},
111112

113+
getAbout: async ({ params: { datasetId } }) => {
114+
const about = getDatasetAbout(datasetId);
115+
116+
return { status: 200, body: about };
117+
},
118+
112119
getVersion: async () => {
113120
return {
114121
status: 200,

apps/back-end/src/services/Dataset.ts

+9
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class Dataset {
1919
id: string;
2020
folderPath: string;
2121
config: ConfigData;
22+
about: string;
2223
propDefs: PropDefs;
2324
searchablePropIndexMap: { [field: string]: number };
2425
searchablePropValues: (string | string[])[][];
@@ -34,6 +35,12 @@ export class Dataset {
3435
),
3536
);
3637

38+
// Load the About text
39+
this.about = fs.readFileSync(
40+
path.join(this.folderPath, "about.md"),
41+
"utf8",
42+
);
43+
3744
// Create prop defs
3845
const pdf = new PropDefsFactory(this.config.vocabs, this.config.languages);
3946
this.propDefs = pdf.mkPropDefs(this.config.itemProps);
@@ -101,6 +108,8 @@ export class Dataset {
101108

102109
getConfig = () => this.config;
103110

111+
getAbout = () => this.about;
112+
104113
getLocations = (): fs.ReadStream =>
105114
fs.createReadStream(path.join(this.folderPath, "locations.json"), "utf8");
106115

apps/back-end/src/services/datasetService.ts

+5
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export const getDatasetConfig = (datasetId: string): GetConfigBody => {
5959
return dataset.getConfig();
6060
};
6161

62+
export const getDatasetAbout = (datasetId: string): string => {
63+
const dataset = getDatasetOrThrow404(contract.getAbout, datasetId);
64+
return dataset.getAbout();
65+
};
66+
6267
export const getDatasetLocations = (datasetId: string): fs.ReadStream => {
6368
const dataset = getDatasetOrThrow404(contract.getDatasetLocations, datasetId);
6469
return dataset.getLocations();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
This [Mykomap](https://digitalcommons.coop/mykomaps/) has been created by the [Digital Commons
2+
Co-operative](https://digitalcommons.coop/) using data published by Mr Douglas Quaid under the Open
3+
Data Commons Attribution Licence ([ODC-By v1.0](https://opendatacommons.org/licenses/by/1-0/).) By
4+
accessing the dataset you are agreeing to accept this license.
5+
6+
The original data can be found in your imagination. The data was last updated in November 2024.

apps/back-end/test/plugin.test.ts

+23
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,29 @@ describe("getDatasetItem", () => {
241241
});
242242
});
243243

244+
describe("getAbout", () => {
245+
describe("dataset exists", () => {
246+
test("status code 200 and non-empty response", async (t) => {
247+
const res = await fastify.inject({
248+
method: "GET",
249+
url: "/dataset/dataset-A/about",
250+
});
251+
expect(res.statusCode).toBe(200);
252+
expect(res.body).toContain("published by Mr Douglas Quaid");
253+
});
254+
});
255+
256+
describe("dataset does not exist", () => {
257+
test("status code 404", async (t) => {
258+
const res = await fastify.inject({
259+
method: "GET",
260+
url: "/dataset/dataset-in-your-imagination/about",
261+
});
262+
expect(res.statusCode).toBe(404);
263+
});
264+
});
265+
});
266+
244267
describe("getConfig", () => {
245268
describe("dataset exists", () => {
246269
test("status code 200 and non-empty response", async (t) => {

apps/front-end/package.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,11 @@
3838
"i18next-http-backend": "^2.6.2",
3939
"maplibre-gl": "^4.5.2",
4040
"maplibregl-spiderfier": "github:ohrie/maplibre-spiderfier",
41+
"mui-markdown": "^1.2.3",
4142
"react": "^18.2.0",
4243
"react-dom": "^18.2.0",
4344
"react-i18next": "^15.1.1",
44-
"react-markdown": "^8.0.6",
45-
"react-redux": "^9.1.0",
46-
"rehype-sanitize": "^6.0.0"
45+
"react-redux": "^9.1.0"
4746
},
4847
"devDependencies": {
4948
"@chromatic-com/storybook": "^1.8.0",

apps/front-end/src/app/configSlice.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createAction, PayloadAction } from "@reduxjs/toolkit";
22
import { createAppSlice } from "./createAppSlice";
33
import { Config, getConfig } from "../services";
4-
import { getUrlSearchParam } from "../utils/window-utils";
4+
import { getDatasetId } from "../utils/window-utils";
55
import i18n from "../i18n";
66

77
export interface ConfigSliceState {
@@ -22,7 +22,7 @@ export const configSlice = createAppSlice({
2222
reducers: (create) => ({
2323
fetchConfig: create.asyncThunk(
2424
async (_, thunkApi) => {
25-
const datasetId = getUrlSearchParam("datasetId");
25+
const datasetId = getDatasetId();
2626
if (datasetId === null) {
2727
return thunkApi.rejectWithValue(
2828
`No datasetId parameter given, so no dataset config can be retrieved`,

apps/front-end/src/components/map/mapSlice.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { z } from "zod";
44
import { notNullish, schemas } from "@mykomap/common";
55
import { createAppSlice } from "../../app/createAppSlice";
66
import { getDatasetLocations } from "../../services";
7-
import { getUrlSearchParam } from "../../utils/window-utils";
7+
import { getDatasetId } from "../../utils/window-utils";
88

99
export type Location = z.infer<typeof schemas.Location>;
1010
export type DatasetLocations = z.infer<typeof schemas.DatasetLocations>;
@@ -25,7 +25,7 @@ export const mapSlice = createAppSlice({
2525
reducers: (create) => ({
2626
fetchLocations: create.asyncThunk(
2727
async (_, thunkApi) => {
28-
const datasetId = getUrlSearchParam("datasetId");
28+
const datasetId = getDatasetId();
2929
if (datasetId === null) {
3030
return thunkApi.rejectWithValue(
3131
`No datasetId parameter given, so dataset locations cannot be fetched`,

apps/front-end/src/components/panel/aboutPanel/AboutPanel.tsx

+14-14
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
import ReactMarkdown from "react-markdown";
2-
import rehypeSanitize from "rehype-sanitize";
1+
import { MuiMarkdown } from "mui-markdown";
32
import Heading from "../heading/Heading";
43
import ContentPanel from "../contentPanel/ContentPanel";
54
import { useTranslation } from "react-i18next";
6-
7-
// Mock data
8-
const aboutContent = `
9-
Ackroydon East TMO was established in 1997 and has been delivering housing management, maintenance and estate services for its members and residents since October 1999 through a Management Agreement with Wandsworth Borough Council. The TMO has successfully passed through 3 Continuation Ballots (in 2004, 2009 and 2014) with high turnouts and strong tenant support.
10-
11-
When the TMO first took over the management of the estate we took on the full range of services but subsequently returned service charge collection in 2003 and rent collection in 2005.
12-
13-
The TMO has invested significant surplus funds and matched these against a number of successful Small Improvement Budget bids to develop a playground in Swanton Gardens, new estate lighting, a new car park at Eastwick Court, brought the previously derelict garages and storesheds on the estate back into use and undertaken a number of renewals to paths, access roads and the estate environment.
14-
`;
5+
import { useEffect, useState } from "react";
6+
import { getDatasetId } from "../../../utils/window-utils";
157

168
const AboutPanel = () => {
179
const { t } = useTranslation();
10+
const [aboutContent, setAboutContent] = useState("");
11+
12+
useEffect(() => {
13+
fetch(`${import.meta.env.VITE_API_URL}/dataset/${getDatasetId()}/about`)
14+
.then((response) => response.text())
15+
.then((text) => {
16+
console.log("About content", text);
17+
setAboutContent(text);
18+
});
19+
});
1820

1921
return (
2022
<>
2123
<Heading title={t("about")} />
2224
<ContentPanel>
23-
<ReactMarkdown rehypePlugins={[rehypeSanitize]}>
24-
{aboutContent}
25-
</ReactMarkdown>
25+
<MuiMarkdown>{aboutContent}</MuiMarkdown>
2626
</ContentPanel>
2727
</>
2828
);

apps/front-end/src/components/panel/contentPanel/ContentPanel.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,16 @@ const StyledContentPanel = styled(Box)(() => ({
1313
padding: "var(--spacing-medium)",
1414
maxWidth: "var(--panel-width-desktop)",
1515
margin: "0 auto",
16-
"@media (min-width: 768px)": {
16+
"@media (min-width: 897px)": {
1717
padding: "var(--spacing-large)",
18+
paddingTop: "5px",
19+
},
20+
"& .MuiLink-root": {
21+
padding: 0,
22+
display: "inline",
23+
},
24+
"& .MuiTypography-root": {
25+
marginBlock: "25px",
1826
},
1927
}));
2028

apps/front-end/src/components/panel/panelSlice.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { PayloadAction } from "@reduxjs/toolkit";
22
import { createAppSlice } from "../../app/createAppSlice";
3-
import { getUrlSearchParam } from "../../utils/window-utils";
3+
import { getDatasetId } from "../../utils/window-utils";
44
import { searchDataset } from "../../services";
55
import { SearchSliceState } from "./searchPanel/searchSlice";
66

@@ -52,7 +52,7 @@ export const panelSlice = createAppSlice({
5252
}),
5353
populateSearchResults: create.asyncThunk(
5454
async (page: number, thunkApi) => {
55-
const datasetId = getUrlSearchParam("datasetId");
55+
const datasetId = getDatasetId();
5656
if (datasetId === null) {
5757
return thunkApi.rejectWithValue(
5858
`No datasetId parameter given, so no dataset can be searched`,

apps/front-end/src/components/panel/searchPanel/searchSlice.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createAppSlice } from "../../../app/createAppSlice";
33
import type { Config } from "../../../services";
44
import { searchDataset } from "../../../services";
55
import { configLoaded } from "../../../app/configSlice";
6-
import { getUrlSearchParam } from "../../../utils/window-utils";
6+
import { getDatasetId } from "../../../utils/window-utils";
77
import { populateSearchResults } from "../panelSlice";
88
import { AppThunk } from "../../../app/store";
99
import i18n from "../../../i18n";
@@ -174,7 +174,7 @@ export const selectFilterOptions = createSelector(
174174

175175
export const performSearch = (): AppThunk => {
176176
return async (dispatch, getState) => {
177-
const datasetId = getUrlSearchParam("datasetId");
177+
const datasetId = getDatasetId();
178178
if (datasetId === null) {
179179
console.error(
180180
`No datasetId parameter given, so no dataset can be searched`,

apps/front-end/src/components/popup/popupSlice.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createAppSlice } from "../../app/createAppSlice";
22
import { getDatasetItem } from "../../services";
3-
import { getUrlSearchParam } from "../../utils/window-utils";
3+
import { getDatasetId } from "../../utils/window-utils";
44

55
interface PopupState {
66
isOpen: boolean;
@@ -49,7 +49,7 @@ export const popupSlice = createAppSlice({
4949
}),
5050
openPopup: create.asyncThunk(
5151
async (index: number, thunkApi) => {
52-
const datasetId = getUrlSearchParam("datasetId");
52+
const datasetId = getDatasetId();
5353
if (datasetId === null) {
5454
return thunkApi.rejectWithValue(
5555
`No datasetId parameter given, so dataset locations cannot be fetched`,

apps/front-end/src/i18n.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import i18n, { Resource, ResourceLanguage } from "i18next";
22
import HttpBackend, { HttpBackendOptions } from "i18next-http-backend";
33
import LanguageDetector from "i18next-browser-languagedetector";
44
import { initReactI18next } from "react-i18next";
5-
import { getUrlSearchParam } from "./utils/window-utils";
5+
import { getDatasetId } from "./utils/window-utils";
66
import { Config } from "./services";
77

88
i18n
@@ -13,7 +13,7 @@ i18n
1313
backend: {
1414
// Load the config for the dataset from the API, then parse it to extract i18n resources from
1515
// the ui vocab
16-
loadPath: `${import.meta.env.VITE_API_URL}/dataset/${getUrlSearchParam("datasetId")}/config`,
16+
loadPath: `${import.meta.env.VITE_API_URL}/dataset/${getDatasetId()}/config`,
1717
parse: (data, languages, namespaces): Resource | ResourceLanguage => {
1818
const config = JSON.parse(data) as Config;
1919
const uiVocab = config.vocabs.ui;

apps/front-end/src/utils/window-utils.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Iso639Set1Codes } from "@mykomap/common";
22

3-
export const getUrlSearchParam = (param: string): string | null =>
3+
const getUrlSearchParam = (param: string): string | null =>
44
new URLSearchParams(window.location.search).get(param);
55

6+
export const getDatasetId = () => getUrlSearchParam("datasetId");
7+
68
/** Get language from URL param and fallback to English */
79
export const getLanguageFromUrl = (): string => {
810
const lang = getUrlSearchParam("lang")?.toLowerCase();

docs/architecture.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ as seen from the SERVER_DATA_ROOT location:
2929
```
3030
├── datasets
3131
│ ├── some-dataset
32+
│ │ ├── config.json (itemProps, vocabs, UI config, languages, etc.)
33+
│ │ ├── about.md (markdown file containing info to be displayed in AboutPanel)
3234
│ │ ├── locations.json (array of lng-lat coordinates for each item)
3335
│ │ ├── searchable.json (array of the property values and searchable strings for each item)
3436
│ │ ├── items
@@ -40,10 +42,7 @@ as seen from the SERVER_DATA_ROOT location:
4042
│ ├── ...
4143
```
4244

43-
Additionally, for each dataset there's a `config.json`. This contains config for displaying the map
44-
in the UI, including the vocabs (translations of data IDs), default sidebar panel, and popup
45-
appearance. This config is not generated into the above folder structure, but kept in source control
46-
in the `@mykomap/config` library.
45+
Note that the `config.json` for each dataset is kept in source control in the `@mykomap/config` library (to be implemented).
4746

4847
See the [`back-end test data`](https://github.com/DigitalCommons/mykomap-monolith/tree/main/apps/back-end/test/data/) for example file contents.
4948

@@ -70,5 +69,6 @@ We will need to manually copy the `standard.csv` from the data factory server to
7069

7170
- `getItem` method
7271
- `getConfig` method, which includes the vocabs
72+
- `getAbout` method
7373
- `getLocations` method, which returns a stream of the data
7474
- `search` method, which iterates through the data loaded from `searchable.json` to find matching items

docs/images/architecture-back-end.drawio.svg

+2-2
Loading

0 commit comments

Comments
 (0)