From 82f16aaba12ee635b8ce6e076a8bc476356b05ff Mon Sep 17 00:00:00 2001 From: MarseyLivesMatter <91437068+MarseyLivesMatter@users.noreply.github.com> Date: Tue, 3 May 2022 03:44:10 +0800 Subject: [PATCH] Add files via upload --- src/api.tsx | 63 +++++++ src/app.tsx | 425 ++++++++++++++++++++++++++++++++++++++++++++ src/error.tsx | 36 ++++ src/favicon.ico | Bin 0 -> 15406 bytes src/github-link.tsx | 11 ++ src/index.html | 21 +++ src/index.pcss | 7 + src/index.tsx | 6 + src/random-link.tsx | 40 +++++ 9 files changed, 609 insertions(+) create mode 100644 src/api.tsx create mode 100644 src/app.tsx create mode 100644 src/error.tsx create mode 100644 src/favicon.ico create mode 100644 src/github-link.tsx create mode 100644 src/index.html create mode 100644 src/index.pcss create mode 100644 src/index.tsx create mode 100644 src/random-link.tsx diff --git a/src/api.tsx b/src/api.tsx new file mode 100644 index 0000000..a3062e0 --- /dev/null +++ b/src/api.tsx @@ -0,0 +1,63 @@ +export enum SearchType { + Comments, + Posts, +} + +export interface SearchSettings { + author: string, + subreddit: string, + searchFor: SearchType, + resultSize: number, + filter: string, + after: Date, + before: Date, + query: string, +} + +export class PushshiftAPI { + get_url(settings: SearchSettings): string { + let args = { + html_decode: "true", + }; + if (settings.after) { + args["after"] = (settings.after.valueOf() / 1000).toString(); + } + if (settings.before) { + args["before"] = (settings.before.valueOf() / 1000).toString(); + } + if (settings.author) { + args["author"] = settings.author; + } + if (settings.subreddit) { + args["subreddit"] = settings.subreddit; + } + if (settings.query) { + args["q"] = settings.query; + } + if (settings.resultSize) { + args["size"] = settings.resultSize.toString(); + } + if (settings.filter) { + args["score"] = settings.filter; + } + let joinedArgs = Object.entries(args).map(([k, v]) => `${k}=${v}`).join('&'); + let endpoint; + if (settings.searchFor == SearchType.Comments) { + endpoint = "comment"; + } + else if (settings.searchFor == SearchType.Posts) { + endpoint = "submission"; + } + return `https://api.pushshift.io/reddit/${endpoint}/search?${joinedArgs}` + } + + async query(url: string): Promise { + + console.log(`Pushshift request ${url}`); + let resp = await fetch(url, { + referrerPolicy: "no-referrer" + }); + let data = await resp.json(); + return data; + } +} diff --git a/src/app.tsx b/src/app.tsx new file mode 100644 index 0000000..c5d67e9 --- /dev/null +++ b/src/app.tsx @@ -0,0 +1,425 @@ +import * as React from 'react'; +import DatePicker from 'react-datepicker' +import { PushshiftAPI, SearchSettings, SearchType } from './api' +import { GithubLink } from './github-link' + +import "react-datepicker/dist/react-datepicker.css"; +import { RandomLink } from './random-link'; + +interface AppState extends SearchSettings { + error: string, + errorTime: Date, + searching: boolean, + comments: Array, + posts: Array, + moreing: boolean, + lastUrl: string, +} + +/** Main class for Reddit Search */ +export class App extends React.Component<{}, AppState> { + lastSearch: SearchSettings; + api: PushshiftAPI; + updatedHash: boolean; + + constructor(props) { + super(props); + this.state = { + author: "", + subreddit: "", + searchFor: SearchType.Comments, + resultSize: 100, + filter: "", + after: null, + before: null, + query: "", + error: null, + errorTime: null, + searching: false, + comments: null, + posts: null, + moreing: false, + lastUrl: "", + }; + this.api = new PushshiftAPI(); + this.updatedHash = false; + } + + loadLocationHash(shouldSearch: boolean = false) { + let params = hash_accessor.load(); + if (params.after) { + params.after = new Date(params.after); + } + if (params.before) { + params.before = new Date(params.before); + } + if (shouldSearch) { + this.setState(params, this.doSearch); + } else { + this.setState(params); + } + } + + componentDidMount() { + // Add hash change event listener + window.addEventListener("hashchange", e => { + if (this.updatedHash) { + this.updatedHash = false; + return; + } + console.log("location.hash changed. loading new params"); + this.loadLocationHash(); + }); + + // Check for location hash. Use it if found + if (window.location.hash) { + this.loadLocationHash(true); + console.log("Loaded params from location.hash"); + return; + } + + // Load stored form data if exists + let formDataJson = localStorage.getItem("search-form"); + if (formDataJson !== null) { + let formData: SearchSettings = JSON.parse(formDataJson); + // Reconstruct `Date`s + if (formData.after) { + formData.after = new Date(formData.after); + } + if (formData.before) { + formData.before = new Date(formData.before); + } + this.setState(formData); + console.log("Loaded params from local storage"); + } + } + + componentDidUpdate() { + let toSave: SearchSettings = { + author: this.state.author, + subreddit: this.state.subreddit, + searchFor: this.state.searchFor, + resultSize: this.state.resultSize, + filter: this.state.filter, + after: this.state.after, + before: this.state.before, + query: this.state.query, + }; + localStorage.setItem("search-form", JSON.stringify(toSave)); + } + + setError = (error: string) => { + this.setState({ error: error, errorTime: new Date() }); + } + + handleAuthorChange = (e: React.ChangeEvent) => { + this.setState({ author: e.target.value }); + } + + handleSubredditChange = (e: React.ChangeEvent) => { + this.setState({ subreddit: e.target.value }); + } + + handleSearchTypeChange = (e: React.ChangeEvent) => { + if (e.target.value === "Comments") { + this.setState({ searchFor: SearchType.Comments }); + } else if (e.target.value === "Posts") { + this.setState({ searchFor: SearchType.Posts }); + } else { + this.setError(e.target.value + " is not a valid search type"); + } + } + + handleResultSizeChange = (e: React.ChangeEvent) => { + let count: number = parseInt(e.target.value); + if (!count) { + return; + } + this.setState({ resultSize: count }); + } + + handleFilterChange = (e: React.ChangeEvent) => { + this.setState({ filter: e.target.value }); + } + + handleAfterDateChange = (date: Date) => { + this.setState({ after: date }); + } + + handleBeforeDateChange = (date: Date) => { + this.setState({ before: date }); + } + + handleQueryChange = (e: React.ChangeEvent) => { + this.setState({ query: e.target.value }); + } + + doSearch = async () => { + this.setState({ error: null, comments: null, posts: null, searching: true }); + this.lastSearch = { ...this.state }; + + // Update location.hash + let toSave = { + author: this.state.author, + subreddit: this.state.subreddit, + searchFor: this.state.searchFor, + resultSize: this.state.resultSize, + filter: this.state.filter, + after: this.state.after, + before: this.state.before, + query: this.state.query, + }; + this.updatedHash = true; + hash_accessor.save(toSave); + + // Search + try { + let url = this.api.get_url(this.lastSearch); + this.setState({ lastUrl: url }); + let data = await this.api.query(url); + // Update state with results + if (this.lastSearch.searchFor == SearchType.Comments) { + this.setState({ comments: data.data, searching: false }); + } else if (this.lastSearch.searchFor == SearchType.Posts) { + this.setState({ posts: data.data, searching: false }); + } + } catch (err) { + this.setState({ searching: false }); + this.setError(String(err)); + } + } + + /** Handle the main form being submitted */ + searchSubmit = async (e) => { + // Update state + e.preventDefault(); + this.doSearch(); + } + + /** Handle the more button being clicked. */ + handleMoreClick = async (e) => { + this.setState({ error: null, moreing: true }); + if (this.state.comments && this.state.comments.length > 0) { + this.lastSearch.before = new Date(this.state.comments[this.state.comments.length - 1].created_utc * 1000); + } else if (this.state.posts && this.state.posts.length > 0) { + this.lastSearch.before = new Date(this.state.posts[this.state.posts.length - 1].created_utc * 1000); + } + let url = this.api.get_url(this.lastSearch); + let data = await this.api.query(url); + if (this.lastSearch.searchFor == SearchType.Comments && data.data) { + this.setState({ comments: this.state.comments.concat(data.data), moreing: false }); + } else if (this.lastSearch.searchFor == SearchType.Posts && data.data) { + this.setState({ posts: this.state.posts.concat(data.data), moreing: false }); + } else { + this.setState({ moreing: false }); + } + } + + /** Render the app + * @return {React.ReactNode} The react node for the app + */ + render(): React.ReactNode { + // Not tidy at all but it's a one page app so WONTFIX + let moreButton = ; + let linkClass = "text-blue-400 hover:text-blue-600"; + let content; + let resultCount; + let inner; + if (this.state.comments) { + resultCount = this.state.comments.length; + // Render comments + inner = this.state.comments.map((comment) => { + if (!comment) { + return; + } + let permalink; + if (comment.permalink) { + permalink = comment.permalink; + } else { + permalink = `/comments/${comment.link_id.split('_')[1]}/_/${comment.id}/` + } + return
+
+ +
/r/{comment.subreddit}
+
+ +
/u/{comment.author}
+
+
{new Date(comment.created_utc * 1000).toLocaleString()}
+
+ +
{comment.body}
+
+
+ }); + } else if (this.state.posts && this.state.posts.length > 0) { + resultCount = this.state.posts.length; + // Render posts + inner = this.state.posts.map((post) => { + if (!post) { + return; + } + let thumbnailUrl; + if (post.thumbnail.startsWith('http')) { + thumbnailUrl = post.thumbnail; + } else if (post.url.split('.').pop() === 'png' || post.url.split('.').pop() === 'jpg') { + thumbnailUrl = post.url; + } + let body; + if (post.selftext && post.selftext.length !== 0) { + body =
{post.selftext}
+ } else { + body = +
{post.url}
+
+ } + return
+
+ +
/r/{post.subreddit}
+
+ +
/u/{post.author}
+
+
{new Date(post.created_utc * 1000).toLocaleString()}
+
+
+
+ +
+
+ +
{post.title}
+
+ {body} +
+
+
+ }); + } + if (this.state.comments || this.state.posts) { + content =
+
{resultCount} results - Generated API URL
+ {inner} + {moreButton} +
+ } else if (this.state.lastUrl) { + content = + } else { + content =
+

Search reddit using the pushshift.io API. For more advanced searches you can directly query the API fairly easily.

+

The 'More' button works by changing the 'before' value to the time of the last post in the results. This means that entries might be missed if they were posted at the same time.

+
+ } + // Combine everything and return + return ( + <> + +
+ {/* Author and Subreddit */} +
+
+ + +
+
+ + +
+
+ {/* Type, Count and Score Filter */} +
+
+ +
+ +
+ +
+
+
+
+ + { }} /> +
+
+ + +
+
+ {/* Time Range */} +
+
+ + +
+
+ + +
+
+ {/* Search Term */} +
+ + +
+ {/* Submit Button and Error text */} + + {this.state.error && + <> +

{this.state.errorTime.toLocaleTimeString()} Error: {this.state.error}

+ + } +
+ {content} +
+ + ); + } +} + +// https://gist.github.com/jokester/4a543ea76dbc5ae1bf05 +var hash_accessor = (function (window) { + return { + load: function () { + try { + // strip ^# + var json_str_escaped = window.location.hash.slice(1); + // unescape + var json_str = decodeURIComponent(json_str_escaped); + return JSON.parse(json_str); + } catch (e) { + return {}; + } + }, + save: function (obj) { + // use replace so that previous url does not go into history + window.location.replace('#' + JSON.stringify(obj, (key, value) => { if (value) return value; })); + } + }; +})(window); diff --git a/src/error.tsx b/src/error.tsx new file mode 100644 index 0000000..ee24835 --- /dev/null +++ b/src/error.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; + +export interface ErrorState { + errorOccurred: boolean, + errorInfo: object, + error: object, +} + +export class ErrorWrapper extends React.Component<{}, ErrorState> { + constructor(props) { + super(props) + this.state = { errorOccurred: false, errorInfo: null, error: null } + } + + componentDidCatch(error, info) { + this.setState({ errorOccurred: true, errorInfo: info, error: error }) + } + + render() { + if (this.state.errorOccurred) { + console.log(this.state.error); + console.log(this.state.errorInfo); + return <> +
+

An error occured! code isn't exactly "enterprise" so feel free to tell me on Github or use pushshift directly

+
+

{this.state.error.message}

+

{this.state.errorInfo.componentStack.trim()}

+
+
+ ; + } else { + return this.props.children; + } + } +} diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9022d4c83c3a02e04b92bd5a1c87931e25509b34 GIT binary patch literal 15406 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 bfMElM4H!0H*g*T)06QLJ{0t4atbzXka3VAe literal 0 HcmV?d00001 diff --git a/src/github-link.tsx b/src/github-link.tsx new file mode 100644 index 0000000..cd30073 --- /dev/null +++ b/src/github-link.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +export class GithubLink extends React.Component { + render(): React.ReactNode { + // http://tholman.com/github-corners/ + // https://magic.reactjs.net/htmltojsx.htm + return
+