1
Fork 0
mirror of https://github.com/pds-nest/nest.git synced 2024-11-22 13:04:19 +00:00

💥 Completely implement filters (in a non-atomic commit)

sorry
This commit is contained in:
Steffo 2021-05-21 19:52:56 +02:00
parent 237417298d
commit c7d425960d
Signed by: steffo
GPG key ID: 6965406171929D01
37 changed files with 638 additions and 361 deletions

View file

@ -23,7 +23,7 @@ export default function BoxChart({chartProps, ...props}) {
}, },
ticks: { ticks: {
color: getCssVar("--fg-primary"), color: getCssVar("--fg-primary"),
} },
}, },
y: { y: {
beginAtZero: true, beginAtZero: true,
@ -33,7 +33,7 @@ export default function BoxChart({chartProps, ...props}) {
}, },
ticks: { ticks: {
color: getCssVar("--fg-primary"), color: getCssVar("--fg-primary"),
} },
}, },
}, },
elements: { elements: {
@ -46,8 +46,8 @@ export default function BoxChart({chartProps, ...props}) {
plugins: { plugins: {
legend: { legend: {
display: false, display: false,
} },
} },
}} }}
{...chartProps} {...chartProps}
/> />

View file

@ -1,25 +1,33 @@
import React from "react" import React, { useCallback, useEffect, useMemo, useState } from "react"
import Style from "./BoxMap.module.css" import Style from "./BoxMap.module.css"
import BoxFull from "./BoxFull" import BoxFull from "./BoxFull"
import { MapContainer, TileLayer } from "react-leaflet" import { MapContainer, TileLayer } from "react-leaflet"
export default function BoxMap({ export default function BoxMap(
setMap, {
startingPosition = { lat: 41.89309, lng: 12.48289 }, mapViewHook,
startingZoom = 3,
button, button,
children, children,
...props ...props
}) { }) {
return ( const [map, setMap] = useState(null)
<BoxFull
childrenClassName={Style.BoxMapContents} const onMapMove = useCallback(
{...props} () => mapViewHook.setCenter(map.getCenter()),
> [mapViewHook, map],
)
const onMapZoom = useCallback(
() => mapViewHook.setZoom(map.getZoom()),
[mapViewHook, map],
)
const mapContainer = useMemo(
() => (
<MapContainer <MapContainer
center={startingPosition} center={[mapViewHook.center.lat, mapViewHook.center.lng]}
zoom={startingZoom} zoom={mapViewHook.zoom}
className={Style.MapContainer} className={Style.MapContainer}
whenCreated={setMap} whenCreated={setMap}
> >
@ -34,6 +42,33 @@ export default function BoxMap({
</div> </div>
</div> </div>
</MapContainer> </MapContainer>
),
[mapViewHook],
)
useEffect(
() => {
if(map === null) {
return
}
map.on("move", onMapMove)
map.on("zoom", onMapZoom)
return () => {
map.off("move", onMapMove)
map.off("zoom", onMapZoom)
}
},
[map, mapViewHook]
)
return (
<BoxFull
childrenClassName={Style.BoxMapContents}
{...props}
>
{mapContainer}
</BoxFull> </BoxFull>
) )
} }

View file

@ -34,7 +34,7 @@ export default function BoxWordcloud({ words, callbacks = {}, ...props }) {
callbacks={callbacks} callbacks={callbacks}
/> />
), ),
[words] [words],
) )
return ( return (

View file

@ -1,6 +1,4 @@
import React, { useContext } from "react" import React, { useContext } from "react"
import { faAt, faClock, faGlobe, faHashtag, faMapPin } from "@fortawesome/free-solid-svg-icons"
import ContextRepositoryEditor from "../../contexts/ContextRepositoryEditor"
import Badge from "../base/Badge" import Badge from "../base/Badge"
import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"

View file

@ -1,19 +1,12 @@
import React, { useContext, useState } from "react" import React, { useContext } from "react"
import BoxFull from "../base/BoxFull" import BoxFull from "../base/BoxFull"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faClock, faPlus } from "@fortawesome/free-solid-svg-icons" import { faClock } from "@fortawesome/free-solid-svg-icons"
import InputWithIcon from "../base/InputWithIcon"
import FormInline from "../base/FormInline"
import Style from "./BoxConditionDatetime.module.css"
import ButtonIconOnly from "../base/ButtonIconOnly"
import useRepositoryEditor from "../../hooks/useRepositoryEditor" import useRepositoryEditor from "../../hooks/useRepositoryEditor"
import ButtonToggleBeforeAfter from "./ButtonToggleBeforeAfter"
import Condition from "../../utils/Condition" import Condition from "../../utils/Condition"
import convertToLocalISODate from "../../utils/convertToLocalISODate" import convertToLocalISODate from "../../utils/convertToLocalISODate"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
import FormInlineBADatetime from "./FormInlineBADatetime"
const INVALID_USER_CHARACTERS = /[^0-9TZ:+-]/g
/** /**
@ -25,30 +18,16 @@ const INVALID_USER_CHARACTERS = /[^0-9TZ:+-]/g
* @constructor * @constructor
*/ */
export default function BoxConditionDatetime({ ...props }) { export default function BoxConditionDatetime({ ...props }) {
const [datetime, setDatetime] = useState("")
const [ba, setBa] = useState(false)
const { addCondition } = useRepositoryEditor() const { addCondition } = useRepositoryEditor()
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const onInputChange = event => { const submit = ({ date, isBefore }) => {
let text = event.target.value if(date.toString() === "Invalid Date") {
text = text.toUpperCase() console.debug("Refusing to add condition: ", date, " is an Invalid Date.")
text = text.replace(INVALID_USER_CHARACTERS, "")
return setDatetime(text)
}
const onButtonClick = e => {
const naive = new Date(datetime)
if(naive.toString() === "Invalid Date") {
console.debug("Refusing to add condition: ", naive, " is an Invalid Date.")
return return
} }
const aware = convertToLocalISODate(naive) const aware = convertToLocalISODate(date)
addCondition(new Condition("TIME", `${ba ? ">" : "<"} ${aware}`)) addCondition(new Condition("TIME", `${isBefore ? "<" : ">"} ${aware}`))
setDatetime("")
// Prevent reloading the page!
e.preventDefault()
} }
return ( return (
@ -64,24 +43,9 @@ export default function BoxConditionDatetime({ ...props }) {
} }
{...props} {...props}
> >
<FormInline onSubmit={onButtonClick}> <FormInlineBADatetime
<ButtonToggleBeforeAfter onUpdate={setBa}/> submit={submit}
<InputWithIcon
className={Style.Input}
id={"condition-datetime"}
type={"datetime-local"}
icon={faClock}
value={datetime}
onChange={onInputChange}
placeholder={"2021-12-31T23:59Z"}
/> />
<ButtonIconOnly
className={Style.Button}
icon={faPlus}
color={"Green"}
onClick={onButtonClick}
/>
</FormInline>
</BoxFull> </BoxFull>
) )
} }

View file

@ -1,18 +1,11 @@
import React, { useContext, useState } from "react" import React, { useContext } from "react"
import BoxFull from "../base/BoxFull" import BoxFull from "../base/BoxFull"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faHashtag, faPlus } from "@fortawesome/free-solid-svg-icons" import { faHashtag } from "@fortawesome/free-solid-svg-icons"
import InputWithIcon from "../base/InputWithIcon"
import FormInline from "../base/FormInline"
import Style from "./BoxConditionHashtag.module.css"
import ButtonIconOnly from "../base/ButtonIconOnly"
import useRepositoryEditor from "../../hooks/useRepositoryEditor" import useRepositoryEditor from "../../hooks/useRepositoryEditor"
import Condition from "../../utils/Condition" import Condition from "../../utils/Condition"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
import FormInlineHashtag from "./FormInlineHashtag"
// Official hashtag regex from https://stackoverflow.com/a/22490853/4334568
// noinspection RegExpAnonymousGroup,LongLine
const INVALID_HASHTAG_CHARACTERS = /([^a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73\u2b740-\u2b81\u2f800-\u2fa1])/g
/** /**
@ -24,22 +17,11 @@ const INVALID_HASHTAG_CHARACTERS = /([^a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\
* @constructor * @constructor
*/ */
export default function BoxConditionHashtag({ ...props }) { export default function BoxConditionHashtag({ ...props }) {
const [hashtag, setHashtag] = useState("")
const { addCondition } = useRepositoryEditor() const { addCondition } = useRepositoryEditor()
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const onInputChange = event => { const submit = value => {
let text = event.target.value addCondition(new Condition("HASHTAG", value))
text = text.replace(INVALID_HASHTAG_CHARACTERS, "")
return setHashtag(text)
}
const onButtonClick = e => {
addCondition(new Condition("HASHTAG", hashtag))
setHashtag("")
// Prevent reloading the page!
e.preventDefault()
} }
return ( return (
@ -55,22 +37,9 @@ export default function BoxConditionHashtag({ ...props }) {
} }
{...props} {...props}
> >
<FormInline onSubmit={onButtonClick}> <FormInlineHashtag
<InputWithIcon submit={submit}
className={Style.Input}
id={"condition-hashtag"}
icon={faHashtag}
value={hashtag}
onChange={onInputChange}
placeholder={"hashtag"}
/> />
<ButtonIconOnly
className={Style.Button}
icon={faPlus}
color={"Green"}
onClick={onButtonClick}
/>
</FormInline>
</BoxFull> </BoxFull>
) )
} }

View file

@ -1,4 +1,4 @@
import React, { useCallback, useContext, useEffect, useState } from "react" import React, { useCallback, useContext } from "react"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faMapPin, faPlus } from "@fortawesome/free-solid-svg-icons" import { faMapPin, faPlus } from "@fortawesome/free-solid-svg-icons"
import ButtonIconOnly from "../base/ButtonIconOnly" import ButtonIconOnly from "../base/ButtonIconOnly"
@ -6,34 +6,8 @@ import useRepositoryEditor from "../../hooks/useRepositoryEditor"
import Condition from "../../utils/Condition" import Condition from "../../utils/Condition"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
import BoxMap from "../base/BoxMap" import BoxMap from "../base/BoxMap"
import useMapView from "../../hooks/useMapView"
import osmZoomLevels from "../../utils/osmZoomLevels"
/**
* https://wiki.openstreetmap.org/wiki/Zoom_levels
*/
const MPIXEL = [
156412,
78206,
39103,
19551,
9776,
4888,
2444,
1222,
610.984,
305.492,
152.746,
76.373,
38.187,
19.093,
9.547,
4.773,
2.387,
1.193,
0.596,
0.298,
0.149,
]
/** /**
@ -44,55 +18,25 @@ const MPIXEL = [
* @constructor * @constructor
*/ */
export default function BoxConditionMap({ ...props }) { export default function BoxConditionMap({ ...props }) {
const [position, setPosition] = useState() const mapViewHook = useMapView()
const [zoom, setZoom] = useState()
const [map, setMap] = useState(null)
const { addCondition } = useRepositoryEditor() const { addCondition } = useRepositoryEditor()
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const onMove = useCallback( const onButtonClick = useCallback(
() => { () => {
setPosition(map.getCenter()) const radius = mapViewHook.zoom * osmZoomLevels[mapViewHook.zoom]
},
[map],
)
const onZoom = useCallback(
() => {
setZoom(map.getZoom())
},
[map],
)
useEffect(
() => {
if(map === null) {
return
}
map.on("move", onMove)
map.on("zoom", onZoom)
return () => {
map.off("move", onMove)
map.off("zoom", onZoom)
}
},
[map, onMove, onZoom],
)
const onButtonClick = () => {
const mapSize = map.getSize()
const minSize = Math.min(mapSize.x, mapSize.y)
const radius = minSize * MPIXEL[zoom]
addCondition(new Condition( addCondition(new Condition(
"COORDINATES", "COORDINATES",
`< ${radius} ${position.lat} ${position.lng}`, `< ${radius} ${mapViewHook.center.lat} ${mapViewHook.center.lng}`,
)) ))
} },
[mapViewHook, addCondition]
)
return ( return (
<BoxMap <BoxMap
mapViewHook={mapViewHook}
header={ header={
<span> <span>
{strings.searchBy} {strings.searchBy}
@ -102,7 +46,6 @@ export default function BoxConditionMap({ ...props }) {
{strings.byZone} {strings.byZone}
</span> </span>
} }
setMap={setMap}
button={ button={
<ButtonIconOnly <ButtonIconOnly
icon={faPlus} icon={faPlus}

View file

@ -1,17 +1,11 @@
import React, { useContext, useState } from "react" import React, { useContext } from "react"
import BoxFull from "../base/BoxFull" import BoxFull from "../base/BoxFull"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faAt, faPlus } from "@fortawesome/free-solid-svg-icons" import { faAt } from "@fortawesome/free-solid-svg-icons"
import InputWithIcon from "../base/InputWithIcon"
import FormInline from "../base/FormInline"
import Style from "./BoxConditionUser.module.css"
import ButtonIconOnly from "../base/ButtonIconOnly"
import useRepositoryEditor from "../../hooks/useRepositoryEditor" import useRepositoryEditor from "../../hooks/useRepositoryEditor"
import Condition from "../../utils/Condition" import Condition from "../../utils/Condition"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
import FormInlineUser from "./FormInlineUser"
const INVALID_USER_CHARACTERS = /[^a-zA-Z0-9]/g
/** /**
@ -23,22 +17,11 @@ const INVALID_USER_CHARACTERS = /[^a-zA-Z0-9]/g
* @constructor * @constructor
*/ */
export default function BoxConditionUser({ ...props }) { export default function BoxConditionUser({ ...props }) {
const [user, setUser] = useState("")
const { addCondition } = useRepositoryEditor() const { addCondition } = useRepositoryEditor()
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const onInputChange = event => { const submit = value => {
let text = event.target.value addCondition(new Condition("USER", value))
text = text.replace(INVALID_USER_CHARACTERS, "")
return setUser(text)
}
const onButtonClick = e => {
addCondition(new Condition("USER", user))
setUser("")
// Prevent reloading the page!
e.preventDefault()
} }
return ( return (
@ -54,22 +37,9 @@ export default function BoxConditionUser({ ...props }) {
} }
{...props} {...props}
> >
<FormInline onSubmit={onButtonClick}> <FormInlineUser
<InputWithIcon submit={submit}
className={Style.Input}
id={"condition-hashtag"}
icon={faAt}
value={user}
onChange={onInputChange}
placeholder={"jack"}
/> />
<ButtonIconOnly
className={Style.Button}
icon={faPlus}
color={"Green"}
onClick={onButtonClick}
/>
</FormInline>
</BoxFull> </BoxFull>
) )
} }

View file

@ -1,7 +0,0 @@
.Input {
flex-shrink: 1;
}
.Button {
}

View file

@ -1,10 +1,11 @@
import React, { useContext, useState } from "react" import React from "react"
import BoxFull from "../base/BoxFull" import BoxFull from "../base/BoxFull"
import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings" import useStrings from "../../hooks/useStrings"
import { ContainsFilter } from "../../utils/Filter" import { ContainsFilter } from "../../utils/Filter"
import FormInlineText from "./FormInlineText" import FormInlineText from "./FormInlineText"
import { faFont } from "@fortawesome/free-solid-svg-icons" import { faFont } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
export default function BoxFilterContains({ ...props }) { export default function BoxFilterContains({ ...props }) {
@ -17,9 +18,19 @@ export default function BoxFilterContains({ ...props }) {
// TODO: add this string // TODO: add this string
return ( return (
<BoxFull header={strings.filterContains} {...props}> <BoxFull
header={
<span>
{strings.searchBy}
&nbsp;
<FontAwesomeIcon icon={faFont}/>
&nbsp;
{strings.byContents}
</span>
}
{...props}
>
<FormInlineText <FormInlineText
textIcon={faFont}
submit={submit} submit={submit}
placeholder={"cat in the box"} placeholder={"cat in the box"}
/> />

View file

@ -0,0 +1,35 @@
import React from "react"
import BoxFull from "../base/BoxFull"
import { faClock, faHashtag } from "@fortawesome/free-solid-svg-icons"
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings"
import { AfterDatetimeFilter } from "../../utils/Filter"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import FormInlineBADatetime from "./FormInlineBADatetime"
export default function BoxFilterDatetime({ ...props }) {
const strings = useStrings()
const { appendFilter } = useRepositoryViewer()
const submit = ({ date, isBefore }) => {
appendFilter(new AfterDatetimeFilter(isBefore, date))
}
return (
<BoxFull
header={
<span>
{strings.searchBy}
&nbsp;
<FontAwesomeIcon icon={faClock}/>
&nbsp;
{strings.byTimePeriod}
</span>
}
{...props}
>
<FormInlineBADatetime submit={submit}/>
</BoxFull>
)
}

View file

@ -0,0 +1,49 @@
import React from "react"
import BoxFull from "../base/BoxFull"
import FormInline from "../base/FormInline"
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faLocationArrow, faMapPin, faPlus } from "@fortawesome/free-solid-svg-icons"
import FormInlineLocation from "./FormInlineLocation"
import { HasPlaceFilter, LocationRadiusFilter } from "../../utils/Filter"
import ButtonIconOnly from "../base/ButtonIconOnly"
export default function BoxFilterHasPlace({ ...props }) {
const strings = useStrings()
const { appendFilter } = useRepositoryViewer()
const submit = () => {
appendFilter(new HasPlaceFilter(false))
}
// TODO: translate this
return (
<BoxFull
header={
<span>
{strings.searchBy}
&nbsp;
<FontAwesomeIcon icon={faLocationArrow}/>
&nbsp;
{strings.byHasPlace}
</span>
}
{...props}
>
<FormInline>
<div style={{"flex-grow": 1}}>
{strings.hasPlaceExplaination}
</div>
<ButtonIconOnly
icon={faPlus}
color={"Green"}
onClick={submit}
/>
</FormInline>
</BoxFull>
)
}

View file

@ -1,43 +1,35 @@
import React, { useContext, useState } from "react" import React from "react"
import BoxFull from "../base/BoxFull" import BoxFull from "../base/BoxFull"
import FormInline from "../base/FormInline" import { faClock } from "@fortawesome/free-solid-svg-icons"
import InputWithIcon from "../base/InputWithIcon"
import Style from "./BoxConditionUser.module.css"
import { faAt, faFilter, faFont, faHashtag } from "@fortawesome/free-solid-svg-icons"
import ButtonIconOnly from "../base/ButtonIconOnly"
import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings" import useStrings from "../../hooks/useStrings"
import { ContainsFilter, HashtagFilter, UserFilter } from "../../utils/Filter" import { HashtagFilter } from "../../utils/Filter"
import Condition from "../../utils/Condition" import FormInlineHashtag from "./FormInlineHashtag"
import FormInlineText from "./FormInlineText" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
const INVALID_HASHTAG_CHARACTERS = /([^a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73\u2b740-\u2b81\u2f800-\u2fa1])/g
export default function BoxFilterHashtag({ ...props }) { export default function BoxFilterHashtag({ ...props }) {
// TODO: Translate this
// TODO: and also use a better string maybe
const strings = useStrings() const strings = useStrings()
const { appendFilter } = useRepositoryViewer() const { appendFilter } = useRepositoryViewer()
const validate = value => {
return value.replace(INVALID_HASHTAG_CHARACTERS, "")
}
const submit = value => { const submit = value => {
appendFilter(new HashtagFilter(false, value)) appendFilter(new HashtagFilter(false, value))
} }
return ( return (
<BoxFull header={strings.filterHashtag} {...props}> <BoxFull
<FormInlineText header={
textIcon={faHashtag} <span>
placeholder={"hashtag"} {strings.searchBy}
validate={validate} &nbsp;
submit={submit} <FontAwesomeIcon icon={faClock}/>
/> &nbsp;
{strings.byTimePeriod}
</span>
}
{...props}
>
<FormInlineHashtag submit={submit}/>
</BoxFull> </BoxFull>
) )
} }

View file

@ -0,0 +1,37 @@
import React from "react"
import BoxFull from "../base/BoxFull"
import FormInline from "../base/FormInline"
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faMapPin } from "@fortawesome/free-solid-svg-icons"
import FormInlineLocation from "./FormInlineLocation"
import { LocationRadiusFilter } from "../../utils/Filter"
export default function BoxFilterLocation({ ...props }) {
const strings = useStrings()
const { appendFilter, mapViewHook } = useRepositoryViewer()
const submit = () => {
appendFilter(new LocationRadiusFilter(false, mapViewHook.center, mapViewHook.radius))
}
return (
<BoxFull
header={
<span>
{strings.searchBy}
&nbsp;
<FontAwesomeIcon icon={faMapPin}/>
&nbsp;
{strings.byZone}
</span>
}
{...props}
>
<FormInlineLocation submit={submit} mapViewHook={mapViewHook}/>
</BoxFull>
)
}

View file

@ -1,15 +1,11 @@
import React, { useContext, useState } from "react" import React from "react"
import BoxFull from "../base/BoxFull" import BoxFull from "../base/BoxFull"
import FormInline from "../base/FormInline" import { faAt } from "@fortawesome/free-solid-svg-icons"
import InputWithIcon from "../base/InputWithIcon"
import Style from "./BoxConditionUser.module.css"
import { faAt, faFilter, faFont } from "@fortawesome/free-solid-svg-icons"
import ButtonIconOnly from "../base/ButtonIconOnly"
import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings" import useStrings from "../../hooks/useStrings"
import { ContainsFilter, UserFilter } from "../../utils/Filter" import { UserFilter } from "../../utils/Filter"
import Condition from "../../utils/Condition" import FormInlineUser from "./FormInlineUser"
import FormInlineText from "./FormInlineText" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
export default function BoxFilterUser({ ...props }) { export default function BoxFilterUser({ ...props }) {
@ -19,20 +15,24 @@ export default function BoxFilterUser({ ...props }) {
const { appendFilter } = useRepositoryViewer() const { appendFilter } = useRepositoryViewer()
const validate = value => {
return value.replace(/[^a-zA-Z0-9]/g, "")
}
const submit = value => { const submit = value => {
appendFilter(new UserFilter(false, value)) appendFilter(new UserFilter(false, value))
} }
return ( return (
<BoxFull header={strings.filterUser} {...props}> <BoxFull
<FormInlineText header={
textIcon={faAt} <span>
placeholder={"jack"} {strings.searchBy}
validate={validate} &nbsp;
<FontAwesomeIcon icon={faAt}/>
&nbsp;
{strings.byUser}
</span>
}
{...props}
>
<FormInlineUser
submit={submit} submit={submit}
/> />
</BoxFull> </BoxFull>

View file

@ -37,9 +37,9 @@ export default function BoxVisualizationChart({ ...props }) {
{ {
label: "Tweets", label: "Tweets",
data: hourlyTweetCount, data: hourlyTweetCount,
} },
], ],
} },
}} }}
{...props} {...props}
/> />

View file

@ -1,4 +1,4 @@
import React, { useContext } from "react" import React, { useContext, useMemo } from "react"
import BoxMap from "../base/BoxMap" import BoxMap from "../base/BoxMap"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
import { Marker, Popup } from "react-leaflet" import { Marker, Popup } from "react-leaflet"
@ -8,10 +8,11 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
export default function BoxVisualizationMap({ ...props }) { export default function BoxVisualizationMap({ ...props }) {
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const {tweets} = useContext(ContextRepositoryViewer) const { tweets, mapViewHook } = useContext(ContextRepositoryViewer)
console.debug(tweets) const markers = useMemo(
const markers = tweets.filter(tweet => tweet.location).map(tweet => { () => {
return tweets.filter(tweet => tweet.location).map(tweet => {
const location = Location.fromTweet(tweet) const location = Location.fromTweet(tweet)
return ( return (
@ -27,9 +28,12 @@ export default function BoxVisualizationMap({ ...props }) {
</Marker> </Marker>
) )
}) })
},
[tweets],
)
return ( return (
<BoxMap header={strings.visualMap} {...props}> <BoxMap header={strings.visualMap} mapViewHook={mapViewHook} {...props}>
{markers} {markers}
</BoxMap> </BoxMap>
) )

View file

@ -47,15 +47,19 @@ export default function BoxVisualizationStats({ ...props }) {
const wordCount = useMemo( const wordCount = useMemo(
() => { () => {
if(words.length === 0) return 0 if(words.length === 0) {
return 0
}
return words.map(word => word.value).reduce((a, b) => a + b) return words.map(word => word.value).reduce((a, b) => a + b)
}, },
[words] [words],
) )
const mostPopularWord = useMemo( const mostPopularWord = useMemo(
() => { () => {
if(words.length === 0) return "❌" if(words.length === 0) {
return "❌"
}
return words.sort((wa, wb) => { return words.sort((wa, wb) => {
if(wa.value > wb.value) { if(wa.value > wb.value) {
return -1 return -1
@ -149,7 +153,11 @@ export default function BoxVisualizationStats({ ...props }) {
<b>{uniqueUsersCount}</b> <b>{uniqueUsersCount}</b>
</FormLabel> </FormLabel>
<FormLabel text={strings.postPop}> <FormLabel text={strings.postPop}>
<b>{mostActiveUser ? `${mostActiveUser.user} (${mostActiveUser.count} tweet${mostActiveUser.count === 1 ? "" : "s"})` : "❌"}</b> <b>{mostActiveUser
? `${mostActiveUser.user} (${mostActiveUser.count} tweet${mostActiveUser.count === 1
? ""
: "s"})`
: "❌"}</b>
</FormLabel> </FormLabel>
</FormLabelled> </FormLabelled>
</BoxFullScrollable> </BoxFullScrollable>

View file

@ -1,17 +1,15 @@
import React, { useContext, useState } from "react" import React, { useContext } from "react"
import Style from "./ButtonToggleBeforeAfter.module.css" import Style from "./ButtonToggleBeforeAfter.module.css"
import classNames from "classnames" import classNames from "classnames"
import Button from "../base/Button" import Button from "../base/Button"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
export default function ButtonToggleBeforeAfter({ onUpdate, className, ...props }) { export default function ButtonToggleBeforeAfter({ isBefore, setBefore, className, ...props }) {
const [value, setValue] = useState(false)
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const onButtonClick = () => { const onButtonClick = () => {
onUpdate(!value) setBefore(a => !a)
setValue(value => !value)
} }
return ( return (
@ -21,7 +19,7 @@ export default function ButtonToggleBeforeAfter({ onUpdate, className, ...props
onClick={onButtonClick} onClick={onButtonClick}
{...props} {...props}
> >
{value ? strings.timeBefore : strings.timeAfter} {isBefore ? strings.timeBefore : strings.timeAfter}
</Button> </Button>
) )
} }

View file

@ -0,0 +1,62 @@
import React, { useState } from "react"
import FormInline from "../base/FormInline"
import InputWithIcon from "../base/InputWithIcon"
import { faClock, faPlus } from "@fortawesome/free-solid-svg-icons"
import ButtonIconOnly from "../base/ButtonIconOnly"
import Style from "./FormInlineText.module.css"
import ButtonToggleBeforeAfter from "./ButtonToggleBeforeAfter"
const INVALID_CHARACTERS = /[^0-9TZ:+-]/g
export default function FormInlineBADatetime(
{
textIcon = faClock,
buttonIcon = faPlus,
buttonColor = "Green",
placeholder = new Date().toISOString(),
validate = value => value,
submit,
...props
},
) {
const [isBefore, setBefore] = useState(false)
const [value, setValue] = useState("")
const _onSubmit = event => {
event.preventDefault()
submit({
date: new Date(value),
isBefore,
})
setValue("")
}
const _onChange = event => {
setValue(validate(event.target.value.replace(INVALID_CHARACTERS, "")))
}
return (
<FormInline onSubmit={_onSubmit} {...props}>
<ButtonToggleBeforeAfter
isBefore={isBefore}
setBefore={setBefore}
/>
<InputWithIcon
className={Style.Input}
type={"datetime-local"}
icon={textIcon}
value={value}
onChange={_onChange}
placeholder={placeholder}
/>
<ButtonIconOnly
className={Style.Button}
icon={buttonIcon}
color={buttonColor}
onClick={_onSubmit}
/>
</FormInline>
)
}

View file

@ -0,0 +1,26 @@
import React from "react"
import FormInlineText from "./FormInlineText"
import { faHashtag } from "@fortawesome/free-solid-svg-icons"
// Official hashtag regex from https://stackoverflow.com/a/22490853/4334568
// noinspection RegExpAnonymousGroup,LongLine
const INVALID_CHARACTERS = /([^a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73\u2b740-\u2b81\u2f800-\u2fa1])/g
export default function FormInlineHashtag({ submit, ...props }) {
const validate = value => {
return value.replace(INVALID_CHARACTERS, "")
}
return (
<FormInlineText
textIcon={faHashtag}
placeholder={"hashtag"}
validate={validate}
submit={submit}
{...props}
/>
)
}

View file

@ -0,0 +1,60 @@
import React from "react"
import FormInline from "../base/FormInline"
import InputWithIcon from "../base/InputWithIcon"
import { faCircle, faPlus, faRulerHorizontal, faRulerVertical } from "@fortawesome/free-solid-svg-icons"
import ButtonIconOnly from "../base/ButtonIconOnly"
import Style from "./FormInlineLocation.module.css"
export default function FormInlineLocation(
{
mapViewHook,
radIcon = faCircle,
latIcon = faRulerHorizontal,
lngIcon = faRulerVertical,
buttonIcon = faPlus,
buttonColor = "Green",
placeholder = new Date().toISOString(),
validate = value => value,
submit,
...props
},
) {
const _onSubmit = event => {
event.preventDefault()
submit()
}
return (
<FormInline onSubmit={_onSubmit} {...props}>
<InputWithIcon
className={Style.Radius}
type={"text"}
icon={radIcon}
value={`${mapViewHook.radius} m`}
disabled={true}
/>
<InputWithIcon
className={Style.Latitude}
type={"text"}
icon={latIcon}
value={mapViewHook.center.lat.toFixed(3)}
disabled={true}
/>
<InputWithIcon
className={Style.Longitude}
type={"text"}
icon={lngIcon}
value={mapViewHook.center.lng.toFixed(3)}
disabled={true}
/>
<ButtonIconOnly
className={Style.Button}
icon={buttonIcon}
color={buttonColor}
onClick={_onSubmit}
/>
</FormInline>
)
}

View file

@ -1,4 +1,4 @@
.Input { .Radius, .Latitude, .Longitude {
flex-shrink: 1; flex-shrink: 1;
} }

View file

@ -1,12 +1,22 @@
import React, { useState } from "react" import React, { useState } from "react"
import FormInline from "../base/FormInline" import FormInline from "../base/FormInline"
import InputWithIcon from "../base/InputWithIcon" import InputWithIcon from "../base/InputWithIcon"
import { faPlus } from "@fortawesome/free-solid-svg-icons" import { faFont, faPlus } from "@fortawesome/free-solid-svg-icons"
import ButtonIconOnly from "../base/ButtonIconOnly" import ButtonIconOnly from "../base/ButtonIconOnly"
import Style from "./FormInlineText.module.css" import Style from "./FormInlineText.module.css"
export default function FormInlineText({ textIcon, placeholder, buttonIcon = faPlus, buttonColor = "Green", validate = value => value, submit, ...props }) { export default function FormInlineText(
{
textIcon = faFont,
buttonIcon = faPlus,
buttonColor = "Green",
placeholder = "",
validate = value => value,
submit,
...props
},
) {
const [value, setValue] = useState("") const [value, setValue] = useState("")
const _onSubmit = event => { const _onSubmit = event => {

View file

@ -0,0 +1,26 @@
import React from "react"
import FormInlineText from "./FormInlineText"
import { faAt } from "@fortawesome/free-solid-svg-icons"
// Official hashtag regex from https://stackoverflow.com/a/22490853/4334568
// noinspection RegExpAnonymousGroup,LongLine
const INVALID_CHARACTERS = /[^a-zA-Z0-9]/g
export default function FormInlineUser({ submit, ...props }) {
const validate = value => {
return value.replace(INVALID_CHARACTERS, "")
}
return (
<FormInlineText
textIcon={faAt}
placeholder={"jack"}
validate={validate}
submit={submit}
{...props}
/>
)
}

View file

@ -1,12 +1,12 @@
import React, { useContext } from "react" import React, { useContext } from "react"
import ButtonIconOnly from "../base/ButtonIconOnly" import ButtonIconOnly from "../base/ButtonIconOnly"
import { faAt, faClock, faFont, faHashtag, faMapPin, faStar } from "@fortawesome/free-solid-svg-icons" import { faAt, faClock, faFont, faHashtag, faLocationArrow, faMapPin } from "@fortawesome/free-solid-svg-icons"
import ButtonPicker from "./ButtonPicker" import ButtonPicker from "./ButtonPicker"
import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
export default function PickerFilter({ ...props }) { export default function PickerFilter({ ...props }) {
const {filterTab, setFilterTab} = useContext(ContextRepositoryViewer) const { filterTab, setFilterTab, setVisualizationTab } = useContext(ContextRepositoryViewer)
return ( return (
<div {...props}> <div {...props}>
@ -37,7 +37,16 @@ export default function PickerFilter({ ...props }) {
<ButtonPicker <ButtonPicker
currentTab={filterTab} currentTab={filterTab}
setTab={setFilterTab} setTab={setFilterTab}
name={"location"} name={"place"}
icon={faLocationArrow}
/>
<ButtonIconOnly
onClick={() => {
setVisualizationTab("map")
setFilterTab("location")
}}
disabled={filterTab === "location"}
color={"Grey"}
icon={faMapPin} icon={faMapPin}
/> />
</div> </div>

View file

@ -23,6 +23,10 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
import BoxFilterContains from "../interactive/BoxFilterContains" import BoxFilterContains from "../interactive/BoxFilterContains"
import BoxFilterUser from "../interactive/BoxFilterUser" import BoxFilterUser from "../interactive/BoxFilterUser"
import BoxFilterHashtag from "../interactive/BoxFilterHashtag" import BoxFilterHashtag from "../interactive/BoxFilterHashtag"
import BoxFilterLocation from "../interactive/BoxFilterLocation"
import useMapView from "../../hooks/useMapView"
import BoxFilterDatetime from "../interactive/BoxFilterDatetime"
import BoxFilterHasPlace from "../interactive/BoxFilterHasPlace"
export default function RepositoryViewer({ id, className, ...props }) { export default function RepositoryViewer({ id, className, ...props }) {
@ -36,9 +40,11 @@ export default function RepositoryViewer({ id, className, ...props }) {
setValue: setFilters, setValue: setFilters,
appendValue: appendFilter, appendValue: appendFilter,
spliceValue: spliceFilter, spliceValue: spliceFilter,
removeValue: removeFilter removeValue: removeFilter,
} = useArrayState([]) } = useArrayState([])
// FIXME: this has a severe performance impact, investigate
const mapViewHook = useMapView()
// Repository // Repository
const repositoryBr = useBackendResource( const repositoryBr = useBackendResource(
@ -117,13 +123,15 @@ export default function RepositoryViewer({ id, className, ...props }) {
{filterTab === "contains" ? <BoxFilterContains className={Style.AddFilter}/> : null} {filterTab === "contains" ? <BoxFilterContains className={Style.AddFilter}/> : null}
{filterTab === "hashtag" ? <BoxFilterHashtag className={Style.AddFilter}/> : null} {filterTab === "hashtag" ? <BoxFilterHashtag className={Style.AddFilter}/> : null}
{filterTab === "user" ? <BoxFilterUser className={Style.AddFilter}/> : null} {filterTab === "user" ? <BoxFilterUser className={Style.AddFilter}/> : null}
{filterTab === "time" ? "Time" : null} {filterTab === "time" ? <BoxFilterDatetime className={Style.AddFilter}/> : null}
{filterTab === "location" ? "Location" : null} {filterTab === "place" ? <BoxFilterHasPlace className={Style.AddFilter}/> : null}
{filterTab === "location" ? <BoxFilterLocation className={Style.AddFilter}/> : null}
</> </>
} }
return ( return (
<ContextRepositoryViewer.Provider value={{ <ContextRepositoryViewer.Provider
value={{
visualizationTab, visualizationTab,
setVisualizationTab, setVisualizationTab,
filterTab, filterTab,
@ -139,7 +147,9 @@ export default function RepositoryViewer({ id, className, ...props }) {
rawTweets, rawTweets,
tweets, tweets,
words, words,
}}> mapViewHook,
}}
>
<div className={classNames(Style.RepositoryViewer, className)} {...props}> <div className={classNames(Style.RepositoryViewer, className)} {...props}>
{contents} {contents}

View file

@ -0,0 +1,18 @@
import { useState } from "react"
import { DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM } from "../utils/defaultMapLocation"
import osmZoomLevels from "../utils/osmZoomLevels"
export default function useMapView() {
const [center, setCenter] = useState(DEFAULT_MAP_CENTER)
const [zoom, setZoom] = useState(DEFAULT_MAP_ZOOM)
const radius = osmZoomLevels[zoom]
return {
center,
setCenter,
zoom,
setZoom,
radius,
}
}

View file

@ -1,4 +1,4 @@
import React, { useContext, useMemo, useState } from "react" import React from "react"
import { useParams } from "react-router" import { useParams } from "react-router"
import RepositoryViewer from "../components/providers/RepositoryViewer" import RepositoryViewer from "../components/providers/RepositoryViewer"
@ -7,6 +7,6 @@ export default function PageRepository({ className, ...props }) {
const { id } = useParams() const { id } = useParams()
return ( return (
<RepositoryViewer id={id}/> <RepositoryViewer id={id} {...props}/>
) )
} }

View file

@ -1,10 +1,11 @@
import { Location } from "./location" import { Location } from "./location"
import { import {
faAt, faAt,
faClock,
faFilter, faFilter,
faFont, faHashtag, faFont,
faHashtag,
faLocationArrow, faLocationArrow,
faMap,
faMapMarkerAlt, faMapMarkerAlt,
faMapPin, faMapPin,
} from "@fortawesome/free-solid-svg-icons" } from "@fortawesome/free-solid-svg-icons"
@ -38,6 +39,7 @@ export class Filter {
} }
} }
export class ContainsFilter extends Filter { export class ContainsFilter extends Filter {
word word
@ -68,7 +70,7 @@ export class HashtagFilter extends ContainsFilter {
hashtag hashtag
constructor(negate, hashtag) { constructor(negate, hashtag) {
super(negate, `#${hashtag}`); super(negate, `#${hashtag}`)
this.hashtag = hashtag this.hashtag = hashtag
} }
@ -109,15 +111,12 @@ export class UserFilter extends Filter {
export class HasLocationFilter extends Filter { export class HasLocationFilter extends Filter {
hasLocation constructor(negate) {
constructor(negate, hasLocation) {
super(negate) super(negate)
this.hasLocation = hasLocation
} }
check(tweet) { check(tweet) {
return (tweet["location"] !== null) === this.hasLocation return Boolean(tweet["location"])
} }
color() { color() {
@ -129,21 +128,18 @@ export class HasLocationFilter extends Filter {
} }
text() { text() {
return this.hasLocation return ""
} }
} }
export class HasPlaceFilter extends Filter { export class HasPlaceFilter extends Filter {
hasPlace constructor(negate) {
constructor(negate, hasPlace) {
super(negate) super(negate)
this.hasPlace = hasPlace
} }
check(tweet) { check(tweet) {
return (tweet["place"] !== null) === this.hasPlace return Boolean(tweet["place"])
} }
color() { color() {
@ -155,7 +151,7 @@ export class HasPlaceFilter extends Filter {
} }
text() { text() {
return this.hasPlace return ""
} }
} }
@ -165,7 +161,7 @@ export class LocationRadiusFilter extends HasLocationFilter {
radius radius
constructor(negate, center, radius) { constructor(negate, center, radius) {
super(negate, true); super(negate)
this.center = center this.center = center
this.radius = radius this.radius = radius
} }
@ -175,12 +171,12 @@ export class LocationRadiusFilter extends HasLocationFilter {
return false return false
} }
// FIXME: assuming the earth is flat // FIXME: Maths is hard
const location = Location.fromTweet(tweet) const location = Location.fromTweet(tweet)
const latDiff = Math.abs(location.lat - this.center.lat) const latDiff = Math.abs(location.lat - this.center.lat)
const lngDiff = Math.abs(location.lng - this.center.lng) const lngDiff = Math.abs(location.lng - this.center.lng)
const squaredDistance = Math.pow(latDiff, 2) + Math.pow(lngDiff, 2) const squaredDistance = Math.pow(latDiff, 2) + Math.pow(lngDiff, 2)
const squaredRadius = Math.pow(radius, 2) const squaredRadius = Math.pow(this.radius, 2)
return squaredDistance < squaredRadius return squaredDistance < squaredRadius
} }
@ -194,6 +190,32 @@ export class LocationRadiusFilter extends HasLocationFilter {
} }
text() { text() {
return `< ${this.radius} ${this.center.toString()}` return `< ${this.radius}m ${this.center.lat.toFixed(3)} ${this.center.lng.toFixed(3)}`
}
}
export class AfterDatetimeFilter extends Filter {
datetime
constructor(negate, datetime) {
super(negate)
this.datetime = datetime
}
check(tweet) {
return this.datetime < new Date(tweet["insert_time"])
}
color() {
return "Yellow"
}
icon() {
return faClock
}
text() {
return `${this.negate ? "<" : ">"} ${this.datetime.toISOString()}`
} }
} }

View file

@ -0,0 +1,2 @@
export const DEFAULT_MAP_CENTER = { lat: 0, lng: 0 }
export const DEFAULT_MAP_ZOOM = 3

View file

@ -0,0 +1,26 @@
/**
* https://wiki.openstreetmap.org/wiki/Zoom_levels
*/
export default [
62914560,
31457280,
15728640,
7864320,
3932160,
1966080,
983040,
491520,
245760,
122880,
61440,
30720,
15360,
7680,
3840,
1920,
960,
480,
240,
120,
60,
]