1.0.0
10
.babelrc
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-react",
|
|
||||||
"@babel/preset-es2015"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-transform-async-to-generator",
|
|
||||||
"@babel/plugin-proposal-object-rest-spread"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
53
.eslintrc
|
|
@ -1,53 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true
|
|
||||||
},
|
|
||||||
"extends": "airbnb",
|
|
||||||
"rules": {
|
|
||||||
"arrow-parens": "off",
|
|
||||||
"comma-dangle": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"arrays": "always-multiline",
|
|
||||||
"exports": "always-multiline",
|
|
||||||
"functions": "ignore",
|
|
||||||
"imports": "always-multiline",
|
|
||||||
"objects": "always-multiline"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"import/extensions": "off",
|
|
||||||
"import/no-extraneous-dependencies": "off",
|
|
||||||
"import/no-unresolved": "off",
|
|
||||||
"import/prefer-default-export": "off",
|
|
||||||
"jsx-a11y/anchor-has-content": "off",
|
|
||||||
"jsx-a11y/anchor-is-valid": "off",
|
|
||||||
"jsx-quotes": [
|
|
||||||
"error",
|
|
||||||
"prefer-single"
|
|
||||||
],
|
|
||||||
"max-len": "off",
|
|
||||||
"no-param-reassign": "warn",
|
|
||||||
"no-plusplus": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"allowForLoopAfterthoughts": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"no-prototype-builtins": "off",
|
|
||||||
"no-script-url": "off",
|
|
||||||
"no-underscore-dangle": "off",
|
|
||||||
"no-unused-vars": "warn",
|
|
||||||
"react/jsx-filename-extension": "off",
|
|
||||||
"react/no-danger": "off",
|
|
||||||
"react/prop-types": "off",
|
|
||||||
"semi": [
|
|
||||||
"error",
|
|
||||||
"never"
|
|
||||||
],
|
|
||||||
"no-use-before-define": "off",
|
|
||||||
"import/first": "off",
|
|
||||||
"prefer-destructuring": "off",
|
|
||||||
"jsx-a11y/click-events-have-key-events": "off",
|
|
||||||
"jsx-a11y/no-static-element-interactions": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
114
README.md
|
|
@ -1,114 +0,0 @@
|
||||||
# Removeddit
|
|
||||||
[Removeddit](https://removeddit.com) is a site for viewing removed stuff from [Reddit](https://www.reddit.com).
|
|
||||||
Just go to a reddit thread and change the `reddit` in the URL to `removeddit` to see all removed comments.
|
|
||||||
|
|
||||||
This is a done by comparing the comments found from Reddit API and comments being stored in [Jason Baumgartners](https://pushshift.io/) [Pushshift Reddit API](https://github.com/pushshift/api). The frontend is written in react with redux for state management. There's also a seperate [backend](https://github.com/JubbeArt/removeddit-api) used for storing removed threads and banned subreddits.
|
|
||||||
|
|
||||||
# Development
|
|
||||||
Get lastest version of [Node](https://nodejs.org/en/download/current/)
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo git clone https://github.com/JubbeArt/removeddit.git && cd removeddit
|
|
||||||
npm install
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
Visit http://localhost:8080 and make sure the site is running. Subreddits and the banned subreddit list will not work unless you also set up a backend. This might change in the future if I decide to open up my backend on removeddit.com/api. Contact me if you want this feature and we can discuss it.
|
|
||||||
|
|
||||||
# Production (basic setup)
|
|
||||||
**THIS WILL BE UPDATED LATER WHEN THE PROJECT IN REWRITTEN**
|
|
||||||
|
|
||||||
Using [Ubuntu 16.04](http://releases.ubuntu.com/16.04/) and [nginx](https://www.nginx.com/resources/wiki/)
|
|
||||||
|
|
||||||
```
|
|
||||||
cd /srv
|
|
||||||
sudo git clone https://github.com/JubbeArt/removeddit.git /srv/removeddit/site
|
|
||||||
chown -R www-data:www-data /srv/removeddit
|
|
||||||
sudo apt install -y nginx
|
|
||||||
sudo cp /srv/removeddit/site/production/nginx-basic /etc/nginx/sites-available/default
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a reddit app [here](https://www.reddit.com/prefs/apps/), select **installed app**. For "redirect url" it doesn't really matter in this case, you can pick `http://localhost`.
|
|
||||||
|
|
||||||
Copy the **client ID** for your app set it as a variable in `id.js`, e.g. with
|
|
||||||
```
|
|
||||||
sudo nano /srv/removeddit/site/..../id.js
|
|
||||||
# Insert with ctrl-shift-v
|
|
||||||
# Save with ctrl-o, exit with ctrl-x
|
|
||||||
```
|
|
||||||
|
|
||||||
Restart nginx and visit "localhost"
|
|
||||||
```
|
|
||||||
sudo service nginx restart
|
|
||||||
```
|
|
||||||
|
|
||||||
# The "I care about HTTPS and security" guide
|
|
||||||
In this part we'll set up [nginx](https://www.nginx.com/resources/wiki/) with SSL and set up a free renewing SSL certificates with [Let's Encrypt](https://letsencrypt.org/). I assume you've already done the guide above.
|
|
||||||
|
|
||||||
## Nginx.conf
|
|
||||||
Add the following to `/etc/nginx/nginx.conf` in the **http-block** (you can read about them [here](https://gist.github.com/plentz/6737338))
|
|
||||||
```
|
|
||||||
server_tokens off;
|
|
||||||
add_header X-Frame-Options SAMEORIGIN;
|
|
||||||
add_header X-Content-Type-Options nosniff;
|
|
||||||
add_header X-XSS-Protection "1; mode=block";
|
|
||||||
```
|
|
||||||
|
|
||||||
In the same file you also want to change `gzip on` to `gzip off` (read more [here](https://github.com/h5bp/server-configs-nginx/issues/72)).
|
|
||||||
|
|
||||||
## Nginx server config
|
|
||||||
Copy the ssl config and create a soft link. Create folder for logs and also remove the default config
|
|
||||||
```
|
|
||||||
sudo cp /srv/removeddit/site/production/nginx-ssl /etc/nginx/sites-available/removeddit.com
|
|
||||||
sudo ln -s /etc/nginx/sites-available/removeddit.com /etc/nginx/sites-enabled/removeddit.com
|
|
||||||
sudo mkdir /var/log/nginx/removeddit
|
|
||||||
sudo rm /etc/nginx/sites-enabled/default
|
|
||||||
```
|
|
||||||
|
|
||||||
Change the "server_name" for both server blocks in the config to your own domain name.
|
|
||||||
|
|
||||||
## SSL with Let's encrypt
|
|
||||||
Read the full guide [here](https://certbot.eff.org/#ubuntutyakkety-nginx). Start of by installing the Let's Encrypt client [certbot](https://certbot.eff.org/)
|
|
||||||
```
|
|
||||||
sudo apt install -y software-properties-common
|
|
||||||
sudo add-apt-repository ppa:certbot/certbot
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y python-certbot-nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy the Let's Encrypt config file for our site
|
|
||||||
```
|
|
||||||
sudo mkdir /etc/letsencrypt/configs
|
|
||||||
cp /srv/removeddit/site/production/letsencrypt.conf /etc/letsencrypt/configs/removeddit.com.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
In this config file change the domains and the email address for your own. The emails tells you when the certificates are close to expiring.
|
|
||||||
|
|
||||||
Pray to God and do this
|
|
||||||
```
|
|
||||||
sudo certbot --config /etc/letsencrypt/configs/removeddit.com.conf certonly
|
|
||||||
```
|
|
||||||
|
|
||||||
You'll now have a valid SSL certificate (hopefully)! You now have to uncomment the `ssl_certificate` and `ssl_certificate_key` in `/etc/nginx/sites-available/removeddit.com` and maybe change the path depending on where the certs are located (check in `/etc/letsencrypt/live/`).
|
|
||||||
|
|
||||||
## Automated renewal of certs
|
|
||||||
The certificate expires after 90 days so we want a way to atomatically update the certs.
|
|
||||||
There are multiple ways of doing this but I find the easiest to be [cron jobs](https://en.wikipedia.org/wiki/Cron).
|
|
||||||
|
|
||||||
First we'll test if renewing atcutally works with
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo certbot renew --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
If everything works fine we can create a cron with ```sudo crontab -e``` and select an editor your comfortable with (I like *nano*).
|
|
||||||
Add the following lines at the bottom
|
|
||||||
|
|
||||||
```
|
|
||||||
# Let's Encrypt cert renewal for all sites (runs every day at 04:30)
|
|
||||||
30 4 * * * certbot renew --post-hook "systemctl reload nginx"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then just restart nginx and that should do it!
|
|
||||||
|
|
||||||
This guide was mostly written for myself, you learn a shitton writing guides, highly recommended. Hopefully you learned something too.
|
|
||||||
366
bundle.d10b86eaab94a05f85f68f96772a41d1.css
Normal file
|
|
@ -0,0 +1,366 @@
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #262626;
|
||||||
|
font-family: verdana, arial, helvetica, sans-serif; }
|
||||||
|
|
||||||
|
.main {
|
||||||
|
margin: 15px; }
|
||||||
|
|
||||||
|
.removed {
|
||||||
|
background-color: #840c09; }
|
||||||
|
|
||||||
|
.deleted {
|
||||||
|
background-color: #00007d; }
|
||||||
|
|
||||||
|
.removed-text {
|
||||||
|
color: #c70300; }
|
||||||
|
|
||||||
|
.deleted-text {
|
||||||
|
color: #00007d; }
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #8cb3d9; }
|
||||||
|
|
||||||
|
a:link, a:visited, a:active {
|
||||||
|
text-decoration: none; }
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline; }
|
||||||
|
|
||||||
|
.author {
|
||||||
|
color: #6a98af; }
|
||||||
|
|
||||||
|
.author:not([href]) {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none; }
|
||||||
|
|
||||||
|
.space {
|
||||||
|
margin-left: 5px; }
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: #828282; }
|
||||||
|
|
||||||
|
header {
|
||||||
|
background-color: #c70300;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px; }
|
||||||
|
header #header a {
|
||||||
|
color: #fff; }
|
||||||
|
header #header h1 {
|
||||||
|
margin: 7px 0;
|
||||||
|
text-align: center; }
|
||||||
|
header #header h1 a {
|
||||||
|
transition: color 0.3s; }
|
||||||
|
header #header h1 a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #ff8b88; }
|
||||||
|
header #header nav a {
|
||||||
|
margin: 0 7px;
|
||||||
|
font-size: 20px; }
|
||||||
|
header #status {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center; }
|
||||||
|
header #status #status-text {
|
||||||
|
color: #fff;
|
||||||
|
margin: 0 20px 0 0; }
|
||||||
|
header #status #status-image {
|
||||||
|
height: 64px;
|
||||||
|
width: 64px;
|
||||||
|
border-radius: 32px; }
|
||||||
|
|
||||||
|
#main-box {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #fff;
|
||||||
|
word-wrap: break-word;
|
||||||
|
max-width: 800px;
|
||||||
|
background-color: #161616; }
|
||||||
|
#main-box h2 {
|
||||||
|
margin: 20px 0px 12px; }
|
||||||
|
#main-box h2.about {
|
||||||
|
margin: 0;
|
||||||
|
color: #c70300; }
|
||||||
|
#main-box h2.todo {
|
||||||
|
color: #239f2b; }
|
||||||
|
#main-box h2.contact {
|
||||||
|
color: #1b767a; }
|
||||||
|
#main-box ul {
|
||||||
|
padding-left: 30px;
|
||||||
|
margin: 0; }
|
||||||
|
#main-box .bookmarklet {
|
||||||
|
background: #c70300;
|
||||||
|
color: #fff;
|
||||||
|
padding: 9px;
|
||||||
|
font-size: large;
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 0 7px; }
|
||||||
|
|
||||||
|
.comment {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
padding: 5px 8px 5px 30px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 14px; }
|
||||||
|
.comment .comment-head {
|
||||||
|
font-size: 10px; }
|
||||||
|
.comment .comment-author {
|
||||||
|
font-weight: bold; }
|
||||||
|
.comment .comment-author:not([href]) {
|
||||||
|
color: #828282; }
|
||||||
|
.comment .comment-score {
|
||||||
|
color: #b4b4b4;
|
||||||
|
font-weight: bold; }
|
||||||
|
.comment .comment-time {
|
||||||
|
color: #828282; }
|
||||||
|
.comment .comment-body {
|
||||||
|
max-width: 840px;
|
||||||
|
line-height: 20px; }
|
||||||
|
.comment .comment-body p {
|
||||||
|
margin: 5px 0;
|
||||||
|
line-height: 20px; }
|
||||||
|
.comment .comment-body a:hover {
|
||||||
|
text-decoration: none; }
|
||||||
|
.comment .comment-links {
|
||||||
|
margin-bottom: 6px; }
|
||||||
|
.comment .comment-links a {
|
||||||
|
color: #828282;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-right: 4px; }
|
||||||
|
|
||||||
|
.comment-odd {
|
||||||
|
background-color: #121212; }
|
||||||
|
|
||||||
|
.comment-even {
|
||||||
|
background-color: #161616; }
|
||||||
|
|
||||||
|
#comment-info {
|
||||||
|
margin-top: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 1px dotted #808080;
|
||||||
|
font-size: 16px; }
|
||||||
|
|
||||||
|
#sort-by {
|
||||||
|
color: #828282;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 5px 0 10px; }
|
||||||
|
|
||||||
|
#comment-info {
|
||||||
|
color: #ca302c;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 1px dotted #808080;
|
||||||
|
font-size: 16px; }
|
||||||
|
|
||||||
|
#comment-sort {
|
||||||
|
color: #808080;
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 5px 0 10px; }
|
||||||
|
|
||||||
|
.thread {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
border-radius: 1px;
|
||||||
|
padding-top: 6px; }
|
||||||
|
.thread .thumbnail {
|
||||||
|
width: 70px;
|
||||||
|
margin-right: 3px;
|
||||||
|
margin-top: -2px; }
|
||||||
|
.thread .thumbnail img {
|
||||||
|
border-radius: 1px; }
|
||||||
|
.thread .thumbnail-default {
|
||||||
|
background-image: url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");
|
||||||
|
background-position: 0px -1099px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
height: 50px; }
|
||||||
|
.thread .thumbnail-self {
|
||||||
|
background-image: url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");
|
||||||
|
background-position: 0px -1267px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
height: 50px; }
|
||||||
|
.thread .thumbnail-image {
|
||||||
|
background-image: url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");
|
||||||
|
background-position: 0px -1043px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
height: 50px; }
|
||||||
|
.thread .thumbnail-nsfw {
|
||||||
|
background-image: url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png");
|
||||||
|
background-position: 0px -1155px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
height: 50px; }
|
||||||
|
.thread .thread-score-box {
|
||||||
|
margin: 0 7px;
|
||||||
|
width: 43px; }
|
||||||
|
.thread .vote {
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC0AAAAcCAMAAADC4sagAAABFFBMVEUjJCQiIiIjIyMkIiIkJCQlJSUlJicmJiYnIyEnJycnKCsoKCgpLC8rIyErKyssMDUvLy8wJCAwMDAxMTEyMjI0NDQ2NjY3JR83Nzc5OTk7Ozs8PDw9PT0+Pj4/Pz9AQEBBQUFCQkJDQ0NGRkZISEhKSkpKV2tLS0tMTExOTk5PT09QYHdSUlJTU1NVVVVWVlZXV1dXaINYWFhaWlpabYlbW1tecpBgYGBhYWFid5dkZGRle5xlfJ1pgKRrhKhshKlshapuLhZwirFyjbV0j7h4lL97mcZ9MBR/nsyCo9OOMxGcNQ+eNg+kNw6sOA2vOAy0OQy7Ogq/OwrBOwrDPAnLPQjPPQfTPgfbPwXjQQTrQgPzQwLKxGgxAAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlJREFUOMu9lF1TwjAQRTcMvmeQOi1QPmxFBQTxW1ERUUFEUVRU/P//w9yGmBTFyZPnpXNmbju76W6IJM8Uw49rLa6Tz4mpq/mCqY3Gvqnj6XQ61lp0mGd8favCwx2tow8wUlpwGWOZrNJKjXO+UVc6fJcMZzV7IswSOU/qej0l0rwyq33wphhAM1kWkcy70GB7mUfUNqG9V02PyPMTMs2Wig5RaTctwzxVXyPqvJh0VkJBIKJ4huncCRAvHOEZyGKegO4a6UDrIdLGCT4C6/QDsE7fA+v0HdAadWqk0SH9D3sgXkm4uJIDYKTFnwwXd3kMrNOnwDp9BmzS5QuTshN1iDRwfkxV9VJTJXLzSWYM+NzEEjWvFE2oGvCE7/2yDeL8riWzM8xmorRazLlNI2rdgNb3ZYLF1Es/t8VE7X6/39Yqlt4tLLwhiLq3XVNLfumP24fo3Opm+wLGCVa252Y8tQAAAABJRU5ErkJggg==);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 15px;
|
||||||
|
height: 14px;
|
||||||
|
cursor: pointer; }
|
||||||
|
.thread .upvote {
|
||||||
|
background-position: -15px 0px;
|
||||||
|
margin-top: 2px; }
|
||||||
|
.thread .downvote {
|
||||||
|
background-position: -15px -14px;
|
||||||
|
margin-top: 2px; }
|
||||||
|
.thread .thread-score {
|
||||||
|
color: #646464;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center; }
|
||||||
|
.thread .thread-content {
|
||||||
|
flex: 1;
|
||||||
|
float: left;
|
||||||
|
margin-left: 3px; }
|
||||||
|
.thread .link-flair {
|
||||||
|
display: inline-block;
|
||||||
|
color: #c8c8c8;
|
||||||
|
background-color: #404040;
|
||||||
|
margin-right: 5px;
|
||||||
|
border: 1px solid #4d4d4d;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0 2px;
|
||||||
|
font-size: 10px; }
|
||||||
|
.thread .thread-title {
|
||||||
|
color: #a6a6a6;
|
||||||
|
margin-right: 5px; }
|
||||||
|
.thread .thread-title:hover {
|
||||||
|
text-decoration: none; }
|
||||||
|
.thread .domain {
|
||||||
|
color: #888;
|
||||||
|
font-size: 10px; }
|
||||||
|
.thread .thread-selftext {
|
||||||
|
border: 1px solid #666;
|
||||||
|
border-radius: 7px;
|
||||||
|
margin: 5px 0 7px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
max-width: 840px;
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 14px; }
|
||||||
|
.thread .thread-selftext p {
|
||||||
|
margin: 5px 0;
|
||||||
|
line-height: 20px; }
|
||||||
|
.thread .thread-selftext a:hover {
|
||||||
|
text-decoration: none; }
|
||||||
|
.thread .thread-selftext p:first-child {
|
||||||
|
margin-top: 0px; }
|
||||||
|
.thread .thread-selftext p:last-child {
|
||||||
|
margin-bottom: 0px; }
|
||||||
|
.thread .thread-image {
|
||||||
|
max-width: 768px;
|
||||||
|
max-height: 768px; }
|
||||||
|
.thread .thread-info {
|
||||||
|
color: #828282;
|
||||||
|
font-size: 10px;
|
||||||
|
margin-top: 2px; }
|
||||||
|
.thread .total-comments {
|
||||||
|
color: #828282;
|
||||||
|
font-size: 10px;
|
||||||
|
margin: 4px 0; }
|
||||||
|
.thread .grey-link, .thread .grey-link:link, .thread .grey-link:visited, .thread .grey-link:hover, .thread .grey-link:focus, .thread .grey-link:active {
|
||||||
|
color: #828282;
|
||||||
|
margin-right: 4px; }
|
||||||
|
|
||||||
|
.post-rank {
|
||||||
|
min-width: 20px;
|
||||||
|
color: #505050;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: right;
|
||||||
|
margin: 14px 4px 0 0; }
|
||||||
|
|
||||||
|
.subreddit-info {
|
||||||
|
border-bottom: 1px dotted gray;
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 5px; }
|
||||||
|
.subreddit-info select {
|
||||||
|
background: #262626;
|
||||||
|
color: #808080;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: underline;
|
||||||
|
border: 0; }
|
||||||
|
|
||||||
|
#pagination {
|
||||||
|
border-top: 1px dotted gray;
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: #ccc;
|
||||||
|
font-size: 16px; }
|
||||||
|
#pagination span, #pagination a {
|
||||||
|
margin: 5px; }
|
||||||
|
|
||||||
|
.subreddit-box {
|
||||||
|
margin-bottom: 15px; }
|
||||||
|
|
||||||
|
.subreddit-title {
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: white; }
|
||||||
|
|
||||||
|
.subreddit-title-link {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: white; }
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
font-size: 16px; }
|
||||||
|
.search-box .search-row {
|
||||||
|
padding: 10px 6px 11px;
|
||||||
|
border-bottom: 1px dotted #ccc;
|
||||||
|
min-height: 26px; }
|
||||||
|
.search-box .search-left {
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
width: 190px;
|
||||||
|
vertical-align: middle; }
|
||||||
|
.search-box input[type="text"] {
|
||||||
|
font-size: 17px;
|
||||||
|
width: 400px;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding-left: 3px; }
|
||||||
|
.search-box .radioButton {
|
||||||
|
background: #8a8888;
|
||||||
|
color: black;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 9px;
|
||||||
|
transition: all 0.3s; }
|
||||||
|
.search-box .radioButton label {
|
||||||
|
padding: 7px 10px 7px 3px;
|
||||||
|
display: inline-block; }
|
||||||
|
.search-box .radioButton input {
|
||||||
|
margin: 0 0 0 5px;
|
||||||
|
height: 35px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top; }
|
||||||
|
.search-box select {
|
||||||
|
padding: 3px;
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 2px; }
|
||||||
|
.search-box input[type="submit"] {
|
||||||
|
margin: 16px auto 0px;
|
||||||
|
display: block;
|
||||||
|
padding: 9px 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
background: #239f2b;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: white; }
|
||||||
1
bundle.e7f2556d686d9e37db02.js
Normal file
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
|
@ -62,9 +62,9 @@ __)|_(_||||||(_|| ) |/\|(_|_) | \ |(_)| )|_
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
<link href="/images/favicon.ico" rel="shotcut icon">
|
<link href="/images/favicon.ico" rel="shotcut icon">
|
||||||
</head>
|
<link href="/bundle.d10b86eaab94a05f85f68f96772a41d1.css" rel="stylesheet"></head>
|
||||||
<body>
|
<body>
|
||||||
<script>console.time('scripts loaded')</script>
|
<script>console.time('scripts loaded')</script>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
</body>
|
<script type="text/javascript" src="/bundle.e7f2556d686d9e37db02.js"></script></body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
// UTC time handling, very usefull when dealing with elasticsearch
|
|
||||||
var Time = (function () {
|
|
||||||
return {
|
|
||||||
utc() {
|
|
||||||
return Math.floor(_.now() / 1000)
|
|
||||||
},
|
|
||||||
|
|
||||||
toUTC(timeString) {
|
|
||||||
const parts = timeString.split(/[a-zA-Z]+/)
|
|
||||||
let times = 1
|
|
||||||
|
|
||||||
// Number before the time (e.g. 2years or 12hour)
|
|
||||||
if (parts.length === 2 && parts[0] !== '') {
|
|
||||||
times = _.parseInt(parts[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.includes(timeString, 'hour')) return times * 3600
|
|
||||||
if (_.includes(timeString, 'day')) return times * 86400
|
|
||||||
if (_.includes(timeString, 'week')) return times * 604800
|
|
||||||
if (_.includes(timeString, 'month')) return times * 2592000
|
|
||||||
if (_.includes(timeString, 'year')) return times * 31536000
|
|
||||||
|
|
||||||
return Time.utc()
|
|
||||||
},
|
|
||||||
|
|
||||||
difference(timeString) {
|
|
||||||
return Time.utc() - Time.toUTC(timeString)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}())
|
|
||||||
232
js/search.js
|
|
@ -1,232 +0,0 @@
|
||||||
var app = (function(){
|
|
||||||
return {
|
|
||||||
loadPage: function(){
|
|
||||||
Vars.loadVars();
|
|
||||||
SearchHTML.createSearch();
|
|
||||||
|
|
||||||
if(Vars.get("search") === "true") {
|
|
||||||
if(Vars.get("thread") === "true") {
|
|
||||||
ElasticSearch.thread();
|
|
||||||
} else {
|
|
||||||
ElasticSearch.comment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
var ElasticSearch = (function(){
|
|
||||||
return {
|
|
||||||
comment: function(){
|
|
||||||
|
|
||||||
},
|
|
||||||
thread: function(){
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
var Vars = (function(){
|
|
||||||
var lookup = {};
|
|
||||||
var keys = ["thread","text", "title", "subreddit", "author", "over_18", "locked", "removed", "deleted", "time", "sort", "search"];
|
|
||||||
var defaults = ["true", "", "", "", "", "both", "both", "", "", "all", "score_dsec", "false"];
|
|
||||||
var defaultLookup = {};
|
|
||||||
var showAdvanced = true;
|
|
||||||
|
|
||||||
for(var i = 0, len = keys.length; i < len; i++) {
|
|
||||||
defaultLookup[keys[i]] = defaults[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
loadVars: function(){
|
|
||||||
_.forEach(keys, function(key){
|
|
||||||
lookup[key] = _.defaultTo(GetVars.get(key), defaultLookup[key]);
|
|
||||||
});
|
|
||||||
|
|
||||||
showAdvanced = _.defaultTo(Cookies.get("showAdvanced"), true);
|
|
||||||
},
|
|
||||||
updateVars: function(){
|
|
||||||
_.forEach(keys, function(key){
|
|
||||||
lookup[key] = document.getElementById(key).value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
get: function(key){
|
|
||||||
if(key === "showAdvanced") {
|
|
||||||
return showAdvanced;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lookup[key];
|
|
||||||
},
|
|
||||||
generateURL : function(){
|
|
||||||
var url = "?";
|
|
||||||
var first = true;
|
|
||||||
_.forEach(lookup, function(key){
|
|
||||||
if(lookup[key] !== defaultLookup[key]) {
|
|
||||||
if(first) {
|
|
||||||
url += key+"="+lookup[key];
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
url += "&"+key+"="+lookup[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if(url === "?") {
|
|
||||||
return "?search=true";
|
|
||||||
}
|
|
||||||
|
|
||||||
return url + "&search=true";
|
|
||||||
},
|
|
||||||
setShowAdvanced: function(shouldShowAdvaned){
|
|
||||||
showAdvanced = shouldShowAdvaned;
|
|
||||||
},
|
|
||||||
lookup: lookup
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
var SearchHTML = (function(){
|
|
||||||
var mainDiv = document.getElementById("main");
|
|
||||||
|
|
||||||
var radioInput = function(name, displayNames, values) {
|
|
||||||
var inputs = "";
|
|
||||||
|
|
||||||
for(var i = 0, len = displayNames.length; i < len; i++){
|
|
||||||
inputs += '<span class="radioButton"'+ (Vars.lookup[name] === values[i] ? ' style="background: #239f2b; color:#fff"':'')+'>';
|
|
||||||
inputs += '<input type="radio" id="'+name+i+'" name="'+name+'" onchange="CSS.radio(this)"';
|
|
||||||
inputs += 'value="'+values[i]+'" '+(Vars.lookup[name] === values[i] ? ' checked "':'')+' >';
|
|
||||||
inputs += '<label for="'+name+i+'">'+displayNames[i]+'</label></span>';
|
|
||||||
}
|
|
||||||
return inputs;
|
|
||||||
};
|
|
||||||
|
|
||||||
var textInput = function(name){
|
|
||||||
return '<input type="text" name="'+name+'" id="'+name+'" value="'+Vars.get(name)+'">';
|
|
||||||
};
|
|
||||||
|
|
||||||
var label = function(name, display) {
|
|
||||||
return '<label for="'+name+'">'+display+': </label>';
|
|
||||||
};
|
|
||||||
|
|
||||||
var inputRow = function(text, input) {
|
|
||||||
return '<div class="search-row"><span class="search-left">'+text+'</span><span>'+input+"</span></div>";
|
|
||||||
};
|
|
||||||
|
|
||||||
var selectTime = function() {
|
|
||||||
var values = ["hour", "12hour", "day", "week", "month", "6month", "year", "all"];
|
|
||||||
var display = ["past hour", "past 12 hours", "past day", "past week", "past month", "past 6 months", "past year", "all time"];
|
|
||||||
var html = '<select name="time" id="time">';
|
|
||||||
|
|
||||||
for(var i = 0, len = values.length; i < len; i++) {
|
|
||||||
html += '<option value="'+values[i]+'"' + ((Vars.get("time") === values[i]) ? " selected" : '')+'>'+display[i]+'</option>';
|
|
||||||
}
|
|
||||||
|
|
||||||
return html + "</select>";
|
|
||||||
};
|
|
||||||
|
|
||||||
var select = function(name, display, values){
|
|
||||||
var html = '<select name="'+name+'" id="'+name+'">';
|
|
||||||
|
|
||||||
for(var i = 0, len = values.length; i < len; i++) {
|
|
||||||
html += '<option value="'+values[i]+'"' + ((Vars.get(name) === values[i]) ? " selected" : '')+'>'+display[i]+'</option>';
|
|
||||||
}
|
|
||||||
|
|
||||||
return html + "</select>";
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
createSearch: function(){
|
|
||||||
var searchBox = document.createElement("div");
|
|
||||||
searchBox.id = "main-box";
|
|
||||||
searchBox.className = "search-box";
|
|
||||||
/*
|
|
||||||
Comments:
|
|
||||||
text (body) string
|
|
||||||
title (title) string
|
|
||||||
subreddit (subreddit) string
|
|
||||||
author (author) string
|
|
||||||
over_18 (over_18) sfw,nsfw,both *
|
|
||||||
locked (locked) bool *
|
|
||||||
between (after/before) "date"
|
|
||||||
sort (sort) asc,desc
|
|
||||||
*/
|
|
||||||
var html = inputRow('Im looking for: ', radioInput("thread", ["Thread","Comment"], ["true", "false"]));
|
|
||||||
html += inputRow(label("text", "Text"), textInput("text"));
|
|
||||||
html += inputRow(label("title","Title"), textInput("title"));
|
|
||||||
html += inputRow(label("subreddit", "Subreddit"), textInput("subreddit"));
|
|
||||||
html += inputRow(label("author", "Author"), textInput("author"));
|
|
||||||
html += inputRow("Over 18: ", radioInput("over_18", ["Both","NSFW","SFW"], ["both","nsfw","sfw"]));
|
|
||||||
html += inputRow("Locked: ", radioInput("locked", ["Both","True", "False"], ["both","true","false"]));
|
|
||||||
html += inputRow("Removed: ", select("removed", ["Removed and non-removed", "Only removed", "Only deleted"], ["all", "removed", "deleted"]));
|
|
||||||
html += inputRow("From:", selectTime());
|
|
||||||
html += inputRow("Sort: ", select("sort", ["Highest score","Lowest score","Newest","Oldest"], ["score_desc","score_asc","time_desc","time_asc"]));
|
|
||||||
html += '<input type="button" value="'+(showAdvanced?'Hide':'Show')+' advanced">';
|
|
||||||
html += '<input type="submit" value="Search">';
|
|
||||||
searchBox.innerHTML = html;
|
|
||||||
|
|
||||||
mainDiv.appendChild(searchBox);
|
|
||||||
},
|
|
||||||
|
|
||||||
commentResults: function(){
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
threadResults: function(){
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
// name
|
|
||||||
//var CSS = (function(){
|
|
||||||
return {
|
|
||||||
radio: function(event) {
|
|
||||||
var radioButtons = document.getElementsByName(event.name);
|
|
||||||
|
|
||||||
for(var i = 0, len = radioButtons.length; i < len; i++){
|
|
||||||
if(radioButtons[i].id == event.id) {
|
|
||||||
radioButtons[i].parentNode.style = "background: #239f2b; color:#fff";
|
|
||||||
} else {
|
|
||||||
radioButtons[i].parentNode.style = "background: #8a8888; color:#000";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateDisplayedOptions: function(){
|
|
||||||
var showAdvanced = Vars.get("showAdvanced") == "true";
|
|
||||||
var thread = document.getElementById("thread").value == "thread";
|
|
||||||
console.log(document.getElementById("thread").value)
|
|
||||||
var display;
|
|
||||||
var hide;
|
|
||||||
|
|
||||||
if(thread && showAdvanced) {
|
|
||||||
display = ["body","title","subreddit","author","over_18","locked","removed","time","sort"];
|
|
||||||
hide = [];
|
|
||||||
} else if(thread && !showAdvanced) {
|
|
||||||
display = ["body", "title", "subreddit"];
|
|
||||||
hide = ["author","over_18","locked","removed","time","sort"];
|
|
||||||
} else if(!thread && showAdvanced) {
|
|
||||||
display = ["body","subreddit","author","removed","time","sort"];
|
|
||||||
hide = ["title","over_18","locked"];
|
|
||||||
} else if(!thread && !showAdvanced) {
|
|
||||||
display = ["body","subreddit"];
|
|
||||||
hide = ["title","over_18","locked", "author","removed","time","sort"];
|
|
||||||
}
|
|
||||||
|
|
||||||
_.forEach(display, function(id){
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
},
|
|
||||||
toggleShowAdvanced: function(){
|
|
||||||
Vars.setShowAdvanced(Vars.get("showAdvanced"));
|
|
||||||
Cookies.set("showAdvanced", Vars.get("showAdvanced"), , { expires: 365 });
|
|
||||||
CSS.
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
if(isSupported) {
|
|
||||||
app.loadPage();
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
take utc from thread and limit pushshift comments
|
|
||||||
|
|
||||||
|
|
||||||
rename show to filter
|
|
||||||
11343
package-lock.json
generated
68
package.json
|
|
@ -1,68 +0,0 @@
|
||||||
{
|
|
||||||
"name": "removeddit",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "View removed comments from reddit",
|
|
||||||
"main": "src/index.js",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/JubbeArt/removeddit.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"reddit",
|
|
||||||
"comments",
|
|
||||||
"removed",
|
|
||||||
"deleted"
|
|
||||||
],
|
|
||||||
"author": "Jesper Wrang",
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/JubbeArt/removeddit/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/JubbeArt/removeddit#readme",
|
|
||||||
"scripts": {
|
|
||||||
"start": "webpack-dev-server --config webpack.dev.js",
|
|
||||||
"build": "webpack --config webpack.dev.js",
|
|
||||||
"build-prod": "webpack --config webpack.prod.js",
|
|
||||||
"clean": "rm -r node_modules && rm package-lock.json",
|
|
||||||
"gh-pages": "npm run build-prod && node publish.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/polyfill": "^7.0.0-beta.37",
|
|
||||||
"react": "^16.2.0",
|
|
||||||
"react-dom": "^16.2.0",
|
|
||||||
"react-redux": "^5.0.6",
|
|
||||||
"react-router-dom": "^4.2.2",
|
|
||||||
"redux": "^3.7.2",
|
|
||||||
"snuownd": "git+https://github.com/JordanMilne/snuownd.git",
|
|
||||||
"whatwg-fetch": "^2.0.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/cli": "^7.0.0-beta.37",
|
|
||||||
"@babel/core": "^7.0.0-beta.37",
|
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.0.0-beta.37",
|
|
||||||
"@babel/plugin-transform-async-to-generator": "^7.0.0-beta.37",
|
|
||||||
"@babel/preset-es2015": "^7.0.0-beta.37",
|
|
||||||
"@babel/preset-react": "^7.0.0-beta.37",
|
|
||||||
"babel-eslint": "^8.2.1",
|
|
||||||
"babel-loader": "^8.0.0-beta.0",
|
|
||||||
"clean-webpack-plugin": "^0.1.17",
|
|
||||||
"copy-webpack-plugin": "^4.3.1",
|
|
||||||
"css-loader": "^0.28.8",
|
|
||||||
"eslint": "^4.15.0",
|
|
||||||
"eslint-config-airbnb": "^16.1.0",
|
|
||||||
"eslint-plugin-import": "^2.8.0",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
|
||||||
"eslint-plugin-react": "^7.5.1",
|
|
||||||
"extract-text-webpack-plugin": "^3.0.2",
|
|
||||||
"gh-pages": "^1.1.0",
|
|
||||||
"html-loader": "^0.5.4",
|
|
||||||
"html-webpack-plugin": "^2.30.1",
|
|
||||||
"node-sass": "^4.7.2",
|
|
||||||
"redux-logger": "^3.0.6",
|
|
||||||
"sass-loader": "^6.0.6",
|
|
||||||
"uglifyjs-webpack-plugin": "^1.1.6",
|
|
||||||
"webpack": "^3.10.0",
|
|
||||||
"webpack-dev-server": "^2.10.0",
|
|
||||||
"webpack-merge": "^4.1.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
# Things to change
|
|
||||||
domains = removeddit.com, www.removeddit.com
|
|
||||||
email = removeddit@gmail.com
|
|
||||||
|
|
||||||
# These defaults are fine (could use 4096 instead of 2048)
|
|
||||||
rsa-key-size = 2048
|
|
||||||
text = True
|
|
||||||
authenticator = webroot
|
|
||||||
webroot-path = /srv/removeddit/site/
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
|
|
||||||
root /srv/removeddit/site;
|
|
||||||
index index.html;
|
|
||||||
server_name _;
|
|
||||||
|
|
||||||
error_page 404 /index.html
|
|
||||||
|
|
||||||
#location /api/ {
|
|
||||||
# include uwsgi_params;
|
|
||||||
# uwsgi_pass unix:/srv/removeddit-api/socket/uwsgi.sock;
|
|
||||||
#}
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html =404;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
server {
|
|
||||||
listen 443 ssl http2 default_server;
|
|
||||||
listen [::]:443 ssl http2 default_server;
|
|
||||||
|
|
||||||
root /srv/removeddit/site;
|
|
||||||
index index.html;
|
|
||||||
server_name removeddit.com www.removeddit.com;
|
|
||||||
|
|
||||||
access_log /var/log/nginx/removeddit/access.log;
|
|
||||||
error_log /var/log/nginx/removeddit/error.log;
|
|
||||||
|
|
||||||
# Uncomment these after you've received the certificates
|
|
||||||
#ssl_certificate /etc/letsencrypt/live/removeddit.com/fullchain.pem;
|
|
||||||
#ssl_certificate_key /etc/letsencrypt/live/removeddit.com/privkey.pem;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html =404;
|
|
||||||
}
|
|
||||||
|
|
||||||
#location /api/ {
|
|
||||||
# include uwsgi_params;
|
|
||||||
# uwsgi_pass unix:/srv/removeddit-api/socket/uwsgi.sock;
|
|
||||||
#}
|
|
||||||
}
|
|
||||||
|
|
||||||
server {
|
|
||||||
listen 80 default_server;
|
|
||||||
listen [::]:80 default_server;
|
|
||||||
server_name removeddit.com www.removeddit.com;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~ /.well-known {
|
|
||||||
allow all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
BrowserRouter,
|
|
||||||
Switch,
|
|
||||||
Route,
|
|
||||||
} from 'react-router-dom'
|
|
||||||
|
|
||||||
import Header from 'components/Header'
|
|
||||||
import { About, Thread, Subreddit } from 'pages'
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<BrowserRouter basename={__dirname}>
|
|
||||||
<React.Fragment>
|
|
||||||
<Header />
|
|
||||||
<div className='main'>
|
|
||||||
<Switch>
|
|
||||||
<Route exact path='/' component={Subreddit} />
|
|
||||||
<Route path='/about' component={About} />
|
|
||||||
<Route path='/r/:subreddit/comments/:threadID/:junk/:commentID' component={Thread} />
|
|
||||||
<Route path='/r/:subreddit/comments/:threadID' component={Thread} />
|
|
||||||
<Route path='/r/:subreddit' component={Subreddit} />
|
|
||||||
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</React.Fragment>
|
|
||||||
</BrowserRouter>
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
import { json, toBase10, toBase36, chunk, flatten } from 'utils'
|
|
||||||
|
|
||||||
const baseURL = 'https://elastic.pushshift.io'
|
|
||||||
const postURL = `${baseURL}/rs/submissions/_search?source=`
|
|
||||||
const commentURL = `${baseURL}/rc/comments/_search?source=`
|
|
||||||
|
|
||||||
export const getPost = threadID => {
|
|
||||||
const elasticQuery = {
|
|
||||||
query: {
|
|
||||||
term: {
|
|
||||||
id: toBase10(threadID),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
fetch(postURL + JSON.stringify(elasticQuery))
|
|
||||||
.then(json)
|
|
||||||
.then(jsonData => jsonData.hits.hits[0]._source)
|
|
||||||
.then(post => {
|
|
||||||
post.id = toBase36(post.id)
|
|
||||||
return post
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getComments = threadID => (
|
|
||||||
fetch(`https://api.pushshift.io/reddit/comment/search?link_id=${threadID}&limit=10000`)
|
|
||||||
.then(json)
|
|
||||||
.then(data => data.data)
|
|
||||||
.then(comments => {
|
|
||||||
comments.forEach(comment => {
|
|
||||||
comment.link_id = comment.link_id.split('_')[1]
|
|
||||||
comment.parent_id = comment.parent_id.split('_')[1]
|
|
||||||
})
|
|
||||||
return comments
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// export const getComments = threadID => {
|
|
||||||
// const elasticQuery = {
|
|
||||||
// query: {
|
|
||||||
// match: {
|
|
||||||
// link_id: toBase10(threadID),
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// size: 10000,
|
|
||||||
// // _source: [
|
|
||||||
// // 'author', 'body', 'created_utc', 'parent_id', 'score', 'subreddit', 'link_id',
|
|
||||||
// // ],
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// fetch(commentURL + JSON.stringify(elasticQuery))
|
|
||||||
// .then(json)
|
|
||||||
// .then(jsonData => jsonData.hits.hits)
|
|
||||||
// .then(comments => comments.map(comment => {
|
|
||||||
// comment._source.id = toBase36(comment._id)
|
|
||||||
// comment._source.link_id = toBase36(comment._source.link_id)
|
|
||||||
|
|
||||||
// // Missing parent id === direct reply to thread
|
|
||||||
// if (!comment._source.parent_id) {
|
|
||||||
// comment._source.parent_id = threadID
|
|
||||||
// } else {
|
|
||||||
// comment._source.parent_id = toBase36(comment._source.parent_id)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (comment._source.id === 'dmrxgic') {
|
|
||||||
// console.log(JSON.stringify(comment._source))
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return comment._source
|
|
||||||
// }))
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import { getToken } from './token'
|
|
||||||
|
|
||||||
// Header for general api calls
|
|
||||||
export const getAuth = () => (
|
|
||||||
getToken()
|
|
||||||
.then(token => ({
|
|
||||||
headers: {
|
|
||||||
Authorization: `bearer ${token}`,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
// Change this to your own client ID: https://www.reddit.com/prefs/apps
|
|
||||||
// The app NEEDS TO BE an installed app and NOT a web apps
|
|
||||||
|
|
||||||
// Current using dummy ID from throwaway
|
|
||||||
export default '33W8M1OOxPv80A'
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { chunk, flatten, json } from 'utils'
|
|
||||||
import { getAuth } from './auth'
|
|
||||||
|
|
||||||
export const getComments = commentIDs => (
|
|
||||||
getAuth()
|
|
||||||
.then(auth => (
|
|
||||||
Promise.all(chunk(commentIDs, 100).map(ids => fetchComments(ids, auth))).then(flatten)
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
export const fetchComments = (commentIDs, auth) => (
|
|
||||||
fetch(`https://oauth.reddit.com/api/info?id=${commentIDs.map(id => `t1_${id}`).join()}`, auth)
|
|
||||||
.then(json)
|
|
||||||
.then(results => results.data.children)
|
|
||||||
.then(commentsData => commentsData.map(commentData => commentData.data))
|
|
||||||
)
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
export { getPost, getThreads } from './thread'
|
|
||||||
export { getComments } from './comment'
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import { json } from 'utils'
|
|
||||||
import { getAuth } from './auth'
|
|
||||||
|
|
||||||
const cachedThreads = {}
|
|
||||||
|
|
||||||
// Thread = Post + Comments
|
|
||||||
// Return the post itself
|
|
||||||
export const getPost = (subreddit, threadID) => (
|
|
||||||
getThread(subreddit, threadID)
|
|
||||||
.then(thread => thread[0].data.children[0].data)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const getThread = (subreddit, threadID, commentID = '') => {
|
|
||||||
// We have already downloaded the thread and can use a cached copy
|
|
||||||
if (cachedThreads.hasOwnProperty(threadID)) {
|
|
||||||
if (cachedThreads[threadID].hasOwnProperty(commentID)) {
|
|
||||||
return Promise.resolve(cachedThreads[threadID][commentID])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `https://oauth.reddit.com/r/${subreddit}/comments/${threadID}/_/${commentID}`
|
|
||||||
// Fetch thread from reddit
|
|
||||||
return (
|
|
||||||
getAuth()
|
|
||||||
.then(auth => fetch(url, auth))
|
|
||||||
.then(json)
|
|
||||||
.then(thread => {
|
|
||||||
// Create cache object for thread if it doesn't exists
|
|
||||||
if (!cachedThreads.hasOwnProperty(threadID)) {
|
|
||||||
cachedThreads[threadID] = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the thread for later
|
|
||||||
cachedThreads[threadID][commentID] = thread
|
|
||||||
|
|
||||||
// Return the thread
|
|
||||||
return thread
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const getThreads = threadIDs => {
|
|
||||||
const threadString = threadIDs.map(id => `t3_${id}`).join()
|
|
||||||
|
|
||||||
return (
|
|
||||||
getAuth()
|
|
||||||
.then(auth => fetch(`https://oauth.reddit.com/api/info?id=${threadString}`, auth))
|
|
||||||
.then(json)
|
|
||||||
.then(response => {
|
|
||||||
const threads = response.data.children
|
|
||||||
return threads.map(threadData => threadData.data)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import { json } from 'utils'
|
|
||||||
import clientID from './clientID'
|
|
||||||
|
|
||||||
// Token for reddit API
|
|
||||||
let token = null
|
|
||||||
|
|
||||||
// Headers for getting reddit api token
|
|
||||||
const tokenInit = {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${btoa(`${clientID}:`)}`,
|
|
||||||
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
|
|
||||||
},
|
|
||||||
method: 'POST',
|
|
||||||
body: `grant_type=${encodeURIComponent('https://oauth.reddit.com/grants/installed_client')}&device_id=DO_NOT_TRACK_THIS_DEVICE`,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getToken = () => {
|
|
||||||
if (token !== null) {
|
|
||||||
return Promise.resolve(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
fetch('https://www.reddit.com/api/v1/access_token', tokenInit)
|
|
||||||
.then(json)
|
|
||||||
.then(response => {
|
|
||||||
token = response.access_token
|
|
||||||
return token
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
import { json } from 'utils'
|
|
||||||
|
|
||||||
const baseURL = 'http://localhost:9000/api'
|
|
||||||
|
|
||||||
export const getRemovedThreadIDs = (subreddit = '', page = 1) => (
|
|
||||||
fetch(`${baseURL}/threads?subreddit=${subreddit}&page=${page - 1}`)
|
|
||||||
.then(json)
|
|
||||||
)
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { prettyScore, prettyDate, parse } from 'utils'
|
|
||||||
|
|
||||||
const Comment = (props) => {
|
|
||||||
let commentStyle = 'comment '
|
|
||||||
|
|
||||||
if (props.removed) {
|
|
||||||
commentStyle += 'removed'
|
|
||||||
} else if (props.deleted) {
|
|
||||||
commentStyle += 'deleted'
|
|
||||||
} else {
|
|
||||||
commentStyle += props.depth % 2 === 0 ? 'comment-even' : 'comment-odd'
|
|
||||||
}
|
|
||||||
|
|
||||||
const innerHTML = (props.body === '[removed]' && props.removed) ? '<p>[removed too quickly to be archived]</p>' : parse(props.body)
|
|
||||||
const permalink = `/r/${props.subreddit}/comments/${props.link_id}/_/${props.id}/`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id={props.id} className={commentStyle}>
|
|
||||||
<div className='comment-head'>
|
|
||||||
<a href='#' onClick={() => false} className='author'>[–]</a>
|
|
||||||
<span className='space' />
|
|
||||||
<a
|
|
||||||
href={props.author !== '[deleted]' ? `https://www.reddit.com/user/${props.author}` : undefined}
|
|
||||||
className='author comment-author'
|
|
||||||
>
|
|
||||||
{props.author}
|
|
||||||
{props.deleted && ' (deleted by user)'}
|
|
||||||
</a>
|
|
||||||
<span className='space' />
|
|
||||||
<span className='comment-score'>{prettyScore(props.score)} point{(props.score !== 1) && 's'}</span>
|
|
||||||
<span className='space' />
|
|
||||||
<span className='comment-time'>{prettyDate(props.created_utc)}</span>
|
|
||||||
</div>
|
|
||||||
<div className='comment-body' dangerouslySetInnerHTML={{ __html: innerHTML }} />
|
|
||||||
<div className='comment-links'>
|
|
||||||
<a href={permalink}>permalink</a>
|
|
||||||
<a href={`https://www.reddit.com${permalink}`}>reddit</a>
|
|
||||||
<a href={`https://snew.github.io${permalink}`}>ceddit</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{props.replies.map(comment => (
|
|
||||||
<Comment
|
|
||||||
key={comment.id}
|
|
||||||
{...comment}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Comment
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
const getProcent = (part, total) => (total === 0 ? '0.0' : ((100 * part) / total).toFixed(1))
|
|
||||||
|
|
||||||
export default props => (
|
|
||||||
<div id='comment-info'>
|
|
||||||
<span className='removed-text'>
|
|
||||||
removed comments: {props.removed}/{props.total} ({getProcent(props.removed, props.total)}%)
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
<span className='deleted-text'>
|
|
||||||
deleted comments: {props.deleted}/{props.total} ({getProcent(props.deleted, props.total)}%)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import Comment from 'components/Comment'
|
|
||||||
import CommentInfo from 'components/CommentInfo'
|
|
||||||
import SortBy from 'components/SortBy'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
import {
|
|
||||||
SORT_TOP, SORT_BOTTOM, SORT_NEW, SORT_OLD,
|
|
||||||
SHOW_ALL, SHOW_REMOVED_DELETED, SHOW_REMOVED, SHOW_DELETED,
|
|
||||||
} from 'state'
|
|
||||||
import {
|
|
||||||
topSort, bottomSort, newSort, oldSort,
|
|
||||||
showRemovedAndDeleted, showRemoved, showDeleted,
|
|
||||||
} from 'utils'
|
|
||||||
|
|
||||||
const arrayToLookup = (commentList, removed, deleted) => {
|
|
||||||
const lookup = {}
|
|
||||||
|
|
||||||
commentList.forEach(comment => {
|
|
||||||
comment.replies = []
|
|
||||||
|
|
||||||
if (removed.includes(comment.id)) {
|
|
||||||
comment.removed = true
|
|
||||||
} else if (deleted.includes(comment.id)) {
|
|
||||||
comment.deleted = true
|
|
||||||
}
|
|
||||||
|
|
||||||
lookup[comment.id] = comment
|
|
||||||
})
|
|
||||||
|
|
||||||
return lookup
|
|
||||||
}
|
|
||||||
|
|
||||||
const unflatten = (comments, root, removed, deleted) => {
|
|
||||||
const lookup = arrayToLookup(comments, removed, deleted)
|
|
||||||
const commentTree = []
|
|
||||||
|
|
||||||
Object.keys(lookup).forEach(commentID => {
|
|
||||||
const comment = lookup[commentID]
|
|
||||||
const parentID = comment.parent_id
|
|
||||||
|
|
||||||
if (parentID === root) {
|
|
||||||
commentTree.push(comment)
|
|
||||||
} else {
|
|
||||||
if (lookup[parentID] === undefined) {
|
|
||||||
console.error('MISSING PARENT ID:', parentID, 'for comment', comment)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lookup[parentID].replies.push(comment)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (lookup[root] !== undefined) {
|
|
||||||
lookup[root].replies = commentTree
|
|
||||||
return [lookup[root]]
|
|
||||||
}
|
|
||||||
|
|
||||||
return commentTree
|
|
||||||
}
|
|
||||||
|
|
||||||
const sortCommentTree = (comments, sortFunction) => {
|
|
||||||
comments.sort(sortFunction)
|
|
||||||
|
|
||||||
comments.forEach(comment => {
|
|
||||||
if (comment.replies.length > 0) {
|
|
||||||
sortCommentTree(comment.replies, sortFunction)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterCommentTree = (comments, filterFunction) => {
|
|
||||||
if (comments.length === 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let hasOkComment = false
|
|
||||||
|
|
||||||
// Reverse for loop since we are removing stuff
|
|
||||||
for (let i = comments.length - 1; i >= 0; i--) {
|
|
||||||
const comment = comments[i]
|
|
||||||
const isRepliesOk = filterCommentTree(comment.replies, filterFunction)
|
|
||||||
const isCommentOk = filterFunction(comment)
|
|
||||||
|
|
||||||
if (!isRepliesOk && !isCommentOk) {
|
|
||||||
comments.splice(i, 1)
|
|
||||||
} else {
|
|
||||||
hasOkComment = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hasOkComment
|
|
||||||
}
|
|
||||||
|
|
||||||
const commentSection = (props) => {
|
|
||||||
const commentTree = unflatten(props.comments, props.root, props.removed, props.deleted)
|
|
||||||
|
|
||||||
if (props.show === SHOW_REMOVED_DELETED) {
|
|
||||||
filterCommentTree(commentTree, showRemovedAndDeleted)
|
|
||||||
} else if (props.show === SHOW_REMOVED) {
|
|
||||||
filterCommentTree(commentTree, showRemoved)
|
|
||||||
} else if (props.show === SHOW_DELETED) {
|
|
||||||
filterCommentTree(commentTree, showDeleted)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.sort === SORT_TOP) {
|
|
||||||
sortCommentTree(commentTree, topSort)
|
|
||||||
} else if (props.sort === SORT_BOTTOM) {
|
|
||||||
sortCommentTree(commentTree, bottomSort)
|
|
||||||
} else if (props.sort === SORT_NEW) {
|
|
||||||
sortCommentTree(commentTree, newSort)
|
|
||||||
} else if (props.sort === SORT_OLD) {
|
|
||||||
sortCommentTree(commentTree, oldSort)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('COMMENT SECTION RENDERED')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<CommentInfo
|
|
||||||
total={props.comments.length}
|
|
||||||
removed={props.removed.length}
|
|
||||||
deleted={props.deleted.length}
|
|
||||||
/>
|
|
||||||
<SortBy />
|
|
||||||
{commentTree.map(comment => (
|
|
||||||
<Comment
|
|
||||||
key={comment.id}
|
|
||||||
{...comment}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
sort: state.commentSection.sort,
|
|
||||||
show: state.commentSection.show,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(commentSection)
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import StatusBox from 'containers/StatusBox'
|
|
||||||
|
|
||||||
export default () => (
|
|
||||||
<header>
|
|
||||||
<div id='header'>
|
|
||||||
<h1>
|
|
||||||
<Link to='/'>Removeddit</Link>
|
|
||||||
</h1>
|
|
||||||
<nav>
|
|
||||||
<Link to='/r/all'>/r/all</Link>
|
|
||||||
<Link to='/about/'>about</Link>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<StatusBox />
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { prettyScore, prettyDate, parse, redditThumbnails, isDeleted } from 'utils'
|
|
||||||
|
|
||||||
export default (props) => {
|
|
||||||
if (!props.title) {
|
|
||||||
return <div />
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = props.url.replace('https://www.reddit.com', '')
|
|
||||||
const userLink = isDeleted(props.author) ? undefined : `https://www.reddit.com/user/${props.author}`
|
|
||||||
|
|
||||||
let thumbnail
|
|
||||||
const thumbnailWidth = props.thumbnail_width ? props.thumbnail_width * 0.5 : 70
|
|
||||||
const thumbnailHeight = props.thumbnail_height ? props.thumbnail_height * 0.5 : 70
|
|
||||||
|
|
||||||
if (redditThumbnails.includes(props.thumbnail)) {
|
|
||||||
thumbnail = <a href={url} className={`thumbnail thumbnail-${props.thumbnail}`} />
|
|
||||||
} else if (props.thumbnail !== '') {
|
|
||||||
thumbnail = (
|
|
||||||
<a href={url}>
|
|
||||||
<img className='thumbnail' src={props.thumbnail} width={thumbnailWidth} height={thumbnailHeight} alt='Thumbnail' />
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`thread ${props.removed && 'removed'}`}>
|
|
||||||
{props.position &&
|
|
||||||
<span className='post-rank'>{props.position}</span>}
|
|
||||||
<div className='thread-score-box'>
|
|
||||||
<div className='vote upvote' />
|
|
||||||
<div className='thread-score'>{prettyScore(props.score)}</div>
|
|
||||||
<div className='vote downvote' />
|
|
||||||
</div>
|
|
||||||
{thumbnail}
|
|
||||||
<div className='thread-content'>
|
|
||||||
<a className='thread-title' href={url}>{props.title}</a>
|
|
||||||
{props.link_flair_text &&
|
|
||||||
<span className='link-flair'>{props.link_flair_text}</span>}
|
|
||||||
<span className='domain'>({props.domain})</span>
|
|
||||||
<div className='thread-info'>
|
|
||||||
submitted <span className='thread-time'>{prettyDate(props.created_utc)}</span> by
|
|
||||||
<a className='thread-author author' href={userLink}>{props.author}</a>
|
|
||||||
to <a className='subreddit-link author' href={`/r/${props.subreddit}`}>/r/{props.subreddit}</a>
|
|
||||||
</div>
|
|
||||||
{props.selftext &&
|
|
||||||
<div className='thread-selftext user-text' dangerouslySetInnerHTML={{ __html: parse(props.selftext) }} />}
|
|
||||||
<div className='total-comments'>
|
|
||||||
<a className='grey-link' href={props.permalink}>
|
|
||||||
<b>{props.num_comments} comments</b>
|
|
||||||
</a>
|
|
||||||
<a className='grey-link' href={`https://www.reddit.com${props.permalink}`}>
|
|
||||||
<b>reddit</b>
|
|
||||||
</a>
|
|
||||||
<a className='grey-link' href={`https://snew.github.io${props.permalink}`}>
|
|
||||||
<b>ceddit</b>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
setCommentSort,
|
|
||||||
setCommentShow,
|
|
||||||
SORT_TOP, SORT_BOTTOM, SORT_NEW, SORT_OLD,
|
|
||||||
SHOW_ALL, SHOW_REMOVED_DELETED, SHOW_REMOVED, SHOW_DELETED,
|
|
||||||
} from 'state'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
|
|
||||||
|
|
||||||
const sortBy = props => (
|
|
||||||
<div id='comment-sort'>
|
|
||||||
sorted by:
|
|
||||||
<span className='space' />
|
|
||||||
<select onChange={e => props.setSort(e.target.value)} defaultValue={props.sort}>
|
|
||||||
<option value={SORT_TOP}>top</option>
|
|
||||||
<option value={SORT_BOTTOM}>bottom</option>
|
|
||||||
<option value={SORT_NEW}>new</option>
|
|
||||||
<option value={SORT_OLD}>old</option>
|
|
||||||
</select>
|
|
||||||
<span className='space' />
|
|
||||||
show:
|
|
||||||
<span className='space' />
|
|
||||||
<select onChange={e => props.setShow(e.target.value)} defaultValue={props.show}>
|
|
||||||
<option value={SHOW_ALL}>All comments</option>
|
|
||||||
<option value={SHOW_REMOVED_DELETED}>Removed and deleted</option>
|
|
||||||
<option value={SHOW_REMOVED}>Removed</option>
|
|
||||||
<option value={SHOW_DELETED}>Deleted</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
sort: state.commentSection.sort,
|
|
||||||
show: state.commentSection.show,
|
|
||||||
})
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
setSort: sortString => dispatch(setCommentSort(sortString)),
|
|
||||||
setShow: showString => dispatch(setCommentShow(showString)),
|
|
||||||
})
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
)(sortBy)
|
|
||||||
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
export default (props) => {
|
|
||||||
let pageination = <React.Fragment />
|
|
||||||
|
|
||||||
for (let i = props.start; i <= props.end; i++) {
|
|
||||||
if (props.currentPage === i) {
|
|
||||||
pageination += <span>{i}</span>
|
|
||||||
} else {
|
|
||||||
pageination += <a href={`${props.url + i}`}>{i}</a>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div id='pagination'>
|
|
||||||
Page:
|
|
||||||
{props.start > 1 &&
|
|
||||||
<React.Fragment>
|
|
||||||
<a href={`${props.url + 1}`}>1</a>
|
|
||||||
<span> ...</span>
|
|
||||||
</React.Fragment>
|
|
||||||
}
|
|
||||||
{pageination}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
export default (props) => {
|
|
||||||
const isSelected = time => props.time === time
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='subreddit-info'>
|
|
||||||
top post of <a href={`https://www.reddit.com/r/${props.subreddit}`}>/r/{props.subreddit}</a> from:
|
|
||||||
{/* <select onchange='Vars.reload(this)'> */}
|
|
||||||
<select>
|
|
||||||
<option value='hour' selected={isSelected('hour')}>past hour</option>
|
|
||||||
<option value='12hour' selected={isSelected('12hour')}>past 12 hours</option>
|
|
||||||
<option value='day' selected={isSelected('day')}>past day</option>
|
|
||||||
<option value='week' selected={isSelected('week')}>past week</option>
|
|
||||||
<option value='month' selected={isSelected('month')}>past month</option>
|
|
||||||
<option value='6month' selected={isSelected('6month')}>past 6 months</option>
|
|
||||||
<option value='year' selected={isSelected('year')}>past year</option>
|
|
||||||
<option value='all' selected={isSelected('all')}>all time</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
export default (props) => (
|
|
||||||
<React.Fragment>
|
|
||||||
<div id='comment-info'>
|
|
||||||
removed comments: {props.removedComments}/${props.totalComments}
|
|
||||||
({ ((100 * props.removedComments) / props.totalComments).toFixed(1) }%)
|
|
||||||
</div>
|
|
||||||
<div id='comment-sort'>sorted by: top</div>
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { connect } from 'react-redux'
|
|
||||||
|
|
||||||
const StatusBox = props => (
|
|
||||||
<div id='status'>
|
|
||||||
{props.text &&
|
|
||||||
<p id='status-text'>{props.text}</p>}
|
|
||||||
{props.image &&
|
|
||||||
<img id='status-image' src={props.image} alt='status' />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
text: state.status.text,
|
|
||||||
image: state.status.image,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(StatusBox)
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import { Provider } from 'react-redux'
|
|
||||||
import { store } from 'state'
|
|
||||||
import App from 'App'
|
|
||||||
|
|
||||||
import '../sass/index.sass'
|
|
||||||
|
|
||||||
ReactDOM.render(
|
|
||||||
<Provider store={store}>
|
|
||||||
<App />
|
|
||||||
</Provider>,
|
|
||||||
document.getElementById('app')
|
|
||||||
)
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
export default props => (
|
|
||||||
<div id='main'>
|
|
||||||
<div id='main-box'>
|
|
||||||
<h2 className='about'>About</h2>
|
|
||||||
<p>
|
|
||||||
Display
|
|
||||||
<b className='removed-text' title='Removed by mods'> removed </b>
|
|
||||||
(by mods) and
|
|
||||||
<b className='deleted-text' title='Deleted by users'> deleted </b>
|
|
||||||
(by users) comments/threads from Reddit.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<b>Usage</b>: Drag this bookmarklet
|
|
||||||
<a className='bookmarklet' href="javascript: document.location = document.URL.replace('reddit.com','removeddit.com');">
|
|
||||||
Removeddit
|
|
||||||
</a>
|
|
||||||
to your bookmark bar and use it to get from reddit to removeddit.
|
|
||||||
<br /><br />
|
|
||||||
Alternatively you can manually replace the <i>reddit</i> in the URL to <i>removeddit</i>.
|
|
||||||
<br />
|
|
||||||
E.g. <a href='/r/TwoXChromosomes/comments/6z1hch/'>https://www.removeddit.com/r/TwoXChromosomes/comments/6z1hch/</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Created by
|
|
||||||
<a href='https://github.com/JubbeArt/'> Jesper Wrang </a> and uses
|
|
||||||
<a href='https://pushshift.io/'> Jason Baumgartner </a> service for getting removed comments.
|
|
||||||
</p>
|
|
||||||
<h2 className='todo'>TODO</h2>
|
|
||||||
<ul>
|
|
||||||
<li>Collapsing comments</li>
|
|
||||||
<li>Maybe for specific users </li>
|
|
||||||
</ul>
|
|
||||||
<h2 className='contact'>Links/Contact</h2>
|
|
||||||
<p style={{ marginBottom: '8px' }}>For feedback and bug reports:</p>
|
|
||||||
<ul>
|
|
||||||
<li>email: removeddit (at) gmail.com</li>
|
|
||||||
<li>reddit: <a href='https://www.reddit.com/user/Jubbeart/'>/u/JubbeArt</a></li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
<a href='https://github.com/JubbeArt/removeddit'>Code on Github.</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
|
|
||||||
// var radioInput = function(name, displayNames, values) {
|
|
||||||
// var inputs = "";
|
|
||||||
|
|
||||||
// for(var i = 0, len = displayNames.length; i < len; i++){
|
|
||||||
// inputs += '<span class="radioButton"'+ (Vars.lookup[name] === values[i] ? ' style="background: #239f2b; color:#fff"':'')+'>';
|
|
||||||
// inputs += '<input type="radio" id="'+name+i+'" name="'+name+'" onchange="CSS.radio(this)"';
|
|
||||||
// inputs += 'value="'+values[i]+'" '+(Vars.lookup[name] === values[i] ? ' checked "':'')+' >';
|
|
||||||
// inputs += '<label for="'+name+i+'">'+displayNames[i]+'</label></span>';
|
|
||||||
// }
|
|
||||||
// return inputs;
|
|
||||||
// };
|
|
||||||
|
|
||||||
// var textInput = function(name){
|
|
||||||
// return '<input type="text" name="'+name+'" id="'+name+'" value="'+Vars.get(name)+'">';
|
|
||||||
// };
|
|
||||||
|
|
||||||
// var label = function(name, display) {
|
|
||||||
// return '<label for="'+name+'">'+display+': </label>';
|
|
||||||
// };
|
|
||||||
|
|
||||||
// var inputRow = function(text, input) {
|
|
||||||
// return '<div class="search-row"><span class="search-left">'+text+'</span><span>'+input+"</span></div>";
|
|
||||||
// };
|
|
||||||
|
|
||||||
// var selectTime = function() {
|
|
||||||
// var values = ["hour", "12hour", "day", "week", "month", "6month", "year", "all"];
|
|
||||||
// var display = ["past hour", "past 12 hours", "past day", "past week", "past month", "past 6 months", "past year", "all time"];
|
|
||||||
// var html = '<select name="time" id="time">';
|
|
||||||
|
|
||||||
// for(var i = 0, len = values.length; i < len; i++) {
|
|
||||||
// html += '<option value="'+values[i]+'"' + ((Vars.get("time") === values[i]) ? " selected" : '')+'>'+display[i]+'</option>';
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return html + "</select>";
|
|
||||||
// };
|
|
||||||
|
|
||||||
// var select = function(name, display, values){
|
|
||||||
// var html = '<select name="'+name+'" id="'+name+'">';
|
|
||||||
|
|
||||||
// for(var i = 0, len = values.length; i < len; i++) {
|
|
||||||
// html += '<option value="'+values[i]+'"' + ((Vars.get(name) === values[i]) ? " selected" : '')+'>'+display[i]+'</option>';
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return html + "</select>";
|
|
||||||
// };
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// createSearch: function(){
|
|
||||||
// var searchBox = document.createElement("div");
|
|
||||||
// searchBox.id = "main-box";
|
|
||||||
// searchBox.className = "search-box";
|
|
||||||
// /*
|
|
||||||
// Comments:
|
|
||||||
// text (body) string
|
|
||||||
// title (title) string
|
|
||||||
// subreddit (subreddit) string
|
|
||||||
// author (author) string
|
|
||||||
// over_18 (over_18) sfw,nsfw,both *
|
|
||||||
// locked (locked) bool *
|
|
||||||
// between (after/before) "date"
|
|
||||||
// sort (sort) asc,desc
|
|
||||||
// */
|
|
||||||
// var html = inputRow('Im looking for: ', radioInput("thread", ["Thread","Comment"], ["true", "false"]));
|
|
||||||
// html += inputRow(label("text", "Text"), textInput("text"));
|
|
||||||
// html += inputRow(label("title","Title"), textInput("title"));
|
|
||||||
// html += inputRow(label("subreddit", "Subreddit"), textInput("subreddit"));
|
|
||||||
// html += inputRow(label("author", "Author"), textInput("author"));
|
|
||||||
// html += inputRow("Over 18: ", radioInput("over_18", ["Both","NSFW","SFW"], ["both","nsfw","sfw"]));
|
|
||||||
// html += inputRow("Locked: ", radioInput("locked", ["Both","True", "False"], ["both","true","false"]));
|
|
||||||
// html += inputRow("Removed: ", select("removed", ["Removed and non-removed", "Only removed", "Only deleted"], ["all", "removed", "deleted"]));
|
|
||||||
// html += inputRow("From:", selectTime());
|
|
||||||
// html += inputRow("Sort: ", select("sort", ["Highest score","Lowest score","Newest","Oldest"], ["score_desc","score_asc","time_desc","time_asc"]));
|
|
||||||
// html += '<input type="button" value="'+(showAdvanced?'Hide':'Show')+' advanced">';
|
|
||||||
// html += '<input type="submit" value="Search">';
|
|
||||||
// searchBox.innerHTML = html;
|
|
||||||
|
|
||||||
// mainDiv.appendChild(searchBox);
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { getRemovedThreadIDs } from 'api/removeddit'
|
|
||||||
import { getThreads } from 'api/reddit'
|
|
||||||
import Post from 'components/Post'
|
|
||||||
|
|
||||||
const displaySubreddit = props => {
|
|
||||||
const subreddit = props.match.params.subreddit
|
|
||||||
if (subreddit === undefined) {
|
|
||||||
return 'all'
|
|
||||||
}
|
|
||||||
return subreddit
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSubredditForAPI = props => {
|
|
||||||
const subreddit = props.match.params.subreddit
|
|
||||||
if (subreddit === undefined) {
|
|
||||||
return ''
|
|
||||||
} else if (subreddit.toLowerCase() === 'all') {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
return subreddit.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default class Subreddit extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
threads: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const subreddit = getSubredditForAPI(this.props)
|
|
||||||
getRemovedThreadIDs(subreddit)
|
|
||||||
.then(threadIDs => getThreads(threadIDs))
|
|
||||||
.then(threads => {
|
|
||||||
threads.forEach(thread => {
|
|
||||||
thread.removed = true
|
|
||||||
thread.selftext = ''
|
|
||||||
})
|
|
||||||
this.setState({ threads })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const subreddit = `/r/${displaySubreddit(this.props)}`
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
<div className='subreddit-box'>
|
|
||||||
<a href={subreddit} className='subreddit-title'>{subreddit}</a>
|
|
||||||
<span className='space' />
|
|
||||||
<a href={`https://www.reddit.com${subreddit}`} className='subreddit-title-link'>reddit</a>
|
|
||||||
<span className='space' />
|
|
||||||
<a href={`https://snew.github.io${subreddit}`} className='subreddit-title-link'>ceddit</a>
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
this.state.threads.map(thread => (
|
|
||||||
<Post key={thread.id} {...thread} />
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</React.Fragment>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import Post from 'components/Post'
|
|
||||||
import CommentSection from 'components/CommentSection'
|
|
||||||
import {
|
|
||||||
getPost,
|
|
||||||
getComments as getRedditComments,
|
|
||||||
} from 'api/reddit'
|
|
||||||
import {
|
|
||||||
getPost as getRemovedPost,
|
|
||||||
getComments as getPushshiftComments,
|
|
||||||
} from 'api/pushshift'
|
|
||||||
import { isDeleted, isRemoved } from 'utils'
|
|
||||||
|
|
||||||
export default class Thread extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props)
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
post: {},
|
|
||||||
pushshiftComments: [],
|
|
||||||
redditComments: [],
|
|
||||||
removed: [],
|
|
||||||
deleted: [],
|
|
||||||
loadingComments: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { subreddit, threadID } = this.props.match.params
|
|
||||||
console.timeEnd('scripts loaded')
|
|
||||||
|
|
||||||
Promise.all([
|
|
||||||
// Get thread from reddit
|
|
||||||
getPost(subreddit, threadID)
|
|
||||||
.then(post => {
|
|
||||||
this.setState({ post })
|
|
||||||
// Fetch the thread from pushshift if it was deleted/removed
|
|
||||||
if (isDeleted(post.selftext)) {
|
|
||||||
getRemovedPost(threadID)
|
|
||||||
.then(removedPost => {
|
|
||||||
removedPost.removed = true
|
|
||||||
this.setState({ post: removedPost })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
// Get comment ids from pushshift
|
|
||||||
getPushshiftComments(threadID),
|
|
||||||
])
|
|
||||||
.then(results => {
|
|
||||||
const pushshiftComments = results[1]
|
|
||||||
|
|
||||||
|
|
||||||
// Extract ids from pushshift response
|
|
||||||
const ids = pushshiftComments.map(comment => comment.id)
|
|
||||||
console.log('pushshift:', ids.length)
|
|
||||||
|
|
||||||
// Get all the comments from reddit
|
|
||||||
return (
|
|
||||||
getRedditComments(ids)
|
|
||||||
.then(redditComments => {
|
|
||||||
pushshiftComments.forEach(comment => {
|
|
||||||
// Replace pushshift score with reddit (its usually more accurate)
|
|
||||||
const redditComment = redditComments.find(rComment => rComment.id === comment.id)
|
|
||||||
if (redditComment !== undefined) {
|
|
||||||
comment.score = redditComment.score
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({ pushshiftComments })
|
|
||||||
return redditComments
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.then(redditComments => {
|
|
||||||
console.log('reddit:', redditComments.length)
|
|
||||||
const removed = []
|
|
||||||
const deleted = []
|
|
||||||
|
|
||||||
// Check what as removed / deleted according to reddit
|
|
||||||
redditComments.forEach(comment => {
|
|
||||||
if (isRemoved(comment.body)) {
|
|
||||||
removed.push(comment.id)
|
|
||||||
} else if (isDeleted(comment.body)) {
|
|
||||||
deleted.push(comment.id)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
removed,
|
|
||||||
deleted,
|
|
||||||
redditComments,
|
|
||||||
loadingComments: false,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
let root = this.state.post.id
|
|
||||||
|
|
||||||
if (this.props.match.params.commentID !== undefined) {
|
|
||||||
root = this.props.match.params.commentID
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Post {...this.state.post} />
|
|
||||||
{
|
|
||||||
!this.state.loadingComments &&
|
|
||||||
<CommentSection
|
|
||||||
root={root}
|
|
||||||
comments={this.state.pushshiftComments}
|
|
||||||
removed={this.state.removed}
|
|
||||||
deleted={this.state.deleted}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
export { default as About } from './About'
|
|
||||||
export { default as Subreddit } from './Subreddit'
|
|
||||||
export { default as Thread } from './Thread'
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
import { get, put } from 'utils'
|
|
||||||
|
|
||||||
// Sort types
|
|
||||||
export const SORT_TOP = 'SORT_TOP'
|
|
||||||
export const SORT_BOTTOM = 'SORT_BOTTOM'
|
|
||||||
export const SORT_NEW = 'SORT_NEW'
|
|
||||||
export const SORT_OLD = 'SORT_OLD'
|
|
||||||
|
|
||||||
// Filter types
|
|
||||||
export const SHOW_ALL = 'SHOW_ALL'
|
|
||||||
export const SHOW_REMOVED_DELETED = 'SHOW_REMOVED_DELETED'
|
|
||||||
export const SHOW_REMOVED = 'SHOW_REMOVED'
|
|
||||||
export const SHOW_DELETED = 'SHOW_DELETED'
|
|
||||||
|
|
||||||
// Action types
|
|
||||||
export const COMMENT_SORT = 'COMMENT_SORT'
|
|
||||||
export const COMMENT_SHOW = 'COMMENT_SHOW'
|
|
||||||
|
|
||||||
// Action creators
|
|
||||||
export const setCommentSort = payload => ({ type: COMMENT_SORT, payload })
|
|
||||||
export const setCommentShow = payload => ({ type: COMMENT_SHOW, payload })
|
|
||||||
|
|
||||||
const sortKey = 'commentSort'
|
|
||||||
const filterKey = 'commentFilter'
|
|
||||||
|
|
||||||
// Init state
|
|
||||||
const initialStatusState = {
|
|
||||||
sort: get(sortKey, SORT_TOP),
|
|
||||||
show: get(filterKey, SHOW_REMOVED_DELETED),
|
|
||||||
}
|
|
||||||
|
|
||||||
export const commentSectionReducer = (state = initialStatusState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case COMMENT_SORT:
|
|
||||||
put(sortKey, action.payload)
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
sort: action.payload,
|
|
||||||
}
|
|
||||||
case COMMENT_SHOW:
|
|
||||||
put(filterKey, action.payload)
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
show: action.payload,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import { createStore, combineReducers, applyMiddleware } from 'redux'
|
|
||||||
import logger from 'redux-logger'
|
|
||||||
import { statusReducer } from './status'
|
|
||||||
import { commentSectionReducer } from './commentSection'
|
|
||||||
|
|
||||||
export { setStatusLoading, setStatusSuccess, setStatusError } from './status'
|
|
||||||
export * from './commentSection'
|
|
||||||
|
|
||||||
const reducer = combineReducers({
|
|
||||||
status: statusReducer,
|
|
||||||
commentSection: commentSectionReducer,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const store = createStore(reducer, applyMiddleware(logger))
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
const images = {
|
|
||||||
loading: '/images/loading.gif',
|
|
||||||
error: '/images/error.png',
|
|
||||||
success: '/images/done.png',
|
|
||||||
}
|
|
||||||
|
|
||||||
// Action types
|
|
||||||
const STATUS_SET_LOADING = 'STATUS_SET_LOADING'
|
|
||||||
const STATUS_SET_SUCCESS = 'STATUS_SET_SUCCESS'
|
|
||||||
const STATUS_SET_ERROR = 'STATUS_SET_ERROR'
|
|
||||||
|
|
||||||
// Action creators
|
|
||||||
export const setStatusLoading = (payload = '') => ({ type: STATUS_SET_LOADING, payload })
|
|
||||||
export const setStatusSuccess = (payload = '') => ({ type: STATUS_SET_SUCCESS, payload })
|
|
||||||
export const setStatusError = (payload = '') => ({ type: STATUS_SET_ERROR, payload })
|
|
||||||
|
|
||||||
// Init state
|
|
||||||
const initialStatusState = {
|
|
||||||
text: null,
|
|
||||||
image: null,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const statusReducer = (state = initialStatusState, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case STATUS_SET_SUCCESS:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
text: action.payload,
|
|
||||||
image: images.success,
|
|
||||||
}
|
|
||||||
case STATUS_SET_LOADING:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
text: action.payload,
|
|
||||||
image: images.loading,
|
|
||||||
}
|
|
||||||
case STATUS_SET_ERROR:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
text: action.payload,
|
|
||||||
image: images.error,
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
import SnuOwnd from 'snuownd'
|
|
||||||
|
|
||||||
const markdown = SnuOwnd.getParser()
|
|
||||||
|
|
||||||
// Flatten arrays one level
|
|
||||||
export const flatten = arr => arr.reduce(
|
|
||||||
(accumulator, value) => accumulator.concat(value),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Same as: array => Set => array
|
|
||||||
export const unique = arr => arr.filter((value, index) => arr.indexOf(value) === index)
|
|
||||||
|
|
||||||
// Everything in arr1 that is not in arr2
|
|
||||||
export const difference = (arr1, arr2) => arr1.filter(x => !arr2.includes(x))
|
|
||||||
|
|
||||||
// Take on big array and split it into an array of chunks with correct size
|
|
||||||
export const chunk = (arr, size) => {
|
|
||||||
const chunks = []
|
|
||||||
for (let i = 0; i < arr.length; i += size) {
|
|
||||||
chunks.push(arr.slice(i, i + size))
|
|
||||||
}
|
|
||||||
return chunks
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON parsing for fetch
|
|
||||||
export const json = x => x.json()
|
|
||||||
|
|
||||||
// Make multiple requests to the same url, with an array of data (usually comment IDs)
|
|
||||||
// This is needed since there is a limit on how long a url can be
|
|
||||||
export const fetchMultiple = (url, arr, header, size = 100) => {
|
|
||||||
const subArrays = chunk(arr, size)
|
|
||||||
|
|
||||||
return Promise.all(subArrays.map(subArr => fetch(url + subArr.join(), header)))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const jsonMultiple = responses => Promise.all(responses.map(json))
|
|
||||||
|
|
||||||
// Change bases
|
|
||||||
export const toBase36 = number => parseInt(number, 10).toString(36)
|
|
||||||
export const toBase10 = numberString => parseInt(numberString, 36)
|
|
||||||
|
|
||||||
// Reddits way of indicating that something is deleted
|
|
||||||
export const isDeleted = textBody => textBody === '[deleted]'
|
|
||||||
|
|
||||||
// Reddits way of indicating that something is deleted
|
|
||||||
export const isRemoved = textBody => textBody === '[removed]'
|
|
||||||
|
|
||||||
// Default thumbnails for reddit threads
|
|
||||||
export const redditThumbnails = ['self', 'default', 'image', 'nsfw']
|
|
||||||
|
|
||||||
// Parse comments
|
|
||||||
export const parse = text => markdown.render(text)
|
|
||||||
|
|
||||||
// UTC to "Reddit time format" (e.g. 5 hours ago, just now, etc...)
|
|
||||||
export const prettyDate = createdUTC => {
|
|
||||||
const currentUTC = Math.floor((new Date()).getTime() / 1000)
|
|
||||||
const secondDiff = currentUTC - createdUTC
|
|
||||||
const dayDiff = Math.floor(secondDiff / 86400)
|
|
||||||
|
|
||||||
if (dayDiff < 0) return ''
|
|
||||||
if (dayDiff === 0) {
|
|
||||||
if (secondDiff < 10) return 'just now'
|
|
||||||
if (secondDiff < 60) return `${secondDiff} seconds ago`
|
|
||||||
if (secondDiff < 120) return 'a minute ago'
|
|
||||||
if (secondDiff < 3600) return `${Math.floor(secondDiff / 60)} minutes ago`
|
|
||||||
if (secondDiff < 7200) return 'an hour ago'
|
|
||||||
if (secondDiff < 86400) return `${Math.floor(secondDiff / 3600)} hours ago`
|
|
||||||
}
|
|
||||||
if (dayDiff < 7) return `${dayDiff} days ago`
|
|
||||||
if (dayDiff < 31) return `${Math.floor(dayDiff / 7)} weeks ago`
|
|
||||||
if (dayDiff < 365) return `${Math.floor(dayDiff / 30)} months ago`
|
|
||||||
return `${Math.floor(dayDiff / 365)} years ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reddit format for scores, e.g. 12000 => 12k
|
|
||||||
export const prettyScore = score => {
|
|
||||||
if (score >= 100000) {
|
|
||||||
return `${(score / 1000).toFixed(0)}k`
|
|
||||||
} else if (score >= 10000) {
|
|
||||||
return `${(score / 1000).toFixed(1)}k`
|
|
||||||
}
|
|
||||||
|
|
||||||
return score
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve, store and delete stuff in the local storage
|
|
||||||
export const get = (key, defaultValue) => (
|
|
||||||
localStorage.getItem(key) !== null ? JSON.parse(localStorage.getItem(key)) : defaultValue
|
|
||||||
)
|
|
||||||
|
|
||||||
export const put = (key, value) => localStorage.setItem(key, JSON.stringify(value))
|
|
||||||
|
|
||||||
|
|
||||||
// Sorting for comments
|
|
||||||
export const topSort = (commentA, commentB) => {
|
|
||||||
if (commentA.score > commentB.score) return -1
|
|
||||||
if (commentA.score < commentB.score) return 1
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
export const bottomSort = (commentA, commentB) => {
|
|
||||||
if (commentA.score < commentB.score) return -1
|
|
||||||
if (commentA.score > commentB.score) return 1
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
export const newSort = (commentA, commentB) => {
|
|
||||||
if (commentA.created_utc > commentB.created_utc) return -1
|
|
||||||
if (commentA.created_utc < commentB.created_utc) return 1
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
export const oldSort = (commentA, commentB) => {
|
|
||||||
if (commentA.created_utc < commentB.created_utc) return -1
|
|
||||||
if (commentA.created_utc > commentB.created_utc) return 1
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter comments
|
|
||||||
export const showRemoved = comment => comment.removed === true
|
|
||||||
export const showDeleted = comment => comment.deleted === true
|
|
||||||
export const showRemovedAndDeleted = comment => comment.removed === true || comment.deleted === true
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
#main-box
|
|
||||||
margin: 0 auto
|
|
||||||
padding: 20px
|
|
||||||
border-radius: 5px
|
|
||||||
color: #fff
|
|
||||||
word-wrap: break-word
|
|
||||||
max-width: 800px
|
|
||||||
background-color: #161616
|
|
||||||
|
|
||||||
h2
|
|
||||||
margin: 20px 0px 12px
|
|
||||||
|
|
||||||
h2.about
|
|
||||||
margin: 0
|
|
||||||
color: $red
|
|
||||||
h2.todo
|
|
||||||
color: #239f2b
|
|
||||||
h2.contact
|
|
||||||
color: #1b767a
|
|
||||||
|
|
||||||
ul
|
|
||||||
padding-left: 30px
|
|
||||||
margin: 0
|
|
||||||
|
|
||||||
.bookmarklet
|
|
||||||
background: $red
|
|
||||||
color: #fff
|
|
||||||
padding: 9px
|
|
||||||
font-size: large
|
|
||||||
font-weight: bold
|
|
||||||
display: inline-block
|
|
||||||
border-radius: 5px
|
|
||||||
margin: 0 7px
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
$background: #262626
|
|
||||||
$red: #c70300
|
|
||||||
$blue: #00007d
|
|
||||||
$link: #8cb3d9
|
|
||||||
$author: #6a98af
|
|
||||||
$removed: #840c09
|
|
||||||
$deleted: #00007d
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
.comment
|
|
||||||
margin: 0 0 8px 0
|
|
||||||
padding: 5px 8px 5px 30px
|
|
||||||
border: 1px solid #333
|
|
||||||
border-radius: 3px
|
|
||||||
color: #ddd
|
|
||||||
font-size: 14px
|
|
||||||
|
|
||||||
.comment-head
|
|
||||||
font-size: 10px
|
|
||||||
|
|
||||||
.comment-author
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
.comment-author:not([href])
|
|
||||||
color: #828282
|
|
||||||
|
|
||||||
.comment-score
|
|
||||||
color: #b4b4b4
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
.comment-time
|
|
||||||
color: #828282
|
|
||||||
|
|
||||||
.comment-body
|
|
||||||
max-width: 840px
|
|
||||||
line-height: 20px
|
|
||||||
|
|
||||||
p
|
|
||||||
margin: 5px 0
|
|
||||||
line-height: 20px
|
|
||||||
|
|
||||||
a:hover
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
.comment-links
|
|
||||||
margin-bottom: 6px
|
|
||||||
|
|
||||||
.comment-links a
|
|
||||||
color: #828282
|
|
||||||
font-weight: bold
|
|
||||||
font-size: 10px
|
|
||||||
margin-right: 4px
|
|
||||||
|
|
||||||
.comment-odd
|
|
||||||
background-color: #121212
|
|
||||||
|
|
||||||
.comment-even
|
|
||||||
background-color: #161616
|
|
||||||
|
|
||||||
#comment-info
|
|
||||||
margin-top: 13px
|
|
||||||
font-weight: bold
|
|
||||||
padding-bottom: 5px
|
|
||||||
border-bottom: 1px dotted #808080
|
|
||||||
font-size: 16px
|
|
||||||
|
|
||||||
#sort-by
|
|
||||||
color: #828282
|
|
||||||
font-size: 12px
|
|
||||||
margin: 5px 0 10px
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
html, body
|
|
||||||
margin: 0
|
|
||||||
padding: 0
|
|
||||||
background-color: $background
|
|
||||||
font-family: verdana, arial, helvetica, sans-serif
|
|
||||||
|
|
||||||
.main
|
|
||||||
margin: 15px
|
|
||||||
|
|
||||||
.removed
|
|
||||||
background-color: $removed
|
|
||||||
|
|
||||||
.deleted
|
|
||||||
background-color: $deleted
|
|
||||||
|
|
||||||
.removed-text
|
|
||||||
color: $red
|
|
||||||
|
|
||||||
.deleted-text
|
|
||||||
color: $blue
|
|
||||||
|
|
||||||
a
|
|
||||||
color: $link
|
|
||||||
|
|
||||||
a:link, a:visited, a:active
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
a:hover
|
|
||||||
text-decoration: underline
|
|
||||||
|
|
||||||
.author
|
|
||||||
color: $author
|
|
||||||
|
|
||||||
.author:not([href])
|
|
||||||
color: inherit
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
.space
|
|
||||||
margin-left: 5px
|
|
||||||
|
|
||||||
select
|
|
||||||
background: transparent
|
|
||||||
border: 0
|
|
||||||
color: #828282
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
header
|
|
||||||
background-color: $red///*#19171c;/*#181818*/
|
|
||||||
display: flex
|
|
||||||
justify-content: space-between
|
|
||||||
align-items: center
|
|
||||||
padding: 10px
|
|
||||||
|
|
||||||
#header
|
|
||||||
a
|
|
||||||
color: #fff
|
|
||||||
|
|
||||||
h1
|
|
||||||
margin: 7px 0
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
a
|
|
||||||
transition: color 0.3s
|
|
||||||
|
|
||||||
|
|
||||||
a:hover
|
|
||||||
text-decoration: none
|
|
||||||
color: #ff8b88
|
|
||||||
nav
|
|
||||||
a
|
|
||||||
margin: 0 7px
|
|
||||||
font-size: 20px
|
|
||||||
|
|
||||||
#status
|
|
||||||
display: flex
|
|
||||||
justify-content: space-between
|
|
||||||
align-items: center
|
|
||||||
|
|
||||||
#status-text
|
|
||||||
color: #fff
|
|
||||||
margin: 0 20px 0 0
|
|
||||||
|
|
||||||
#status-image
|
|
||||||
height: 64px
|
|
||||||
width: 64px
|
|
||||||
border-radius: 32px
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
@import colors
|
|
||||||
@import common
|
|
||||||
@import header
|
|
||||||
@import about
|
|
||||||
@import comment
|
|
||||||
@import post
|
|
||||||
@import thread
|
|
||||||
@import subreddit
|
|
||||||
@import search
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
#comment-info
|
|
||||||
color: #ca302c
|
|
||||||
margin-top: 10px
|
|
||||||
font-weight: bold
|
|
||||||
padding-bottom: 5px
|
|
||||||
border-bottom: 1px dotted #808080
|
|
||||||
font-size: 16px
|
|
||||||
|
|
||||||
|
|
||||||
#comment-sort
|
|
||||||
color: #808080
|
|
||||||
font-size: 12px
|
|
||||||
margin: 5px 0 10px
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
.search-box
|
|
||||||
font-size: 16px
|
|
||||||
|
|
||||||
.search-row
|
|
||||||
padding: 10px 6px 11px
|
|
||||||
border-bottom: 1px dotted #ccc
|
|
||||||
min-height: 26px
|
|
||||||
|
|
||||||
.search-left
|
|
||||||
font-weight: bold
|
|
||||||
display: inline-block
|
|
||||||
width: 190px
|
|
||||||
vertical-align: middle
|
|
||||||
|
|
||||||
input[type="text"]
|
|
||||||
font-size: 17px
|
|
||||||
width: 400px
|
|
||||||
border-radius: 2px
|
|
||||||
padding-left: 3px
|
|
||||||
|
|
||||||
.radioButton
|
|
||||||
background: #8a8888
|
|
||||||
color: black
|
|
||||||
border-radius: 3px
|
|
||||||
display: inline-block
|
|
||||||
margin-right: 9px
|
|
||||||
transition: all 0.3s
|
|
||||||
|
|
||||||
label
|
|
||||||
padding: 7px 10px 7px 3px
|
|
||||||
display: inline-block
|
|
||||||
|
|
||||||
input
|
|
||||||
margin: 0 0 0 5px
|
|
||||||
height: 35px
|
|
||||||
display: inline-block
|
|
||||||
vertical-align: top
|
|
||||||
|
|
||||||
select
|
|
||||||
padding: 3px
|
|
||||||
font-size: 15px
|
|
||||||
border-radius: 2px
|
|
||||||
|
|
||||||
input[type="submit"]
|
|
||||||
margin: 16px auto 0px
|
|
||||||
display: block
|
|
||||||
padding: 9px 16px
|
|
||||||
font-size: 18px
|
|
||||||
background: #239f2b
|
|
||||||
border: 0
|
|
||||||
border-radius: 2px
|
|
||||||
color: white
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
.post-rank
|
|
||||||
min-width: 20px
|
|
||||||
color: #505050
|
|
||||||
font-size: 16px
|
|
||||||
text-align: right
|
|
||||||
margin: 14px 4px 0 0
|
|
||||||
|
|
||||||
.subreddit-info
|
|
||||||
border-bottom: 1px dotted gray
|
|
||||||
padding: 5px 10px
|
|
||||||
color: #ccc
|
|
||||||
font-size: 14px
|
|
||||||
margin-bottom: 5px
|
|
||||||
|
|
||||||
select
|
|
||||||
background: #262626
|
|
||||||
color: #808080
|
|
||||||
font-size: 14px
|
|
||||||
font-weight: bold
|
|
||||||
text-decoration: underline
|
|
||||||
border: 0
|
|
||||||
|
|
||||||
#pagination
|
|
||||||
border-top: 1px dotted gray
|
|
||||||
padding: 5px 10px
|
|
||||||
color: #ccc
|
|
||||||
font-size: 16px
|
|
||||||
span, a
|
|
||||||
margin: 5px
|
|
||||||
|
|
||||||
.subreddit-box
|
|
||||||
margin-bottom: 15px
|
|
||||||
|
|
||||||
.subreddit-title
|
|
||||||
margin-left: 5px
|
|
||||||
font-size: 22px
|
|
||||||
font-weight: bold
|
|
||||||
color: white
|
|
||||||
|
|
||||||
.subreddit-title-link
|
|
||||||
margin-bottom: 15px
|
|
||||||
margin-left: 5px
|
|
||||||
font-size: 14px
|
|
||||||
color: white
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
.thread
|
|
||||||
display: flex
|
|
||||||
flex-grow: 1
|
|
||||||
margin-bottom: 7px
|
|
||||||
border-radius: 1px
|
|
||||||
padding-top: 6px
|
|
||||||
|
|
||||||
.thumbnail
|
|
||||||
width: 70px
|
|
||||||
margin-right: 3px
|
|
||||||
margin-top: -2px
|
|
||||||
|
|
||||||
img
|
|
||||||
border-radius: 1px
|
|
||||||
|
|
||||||
.thumbnail-default
|
|
||||||
background-image: url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png")
|
|
||||||
background-position: 0px -1099px
|
|
||||||
background-repeat: no-repeat
|
|
||||||
height: 50px
|
|
||||||
|
|
||||||
.thumbnail-self
|
|
||||||
background-image: url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png")
|
|
||||||
background-position: 0px -1267px
|
|
||||||
background-repeat: no-repeat
|
|
||||||
height: 50px
|
|
||||||
|
|
||||||
.thumbnail-image
|
|
||||||
background-image: url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png")
|
|
||||||
background-position: 0px -1043px
|
|
||||||
background-repeat: no-repeat
|
|
||||||
height: 50px
|
|
||||||
|
|
||||||
.thumbnail-nsfw
|
|
||||||
background-image: url("https://www.redditstatic.com/sprite-reddit.6Om8v6KMv28.png")
|
|
||||||
background-position: 0px -1155px
|
|
||||||
background-repeat: no-repeat
|
|
||||||
height: 50px
|
|
||||||
.thread-score-box
|
|
||||||
margin: 0 7px
|
|
||||||
width: 43px
|
|
||||||
|
|
||||||
.vote
|
|
||||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC0AAAAcCAMAAADC4sagAAABFFBMVEUjJCQiIiIjIyMkIiIkJCQlJSUlJicmJiYnIyEnJycnKCsoKCgpLC8rIyErKyssMDUvLy8wJCAwMDAxMTEyMjI0NDQ2NjY3JR83Nzc5OTk7Ozs8PDw9PT0+Pj4/Pz9AQEBBQUFCQkJDQ0NGRkZISEhKSkpKV2tLS0tMTExOTk5PT09QYHdSUlJTU1NVVVVWVlZXV1dXaINYWFhaWlpabYlbW1tecpBgYGBhYWFid5dkZGRle5xlfJ1pgKRrhKhshKlshapuLhZwirFyjbV0j7h4lL97mcZ9MBR/nsyCo9OOMxGcNQ+eNg+kNw6sOA2vOAy0OQy7Ogq/OwrBOwrDPAnLPQjPPQfTPgfbPwXjQQTrQgPzQwLKxGgxAAAAAXRSTlMAQObYZgAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlJREFUOMu9lF1TwjAQRTcMvmeQOi1QPmxFBQTxW1ERUUFEUVRU/P//w9yGmBTFyZPnpXNmbju76W6IJM8Uw49rLa6Tz4mpq/mCqY3Gvqnj6XQ61lp0mGd8favCwx2tow8wUlpwGWOZrNJKjXO+UVc6fJcMZzV7IswSOU/qej0l0rwyq33wphhAM1kWkcy70GB7mUfUNqG9V02PyPMTMs2Wig5RaTctwzxVXyPqvJh0VkJBIKJ4huncCRAvHOEZyGKegO4a6UDrIdLGCT4C6/QDsE7fA+v0HdAadWqk0SH9D3sgXkm4uJIDYKTFnwwXd3kMrNOnwDp9BmzS5QuTshN1iDRwfkxV9VJTJXLzSWYM+NzEEjWvFE2oGvCE7/2yDeL8riWzM8xmorRazLlNI2rdgNb3ZYLF1Es/t8VE7X6/39Yqlt4tLLwhiLq3XVNLfumP24fo3Opm+wLGCVa252Y8tQAAAABJRU5ErkJggg==)
|
|
||||||
background-repeat: no-repeat
|
|
||||||
margin-left: auto
|
|
||||||
margin-right: auto
|
|
||||||
width: 15px
|
|
||||||
height: 14px
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
.upvote
|
|
||||||
background-position: -15px 0px
|
|
||||||
margin-top: 2px
|
|
||||||
|
|
||||||
.downvote
|
|
||||||
background-position: -15px -14px
|
|
||||||
margin-top: 2px
|
|
||||||
|
|
||||||
.thread-score
|
|
||||||
color: #646464
|
|
||||||
font-size: 13px
|
|
||||||
font-weight: bold
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
.thread-content
|
|
||||||
flex: 1
|
|
||||||
float: left
|
|
||||||
margin-left: 3px
|
|
||||||
|
|
||||||
.link-flair
|
|
||||||
display: inline-block
|
|
||||||
color: #c8c8c8
|
|
||||||
background-color: #404040
|
|
||||||
margin-right: 5px
|
|
||||||
border: 1px solid #4d4d4d
|
|
||||||
border-radius: 2px
|
|
||||||
padding: 0 2px
|
|
||||||
font-size: 10px
|
|
||||||
|
|
||||||
.thread-title
|
|
||||||
color: #a6a6a6
|
|
||||||
margin-right: 5px
|
|
||||||
|
|
||||||
.thread-title:hover
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
.domain
|
|
||||||
color: #888
|
|
||||||
font-size: 10px
|
|
||||||
|
|
||||||
.thread-selftext
|
|
||||||
border: 1px solid #666
|
|
||||||
border-radius: 7px
|
|
||||||
margin: 5px 0 7px
|
|
||||||
padding: 5px 10px
|
|
||||||
max-width: 840px
|
|
||||||
color: #ddd
|
|
||||||
font-size: 14px
|
|
||||||
|
|
||||||
p
|
|
||||||
margin: 5px 0
|
|
||||||
line-height: 20px
|
|
||||||
|
|
||||||
a:hover
|
|
||||||
text-decoration: none
|
|
||||||
|
|
||||||
p:first-child
|
|
||||||
margin-top: 0px
|
|
||||||
|
|
||||||
|
|
||||||
p:last-child
|
|
||||||
margin-bottom: 0px
|
|
||||||
|
|
||||||
|
|
||||||
.thread-image
|
|
||||||
max-width: 768px
|
|
||||||
max-height: 768px
|
|
||||||
|
|
||||||
.thread-info
|
|
||||||
color: #828282
|
|
||||||
font-size: 10px
|
|
||||||
margin-top: 2px
|
|
||||||
|
|
||||||
|
|
||||||
.total-comments
|
|
||||||
color: #828282
|
|
||||||
font-size: 10px
|
|
||||||
margin: 4px 0
|
|
||||||
|
|
||||||
|
|
||||||
.grey-link, .grey-link:link, .grey-link:visited, .grey-link:hover, .grey-link:focus, .grey-link:active
|
|
||||||
color: #828282
|
|
||||||
margin-right: 4px
|
|
||||||
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
|
||||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
|
||||||
|
|
||||||
const extractSass = new ExtractTextPlugin({
|
|
||||||
filename: 'bundle.[contenthash].css',
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: [
|
|
||||||
'@babel/polyfill',
|
|
||||||
'whatwg-fetch',
|
|
||||||
'./src/js/index.js',
|
|
||||||
],
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, 'dist'),
|
|
||||||
filename: 'bundle.[chunkhash].js',
|
|
||||||
publicPath: '/',
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
modules: [
|
|
||||||
path.resolve('./src/js'),
|
|
||||||
path.resolve('./node_modules'),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
module: {
|
|
||||||
loaders: [
|
|
||||||
{
|
|
||||||
test: /\.js$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
cacheDirectory: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.html$/,
|
|
||||||
loader: 'html-loader',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
test: /\.sass$/,
|
|
||||||
loader: extractSass.extract({
|
|
||||||
use: ['css-loader', 'sass-loader'],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new CleanWebpackPlugin(['dist']),
|
|
||||||
extractSass,
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
template: 'src/index.html',
|
|
||||||
}),
|
|
||||||
new CopyWebpackPlugin([
|
|
||||||
{
|
|
||||||
from: 'src/404.html',
|
|
||||||
to: '404.html',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
from: 'src/images',
|
|
||||||
to: 'images/',
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
stats: {
|
|
||||||
colors: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
const webpack = require('webpack');
|
|
||||||
const merge = require('webpack-merge');
|
|
||||||
const common = require('./webpack.common.js');
|
|
||||||
|
|
||||||
module.exports = merge(common, {
|
|
||||||
devServer: {
|
|
||||||
historyApiFallback: true,
|
|
||||||
},
|
|
||||||
devtool: 'cheap-module-eval-source-map',
|
|
||||||
});
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
const webpack = require('webpack');
|
|
||||||
const merge = require('webpack-merge');
|
|
||||||
const common = require('./webpack.common.js');
|
|
||||||
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
|
|
||||||
|
|
||||||
module.exports = merge(common, {
|
|
||||||
plugins: [
|
|
||||||
new UglifyJSPlugin(),
|
|
||||||
new webpack.DefinePlugin({
|
|
||||||
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||