mirror of
https://github.com/pds-nest/nest.git
synced 2024-11-25 14:34:19 +00:00
✨ Accidentaly make wordcloud words clickable
This commit is contained in:
parent
0a34bab3d3
commit
87791a0e84
15 changed files with 247 additions and 26 deletions
|
@ -1,21 +1,21 @@
|
||||||
import React from "react"
|
import React, { useMemo } from "react"
|
||||||
import BoxFull from "../base/BoxFull"
|
import BoxFull from "../base/BoxFull"
|
||||||
import ReactWordcloud from "@steffo/nest-react-wordcloud"
|
import ReactWordcloud from "@steffo/nest-react-wordcloud"
|
||||||
import Style from "./BoxWordcloud.module.css"
|
import Style from "./BoxWordcloud.module.css"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Box which displays a wordcloud.
|
* A {@link BoxFull} which displays a wordcloud.
|
||||||
*
|
*
|
||||||
* @param words - A list of word objects, made of a string "text" and a number "value"
|
* @param words - A list of word objects, made of a string "text" and a number "value"
|
||||||
|
* @param options - Additional options to pass to {@link ReactWordcloud}.
|
||||||
* @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 BoxWordcloud({ words, ...props }) {
|
export default function BoxWordcloud({ words, callbacks = {}, ...props }) {
|
||||||
return (
|
const wordcloud = useMemo(
|
||||||
<BoxFull {...props}>
|
() => (
|
||||||
<div className={Style.WordcloudContainer}>
|
|
||||||
<ReactWordcloud
|
<ReactWordcloud
|
||||||
options={{
|
options={{
|
||||||
colors: [
|
colors: [
|
||||||
|
@ -25,9 +25,22 @@ export default function BoxWordcloud({ words, ...props }) {
|
||||||
fontSizes: [8, 64],
|
fontSizes: [8, 64],
|
||||||
size: undefined,
|
size: undefined,
|
||||||
deterministic: true,
|
deterministic: true,
|
||||||
|
rotations: 0,
|
||||||
|
rotationAngles: [0, 0],
|
||||||
|
enableOptimizations: true,
|
||||||
|
enableTooltip: false,
|
||||||
}}
|
}}
|
||||||
words={words}
|
words={words}
|
||||||
|
callbacks={callbacks}
|
||||||
/>
|
/>
|
||||||
|
),
|
||||||
|
[words]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoxFull {...props}>
|
||||||
|
<div className={Style.WordcloudContainer}>
|
||||||
|
{wordcloud}
|
||||||
</div>
|
</div>
|
||||||
</BoxFull>
|
</BoxFull>
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default function BadgeFilter({ filter }) {
|
||||||
<Badge
|
<Badge
|
||||||
color={filter.color()}
|
color={filter.color()}
|
||||||
icon={filter.icon()}
|
icon={filter.icon()}
|
||||||
onClickDelete={null}
|
onClickDelete={() => removeFilter(filter)}
|
||||||
>
|
>
|
||||||
{filter.text()}
|
{filter.text()}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
28
nest_frontend/components/interactive/BoxFilterContains.js
Normal file
28
nest_frontend/components/interactive/BoxFilterContains.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React, { useContext, useState } from "react"
|
||||||
|
import BoxFull from "../base/BoxFull"
|
||||||
|
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
|
||||||
|
import useStrings from "../../hooks/useStrings"
|
||||||
|
import { ContainsFilter } from "../../utils/Filter"
|
||||||
|
import FormInlineText from "./FormInlineText"
|
||||||
|
import { faFont } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
|
||||||
|
|
||||||
|
export default function BoxFilterContains({ ...props }) {
|
||||||
|
const strings = useStrings()
|
||||||
|
const { appendFilter } = useRepositoryViewer()
|
||||||
|
|
||||||
|
const submit = value => {
|
||||||
|
appendFilter(new ContainsFilter(false, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add this string
|
||||||
|
return (
|
||||||
|
<BoxFull header={strings.filterContains} {...props}>
|
||||||
|
<FormInlineText
|
||||||
|
textIcon={faFont}
|
||||||
|
submit={submit}
|
||||||
|
placeholder={"cat in the box"}
|
||||||
|
/>
|
||||||
|
</BoxFull>
|
||||||
|
)
|
||||||
|
}
|
43
nest_frontend/components/interactive/BoxFilterHashtag.js
Normal file
43
nest_frontend/components/interactive/BoxFilterHashtag.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import React, { useContext, useState } from "react"
|
||||||
|
import BoxFull from "../base/BoxFull"
|
||||||
|
import FormInline from "../base/FormInline"
|
||||||
|
import InputWithIcon from "../base/InputWithIcon"
|
||||||
|
import Style from "./BoxConditionUser.module.css"
|
||||||
|
import { faAt, faFilter, faFont, faHashtag } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import ButtonIconOnly from "../base/ButtonIconOnly"
|
||||||
|
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
|
||||||
|
import useStrings from "../../hooks/useStrings"
|
||||||
|
import { ContainsFilter, HashtagFilter, UserFilter } from "../../utils/Filter"
|
||||||
|
import Condition from "../../utils/Condition"
|
||||||
|
import FormInlineText from "./FormInlineText"
|
||||||
|
|
||||||
|
|
||||||
|
const INVALID_HASHTAG_CHARACTERS = /([^a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73\u2b740-\u2b81\u2f800-\u2fa1])/g
|
||||||
|
|
||||||
|
|
||||||
|
export default function BoxFilterHashtag({ ...props }) {
|
||||||
|
// TODO: Translate this
|
||||||
|
// TODO: and also use a better string maybe
|
||||||
|
const strings = useStrings()
|
||||||
|
|
||||||
|
const { appendFilter } = useRepositoryViewer()
|
||||||
|
|
||||||
|
const validate = value => {
|
||||||
|
return value.replace(INVALID_HASHTAG_CHARACTERS, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = value => {
|
||||||
|
appendFilter(new HashtagFilter(false, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoxFull header={strings.filterHashtag} {...props}>
|
||||||
|
<FormInlineText
|
||||||
|
textIcon={faHashtag}
|
||||||
|
placeholder={"hashtag"}
|
||||||
|
validate={validate}
|
||||||
|
submit={submit}
|
||||||
|
/>
|
||||||
|
</BoxFull>
|
||||||
|
)
|
||||||
|
}
|
40
nest_frontend/components/interactive/BoxFilterUser.js
Normal file
40
nest_frontend/components/interactive/BoxFilterUser.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import React, { useContext, useState } from "react"
|
||||||
|
import BoxFull from "../base/BoxFull"
|
||||||
|
import FormInline from "../base/FormInline"
|
||||||
|
import InputWithIcon from "../base/InputWithIcon"
|
||||||
|
import Style from "./BoxConditionUser.module.css"
|
||||||
|
import { faAt, faFilter, faFont } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import ButtonIconOnly from "../base/ButtonIconOnly"
|
||||||
|
import useRepositoryViewer from "../../hooks/useRepositoryViewer"
|
||||||
|
import useStrings from "../../hooks/useStrings"
|
||||||
|
import { ContainsFilter, UserFilter } from "../../utils/Filter"
|
||||||
|
import Condition from "../../utils/Condition"
|
||||||
|
import FormInlineText from "./FormInlineText"
|
||||||
|
|
||||||
|
|
||||||
|
export default function BoxFilterUser({ ...props }) {
|
||||||
|
// TODO: Translate this
|
||||||
|
// TODO: and also use a better string maybe
|
||||||
|
const strings = useStrings()
|
||||||
|
|
||||||
|
const { appendFilter } = useRepositoryViewer()
|
||||||
|
|
||||||
|
const validate = value => {
|
||||||
|
return value.replace(/[^a-zA-Z0-9]/g, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = value => {
|
||||||
|
appendFilter(new UserFilter(false, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoxFull header={strings.filterUser} {...props}>
|
||||||
|
<FormInlineText
|
||||||
|
textIcon={faAt}
|
||||||
|
placeholder={"jack"}
|
||||||
|
validate={validate}
|
||||||
|
submit={submit}
|
||||||
|
/>
|
||||||
|
</BoxFull>
|
||||||
|
)
|
||||||
|
}
|
|
@ -4,11 +4,12 @@ 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"
|
||||||
|
|
||||||
|
|
||||||
export default function BoxVisualizationWordcloud({ ...props }) {
|
export default function BoxVisualizationWordcloud({ ...props }) {
|
||||||
const { strings } = useContext(ContextLanguage)
|
const { strings } = useContext(ContextLanguage)
|
||||||
const {words} = useContext(ContextRepositoryViewer)
|
const {words, appendFilter} = useContext(ContextRepositoryViewer)
|
||||||
|
|
||||||
if(words.length === 0) {
|
if(words.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
@ -18,7 +19,16 @@ export default function BoxVisualizationWordcloud({ ...props }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onWordClick = word => {
|
||||||
|
appendFilter(new ContainsFilter(false, word.text))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxWordcloud header={strings.wordcloud} words={words} {...props}/>
|
<BoxWordcloud
|
||||||
|
header={strings.wordcloud}
|
||||||
|
words={words}
|
||||||
|
callbacks={{onWordClick}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
39
nest_frontend/components/interactive/FormInlineText.js
Normal file
39
nest_frontend/components/interactive/FormInlineText.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import React, { useState } from "react"
|
||||||
|
import FormInline from "../base/FormInline"
|
||||||
|
import InputWithIcon from "../base/InputWithIcon"
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
import ButtonIconOnly from "../base/ButtonIconOnly"
|
||||||
|
import Style from "./FormInlineText.module.css"
|
||||||
|
|
||||||
|
|
||||||
|
export default function FormInlineText({ textIcon, placeholder, buttonIcon = faPlus, buttonColor = "Green", validate = value => value, submit, ...props }) {
|
||||||
|
const [value, setValue] = useState("")
|
||||||
|
|
||||||
|
const _onSubmit = event => {
|
||||||
|
event.preventDefault()
|
||||||
|
submit(value)
|
||||||
|
setValue("")
|
||||||
|
}
|
||||||
|
|
||||||
|
const _onChange = event => {
|
||||||
|
setValue(validate(event.target.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormInline onSubmit={_onSubmit} {...props}>
|
||||||
|
<InputWithIcon
|
||||||
|
className={Style.Input}
|
||||||
|
icon={textIcon}
|
||||||
|
value={value}
|
||||||
|
onChange={_onChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
<ButtonIconOnly
|
||||||
|
className={Style.Button}
|
||||||
|
icon={buttonIcon}
|
||||||
|
color={buttonColor}
|
||||||
|
onClick={_onSubmit}
|
||||||
|
/>
|
||||||
|
</FormInline>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
.Input {
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button {
|
||||||
|
|
||||||
|
}
|
|
@ -16,6 +16,12 @@ export default function PickerFilter({ ...props }) {
|
||||||
name={"contains"}
|
name={"contains"}
|
||||||
icon={faFont}
|
icon={faFont}
|
||||||
/>
|
/>
|
||||||
|
<ButtonPicker
|
||||||
|
currentTab={filterTab}
|
||||||
|
setTab={setFilterTab}
|
||||||
|
name={"hashtag"}
|
||||||
|
icon={faHashtag}
|
||||||
|
/>
|
||||||
<ButtonPicker
|
<ButtonPicker
|
||||||
currentTab={filterTab}
|
currentTab={filterTab}
|
||||||
setTab={setFilterTab}
|
setTab={setFilterTab}
|
||||||
|
|
|
@ -20,6 +20,9 @@ import PickerFilter from "../interactive/PickerFilter"
|
||||||
import useArrayState from "../../hooks/useArrayState"
|
import useArrayState from "../../hooks/useArrayState"
|
||||||
import BoxFilters from "../interactive/BoxFilters"
|
import BoxFilters from "../interactive/BoxFilters"
|
||||||
import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
|
||||||
|
import BoxFilterContains from "../interactive/BoxFilterContains"
|
||||||
|
import BoxFilterUser from "../interactive/BoxFilterUser"
|
||||||
|
import BoxFilterHashtag from "../interactive/BoxFilterHashtag"
|
||||||
|
|
||||||
|
|
||||||
export default function RepositoryViewer({ id, className, ...props }) {
|
export default function RepositoryViewer({ id, className, ...props }) {
|
||||||
|
@ -111,8 +114,9 @@ export default function RepositoryViewer({ id, className, ...props }) {
|
||||||
|
|
||||||
<BoxFilters className={Style.Filters}/>
|
<BoxFilters className={Style.Filters}/>
|
||||||
<PickerFilter className={Style.FilterPicker}/>
|
<PickerFilter className={Style.FilterPicker}/>
|
||||||
{filterTab === "contains" ? "Contains" : null}
|
{filterTab === "contains" ? <BoxFilterContains className={Style.AddFilter}/> : null}
|
||||||
{filterTab === "user" ? "User" : null}
|
{filterTab === "hashtag" ? <BoxFilterHashtag className={Style.AddFilter}/> : null}
|
||||||
|
{filterTab === "user" ? <BoxFilterUser className={Style.AddFilter}/> : null}
|
||||||
{filterTab === "time" ? "Time" : null}
|
{filterTab === "time" ? "Time" : null}
|
||||||
{filterTab === "location" ? "Location" : null}
|
{filterTab === "location" ? "Location" : null}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-template-rows: auto auto 3fr auto 1fr;
|
grid-template-rows: auto auto 1fr auto auto;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { createContext } from "react"
|
import { createContext } from "react"
|
||||||
|
import LocalizationStrings from "../LocalizationStrings"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,9 +7,11 @@ import { createContext } from "react"
|
||||||
* - `lang`: a string corresponding to the ISO 639-1 code of the current language
|
* - `lang`: a string corresponding to the ISO 639-1 code of the current language
|
||||||
* - `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.
|
||||||
*/
|
*/
|
||||||
export default createContext({
|
export default createContext({
|
||||||
lang: null,
|
lang: "it",
|
||||||
setLang: () => console.error("Trying to setLang while outside a ContextServer.Provider!"),
|
setLang: () => console.error("Trying to setLang while outside a ContextServer.Provider!"),
|
||||||
strings: null,
|
strings: LocalizationStrings.it,
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,7 +6,7 @@ import ContextRepositoryViewer from "../contexts/ContextRepositoryViewer"
|
||||||
/**
|
/**
|
||||||
* Hook to quickly use {@link ContextRepositoryEditor}.
|
* Hook to quickly use {@link ContextRepositoryEditor}.
|
||||||
*/
|
*/
|
||||||
export default function useRepositoryEditor() {
|
export default function useRepositoryViewer() {
|
||||||
const context = useContext(ContextRepositoryViewer)
|
const context = useContext(ContextRepositoryViewer)
|
||||||
if(!context) {
|
if(!context) {
|
||||||
throw new Error("This component must be placed inside a RepositoryViewer.")
|
throw new Error("This component must be placed inside a RepositoryViewer.")
|
||||||
|
|
10
nest_frontend/hooks/useStrings.js
Normal file
10
nest_frontend/hooks/useStrings.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { useContext } from "react"
|
||||||
|
import ContextLanguage from "../contexts/ContextLanguage"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to quickly use the strings of {@link ContextLanguage}.
|
||||||
|
*/
|
||||||
|
export default function useStrings() {
|
||||||
|
return useContext(ContextLanguage).strings
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { Location } from "../components/interactive/location"
|
import {Location} from "./location"
|
||||||
import {
|
import {
|
||||||
faAt,
|
faAt,
|
||||||
faFilter,
|
faFilter,
|
||||||
faFont,
|
faFont, faHashtag,
|
||||||
faLocationArrow,
|
faLocationArrow,
|
||||||
faMap,
|
faMap,
|
||||||
faMapMarkerAlt,
|
faMapMarkerAlt,
|
||||||
|
@ -64,6 +64,24 @@ export class ContainsFilter extends Filter {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 {
|
export class UserFilter extends Filter {
|
||||||
user
|
user
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue