Creating useMessageDismissal which can be used to dismiss hint messages to the user.

This commit is contained in:
Roland 2025-01-22 18:58:18 -07:00
parent bf36568e13
commit f7bcf29cce
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
4 changed files with 130 additions and 47 deletions

View file

@ -1547,17 +1547,39 @@ export function APIButton({
return button;
}
export function InfoMessage({children, size = null}) {
return <Message info icon size={size}>
<SIcon name='question'/>
export const useMessageDismissal = (messageName) => {
const [dismissed, setDismissed] = useLocalStorage('dismissed_hints', {});
return {
dismissed: dismissed[messageName] || false, // true or false
setDismissed: (value) => setDismissed({...dismissed, [messageName]: value}),
clearAll: () => setDismissed({}),
}
}
export function InfoMessage({children, size = null, storageName = null, icon = 'question'}) {
const {dismissed, setDismissed} = useMessageDismissal(storageName);
if (dismissed) {
return <React.Fragment/>
}
return <Message info icon size={size} onDismiss={storageName ? () => setDismissed(true) : undefined}>
<SIcon name={icon}/>
<Message.Content>
{children}
</Message.Content>
</Message>
}
export function HandPointMessage({children, size = null}) {
return <Message info icon size={size}>
export function HandPointMessage({children, size = null, storageName = null}) {
const {dismissed, setDismissed} = useMessageDismissal(storageName);
if (dismissed) {
return <React.Fragment/>
}
return <Message info icon size={size} onDismiss={storageName ? () => setDismissed(true) : undefined}>
<SIcon name='hand point right'/>
<Message.Content>
{children}
@ -1565,9 +1587,15 @@ export function HandPointMessage({children, size = null}) {
</Message>
}
export function WarningMessage({children, size = null, icon = 'exclamation'}) {
export function WarningMessage({children, size = null, icon = 'exclamation', storageName = null}) {
const {dismissed, setDismissed} = useMessageDismissal(storageName);
if (dismissed) {
return <React.Fragment/>
}
// Use color='yellow' because "warning" does not work.
return <Message color='yellow' icon size={size}>
return <Message color='yellow' icon size={size} onDismiss={storageName ? () => setDismissed(true) : undefined}>
<SIcon name={icon}/>
<Message.Content>
{children}
@ -1575,10 +1603,18 @@ export function WarningMessage({children, size = null, icon = 'exclamation'}) {
</Message>
}
export function ErrorMessage({children, size = null, icon = 'exclamation'}) {
return <Message negative icon size={size}>
export function ErrorMessage({children, size = null, icon = 'exclamation', storageName = null}) {
const {dismissed, setDismissed} = useMessageDismissal(storageName);
if (dismissed) {
return <React.Fragment/>
}
return <Message negative icon size={size} onDismiss={storageName ? () => setDismissed(true) : undefined}>
<SIcon name={icon}/>
<Message.Content>{children}</Message.Content>
<Message.Content>
{children}
</Message.Content>
</Message>
}
@ -1788,6 +1824,10 @@ export function useLocalStorage(key, initialValue, decode = JSON.parse, encode =
// Save to localStorage when the value changes
useEffect(() => {
if (key === null || key === undefined) {
// key was not defined, do not add this to storage.
return
}
try {
decode(value);
window.localStorage.setItem(key, value);
@ -1920,10 +1960,14 @@ export function getDistinctColor(hexColors) {
return hslToHex(newColor.h, newColor.s, newColor.l);
}
export const isReactChildNull = (children) => {
// Returns `true` if a React element returns `null` as it's child.
const nonNullChildren = React.Children.map(children,
(child) => React.isValidElement(child) && child.type(child.props)
);
return nonNullChildren.length === 0;
}
export const DismissibleMessage = ({storageName, children}) => {
const [dismissed, setDismissed] = useLocalStorage(storageName, false);
if (dismissed) {
return <React.Fragment/>
}
return <Message onDismiss={() => setDismissed(true)}>
{children}
</Message>
}

View file

@ -36,46 +36,41 @@ import {MAP_VIEWER_URI} from "./Vars";
function DockerMapImportWarning() {
const {status} = useContext(StatusContext);
if (status['dockerized']) {
return <Message negative icon>
<SIcon name='hand point right'/>
<Message.Content>
<Message.Header>
Maps are not fully supported in a Docker container!
</Message.Header>
return <ErrorMessage storageName='hint_map_docker_warning'>
<Message.Header>
Maps are not fully supported in a Docker container!
</Message.Header>
<p><b>Only one PBF can be imported and displayed in the docker container.</b></p>
<p><b>Only one PBF can be imported and displayed in the docker container.</b></p>
<p>To import a map file, run the following docker-compose commands:</p>
<pre> docker-compose stop map</pre>
<pre> docker-compose rm map</pre>
<pre> docker-compose run --rm -v /absolute/path/to/map.osm.pbf:/data.osm.pbf
-v openstreetmap-data:/var/lib/postgresql/12/main map import
</pre>
<p>To import a map file, run the following docker-compose commands:</p>
<pre> docker-compose stop map</pre>
<pre> docker-compose rm map</pre>
<pre> docker-compose run --rm -v /absolute/path/to/map.osm.pbf:/data.osm.pbf
-v openstreetmap-data:/var/lib/postgresql/12/main map import
</pre>
<p>Be sure to change <b>/absolute/path/to/map.osm.pbf</b>!</p>
<p>Be sure to change <b>/absolute/path/to/map.osm.pbf</b>!</p>
<p>After you have imported a new PBF file, you need to clear the rendered tile cache:</p>
<pre> docker volume rm openstreetmap-rendered-tiles</pre>
<pre> docker volume create openstreetmap-rendered-tiles</pre>
<p>After you have imported a new PBF file, you need to clear the rendered tile cache:</p>
<pre> docker volume rm openstreetmap-rendered-tiles</pre>
<pre> docker volume create openstreetmap-rendered-tiles</pre>
<p>Start your map container:</p>
<pre> docker-compose up -d map</pre>
<p>Start your map container:</p>
<pre> docker-compose up -d map</pre>
<Divider/>
<Divider/>
<p>You can merge multiple PBF files using osmium (the merged file can then be imported):</p>
<pre> osmium merge file1.osm.pbf file2.osm.pbf -o merged.osm.pbf</pre>
</Message.Content>
</Message>;
<p>You can merge multiple PBF files using osmium (the merged file can then be imported):</p>
<pre> osmium merge file1.osm.pbf file2.osm.pbf -o merged.osm.pbf</pre>
</ErrorMessage>;
}
return <></>;
}
function DownloadMessage() {
return <InfoMessage>
<Message.Header>
Where do I get map files?
</Message.Header>
return <InfoMessage storageName='hint_map_download'>
<Message.Header>Where do I get map files?</Message.Header>
<p>You can download map files from&nbsp;
<a href='https://download.geofabrik.de/'>https://download.geofabrik.de/</a>

View file

@ -23,6 +23,7 @@ import {
HotspotToggle,
ThrottleToggle,
Toggle,
useMessageDismissal,
WROLModeMessage
} from "../Common";
import QRCode from "react-qr-code";
@ -177,6 +178,7 @@ export function SettingsPage() {
const [qrOpen, setQrOpen] = React.useState(false);
const [ready, setReady] = React.useState(false);
const [pendingSave, setPendingSave] = React.useState(false);
const {clearAll} = useMessageDismissal();
// Get current settings from the API.
const {settings, saveSettings, fetchSettings} = React.useContext(SettingsContext);
@ -539,6 +541,11 @@ export function SettingsPage() {
</Segment>
{configsSegment}
<Segment>
<Header as='h1'>Browser Settings</Header>
<APIButton onClick={clearAll}>Show All Hints</APIButton>
</Segment>
</Container>;
}

View file

@ -1,7 +1,7 @@
import {Dropdown, TableBody, TableCell, TableRow} from "semantic-ui-react";
import {Dropdown, MessageHeader, TableBody, TableCell, TableRow} from "semantic-ui-react";
import {Form, Header, Table} from "../Theme";
import React from "react";
import {roundDigits, useLocalStorage} from "../Common";
import {HandPointMessage, roundDigits, useLocalStorage} from "../Common";
import {createUnit, multiply, unit} from "mathjs";
import {Media} from "../../contexts/contexts";
@ -211,6 +211,11 @@ const RatioCalculator = () => {
setStorageRecentUnits({...storageRecentUnits, [key]: state.dUnit});
}, [state.dUnit]);
const inputARef = React.useRef(null);
const inputBRef = React.useRef(null);
const inputCRef = React.useRef(null);
const inputDRef = React.useRef(null);
const unitOptions = baseToUnitsMap[state.base];
const unitDropdownOptions = {
@ -234,14 +239,29 @@ const RatioCalculator = () => {
style={{marginBottom: '1em'}}
/>;
const handleInputChange = (e) => {
// Allow the user to switch between inputs using a, b, c, and d keys.
if (e?.key === 'a') {
inputARef.current.focus();
} else if (e?.key === 'b') {
inputBRef.current.focus();
} else if (e?.key === 'c') {
inputCRef.current.focus();
} else if (e?.key === 'd') {
inputDRef.current.focus();
}
}
const inputA = <div className="ui fluid labeled input" style={{marginBottom: '0.25em'}}>
<div className={`ui label ${aColor}`}>A</div>
<input
id="a"
name="a"
type="number"
ref={inputARef}
value={unitToInputValue(state.a)}
onChange={e => dispatch(e)}
onKeyPress={handleInputChange}
onFocus={e => e.target.select()}
/>
{state.base &&
@ -254,8 +274,10 @@ const RatioCalculator = () => {
id="b"
name="b"
type="number"
ref={inputBRef}
value={unitToInputValue(state.b)}
onChange={e => dispatch(e)}
onKeyPress={handleInputChange}
onFocus={e => e.target.select()}
/>
{state.base &&
@ -268,8 +290,10 @@ const RatioCalculator = () => {
id="c"
name="c"
type="number"
ref={inputCRef}
value={unitToInputValue(state.c)}
onChange={e => dispatch(e)}
onKeyPress={handleInputChange}
onFocus={e => e.target.select()}
/>
{state.base &&
@ -282,14 +306,23 @@ const RatioCalculator = () => {
id="d"
name="d"
type="number"
ref={inputDRef}
value={unitToInputValue(state.d)}
onChange={e => dispatch(e)}
onKeyPress={handleInputChange}
onFocus={e => e.target.select()}
/>
{state.base &&
<Dropdown {...unitDropdownOptions} name='dUnit' value={state.dUnit}/>}
</div>;
React.useEffect(() => {
if (inputARef.current) {
// Focus on input A on page load.
inputARef.current.focus();
}
}, []);
return <Form>
<Header as='h1'>Ratio</Header>
<Header as='h2'>A : B = C : D</Header>
@ -333,6 +366,10 @@ const RatioCalculator = () => {
</TableBody>
</Table>
</Media>
<HandPointMessage storageName='hint_ratio_calculator'>
<MessageHeader>Tip</MessageHeader>
You can change inputs using the <b>a</b>, <b>b</b>, <b>c</b>, and <b>d</b> keys.
</HandPointMessage>
</Form>
}