diff --git a/nest_frontend/LocalizationStrings.js b/nest_frontend/LocalizationStrings.js index 40d9d9e..481f91f 100644 --- a/nest_frontend/LocalizationStrings.js +++ b/nest_frontend/LocalizationStrings.js @@ -46,7 +46,7 @@ export default { repoEdit: "Modifica repository", menuActive: "Le tue repository attive", menuArchived: "Le tue repository archiviate", - emptyMenu: "Non c'è nulla qui", + emptyMenu: "Non c'è nulla qui.", delete: "Elimina", archive: "Archivia", edit: "Modifica", @@ -117,7 +117,7 @@ export default { repoEdit: "Edit repository", menuActive: "Your active repositories", menuArchived: "Your archived repositories", - emptyMenu: "There's nothing here", + emptyMenu: "There's nothing here.", delete: "Delete", archive: "Archive", edit: "Edit", @@ -188,7 +188,7 @@ export default { repoEdit: "Muokkaa arkistoa", menuActive: "Aktiiviset arkistosi", menuArchived: "Arkistoidut arkistosi", - emptyMenu: "Täällä ei ole mitään", + emptyMenu: "Täällä ei ole mitään.", delete: "Poista", archive: "Arkistoi", edit: "Muokkaa", diff --git a/nest_frontend/components/base/BoxMap.js b/nest_frontend/components/base/BoxMap.js index 5dda737..1ba1ad8 100644 --- a/nest_frontend/components/base/BoxMap.js +++ b/nest_frontend/components/base/BoxMap.js @@ -4,7 +4,7 @@ import BoxFull from "./BoxFull" import { MapContainer, TileLayer } from "react-leaflet" -export default function BoxMap({ header, setMap, startingPosition, startingZoom, button, children, additions, ...props }) { +export default function BoxMap({ header, setMap, startingPosition = { lat: 41.89309, lng: 12.48289 }, startingZoom = 3, button, children, ...props }) { return ( - {additions} + {children}
{button} diff --git a/nest_frontend/components/interactive/BoxWordcloud.js b/nest_frontend/components/base/BoxWordcloud.js similarity index 67% rename from nest_frontend/components/interactive/BoxWordcloud.js rename to nest_frontend/components/base/BoxWordcloud.js index d8e5177..a74cf3d 100644 --- a/nest_frontend/components/interactive/BoxWordcloud.js +++ b/nest_frontend/components/base/BoxWordcloud.js @@ -1,7 +1,7 @@ -import React, { useContext } from "react" +import React from "react" import BoxFull from "../base/BoxFull" import ReactWordcloud from "@steffo/nest-react-wordcloud" -import ContextLanguage from "../../contexts/ContextLanguage" +import Style from "./BoxWordcloud.module.css" /** @@ -12,19 +12,17 @@ import ContextLanguage from "../../contexts/ContextLanguage" * @returns {JSX.Element} * @constructor */ -export default function BoxWordcloud({ words, props }) { - const { strings } = useContext(ContextLanguage) - +export default function BoxWordcloud({ words, ...props }) { return ( - -
+ +
} - startingPosition={STARTING_POSITION} - startingZoom={STARTING_ZOOM} setMap={setMap} button={ } else if(repositories.length === 0) { - contents = {strings.emptyMenu} + contents = } else { contents = repositories.map(repo => ( diff --git a/nest_frontend/components/interactive/BoxRepositoryTweets.js b/nest_frontend/components/interactive/BoxRepositoryTweets.js index d5e32fa..166f65a 100644 --- a/nest_frontend/components/interactive/BoxRepositoryTweets.js +++ b/nest_frontend/components/interactive/BoxRepositoryTweets.js @@ -1,14 +1,23 @@ import React from "react" import BoxFullScrollable from "../base/BoxFullScrollable" import SummaryTweet from "./SummaryTweet" +import Empty from "./Empty" export default function BoxRepositoryTweets({ tweets, ...props }) { // TODO: Translate this + let content + if(tweets.length === 0) { + content = + } + else { + content = tweets.map(tweet => ) + } + return ( - {tweets.map(tweet => )} + {content} ) } diff --git a/nest_frontend/components/interactive/BoxVisualizationGraph.js b/nest_frontend/components/interactive/BoxVisualizationGraph.js new file mode 100644 index 0000000..fbcf990 --- /dev/null +++ b/nest_frontend/components/interactive/BoxVisualizationGraph.js @@ -0,0 +1,14 @@ +import React from "react" +import BoxFull from "../base/BoxFull" + + +export default function BoxVisualizationGraph({ children, className, ...props }) { + // TODO: translate this + // TODO: implement this + + return ( + + {children} + + ) +} diff --git a/nest_frontend/components/interactive/BoxVisualizationMap.js b/nest_frontend/components/interactive/BoxVisualizationMap.js new file mode 100644 index 0000000..96f7f8a --- /dev/null +++ b/nest_frontend/components/interactive/BoxVisualizationMap.js @@ -0,0 +1,40 @@ +import React, { useContext } from "react" +import BoxMap from "../base/BoxMap" +import ContextLanguage from "../../contexts/ContextLanguage" +import { Marker, Popup } from "react-leaflet" + + +const locationRegex = /[{](?[0-9.]+),(?[0-9.]+)[}]/ + +export default function BoxVisualizationMap({ tweets, ...props }) { + // TODO: translate this + const {strings} = useContext(ContextLanguage) + + console.debug(tweets) + const markers = tweets.filter(tweet => tweet.location).map(tweet => { + const match = locationRegex.exec(tweet.location) + if(!match) { + console.error("No match for location ", tweet.location) + return null + } + const {lat, lng} = match.groups + return ( + + +

+ {tweet["content"]} +

+

+ — @{tweet["poster"]} +

+
+
+ ) + }) + + return ( + + {markers} + + ) +} diff --git a/nest_frontend/components/interactive/BoxVisualizationStats.js b/nest_frontend/components/interactive/BoxVisualizationStats.js new file mode 100644 index 0000000..404bbce --- /dev/null +++ b/nest_frontend/components/interactive/BoxVisualizationStats.js @@ -0,0 +1,77 @@ +import React, { useMemo } from "react" +import FormLabelled from "../base/FormLabelled" +import FormLabel from "../base/formparts/FormLabel" +import BoxFullScrollable from "../base/BoxFullScrollable" +import tokenizeTweetWords from "../../utils/tokenizeTweetWords" + + +export default function BoxVisualizationStats({ tweets, totalTweetCount, ...props }) { + + const words = useMemo( + () => tokenizeTweetWords(tweets), + [tweets] + ) + + const tweetCount = tweets.length + const tweetPct = tweetCount / totalTweetCount * 100 + const tweetLocationCount = tweets.filter(tweet => tweet.location).length + const tweetLocationPct = tweetLocationCount / tweetCount * 100 + const tweetContent = tweets.filter(tweet => tweet.content) + const tweetContentCount = tweetContent.length + const tweetContentPct = tweetContentCount / tweetCount * 100 + const wordCount = words.map(word => word.value).reduce((a, b) => a+b) + const mostPopularWord = words.sort((wa, wb) => { + if(wa.value > wb.value) return -1 + if(wa.value < wb.value) return 1 + return 0 + })[0].text + const users = [...new Set(tweets.map(tweet => tweet.poster))] + const usersCount = users.length + + // TODO: tweets with picture count + // TODO: tweets with picture pct + + // TODO: translate this + return ( + + + + {totalTweetCount} + + + {tweetCount} + + + {tweetPct.toFixed(2)}% + + + {tweetLocationCount} + + + {tweetLocationPct.toFixed(2)}% + + + {tweetContentCount} + + + {tweetContentPct.toFixed(2)}% + + + {wordCount} + + + {mostPopularWord} + + + 🚧 + + + 🚧 + + + {usersCount} + + + + ) +} diff --git a/nest_frontend/components/interactive/BoxVisualizationWordcloud.js b/nest_frontend/components/interactive/BoxVisualizationWordcloud.js new file mode 100644 index 0000000..6806ff2 --- /dev/null +++ b/nest_frontend/components/interactive/BoxVisualizationWordcloud.js @@ -0,0 +1,18 @@ +import React, { useContext, useMemo } from "react" +import BoxWordcloud from "../base/BoxWordcloud" +import ContextLanguage from "../../contexts/ContextLanguage" +import tokenizeTweetWords from "../../utils/tokenizeTweetWords" + + +export default function BoxVisualizationWordcloud({ tweets = [], ...props }) { + const {strings} = useContext(ContextLanguage) + + const words = useMemo( + () => tokenizeTweetWords(tweets), + [tweets] + ) + + return ( + + ) +} diff --git a/nest_frontend/components/interactive/Empty.js b/nest_frontend/components/interactive/Empty.js new file mode 100644 index 0000000..8393748 --- /dev/null +++ b/nest_frontend/components/interactive/Empty.js @@ -0,0 +1,15 @@ +import React, { useContext } from "react" +import Style from "./Empty.module.css" +import classNames from "classnames" +import ContextLanguage from "../../contexts/ContextLanguage" + + +export default function Empty({ children, className, ...props }) { + const { strings } = useContext(ContextLanguage) + + return ( + + {strings.emptyMenu} + + ) +} diff --git a/nest_frontend/components/interactive/Empty.module.css b/nest_frontend/components/interactive/Empty.module.css new file mode 100644 index 0000000..558041f --- /dev/null +++ b/nest_frontend/components/interactive/Empty.module.css @@ -0,0 +1,3 @@ +.Empty { + opacity: 0.5; +} diff --git a/nest_frontend/components/interactive/SummaryTweet.js b/nest_frontend/components/interactive/SummaryTweet.js index 5f64122..2b45ac7 100644 --- a/nest_frontend/components/interactive/SummaryTweet.js +++ b/nest_frontend/components/interactive/SummaryTweet.js @@ -1,15 +1,18 @@ import React from "react" import SummaryBase from "../base/summary/SummaryBase" import SummaryLeft from "../base/summary/SummaryLeft" -import { faComment, faMapPin } from "@fortawesome/free-solid-svg-icons" +import { faComment, faLocationArrow, faMapMarker, faMapMarkerAlt, faMapPin } from "@fortawesome/free-solid-svg-icons" import SummaryText from "../base/summary/SummaryText" import SummaryRight from "../base/summary/SummaryRight" export default function SummaryTweet({ tweet, ...props }) { let icon - if(tweet.place) { - icon = faMapPin + if(tweet["location"]) { + icon = faMapMarkerAlt + } + else if(tweet["place"]) { + icon = faLocationArrow } else { icon = faComment @@ -19,9 +22,9 @@ export default function SummaryTweet({ tweet, ...props }) { window.open(`https://twitter.com/${tweet.poster}/status/${tweet.snowflake}`)} + title={`@${tweet["poster"]}`} + subtitle={new Date(tweet["insert_time"]).toLocaleString()} + onClick={() => window.open(`https://twitter.com/${tweet["poster"]}/status/${tweet["snowflake"]}`)} /> {tweet.content} diff --git a/nest_frontend/components/providers/RepositoryViewer.js b/nest_frontend/components/providers/RepositoryViewer.js deleted file mode 100644 index 7d0fa6d..0000000 --- a/nest_frontend/components/providers/RepositoryViewer.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react" -import Style from "./RepositoryViewer.module.css" -import classNames from "classnames" -import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer" - - -export default function RepositoryViewer({ - id, - }) { - return ( - -
- -
-
- ) -} diff --git a/nest_frontend/hooks/useRepositoryViewer.js b/nest_frontend/hooks/useRepositoryViewer.js deleted file mode 100644 index f4ae358..0000000 --- a/nest_frontend/hooks/useRepositoryViewer.js +++ /dev/null @@ -1,14 +0,0 @@ -import { useContext } from "react" -import ContextRepositoryViewer from "../contexts/ContextRepositoryViewer" - - -/** - * Hook to quickly use {@link ContextRepositoryViewer}. - */ -export default function useRepositoryViewer() { - const context = useContext(ContextRepositoryViewer) - if(!context) { - throw new Error("This component must be placed inside a RepositoryViewer.") - } - return context -} diff --git a/nest_frontend/routes/PageRepository.js b/nest_frontend/routes/PageRepository.js index 3386b2a..1aa2833 100644 --- a/nest_frontend/routes/PageRepository.js +++ b/nest_frontend/routes/PageRepository.js @@ -1,8 +1,7 @@ -import React, { useMemo, useState } from "react" +import React, { useContext, useMemo, useState } from "react" import Style from "./PageRepository.module.css" import classNames from "classnames" import BoxRepositoryTweets from "../components/interactive/BoxRepositoryTweets" -import BoxWordcloud from "../components/interactive/BoxWordcloud" import BoxHeader from "../components/base/BoxHeader" import PickerVisualization from "../components/interactive/PickerVisualization" import PickerFilter from "../components/interactive/PickerFilter" @@ -12,10 +11,17 @@ import { faFolder, faFolderOpen, faTrash } from "@fortawesome/free-solid-svg-ico import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import { useParams } from "react-router" import Loading from "../components/base/Loading" +import BoxVisualizationStats from "../components/interactive/BoxVisualizationStats" +import BoxVisualizationGraph from "../components/interactive/BoxVisualizationGraph" +import BoxVisualizationMap from "../components/interactive/BoxVisualizationMap" +import BoxVisualizationWordcloud from "../components/interactive/BoxVisualizationWordcloud" +import BoxFull from "../components/base/BoxFull" +import ContextLanguage from "../contexts/ContextLanguage" export default function PageRepository({ className, ...props }) { const {id} = useParams() + const {strings} = useContext(ContextLanguage) const [visualizationTab, setVisualizationTab] = useState("wordcloud") const [addFilterTab, setAddFilterTab] = useState("hashtag") @@ -46,36 +52,6 @@ export default function PageRepository({ className, ...props }) { ) const tweets = tweetsBv.resources && tweetsBv.error ? [] : tweetsBv.resources - const words = useMemo( - () => { - let preprocessedWords = {} - for(const tweet of tweets) { - if(!tweet.content) { - continue - } - for(const word of tweet.content.toLowerCase().split(/\s+/)) { - if(!preprocessedWords.hasOwnProperty(word)) { - preprocessedWords[word] = 0 - } - preprocessedWords[word] += 1 - } - } - - let processedWords = [] - for(const word in preprocessedWords) { - if(!preprocessedWords.hasOwnProperty(word)) { - continue - } - processedWords.push({ - text: word, - value: preprocessedWords[word] - }) - } - return processedWords - }, - [tweets] - ) - let contents; if(!repositoryBr.firstLoad || !tweetsBv.firstLoad) { contents = <> @@ -85,8 +61,6 @@ export default function PageRepository({ className, ...props }) { } else if(repository === null) { - console.debug("repositoryBr: ", repositoryBr, ", tweetsBv: ", tweetsBv) - // TODO: Translate this! contents = <> @@ -111,17 +85,44 @@ export default function PageRepository({ className, ...props }) { setTab={setVisualizationTab} /> {visualizationTab === "wordcloud" ? - - : null} + + : null} + {visualizationTab === "histogram" ? + + : null} + {visualizationTab === "map" ? + + : null} + {visualizationTab === "stats" ? + + : null} + + + {strings.notImplemented} + + + + {strings.notImplemented} + } diff --git a/nest_frontend/utils/tokenizeTweetWords.js b/nest_frontend/utils/tokenizeTweetWords.js new file mode 100644 index 0000000..ce48ff8 --- /dev/null +++ b/nest_frontend/utils/tokenizeTweetWords.js @@ -0,0 +1,35 @@ +import sw from "stopword" + + +const stopwords = [...sw.it, ...sw.en, "rt"] + + +export default function(tweets = {}) { + let preprocessedWords = {} + for(const tweet of tweets) { + if(!tweet.content) { + continue + } + for(const word of tweet.content.toLowerCase().split(/\s+/)) { + if(stopwords.includes(word)) continue + if(word.startsWith("https://")) continue + + if(!preprocessedWords.hasOwnProperty(word)) { + preprocessedWords[word] = 0 + } + preprocessedWords[word] += 1 + } + } + + let processedWords = [] + for(const word in preprocessedWords) { + if(!preprocessedWords.hasOwnProperty(word)) { + continue + } + processedWords.push({ + text: word, + value: preprocessedWords[word] + }) + } + return processedWords +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 946b637..598406a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "serve": "^11.3.2", + "stopword": "^1.0.7", "web-vitals": "^1.1.1" }, "devDependencies": { @@ -18917,6 +18918,11 @@ "node": ">=0.10.0" } }, + "node_modules/stopword": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/stopword/-/stopword-1.0.7.tgz", + "integrity": "sha512-KKPM5LGeulAxoNrg384AfNSoUvphG87sgj/vTHOkJ5M5U7hO99XA6Sl0HBhtCBv17Tv3kQPLj+5fCt7cadjXWQ==" + }, "node_modules/stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -37540,6 +37546,11 @@ "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" }, + "stopword": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/stopword/-/stopword-1.0.7.tgz", + "integrity": "sha512-KKPM5LGeulAxoNrg384AfNSoUvphG87sgj/vTHOkJ5M5U7hO99XA6Sl0HBhtCBv17Tv3kQPLj+5fCt7cadjXWQ==" + }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", diff --git a/package.json b/package.json index e677b62..9dee233 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "serve": "^11.3.2", + "stopword": "^1.0.7", "web-vitals": "^1.1.1" }, "main": "nest_frontend/index.js",