mirror of
https://github.com/pds-nest/nest.git
synced 2024-11-22 04:54:18 +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"
|
||||
|
||||
|
||||
export default function BoxChart({chartProps, ...props}) {
|
||||
export default function BoxChart({ chartProps, ...props }) {
|
||||
const getCssVar = (variable) => {
|
||||
const computedStyle = window.getComputedStyle(document.querySelector("main"))
|
||||
return computedStyle.getPropertyValue(variable).trim()
|
||||
|
@ -23,7 +23,7 @@ export default function BoxChart({chartProps, ...props}) {
|
|||
},
|
||||
ticks: {
|
||||
color: getCssVar("--fg-primary"),
|
||||
}
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
|
@ -33,7 +33,7 @@ export default function BoxChart({chartProps, ...props}) {
|
|||
},
|
||||
ticks: {
|
||||
color: getCssVar("--fg-primary"),
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
|
@ -46,8 +46,8 @@ export default function BoxChart({chartProps, ...props}) {
|
|||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
{...chartProps}
|
||||
/>
|
||||
|
|
|
@ -1,25 +1,33 @@
|
|||
import React from "react"
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import Style from "./BoxMap.module.css"
|
||||
import BoxFull from "./BoxFull"
|
||||
import { MapContainer, TileLayer } from "react-leaflet"
|
||||
|
||||
|
||||
export default function BoxMap({
|
||||
setMap,
|
||||
startingPosition = { lat: 41.89309, lng: 12.48289 },
|
||||
startingZoom = 3,
|
||||
button,
|
||||
children,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<BoxFull
|
||||
childrenClassName={Style.BoxMapContents}
|
||||
{...props}
|
||||
>
|
||||
export default function BoxMap(
|
||||
{
|
||||
mapViewHook,
|
||||
button,
|
||||
children,
|
||||
...props
|
||||
}) {
|
||||
const [map, setMap] = useState(null)
|
||||
|
||||
const onMapMove = useCallback(
|
||||
() => mapViewHook.setCenter(map.getCenter()),
|
||||
[mapViewHook, map],
|
||||
)
|
||||
|
||||
const onMapZoom = useCallback(
|
||||
() => mapViewHook.setZoom(map.getZoom()),
|
||||
[mapViewHook, map],
|
||||
)
|
||||
|
||||
const mapContainer = useMemo(
|
||||
() => (
|
||||
<MapContainer
|
||||
center={startingPosition}
|
||||
zoom={startingZoom}
|
||||
center={[mapViewHook.center.lat, mapViewHook.center.lng]}
|
||||
zoom={mapViewHook.zoom}
|
||||
className={Style.MapContainer}
|
||||
whenCreated={setMap}
|
||||
>
|
||||
|
@ -34,6 +42,33 @@ export default function BoxMap({
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ export default function BoxWordcloud({ words, callbacks = {}, ...props }) {
|
|||
callbacks={callbacks}
|
||||
/>
|
||||
),
|
||||
[words]
|
||||
[words],
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
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 ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
||||
|
||||
|
@ -13,7 +11,7 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
|||
* @constructor
|
||||
*/
|
||||
export default function BadgeFilter({ filter }) {
|
||||
const {removeFilter} = useContext(ContextRepositoryViewer)
|
||||
const { removeFilter } = useContext(ContextRepositoryViewer)
|
||||
|
||||
return (
|
||||
<Badge
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
import React, { useContext, useState } from "react"
|
||||
import React, { useContext } from "react"
|
||||
import BoxFull from "../base/BoxFull"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faClock, faPlus } 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 { faClock } from "@fortawesome/free-solid-svg-icons"
|
||||
import useRepositoryEditor from "../../hooks/useRepositoryEditor"
|
||||
import ButtonToggleBeforeAfter from "./ButtonToggleBeforeAfter"
|
||||
import Condition from "../../utils/Condition"
|
||||
import convertToLocalISODate from "../../utils/convertToLocalISODate"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
|
||||
|
||||
const INVALID_USER_CHARACTERS = /[^0-9TZ:+-]/g
|
||||
import FormInlineBADatetime from "./FormInlineBADatetime"
|
||||
|
||||
|
||||
/**
|
||||
|
@ -25,30 +18,16 @@ const INVALID_USER_CHARACTERS = /[^0-9TZ:+-]/g
|
|||
* @constructor
|
||||
*/
|
||||
export default function BoxConditionDatetime({ ...props }) {
|
||||
const [datetime, setDatetime] = useState("")
|
||||
const [ba, setBa] = useState(false)
|
||||
const { addCondition } = useRepositoryEditor()
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
|
||||
const onInputChange = event => {
|
||||
let text = event.target.value
|
||||
text = text.toUpperCase()
|
||||
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.")
|
||||
const submit = ({ date, isBefore }) => {
|
||||
if(date.toString() === "Invalid Date") {
|
||||
console.debug("Refusing to add condition: ", date, " is an Invalid Date.")
|
||||
return
|
||||
}
|
||||
const aware = convertToLocalISODate(naive)
|
||||
addCondition(new Condition("TIME", `${ba ? ">" : "<"} ${aware}`))
|
||||
setDatetime("")
|
||||
|
||||
// Prevent reloading the page!
|
||||
e.preventDefault()
|
||||
const aware = convertToLocalISODate(date)
|
||||
addCondition(new Condition("TIME", `${isBefore ? "<" : ">"} ${aware}`))
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -64,24 +43,9 @@ export default function BoxConditionDatetime({ ...props }) {
|
|||
}
|
||||
{...props}
|
||||
>
|
||||
<FormInline onSubmit={onButtonClick}>
|
||||
<ButtonToggleBeforeAfter onUpdate={setBa}/>
|
||||
<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>
|
||||
<FormInlineBADatetime
|
||||
submit={submit}
|
||||
/>
|
||||
</BoxFull>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,11 @@
|
|||
import React, { useContext, useState } from "react"
|
||||
import React, { useContext } from "react"
|
||||
import BoxFull from "../base/BoxFull"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faHashtag, faPlus } 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 { faHashtag } from "@fortawesome/free-solid-svg-icons"
|
||||
import useRepositoryEditor from "../../hooks/useRepositoryEditor"
|
||||
import Condition from "../../utils/Condition"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
|
||||
// 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
|
||||
import FormInlineHashtag from "./FormInlineHashtag"
|
||||
|
||||
|
||||
/**
|
||||
|
@ -24,22 +17,11 @@ const INVALID_HASHTAG_CHARACTERS = /([^a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\
|
|||
* @constructor
|
||||
*/
|
||||
export default function BoxConditionHashtag({ ...props }) {
|
||||
const [hashtag, setHashtag] = useState("")
|
||||
const { addCondition } = useRepositoryEditor()
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
|
||||
const onInputChange = event => {
|
||||
let text = event.target.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()
|
||||
const submit = value => {
|
||||
addCondition(new Condition("HASHTAG", value))
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -55,22 +37,9 @@ export default function BoxConditionHashtag({ ...props }) {
|
|||
}
|
||||
{...props}
|
||||
>
|
||||
<FormInline onSubmit={onButtonClick}>
|
||||
<InputWithIcon
|
||||
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>
|
||||
<FormInlineHashtag
|
||||
submit={submit}
|
||||
/>
|
||||
</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 { faMapPin, faPlus } from "@fortawesome/free-solid-svg-icons"
|
||||
import ButtonIconOnly from "../base/ButtonIconOnly"
|
||||
|
@ -6,34 +6,8 @@ import useRepositoryEditor from "../../hooks/useRepositoryEditor"
|
|||
import Condition from "../../utils/Condition"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
import BoxMap from "../base/BoxMap"
|
||||
|
||||
|
||||
/**
|
||||
* 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,
|
||||
]
|
||||
import useMapView from "../../hooks/useMapView"
|
||||
import osmZoomLevels from "../../utils/osmZoomLevels"
|
||||
|
||||
|
||||
/**
|
||||
|
@ -44,55 +18,25 @@ const MPIXEL = [
|
|||
* @constructor
|
||||
*/
|
||||
export default function BoxConditionMap({ ...props }) {
|
||||
const [position, setPosition] = useState()
|
||||
const [zoom, setZoom] = useState()
|
||||
const [map, setMap] = useState(null)
|
||||
const mapViewHook = useMapView()
|
||||
const { addCondition } = useRepositoryEditor()
|
||||
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 (
|
||||
<BoxMap
|
||||
mapViewHook={mapViewHook}
|
||||
header={
|
||||
<span>
|
||||
{strings.searchBy}
|
||||
|
@ -102,7 +46,6 @@ export default function BoxConditionMap({ ...props }) {
|
|||
{strings.byZone}
|
||||
</span>
|
||||
}
|
||||
setMap={setMap}
|
||||
button={
|
||||
<ButtonIconOnly
|
||||
icon={faPlus}
|
||||
|
|
|
@ -1,17 +1,11 @@
|
|||
import React, { useContext, useState } from "react"
|
||||
import React, { useContext } from "react"
|
||||
import BoxFull from "../base/BoxFull"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faAt, faPlus } 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 { faAt } from "@fortawesome/free-solid-svg-icons"
|
||||
import useRepositoryEditor from "../../hooks/useRepositoryEditor"
|
||||
import Condition from "../../utils/Condition"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
|
||||
|
||||
const INVALID_USER_CHARACTERS = /[^a-zA-Z0-9]/g
|
||||
import FormInlineUser from "./FormInlineUser"
|
||||
|
||||
|
||||
/**
|
||||
|
@ -23,22 +17,11 @@ const INVALID_USER_CHARACTERS = /[^a-zA-Z0-9]/g
|
|||
* @constructor
|
||||
*/
|
||||
export default function BoxConditionUser({ ...props }) {
|
||||
const [user, setUser] = useState("")
|
||||
const { addCondition } = useRepositoryEditor()
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
|
||||
const onInputChange = event => {
|
||||
let text = event.target.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()
|
||||
const submit = value => {
|
||||
addCondition(new Condition("USER", value))
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -54,22 +37,9 @@ export default function BoxConditionUser({ ...props }) {
|
|||
}
|
||||
{...props}
|
||||
>
|
||||
<FormInline onSubmit={onButtonClick}>
|
||||
<InputWithIcon
|
||||
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>
|
||||
<FormInlineUser
|
||||
submit={submit}
|
||||
/>
|
||||
</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 useRepositoryViewer from "../../hooks/useRepositoryViewer"
|
||||
import useStrings from "../../hooks/useStrings"
|
||||
import { ContainsFilter } from "../../utils/Filter"
|
||||
import FormInlineText from "./FormInlineText"
|
||||
import { faFont } from "@fortawesome/free-solid-svg-icons"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
|
||||
|
||||
export default function BoxFilterContains({ ...props }) {
|
||||
|
@ -17,9 +18,19 @@ export default function BoxFilterContains({ ...props }) {
|
|||
|
||||
// TODO: add this string
|
||||
return (
|
||||
<BoxFull header={strings.filterContains} {...props}>
|
||||
<BoxFull
|
||||
header={
|
||||
<span>
|
||||
{strings.searchBy}
|
||||
|
||||
<FontAwesomeIcon icon={faFont}/>
|
||||
|
||||
{strings.byContents}
|
||||
</span>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<FormInlineText
|
||||
textIcon={faFont}
|
||||
submit={submit}
|
||||
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 FormInline from "../base/FormInline"
|
||||
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 { faClock } from "@fortawesome/free-solid-svg-icons"
|
||||
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
|
||||
import useStrings from "../../hooks/useStrings"
|
||||
import { ContainsFilter, HashtagFilter, UserFilter } from "../../utils/Filter"
|
||||
import Condition from "../../utils/Condition"
|
||||
import FormInlineText from "./FormInlineText"
|
||||
|
||||
|
||||
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
|
||||
import { HashtagFilter } from "../../utils/Filter"
|
||||
import FormInlineHashtag from "./FormInlineHashtag"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
|
||||
|
||||
export default function BoxFilterHashtag({ ...props }) {
|
||||
// TODO: Translate this
|
||||
// TODO: and also use a better string maybe
|
||||
const strings = useStrings()
|
||||
|
||||
const { appendFilter } = useRepositoryViewer()
|
||||
|
||||
const validate = value => {
|
||||
return value.replace(INVALID_HASHTAG_CHARACTERS, "")
|
||||
}
|
||||
|
||||
const submit = value => {
|
||||
appendFilter(new HashtagFilter(false, value))
|
||||
}
|
||||
|
||||
return (
|
||||
<BoxFull header={strings.filterHashtag} {...props}>
|
||||
<FormInlineText
|
||||
textIcon={faHashtag}
|
||||
placeholder={"hashtag"}
|
||||
validate={validate}
|
||||
submit={submit}
|
||||
/>
|
||||
<BoxFull
|
||||
header={
|
||||
<span>
|
||||
{strings.searchBy}
|
||||
|
||||
<FontAwesomeIcon icon={faClock}/>
|
||||
|
||||
{strings.byTimePeriod}
|
||||
</span>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<FormInlineHashtag submit={submit}/>
|
||||
</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 FormInline from "../base/FormInline"
|
||||
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 { faAt } from "@fortawesome/free-solid-svg-icons"
|
||||
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
|
||||
import useStrings from "../../hooks/useStrings"
|
||||
import { ContainsFilter, UserFilter } from "../../utils/Filter"
|
||||
import Condition from "../../utils/Condition"
|
||||
import FormInlineText from "./FormInlineText"
|
||||
import { UserFilter } from "../../utils/Filter"
|
||||
import FormInlineUser from "./FormInlineUser"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
|
||||
|
||||
export default function BoxFilterUser({ ...props }) {
|
||||
|
@ -19,20 +15,24 @@ export default function BoxFilterUser({ ...props }) {
|
|||
|
||||
const { appendFilter } = useRepositoryViewer()
|
||||
|
||||
const validate = value => {
|
||||
return value.replace(/[^a-zA-Z0-9]/g, "")
|
||||
}
|
||||
|
||||
const submit = value => {
|
||||
appendFilter(new UserFilter(false, value))
|
||||
}
|
||||
|
||||
return (
|
||||
<BoxFull header={strings.filterUser} {...props}>
|
||||
<FormInlineText
|
||||
textIcon={faAt}
|
||||
placeholder={"jack"}
|
||||
validate={validate}
|
||||
<BoxFull
|
||||
header={
|
||||
<span>
|
||||
{strings.searchBy}
|
||||
|
||||
<FontAwesomeIcon icon={faAt}/>
|
||||
|
||||
{strings.byUser}
|
||||
</span>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<FormInlineUser
|
||||
submit={submit}
|
||||
/>
|
||||
</BoxFull>
|
||||
|
|
|
@ -14,7 +14,7 @@ import BadgeFilter from "./BadgeFilter"
|
|||
*/
|
||||
export default function BoxFilters({ ...props }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
const {filters} = useContext(ContextRepositoryViewer)
|
||||
const { filters } = useContext(ContextRepositoryViewer)
|
||||
|
||||
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 }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
const {tweets} = useContext(ContextRepositoryViewer)
|
||||
const { tweets } = useContext(ContextRepositoryViewer)
|
||||
|
||||
let content
|
||||
if(tweets.length === 0) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
|||
|
||||
export default function BoxVisualizationChart({ ...props }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
const {tweets} = useContext(ContextRepositoryViewer)
|
||||
const { tweets } = useContext(ContextRepositoryViewer)
|
||||
|
||||
const hours = [...Array(24).keys()].map(hour => hour.toString())
|
||||
const hourlyTweetCount = Array(24).fill(0)
|
||||
|
@ -37,9 +37,9 @@ export default function BoxVisualizationChart({ ...props }) {
|
|||
{
|
||||
label: "Tweets",
|
||||
data: hourlyTweetCount,
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext } from "react"
|
||||
import React, { useContext, useMemo } from "react"
|
||||
import BoxMap from "../base/BoxMap"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
import { Marker, Popup } from "react-leaflet"
|
||||
|
@ -8,28 +8,32 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
|||
|
||||
export default function BoxVisualizationMap({ ...props }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
const {tweets} = useContext(ContextRepositoryViewer)
|
||||
const { tweets, mapViewHook } = useContext(ContextRepositoryViewer)
|
||||
|
||||
console.debug(tweets)
|
||||
const markers = tweets.filter(tweet => tweet.location).map(tweet => {
|
||||
const location = Location.fromTweet(tweet)
|
||||
const markers = useMemo(
|
||||
() => {
|
||||
return tweets.filter(tweet => tweet.location).map(tweet => {
|
||||
const location = Location.fromTweet(tweet)
|
||||
|
||||
return (
|
||||
<Marker key={tweet["snowflake"]} position={location.toArray()}>
|
||||
<Popup>
|
||||
<p>
|
||||
{tweet["content"]}
|
||||
</p>
|
||||
<p>
|
||||
— <a href={`https://twitter.com/${tweet["poster"]}/status/${tweet["snowflake"]}`}>@{tweet["poster"]}</a>
|
||||
</p>
|
||||
</Popup>
|
||||
</Marker>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<Marker key={tweet["snowflake"]} position={location.toArray()}>
|
||||
<Popup>
|
||||
<p>
|
||||
{tweet["content"]}
|
||||
</p>
|
||||
<p>
|
||||
— <a href={`https://twitter.com/${tweet["poster"]}/status/${tweet["snowflake"]}`}>@{tweet["poster"]}</a>
|
||||
</p>
|
||||
</Popup>
|
||||
</Marker>
|
||||
)
|
||||
})
|
||||
},
|
||||
[tweets],
|
||||
)
|
||||
|
||||
return (
|
||||
<BoxMap header={strings.visualMap} {...props}>
|
||||
<BoxMap header={strings.visualMap} mapViewHook={mapViewHook} {...props}>
|
||||
{markers}
|
||||
</BoxMap>
|
||||
)
|
||||
|
|
|
@ -8,7 +8,7 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
|||
|
||||
export default function BoxVisualizationStats({ ...props }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
const {tweets, words, rawTweets} = useContext(ContextRepositoryViewer)
|
||||
const { tweets, words, rawTweets } = useContext(ContextRepositoryViewer)
|
||||
|
||||
const tweetCount = useMemo(
|
||||
() => tweets.length,
|
||||
|
@ -47,15 +47,19 @@ export default function BoxVisualizationStats({ ...props }) {
|
|||
|
||||
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)
|
||||
},
|
||||
[words]
|
||||
[words],
|
||||
)
|
||||
|
||||
const mostPopularWord = useMemo(
|
||||
() => {
|
||||
if(words.length === 0) return "❌"
|
||||
if(words.length === 0) {
|
||||
return "❌"
|
||||
}
|
||||
return words.sort((wa, wb) => {
|
||||
if(wa.value > wb.value) {
|
||||
return -1
|
||||
|
@ -149,7 +153,11 @@ export default function BoxVisualizationStats({ ...props }) {
|
|||
<b>{uniqueUsersCount}</b>
|
||||
</FormLabel>
|
||||
<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>
|
||||
</FormLabelled>
|
||||
</BoxFullScrollable>
|
||||
|
|
|
@ -9,7 +9,7 @@ import { ContainsFilter } from "../../utils/Filter"
|
|||
|
||||
export default function BoxVisualizationWordcloud({ ...props }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
const {words, appendFilter} = useContext(ContextRepositoryViewer)
|
||||
const { words, appendFilter } = useContext(ContextRepositoryViewer)
|
||||
|
||||
if(words.length === 0) {
|
||||
return (
|
||||
|
@ -27,7 +27,7 @@ export default function BoxVisualizationWordcloud({ ...props }) {
|
|||
<BoxWordcloud
|
||||
header={strings.wordcloud}
|
||||
words={words}
|
||||
callbacks={{onWordClick}}
|
||||
callbacks={{ onWordClick }}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import React, { useContext, useState } from "react"
|
||||
import React, { useContext } from "react"
|
||||
import Style from "./ButtonToggleBeforeAfter.module.css"
|
||||
import classNames from "classnames"
|
||||
import Button from "../base/Button"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
|
||||
|
||||
export default function ButtonToggleBeforeAfter({ onUpdate, className, ...props }) {
|
||||
const [value, setValue] = useState(false)
|
||||
export default function ButtonToggleBeforeAfter({ isBefore, setBefore, className, ...props }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
|
||||
const onButtonClick = () => {
|
||||
onUpdate(!value)
|
||||
setValue(value => !value)
|
||||
setBefore(a => !a)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -21,7 +19,7 @@ export default function ButtonToggleBeforeAfter({ onUpdate, className, ...props
|
|||
onClick={onButtonClick}
|
||||
{...props}
|
||||
>
|
||||
{value ? strings.timeBefore : strings.timeAfter}
|
||||
{isBefore ? strings.timeBefore : strings.timeAfter}
|
||||
</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;
|
||||
}
|
||||
|
|
@ -1,12 +1,22 @@
|
|||
import React, { useState } from "react"
|
||||
import FormInline from "../base/FormInline"
|
||||
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 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 _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 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 ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
||||
|
||||
|
||||
export default function PickerFilter({ ...props }) {
|
||||
const {filterTab, setFilterTab} = useContext(ContextRepositoryViewer)
|
||||
const { filterTab, setFilterTab, setVisualizationTab } = useContext(ContextRepositoryViewer)
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
|
@ -37,7 +37,16 @@ export default function PickerFilter({ ...props }) {
|
|||
<ButtonPicker
|
||||
currentTab={filterTab}
|
||||
setTab={setFilterTab}
|
||||
name={"location"}
|
||||
name={"place"}
|
||||
icon={faLocationArrow}
|
||||
/>
|
||||
<ButtonIconOnly
|
||||
onClick={() => {
|
||||
setVisualizationTab("map")
|
||||
setFilterTab("location")
|
||||
}}
|
||||
disabled={filterTab === "location"}
|
||||
color={"Grey"}
|
||||
icon={faMapPin}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -4,8 +4,8 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
|||
import ButtonPicker from "./ButtonPicker"
|
||||
|
||||
|
||||
export default function PickerVisualization({...props}) {
|
||||
const {visualizationTab, setVisualizationTab} = useContext(ContextRepositoryViewer)
|
||||
export default function PickerVisualization({ ...props }) {
|
||||
const { visualizationTab, setVisualizationTab } = useContext(ContextRepositoryViewer)
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
|
|
|
@ -23,6 +23,10 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
|||
import BoxFilterContains from "../interactive/BoxFilterContains"
|
||||
import BoxFilterUser from "../interactive/BoxFilterUser"
|
||||
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 }) {
|
||||
|
@ -36,9 +40,11 @@ export default function RepositoryViewer({ id, className, ...props }) {
|
|||
setValue: setFilters,
|
||||
appendValue: appendFilter,
|
||||
spliceValue: spliceFilter,
|
||||
removeValue: removeFilter
|
||||
removeValue: removeFilter,
|
||||
} = useArrayState([])
|
||||
|
||||
// FIXME: this has a severe performance impact, investigate
|
||||
const mapViewHook = useMapView()
|
||||
|
||||
// Repository
|
||||
const repositoryBr = useBackendResource(
|
||||
|
@ -117,29 +123,33 @@ export default function RepositoryViewer({ id, className, ...props }) {
|
|||
{filterTab === "contains" ? <BoxFilterContains className={Style.AddFilter}/> : null}
|
||||
{filterTab === "hashtag" ? <BoxFilterHashtag className={Style.AddFilter}/> : null}
|
||||
{filterTab === "user" ? <BoxFilterUser className={Style.AddFilter}/> : null}
|
||||
{filterTab === "time" ? "Time" : null}
|
||||
{filterTab === "location" ? "Location" : null}
|
||||
{filterTab === "time" ? <BoxFilterDatetime className={Style.AddFilter}/> : null}
|
||||
{filterTab === "place" ? <BoxFilterHasPlace className={Style.AddFilter}/> : null}
|
||||
{filterTab === "location" ? <BoxFilterLocation className={Style.AddFilter}/> : null}
|
||||
</>
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextRepositoryViewer.Provider value={{
|
||||
visualizationTab,
|
||||
setVisualizationTab,
|
||||
filterTab,
|
||||
setFilterTab,
|
||||
filters,
|
||||
setFilters,
|
||||
appendFilter,
|
||||
spliceFilter,
|
||||
removeFilter,
|
||||
repositoryBr,
|
||||
repository,
|
||||
rawTweetsBv,
|
||||
rawTweets,
|
||||
tweets,
|
||||
words,
|
||||
}}>
|
||||
<ContextRepositoryViewer.Provider
|
||||
value={{
|
||||
visualizationTab,
|
||||
setVisualizationTab,
|
||||
filterTab,
|
||||
setFilterTab,
|
||||
filters,
|
||||
setFilters,
|
||||
appendFilter,
|
||||
spliceFilter,
|
||||
removeFilter,
|
||||
repositoryBr,
|
||||
repository,
|
||||
rawTweetsBv,
|
||||
rawTweets,
|
||||
tweets,
|
||||
words,
|
||||
mapViewHook,
|
||||
}}
|
||||
>
|
||||
|
||||
<div className={classNames(Style.RepositoryViewer, className)} {...props}>
|
||||
{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 RepositoryViewer from "../components/providers/RepositoryViewer"
|
||||
|
||||
|
@ -7,6 +7,6 @@ export default function PageRepository({ className, ...props }) {
|
|||
const { id } = useParams()
|
||||
|
||||
return (
|
||||
<RepositoryViewer id={id}/>
|
||||
<RepositoryViewer id={id} {...props}/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {Location} from "./location"
|
||||
import { Location } from "./location"
|
||||
import {
|
||||
faAt,
|
||||
faClock,
|
||||
faFilter,
|
||||
faFont, faHashtag,
|
||||
faFont,
|
||||
faHashtag,
|
||||
faLocationArrow,
|
||||
faMap,
|
||||
faMapMarkerAlt,
|
||||
faMapPin,
|
||||
} from "@fortawesome/free-solid-svg-icons"
|
||||
|
@ -38,6 +39,7 @@ export class Filter {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
export class ContainsFilter extends Filter {
|
||||
word
|
||||
|
||||
|
@ -68,7 +70,7 @@ export class HashtagFilter extends ContainsFilter {
|
|||
hashtag
|
||||
|
||||
constructor(negate, hashtag) {
|
||||
super(negate, `#${hashtag}`);
|
||||
super(negate, `#${hashtag}`)
|
||||
this.hashtag = hashtag
|
||||
}
|
||||
|
||||
|
@ -109,15 +111,12 @@ export class UserFilter extends Filter {
|
|||
|
||||
|
||||
export class HasLocationFilter extends Filter {
|
||||
hasLocation
|
||||
|
||||
constructor(negate, hasLocation) {
|
||||
constructor(negate) {
|
||||
super(negate)
|
||||
this.hasLocation = hasLocation
|
||||
}
|
||||
|
||||
check(tweet) {
|
||||
return (tweet["location"] !== null) === this.hasLocation
|
||||
return Boolean(tweet["location"])
|
||||
}
|
||||
|
||||
color() {
|
||||
|
@ -129,21 +128,18 @@ export class HasLocationFilter extends Filter {
|
|||
}
|
||||
|
||||
text() {
|
||||
return this.hasLocation
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class HasPlaceFilter extends Filter {
|
||||
hasPlace
|
||||
|
||||
constructor(negate, hasPlace) {
|
||||
constructor(negate) {
|
||||
super(negate)
|
||||
this.hasPlace = hasPlace
|
||||
}
|
||||
|
||||
check(tweet) {
|
||||
return (tweet["place"] !== null) === this.hasPlace
|
||||
return Boolean(tweet["place"])
|
||||
}
|
||||
|
||||
color() {
|
||||
|
@ -155,7 +151,7 @@ export class HasPlaceFilter extends Filter {
|
|||
}
|
||||
|
||||
text() {
|
||||
return this.hasPlace
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,7 +161,7 @@ export class LocationRadiusFilter extends HasLocationFilter {
|
|||
radius
|
||||
|
||||
constructor(negate, center, radius) {
|
||||
super(negate, true);
|
||||
super(negate)
|
||||
this.center = center
|
||||
this.radius = radius
|
||||
}
|
||||
|
@ -175,12 +171,12 @@ export class LocationRadiusFilter extends HasLocationFilter {
|
|||
return false
|
||||
}
|
||||
|
||||
// FIXME: assuming the earth is flat
|
||||
// FIXME: Maths is hard
|
||||
const location = Location.fromTweet(tweet)
|
||||
const latDiff = Math.abs(location.lat - this.center.lat)
|
||||
const lngDiff = Math.abs(location.lng - this.center.lng)
|
||||
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
|
||||
}
|
||||
|
@ -194,6 +190,32 @@ export class LocationRadiusFilter extends HasLocationFilter {
|
|||
}
|
||||
|
||||
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) {
|
||||
throw new Error(`Invalid location string: ${locString}`)
|
||||
}
|
||||
const {lat, lng} = match.groups
|
||||
const { lat, lng } = match.groups
|
||||
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