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