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>
|
||||
</option>
|
||||
</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="RegExpOctalEscape" 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.collectCoverageFrom = config.collectCoverageFrom.map(root => root.replace("src", "nest_frontend"))
|
||||
config.testMatch = config.testMatch.map(root => root.replace("src", "nest_frontend"))
|
||||
console.debug(config)
|
||||
return config;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
// Link.react.test.js
|
||||
import React from "react"
|
||||
import "@testing-library/jest-dom/extend-expect"
|
||||
import { render, screen } from "@testing-library/react"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react"
|
||||
import Style from "./Button.module.css"
|
||||
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}
|
||||
{...props}
|
||||
>
|
||||
{children} {make_icon(icon, Style.Icon)}
|
||||
{children} {makeIcon(icon, {className: Style.Icon})}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
|
||||
.Button[disabled] {
|
||||
opacity: 0.5;
|
||||
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Button:focus-visible {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react"
|
||||
import Style from "./ButtonSidebar.module.css"
|
||||
import classNames from "classnames"
|
||||
import make_icon from "../../utils/make_icon"
|
||||
import makeIcon from "../../utils/makeIcon"
|
||||
import { Link } from "react-router-dom"
|
||||
import { useRouteMatch } from "react-router"
|
||||
|
||||
|
@ -31,7 +31,7 @@ export default function ButtonSidebar({ icon, children, to, className, ...props
|
|||
return (
|
||||
<Link to={to} className={Style.ButtonLink}>
|
||||
<div className={classNames(Style.ButtonSidebar, "Clickable", className)} {...props}>
|
||||
{make_icon(icon, Style.ButtonIcon)}
|
||||
{makeIcon(icon, {className: Style.ButtonIcon})}
|
||||
<div className={Style.ButtonText}>
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from "react"
|
||||
import Style from "./InputWithIcon.module.css"
|
||||
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 (
|
||||
<div className={classNames(Style.InputWithIcon, isFocused ? Style.Focused : null, className)}>
|
||||
<div className={Style.IconPart}>
|
||||
{make_icon(icon)}
|
||||
{makeIcon(icon)}
|
||||
</div>
|
||||
<input
|
||||
className={Style.InputPart}
|
||||
|
|
|
@ -1,65 +1,22 @@
|
|||
import React, { useContext } from "react"
|
||||
import { faAt, faClock, faGlobe, faHashtag, faMapPin } from "@fortawesome/free-solid-svg-icons"
|
||||
import ContextRepositoryEditor from "../../contexts/ContextRepositoryEditor"
|
||||
import Badge from "../base/Badge"
|
||||
|
||||
|
||||
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}
|
||||
* @constructor
|
||||
*/
|
||||
export default function BadgeCondition({ ...condition }) {
|
||||
const { id, type, content } = condition
|
||||
const color = CONDITION_COLORS[type]
|
||||
const icon = CONDITION_ICONS[type]
|
||||
export default function BadgeCondition({ condition }) {
|
||||
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 (
|
||||
<Badge
|
||||
title={id ? `💠 Condition ID: ${id}` : "✨ New Condition"}
|
||||
color={color}
|
||||
icon={icon}
|
||||
onClickDelete={() => {
|
||||
console.debug(`Removing Condition: `, condition)
|
||||
removeRawCondition(condition)
|
||||
}}
|
||||
>
|
||||
{displayedContent}
|
||||
</Badge>
|
||||
{...condition.display()}
|
||||
onClickDelete={() => removeRawCondition(condition)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 Badge from "../base/Badge"
|
||||
import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
||||
import Badge from "../base/Badge"
|
||||
|
||||
|
||||
/**
|
||||
|
@ -15,11 +15,8 @@ export default function BadgeFilter({ filter }) {
|
|||
|
||||
return (
|
||||
<Badge
|
||||
color={filter.color()}
|
||||
icon={filter.icon()}
|
||||
{...filter.display()}
|
||||
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faClock } from "@fortawesome/free-solid-svg-icons"
|
||||
import useRepositoryEditor from "../../hooks/useRepositoryEditor"
|
||||
import Condition from "../../utils/Condition"
|
||||
import convertToLocalISODate from "../../utils/convertToLocalISODate"
|
||||
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
|
||||
* to the {@link ContextRepositoryEditor}.
|
||||
* A {@link BoxFull} that allows the user to select a Twitter user to search for, and then to add it as a
|
||||
* {@link ConditionTime} of a RepositoryEditor.
|
||||
*
|
||||
* @param props - Additional props to pass to the box.
|
||||
* @returns {JSX.Element}
|
||||
|
@ -21,14 +20,10 @@ export default function BoxConditionDatetime({ ...props }) {
|
|||
const { addCondition } = useRepositoryEditor()
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
|
||||
const submit = ({ date, isBefore }) => {
|
||||
if(date.toString() === "Invalid Date") {
|
||||
console.debug("Refusing to add condition: ", date, " is an Invalid Date.")
|
||||
return
|
||||
}
|
||||
const aware = convertToLocalISODate(date)
|
||||
addCondition(new Condition("TIME", `${isBefore ? "<" : ">"} ${aware}`))
|
||||
}
|
||||
const submit = useCallback(
|
||||
timeRay => addCondition(new ConditionTime(timeRay)),
|
||||
[addCondition]
|
||||
)
|
||||
|
||||
return (
|
||||
<BoxFull
|
||||
|
@ -43,7 +38,7 @@ export default function BoxConditionDatetime({ ...props }) {
|
|||
}
|
||||
{...props}
|
||||
>
|
||||
<FormInlineBADatetime
|
||||
<FormInlineTimeRay
|
||||
submit={submit}
|
||||
/>
|
||||
</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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faHashtag } from "@fortawesome/free-solid-svg-icons"
|
||||
import useRepositoryEditor from "../../hooks/useRepositoryEditor"
|
||||
import Condition from "../../utils/Condition"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
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
|
||||
* to the {@link ContextRepositoryEditor}.
|
||||
* A {@link BoxFull} that allows the user to select a Twitter hashtag to search for, and then to add it as a
|
||||
* {@link ConditionHashtag} of a RepositoryEditor.
|
||||
*
|
||||
* @param props - Additional props to pass to the box.
|
||||
* @returns {JSX.Element}
|
||||
|
@ -20,9 +20,10 @@ export default function BoxConditionHashtag({ ...props }) {
|
|||
const { addCondition } = useRepositoryEditor()
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
|
||||
const submit = value => {
|
||||
addCondition(new Condition("HASHTAG", value))
|
||||
}
|
||||
const submit = useCallback(
|
||||
value => addCondition(new ConditionHashtag(value)),
|
||||
[addCondition]
|
||||
)
|
||||
|
||||
return (
|
||||
<BoxFull
|
||||
|
|
|
@ -1,36 +1,29 @@
|
|||
import React, { useCallback, useContext } from "react"
|
||||
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 useRepositoryEditor from "../../hooks/useRepositoryEditor"
|
||||
import Condition from "../../utils/Condition"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
import BoxMap from "../base/BoxMap"
|
||||
import useMapView from "../../hooks/useMapView"
|
||||
import osmZoomLevels from "../../utils/osmZoomLevels"
|
||||
import useMapAreaState from "../../hooks/useMapAreaState"
|
||||
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.
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function BoxConditionMap({ ...props }) {
|
||||
const mapViewHook = useMapView()
|
||||
export default function BoxConditionLocation({ ...props }) {
|
||||
const mapViewHook = useMapAreaState()
|
||||
const { addCondition } = useRepositoryEditor()
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
|
||||
const onButtonClick = useCallback(
|
||||
() => {
|
||||
const radius = mapViewHook.zoom * osmZoomLevels[mapViewHook.zoom]
|
||||
|
||||
addCondition(new Condition(
|
||||
"COORDINATES",
|
||||
`< ${radius} ${mapViewHook.center.lat} ${mapViewHook.center.lng}`,
|
||||
))
|
||||
},
|
||||
() => addCondition(new ConditionLocation(mapViewHook.mapArea)),
|
||||
[mapViewHook, addCondition]
|
||||
)
|
||||
|
||||
|
@ -41,7 +34,7 @@ export default function BoxConditionMap({ ...props }) {
|
|||
<span>
|
||||
{strings.searchBy}
|
||||
|
||||
<FontAwesomeIcon icon={faMapPin}/>
|
||||
<FontAwesomeIcon icon={faLocationArrow}/>
|
||||
|
||||
{strings.byZone}
|
||||
</span>
|
|
@ -1,16 +1,16 @@
|
|||
import React, { useContext } from "react"
|
||||
import React, { useCallback, useContext } from "react"
|
||||
import BoxFull from "../base/BoxFull"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faAt } from "@fortawesome/free-solid-svg-icons"
|
||||
import useRepositoryEditor from "../../hooks/useRepositoryEditor"
|
||||
import Condition from "../../utils/Condition"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
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
|
||||
* to the {@link ContextRepositoryEditor}.
|
||||
* A {@link BoxFull} that allows the user to select a Twitter user to search for, and then to add it as a
|
||||
* {@link ConditionUser} of a RepositoryEditor.
|
||||
*
|
||||
* @param props - Additional props to pass to the box.
|
||||
* @returns {JSX.Element}
|
||||
|
@ -20,9 +20,10 @@ export default function BoxConditionUser({ ...props }) {
|
|||
const { addCondition } = useRepositoryEditor()
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
|
||||
const submit = value => {
|
||||
addCondition(new Condition("USER", value))
|
||||
}
|
||||
const submit = useCallback(
|
||||
value => addCondition(new ConditionUser(value)),
|
||||
[addCondition]
|
||||
)
|
||||
|
||||
return (
|
||||
<BoxFull
|
||||
|
|
|
@ -16,7 +16,7 @@ export default function BoxConditions({ ...props }) {
|
|||
const { conditions } = useRepositoryEditor()
|
||||
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 (
|
||||
<BoxFull header={strings.conditions} {...props}>
|
||||
|
|
|
@ -2,18 +2,26 @@ import React from "react"
|
|||
import BoxFull from "../base/BoxFull"
|
||||
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
|
||||
import useStrings from "../../hooks/useStrings"
|
||||
import { ContainsFilter } from "../../utils/Filter"
|
||||
import { FilterContains } from "../../objects/Filter"
|
||||
import FormInlineText from "./FormInlineText"
|
||||
import { faFont } from "@fortawesome/free-solid-svg-icons"
|
||||
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 }) {
|
||||
const strings = useStrings()
|
||||
const { appendFilter } = useRepositoryViewer()
|
||||
|
||||
const submit = value => {
|
||||
appendFilter(new ContainsFilter(false, value))
|
||||
appendFilter(new FilterContains(value))
|
||||
}
|
||||
|
||||
// TODO: add this string
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
import React from "react"
|
||||
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 useStrings from "../../hooks/useStrings"
|
||||
import { AfterDatetimeFilter } from "../../utils/Filter"
|
||||
import { FilterInsideTimeRay } from "../../objects/Filter"
|
||||
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 }) {
|
||||
const strings = useStrings()
|
||||
const { appendFilter } = useRepositoryViewer()
|
||||
|
||||
const submit = ({ date, isBefore }) => {
|
||||
appendFilter(new AfterDatetimeFilter(isBefore, date))
|
||||
const submit = (timeRay) => {
|
||||
appendFilter(new FilterInsideTimeRay(timeRay))
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -29,7 +37,7 @@ export default function BoxFilterDatetime({ ...props }) {
|
|||
}
|
||||
{...props}
|
||||
>
|
||||
<FormInlineBADatetime submit={submit}/>
|
||||
<FormInlineTimeRay submit={submit}/>
|
||||
</BoxFull>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,18 +4,25 @@ import FormInline from "../base/FormInline"
|
|||
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
|
||||
import useStrings from "../../hooks/useStrings"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faLocationArrow, faPlus } from "@fortawesome/free-solid-svg-icons"
|
||||
import { HasPlaceFilter } from "../../utils/Filter"
|
||||
import { faLocationArrow, faMapMarkerAlt, faPlus } from "@fortawesome/free-solid-svg-icons"
|
||||
import { FilterWithPlace } from "../../objects/Filter"
|
||||
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 }) {
|
||||
const strings = useStrings()
|
||||
|
||||
const { appendFilter } = useRepositoryViewer()
|
||||
|
||||
const submit = () => {
|
||||
appendFilter(new HasPlaceFilter(false))
|
||||
appendFilter(new FilterWithPlace())
|
||||
}
|
||||
|
||||
// TODO: translate this
|
||||
|
@ -26,7 +33,7 @@ export default function BoxFilterHasPlace({ ...props }) {
|
|||
<span>
|
||||
{strings.searchBy}
|
||||
|
||||
<FontAwesomeIcon icon={faLocationArrow}/>
|
||||
<FontAwesomeIcon icon={faMapMarkerAlt}/>
|
||||
|
||||
{strings.byHasPlace}
|
||||
</span>
|
||||
|
|
|
@ -3,17 +3,25 @@ import BoxFull from "../base/BoxFull"
|
|||
import { faHashtag } from "@fortawesome/free-solid-svg-icons"
|
||||
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
|
||||
import useStrings from "../../hooks/useStrings"
|
||||
import { HashtagFilter } from "../../utils/Filter"
|
||||
import { FilterHashtag } from "../../objects/Filter"
|
||||
import FormInlineHashtag from "./FormInlineHashtag"
|
||||
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 }) {
|
||||
const strings = useStrings()
|
||||
const { appendFilter } = useRepositoryViewer()
|
||||
|
||||
const submit = value => {
|
||||
appendFilter(new HashtagFilter(false, value))
|
||||
appendFilter(new FilterHashtag(value))
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
import React from "react"
|
||||
import BoxFull from "../base/BoxFull"
|
||||
import FormInline from "../base/FormInline"
|
||||
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
|
||||
import useStrings from "../../hooks/useStrings"
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faMapPin } from "@fortawesome/free-solid-svg-icons"
|
||||
import { faLocationArrow, faMapPin } from "@fortawesome/free-solid-svg-icons"
|
||||
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 }) {
|
||||
const strings = useStrings()
|
||||
|
||||
const { appendFilter, mapViewHook } = useRepositoryViewer()
|
||||
|
||||
const submit = () => {
|
||||
appendFilter(new LocationRadiusFilter(false, mapViewHook.center, mapViewHook.radius))
|
||||
appendFilter(new FilterInsideMapArea(mapViewHook.mapArea))
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -24,7 +33,7 @@ export default function BoxFilterLocation({ ...props }) {
|
|||
<span>
|
||||
{strings.searchBy}
|
||||
|
||||
<FontAwesomeIcon icon={faMapPin}/>
|
||||
<FontAwesomeIcon icon={faLocationArrow}/>
|
||||
|
||||
{strings.byZone}
|
||||
</span>
|
||||
|
|
|
@ -3,20 +3,26 @@ import BoxFull from "../base/BoxFull"
|
|||
import { faAt } from "@fortawesome/free-solid-svg-icons"
|
||||
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
|
||||
import useStrings from "../../hooks/useStrings"
|
||||
import { UserFilter } from "../../utils/Filter"
|
||||
import { FilterPoster } from "../../objects/Filter"
|
||||
import FormInlineUser from "./FormInlineUser"
|
||||
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 }) {
|
||||
// TODO: Translate this
|
||||
// TODO: and also use a better string maybe
|
||||
const strings = useStrings()
|
||||
|
||||
const { appendFilter } = useRepositoryViewer()
|
||||
|
||||
const submit = value => {
|
||||
appendFilter(new UserFilter(false, value))
|
||||
appendFilter(new FilterPoster(value))
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
* @param props
|
||||
* @param props - Additional props to pass to the box.
|
||||
* @returns {JSX.Element}
|
||||
* @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 SummaryTweet from "./SummaryTweet"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
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 }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
const { tweets } = useContext(ContextRepositoryViewer)
|
||||
const strings = useStrings()
|
||||
const { tweets } = useRepositoryViewer()
|
||||
|
||||
let content
|
||||
if(tweets.length === 0) {
|
||||
|
|
|
@ -9,6 +9,15 @@ import FormAlert from "../base/formparts/FormAlert"
|
|||
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 }) {
|
||||
const [username, setUsername] = useState("")
|
||||
const [email, setEmail] = useState("")
|
||||
|
|
|
@ -5,6 +5,16 @@ import SummaryUser from "./SummaryUser"
|
|||
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 }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
|
||||
|
|
|
@ -2,10 +2,17 @@ import React, { useContext, useMemo } from "react"
|
|||
import BoxMap from "../base/BoxMap"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
import { Marker, Popup } from "react-leaflet"
|
||||
import { Location } from "../../utils/location"
|
||||
import Coordinates from "../../objects/Coordinates"
|
||||
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 }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
const { tweets, mapViewHook } = useContext(ContextRepositoryViewer)
|
||||
|
@ -13,10 +20,12 @@ export default function BoxVisualizationMap({ ...props }) {
|
|||
const markers = useMemo(
|
||||
() => {
|
||||
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 (
|
||||
<Marker key={tweet["snowflake"]} position={location.toArray()}>
|
||||
<Marker key={tweet["snowflake"]} position={coords.toLatLng()}>
|
||||
<Popup>
|
||||
<p>
|
||||
{tweet["content"]}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, { useContext } from "react"
|
||||
import React, { useCallback, useContext } from "react"
|
||||
import BoxWordcloud from "../base/BoxWordcloud"
|
||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||
import BoxFull from "../base/BoxFull"
|
||||
import Empty from "./Empty"
|
||||
import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
||||
import { ContainsFilter } from "../../utils/Filter"
|
||||
import { FilterContains } from "../../objects/Filter"
|
||||
|
||||
|
||||
export default function BoxVisualizationWordcloud({ ...props }) {
|
||||
|
@ -19,9 +19,12 @@ export default function BoxVisualizationWordcloud({ ...props }) {
|
|||
)
|
||||
}
|
||||
|
||||
const onWordClick = word => {
|
||||
appendFilter(new ContainsFilter(false, word.text))
|
||||
}
|
||||
const onWordClick = useCallback(
|
||||
word => {
|
||||
appendFilter(new FilterContains(word.text))
|
||||
},
|
||||
[appendFilter]
|
||||
)
|
||||
|
||||
return (
|
||||
<BoxWordcloud
|
||||
|
|
|
@ -2,6 +2,16 @@ import React from "react"
|
|||
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 }) {
|
||||
return (
|
||||
<ButtonIconOnly
|
||||
|
|
|
@ -5,6 +5,16 @@ import Button from "../base/Button"
|
|||
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 }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
|
||||
|
|
|
@ -4,7 +4,15 @@ import classNames from "classnames"
|
|||
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)
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 => {
|
||||
return value.replace(INVALID_CHARACTERS, "")
|
||||
|
@ -19,7 +26,6 @@ export default function FormInlineHashtag({ submit, ...props }) {
|
|||
textIcon={faHashtag}
|
||||
placeholder={"hashtag"}
|
||||
validate={validate}
|
||||
submit={submit}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -6,6 +6,21 @@ import ButtonIconOnly from "../base/ButtonIconOnly"
|
|||
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(
|
||||
{
|
||||
mapViewHook,
|
||||
|
@ -32,21 +47,21 @@ export default function FormInlineLocation(
|
|||
className={Style.Radius}
|
||||
type={"text"}
|
||||
icon={radIcon}
|
||||
value={`${mapViewHook.radius} m`}
|
||||
value={`${Math.round(mapViewHook.mapArea.radius / 1000)} km`}
|
||||
disabled={true}
|
||||
/>
|
||||
<InputWithIcon
|
||||
className={Style.Latitude}
|
||||
type={"text"}
|
||||
icon={latIcon}
|
||||
value={mapViewHook.center.lat.toFixed(3)}
|
||||
value={mapViewHook.mapArea.center.lat.toFixed(3)}
|
||||
disabled={true}
|
||||
/>
|
||||
<InputWithIcon
|
||||
className={Style.Longitude}
|
||||
type={"text"}
|
||||
icon={lngIcon}
|
||||
value={mapViewHook.center.lng.toFixed(3)}
|
||||
value={mapViewHook.mapArea.center.lng.toFixed(3)}
|
||||
disabled={true}
|
||||
/>
|
||||
<ButtonIconOnly
|
||||
|
|
|
@ -6,6 +6,19 @@ import ButtonIconOnly from "../base/ButtonIconOnly"
|
|||
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(
|
||||
{
|
||||
textIcon = faFont,
|
||||
|
@ -21,6 +34,7 @@ export default function FormInlineText(
|
|||
|
||||
const _onSubmit = event => {
|
||||
event.preventDefault()
|
||||
if(!value) return
|
||||
submit(value)
|
||||
setValue("")
|
||||
}
|
||||
|
@ -43,6 +57,7 @@ export default function FormInlineText(
|
|||
icon={buttonIcon}
|
||||
color={buttonColor}
|
||||
onClick={_onSubmit}
|
||||
disabled={!value}
|
||||
/>
|
||||
</FormInline>
|
||||
)
|
||||
|
|
|
@ -5,12 +5,26 @@ import { faClock, faPlus } from "@fortawesome/free-solid-svg-icons"
|
|||
import ButtonIconOnly from "../base/ButtonIconOnly"
|
||||
import Style from "./FormInlineText.module.css"
|
||||
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,
|
||||
buttonIcon = faPlus,
|
||||
|
@ -26,15 +40,14 @@ export default function FormInlineBADatetime(
|
|||
|
||||
const _onSubmit = event => {
|
||||
event.preventDefault()
|
||||
submit({
|
||||
date: new Date(value),
|
||||
isBefore,
|
||||
})
|
||||
if(!value) return
|
||||
console.debug(value)
|
||||
submit(new TimeRay(isBefore, new Date(value)))
|
||||
setValue("")
|
||||
}
|
||||
|
||||
const _onChange = event => {
|
||||
setValue(validate(event.target.value.replace(INVALID_CHARACTERS, "")))
|
||||
setValue(validate(event.target.value.toUpperCase().replace(INVALID_CHARACTERS, "")))
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -56,6 +69,7 @@ export default function FormInlineBADatetime(
|
|||
icon={buttonIcon}
|
||||
color={buttonColor}
|
||||
onClick={_onSubmit}
|
||||
disabled={!value}
|
||||
/>
|
||||
</FormInline>
|
||||
)
|
|
@ -3,12 +3,17 @@ import FormInlineText from "./FormInlineText"
|
|||
import { faAt } from "@fortawesome/free-solid-svg-icons"
|
||||
|
||||
|
||||
// Official hashtag regex from https://stackoverflow.com/a/22490853/4334568
|
||||
// noinspection RegExpAnonymousGroup,LongLine
|
||||
const INVALID_CHARACTERS = /[^a-zA-Z0-9]/g
|
||||
|
||||
|
||||
export default function FormInlineUser({ submit, ...props }) {
|
||||
/**
|
||||
* 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 => {
|
||||
return value.replace(INVALID_CHARACTERS, "")
|
||||
|
@ -19,7 +24,6 @@ export default function FormInlineUser({ submit, ...props }) {
|
|||
textIcon={faAt}
|
||||
placeholder={"jack"}
|
||||
validate={validate}
|
||||
submit={submit}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -1,10 +1,25 @@
|
|||
import React, { useContext } from "react"
|
||||
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 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 }) {
|
||||
const { filterTab, setFilterTab, setVisualizationTab } = useContext(ContextRepositoryViewer)
|
||||
|
||||
|
@ -38,7 +53,7 @@ export default function PickerFilter({ ...props }) {
|
|||
currentTab={filterTab}
|
||||
setTab={setFilterTab}
|
||||
name={"place"}
|
||||
icon={faLocationArrow}
|
||||
icon={faMapMarkerAlt}
|
||||
/>
|
||||
<ButtonIconOnly
|
||||
onClick={() => {
|
||||
|
@ -47,7 +62,7 @@ export default function PickerFilter({ ...props }) {
|
|||
}}
|
||||
disabled={filterTab === "location"}
|
||||
color={"Grey"}
|
||||
icon={faMapPin}
|
||||
icon={faLocationArrow}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -4,6 +4,13 @@ import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
|||
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 }) {
|
||||
const { visualizationTab, setVisualizationTab } = useContext(ContextRepositoryViewer)
|
||||
|
||||
|
|
|
@ -6,6 +6,14 @@ import SummaryText from "../base/summary/SummaryText"
|
|||
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 }) {
|
||||
let icon
|
||||
if(tweet["location"]) {
|
||||
|
|
|
@ -8,6 +8,16 @@ import SummaryButton from "../base/summary/SummaryButton"
|
|||
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 }) {
|
||||
const { strings } = useContext(ContextLanguage)
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import isString from "is-string"
|
|||
* @constructor
|
||||
*/
|
||||
export default function GlobalServer({ children }) {
|
||||
// TODO: Set this using an envvar
|
||||
const [server, setServer] = useLocalStorageState("server", "http://127.0.0.1:5000")
|
||||
|
||||
/**
|
||||
|
|
|
@ -50,13 +50,13 @@ export default function GlobalUser({ children }) {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
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`, {
|
||||
"email": email,
|
||||
"password": password,
|
||||
})
|
||||
|
||||
console.debug("Memorizzando lo stato di login...")
|
||||
console.debug("Saving login state...")
|
||||
setUser({
|
||||
email: data["user"]["email"],
|
||||
isAdmin: data["user"]["isAdmin"],
|
||||
|
@ -64,18 +64,18 @@ export default function GlobalUser({ children }) {
|
|||
token: data["access_token"],
|
||||
})
|
||||
|
||||
console.info("Accesso effettuato!")
|
||||
console.info("Login successful!")
|
||||
}, [fetchData, setUser])
|
||||
|
||||
/**
|
||||
* Logout from the currently active server.
|
||||
*/
|
||||
const logout = useCallback(() => {
|
||||
console.debug("Ripulendo lo stato di login...")
|
||||
console.debug("Clearing login state...")
|
||||
setUser(null)
|
||||
console.debug("Stato di login ripulito!")
|
||||
console.debug("Cleared login state!")
|
||||
|
||||
console.info("Logout avvenuto con successo!")
|
||||
console.info("Logout successful!")
|
||||
}, [setUser])
|
||||
|
||||
return (
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useCallback, useContext, useMemo, useState } from "react"
|
|||
import ContextRepositoryEditor from "../../contexts/ContextRepositoryEditor"
|
||||
import useArrayState from "../../hooks/useArrayState"
|
||||
import Style from "./RepositoryEditor.module.css"
|
||||
import BoxConditionMap from "../interactive/BoxConditionMap"
|
||||
import BoxConditionLocation from "../interactive/BoxConditionLocation"
|
||||
import BoxConditionHashtag from "../interactive/BoxConditionHashtag"
|
||||
import BoxConditionUser from "../interactive/BoxConditionUser"
|
||||
import BoxConditionDatetime from "../interactive/BoxConditionDatetime"
|
||||
|
@ -142,7 +142,7 @@ export default function RepositoryEditor({
|
|||
}}
|
||||
>
|
||||
<div className={classNames(Style.RepositoryEditor, className)}>
|
||||
<BoxConditionMap className={Style.SearchByZone}/>
|
||||
<BoxConditionLocation className={Style.SearchByZone}/>
|
||||
<BoxConditionHashtag className={Style.SearchByHashtags}/>
|
||||
<BoxConditionUser className={Style.SearchByUser}/>
|
||||
<BoxConditionDatetime className={Style.SearchByTimePeriod}/>
|
||||
|
|
|
@ -24,7 +24,7 @@ import BoxFilterContains from "../interactive/BoxFilterContains"
|
|||
import BoxFilterUser from "../interactive/BoxFilterUser"
|
||||
import BoxFilterHashtag from "../interactive/BoxFilterHashtag"
|
||||
import BoxFilterLocation from "../interactive/BoxFilterLocation"
|
||||
import useMapView from "../../hooks/useMapView"
|
||||
import useMapAreaState from "../../hooks/useMapAreaState"
|
||||
import BoxFilterDatetime from "../interactive/BoxFilterDatetime"
|
||||
import BoxFilterHasPlace from "../interactive/BoxFilterHasPlace"
|
||||
|
||||
|
@ -44,7 +44,7 @@ export default function RepositoryViewer({ id, className, ...props }) {
|
|||
} = useArrayState([])
|
||||
|
||||
// FIXME: this has a severe performance impact, investigate
|
||||
const mapViewHook = useMapView()
|
||||
const mapViewHook = useMapAreaState()
|
||||
|
||||
// Repository
|
||||
const repositoryBr = useBackendResource(
|
||||
|
|
|
@ -8,7 +8,7 @@ import LocalizationStrings from "../LocalizationStrings"
|
|||
* - `setLang`: a function to change the current language
|
||||
* - `strings`: an object containing all strings of the current language
|
||||
*
|
||||
* Defaults to Italian.
|
||||
* Defaults to Italian `it`.
|
||||
*/
|
||||
export default createContext({
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
* - `setServer`: a function to change `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.
|
||||
* - `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
|
||||
* - `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
|
||||
|
|
|
@ -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.
|
||||
* @returns {{spliceValue, removeValue, setValue, appendValue, value}}
|
||||
|
@ -12,7 +12,7 @@ export default function useArrayState(def) {
|
|||
|
||||
const appendValue = useCallback(
|
||||
newSingle => {
|
||||
console.debug("Aggiungendo ", newSingle, " ad ArrayState")
|
||||
console.debug("Adding ", newSingle, " to ArrayState")
|
||||
setValue(
|
||||
oldArray => [...oldArray, newSingle],
|
||||
)
|
||||
|
@ -22,7 +22,7 @@ export default function useArrayState(def) {
|
|||
|
||||
const spliceValue = useCallback(
|
||||
position => {
|
||||
console.debug("Estraendo ", position, " da ArrayState")
|
||||
console.debug("Splicing ", position, " from ArrayState")
|
||||
setValue(
|
||||
oldArray => {
|
||||
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 ContextServer from "../contexts/ContextServer"
|
||||
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
|
||||
if(body) {
|
||||
if(["GET", "HEAD"].includes(method.toUpperCase())) {
|
||||
path += makeURLSearchParams(body).toString()
|
||||
path += URLSearchParams.fromSerializableObject(body).toString()
|
||||
}
|
||||
else {
|
||||
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) {
|
||||
/**
|
||||
|
|
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 ContextRepositoryEditor from "../contexts/ContextRepositoryEditor"
|
||||
import ContextRepositoryViewer from "../contexts/ContextRepositoryViewer"
|
||||
|
||||
|
||||
/**
|
||||
* Hook to quickly use {@link ContextRepositoryEditor}.
|
||||
* Hook to quickly use {@link ContextRepositoryViewer}.
|
||||
*/
|
||||
export default function useRepositoryViewer() {
|
||||
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() {
|
||||
return useContext(ContextLanguage).strings
|
||||
|
|
|
@ -3,6 +3,7 @@ import ReactDOM from "react-dom"
|
|||
import "./index.css"
|
||||
import App from "./App"
|
||||
import reportWebVitals from "./reportWebVitals"
|
||||
import "./prototypes"
|
||||
|
||||
|
||||
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?
|
||||
// Davvero tutte le date.toISOString() sono considerate UTC?
|
||||
// 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") {
|
||||
Date.prototype.toAwareISOString = function() {
|
||||
if(this.toString() === "Invalid Date") {
|
||||
throw new Error("Data non valida ricevuta come parametro.")
|
||||
}
|
||||
|
||||
// Create a timezone naive ISO string
|
||||
const naive = date.toISOString()
|
||||
const naive = this.toISOString()
|
||||
|
||||
// Find the local timezone
|
||||
const tz = -new Date().getTimezoneOffset()
|
|
@ -1,7 +1,7 @@
|
|||
import isString from "is-string"
|
||||
|
||||
|
||||
export default function makeURLSearchParams(obj) {
|
||||
URLSearchParams.fromSerializableObject = function(obj) {
|
||||
let usp = new URLSearchParams()
|
||||
for(const key in obj) {
|
||||
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 history - The history to push the destination to.
|
||||
* @param destination - The path of the destination.
|
||||
* @returns {(function(): void)|*}
|
||||
*/
|
||||
export default function goToOnSuccess(func, history, destination) {
|
||||
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",
|
||||
"chart.js": "^3.2.1",
|
||||
"classnames": "^2.3.1",
|
||||
"geolib": "^3.3.1",
|
||||
"is-string": "^1.0.5",
|
||||
"leaflet": "^1.7.1",
|
||||
"react": "^17.0.2",
|
||||
|
@ -9416,6 +9417,11 @@
|
|||
"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": {
|
||||
"version": "2.0.5",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.0.5",
|
||||
"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",
|
||||
"chart.js": "^3.2.1",
|
||||
"classnames": "^2.3.1",
|
||||
"geolib": "^3.3.1",
|
||||
"is-string": "^1.0.5",
|
||||
"leaflet": "^1.7.1",
|
||||
"react": "^17.0.2",
|
||||
|
|
Loading…
Reference in a new issue