+5
-2
package.json
+5
-2
package.json
···
7
7
"pug-cli": "^1.0.0-alpha6"
8
8
},
9
9
"peerDependencies": {
10
-
"typescript": "^5.0.0"
10
+
"typescript": "^5.6.3"
11
11
},
12
12
"dependencies": {
13
-
"express": "^4.19.2",
13
+
"cookie-parser": "^1.4.7",
14
+
"express": "^4.21.1",
15
+
"express-rate-limit": "^7.4.1",
14
16
"he": "^1.2.0",
17
+
"jsonwebtoken": "^9.0.2",
15
18
"pug": "^3.0.3",
16
19
"timeago.js": "^4.0.2"
17
20
}
+1
-1
readme.txt
+1
-1
readme.txt
···
13
13
- [x] fix spacing between comments
14
14
- [x] collapse even singular comments
15
15
- [ ] highlights for op, sticky etc.
16
-
- [ ] support 'more comments'
16
+
- [x] support 'more comments'
17
17
- [ ] avoid js to toggle details in views/index.pug
18
18
- [x] set home to sum of subs
19
19
- [x] details tag on safari
+27
src/auth.js
+27
src/auth.js
···
1
+
const jwt = require("jsonwebtoken");
2
+
const { JWT_KEY } = require("./");
3
+
4
+
function authenticateToken(req, res, next) {
5
+
if (!req.cookies || !req.cookies.auth_token) {
6
+
return res.redirect("/login");
7
+
}
8
+
9
+
const token = req.cookies.auth_token;
10
+
11
+
// If no token, deny access
12
+
if (!token) {
13
+
return res.redirect(
14
+
`/login?redirect=${encodeURIComponent(req.originalUrl)}`,
15
+
);
16
+
}
17
+
18
+
try {
19
+
const user = jwt.verify(token, JWT_KEY);
20
+
req.user = user;
21
+
next();
22
+
} catch (error) {
23
+
res.redirect(`/login?redirect=${encodeURIComponent(req.originalUrl)}`);
24
+
}
25
+
}
26
+
27
+
module.exports = { authenticateToken };
+24
src/db.js
+24
src/db.js
···
1
+
const { Database } = require("bun:sqlite");
2
+
const db = new Database("readit.db", {
3
+
strict: true,
4
+
});
5
+
6
+
db.query(`
7
+
CREATE TABLE IF NOT EXISTS users (
8
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
9
+
username TEXT UNIQUE,
10
+
password_hash TEXT
11
+
)
12
+
`).run();
13
+
14
+
db.query(`
15
+
CREATE TABLE IF NOT EXISTS subscriptions (
16
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+
user_id INTEGER,
18
+
subreddit TEXT,
19
+
FOREIGN KEY(user_id) REFERENCES users(id),
20
+
UNIQUE(user_id, subreddit)
21
+
)
22
+
`).run();
23
+
24
+
module.exports = { db };
+4
-4
src/geddit.js
+4
-4
src/geddit.js
···
18
18
include_over_18: true,
19
19
};
20
20
21
-
subreddit = subreddit ? `/r/${subreddit}` : "";
21
+
const subredditStr = subreddit ? `/r/${subreddit}` : "";
22
22
23
23
return await fetch(
24
24
`${
25
-
this.host + subreddit
25
+
this.host + subredditStr
26
26
}/${sort}.json?${new URLSearchParams(Object.assign(params, options))}`,
27
27
)
28
28
.then((res) => res.json())
···
300
300
301
301
async searchAll(query, subreddit = null, options = {}) {
302
302
options.q = query;
303
-
subreddit = subreddit ? `/r/${subreddit}` : "";
303
+
const subredditStr = subreddit ? `/r/${subreddit}` : "";
304
304
305
305
const params = {
306
306
limit: 25,
···
310
310
311
311
return await fetch(
312
312
`${
313
-
this.host + subreddit
313
+
this.host + subredditStr
314
314
}/search.json?${new URLSearchParams(Object.assign(params, options))}`,
315
315
)
316
316
.then((res) => res.json())
+16
-28
src/index.js
+16
-28
src/index.js
···
1
1
const express = require("express");
2
+
const rateLimit = require("express-rate-limit");
2
3
const path = require("node:path");
3
4
const geddit = require("./geddit.js");
4
-
const { Database } = require("bun:sqlite");
5
-
6
-
const db = new Database("readit.db");
7
-
8
-
const createUsers = db.query(`
9
-
CREATE TABLE IF NOT EXISTS users (
10
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
11
-
username TEXT UNIQUE,
12
-
password_hash TEXT
13
-
)
14
-
`);
15
-
16
-
createUsers.run();
17
-
18
-
const createSubs = db.query(`
19
-
CREATE TABLE IF NOT EXISTS subscriptions (
20
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
21
-
user_id INTEGER,
22
-
subreddit TEXT,
23
-
FOREIGN KEY(user_id) REFERENCES users(id),
24
-
UNIQUE(user_id, subreddit)
25
-
)
26
-
`);
27
-
28
-
createSubs.run();
29
-
30
-
module.exports = { db };
31
-
5
+
const cookieParser = require("cookie-parser");
32
6
const app = express();
7
+
const hasher = new Bun.CryptoHasher("sha256", "secret-key");
8
+
const JWT_KEY = hasher.update(Math.random().toString()).digest("hex");
9
+
10
+
module.exports = { JWT_KEY };
33
11
34
12
app.set("views", path.join(__dirname, "views"));
35
13
app.set("view engine", "pug");
···
38
16
app.use(express.json());
39
17
app.use(express.urlencoded({ extended: true }));
40
18
app.use(express.static(path.join(__dirname, "public")));
19
+
app.use(cookieParser());
20
+
app.use(
21
+
rateLimit({
22
+
windowMs: 15 * 60 * 1000,
23
+
max: 100,
24
+
message: "Too many requests from this IP, please try again later.",
25
+
standardHeaders: true,
26
+
legacyHeaders: false,
27
+
}),
28
+
);
41
29
app.use("/", routes);
42
30
43
31
const port = process.env.READIT_PORT;
+2
-2
src/mixins/head.pug
+2
-2
src/mixins/head.pug
···
1
-
mixin head()
1
+
mixin head(title)
2
2
head
3
3
meta(name="viewport" content="width=device-width, initial-scale=1.0")
4
4
meta(charset='UTF-8')
5
-
title reddit
5
+
title #{`readit ${title}`}
6
6
link(rel="stylesheet", href="/styles.css")
7
7
link(rel="preconnect" href="https://rsms.me/")
8
8
link(rel="stylesheet" href="https://rsms.me/inter/inter.css")
+9
-1
src/mixins/header.pug
+9
-1
src/mixins/header.pug
···
1
-
mixin header()
1
+
mixin header(user)
2
2
div.header
3
3
div.header-item
4
4
a(href=`/`) home
···
8
8
a(href=`/r/popular`) popular
9
9
div.header-item
10
10
a(href=`/subs`) subscriptions
11
+
if user
12
+
div.header-item
13
+
| #{user.username}
14
+
|
15
+
a(href='/logout') (logout)
16
+
else
17
+
div.header-item
18
+
a(href=`/login`) login
11
19
+45
-16
src/public/styles.css
+45
-16
src/public/styles.css
···
8
8
--link-color: #29BC9B;
9
9
--link-visited-color: #999;
10
10
--accent: var(--link-color);
11
+
--error-text-color: red;
11
12
12
13
font-family: Inter, sans-serif;
13
14
font-feature-settings: 'ss01' 1, 'kern' 1, 'liga' 1, 'cv05' 1, 'dlig' 1, 'ss01' 1, 'ss07' 1, 'ss08' 1;
···
24
25
--link-color: #79ffe1;
25
26
--link-visited-color: #999;
26
27
--accent: var(--link-color);
28
+
--error-text-color: lightcoral;
27
29
}
28
30
}
29
31
···
156
158
.info-item, .header-item, .footer-item {
157
159
margin-right: 14px;
158
160
}
159
-
160
-
161
+
161
162
.media-preview img,
162
163
.media-preview video {
163
164
object-fit: cover;
···
185
186
max-width: 95%;
186
187
padding: 5px;
187
188
}
189
+
190
+
form {
191
+
display: flex;
192
+
flex-direction: column;
193
+
align-items: center;
194
+
width: 90%;
195
+
}
188
196
189
197
@media (min-width: 768px) {
190
198
.post, .comments-container, .hero, .header, .footer {
···
202
210
}
203
211
.post-media {
204
212
max-width: 50%;
213
+
}
214
+
form {
215
+
width: 40%;
205
216
}
206
217
}
207
218
···
226
237
.post-media {
227
238
max-width: 50%;
228
239
}
240
+
form {
241
+
width: 20%;
242
+
}
229
243
}
230
244
231
245
@media (min-width: 2560px) {
···
233
247
flex: 1 1 40%;
234
248
width: 40%;
235
249
}
236
-
}
237
-
238
-
.comments-container, .self-text {
239
-
text-align: justify;
240
250
}
241
251
242
252
.comment, .more {
···
320
330
blockquote {
321
331
margin: 0px;
322
332
padding-left: 10px;
323
-
border-left: 4px solid var(--blockquote-color);
333
+
border-left: 2px solid var(--blockquote-color);
324
334
color: var(--blockquote-color);
325
335
}
326
336
···
400
410
}
401
411
402
412
.gallery-item {
403
-
flex: 0 0 auto;
404
-
margin-right: 10px;
413
+
flex: 0 0 auto;
414
+
margin-right: 10px;
405
415
}
406
416
407
417
.gallery img {
···
439
449
color: var(--text-color);
440
450
}
441
451
442
-
form {
443
-
display: flex;
444
-
flex-direction: column;
445
-
align-items: center;
446
-
}
447
-
448
452
form label {
449
453
width: 100%;
454
+
flex-basis: 100%;
450
455
margin: 5px 0;
451
456
color: var(--text-color);
452
457
}
453
458
454
459
form input[type="submit"] {
455
-
width: auto;
460
+
width: 100%;
456
461
padding: 10px 20px;
457
462
margin-top: 20px;
458
463
background-color: var(--link-color);
···
466
471
background-color: var(--link-color);
467
472
opacity: 0.8;
468
473
}
474
+
475
+
.input-text {
476
+
width: 100%;
477
+
}
478
+
479
+
.submit-button {
480
+
margin: 24px 0;
481
+
width: 100%;
482
+
display: flex;
483
+
flex-direction: row;
484
+
justify-content: center;
485
+
}
486
+
487
+
.submit-button button {
488
+
width: 100%;
489
+
padding: 12px;
490
+
background-color: var(--accent);
491
+
color: var(--bg-color);
492
+
}
493
+
494
+
.register-error-message {
495
+
flex-flow: row wrap;
496
+
color: var(--error-text-color);
497
+
}
+149
-85
src/routes/index.js
+149
-85
src/routes/index.js
···
2
2
const he = require("he");
3
3
const { hash, compare } = require("bun");
4
4
const jwt = require("jsonwebtoken");
5
-
const router = express.Router();
6
-
const secretKey = "your_secret_key"; // Replace with your actual secret key
7
5
const geddit = require("../geddit.js");
8
-
const { db } = require("../index");
6
+
const { JWT_KEY } = require("../");
7
+
const { db } = require("../db");
8
+
const { authenticateToken } = require("../auth");
9
+
10
+
const router = express.Router();
9
11
const G = new geddit.Geddit();
10
12
11
13
// GET /
12
-
router.get("/", async (req, res) => {
14
+
router.get("/", authenticateToken, async (req, res) => {
13
15
res.render("home");
14
16
});
15
17
16
18
// GET /r/:id
17
-
router.get("/r/:subreddit", async (req, res) => {
19
+
router.get("/r/:subreddit", authenticateToken, async (req, res) => {
18
20
const subreddit = req.params.subreddit;
19
21
const isMulti = subreddit.includes("+");
20
22
const query = req.query ? req.query : {};
···
22
24
query.sort = "hot";
23
25
}
24
26
27
+
let isSubbed = false;
28
+
if (!isMulti) {
29
+
isSubbed =
30
+
db
31
+
.query(
32
+
"SELECT * FROM subscriptions WHERE user_id = $id AND subreddit = $subreddit",
33
+
)
34
+
.get({ id: req.user.id, subreddit }) !== null;
35
+
}
25
36
const postsReq = G.getSubmissions(query.sort, `${subreddit}`, query);
26
37
const aboutReq = G.getSubreddit(`${subreddit}`);
27
38
28
39
const [posts, about] = await Promise.all([postsReq, aboutReq]);
29
40
30
-
res.render("index", { subreddit, posts, about, query, isMulti });
41
+
res.render("index", {
42
+
subreddit,
43
+
posts,
44
+
about,
45
+
query,
46
+
isMulti,
47
+
user: req.user,
48
+
isSubbed,
49
+
});
31
50
});
32
51
33
52
// GET /comments/:id
34
-
router.get("/comments/:id", async (req, res) => {
53
+
router.get("/comments/:id", authenticateToken, async (req, res) => {
35
54
const id = req.params.id;
36
55
37
56
const params = {
···
39
58
};
40
59
response = await G.getSubmissionComments(id, params);
41
60
42
-
res.render("comments", unescape_submission(response));
61
+
res.render("comments", {
62
+
data: unescape_submission(response),
63
+
user: req.user,
64
+
});
43
65
});
44
66
45
67
// GET /comments/:parent_id/comment/:child_id
46
-
router.get("/comments/:parent_id/comment/:child_id", async (req, res) => {
47
-
const parent_id = req.params.parent_id;
48
-
const child_id = req.params.child_id;
68
+
router.get(
69
+
"/comments/:parent_id/comment/:child_id",
70
+
authenticateToken,
71
+
async (req, res) => {
72
+
const parent_id = req.params.parent_id;
73
+
const child_id = req.params.child_id;
49
74
50
-
const params = {
51
-
limit: 50,
52
-
};
53
-
response = await G.getSingleCommentThread(parent_id, child_id, params);
54
-
const comments = response.comments;
55
-
comments.forEach(unescape_comment);
56
-
res.render("single_comment_thread", { comments, parent_id });
57
-
});
58
-
59
-
router.get("/login", async (req, res) => {
60
-
res.render("login");
61
-
});
75
+
const params = {
76
+
limit: 50,
77
+
};
78
+
response = await G.getSingleCommentThread(parent_id, child_id, params);
79
+
const comments = response.comments;
80
+
comments.forEach(unescape_comment);
81
+
res.render("single_comment_thread", {
82
+
comments,
83
+
parent_id,
84
+
user: req.user,
85
+
});
86
+
},
87
+
);
62
88
63
89
// GET /subs
64
-
router.get("/subs", async (req, res) => {
65
-
res.render("subs");
90
+
router.get("/subs", authenticateToken, async (req, res) => {
91
+
const subs = db
92
+
.query("SELECT * FROM subscriptions WHERE user_id = $id")
93
+
.all({ id: req.user.id });
94
+
res.render("subs", { subs, user: req.user });
66
95
});
67
96
68
97
// GET /media
69
-
router.get("/media/*", async (req, res) => {
98
+
router.get("/media/*", authenticateToken, async (req, res) => {
70
99
const url = req.params[0];
71
100
const ext = url.split(".").pop().toLowerCase();
72
101
const kind = ["jpg", "jpeg", "png", "gif", "webp"].includes(ext)
···
81
110
82
111
router.post("/register", async (req, res) => {
83
112
const { username, password, confirm_password } = req.body;
84
-
console.log("Request body:", req.body);
113
+
85
114
if (!username || !password || !confirm_password) {
86
115
return res.status(400).send("All fields are required");
87
116
}
117
+
118
+
const user = db
119
+
.query("SELECT * FROM users WHERE username = $username")
120
+
.get({ username });
121
+
if (user) {
122
+
return res.render("register", {
123
+
message: `user by the name "${username}" exists, choose a different username`,
124
+
});
125
+
}
126
+
88
127
if (password !== confirm_password) {
89
-
return res.status(400).send("Passwords do not match");
128
+
return res.render("register", {
129
+
message: "passwords do not match, try again",
130
+
});
90
131
}
132
+
91
133
try {
92
-
const hashedPassword = await hash(password);
93
-
db.query("INSERT INTO users (username, password_hash) VALUES (?, ?)", [
94
-
username,
95
-
hashedPassword,
96
-
]).run();
97
-
res.status(201).redirect("/");
134
+
const hashedPassword = await Bun.password.hash(password);
135
+
const insertedRecord = db
136
+
.query(
137
+
"INSERT INTO users (username, password_hash) VALUES ($username, $hashedPassword)",
138
+
)
139
+
.run({
140
+
username,
141
+
hashedPassword,
142
+
});
143
+
const id = insertedRecord.lastInsertRowid;
144
+
const token = jwt.sign({ username, id }, JWT_KEY, { expiresIn: "100h" });
145
+
res
146
+
.status(200)
147
+
.cookie("auth_token", token, {
148
+
httpOnly: true,
149
+
maxAge: 2 * 24 * 60 * 60 * 1000,
150
+
})
151
+
.redirect("/");
98
152
} catch (err) {
99
-
console.log(err);
100
-
res.status(400).send("Error registering user");
153
+
return res.render("register", {
154
+
message: "error registering user, try again later",
155
+
});
101
156
}
157
+
});
158
+
159
+
router.get("/login", async (req, res) => {
160
+
res.render("login", req.query);
102
161
});
103
162
104
163
// POST /login
105
164
router.post("/login", async (req, res) => {
106
165
const { username, password } = req.body;
107
166
const user = db
108
-
.query("SELECT * FROM users WHERE username = ?", [username])
109
-
.get();
110
-
if (user && await compare(password, user.password_hash)) {
111
-
res.status(200).redirect("/");
167
+
.query("SELECT * FROM users WHERE username = $username")
168
+
.get({ username });
169
+
if (user && (await Bun.password.verify(password, user.password_hash))) {
170
+
const token = jwt.sign({ username, id: user.id }, JWT_KEY, {
171
+
expiresIn: "1h",
172
+
});
173
+
res
174
+
.cookie("auth_token", token, {
175
+
httpOnly: true,
176
+
maxAge: 2 * 24 * 60 * 60 * 1000,
177
+
})
178
+
.redirect(req.query.redirect || "/");
112
179
} else {
113
-
res.status(401).send("Invalid credentials");
180
+
res.render("login", {
181
+
message: "invalid credentials, try again",
182
+
});
114
183
}
115
184
});
116
185
186
+
// this would be post, but i cant stuff it in a link
187
+
router.get("/logout", (req, res) => {
188
+
res.clearCookie("auth_token", {
189
+
httpOnly: true,
190
+
secure: true,
191
+
});
192
+
res.redirect("/login");
193
+
});
194
+
117
195
// POST /subscribe
118
-
router.post("/subscribe", async (req, res) => {
119
-
const { username, subreddit } = req.body;
120
-
const user = db
121
-
.query("SELECT * FROM users WHERE username = ?", [username])
122
-
.get();
123
-
if (user) {
124
-
const existingSubscription = db
125
-
.query(
126
-
"SELECT * FROM subscriptions WHERE user_id = ? AND subreddit = ?",
127
-
[user.id, subreddit],
128
-
)
129
-
.get();
130
-
if (existingSubscription) {
131
-
res.status(400).send("Already subscribed to this subreddit");
132
-
} else {
133
-
db.query("INSERT INTO subscriptions (user_id, subreddit) VALUES (?, ?)", [
134
-
user.id,
135
-
subreddit,
136
-
]).run();
137
-
res.status(201).send("Subscribed successfully");
138
-
}
196
+
router.post("/subscribe", authenticateToken, async (req, res) => {
197
+
const { subreddit } = req.body;
198
+
const user = req.user;
199
+
const existingSubscription = db
200
+
.query(
201
+
"SELECT * FROM subscriptions WHERE user_id = $id AND subreddit = $subreddit",
202
+
)
203
+
.get({ id: user.id, subreddit });
204
+
if (existingSubscription) {
205
+
res.status(400).send("Already subscribed to this subreddit");
139
206
} else {
140
-
res.status(404).send("User not found");
207
+
db.query(
208
+
"INSERT INTO subscriptions (user_id, subreddit) VALUES ($id, $subreddit)",
209
+
).run({ id: user.id, subreddit });
210
+
res.status(201).send("Subscribed successfully");
141
211
}
142
212
});
143
213
144
-
router.post("/unsubscribe", async (req, res) => {
145
-
const { username, subreddit } = req.body;
146
-
const user = db
147
-
.query("SELECT * FROM users WHERE username = ?", [username])
148
-
.get();
149
-
if (user) {
150
-
const existingSubscription = db
151
-
.query(
152
-
"SELECT * FROM subscriptions WHERE user_id = ? AND subreddit = ?",
153
-
[user.id, subreddit],
154
-
)
155
-
.get();
156
-
if (existingSubscription) {
157
-
db.run("DELETE FROM subscriptions WHERE user_id = ? AND subreddit = ?", [
158
-
user.id,
159
-
subreddit,
160
-
]);
161
-
res.status(200).send("Unsubscribed successfully");
162
-
} else {
163
-
res.status(400).send("Subscription not found");
164
-
}
214
+
router.post("/unsubscribe", authenticateToken, async (req, res) => {
215
+
const { subreddit } = req.body;
216
+
const user = req.user;
217
+
const existingSubscription = db
218
+
.query(
219
+
"SELECT * FROM subscriptions WHERE user_id = $id AND subreddit = $subreddit",
220
+
)
221
+
.get({ id: user.id, subreddit });
222
+
if (existingSubscription) {
223
+
db.query(
224
+
"DELETE FROM subscriptions WHERE user_id = $id AND subreddit = $subreddit",
225
+
).run({ id: user.id, subreddit });
226
+
console.log("done");
227
+
res.status(200).send("Unsubscribed successfully");
165
228
} else {
166
-
res.status(404).send("User not found");
229
+
console.log("not");
230
+
res.status(400).send("Subscription not found");
167
231
}
168
232
});
169
233
+6
-4
src/views/comments.pug
+6
-4
src/views/comments.pug
···
3
3
include ../mixins/head
4
4
include ../utils
5
5
6
+
- var post = data.post
7
+
- var comments = data.comments
6
8
doctype html
7
9
html
8
-
+head()
10
+
+head(post.title)
9
11
script.
10
12
function toggleDetails(details_id) {
11
13
var detailsElement = document.getElementById(details_id);
···
16
18
17
19
body
18
20
main#content
19
-
+header()
21
+
+header(user)
20
22
div.hero
21
23
h3.sub-title
22
24
a(href=`/r/${post.subreddit}`) ← r/#{post.subreddit}
···
40
42
each item in post.gallery_data.items
41
43
- var url = `https://i.redd.it/${item.media_id}.jpg`
42
44
div.gallery-item
45
+
div.gallery-item-idx
46
+
| #{`${++idx}/${total}`}
43
47
a(href=`/media/${url}`)
44
48
img(src=url loading="lazy")
45
-
div.gallery-item-idx
46
-
| #{`${++idx}/${total}`}
47
49
else if post.post_hint == "image" && post.thumbnail && post.thumbnail != "self" && post.thumbnail != "default"
48
50
img(src=post.url).post-media
49
51
else if post.post_hint == 'hosted:video'
+22
-34
src/views/index.pug
+22
-34
src/views/index.pug
···
9
9
+head("home")
10
10
+subMgmt()
11
11
script(defer).
12
-
async function updateButton(sub) {
13
-
var b = document.getElementById("button-container");
14
-
b.innerHTML = '';
15
-
16
-
const button = document.createElement("button");
17
-
18
-
if (issub(sub)) {
19
-
button.innerText = "unsubscribe";
20
-
button.onclick = async () => await unsubscribe(sub);
21
-
} else {
22
-
button.innerText = "subscribe";
23
-
button.onclick = async () => await subscribe(sub);
24
-
}
25
-
b.appendChild(button);
26
-
}
27
-
28
12
async function subscribe(sub) {
29
-
await postSubscription(sub, true);
30
-
updateButton(sub);
13
+
await doThing(sub, 'subscribe');
31
14
}
32
15
33
16
async function unsubscribe(sub) {
34
-
await postUnsubscription(sub);
35
-
updateButton(sub);
17
+
await doThing(sub, 'unsubscribe');
36
18
}
37
19
38
-
async function postUnsubscription(sub) {
39
-
const response = await fetch('/unsubscribe', {
20
+
function getCookie(name) {
21
+
const value = `; ${document.cookie}`;
22
+
const parts = value.split(`; ${name}=`);
23
+
if (parts.length === 2) return parts.pop().split(";").shift();
24
+
}
25
+
26
+
async function doThing(sub, thing) {
27
+
const jwtToken = getCookie("auth_token");
28
+
const response = await fetch(`/${thing}`, {
40
29
method: 'POST',
41
30
headers: {
31
+
'Authorization': `Bearer ${jwtToken}`,
42
32
'Content-Type': 'application/json',
43
33
},
44
34
body: JSON.stringify({ subreddit: sub }),
45
35
});
46
36
47
-
if (!response.ok) {
48
-
console.error('Failed to update unsubscription');
37
+
let thinger = document.getElementById('thinger');
38
+
if (thing == 'subscribe') {
39
+
thinger.innerText = 'unsubscribe';
40
+
} else {
41
+
thinger.innerText = 'subscribe';
49
42
}
50
-
}
51
-
const response = await fetch('/subscribe', {
52
-
method: 'POST',
53
-
headers: {
54
-
'Content-Type': 'application/json',
55
-
},
56
-
body: JSON.stringify({ subreddit: sub, subscribe: subscribe }),
57
-
});
58
43
59
44
if (!response.ok) {
60
-
console.error('Failed to update subscription');
45
+
console.error(`Failed to do ${thing}`);
61
46
}
62
47
}
63
48
···
68
53
}
69
54
}
70
55
71
-
document.addEventListener('DOMContentLoaded', () => updateButton("#{subreddit}"));
72
56
body
73
57
main#content
74
58
+header(user)
···
82
66
| r/#{subreddit}
83
67
if !isMulti
84
68
div#button-container
69
+
if isSubbed
70
+
button(onclick=`unsubscribe('${subreddit}')`)#thinger unsubscribe
71
+
else
72
+
button(onclick=`subscribe('${subreddit}')`)#thinger subscribe
85
73
if about
86
74
p #{about.public_description}
87
75
details
+26
src/views/login.pug
+26
src/views/login.pug
···
1
+
include ../mixins/head
2
+
3
+
doctype html
4
+
html
5
+
+head("login")
6
+
body
7
+
main#content
8
+
h1 login
9
+
if message
10
+
div.register-error-message
11
+
| #{message}
12
+
- var url = redirect ? `/login?redirect=${redirect}` : '/login'
13
+
form(action=url method="post")
14
+
div.input-text
15
+
label(for="username") username
16
+
input(type="text" name="username" required)
17
+
div.input-text
18
+
label(for="password") password
19
+
input(type="password" name="password" required)
20
+
div.submit-button
21
+
button(type="submit") login
22
+
div
23
+
p
24
+
| don't have an account?
25
+
a(href="/register") register
26
+
+28
src/views/register.pug
+28
src/views/register.pug
···
1
+
include ../mixins/head
2
+
3
+
doctype html
4
+
html
5
+
+head("register")
6
+
body
7
+
main#content
8
+
h1 register
9
+
if message
10
+
div.register-error-message
11
+
| #{message}
12
+
form(action="/register" method="post")
13
+
div.input-text
14
+
label(for="username") username
15
+
input(type="text" name="username" required)
16
+
div.input-text
17
+
label(for="password") password
18
+
input(type="password" name="password" required)
19
+
div.input-text
20
+
label(for="confirm_password") confirm password
21
+
input(type="password" name="confirm_password" required)
22
+
div.submit-button
23
+
button(type="submit") register
24
+
div
25
+
p
26
+
| already have an account?
27
+
a(href="/login") login
28
+
+1
-1
src/views/single_comment_thread.pug
+1
-1
src/views/single_comment_thread.pug
+7
-3
src/views/subs.pug
+7
-3
src/views/subs.pug
···
4
4
5
5
doctype html
6
6
html
7
-
+head()
7
+
+head("subscriptions")
8
8
+subMgmt()
9
9
script.
10
10
function newSubItem(sub) {
···
26
26
document.addEventListener('DOMContentLoaded', buildSubList);
27
27
body
28
28
main#content
29
-
+header()
29
+
+header(user)
30
30
div.hero
31
31
h1 subscriptions
32
-
div#subList
32
+
p
33
+
each s in subs
34
+
a(href=`/r/${s.subreddit}`)
35
+
| r/#{s.subreddit}
36
+
br