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:
commit
10baee0215
85 changed files with 1118 additions and 689 deletions
|
@ -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" />
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
|
|
||||||
.Button[disabled] {
|
.Button[disabled] {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button:focus-visible {
|
.Button:focus-visible {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
.Input {
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Button {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
<FontAwesomeIcon icon={faMapPin}/>
|
<FontAwesomeIcon icon={faLocationArrow}/>
|
||||||
|
|
||||||
{strings.byZone}
|
{strings.byZone}
|
||||||
</span>
|
</span>
|
|
@ -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
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
<FontAwesomeIcon icon={faLocationArrow}/>
|
<FontAwesomeIcon icon={faMapMarkerAlt}/>
|
||||||
|
|
||||||
{strings.byHasPlace}
|
{strings.byHasPlace}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
<FontAwesomeIcon icon={faMapPin}/>
|
<FontAwesomeIcon icon={faLocationArrow}/>
|
||||||
|
|
||||||
{strings.byZone}
|
{strings.byZone}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
.BoxRepositoryCreate {
|
|
||||||
|
|
||||||
}
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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("")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"]}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"]) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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}/>
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Contexts
|
|
||||||
|
|
||||||
In questa cartella sono contenuti i `Context` globali di React.
|
|
|
@ -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)
|
||||||
|
|
|
@ -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])
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
/**
|
/**
|
||||||
|
|
21
nest_frontend/hooks/useMapAreaState.js
Normal file
21
nest_frontend/hooks/useMapAreaState.js
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Media
|
|
||||||
|
|
||||||
In questa cartella sono contenute le immagini statiche del sito web.
|
|
135
nest_frontend/objects/Condition.js
Normal file
135
nest_frontend/objects/Condition.js
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
nest_frontend/objects/Condition.test.js
Normal file
56
nest_frontend/objects/Condition.test.js
Normal 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)
|
||||||
|
})
|
77
nest_frontend/objects/Coordinates.js
Normal file
77
nest_frontend/objects/Coordinates.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
104
nest_frontend/objects/Errors.js
Normal file
104
nest_frontend/objects/Errors.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
222
nest_frontend/objects/Filter.js
Normal file
222
nest_frontend/objects/Filter.js
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
64
nest_frontend/objects/MapArea.js
Normal file
64
nest_frontend/objects/MapArea.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
13
nest_frontend/objects/MapArea.test.js
Normal file
13
nest_frontend/objects/MapArea.test.js
Normal 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")
|
||||||
|
})
|
28
nest_frontend/objects/TimeRay.js
Normal file
28
nest_frontend/objects/TimeRay.js
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
|
@ -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
2
nest_frontend/prototypes/index.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import "./Date"
|
||||||
|
import "./URLSearchParams"
|
|
@ -1,3 +0,0 @@
|
||||||
# Routes
|
|
||||||
|
|
||||||
In questa cartella sono contenuti i `Component` che vengono renderati come pagine intere.
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()}`
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
# Utils
|
|
||||||
|
|
||||||
In questa cartella sono contenute alcune funzioni di utility per il sito.
|
|
|
@ -1,2 +0,0 @@
|
||||||
export const DEFAULT_MAP_CENTER = { lat: 0, lng: 0 }
|
|
||||||
export const DEFAULT_MAP_ZOOM = 3
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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)}`
|
|
||||||
}
|
|
||||||
}
|
|
29
nest_frontend/utils/makeIcon.js
Normal file
29
nest_frontend/utils/makeIcon.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
11
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue