1
Fork 0
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:
Steffo 2021-05-21 19:52:56 +02:00
parent 237417298d
commit c7d425960d
Signed by: steffo
GPG key ID: 6965406171929D01
37 changed files with 638 additions and 361 deletions

View file

@ -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}
/>

View file

@ -1,25 +1,33 @@
import React from "react"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import Style from "./BoxMap.module.css"
import BoxFull from "./BoxFull"
import { MapContainer, TileLayer } from "react-leaflet"
export default function BoxMap({
setMap,
startingPosition = { lat: 41.89309, lng: 12.48289 },
startingZoom = 3,
export default function BoxMap(
{
mapViewHook,
button,
children,
...props
}) {
return (
<BoxFull
childrenClassName={Style.BoxMapContents}
{...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>
)
}

View file

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

View file

@ -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

View file

@ -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"}
<FormInlineBADatetime
submit={submit}
/>
<ButtonIconOnly
className={Style.Button}
icon={faPlus}
color={"Green"}
onClick={onButtonClick}
/>
</FormInline>
</BoxFull>
)
}

View file

@ -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"}
<FormInlineHashtag
submit={submit}
/>
<ButtonIconOnly
className={Style.Button}
icon={faPlus}
color={"Green"}
onClick={onButtonClick}
/>
</FormInline>
</BoxFull>
)
}

View file

@ -1,4 +1,4 @@
import React, { useCallback, useContext, useEffect, useState } from "react"
import React, { useCallback, useContext } from "react"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { 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())
},
[map],
)
const onZoom = useCallback(
() => {
setZoom(map.getZoom())
},
[map],
)
useEffect(
() => {
if(map === null) {
return
}
map.on("move", onMove)
map.on("zoom", onZoom)
return () => {
map.off("move", onMove)
map.off("zoom", onZoom)
}
},
[map, onMove, onZoom],
)
const onButtonClick = () => {
const mapSize = map.getSize()
const minSize = Math.min(mapSize.x, mapSize.y)
const radius = minSize * MPIXEL[zoom]
const radius = mapViewHook.zoom * osmZoomLevels[mapViewHook.zoom]
addCondition(new Condition(
"COORDINATES",
`< ${radius} ${position.lat} ${position.lng}`,
`< ${radius} ${mapViewHook.center.lat} ${mapViewHook.center.lng}`,
))
}
},
[mapViewHook, addCondition]
)
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}

View file

@ -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"}
<FormInlineUser
submit={submit}
/>
<ButtonIconOnly
className={Style.Button}
icon={faPlus}
color={"Green"}
onClick={onButtonClick}
/>
</FormInline>
</BoxFull>
)
}

View file

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

View file

@ -1,10 +1,11 @@
import React, { useContext, useState } from "react"
import React from "react"
import BoxFull from "../base/BoxFull"
import 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}
&nbsp;
<FontAwesomeIcon icon={faFont}/>
&nbsp;
{strings.byContents}
</span>
}
{...props}
>
<FormInlineText
textIcon={faFont}
submit={submit}
placeholder={"cat in the box"}
/>

View file

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

View file

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

View file

@ -1,43 +1,35 @@
import React, { useContext, useState } from "react"
import React from "react"
import BoxFull from "../base/BoxFull"
import 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}
&nbsp;
<FontAwesomeIcon icon={faClock}/>
&nbsp;
{strings.byTimePeriod}
</span>
}
{...props}
>
<FormInlineHashtag submit={submit}/>
</BoxFull>
)
}

View file

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

View file

@ -1,15 +1,11 @@
import React, { useContext, useState } from "react"
import React from "react"
import BoxFull from "../base/BoxFull"
import 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}
&nbsp;
<FontAwesomeIcon icon={faAt}/>
&nbsp;
{strings.byUser}
</span>
}
{...props}
>
<FormInlineUser
submit={submit}
/>
</BoxFull>

View file

@ -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}/>)

View file

@ -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) {

View file

@ -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}
/>

View file

@ -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,10 +8,11 @@ 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 markers = useMemo(
() => {
return tweets.filter(tweet => tweet.location).map(tweet => {
const location = Location.fromTweet(tweet)
return (
@ -27,9 +28,12 @@ export default function BoxVisualizationMap({ ...props }) {
</Marker>
)
})
},
[tweets],
)
return (
<BoxMap header={strings.visualMap} {...props}>
<BoxMap header={strings.visualMap} mapViewHook={mapViewHook} {...props}>
{markers}
</BoxMap>
)

View file

@ -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>

View file

@ -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}
/>
)

View file

@ -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>
)
}

View file

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

View file

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

View file

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

View file

@ -1,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 => {

View file

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

View file

@ -1,12 +1,12 @@
import React, { useContext } from "react"
import 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>

View file

@ -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}>

View file

@ -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,13 +123,15 @@ 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={{
<ContextRepositoryViewer.Provider
value={{
visualizationTab,
setVisualizationTab,
filterTab,
@ -139,7 +147,9 @@ export default function RepositoryViewer({ id, className, ...props }) {
rawTweets,
tweets,
words,
}}>
mapViewHook,
}}
>
<div className={classNames(Style.RepositoryViewer, className)} {...props}>
{contents}

View file

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

View file

@ -1,4 +1,4 @@
import React, { useContext, useMemo, useState } from "react"
import React from "react"
import { useParams } from "react-router"
import 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}/>
)
}

View file

@ -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()}`
}
}

View file

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

View file

@ -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)
}

View file

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