1
Fork 0
mirror of https://github.com/pds-nest/nest.git synced 2024-11-24 22:14:18 +00:00

Merge branch 'refactor' into 'main'

Mega refactorone frontend

See merge request nest/g2-progetto!6
This commit is contained in:
Stefano Pigozzi 2021-05-23 03:39:49 +00:00
commit 10baee0215
85 changed files with 1118 additions and 689 deletions

View file

@ -58,7 +58,7 @@
</list> </list>
</option> </option>
</inspection_tool> </inspection_tool>
<inspection_tool class="RegExpAnonymousGroup" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="RegExpAnonymousGroup" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="RegExpEscapedMetaCharacter" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="RegExpEscapedMetaCharacter" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="RegExpOctalEscape" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="RegExpOctalEscape" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
<inspection_tool class="RegExpRedundantEscape" enabled="true" level="WEAK WARNING" enabled_by_default="true" /> <inspection_tool class="RegExpRedundantEscape" enabled="true" level="WEAK WARNING" enabled_by_default="true" />

View file

@ -11,6 +11,7 @@ module.exports = {
config.roots = config.roots.map(root => root.replace("src", "nest_frontend")) config.roots = config.roots.map(root => root.replace("src", "nest_frontend"))
config.collectCoverageFrom = config.collectCoverageFrom.map(root => root.replace("src", "nest_frontend")) config.collectCoverageFrom = config.collectCoverageFrom.map(root => root.replace("src", "nest_frontend"))
config.testMatch = config.testMatch.map(root => root.replace("src", "nest_frontend")) config.testMatch = config.testMatch.map(root => root.replace("src", "nest_frontend"))
console.debug(config)
return config; return config;
} }
} }

View file

@ -1,4 +1,3 @@
// Link.react.test.js
import React from "react" import React from "react"
import "@testing-library/jest-dom/extend-expect" import "@testing-library/jest-dom/extend-expect"
import { render, screen } from "@testing-library/react" import { render, screen } from "@testing-library/react"

View file

@ -1,7 +1,7 @@
import React from "react" import React from "react"
import Style from "./Button.module.css" import Style from "./Button.module.css"
import classNames from "classnames" import classNames from "classnames"
import make_icon from "../../utils/make_icon" import makeIcon from "../../utils/makeIcon"
/** /**
@ -26,7 +26,7 @@ export default function Button({ children, disabled, onClick, className, color,
disabled={disabled} disabled={disabled}
{...props} {...props}
> >
{children} {make_icon(icon, Style.Icon)} {children} {makeIcon(icon, {className: Style.Icon})}
</button> </button>
) )
} }

View file

@ -13,6 +13,8 @@
.Button[disabled] { .Button[disabled] {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed;
} }
.Button:focus-visible { .Button:focus-visible {

View file

@ -1,7 +1,7 @@
import React from "react" import React from "react"
import Style from "./ButtonSidebar.module.css" import Style from "./ButtonSidebar.module.css"
import classNames from "classnames" import classNames from "classnames"
import make_icon from "../../utils/make_icon" import makeIcon from "../../utils/makeIcon"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { useRouteMatch } from "react-router" import { useRouteMatch } from "react-router"
@ -31,7 +31,7 @@ export default function ButtonSidebar({ icon, children, to, className, ...props
return ( return (
<Link to={to} className={Style.ButtonLink}> <Link to={to} className={Style.ButtonLink}>
<div className={classNames(Style.ButtonSidebar, "Clickable", className)} {...props}> <div className={classNames(Style.ButtonSidebar, "Clickable", className)} {...props}>
{make_icon(icon, Style.ButtonIcon)} {makeIcon(icon, {className: Style.ButtonIcon})}
<div className={Style.ButtonText}> <div className={Style.ButtonText}>
{children} {children}
</div> </div>

View file

@ -1,7 +1,7 @@
import React, { useState } from "react" import React, { useState } from "react"
import Style from "./InputWithIcon.module.css" import Style from "./InputWithIcon.module.css"
import classNames from "classnames" import classNames from "classnames"
import make_icon from "../../utils/make_icon" import makeIcon from "../../utils/makeIcon"
/** /**
@ -22,7 +22,7 @@ export default function InputWithIcon({ icon, className, ...props }) {
return ( return (
<div className={classNames(Style.InputWithIcon, isFocused ? Style.Focused : null, className)}> <div className={classNames(Style.InputWithIcon, isFocused ? Style.Focused : null, className)}>
<div className={Style.IconPart}> <div className={Style.IconPart}>
{make_icon(icon)} {makeIcon(icon)}
</div> </div>
<input <input
className={Style.InputPart} className={Style.InputPart}

View file

@ -1,65 +1,22 @@
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 ContextRepositoryEditor from "../../contexts/ContextRepositoryEditor"
import Badge from "../base/Badge" import Badge from "../base/Badge"
const CONDITION_COLORS = {
0: "Grey", // Hashtag
2: "Yellow", // Time
3: "Red", // Coordinates
4: "Red", // Place
5: "Green", // User
}
const CONDITION_ICONS = {
0: faHashtag, // Hashtag
2: faClock, // Time
3: faGlobe, // Coordinates
4: faMapPin, // Place
5: faAt, // User
}
/** /**
* A {@link Badge} representing a Condition for a filter. * A {@link Badge} representing a {@link Condition}.
* *
* @param condition - The Condition that this badge represents. * @param condition - The {@link Condition} that this badge represents.
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
export default function BadgeCondition({ ...condition }) { export default function BadgeCondition({ condition }) {
const { id, type, content } = condition
const color = CONDITION_COLORS[type]
const icon = CONDITION_ICONS[type]
const { removeRawCondition } = useContext(ContextRepositoryEditor) const { removeRawCondition } = useContext(ContextRepositoryEditor)
let displayedContent = content
if(type === 3) {
let split = displayedContent.split(" ")
let radius = Number.parseFloat(split[1]).toFixed(0)
let radiusType = "m"
if(radius >= 2000) {
radius = Math.round(radius / 1000)
radiusType = "km"
}
let lat = Number(split[2]).toFixed(3)
let lng = Number(split[3]).toFixed(3)
displayedContent = `${split[0]} ${radius}${radiusType} ${lat} ${lng}`
}
return ( return (
<Badge <Badge
title={id ? `💠 Condition ID: ${id}` : "✨ New Condition"} {...condition.display()}
color={color} onClickDelete={() => removeRawCondition(condition)}
icon={icon} />
onClickDelete={() => {
console.debug(`Removing Condition: `, condition)
removeRawCondition(condition)
}}
>
{displayedContent}
</Badge>
) )
} }

View file

@ -1,38 +0,0 @@
.ConditionBadge {
display: inline-flex;
gap: 5px;
padding: 0 5px;
border-radius: 25px;
margin: 0 2px;
}
.ConditionBadgeRed {
background-color: var(--bg-red);
color: var(--fg-red)
}
.ConditionBadgeYellow {
background-color: var(--bg-yellow);
color: var(--fg-yellow)
}
.ConditionBadgeGrey {
background-color: var(--bg-grey);
color: var(--fg-grey)
}
.ConditionBadgeGreen {
background-color: var(--bg-green);
color: var(--fg-green)
}
.Text {
max-width: 350px;
overflow-x: hidden;
}
.Icon {
width: 15px;
text-align: center;
}

View file

@ -1,6 +1,6 @@
import React, { useContext } from "react" import React, { useContext } from "react"
import Badge from "../base/Badge"
import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
import Badge from "../base/Badge"
/** /**
@ -15,11 +15,8 @@ export default function BadgeFilter({ filter }) {
return ( return (
<Badge <Badge
color={filter.color()} {...filter.display()}
icon={filter.icon()}
onClickDelete={() => removeFilter(filter)} onClickDelete={() => removeFilter(filter)}
> />
{filter.text()}
</Badge>
) )
} }

View file

@ -1,17 +1,16 @@
import React, { useContext } from "react" import React, { useCallback, 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 } from "@fortawesome/free-solid-svg-icons" import { faClock } from "@fortawesome/free-solid-svg-icons"
import useRepositoryEditor from "../../hooks/useRepositoryEditor" import useRepositoryEditor from "../../hooks/useRepositoryEditor"
import Condition from "../../utils/Condition"
import convertToLocalISODate from "../../utils/convertToLocalISODate"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
import FormInlineBADatetime from "./FormInlineBADatetime" import FormInlineTimeRay from "./FormInlineTimeRay"
import { ConditionTime } from "../../objects/Condition"
/** /**
* A {@link BoxFull} that allows the user to select a Twitter user to search for, and then to add it as a Condition * A {@link BoxFull} that allows the user to select a Twitter user to search for, and then to add it as a
* to the {@link ContextRepositoryEditor}. * {@link ConditionTime} of a RepositoryEditor.
* *
* @param props - Additional props to pass to the box. * @param props - Additional props to pass to the box.
* @returns {JSX.Element} * @returns {JSX.Element}
@ -21,14 +20,10 @@ export default function BoxConditionDatetime({ ...props }) {
const { addCondition } = useRepositoryEditor() const { addCondition } = useRepositoryEditor()
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const submit = ({ date, isBefore }) => { const submit = useCallback(
if(date.toString() === "Invalid Date") { timeRay => addCondition(new ConditionTime(timeRay)),
console.debug("Refusing to add condition: ", date, " is an Invalid Date.") [addCondition]
return )
}
const aware = convertToLocalISODate(date)
addCondition(new Condition("TIME", `${isBefore ? "<" : ">"} ${aware}`))
}
return ( return (
<BoxFull <BoxFull
@ -43,7 +38,7 @@ export default function BoxConditionDatetime({ ...props }) {
} }
{...props} {...props}
> >
<FormInlineBADatetime <FormInlineTimeRay
submit={submit} submit={submit}
/> />
</BoxFull> </BoxFull>

View file

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

View file

@ -1,16 +1,16 @@
import React, { useContext } from "react" import React, { useCallback, 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 } from "@fortawesome/free-solid-svg-icons" import { faHashtag } from "@fortawesome/free-solid-svg-icons"
import useRepositoryEditor from "../../hooks/useRepositoryEditor" import useRepositoryEditor from "../../hooks/useRepositoryEditor"
import Condition from "../../utils/Condition"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
import FormInlineHashtag from "./FormInlineHashtag" import FormInlineHashtag from "./FormInlineHashtag"
import { ConditionHashtag } from "../../objects/Condition"
/** /**
* A {@link BoxFull} that allows the user to select a Twitter hashtag to search for, and then to add it as a Condition * A {@link BoxFull} that allows the user to select a Twitter hashtag to search for, and then to add it as a
* to the {@link ContextRepositoryEditor}. * {@link ConditionHashtag} of a RepositoryEditor.
* *
* @param props - Additional props to pass to the box. * @param props - Additional props to pass to the box.
* @returns {JSX.Element} * @returns {JSX.Element}
@ -20,9 +20,10 @@ export default function BoxConditionHashtag({ ...props }) {
const { addCondition } = useRepositoryEditor() const { addCondition } = useRepositoryEditor()
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const submit = value => { const submit = useCallback(
addCondition(new Condition("HASHTAG", value)) value => addCondition(new ConditionHashtag(value)),
} [addCondition]
)
return ( return (
<BoxFull <BoxFull

View file

@ -1,36 +1,29 @@
import React, { useCallback, useContext } 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 { faLocationArrow, faPlus } from "@fortawesome/free-solid-svg-icons"
import ButtonIconOnly from "../base/ButtonIconOnly" import ButtonIconOnly from "../base/ButtonIconOnly"
import useRepositoryEditor from "../../hooks/useRepositoryEditor" import useRepositoryEditor from "../../hooks/useRepositoryEditor"
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 useMapAreaState from "../../hooks/useMapAreaState"
import osmZoomLevels from "../../utils/osmZoomLevels" import { ConditionLocation } from "../../objects/Condition"
/** /**
* A {@link BoxFull} that allows the user to select a geographical location to use to filter tweets. * A {@link BoxMap} that allows the user to select a geographical location, and then to add it as a
* {@link ConditionLocation} of a RepositoryEditor.
* *
* @param props - Additional props to pass to the box. * @param props - Additional props to pass to the box.
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */
export default function BoxConditionMap({ ...props }) { export default function BoxConditionLocation({ ...props }) {
const mapViewHook = useMapView() const mapViewHook = useMapAreaState()
const { addCondition } = useRepositoryEditor() const { addCondition } = useRepositoryEditor()
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const onButtonClick = useCallback( const onButtonClick = useCallback(
() => { () => addCondition(new ConditionLocation(mapViewHook.mapArea)),
const radius = mapViewHook.zoom * osmZoomLevels[mapViewHook.zoom]
addCondition(new Condition(
"COORDINATES",
`< ${radius} ${mapViewHook.center.lat} ${mapViewHook.center.lng}`,
))
},
[mapViewHook, addCondition] [mapViewHook, addCondition]
) )
@ -41,7 +34,7 @@ export default function BoxConditionMap({ ...props }) {
<span> <span>
{strings.searchBy} {strings.searchBy}
&nbsp; &nbsp;
<FontAwesomeIcon icon={faMapPin}/> <FontAwesomeIcon icon={faLocationArrow}/>
&nbsp; &nbsp;
{strings.byZone} {strings.byZone}
</span> </span>

View file

@ -1,16 +1,16 @@
import React, { useContext } from "react" import React, { useCallback, 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 } from "@fortawesome/free-solid-svg-icons" import { faAt } from "@fortawesome/free-solid-svg-icons"
import useRepositoryEditor from "../../hooks/useRepositoryEditor" import useRepositoryEditor from "../../hooks/useRepositoryEditor"
import Condition from "../../utils/Condition"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
import FormInlineUser from "./FormInlineUser" import FormInlineUser from "./FormInlineUser"
import { ConditionHashtag, ConditionUser } from "../../objects/Condition"
/** /**
* A {@link BoxFull} that allows the user to select a Twitter user to search for, and then to add it as a Condition * A {@link BoxFull} that allows the user to select a Twitter user to search for, and then to add it as a
* to the {@link ContextRepositoryEditor}. * {@link ConditionUser} of a RepositoryEditor.
* *
* @param props - Additional props to pass to the box. * @param props - Additional props to pass to the box.
* @returns {JSX.Element} * @returns {JSX.Element}
@ -20,9 +20,10 @@ export default function BoxConditionUser({ ...props }) {
const { addCondition } = useRepositoryEditor() const { addCondition } = useRepositoryEditor()
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const submit = value => { const submit = useCallback(
addCondition(new Condition("USER", value)) value => addCondition(new ConditionUser(value)),
} [addCondition]
)
return ( return (
<BoxFull <BoxFull

View file

@ -16,7 +16,7 @@ export default function BoxConditions({ ...props }) {
const { conditions } = useRepositoryEditor() const { conditions } = useRepositoryEditor()
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const badges = conditions.map((cond, pos) => <BadgeCondition key={pos} {...cond}/>) const badges = conditions.map((cond, pos) => <BadgeCondition key={pos} condition={cond}/>)
return ( return (
<BoxFull header={strings.conditions} {...props}> <BoxFull header={strings.conditions} {...props}>

View file

@ -2,18 +2,26 @@ 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 { FilterContains } from "../../objects/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" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
/**
* A {@link BoxFull} that allows the user to select a word to search for, and then to add it as a
* {@link FilterContains} of a RepositoryViewer.
*
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxFilterContains({ ...props }) { export default function BoxFilterContains({ ...props }) {
const strings = useStrings() const strings = useStrings()
const { appendFilter } = useRepositoryViewer() const { appendFilter } = useRepositoryViewer()
const submit = value => { const submit = value => {
appendFilter(new ContainsFilter(false, value)) appendFilter(new FilterContains(value))
} }
// TODO: add this string // TODO: add this string

View file

@ -1,19 +1,27 @@
import React from "react" import React from "react"
import BoxFull from "../base/BoxFull" import BoxFull from "../base/BoxFull"
import { faClock, faHashtag } from "@fortawesome/free-solid-svg-icons" import { faClock } from "@fortawesome/free-solid-svg-icons"
import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings" import useStrings from "../../hooks/useStrings"
import { AfterDatetimeFilter } from "../../utils/Filter" import { FilterInsideTimeRay } from "../../objects/Filter"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import FormInlineBADatetime from "./FormInlineBADatetime" import FormInlineTimeRay from "./FormInlineTimeRay"
/**
* A {@link BoxFull} that allows the user to select a {@link TimeRay}, and then to add it as a
* {@link FilterInsideTimeRay} of a RepositoryViewer.
*
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxFilterDatetime({ ...props }) { export default function BoxFilterDatetime({ ...props }) {
const strings = useStrings() const strings = useStrings()
const { appendFilter } = useRepositoryViewer() const { appendFilter } = useRepositoryViewer()
const submit = ({ date, isBefore }) => { const submit = (timeRay) => {
appendFilter(new AfterDatetimeFilter(isBefore, date)) appendFilter(new FilterInsideTimeRay(timeRay))
} }
return ( return (
@ -29,7 +37,7 @@ export default function BoxFilterDatetime({ ...props }) {
} }
{...props} {...props}
> >
<FormInlineBADatetime submit={submit}/> <FormInlineTimeRay submit={submit}/>
</BoxFull> </BoxFull>
) )
} }

View file

@ -4,18 +4,25 @@ import FormInline from "../base/FormInline"
import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings" import useStrings from "../../hooks/useStrings"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faLocationArrow, faPlus } from "@fortawesome/free-solid-svg-icons" import { faLocationArrow, faMapMarkerAlt, faPlus } from "@fortawesome/free-solid-svg-icons"
import { HasPlaceFilter } from "../../utils/Filter" import { FilterWithPlace } from "../../objects/Filter"
import ButtonIconOnly from "../base/ButtonIconOnly" import ButtonIconOnly from "../base/ButtonIconOnly"
/**
* A {@link BoxFull} that allows the user to add a {@link FilterWithPlace} to a RepositoryViewer.
*
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxFilterHasPlace({ ...props }) { export default function BoxFilterHasPlace({ ...props }) {
const strings = useStrings() const strings = useStrings()
const { appendFilter } = useRepositoryViewer() const { appendFilter } = useRepositoryViewer()
const submit = () => { const submit = () => {
appendFilter(new HasPlaceFilter(false)) appendFilter(new FilterWithPlace())
} }
// TODO: translate this // TODO: translate this
@ -26,7 +33,7 @@ export default function BoxFilterHasPlace({ ...props }) {
<span> <span>
{strings.searchBy} {strings.searchBy}
&nbsp; &nbsp;
<FontAwesomeIcon icon={faLocationArrow}/> <FontAwesomeIcon icon={faMapMarkerAlt}/>
&nbsp; &nbsp;
{strings.byHasPlace} {strings.byHasPlace}
</span> </span>

View file

@ -3,17 +3,25 @@ import BoxFull from "../base/BoxFull"
import { faHashtag } from "@fortawesome/free-solid-svg-icons" import { faHashtag } from "@fortawesome/free-solid-svg-icons"
import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings" import useStrings from "../../hooks/useStrings"
import { HashtagFilter } from "../../utils/Filter" import { FilterHashtag } from "../../objects/Filter"
import FormInlineHashtag from "./FormInlineHashtag" import FormInlineHashtag from "./FormInlineHashtag"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
/**
* A {@link BoxFull} that allows the user to select a Twitter hashtag to search for, and then to add it as a
* {@link FilterContains} of a RepositoryViewer.
*
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxFilterHashtag({ ...props }) { export default function BoxFilterHashtag({ ...props }) {
const strings = useStrings() const strings = useStrings()
const { appendFilter } = useRepositoryViewer() const { appendFilter } = useRepositoryViewer()
const submit = value => { const submit = value => {
appendFilter(new HashtagFilter(false, value)) appendFilter(new FilterHashtag(value))
} }
return ( return (

View file

@ -1,21 +1,30 @@
import React from "react" import React from "react"
import BoxFull from "../base/BoxFull" import BoxFull from "../base/BoxFull"
import FormInline from "../base/FormInline"
import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings" import useStrings from "../../hooks/useStrings"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faMapPin } from "@fortawesome/free-solid-svg-icons" import { faLocationArrow, faMapPin } from "@fortawesome/free-solid-svg-icons"
import FormInlineLocation from "./FormInlineLocation" import FormInlineLocation from "./FormInlineLocation"
import { LocationRadiusFilter } from "../../utils/Filter" import { FilterInsideMapArea } from "../../objects/Filter"
/**
* A {@link BoxFull} that allows the user to add a {@link FilterInsideMapArea} to a RepositoryViewer.
*
* It connects to the `mapViewHook` of the RepositoryViewer.
*
* @deprecated to be refactored
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxFilterLocation({ ...props }) { export default function BoxFilterLocation({ ...props }) {
const strings = useStrings() const strings = useStrings()
const { appendFilter, mapViewHook } = useRepositoryViewer() const { appendFilter, mapViewHook } = useRepositoryViewer()
const submit = () => { const submit = () => {
appendFilter(new LocationRadiusFilter(false, mapViewHook.center, mapViewHook.radius)) appendFilter(new FilterInsideMapArea(mapViewHook.mapArea))
} }
return ( return (
@ -24,7 +33,7 @@ export default function BoxFilterLocation({ ...props }) {
<span> <span>
{strings.searchBy} {strings.searchBy}
&nbsp; &nbsp;
<FontAwesomeIcon icon={faMapPin}/> <FontAwesomeIcon icon={faLocationArrow}/>
&nbsp; &nbsp;
{strings.byZone} {strings.byZone}
</span> </span>

View file

@ -3,20 +3,26 @@ import BoxFull from "../base/BoxFull"
import { faAt } from "@fortawesome/free-solid-svg-icons" import { faAt } from "@fortawesome/free-solid-svg-icons"
import useRepositoryViewer from "../../hooks/useRepositoryViewer" import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings" import useStrings from "../../hooks/useStrings"
import { UserFilter } from "../../utils/Filter" import { FilterPoster } from "../../objects/Filter"
import FormInlineUser from "./FormInlineUser" import FormInlineUser from "./FormInlineUser"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
/**
* A {@link BoxFull} that allows the user to select a Twitter user to search for, and then to add it as a
* {@link FilterPoster} of a RepositoryViewer.
*
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxFilterUser({ ...props }) { export default function BoxFilterUser({ ...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 submit = value => { const submit = value => {
appendFilter(new UserFilter(false, value)) appendFilter(new FilterPoster(value))
} }
return ( return (

View file

@ -13,7 +13,7 @@ import ContextLanguage from "../../contexts/ContextLanguage"
/** /**
* A {@link BoxFull} displaying the user's current login status, and allowing them to logout. * A {@link BoxFull} displaying the user's current login status, and allowing them to logout.
* *
* @param props * @param props - Additional props to pass to the box.
* @returns {JSX.Element} * @returns {JSX.Element}
* @constructor * @constructor
*/ */

View file

@ -1,3 +0,0 @@
.BoxRepositoryCreate {
}

View file

@ -1,14 +1,22 @@
import React, { useContext } from "react" import React from "react"
import BoxFullScrollable from "../base/BoxFullScrollable" import BoxFullScrollable from "../base/BoxFullScrollable"
import SummaryTweet from "./SummaryTweet" import SummaryTweet from "./SummaryTweet"
import ContextLanguage from "../../contexts/ContextLanguage"
import Empty from "./Empty" import Empty from "./Empty"
import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" import useRepositoryViewer from "../../hooks/useRepositoryViewer"
import useStrings from "../../hooks/useStrings"
/**
* A {@link BoxFullScrollable} rendering all the tweets currently displayed in a RepositoryViewer as
* {@link SummaryTweet}s.
*
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxRepositoryTweets({ ...props }) { export default function BoxRepositoryTweets({ ...props }) {
const { strings } = useContext(ContextLanguage) const strings = useStrings()
const { tweets } = useContext(ContextRepositoryViewer) const { tweets } = useRepositoryViewer()
let content let content
if(tweets.length === 0) { if(tweets.length === 0) {

View file

@ -9,6 +9,15 @@ import FormAlert from "../base/formparts/FormAlert"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
/**
* A {@link BoxFull} allowing an administrator user to create a new user.
*
* @param createUser - Async function to call to create an user.
* @param running - Whether another request is currently running.
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxUserCreate({ createUser, running, ...props }) { export default function BoxUserCreate({ createUser, running, ...props }) {
const [username, setUsername] = useState("") const [username, setUsername] = useState("")
const [email, setEmail] = useState("") const [email, setEmail] = useState("")

View file

@ -5,6 +5,16 @@ import SummaryUser from "./SummaryUser"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
/**
* A {@link BoxFullScrollable} rendering an array of users as {@link SummaryUser}s.
*
* @param users - Array of users to render.
* @param destroyUser - Async function to destroy an user, to be passed to {@link SummaryUser}.
* @param running - Whether another request is currently running.
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxUserList({ users, destroyUser, running, ...props }) { export default function BoxUserList({ users, destroyUser, running, ...props }) {
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)

View file

@ -2,10 +2,17 @@ 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"
import { Location } from "../../utils/location" import Coordinates from "../../objects/Coordinates"
import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
/**
* A {@link BoxMap} displaying the displayed tweets of a RepositoryViewer as {@link Marker}s.
*
* @param props - Additional props to pass to the box.
* @returns {JSX.Element}
* @constructor
*/
export default function BoxVisualizationMap({ ...props }) { export default function BoxVisualizationMap({ ...props }) {
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
const { tweets, mapViewHook } = useContext(ContextRepositoryViewer) const { tweets, mapViewHook } = useContext(ContextRepositoryViewer)
@ -13,10 +20,12 @@ export default function BoxVisualizationMap({ ...props }) {
const markers = useMemo( const markers = useMemo(
() => { () => {
return tweets.filter(tweet => tweet.location).map(tweet => { return tweets.filter(tweet => tweet.location).map(tweet => {
const location = Location.fromTweet(tweet) if(!tweet.location) return null
const coords = Coordinates.fromCrawlerString(tweet.location)
return ( return (
<Marker key={tweet["snowflake"]} position={location.toArray()}> <Marker key={tweet["snowflake"]} position={coords.toLatLng()}>
<Popup> <Popup>
<p> <p>
{tweet["content"]} {tweet["content"]}

View file

@ -1,10 +1,10 @@
import React, { useContext } from "react" import React, { useCallback, useContext } from "react"
import BoxWordcloud from "../base/BoxWordcloud" import BoxWordcloud from "../base/BoxWordcloud"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
import BoxFull from "../base/BoxFull" import BoxFull from "../base/BoxFull"
import Empty from "./Empty" import Empty from "./Empty"
import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
import { ContainsFilter } from "../../utils/Filter" import { FilterContains } from "../../objects/Filter"
export default function BoxVisualizationWordcloud({ ...props }) { export default function BoxVisualizationWordcloud({ ...props }) {
@ -19,9 +19,12 @@ export default function BoxVisualizationWordcloud({ ...props }) {
) )
} }
const onWordClick = word => { const onWordClick = useCallback(
appendFilter(new ContainsFilter(false, word.text)) word => {
} appendFilter(new FilterContains(word.text))
},
[appendFilter]
)
return ( return (
<BoxWordcloud <BoxWordcloud

View file

@ -2,6 +2,16 @@ import React from "react"
import ButtonIconOnly from "../base/ButtonIconOnly" import ButtonIconOnly from "../base/ButtonIconOnly"
/**
* A {@link ButtonIconOnly} to be used to switch between RepositoryViewer tabs.
*
* @param setTab - Function to change tab.
* @param currentTab - Name of the current tab, as a string.
* @param name - Name of the tab this button should switch to, as a string.
* @param props - Additional props to pass to the button.
* @returns {JSX.Element}
* @constructor
*/
export default function ButtonPicker({ setTab, currentTab, name, ...props }) { export default function ButtonPicker({ setTab, currentTab, name, ...props }) {
return ( return (
<ButtonIconOnly <ButtonIconOnly

View file

@ -5,6 +5,16 @@ import Button from "../base/Button"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
/**
* A {@link Button} allowing the user to select between **Before** and **After**.
*
* @param isBefore - The current value of the button.
* @param setBefore - Function to set the current value of the button.
* @param className - Additional class(es) to append to the button.
* @param props - Additional props to pass to the button.
* @returns {JSX.Element}
* @constructor
*/
export default function ButtonToggleBeforeAfter({ isBefore, setBefore, className, ...props }) { export default function ButtonToggleBeforeAfter({ isBefore, setBefore, className, ...props }) {
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)

View file

@ -4,7 +4,15 @@ import classNames from "classnames"
import ContextLanguage from "../../contexts/ContextLanguage" import ContextLanguage from "../../contexts/ContextLanguage"
export default function Empty({ children, className, ...props }) { /**
* A simple inline `<i>` element to be used when there is nothing to be displayed inside a box.
*
* @param className - Additional class(es) to append to the element.
* @param props - Additional props to pass to the element.
* @returns {JSX.Element}
* @constructor
*/
export default function Empty({ className, ...props }) {
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)
return ( return (

View file

@ -8,7 +8,14 @@ import { faHashtag } from "@fortawesome/free-solid-svg-icons"
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 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 }) { /**
* A {@link FormInline} allowing the user to select a Twitter hashtag.
*
* @param props - Additional props to pass to the form.
* @returns {JSX.Element}
* @constructor
*/
export default function FormInlineHashtag({ ...props }) {
const validate = value => { const validate = value => {
return value.replace(INVALID_CHARACTERS, "") return value.replace(INVALID_CHARACTERS, "")
@ -19,7 +26,6 @@ export default function FormInlineHashtag({ submit, ...props }) {
textIcon={faHashtag} textIcon={faHashtag}
placeholder={"hashtag"} placeholder={"hashtag"}
validate={validate} validate={validate}
submit={submit}
{...props} {...props}
/> />
) )

View file

@ -6,6 +6,21 @@ import ButtonIconOnly from "../base/ButtonIconOnly"
import Style from "./FormInlineLocation.module.css" import Style from "./FormInlineLocation.module.css"
/**
* @deprecated to be refactored
* @param mapViewHook
* @param radIcon
* @param latIcon
* @param lngIcon
* @param buttonIcon
* @param buttonColor
* @param placeholder
* @param validate
* @param submit
* @param props
* @returns {JSX.Element}
* @constructor
*/
export default function FormInlineLocation( export default function FormInlineLocation(
{ {
mapViewHook, mapViewHook,
@ -32,21 +47,21 @@ export default function FormInlineLocation(
className={Style.Radius} className={Style.Radius}
type={"text"} type={"text"}
icon={radIcon} icon={radIcon}
value={`${mapViewHook.radius} m`} value={`${Math.round(mapViewHook.mapArea.radius / 1000)} km`}
disabled={true} disabled={true}
/> />
<InputWithIcon <InputWithIcon
className={Style.Latitude} className={Style.Latitude}
type={"text"} type={"text"}
icon={latIcon} icon={latIcon}
value={mapViewHook.center.lat.toFixed(3)} value={mapViewHook.mapArea.center.lat.toFixed(3)}
disabled={true} disabled={true}
/> />
<InputWithIcon <InputWithIcon
className={Style.Longitude} className={Style.Longitude}
type={"text"} type={"text"}
icon={lngIcon} icon={lngIcon}
value={mapViewHook.center.lng.toFixed(3)} value={mapViewHook.mapArea.center.lng.toFixed(3)}
disabled={true} disabled={true}
/> />
<ButtonIconOnly <ButtonIconOnly

View file

@ -6,6 +6,19 @@ import ButtonIconOnly from "../base/ButtonIconOnly"
import Style from "./FormInlineText.module.css" import Style from "./FormInlineText.module.css"
/**
* A {@link FormInline} allowing the user to enter a string.
*
* @param textIcon - The icon to display in the text field.
* @param buttonIcon - The icon to display on the submit button.
* @param buttonColor - The color of the submit button.
* @param placeholder - The placeholder of the text field.
* @param validate - Function <string -> string> called to set the value of the text field.
* @param submit - Function <string> called when the submit button is pressed.
* @param props - Additional props to pass to the form.
* @returns {JSX.Element}
* @constructor
*/
export default function FormInlineText( export default function FormInlineText(
{ {
textIcon = faFont, textIcon = faFont,
@ -21,6 +34,7 @@ export default function FormInlineText(
const _onSubmit = event => { const _onSubmit = event => {
event.preventDefault() event.preventDefault()
if(!value) return
submit(value) submit(value)
setValue("") setValue("")
} }
@ -43,6 +57,7 @@ export default function FormInlineText(
icon={buttonIcon} icon={buttonIcon}
color={buttonColor} color={buttonColor}
onClick={_onSubmit} onClick={_onSubmit}
disabled={!value}
/> />
</FormInline> </FormInline>
) )

View file

@ -5,12 +5,26 @@ import { faClock, 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"
import ButtonToggleBeforeAfter from "./ButtonToggleBeforeAfter" import ButtonToggleBeforeAfter from "./ButtonToggleBeforeAfter"
import TimeRay from "../../objects/TimeRay"
const INVALID_CHARACTERS = /[^0-9TZ:+-]/g const INVALID_CHARACTERS = /[^0-9TZ:+.-]/g
export default function FormInlineBADatetime( /**
* A {@link FormInline} allowing the user to select a {@link TimeRay}.
*
* @param textIcon - The icon to display in the text field.
* @param buttonIcon - The icon to display on the submit button.
* @param buttonColor - The color of the submit button.
* @param placeholder - The placeholder of the text field.
* @param validate - Function <string -> string> called to set the value of the text field.
* @param submit - Function <{@link TimeRay}> called when the submit button is pressed.
* @param props - Additional props to pass to the form.
* @returns {JSX.Element}
* @constructor
*/
export default function FormInlineTimeRay(
{ {
textIcon = faClock, textIcon = faClock,
buttonIcon = faPlus, buttonIcon = faPlus,
@ -26,15 +40,14 @@ export default function FormInlineBADatetime(
const _onSubmit = event => { const _onSubmit = event => {
event.preventDefault() event.preventDefault()
submit({ if(!value) return
date: new Date(value), console.debug(value)
isBefore, submit(new TimeRay(isBefore, new Date(value)))
})
setValue("") setValue("")
} }
const _onChange = event => { const _onChange = event => {
setValue(validate(event.target.value.replace(INVALID_CHARACTERS, ""))) setValue(validate(event.target.value.toUpperCase().replace(INVALID_CHARACTERS, "")))
} }
return ( return (
@ -56,6 +69,7 @@ export default function FormInlineBADatetime(
icon={buttonIcon} icon={buttonIcon}
color={buttonColor} color={buttonColor}
onClick={_onSubmit} onClick={_onSubmit}
disabled={!value}
/> />
</FormInline> </FormInline>
) )

View file

@ -3,12 +3,17 @@ import FormInlineText from "./FormInlineText"
import { faAt } from "@fortawesome/free-solid-svg-icons" 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 const INVALID_CHARACTERS = /[^a-zA-Z0-9]/g
export default function FormInlineUser({ submit, ...props }) { /**
* A {@link FormInline} allowing the user to select a Twitter user.
*
* @param props - Additional props to pass to the form.
* @returns {JSX.Element}
* @constructor
*/
export default function FormInlineUser({ ...props }) {
const validate = value => { const validate = value => {
return value.replace(INVALID_CHARACTERS, "") return value.replace(INVALID_CHARACTERS, "")
@ -19,7 +24,6 @@ export default function FormInlineUser({ submit, ...props }) {
textIcon={faAt} textIcon={faAt}
placeholder={"jack"} placeholder={"jack"}
validate={validate} validate={validate}
submit={submit}
{...props} {...props}
/> />
) )

View file

@ -1,10 +1,25 @@
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, faLocationArrow, faMapPin } from "@fortawesome/free-solid-svg-icons" import {
faAt,
faClock,
faFont,
faHashtag,
faLocationArrow,
faMapMarkerAlt,
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"
/**
* Tab selector for the Add Filter box of a RepositoryViewer.
*
* @param props - Additional props to pass to the div.
* @returns {JSX.Element}
* @constructor
*/
export default function PickerFilter({ ...props }) { export default function PickerFilter({ ...props }) {
const { filterTab, setFilterTab, setVisualizationTab } = useContext(ContextRepositoryViewer) const { filterTab, setFilterTab, setVisualizationTab } = useContext(ContextRepositoryViewer)
@ -38,7 +53,7 @@ export default function PickerFilter({ ...props }) {
currentTab={filterTab} currentTab={filterTab}
setTab={setFilterTab} setTab={setFilterTab}
name={"place"} name={"place"}
icon={faLocationArrow} icon={faMapMarkerAlt}
/> />
<ButtonIconOnly <ButtonIconOnly
onClick={() => { onClick={() => {
@ -47,7 +62,7 @@ export default function PickerFilter({ ...props }) {
}} }}
disabled={filterTab === "location"} disabled={filterTab === "location"}
color={"Grey"} color={"Grey"}
icon={faMapPin} icon={faLocationArrow}
/> />
</div> </div>
) )

View file

@ -4,6 +4,13 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
import ButtonPicker from "./ButtonPicker" import ButtonPicker from "./ButtonPicker"
/**
* Tab selector for the Visualization box of a RepositoryViewer.
*
* @param props - Additional props to pass to the div.
* @returns {JSX.Element}
* @constructor
*/
export default function PickerVisualization({ ...props }) { export default function PickerVisualization({ ...props }) {
const { visualizationTab, setVisualizationTab } = useContext(ContextRepositoryViewer) const { visualizationTab, setVisualizationTab } = useContext(ContextRepositoryViewer)

View file

@ -6,6 +6,14 @@ import SummaryText from "../base/summary/SummaryText"
import SummaryRight from "../base/summary/SummaryRight" import SummaryRight from "../base/summary/SummaryRight"
/**
* A {@link SummaryBase} representing a tweet.
*
* @param tweet - The tweet to represent.
* @param props - Additional props to pass to the summary.
* @returns {JSX.Element}
* @constructor
*/
export default function SummaryTweet({ tweet, ...props }) { export default function SummaryTweet({ tweet, ...props }) {
let icon let icon
if(tweet["location"]) { if(tweet["location"]) {

View file

@ -8,6 +8,16 @@ import SummaryButton from "../base/summary/SummaryButton"
import SummaryRight from "../base/summary/SummaryRight" import SummaryRight from "../base/summary/SummaryRight"
/**
* A {@link SummaryBase} representing a N.E.S.T. user.
*
* @param user - The user to represent.
* @param destroyUser - Async function <string> to destroy an user from the frontend.
* @param running - Whether another request is already running.
* @param props - Additional props to pass to the summary.
* @returns {JSX.Element}
* @constructor
*/
export default function SummaryUser({ user, destroyUser, running, ...props }) { export default function SummaryUser({ user, destroyUser, running, ...props }) {
const { strings } = useContext(ContextLanguage) const { strings } = useContext(ContextLanguage)

View file

@ -14,6 +14,7 @@ import isString from "is-string"
* @constructor * @constructor
*/ */
export default function GlobalServer({ children }) { export default function GlobalServer({ children }) {
// TODO: Set this using an envvar
const [server, setServer] = useLocalStorageState("server", "http://127.0.0.1:5000") const [server, setServer] = useLocalStorageState("server", "http://127.0.0.1:5000")
/** /**

View file

@ -50,13 +50,13 @@ export default function GlobalUser({ children }) {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const login = useCallback(async (email, password) => { const login = useCallback(async (email, password) => {
console.debug("Contattando il server per accedere...") console.debug("Contacting the server to login...")
const data = await fetchData("POST", `/api/v1/login`, { const data = await fetchData("POST", `/api/v1/login`, {
"email": email, "email": email,
"password": password, "password": password,
}) })
console.debug("Memorizzando lo stato di login...") console.debug("Saving login state...")
setUser({ setUser({
email: data["user"]["email"], email: data["user"]["email"],
isAdmin: data["user"]["isAdmin"], isAdmin: data["user"]["isAdmin"],
@ -64,18 +64,18 @@ export default function GlobalUser({ children }) {
token: data["access_token"], token: data["access_token"],
}) })
console.info("Accesso effettuato!") console.info("Login successful!")
}, [fetchData, setUser]) }, [fetchData, setUser])
/** /**
* Logout from the currently active server. * Logout from the currently active server.
*/ */
const logout = useCallback(() => { const logout = useCallback(() => {
console.debug("Ripulendo lo stato di login...") console.debug("Clearing login state...")
setUser(null) setUser(null)
console.debug("Stato di login ripulito!") console.debug("Cleared login state!")
console.info("Logout avvenuto con successo!") console.info("Logout successful!")
}, [setUser]) }, [setUser])
return ( return (

View file

@ -2,7 +2,7 @@ import React, { useCallback, useContext, useMemo, useState } from "react"
import ContextRepositoryEditor from "../../contexts/ContextRepositoryEditor" import ContextRepositoryEditor from "../../contexts/ContextRepositoryEditor"
import useArrayState from "../../hooks/useArrayState" import useArrayState from "../../hooks/useArrayState"
import Style from "./RepositoryEditor.module.css" import Style from "./RepositoryEditor.module.css"
import BoxConditionMap from "../interactive/BoxConditionMap" import BoxConditionLocation from "../interactive/BoxConditionLocation"
import BoxConditionHashtag from "../interactive/BoxConditionHashtag" import BoxConditionHashtag from "../interactive/BoxConditionHashtag"
import BoxConditionUser from "../interactive/BoxConditionUser" import BoxConditionUser from "../interactive/BoxConditionUser"
import BoxConditionDatetime from "../interactive/BoxConditionDatetime" import BoxConditionDatetime from "../interactive/BoxConditionDatetime"
@ -142,7 +142,7 @@ export default function RepositoryEditor({
}} }}
> >
<div className={classNames(Style.RepositoryEditor, className)}> <div className={classNames(Style.RepositoryEditor, className)}>
<BoxConditionMap className={Style.SearchByZone}/> <BoxConditionLocation className={Style.SearchByZone}/>
<BoxConditionHashtag className={Style.SearchByHashtags}/> <BoxConditionHashtag className={Style.SearchByHashtags}/>
<BoxConditionUser className={Style.SearchByUser}/> <BoxConditionUser className={Style.SearchByUser}/>
<BoxConditionDatetime className={Style.SearchByTimePeriod}/> <BoxConditionDatetime className={Style.SearchByTimePeriod}/>

View file

@ -24,7 +24,7 @@ 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 BoxFilterLocation from "../interactive/BoxFilterLocation"
import useMapView from "../../hooks/useMapView" import useMapAreaState from "../../hooks/useMapAreaState"
import BoxFilterDatetime from "../interactive/BoxFilterDatetime" import BoxFilterDatetime from "../interactive/BoxFilterDatetime"
import BoxFilterHasPlace from "../interactive/BoxFilterHasPlace" import BoxFilterHasPlace from "../interactive/BoxFilterHasPlace"
@ -44,7 +44,7 @@ export default function RepositoryViewer({ id, className, ...props }) {
} = useArrayState([]) } = useArrayState([])
// FIXME: this has a severe performance impact, investigate // FIXME: this has a severe performance impact, investigate
const mapViewHook = useMapView() const mapViewHook = useMapAreaState()
// Repository // Repository
const repositoryBr = useBackendResource( const repositoryBr = useBackendResource(

View file

@ -8,7 +8,7 @@ import LocalizationStrings from "../LocalizationStrings"
* - `setLang`: a function to change the current language * - `setLang`: a function to change the current language
* - `strings`: an object containing all strings of the current language * - `strings`: an object containing all strings of the current language
* *
* Defaults to Italian. * Defaults to Italian `it`.
*/ */
export default createContext({ export default createContext({
lang: "it", lang: "it",

View file

@ -2,6 +2,8 @@ import { createContext } from "react"
/** /**
* Context to quickly pass props to the children of {@link RepositoryEditor}. * React Context representing containing all variables of a {@link RepositoryEditor}.
*
* It is `null` outside a RepositoryEditor.
*/ */
export default createContext(null) export default createContext(null)

View file

@ -2,6 +2,8 @@ import { createContext } from "react"
/** /**
* Context to quickly pass props to the children of {@link RepositoryViewer}. * React Context representing containing all variables of a {@link RepositoryViewer}.
*
* It is `null` outside a RepositoryViewer.
*/ */
export default createContext(null) export default createContext(null)

View file

@ -2,7 +2,7 @@ import { createContext } from "react"
/** /**
* A context containing an object with the following values: * A React Context containing an object with the following values:
* - `server`: the base URL of the currently active backend server * - `server`: the base URL of the currently active backend server
* - `setServer`: a function to change `server` * - `setServer`: a function to change `server`
* - `fetchData`: a function to fetch JSON data from the backend server * - `fetchData`: a function to fetch JSON data from the backend server

View file

@ -2,7 +2,7 @@ import { createContext } from "react"
/** /**
* A context containing an object with the following elements: * A React Context containing an object with the following elements:
* - `theme` - A string containing the name of the current theme. * - `theme` - A string containing the name of the current theme.
* - `setTheme` - A function that allows changing the `theme`. * - `setTheme` - A function that allows changing the `theme`.
* *

View file

@ -2,7 +2,7 @@ import { createContext } from "react"
/** /**
* A context containing an object with the following values: * A React Context containing an object with the following values:
* - `user`: an object containing data about the currently logged in user * - `user`: an object containing data about the currently logged in user
* - `login`: a function accepting `email, password` as parameters which tries to login the user * - `login`: a function accepting `email, password` as parameters which tries to login the user
* - `logout`: a function accepting no parameters which logs the user out * - `logout`: a function accepting no parameters which logs the user out

View file

@ -1,3 +0,0 @@
# Contexts
In questa cartella sono contenuti i `Context` globali di React.

View file

@ -2,7 +2,7 @@ import { useCallback, useState } from "react"
/** /**
* An hook similar to {@link useState} which stores an array of values. * An hook similar to {@link useState} which stores an array instead of a single value.
* *
* @param def - The starting value of the hook. * @param def - The starting value of the hook.
* @returns {{spliceValue, removeValue, setValue, appendValue, value}} * @returns {{spliceValue, removeValue, setValue, appendValue, value}}
@ -12,7 +12,7 @@ export default function useArrayState(def) {
const appendValue = useCallback( const appendValue = useCallback(
newSingle => { newSingle => {
console.debug("Aggiungendo ", newSingle, " ad ArrayState") console.debug("Adding ", newSingle, " to ArrayState")
setValue( setValue(
oldArray => [...oldArray, newSingle], oldArray => [...oldArray, newSingle],
) )
@ -22,7 +22,7 @@ export default function useArrayState(def) {
const spliceValue = useCallback( const spliceValue = useCallback(
position => { position => {
console.debug("Estraendo ", position, " da ArrayState") console.debug("Splicing ", position, " from ArrayState")
setValue( setValue(
oldArray => { oldArray => {
oldArray.splice(position, 1) oldArray.splice(position, 1)

View file

@ -1,17 +0,0 @@
/* eslint-disable */
import { useEffect } from "react"
/**
* {@link useEffect}, but with an async effect.
*
* @warning Breaks `react-hooks/exaustive-deps`.
*
* @param effect - The async effect.
* @param deps - The dependencies of the hook.
*/
export default function useAsyncEffect(effect, deps) {
useEffect(() => {
effect()
}, [effect, ...deps])
}

View file

@ -1,7 +1,6 @@
import { useCallback, useContext, useState } from "react" import { useCallback, useContext, useState } from "react"
import ContextServer from "../contexts/ContextServer" import ContextServer from "../contexts/ContextServer"
import ContextUser from "../contexts/ContextUser" import ContextUser from "../contexts/ContextUser"
import makeURLSearchParams from "../utils/makeURLSearchParams"
/** /**
@ -45,7 +44,7 @@ export default function useBackendRequest() {
// Use the body param as either search parameter or request body // Use the body param as either search parameter or request body
if(body) { if(body) {
if(["GET", "HEAD"].includes(method.toUpperCase())) { if(["GET", "HEAD"].includes(method.toUpperCase())) {
path += makeURLSearchParams(body).toString() path += URLSearchParams.fromSerializableObject(body).toString()
} }
else { else {
init["body"] = JSON.stringify(body) init["body"] = JSON.stringify(body)

View file

@ -2,7 +2,8 @@ import { useCallback, useState } from "react"
/** /**
* Hook with the same API as {@link React.useState} which stores its value in the browser's {@link localStorage}. * Hook with the same API as {@link React.useState} which additionally stores its value in the browser's
* {@link localStorage}.
*/ */
export default function useLocalStorageState(key, def) { export default function useLocalStorageState(key, def) {
/** /**

View file

@ -0,0 +1,21 @@
import { useState } from "react"
import Coordinates from "../objects/Coordinates"
import MapArea from "../objects/MapArea"
/**
* Hook which holds values required to create a {@link MapArea}.
*/
export default function useMapAreaState() {
const [zoom, setZoom] = useState(3)
const [center, setCenter] = useState(new Coordinates(0, 0))
const mapArea = MapArea.fromZoomLevel(zoom, center)
return {
zoom,
setZoom,
center,
setCenter,
mapArea,
}
}

View file

@ -1,18 +0,0 @@
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,10 +1,9 @@
import { useContext } from "react" import { useContext } from "react"
import ContextRepositoryEditor from "../contexts/ContextRepositoryEditor"
import ContextRepositoryViewer from "../contexts/ContextRepositoryViewer" import ContextRepositoryViewer from "../contexts/ContextRepositoryViewer"
/** /**
* Hook to quickly use {@link ContextRepositoryEditor}. * Hook to quickly use {@link ContextRepositoryViewer}.
*/ */
export default function useRepositoryViewer() { export default function useRepositoryViewer() {
const context = useContext(ContextRepositoryViewer) const context = useContext(ContextRepositoryViewer)

View file

@ -3,7 +3,7 @@ import ContextLanguage from "../contexts/ContextLanguage"
/** /**
* Hook to quickly use the strings of {@link ContextLanguage}. * Hook to quickly use the `strings` attribute of {@link ContextLanguage}.
*/ */
export default function useStrings() { export default function useStrings() {
return useContext(ContextLanguage).strings return useContext(ContextLanguage).strings

View file

@ -3,6 +3,7 @@ import ReactDOM from "react-dom"
import "./index.css" import "./index.css"
import App from "./App" import App from "./App"
import reportWebVitals from "./reportWebVitals" import reportWebVitals from "./reportWebVitals"
import "./prototypes"
ReactDOM.render( ReactDOM.render(

View file

@ -1,3 +0,0 @@
# Media
In questa cartella sono contenute le immagini statiche del sito web.

View file

@ -0,0 +1,135 @@
import {
IconDefinition,
faQuestionCircle,
faHashtag,
faAt,
faClock,
faLocationArrow,
} from "@fortawesome/free-solid-svg-icons"
/**
* Condition class for an undefined/unknown condition.
*
* See [the Condition spec](https://gitlab.steffo.eu/nest/g2-progetto/-/wikis/sprint-2/Specifica-delle-Conditions).
*/
export class Condition {
content
type
id
constructor(type, content, id = null) {
this.content = content
this.type = type
this.id = id
}
/**
* Get the condition as an object readable by the backend.
*
* @returns {{id, type, content}}
*/
serialize() {
return {
type: this.type,
content: this.content,
id: this.id,
}
}
/**
* Display parameters for the badge representing this condition.
*
* @returns {{color: string, icon: IconDefinition, title, content}}
*/
display() {
return {
color: "Grey",
icon: faQuestionCircle,
title: this.id,
children: this.content,
}
}
}
/**
* Require a tweet to contain a specific hashtag to be gathered.
*/
export class ConditionHashtag extends Condition {
constructor(hashtag, id = null) {
super(0, hashtag, id)
}
display() {
return {
color: "Grey",
icon: faHashtag,
title: this.id,
children: this.content,
}
}
}
/**
* Require a tweet to be posted by a certain user to be gathered.
*/
export class ConditionUser extends Condition {
constructor(user, id = null) {
super(5, user, id)
}
display() {
return {
color: "Green",
icon: faAt,
title: this.id,
children: this.content,
}
}
}
/**
* Require a tweet to be posted before or after a certain time to be gathered.
*/
export class ConditionTime extends Condition {
timeRay
constructor(timeRay, id = null) {
super(2, timeRay.toString(), id)
this.timeRay = timeRay
}
display() {
return {
color: "Yellow",
icon: faClock,
title: this.id,
children: this.content,
}
}
}
/**
* Require a tweet to have coordinates associated and to be posted within the {@link MapArea}.
*/
export class ConditionLocation extends Condition {
mapArea
constructor(mapArea, id = null) {
super(3, mapArea.toString(), id)
this.mapArea = mapArea
}
display() {
return {
color: "Red",
icon: faLocationArrow,
title: this.id,
children: this.mapArea.toHumanString(),
}
}
}

View file

@ -0,0 +1,56 @@
import { Condition, ConditionHashtag, ConditionLocation, ConditionTime, ConditionUser } from "./Condition"
import TimeRay from "./TimeRay"
import MapArea from "./MapArea"
import Coordinates from "./Coordinates"
test("Condition can be constructed", () => {
expect(new Condition(0, "hi")).toBeTruthy()
expect(new Condition(0, "hi", 1)).toBeTruthy()
})
test("ConditionHashtag can be constructed", () => {
expect(new ConditionHashtag("PdS2021")).toBeTruthy()
expect(new ConditionHashtag("PdS2021", 1)).toBeTruthy()
})
test("ConditionUser can be constructed", () => {
expect(new ConditionUser("USteffo")).toBeTruthy()
expect(new ConditionUser("USteffo", 1)).toBeTruthy()
})
test("ConditionTime can be constructed", () => {
const now = new Date()
const timeRay = new TimeRay(true, now)
expect(new ConditionTime(timeRay)).toBeTruthy()
expect(new ConditionTime(timeRay, 1)).toBeTruthy()
})
test("ConditionLocation can be constructed", () => {
const mapArea = new MapArea(1000, new Coordinates(0.000, 0.000))
expect(new ConditionLocation(mapArea)).toBeTruthy()
expect(new ConditionLocation(mapArea, 1)).toBeTruthy()
})
test("ConditionHashtag has the correct type", () => {
expect(new ConditionHashtag("PdS2021").type).toBe(0)
})
test("ConditionUser has the correct type", () => {
expect(new ConditionUser("USteffo").type).toBe(5)
})
test("ConditionTime has the correct type", () => {
const now = new Date()
const timeRay = new TimeRay(true, now)
expect(new ConditionTime(timeRay).type).toBe(5)
})
test("ConditionLocation has the correct type", () => {
const mapArea = new MapArea(1000, new Coordinates(0.000, 0.000))
expect(new ConditionLocation(mapArea).type).toBe(3)
})

View file

@ -0,0 +1,77 @@
/**
* A pair of coordinates, latitude `lat` and longitude `lng`.
*/
import { LatLng } from "leaflet/dist/leaflet-src.esm"
export default class Coordinates {
lat
lng
/**
* @param lat - Latitude.
* @param lng - Longitude.
*/
constructor(lat, lng) {
this.lat = lat
this.lng = lng
}
/**
* Create a new {@link Coordinates} from the format used by the backend.
*
* @param str - The string to create the object from.
* @returns {Coordinates}
*/
static fromCrawlerString(str) {
const match = /[{]([0-9.]+),([0-9.]+)[}]/.exec(str)
if(!match) {
throw new Error(`Invalid location string: ${str}`)
}
return new Coordinates(match[1], match[2])
}
/**
* @returns {string}
*/
toString() {
return `${this.lat.toFixed(7)} ${this.lng.toFixed(7)}`
}
/**
* Render the Coordinates as an human-readable string.
*
* @returns {string}
*/
toHumanString() {
return `${this.lat.toFixed(3)} ${this.lng.toFixed(3)}`
}
/**
* Transform this object in a Geolib compatible-one.
*/
toGeolib() {
return {
latitude: this.lat,
longitude: this.lng,
}
}
/**
* Transform this object in a 2-ple.
*
* @returns {[Number, Number]}
*/
toArray() {
return [this.lat, this.lng]
}
/**
* Transform this object in a {@link LatLng} / Leaflet compatible-one.
*
* @returns {LatLng}
*/
toLatLng() {
return new LatLng(this.lat, this.lng)
}
}

View file

@ -0,0 +1,104 @@
/**
* Error thrown when a function is not implemented in the current class/instance.
*/
class NotImplementedError {
name
constructor(name) {
this.name = name
}
}
/**
* An error in the N.E.S.T. frontend-backend communication.
*/
class BackendCommunicationError {
}
/**
* Error thrown when trying to access a backend view which doesn't exist or isn't allowed in the used hook.
*/
class ViewNotAllowedError extends BackendCommunicationError {
view
constructor(view) {
super()
this.view = view
}
}
/**
* Error thrown when trying to access a backend view when outside a {@link ContextServer}.
*/
class ServerNotConfiguredError extends BackendCommunicationError {
}
/**
* Error thrown when trying to access a backend view while another access is ongoing.
*
* This is not allowed due to potential race conditions.
*/
class FetchAlreadyRunningError extends BackendCommunicationError {
}
/**
* Abstract class for {@link DecodeError} and {@link ResultError}.
*/
class FetchError extends BackendCommunicationError {
status
statusText
constructor(status, statusText) {
super()
this.status = status
this.statusText = statusText
}
}
/**
* Error thrown when the frontend can't parse the data received from the backend.
*/
class DecodeError extends FetchError {
error
constructor(status, statusText, error) {
super(status, statusText)
this.error = error
}
}
/**
* Error thrown when the backend returns a falsy `"result"` value.
*/
class ResultError extends FetchError {
status
statusText
data
constructor(status, statusText, data) {
super(status, statusText)
this.data = data
}
getMsg() {
return this.data.msg
}
getCode() {
return this.data.code
}
}

View file

@ -0,0 +1,222 @@
import {
faAt,
faClock,
faFilter,
faFont,
faHashtag,
faLocationArrow,
faMapMarkerAlt,
faMapPin,
} from "@fortawesome/free-solid-svg-icons"
/**
* A filter applicable in the Analysis mode.
*/
export class Filter {
negate
/**
* @param negate - If the filter output should be reversed.
*/
constructor(negate = false) {
this.negate = negate
}
/**
* Check if a tweet passed through the filter or not, without applying `negate`.
*
* @param tweet - The tweet to check.
* @returns {boolean}
*/
check(tweet) {
return true
}
/**
* Check if a tweet passed through the filter or not, applying `negate`.
*
* @param tweet - The tweet to check.
* @returns {boolean}
*/
exec(tweet) {
return Boolean(this.check(tweet) ^ this.negate)
}
display() {
return {
color: "Grey",
icon: faFilter,
children: this.negate ? "False" : "True"
}
}
}
/**
* Checks if a tweet contains a string.
*/
export class FilterContains extends Filter {
string
constructor(string, negate = false) {
super(negate)
this.string = string.toLowerCase().trim()
}
check(tweet) {
return tweet.content?.toLowerCase().includes(this.string)
}
display() {
return {
color: "Grey",
icon: faFont,
children: this.string
}
}
}
/**
* Check if a tweet contains an hashtag.
*/
export class FilterHashtag extends FilterContains {
hashtag
constructor(hashtag, negate = false) {
super(`#${hashtag}`, negate)
this.hashtag = hashtag
}
display() {
return {
color: "Grey",
icon: faHashtag,
children: this.hashtag
}
}
}
/**
* Check if a tweet was posted by a certain user.
*/
export class FilterPoster extends Filter {
poster
constructor(poster, negate = false) {
super(negate)
this.poster = poster
}
check(tweet) {
return tweet.poster.toLowerCase() === this.poster.toLowerCase()
}
display() {
return {
color: "Green",
icon: faAt,
children: this.poster
}
}
}
/**
* Check if a tweet contains `location` metadata.
*/
export class FilterWithLocation extends Filter {
constructor(negate = false) {
super(negate)
}
check(tweet) {
return Boolean(tweet["location"])
}
display() {
return {
color: "Red",
icon: faLocationArrow,
children: ""
}
}
}
/**
* Check if a tweet contains `place` metadata.
*/
export class FilterWithPlace extends Filter {
constructor(negate = false) {
super(negate)
}
check(tweet) {
return Boolean(tweet["place"])
}
display() {
return {
color: "Red",
icon: faMapMarkerAlt,
children: ""
}
}
}
/**
* Check if a tweet's `location` is inside a {@link MapArea}.
*/
export class FilterInsideMapArea extends FilterWithLocation {
mapArea
constructor(mapArea, negate = false) {
super(negate)
this.mapArea = mapArea
}
check(tweet) {
if(!super.check(tweet)) {
return false
}
return this.mapArea.includes(tweet.location)
}
display() {
return {
color: "Red",
icon: faLocationArrow,
children: this.mapArea.toHumanString()
}
}
}
/**
* Check if a tweet's `post_time` is inside a {@link TimeRay}.
*/
export class FilterInsideTimeRay extends Filter {
timeRay
constructor(timeRay, negate = false) {
super(negate)
this.timeRay = timeRay
}
check(tweet) {
return this.datetime < new Date(tweet["insert_time"])
}
display() {
return {
color: "Yellow",
icon: faClock,
children: this.timeRay.toString()
}
}
}

View file

@ -0,0 +1,64 @@
import {getDistance} from "geolib"
import osmZoomLevels from "../utils/osmZoomLevels"
/**
* An area on a map, defined by a `center` and a `radius` in meters.
*/
export default class MapArea {
radius
center
/**
* @param radius - The radius of the area in meters.
* @param center - The center of the area.
*/
constructor(radius, center) {
this.radius = radius
this.center = center
}
/**
* Create a new {@link MapArea} from the [zoom level of OpenStreetMaps][1], assuming the window is
* ~400 pixels large.
*
* [1]: https://wiki.openstreetmap.org/wiki/Zoom_levels
*
* @param zoom
* @param center
* @returns {MapArea}
*/
static fromZoomLevel(zoom, center) {
return new MapArea(osmZoomLevels[zoom], center)
}
/**
* @returns {string}
*/
toString() {
return `< ${this.radius} ${this.center.toString()}`
}
/**
* Render the {@link MapArea} as an human-readable string.
*
* @returns {string}
*/
toHumanString() {
if(this.radius >= 2000) {
const kmRadius = Math.round(this.radius / 1000)
return `< ${kmRadius}km ${this.center.toHumanString()}`
}
return `< ${this.radius}m ${this.center.toHumanString()}`
}
/**
* Check if a pair of coordinates is included in the area.
*
* @param coords - The coordinates to check.
* @returns {boolean}
*/
includes(coords) {
return getDistance(this.center.toGeolib(), coords.toGeolib()) <= this.radius
}
}

View file

@ -0,0 +1,13 @@
import Coordinates from "./Coordinates"
import MapArea from "./MapArea"
test("MapArea can be constructed", () => {
const mapArea = new MapArea(1000, new Coordinates(0.0, 0.0))
expect(mapArea).toBeTruthy()
})
test("MapArea can be rendered to a spec-compatible string", () => {
const mapArea = new MapArea(1000, new Coordinates(0.0, 0.0))
expect(mapArea.toString()).toBe("< 1000 0.0000000 0.0000000")
})

View file

@ -0,0 +1,28 @@
/**
* An half-line of time, defined by a `date` and a boolean `isBefore` indicating if the time before or after the
* specified date should be selected.
*/
export default class TimeRay {
isBefore
date
/**
* @param isBefore - `true` to select times earlier than the date, `false` to select times after the date.
* @param date - The date to start measurements from.
*/
constructor(isBefore, date) {
this.isBefore = isBefore
this.date = date
}
/**
* @returns {string}
*/
toString() {
return `${this.isBefore ? "<" : ">"} ${this.date.toISOString()}`
}
includes(date) {
return Boolean((this.date > date) ^ this.isBefore)
}
}

View file

@ -1,20 +1,10 @@
// Wow, JS, davvero? Date.prototype.toAwareISOString = function() {
// Davvero tutte le date.toISOString() sono considerate UTC? if(this.toString() === "Invalid Date") {
// Wow.
/**
* Convert a {@link Date} object to a timezone aware ISO String, using the user's local timezone.
*
* @param date
* @returns {string}
*/
export default function convertToLocalISODate(date) {
if(date.toString() === "Invalid Date") {
throw new Error("Data non valida ricevuta come parametro.") throw new Error("Data non valida ricevuta come parametro.")
} }
// Create a timezone naive ISO string // Create a timezone naive ISO string
const naive = date.toISOString() const naive = this.toISOString()
// Find the local timezone // Find the local timezone
const tz = -new Date().getTimezoneOffset() const tz = -new Date().getTimezoneOffset()

View file

@ -1,7 +1,7 @@
import isString from "is-string" import isString from "is-string"
export default function makeURLSearchParams(obj) { URLSearchParams.fromSerializableObject = function(obj) {
let usp = new URLSearchParams() let usp = new URLSearchParams()
for(const key in obj) { for(const key in obj) {
if(!obj.hasOwnProperty(key)) { if(!obj.hasOwnProperty(key)) {

2
nest_frontend/prototypes/index.js vendored Normal file
View file

@ -0,0 +1,2 @@
import "./Date"
import "./URLSearchParams"

View file

@ -1,3 +0,0 @@
# Routes
In questa cartella sono contenuti i `Component` che vengono renderati come pagine intere.

View file

@ -1,40 +0,0 @@
import isString from "is-string"
const typeEnums = {
"HASHTAG": 0,
"TIME": 2,
"COORDINATES": 3,
"PLACE": 4,
"USER": 5,
}
/**
* A search/filtering Condition.
*
* See https://gitlab.steffo.eu/nest/g2-progetto/-/wikis/Specifica-delle-Conditions .
*/
export default class Condition {
/**
* Create a new Condition.
*
* @param type - The type of Condition to create.
* It can be a number or one of the following strings:
* `"hashtag"`, `"time"`, `"coordinates"`, `"place"`.
* @param content - The content of the Condition.
* @param id - The id of the Condition on the backend, or null if the Condition hasn't been committed yet.
*/
constructor(type, content, id = null) {
if(isString(type)) {
this.type = typeEnums[type.toUpperCase()]
}
else {
this.type = type
}
this.content = content
this.id = id
}
}

View file

@ -1,69 +0,0 @@
class NestError {
}
class ViewNotAllowedError extends NestError {
view
constructor(view) {
super()
this.view = view
}
}
class ServerNotConfiguredError extends NestError {
}
class FetchAlreadyRunningError extends NestError {
}
class FetchError extends NestError {
status
statusText
constructor(status, statusText) {
super()
this.status = status
this.statusText = statusText
}
}
class DecodeError extends FetchError {
error
constructor(status, statusText, error) {
super(status, statusText)
this.error = error
}
}
class ResultError extends FetchError {
status
statusText
data
constructor(status, statusText, data) {
super(status, statusText)
this.data = data
}
getMsg() {
return this.data.msg
}
getCode() {
return this.data.code
}
}

View file

@ -1,221 +0,0 @@
import { Location } from "./location"
import {
faAt,
faClock,
faFilter,
faFont,
faHashtag,
faLocationArrow,
faMapMarkerAlt,
faMapPin,
} from "@fortawesome/free-solid-svg-icons"
export class Filter {
negate
constructor(negate) {
this.negate = negate
}
check(tweet) {
return true
}
exec(tweet) {
return this.check(tweet) ^ this.negate
}
color() {
return "Grey"
}
icon() {
return faFilter
}
text() {
return this.negate ? "False" : "True"
}
}
export class ContainsFilter extends Filter {
word
constructor(negate, word) {
super(negate)
this.word = word.toLowerCase().trim()
}
check(tweet) {
return tweet.content?.toLowerCase().includes(this.word)
}
color() {
return "Grey"
}
icon() {
return faFont
}
text() {
return this.word
}
}
export class HashtagFilter extends ContainsFilter {
hashtag
constructor(negate, hashtag) {
super(negate, `#${hashtag}`)
this.hashtag = hashtag
}
icon() {
return faHashtag
}
text() {
return this.hashtag
}
}
export class UserFilter extends Filter {
user
constructor(negate, user) {
super(negate)
this.user = user.toLowerCase().trim().replace(/^@/, "")
}
check(tweet) {
return tweet.poster.toLowerCase() === this.user
}
color() {
return "Green"
}
icon() {
return faAt
}
text() {
return this.user
}
}
export class HasLocationFilter extends Filter {
constructor(negate) {
super(negate)
}
check(tweet) {
return Boolean(tweet["location"])
}
color() {
return "Red"
}
icon() {
return faMapMarkerAlt
}
text() {
return ""
}
}
export class HasPlaceFilter extends Filter {
constructor(negate) {
super(negate)
}
check(tweet) {
return Boolean(tweet["place"])
}
color() {
return "Red"
}
icon() {
return faLocationArrow
}
text() {
return ""
}
}
export class LocationRadiusFilter extends HasLocationFilter {
center
radius
constructor(negate, center, radius) {
super(negate)
this.center = center
this.radius = radius
}
check(tweet) {
if(!super.check(tweet)) {
return false
}
// 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(this.radius, 2)
return squaredDistance < squaredRadius
}
color() {
return "Red"
}
icon() {
return faMapPin
}
text() {
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

@ -1,3 +0,0 @@
# Utils
In questa cartella sono contenute alcune funzioni di utility per il sito.

View file

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

View file

@ -4,7 +4,6 @@
* @param func - The function to decorate. * @param func - The function to decorate.
* @param history - The history to push the destination to. * @param history - The history to push the destination to.
* @param destination - The path of the destination. * @param destination - The path of the destination.
* @returns {(function(): void)|*}
*/ */
export default function goToOnSuccess(func, history, destination) { export default function goToOnSuccess(func, history, destination) {
return async (...args) => { return async (...args) => {

View file

@ -1,37 +0,0 @@
export const locationRegex = /[{](?<lat>[0-9.]+),(?<lng>[0-9.]+)[}]/
export class Location {
lat
lng
constructor(lat, lng) {
this.lat = lat
this.lng = lng
}
static fromString(locString) {
const match = locationRegex.exec(locString)
if(!match) {
throw new Error(`Invalid location string: ${locString}`)
}
const { lat, lng } = match.groups
return new Location(lat, lng)
}
static fromTweet(tweet) {
if(tweet.location === null) {
throw new Error(`Tweet has no location: ${tweet}`)
}
return Location.fromString(tweet.location)
}
toArray() {
return [this.lat, this.lng]
}
toString() {
return `${this.lat.toFixed(3)} ${this.lng.toFixed(3)}`
}
}

View file

@ -0,0 +1,29 @@
import React, { isValidElement } from "react"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import {IconDefinition} from "@fortawesome/fontawesome-svg-core"
/**
* Try to create an icon element based on what is passed to the function:
* - If a {@link JSX.Element} is passed, a `<span>` element containing it will be created and returned.
* - If a {@link IconDefinition} is passed, a `<span>` element containing a {@link FontAwesomeIcon} will be created
* and returned.
* - If a falsy value is passed, `null` will be returned.
*
* @param icon - The icon value.
* @param props - Props to pass to the span element when it is created.
* @returns {JSX.Element|null}
*/
export default function makeIcon(icon, props) {
if(isValidElement(icon)) {
return <span {...props}>{icon}</span>
}
else if(icon) {
return (
<span {...props}><FontAwesomeIcon icon={icon}/></span>
)
}
else {
return null
}
}

View file

@ -1,17 +0,0 @@
import React, { isValidElement } from "react"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
export default function make_icon(icon, className) {
if(isValidElement(icon)) {
return <span className={className}>icon</span>
}
else if(icon) {
return (
<span className={className}><FontAwesomeIcon icon={icon}/></span>
)
}
else {
return null
}
}

11
package-lock.json generated
View file

@ -20,6 +20,7 @@
"@testing-library/user-event": "^12.8.3", "@testing-library/user-event": "^12.8.3",
"chart.js": "^3.2.1", "chart.js": "^3.2.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"geolib": "^3.3.1",
"is-string": "^1.0.5", "is-string": "^1.0.5",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"react": "^17.0.2", "react": "^17.0.2",
@ -9416,6 +9417,11 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/geolib": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/geolib/-/geolib-3.3.1.tgz",
"integrity": "sha512-sfahBXFcgELdpumDZV5b3KWiINkZxC5myAkLk067UUcTmTXaiE9SWmxMEHztn/Eus4JX6kesHxaIuZlniYgUtg=="
},
"node_modules/get-caller-file": { "node_modules/get-caller-file": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@ -30086,6 +30092,11 @@
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="
}, },
"geolib": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/geolib/-/geolib-3.3.1.tgz",
"integrity": "sha512-sfahBXFcgELdpumDZV5b3KWiINkZxC5myAkLk067UUcTmTXaiE9SWmxMEHztn/Eus4JX6kesHxaIuZlniYgUtg=="
},
"get-caller-file": { "get-caller-file": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",

View file

@ -18,6 +18,7 @@
"@testing-library/user-event": "^12.8.3", "@testing-library/user-event": "^12.8.3",
"chart.js": "^3.2.1", "chart.js": "^3.2.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"geolib": "^3.3.1",
"is-string": "^1.0.5", "is-string": "^1.0.5",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",
"react": "^17.0.2", "react": "^17.0.2",