Skip to content

Commit

Permalink
feat: add about sidebar collapsible panel
Browse files Browse the repository at this point in the history
Also add better error message if config or about content requests fail.

BREAKING CHANGE: Added `aboutTitle` required config

Closes #151
  • Loading branch information
stdavis committed Apr 13, 2023
1 parent 2cb05cc commit 57cb3fa
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 73 deletions.
5 changes: 2 additions & 3 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import appConfig from '../public/config.json';
import configSchema from '../public/config.schema.json';
import '../src/App.scss';
import '../src/index.css';
import '../src/index.scss';
import { setConfigs } from '../src/services/config';

setConfigs(appConfig, configSchema);
setConfigs(appConfig, null, configSchema);

export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@
},
"eslintConfig": {
"env": {
"browser": true
"browser": true,
"es2022": true
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"settings": {
Expand Down
1 change: 1 addition & 0 deletions public/about.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<b>this</b> is <i>content</i> with HTML markup
1 change: 1 addition & 0 deletions public/config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"appTitle": "RTP Projects",
"aboutTitle": "About this map",
"defaultExtent": {
"x": -12453807,
"y": 5014773,
Expand Down
6 changes: 6 additions & 0 deletions public/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
}
},
"required": [
"aboutTitle",
"appTitle",
"defaultExtent",
"filter",
Expand All @@ -62,6 +63,11 @@
],
"additionalProperties": false,
"properties": {
"aboutTitle": {
"title": "About Title",
"description": "The title of the about dialog",
"type": "string"
},
"appTitle": {
"title": "Application Title",
"description": "The title of the application",
Expand Down
7 changes: 7 additions & 0 deletions public/web.config
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
</staticContent>
</system.webServer>
</location>
<location path="about.html">
<system.webServer>
<staticContent>
<clientCache cacheControlMode="DisableCache" />
</staticContent>
</system.webServer>
</location>
<system.webServer>
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="30.00:00:00" />
Expand Down
106 changes: 63 additions & 43 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import Graphic from '@arcgis/core/Graphic';
import Viewpoint from '@arcgis/core/Viewpoint';
import WebMap from '@arcgis/core/WebMap';
import * as reactiveUtils from '@arcgis/core/core/reactiveUtils';
import { whenOnce } from '@arcgis/core/core/reactiveUtils';
import Graphic from '@arcgis/core/Graphic';
import WebTileLayer from '@arcgis/core/layers/WebTileLayer';
import Viewpoint from '@arcgis/core/Viewpoint';
import MapView from '@arcgis/core/views/MapView';
import WebMap from '@arcgis/core/WebMap';
import Home from '@arcgis/core/widgets/Home';
import { faFilter, faHandPointer } from '@fortawesome/free-solid-svg-icons';
import { faCircleQuestion, faFilter, faHandPointer } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import LayerSelector from '@ugrc/layer-selector';
import debounce from 'lodash.debounce';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Collapse } from 'reactstrap';
import 'typeface-montserrat';
import { NumberParam, StringParam, useQueryParams } from 'use-query-params';
import './App.scss';
import Filter from './components/Filter';
import MapWidget from './components/MapWidget';
import MapWidgetContainer from './components/MapWidgetContainer';
Expand Down Expand Up @@ -271,47 +272,66 @@ function App() {
const [projectInfoIsOpen, setProjectInfoIsOpen] = useState(config.openOnLoad.projectInfo);
const [resize, setResize] = useState(0);

const [aboutIsOpen, setAboutIsOpen] = useState(false);

return (
<QueryClientProvider client={queryClient}>
<div className="d-flex flex-column w-100 h-100">
<div className="m-3 title">
<h4 className="my-0">{config.appTitle}</h4>
</div>
<div id="mapDiv" className="flex-fill border-top border position-relative">
<MapWidgetContainer openStates={[filterIsOpen, projectInfoIsOpen]}>
<MapWidget
defaultOpen={config.openOnLoad.filter}
isOpen={filterIsOpen}
name={t('trans:filter')}
icon={faFilter}
position={0}
mapView={mapView}
showReset={true}
onReset={() => filterDispatch({ type: 'reset' })}
toggle={() => setFilterIsOpen((current) => !current)}
resize={resize}
isAlone={filterIsOpen && !projectInfoIsOpen}
>
<Filter mapView={mapView} state={filterState} dispatch={filterDispatch} />
</MapWidget>
<MapWidgetResizeHandle onResize={setResize} show={filterIsOpen && projectInfoIsOpen} />
<MapWidget
defaultOpen={config.openOnLoad.projectInfo}
isOpen={projectInfoIsOpen}
name={t('trans:projectInformation')}
icon={faHandPointer}
position={1}
mapView={mapView}
toggle={() => setProjectInfoIsOpen((current) => !current)}
resize={resize}
isAlone={!filterIsOpen && projectInfoIsOpen}
>
<ProjectInformation graphics={selectedGraphics} highlightGraphic={highlightGraphic} />
</MapWidget>
</MapWidgetContainer>
<Sherlock {...sherlockConfig}></Sherlock>
{layerSelectorOptions && <LayerSelector {...layerSelectorOptions} />}
<div className="w-100 h-100 d-flex flex-row">
<div className="d-flex flex-fill flex-column">
<div className="m-3 title d-flex flex-row justify-content-between align-items-center">
<h4 className="my-0">{config.appTitle}</h4>
<FontAwesomeIcon
icon={faCircleQuestion}
onClick={() => setAboutIsOpen(!aboutIsOpen)}
role="button"
size="xl"
/>
</div>
<div id="mapDiv" className="flex-fill position-relative border-top">
<MapWidgetContainer openStates={[filterIsOpen, projectInfoIsOpen]}>
<MapWidget
defaultOpen={config.openOnLoad.filter}
isOpen={filterIsOpen}
name={t('trans:filter')}
icon={faFilter}
position={0}
mapView={mapView}
showReset={true}
onReset={() => filterDispatch({ type: 'reset' })}
toggle={() => setFilterIsOpen((current) => !current)}
resize={resize}
isAlone={filterIsOpen && !projectInfoIsOpen}
>
<Filter mapView={mapView} state={filterState} dispatch={filterDispatch} />
</MapWidget>
<MapWidgetResizeHandle onResize={setResize} show={filterIsOpen && projectInfoIsOpen} />
<MapWidget
defaultOpen={config.openOnLoad.projectInfo}
isOpen={projectInfoIsOpen}
name={t('trans:projectInformation')}
icon={faHandPointer}
position={1}
mapView={mapView}
toggle={() => setProjectInfoIsOpen((current) => !current)}
resize={resize}
isAlone={!filterIsOpen && projectInfoIsOpen}
>
<ProjectInformation graphics={selectedGraphics} highlightGraphic={highlightGraphic} />
</MapWidget>
</MapWidgetContainer>
<Sherlock {...sherlockConfig}></Sherlock>
{layerSelectorOptions && <LayerSelector {...layerSelectorOptions} />}
</div>
</div>
<Collapse horizontal isOpen={aboutIsOpen}>
<div className="h-100 about-content border-start p-3">
<div className="mb-1 w-100 d-flex flex-row justify-content-between align-items-center">
<h5 className="m-0">{config.aboutTitle}</h5>
<Button close onClick={() => setAboutIsOpen(false)} />
</div>
<div dangerouslySetInnerHTML={{ __html: config.aboutContent }}></div>
</div>
</Collapse>
</div>
{config.splashScreen.enabled ? <SplashScreen /> : null}
</QueryClientProvider>
Expand Down
4 changes: 4 additions & 0 deletions src/App.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
font-family: Montserrat, Arial, sans-serif;
color: $highlightColor;
}

.about-content {
width: 350px;
}
5 changes: 4 additions & 1 deletion src/index.css → src/index.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
@import '@arcgis/core/assets/esri/themes/light/main.css';
@import 'App.scss';

html, body, #root {
html,
body,
#root {
padding: 0;
margin: 0;
height: 100%;
Expand Down
62 changes: 41 additions & 21 deletions src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,48 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { QueryParamProvider } from 'use-query-params';
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
import ErrorFallback from './components/ErrorFallback';
import './index.css';
import './index.scss';
import { setConfigs } from './services/config';

console.log('fetching config.json');
fetch('config.json')
.then((response) => response.json())
.then(async (appConfig) => {
await setConfigs(appConfig);
const { default: App } = await import('./App');

console.log('rendering app');
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => document.location.reload()}>
<BrowserRouter>
<QueryParamProvider adapter={ReactRouter6Adapter} options={{ updateType: 'replaceIn' }}>
<Routes>
<Route path="*" element={<App />} />
</Routes>
</QueryParamProvider>
</BrowserRouter>
</ErrorBoundary>
</React.StrictMode>
);
});
async function fetchConfig(resource, type) {
const response = await fetch(resource);
if (!response.ok) {
throw new Error(`Failed to fetch ${resource}`);
}

return type === 'json' ? response.json() : response.text();
}

let appConfig;
let aboutContent;
try {
[appConfig, aboutContent] = await Promise.all([
fetchConfig('config.json', 'json'),
fetchConfig('about.html', 'text'),
]);
} catch (error) {
ReactDOM.createRoot(document.getElementById('root')).render(
<ErrorFallback error={error} resetErrorBoundary={() => document.location.reload()} />
);
}

await setConfigs(appConfig, aboutContent);

const { default: App } = await import('./App');

console.log('rendering app');
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => document.location.reload()}>
<BrowserRouter>
<QueryParamProvider adapter={ReactRouter6Adapter} options={{ updateType: 'replaceIn' }}>
<Routes>
<Route path="*" element={<App />} />
</Routes>
</QueryParamProvider>
</BrowserRouter>
</ErrorBoundary>
</React.StrictMode>
);
4 changes: 3 additions & 1 deletion src/services/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const config = {

// optional configSchema is for vitest and storybook since they are clumsy when it comes to
// async setup
export const setConfigs = async (appConfigs, configSchema = null) => {
export const setConfigs = async (appConfigs, aboutContent, configSchema = null) => {
// we are fetching this rather than importing it so that it can be hosted publicly and available
// for WFRC to reference it in their config files
if (!configSchema) {
Expand All @@ -28,6 +28,8 @@ export const setConfigs = async (appConfigs, configSchema = null) => {
console.error('There is an error in config.json!', error.stack);
}

appConfigs.aboutContent = aboutContent;

i18n
.use(initReactI18next)
.use(LanguageDetector)
Expand Down
2 changes: 1 addition & 1 deletion tests/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import configSchema from '../public/config.schema.json';

import { setConfigs } from '../src/services/config';

setConfigs(appConfig, configSchema);
setConfigs(appConfig, null, configSchema);
2 changes: 1 addition & 1 deletion vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ function watchConfigFiles() {
name: 'watch-config-files',
enforce: 'post',
handleHotUpdate({ file, server }) {
if (file.endsWith('.json')) {
if (file.endsWith('.json') || file.endsWith('about.html')) {
server.ws.send({
type: 'full-reload',
path: '*',
Expand Down

0 comments on commit 57cb3fa

Please sign in to comment.