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:
parent
237417298d
commit
c7d425960d
37 changed files with 638 additions and 361 deletions
|
@ -3,7 +3,7 @@ import BoxFull from "./BoxFull"
|
||||||
import ChartComponent from "react-chartjs-2"
|
import ChartComponent from "react-chartjs-2"
|
||||||
|
|
||||||
|
|
||||||
export default function BoxChart({chartProps, ...props}) {
|
export default function BoxChart({ chartProps, ...props }) {
|
||||||
const getCssVar = (variable) => {
|
const getCssVar = (variable) => {
|
||||||
const computedStyle = window.getComputedStyle(document.querySelector("main"))
|
const computedStyle = window.getComputedStyle(document.querySelector("main"))
|
||||||
return computedStyle.getPropertyValue(variable).trim()
|
return computedStyle.getPropertyValue(variable).trim()
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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
|
}) {
|
||||||
}) {
|
const [map, setMap] = useState(null)
|
||||||
return (
|
|
||||||
<BoxFull
|
const onMapMove = useCallback(
|
||||||
childrenClassName={Style.BoxMapContents}
|
() => mapViewHook.setCenter(map.getCenter()),
|
||||||
{...props}
|
[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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ export default function BoxWordcloud({ words, callbacks = {}, ...props }) {
|
||||||
callbacks={callbacks}
|
callbacks={callbacks}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[words]
|
[words],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
@ -13,7 +11,7 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function BadgeFilter({ filter }) {
|
export default function BadgeFilter({ filter }) {
|
||||||
const {removeFilter} = useContext(ContextRepositoryViewer)
|
const { removeFilter } = useContext(ContextRepositoryViewer)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
addCondition(new Condition(
|
||||||
|
"COORDINATES",
|
||||||
|
`< ${radius} ${mapViewHook.center.lat} ${mapViewHook.center.lng}`,
|
||||||
|
))
|
||||||
},
|
},
|
||||||
[map],
|
[mapViewHook, addCondition]
|
||||||
)
|
)
|
||||||
|
|
||||||
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(
|
|
||||||
"COORDINATES",
|
|
||||||
`< ${radius} ${position.lat} ${position.lng}`,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
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}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
.Input {
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Button {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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}
|
||||||
|
|
||||||
|
<FontAwesomeIcon icon={faFont}/>
|
||||||
|
|
||||||
|
{strings.byContents}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
<FormInlineText
|
<FormInlineText
|
||||||
textIcon={faFont}
|
|
||||||
submit={submit}
|
submit={submit}
|
||||||
placeholder={"cat in the box"}
|
placeholder={"cat in the box"}
|
||||||
/>
|
/>
|
||||||
|
|
35
nest_frontend/components/interactive/BoxFilterDatetime.js
Normal file
35
nest_frontend/components/interactive/BoxFilterDatetime.js
Normal 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}
|
||||||
|
|
||||||
|
<FontAwesomeIcon icon={faClock}/>
|
||||||
|
|
||||||
|
{strings.byTimePeriod}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<FormInlineBADatetime submit={submit}/>
|
||||||
|
</BoxFull>
|
||||||
|
)
|
||||||
|
}
|
49
nest_frontend/components/interactive/BoxFilterHasPlace.js
Normal file
49
nest_frontend/components/interactive/BoxFilterHasPlace.js
Normal 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}
|
||||||
|
|
||||||
|
<FontAwesomeIcon icon={faLocationArrow}/>
|
||||||
|
|
||||||
|
{strings.byHasPlace}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<FormInline>
|
||||||
|
<div style={{"flex-grow": 1}}>
|
||||||
|
{strings.hasPlaceExplaination}
|
||||||
|
</div>
|
||||||
|
<ButtonIconOnly
|
||||||
|
icon={faPlus}
|
||||||
|
color={"Green"}
|
||||||
|
onClick={submit}
|
||||||
|
/>
|
||||||
|
</FormInline>
|
||||||
|
</BoxFull>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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}
|
|
||||||
submit={submit}
|
<FontAwesomeIcon icon={faClock}/>
|
||||||
/>
|
|
||||||
|
{strings.byTimePeriod}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<FormInlineHashtag submit={submit}/>
|
||||||
</BoxFull>
|
</BoxFull>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
37
nest_frontend/components/interactive/BoxFilterLocation.js
Normal file
37
nest_frontend/components/interactive/BoxFilterLocation.js
Normal 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}
|
||||||
|
|
||||||
|
<FontAwesomeIcon icon={faMapPin}/>
|
||||||
|
|
||||||
|
{strings.byZone}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<FormInlineLocation submit={submit} mapViewHook={mapViewHook}/>
|
||||||
|
</BoxFull>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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}
|
|
||||||
|
<FontAwesomeIcon icon={faAt}/>
|
||||||
|
|
||||||
|
{strings.byUser}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<FormInlineUser
|
||||||
submit={submit}
|
submit={submit}
|
||||||
/>
|
/>
|
||||||
</BoxFull>
|
</BoxFull>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import BadgeFilter from "./BadgeFilter"
|
||||||
*/
|
*/
|
||||||
export default function BoxFilters({ ...props }) {
|
export default function BoxFilters({ ...props }) {
|
||||||
const { strings } = useContext(ContextLanguage)
|
const { strings } = useContext(ContextLanguage)
|
||||||
const {filters} = useContext(ContextRepositoryViewer)
|
const { filters } = useContext(ContextRepositoryViewer)
|
||||||
|
|
||||||
const badges = filters.map((filter, pos) => <BadgeFilter key={pos} filter={filter}/>)
|
const badges = filters.map((filter, pos) => <BadgeFilter key={pos} filter={filter}/>)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
||||||
|
|
||||||
export default function BoxRepositoryTweets({ ...props }) {
|
export default function BoxRepositoryTweets({ ...props }) {
|
||||||
const { strings } = useContext(ContextLanguage)
|
const { strings } = useContext(ContextLanguage)
|
||||||
const {tweets} = useContext(ContextRepositoryViewer)
|
const { tweets } = useContext(ContextRepositoryViewer)
|
||||||
|
|
||||||
let content
|
let content
|
||||||
if(tweets.length === 0) {
|
if(tweets.length === 0) {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
||||||
|
|
||||||
export default function BoxVisualizationChart({ ...props }) {
|
export default function BoxVisualizationChart({ ...props }) {
|
||||||
const { strings } = useContext(ContextLanguage)
|
const { strings } = useContext(ContextLanguage)
|
||||||
const {tweets} = useContext(ContextRepositoryViewer)
|
const { tweets } = useContext(ContextRepositoryViewer)
|
||||||
|
|
||||||
const hours = [...Array(24).keys()].map(hour => hour.toString())
|
const hours = [...Array(24).keys()].map(hour => hour.toString())
|
||||||
const hourlyTweetCount = Array(24).fill(0)
|
const hourlyTweetCount = Array(24).fill(0)
|
||||||
|
@ -37,9 +37,9 @@ export default function BoxVisualizationChart({ ...props }) {
|
||||||
{
|
{
|
||||||
label: "Tweets",
|
label: "Tweets",
|
||||||
data: hourlyTweetCount,
|
data: hourlyTweetCount,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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,28 +8,32 @@ 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 => {
|
() => {
|
||||||
const location = Location.fromTweet(tweet)
|
return tweets.filter(tweet => tweet.location).map(tweet => {
|
||||||
|
const location = Location.fromTweet(tweet)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker key={tweet["snowflake"]} position={location.toArray()}>
|
<Marker key={tweet["snowflake"]} position={location.toArray()}>
|
||||||
<Popup>
|
<Popup>
|
||||||
<p>
|
<p>
|
||||||
{tweet["content"]}
|
{tweet["content"]}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
— <a href={`https://twitter.com/${tweet["poster"]}/status/${tweet["snowflake"]}`}>@{tweet["poster"]}</a>
|
— <a href={`https://twitter.com/${tweet["poster"]}/status/${tweet["snowflake"]}`}>@{tweet["poster"]}</a>
|
||||||
</p>
|
</p>
|
||||||
</Popup>
|
</Popup>
|
||||||
</Marker>
|
</Marker>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
[tweets],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxMap header={strings.visualMap} {...props}>
|
<BoxMap header={strings.visualMap} mapViewHook={mapViewHook} {...props}>
|
||||||
{markers}
|
{markers}
|
||||||
</BoxMap>
|
</BoxMap>
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,7 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
||||||
|
|
||||||
export default function BoxVisualizationStats({ ...props }) {
|
export default function BoxVisualizationStats({ ...props }) {
|
||||||
const { strings } = useContext(ContextLanguage)
|
const { strings } = useContext(ContextLanguage)
|
||||||
const {tweets, words, rawTweets} = useContext(ContextRepositoryViewer)
|
const { tweets, words, rawTweets } = useContext(ContextRepositoryViewer)
|
||||||
|
|
||||||
const tweetCount = useMemo(
|
const tweetCount = useMemo(
|
||||||
() => tweets.length,
|
() => tweets.length,
|
||||||
|
@ -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>
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { ContainsFilter } from "../../utils/Filter"
|
||||||
|
|
||||||
export default function BoxVisualizationWordcloud({ ...props }) {
|
export default function BoxVisualizationWordcloud({ ...props }) {
|
||||||
const { strings } = useContext(ContextLanguage)
|
const { strings } = useContext(ContextLanguage)
|
||||||
const {words, appendFilter} = useContext(ContextRepositoryViewer)
|
const { words, appendFilter } = useContext(ContextRepositoryViewer)
|
||||||
|
|
||||||
if(words.length === 0) {
|
if(words.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
@ -27,7 +27,7 @@ export default function BoxVisualizationWordcloud({ ...props }) {
|
||||||
<BoxWordcloud
|
<BoxWordcloud
|
||||||
header={strings.wordcloud}
|
header={strings.wordcloud}
|
||||||
words={words}
|
words={words}
|
||||||
callbacks={{onWordClick}}
|
callbacks={{ onWordClick }}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
62
nest_frontend/components/interactive/FormInlineBADatetime.js
Normal file
62
nest_frontend/components/interactive/FormInlineBADatetime.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
26
nest_frontend/components/interactive/FormInlineHashtag.js
Normal file
26
nest_frontend/components/interactive/FormInlineHashtag.js
Normal 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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
60
nest_frontend/components/interactive/FormInlineLocation.js
Normal file
60
nest_frontend/components/interactive/FormInlineLocation.js
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
.Input {
|
.Radius, .Latitude, .Longitude {
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
26
nest_frontend/components/interactive/FormInlineUser.js
Normal file
26
nest_frontend/components/interactive/FormInlineUser.js
Normal 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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -4,8 +4,8 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
||||||
import ButtonPicker from "./ButtonPicker"
|
import ButtonPicker from "./ButtonPicker"
|
||||||
|
|
||||||
|
|
||||||
export default function PickerVisualization({...props}) {
|
export default function PickerVisualization({ ...props }) {
|
||||||
const {visualizationTab, setVisualizationTab} = useContext(ContextRepositoryViewer)
|
const { visualizationTab, setVisualizationTab } = useContext(ContextRepositoryViewer)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
|
|
|
@ -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,29 +123,33 @@ 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
|
||||||
visualizationTab,
|
value={{
|
||||||
setVisualizationTab,
|
visualizationTab,
|
||||||
filterTab,
|
setVisualizationTab,
|
||||||
setFilterTab,
|
filterTab,
|
||||||
filters,
|
setFilterTab,
|
||||||
setFilters,
|
filters,
|
||||||
appendFilter,
|
setFilters,
|
||||||
spliceFilter,
|
appendFilter,
|
||||||
removeFilter,
|
spliceFilter,
|
||||||
repositoryBr,
|
removeFilter,
|
||||||
repository,
|
repositoryBr,
|
||||||
rawTweetsBv,
|
repository,
|
||||||
rawTweets,
|
rawTweetsBv,
|
||||||
tweets,
|
rawTweets,
|
||||||
words,
|
tweets,
|
||||||
}}>
|
words,
|
||||||
|
mapViewHook,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
<div className={classNames(Style.RepositoryViewer, className)} {...props}>
|
<div className={classNames(Style.RepositoryViewer, className)} {...props}>
|
||||||
{contents}
|
{contents}
|
||||||
|
|
18
nest_frontend/hooks/useMapView.js
Normal file
18
nest_frontend/hooks/useMapView.js
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()}`
|
||||||
}
|
}
|
||||||
}
|
}
|
2
nest_frontend/utils/defaultMapLocation.js
Normal file
2
nest_frontend/utils/defaultMapLocation.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export const DEFAULT_MAP_CENTER = { lat: 0, lng: 0 }
|
||||||
|
export const DEFAULT_MAP_ZOOM = 3
|
|
@ -15,7 +15,7 @@ export class Location {
|
||||||
if(!match) {
|
if(!match) {
|
||||||
throw new Error(`Invalid location string: ${locString}`)
|
throw new Error(`Invalid location string: ${locString}`)
|
||||||
}
|
}
|
||||||
const {lat, lng} = match.groups
|
const { lat, lng } = match.groups
|
||||||
return new Location(lat, lng)
|
return new Location(lat, lng)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
26
nest_frontend/utils/osmZoomLevels.js
Normal file
26
nest_frontend/utils/osmZoomLevels.js
Normal 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,
|
||||||
|
]
|
Loading…
Reference in a new issue