selfhostable, read-only reddit client

Merge pull request #28 from GeorgeSG/georgesg/add-theme-support

Add theme support

authored by oppi.li and committed by GitHub bb9718ef f282ba07

Changed files
+87 -15
src
mixins
public
routes
+7 -2
readme.md
··· 9 - no account necessary for over-18 content 10 11 i host a version for myself and a few friends. reach out to 12 - me if you would like an invite. 13 14 ### features 15 ··· 87 or with just [bun](https://bun.sh/): 88 89 ```bash 90 - bun run src/index.js 91 ``` 92 93 ### usage ··· 98 username at the top-right to view the dashboard and to 99 invite other users to your instance. copy the link and send 100 it to your friends! 101 102 ### technical 103
··· 9 - no account necessary for over-18 content 10 11 i host a version for myself and a few friends. reach out to 12 + me if you would like an invite. 13 14 ### features 15 ··· 87 or with just [bun](https://bun.sh/): 88 89 ```bash 90 + bun run src/index.js 91 ``` 92 93 ### usage ··· 98 username at the top-right to view the dashboard and to 99 invite other users to your instance. copy the link and send 100 it to your friends! 101 + 102 + ### environment variables 103 + 104 + - `LURKER_PORT`: port to listen on, defaults to `3000`. 105 + - `LURKER_THEME`: name of CSS theme file. The file must be present in `src/public`. 106 107 ### technical 108
+2 -1
src/mixins/head.pug
··· 4 meta(charset='UTF-8') 5 title #{`${title} · lurker `} 6 link(rel="stylesheet", href="/styles.css") 7 link(rel="preconnect" href="https://rsms.me/") 8 link(rel="stylesheet" href="https://rsms.me/inter/inter.css") 9 script(src="https://cdn.dashjs.org/latest/dash.all.min.js") 10 -
··· 4 meta(charset='UTF-8') 5 title #{`${title} · lurker `} 6 link(rel="stylesheet", href="/styles.css") 7 + if theme 8 + link(rel="stylesheet", href=`/${theme}.css`) 9 link(rel="preconnect" href="https://rsms.me/") 10 link(rel="stylesheet" href="https://rsms.me/inter/inter.css") 11 script(src="https://cdn.dashjs.org/latest/dash.all.min.js")
+36
src/public/theme.css
···
··· 1 + /* 2 + Uncomment and modify the values in this file to change the theme of the app. 3 + 4 + :root { 5 + --bg-color: white; 6 + --bg-color-muted: #eee; 7 + --text-color: black; 8 + --text-color-muted: #999; 9 + --blockquote-color: green; 10 + --sticky-color: #dcfeda; 11 + --gilded: darkorange; 12 + --link-color: #29bc9b; 13 + --link-visited-color: #999; 14 + --accent: var(--link-color); 15 + --error-text-color: red; 16 + --border-radius-card: 0.5vmin; 17 + --border-radius-media: 0.5vmin; 18 + --border-radius-preview: 0.3vmin; 19 + } 20 + 21 + @media (prefers-color-scheme: dark) { 22 + :root { 23 + --bg-color: black; 24 + --bg-color-muted: #333; 25 + --text-color: white; 26 + --text-color-muted: #999; 27 + --blockquote-color: lightgreen; 28 + --sticky-color: #014413; 29 + --gilded: gold; 30 + --link-color: #79ffe1; 31 + --link-visited-color: #999; 32 + --accent: var(--link-color); 33 + --error-text-color: lightcoral; 34 + } 35 + } 36 + */
+42 -12
src/routes/index.js
··· 6 const { db } = require("../db"); 7 const { authenticateToken, authenticateAdmin } = require("../auth"); 8 const { validateInviteToken } = require("../invite"); 9 - const url = require("url"); 10 11 const router = express.Router(); 12 const G = new geddit.Geddit(); 13 14 // GET / 15 router.get("/", authenticateToken, async (req, res) => { 16 const subs = db 17 .query("SELECT * FROM subscriptions WHERE user_id = $id") 18 .all({ id: req.user.id }); 19 20 - const qs = req.query ? ('?' + new URLSearchParams(req.query).toString()) : ''; 21 22 if (subs.length === 0) { 23 res.redirect(`/r/all${qs}`); ··· 53 54 const [posts, about] = await Promise.all([postsReq, aboutReq]); 55 56 - if (query.view == 'card' && posts && posts.posts) { 57 posts.posts.forEach(unescape_selftext); 58 } 59 ··· 66 user: req.user, 67 isSubbed, 68 currentUrl: req.url, 69 }); 70 }); 71 ··· 82 user: req.user, 83 from: req.query.from, 84 query: req.query, 85 }); 86 }); 87 ··· 103 comments, 104 parent_id, 105 user: req.user, 106 }); 107 }, 108 ); ··· 115 ) 116 .all({ id: req.user.id }); 117 118 - res.render("subs", { subs, user: req.user, query: req.query }); 119 }); 120 121 // GET /search 122 router.get("/search", authenticateToken, async (req, res) => { 123 - res.render("search", { user: req.user, query: req.query }); 124 }); 125 126 // GET /sub-search 127 router.get("/sub-search", authenticateToken, async (req, res) => { 128 if (!req.query || !req.query.q) { 129 - res.render("sub-search", { user: req.user }); 130 } else { 131 const { items, after } = await G.searchSubreddits(req.query.q); 132 const subs = db ··· 145 user: req.user, 146 original_query: req.query.q, 147 query: req.query, 148 }); 149 } 150 }); ··· 152 // GET /post-search 153 router.get("/post-search", authenticateToken, async (req, res) => { 154 if (!req.query || !req.query.q) { 155 - res.render("post-search", { user: req.user }); 156 } else { 157 const { items, after } = await G.searchSubmissions(req.query.q); 158 const message = ··· 160 ? "no results found" 161 : `showing ${items.length} results`; 162 163 - if (req.query.view == 'card' && items) { 164 items.forEach(unescape_selftext); 165 } 166 - 167 res.render("post-search", { 168 items, 169 after, ··· 172 original_query: req.query.q, 173 currentUrl: req.url, 174 query: req.query, 175 }); 176 } 177 }); ··· 194 usedAt: Date.parse(inv.usedAt), 195 })); 196 } 197 - res.render("dashboard", { invites, isAdmin, user: req.user, query: req.query }); 198 }); 199 200 router.get("/create-invite", authenticateAdmin, async (req, res) => { ··· 233 const kind = ["jpg", "jpeg", "png", "gif", "webp"].includes(ext) 234 ? "img" 235 : "video"; 236 - res.render("media", { kind, url }); 237 }); 238 239 router.get("/register", validateInviteToken, async (req, res) => { 240 - res.render("register", { isDisabled: false, token: req.query.token }); 241 }); 242 243 router.post("/register", validateInviteToken, async (req, res) => { ··· 253 if (user) { 254 return res.render("register", { 255 message: `user by the name "${username}" exists, choose a different username`, 256 }); 257 } 258 259 if (password !== confirm_password) { 260 return res.render("register", { 261 message: "passwords do not match, try again", 262 }); 263 } 264 ··· 294 } catch (err) { 295 return res.render("register", { 296 message: "error registering user, try again later", 297 }); 298 } 299 });
··· 6 const { db } = require("../db"); 7 const { authenticateToken, authenticateAdmin } = require("../auth"); 8 const { validateInviteToken } = require("../invite"); 9 10 const router = express.Router(); 11 const G = new geddit.Geddit(); 12 13 + const commonRenderOptions = { 14 + theme: process.env.LURKER_THEME, 15 + }; 16 + 17 // GET / 18 router.get("/", authenticateToken, async (req, res) => { 19 const subs = db 20 .query("SELECT * FROM subscriptions WHERE user_id = $id") 21 .all({ id: req.user.id }); 22 23 + const qs = req.query ? "?" + new URLSearchParams(req.query).toString() : ""; 24 25 if (subs.length === 0) { 26 res.redirect(`/r/all${qs}`); ··· 56 57 const [posts, about] = await Promise.all([postsReq, aboutReq]); 58 59 + if (query.view == "card" && posts && posts.posts) { 60 posts.posts.forEach(unescape_selftext); 61 } 62 ··· 69 user: req.user, 70 isSubbed, 71 currentUrl: req.url, 72 + ...commonRenderOptions, 73 }); 74 }); 75 ··· 86 user: req.user, 87 from: req.query.from, 88 query: req.query, 89 + ...commonRenderOptions, 90 }); 91 }); 92 ··· 108 comments, 109 parent_id, 110 user: req.user, 111 + ...commonRenderOptions, 112 }); 113 }, 114 ); ··· 121 ) 122 .all({ id: req.user.id }); 123 124 + res.render("subs", { 125 + subs, 126 + user: req.user, 127 + query: req.query, 128 + ...commonRenderOptions, 129 + }); 130 }); 131 132 // GET /search 133 router.get("/search", authenticateToken, async (req, res) => { 134 + res.render("search", { 135 + user: req.user, 136 + query: req.query, 137 + ...commonRenderOptions, 138 + }); 139 }); 140 141 // GET /sub-search 142 router.get("/sub-search", authenticateToken, async (req, res) => { 143 if (!req.query || !req.query.q) { 144 + res.render("sub-search", { user: req.user, ...commonRenderOptions }); 145 } else { 146 const { items, after } = await G.searchSubreddits(req.query.q); 147 const subs = db ··· 160 user: req.user, 161 original_query: req.query.q, 162 query: req.query, 163 + ...commonRenderOptions, 164 }); 165 } 166 }); ··· 168 // GET /post-search 169 router.get("/post-search", authenticateToken, async (req, res) => { 170 if (!req.query || !req.query.q) { 171 + res.render("post-search", { user: req.user, ...commonRenderOptions }); 172 } else { 173 const { items, after } = await G.searchSubmissions(req.query.q); 174 const message = ··· 176 ? "no results found" 177 : `showing ${items.length} results`; 178 179 + if (req.query.view == "card" && items) { 180 items.forEach(unescape_selftext); 181 } 182 + 183 res.render("post-search", { 184 items, 185 after, ··· 188 original_query: req.query.q, 189 currentUrl: req.url, 190 query: req.query, 191 + ...commonRenderOptions, 192 }); 193 } 194 }); ··· 211 usedAt: Date.parse(inv.usedAt), 212 })); 213 } 214 + res.render("dashboard", { 215 + invites, 216 + isAdmin, 217 + user: req.user, 218 + query: req.query, 219 + ...commonRenderOptions, 220 + }); 221 }); 222 223 router.get("/create-invite", authenticateAdmin, async (req, res) => { ··· 256 const kind = ["jpg", "jpeg", "png", "gif", "webp"].includes(ext) 257 ? "img" 258 : "video"; 259 + res.render("media", { kind, url, ...commonRenderOptions }); 260 }); 261 262 router.get("/register", validateInviteToken, async (req, res) => { 263 + res.render("register", { 264 + isDisabled: false, 265 + token: req.query.token, 266 + ...commonRenderOptions, 267 + }); 268 }); 269 270 router.post("/register", validateInviteToken, async (req, res) => { ··· 280 if (user) { 281 return res.render("register", { 282 message: `user by the name "${username}" exists, choose a different username`, 283 + ...commonRenderOptions, 284 }); 285 } 286 287 if (password !== confirm_password) { 288 return res.render("register", { 289 message: "passwords do not match, try again", 290 + ...commonRenderOptions, 291 }); 292 } 293 ··· 323 } catch (err) { 324 return res.render("register", { 325 message: "error registering user, try again later", 326 + ...commonRenderOptions, 327 }); 328 } 329 });