selfhostable, read-only reddit client

init

oppi.li 15f20f32

+179
.gitignore
··· 1 + # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 + 3 + # Logs 4 + 5 + logs 6 + _.log 7 + npm-debug.log_ 8 + yarn-debug.log* 9 + yarn-error.log* 10 + lerna-debug.log* 11 + .pnpm-debug.log* 12 + 13 + # Caches 14 + 15 + .cache 16 + 17 + # Diagnostic reports (https://nodejs.org/api/report.html) 18 + 19 + report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 + 21 + # Runtime data 22 + 23 + pids 24 + _.pid 25 + _.seed 26 + *.pid.lock 27 + 28 + # Directory for instrumented libs generated by jscoverage/JSCover 29 + 30 + lib-cov 31 + 32 + # Coverage directory used by tools like istanbul 33 + 34 + coverage 35 + *.lcov 36 + 37 + # nyc test coverage 38 + 39 + .nyc_output 40 + 41 + # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 + 43 + .grunt 44 + 45 + # Bower dependency directory (https://bower.io/) 46 + 47 + bower_components 48 + 49 + # node-waf configuration 50 + 51 + .lock-wscript 52 + 53 + # Compiled binary addons (https://nodejs.org/api/addons.html) 54 + 55 + build/Release 56 + 57 + # Dependency directories 58 + 59 + node_modules/ 60 + jspm_packages/ 61 + 62 + # Snowpack dependency directory (https://snowpack.dev/) 63 + 64 + web_modules/ 65 + 66 + # TypeScript cache 67 + 68 + *.tsbuildinfo 69 + 70 + # Optional npm cache directory 71 + 72 + .npm 73 + 74 + # Optional eslint cache 75 + 76 + .eslintcache 77 + 78 + # Optional stylelint cache 79 + 80 + .stylelintcache 81 + 82 + # Microbundle cache 83 + 84 + .rpt2_cache/ 85 + .rts2_cache_cjs/ 86 + .rts2_cache_es/ 87 + .rts2_cache_umd/ 88 + 89 + # Optional REPL history 90 + 91 + .node_repl_history 92 + 93 + # Output of 'npm pack' 94 + 95 + *.tgz 96 + 97 + # Yarn Integrity file 98 + 99 + .yarn-integrity 100 + 101 + # dotenv environment variable files 102 + 103 + .env 104 + .env.development.local 105 + .env.test.local 106 + .env.production.local 107 + .env.local 108 + 109 + # parcel-bundler cache (https://parceljs.org/) 110 + 111 + .parcel-cache 112 + 113 + # Next.js build output 114 + 115 + .next 116 + out 117 + 118 + # Nuxt.js build / generate output 119 + 120 + .nuxt 121 + dist 122 + 123 + # Gatsby files 124 + 125 + # Comment in the public line in if your project uses Gatsby and not Next.js 126 + 127 + # https://nextjs.org/blog/next-9-1#public-directory-support 128 + 129 + # public 130 + 131 + # vuepress build output 132 + 133 + .vuepress/dist 134 + 135 + # vuepress v2.x temp and cache directory 136 + 137 + .temp 138 + 139 + # Docusaurus cache and generated files 140 + 141 + .docusaurus 142 + 143 + # Serverless directories 144 + 145 + .serverless/ 146 + 147 + # FuseBox cache 148 + 149 + .fusebox/ 150 + 151 + # DynamoDB Local files 152 + 153 + .dynamodb/ 154 + 155 + # TernJS port file 156 + 157 + .tern-port 158 + 159 + # Stores VSCode versions used for testing VSCode extensions 160 + 161 + .vscode-test 162 + 163 + # yarn v2 164 + 165 + .yarn/cache 166 + .yarn/unplugged 167 + .yarn/build-state.yml 168 + .yarn/install-state.gz 169 + .pnp.* 170 + 171 + # IntelliJ based IDEs 172 + .idea 173 + 174 + # Finder (MacOS) folder config 175 + .DS_Store 176 + 177 + reference 178 + bun.lockb 179 + result
+9
build.js
··· 1 + import { execSync } from "bun"; 2 + 3 + try { 4 + // Precompile Pug templates in the `src` directory to the `dist` directory 5 + execSync("pug src -o dist"); 6 + console.log("Pug templates compiled successfully."); 7 + } catch (error) { 8 + console.error("Failed to compile Pug templates:", error); 9 + }
+27
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1724748588, 6 + "narHash": "sha256-NlpGA4+AIf1dKNq76ps90rxowlFXUsV9x7vK/mN37JM=", 7 + "owner": "nixos", 8 + "repo": "nixpkgs", 9 + "rev": "a6292e34000dc93d43bccf78338770c1c5ec8a99", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "nixos", 14 + "ref": "nixpkgs-unstable", 15 + "repo": "nixpkgs", 16 + "type": "github" 17 + } 18 + }, 19 + "root": { 20 + "inputs": { 21 + "nixpkgs": "nixpkgs" 22 + } 23 + } 24 + }, 25 + "root": "root", 26 + "version": 7 27 + }
+99
flake.nix
··· 1 + { 2 + inputs = { 3 + 4 + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 5 + 6 + }; 7 + 8 + outputs = 9 + { self 10 + , nixpkgs 11 + }: 12 + let 13 + supportedSystems = [ "x86_64-linux" ]; 14 + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 15 + nixpkgsFor = forAllSystems (system: 16 + import nixpkgs { 17 + inherit system; 18 + overlays = [ self.overlays.default ]; 19 + }); 20 + 21 + in 22 + { 23 + overlays.default = final: prev: { 24 + node_modules = with final; stdenv.mkDerivation { 25 + pname = "readit-node-modules"; 26 + version = "0.0.1"; 27 + impureEnvVars = lib.fetchers.proxyImpureEnvVars 28 + ++ [ "GIT_PROXY_COMMAND" "SOCKS_SERVER" ]; 29 + src = ./.; 30 + nativeBuildInputs = [ bun ]; 31 + buildInputs = [ nodejs-slim_latest ]; 32 + dontConfigure = true; 33 + dontFixup = true; 34 + buildPhase = '' 35 + bun install --no-progress --frozen-lockfile 36 + ''; 37 + installPhase = '' 38 + mkdir -p $out/node_modules 39 + cp -R ./node_modules/* $out/node_modules 40 + ls -la $out/node_modules 41 + ''; 42 + outputHash = "sha256-qFYgRIarDChHQu0ZrUKd/Y61gxaagMWpf2h9xizwGv4="; 43 + outputHashAlgo = "sha256"; 44 + outputHashMode = "recursive"; 45 + }; 46 + readit = with final; stdenv.mkDerivation { 47 + pname = "readit"; 48 + version = "0.0.1"; 49 + src = ./.; 50 + nativeBuildInputs = [ makeBinaryWrapper ]; 51 + buildInputs = [ bun ]; 52 + 53 + buildPhase = '' 54 + runHook preBuild 55 + 56 + 57 + runHook postBuild 58 + ''; 59 + 60 + dontFixup = true; 61 + 62 + installPhase = '' 63 + runHook preInstall 64 + 65 + mkdir -p $out/bin 66 + 67 + # cp app.js $out/app.js 68 + cp -R ./* $out 69 + 70 + # bun is referenced naked in the package.json generated script 71 + # makeBinaryWrapper ${bun}/bin/bun $out/bin/$pname \ 72 + # --add-flags "run --prefer-offline --no-install $out/app.js" 73 + 74 + makeBinaryWrapper ${bun}/bin/bun $out/bin/$pname \ 75 + --prefix PATH : ${lib.makeBinPath [ bun ]} \ 76 + --add-flags "run --prefer-offline --no-install $out/src/index.js" 77 + 78 + ''; 79 + }; 80 + }; 81 + 82 + devShell = forAllSystems (system: 83 + let 84 + pkgs = nixpkgsFor."${system}"; 85 + in 86 + pkgs.mkShell { 87 + nativeBuildInputs = [ 88 + pkgs.bun 89 + ]; 90 + RUST_BACKTRACE = 1; 91 + }); 92 + 93 + packages = forAllSystems(system: { 94 + inherit (nixpkgsFor."${system}") readit node_modules; 95 + }); 96 + }; 97 + } 98 + 99 +
+17
package.json
··· 1 + { 2 + "name": "scarlet", 3 + "module": "index.ts", 4 + "type": "module", 5 + "devDependencies": { 6 + "@types/bun": "latest", 7 + "pug-cli": "^1.0.0-alpha6" 8 + }, 9 + "peerDependencies": { 10 + "typescript": "^5.0.0" 11 + }, 12 + "dependencies": { 13 + "express": "^4.19.2", 14 + "pug": "^3.0.3", 15 + "timeago.js": "^4.0.2" 16 + } 17 + }
+92
public/styles.css
··· 1 + main { 2 + display: flex; 3 + flex-direction: column; 4 + gap: 1rem; 5 + align-items: center; 6 + } 7 + 8 + .post, .comments-container, .header { 9 + padding: 0.3rem; 10 + flex: 1 1 95%; 11 + font-size: 1rem; 12 + width: 95%; 13 + } 14 + 15 + @media (min-width: 768px) { 16 + .post, .comments-container, .header { 17 + flex: 1 1 65%; 18 + width: 65%; 19 + } 20 + } 21 + 22 + @media (min-width: 1080px) { 23 + .post, .comments-container, .header { 24 + flex: 1 1 50%; 25 + width: 50%; 26 + } 27 + } 28 + 29 + .comments-container, .self-text { 30 + text-align: justify; 31 + } 32 + 33 + .comment, .more { 34 + width: 100%; 35 + border-left: 1px dashed; 36 + padding: 10px 0px 10px 24px; 37 + box-sizing: border-box; 38 + } 39 + 40 + .more { 41 + margin-bottom: 0px; 42 + font-size: 0.7rem; 43 + color: #777; 44 + } 45 + 46 + .first { 47 + border-left: none; 48 + padding-left: 0; 49 + margin: 0; 50 + margin-top: 12px; 51 + } 52 + 53 + .post-container { 54 + align-self: stretch; 55 + display: flex; 56 + } 57 + 58 + .post-text { 59 + flex-direction: column; 60 + align-items: stretch; 61 + justify-content: space-between; 62 + } 63 + 64 + .media-preview { 65 + padding-left: 10px; 66 + margin-left: auto; 67 + } 68 + 69 + .post-media { 70 + max-width: 100%; 71 + } 72 + 73 + .title-container,.info-container { 74 + flex: 1; 75 + margin-top: 10px; 76 + margin-bottom: 10px; 77 + } 78 + 79 + .info-container { 80 + color: #777; 81 + font-size: 0.8rem; 82 + display: flex; 83 + align-items: center; 84 + } 85 + 86 + .info-item { 87 + margin-right: 10px; 88 + } 89 + 90 + hr { 91 + border 1px solid #000; 92 + }
+1
readme.txt
··· 1 + nix build .#readit
+392
src/geddit.js
··· 1 + class Geddit { 2 + constructor() { 3 + this.host = "https://www.reddit.com"; 4 + this.parameters = { 5 + limit: 25, 6 + include_over_18: true, 7 + } 8 + this.search_params = { 9 + limit: 25, 10 + include_over_18: true, 11 + type: "sr,link,user", 12 + } 13 + } 14 + 15 + async getSubmissions(sort = null, subreddit = null, options = {}) { 16 + let params = { 17 + limit: 25, 18 + include_over_18: true, 19 + } 20 + 21 + sort = sort ? sort : "hot"; 22 + subreddit = subreddit ? "/r/" + subreddit : ""; 23 + 24 + return await fetch(this.host + subreddit + `/${sort}.json?` + new URLSearchParams(Object.assign(params, options))) 25 + .then(res => res.json()) 26 + .then(json => json.data) 27 + .then(data => ({ 28 + after: data.after, 29 + posts: data.children 30 + })) 31 + .catch(err => null); 32 + 33 + } 34 + 35 + async getDomainHot(domain, options = this.parameters) { 36 + return await fetch(this.host + "/domain/" + domain + "/hot.json?" + new URLSearchParams(options)) 37 + .then(res => res.json()) 38 + .then(json => json.data) 39 + .then(data => ({ 40 + after: data.after, 41 + posts: data.children 42 + })) 43 + .catch(err => null); 44 + } 45 + 46 + async getDomainBest(domain, options = this.parameters) { 47 + return await fetch(this.host + "/domain/" + domain + "/best.json?" + new URLSearchParams(options)) 48 + .then(res => res.json()) 49 + .then(json => json.data) 50 + .then(data => ({ 51 + after: data.after, 52 + posts: data.children 53 + })) 54 + .catch(err => null); 55 + } 56 + 57 + async getDomainTop(domain, options = this.parameters) { 58 + return await fetch(this.host + "/domain/" + domain + "/top.json?" + new URLSearchParams(options)) 59 + .then(res => res.json()) 60 + .then(json => json.data) 61 + .then(data => ({ 62 + after: data.after, 63 + posts: data.children 64 + })) 65 + .catch(err => null); 66 + } 67 + 68 + async getDomainNew(domain, options = this.parameters) { 69 + return await fetch(this.host + "/domain/" + domain + "/new.json?" + new URLSearchParams(options)) 70 + .then(res => res.json()) 71 + .then(json => json.data) 72 + .then(data => ({ 73 + after: data.after, 74 + posts: data.children 75 + })) 76 + .catch(err => null); 77 + } 78 + 79 + async getDomainRising(domain, options = this.parameters) { 80 + return await fetch(this.host + "/domain/" + domain + "/rising.json?" + new URLSearchParams(options)) 81 + .then(res => res.json()) 82 + .then(json => json.data) 83 + .then(data => ({ 84 + after: data.after, 85 + posts: data.children 86 + })) 87 + .catch(err => null); 88 + } 89 + 90 + async getDomainControversial(domain, options = this.parameters) { 91 + return await fetch(this.host + "/domain/" + domain + "/controversial.json?" + new URLSearchParams(options)) 92 + .then(res => res.json()) 93 + .then(json => json.data) 94 + .then(data => ({ 95 + after: data.after, 96 + posts: data.children 97 + })) 98 + .catch(err => null); 99 + } 100 + 101 + async getSubreddit(subreddit) { 102 + return await fetch(this.host + "/r/" + subreddit + "/about.json") 103 + .then(res => res.json()) 104 + .then(json => json.data) 105 + .catch(err => null); 106 + } 107 + 108 + async getSubredditRules(subreddit) { 109 + return await fetch(this.host + "/r/" + subreddit + "/about/rules.json") 110 + .then(res => res.json()) 111 + .then(json => json.data) 112 + .catch(err => null); 113 + } 114 + 115 + async getSubredditModerators(subreddit) { 116 + return await fetch(this.host + "/r/" + subreddit + "/about/moderators.json") 117 + .then(res => res.json()) 118 + .then(json => json.data) 119 + .then(data = ({ 120 + users: data.children 121 + })) 122 + .catch(err => null); 123 + } 124 + 125 + async getSubredditWikiPages(subreddit) { 126 + return await fetch(this.host + "/r/" + subreddit + "/wiki/pages.json") 127 + .then(res => res.json()) 128 + .then(json => json.data) 129 + .catch(err => null); 130 + } 131 + 132 + async getSubredditWikiPage(subreddit, page) { 133 + return await fetch(this.host + "/r/" + subreddit + "/wiki/" + page + ".json") 134 + .then(res => res.json()) 135 + .then(json => json.data) 136 + .catch(err => null); 137 + } 138 + 139 + async getSubredditWikiPageRevisions(subreddit, page) { 140 + return await fetch(this.host + "/r/" + subreddit + "/wiki/revisions" + page + ".json") 141 + .then(res => res.json()) 142 + .then(json => json.data.children) 143 + .catch(err => null); 144 + } 145 + 146 + async getPopularSubreddits(options = this.parameters) { 147 + return await fetch(this.host + "/subreddits/popular.json?" + new URLSearchParams(options)) 148 + .then(res => res.json()) 149 + .then(json => json.data) 150 + .then(data => ({ 151 + after: data.after, 152 + subreddits: data.children 153 + })) 154 + .catch(err => null); 155 + } 156 + 157 + async getNewSubreddits(options = this.parameters) { 158 + return await fetch(this.host + "/subreddits/new.json?" + new URLSearchParams(options)) 159 + .then(res => res.json()) 160 + .then(json => json.data) 161 + .then(data => ({ 162 + after: data.after, 163 + subreddits: data.children 164 + })) 165 + .catch(err => null); 166 + } 167 + 168 + async getPremiumSubreddits(options = this.parameters) { 169 + return await fetch(this.host + "/subreddits/premium.json?" + new URLSearchParams(options)) 170 + .then(res => res.json()) 171 + .then(json => json.data) 172 + .then(data => ({ 173 + after: data.after, 174 + subreddits: data.children 175 + })) 176 + .catch(err => null); 177 + } 178 + 179 + async getDefaultSubreddits(options = this.parameters) { 180 + return await fetch(this.host + "/subreddits/default.json?" + new URLSearchParams(options)) 181 + .then(res => res.json()) 182 + .then(json => json.data) 183 + .then(data => ({ 184 + after: data.after, 185 + subreddits: data.children 186 + })) 187 + .catch(err => null); 188 + } 189 + 190 + async getPopularUsers(options = this.parameters) { 191 + return await fetch(this.host + "/users/popular.json?" + new URLSearchParams(options)) 192 + .then(res => res.json()) 193 + .then(json => json.data) 194 + .then(data => ({ 195 + after: data.after, 196 + users: data.children 197 + })) 198 + .catch(err => null); 199 + } 200 + 201 + async getNewUsers(options = this.parameters) { 202 + return await fetch(this.host + "/users/new.json?" + new URLSearchParams(options)) 203 + .then(res => res.json()) 204 + .then(json => json.data) 205 + .then(data => ({ 206 + after: data.after, 207 + users: data.children 208 + })) 209 + .catch(err => null); 210 + } 211 + 212 + async searchSubmissions(query, options = {}) { 213 + options.q = query; 214 + options.type = "link"; 215 + 216 + let params = { 217 + limit: 25, 218 + include_over_18: true 219 + } 220 + 221 + console.log(this.host + "/search.json?" + new URLSearchParams(Object.assign(params, options))); 222 + 223 + return await fetch(this.host + "/search.json?" + new URLSearchParams(Object.assign(params, options))) 224 + .then(res => res.json()) 225 + .then(json => json.data) 226 + .then(data => ({ 227 + after: data.after, 228 + items: data.children 229 + })) 230 + .catch(err => null); 231 + } 232 + 233 + async searchSubreddits(query, options = {}) { 234 + options.q = query; 235 + 236 + let params = { 237 + limit: 25, 238 + include_over_18: true 239 + } 240 + 241 + return await fetch(this.host + "/subreddits/search.json?" + new URLSearchParams(Object.assign(params, options))) 242 + .then(res => res.json()) 243 + .then(json => json.data) 244 + .then(data => ({ 245 + after: data.after, 246 + items: data.children 247 + })) 248 + .catch(err => null); 249 + } 250 + 251 + async searchUsers(query, options = {}) { 252 + options.q = query; 253 + 254 + let params = { 255 + limit: 25, 256 + include_over_18: true 257 + } 258 + 259 + return await fetch(this.host + "/users/search.json?" + new URLSearchParams(Object.assign(params, options))) 260 + .then(res => res.json()) 261 + .then(json => json.data) 262 + .then(data => ({ 263 + after: data.after, 264 + items: data.children 265 + })) 266 + .catch(err => null); 267 + } 268 + 269 + async searchAll(query, subreddit = null, options = {}) { 270 + options.q = query; 271 + subreddit = subreddit ? "/r/" + subreddit : ""; 272 + 273 + let params = { 274 + limit: 25, 275 + include_over_18: true, 276 + type: "sr,link,user", 277 + } 278 + 279 + return await fetch(this.host + subreddit + "/search.json?" + new URLSearchParams(Object.assign(params, options))) 280 + .then(res => res.json()) 281 + .then(json => Array.isArray(json) ? ({ 282 + after: json[1].data.after, 283 + items: json[0].data.children.concat(json[1].data.children) 284 + }) : ({ 285 + after: json.data.after, 286 + items: json.data.children 287 + })) 288 + .catch(err => null); 289 + } 290 + 291 + async getSubmission(id) { 292 + return await fetch(this.host + "/by_id/" + id + ".json") 293 + .then(res => res.json()) 294 + .then(json => json.data.children[0].data) 295 + .catch(err => null); 296 + } 297 + 298 + async getSubmissionComments(id, options = this.parameters) { 299 + return await fetch(this.host + "/comments/" + id + ".json?" + new URLSearchParams(options)) 300 + .then(res => res.json()) 301 + .then(json => ({ 302 + submission: json[0].data.children[0], 303 + comments: json[1].data.children 304 + })) 305 + .catch(err => null); 306 + } 307 + 308 + async getSubredditComments(subreddit, options = this.parameters) { 309 + return await fetch(this.host + "/r/" + subreddit + "/comments.json?" + new URLSearchParams(options)) 310 + .then(res => res.json()) 311 + .then(json => json.data.children) 312 + .catch(err => null); 313 + } 314 + 315 + async getUser(username) { 316 + return await fetch(this.host + "/user/" + username + "/about.json") 317 + .then(res => res.json()) 318 + .then(json => json.data) 319 + .catch(err => null); 320 + } 321 + 322 + async getUserOverview(username, options = this.parameters) { 323 + return await fetch(this.host + "/user/" + username + "/overview.json?" + new URLSearchParams(options)) 324 + .then(res => res.json()) 325 + .then(json => json.data) 326 + .then(data => ({ 327 + after: data.after, 328 + items: data.children 329 + })) 330 + .catch(err => null); 331 + } 332 + 333 + async getUserComments(username, options = this.parameters) { 334 + return await fetch(this.host + "/user/" + username + "/comments.json?" + new URLSearchParams(options)) 335 + .then(res => res.json()) 336 + .then(json => json.data) 337 + .then(data => ({ 338 + after: data.after, 339 + items: data.children 340 + })) 341 + .catch(err => null); 342 + } 343 + 344 + async getUserSubmissions(username, options = this.parameters) { 345 + return await fetch(this.host + "/user/" + username + "/submitted.json?" + new URLSearchParams(options)) 346 + .then(res => res.json()) 347 + .then(json => json.data) 348 + .then(data => ({ 349 + after: data.after, 350 + items: data.children 351 + })) 352 + .catch(err => null); 353 + } 354 + 355 + async getLiveThread(id) { 356 + return await fetch(this.host + "/live/" + id + "/about.json") 357 + .then(res => res.json()) 358 + .then(json => json.data) 359 + .catch(err => null); 360 + } 361 + 362 + async getLiveThreadUpdates(id, options = this.parameters) { 363 + return await fetch(this.host + "/live/" + id + ".json?" + new URLSearchParams(options)) 364 + .then(res => res.json()) 365 + .then(json => json.data.children) 366 + .catch(err => null); 367 + } 368 + 369 + 370 + async getLiveThreadContributors(id, options = this.parameters) { 371 + return await fetch(this.host + "/live/" + id + "/contributors.json?" + new URLSearchParams(options)) 372 + .then(res => res.json()) 373 + .then(json => json.data.children) 374 + .catch(err => null); 375 + } 376 + 377 + async getLiveThreadDiscussions(id, options = this.parameters) { 378 + return await fetch(this.host + "/live/" + id + "/discussions.json?" + new URLSearchParams(options)) 379 + .then(res => res.json()) 380 + .then(json => json.data.children) 381 + .catch(err => null); 382 + } 383 + 384 + async getLiveThreadsNow(options = this.parameters) { 385 + return await fetch(this.host + "/live/happening_now.json?" + new URLSearchParams(options)) 386 + .then(res => res.json()) 387 + .then(json => json.data.children) 388 + .catch(err => null); 389 + } 390 + } 391 + 392 + export { Geddit }
+17
src/index.js
··· 1 + const express = require('express'); 2 + const path = require('path'); 3 + const routes = require('./routes/index'); 4 + const geddit = require('./geddit.js'); 5 + 6 + const app = express(); 7 + 8 + app.set('views', path.join(__dirname, 'views')); 9 + app.set('view engine', 'pug'); 10 + 11 + app.use(express.static('public')); 12 + app.use('/', routes); 13 + 14 + const server = app.listen(3000, () => { 15 + console.log(`started on ${server.address().port}`); 16 + }); 17 +
+35
src/routes/index.js
··· 1 + const express = require('express'); 2 + const router = express.Router(); 3 + const geddit = require('../geddit.js'); 4 + const G = new geddit.Geddit(); 5 + const fs = require('fs/promises'); 6 + 7 + 8 + // GET / 9 + router.get('/', async (req, res) => { 10 + res.redirect("/r/all") 11 + }); 12 + 13 + // GET /r/:id 14 + router.get('/r/:subreddit', async (req, res) => { 15 + var subreddit = req.params.subreddit; 16 + 17 + var postsReq = G.getSubmissions(`r/${subreddit}`); 18 + var aboutReq = G.getSubreddit(`${subreddit}`); 19 + 20 + var [posts, about] = await Promise.all([postsReq, aboutReq]); 21 + res.render('index', { subreddit, posts, about }); 22 + }); 23 + 24 + // GET /comments/:id 25 + router.get('/comments/:id', async (req, res) => { 26 + var id = req.params.id; 27 + 28 + response = await G.getSubmissionComments(id); 29 + var post = response.submission.data; 30 + var comments = response.comments; 31 + 32 + res.render('comments', { post, comments }); 33 + }); 34 + 35 + module.exports = router;
+18
src/views/comment.pug
··· 1 + include utils 2 + mixin comment(com, isfirst) 3 + - var data = com.data 4 + - var kind = com.kind 5 + if kind == "more" 6 + div.more #{data.count} more comments 7 + else 8 + div(class=`comment ${isfirst?'first':''}`) 9 + div.comment-body !{data.body} 10 + div.info-container 11 + div.info-item by u/#{data.author} 12 + div.info-item ↑ #{fmtnum(data.ups)} 13 + div.replies 14 + if data.replies 15 + if data.replies.data 16 + if data.replies.data.children 17 + each reply in data.replies.data.children 18 + +comment(reply,false)
+25
src/views/comments.pug
··· 1 + doctype html 2 + html 3 + head 4 + meta(charset='UTF-8') 5 + title reddit 6 + link(rel='stylesheet', href='/styles.css') 7 + body 8 + main#content 9 + div.header 10 + a(href=`/r/${post.subreddit}`) 11 + h4 ← r/#{post.subreddit} 12 + h2 #{post.title} 13 + if post.post_hint == 'image' 14 + img(src=post.url).post-media 15 + else if post.post_hint == 'hosted:video' 16 + video(src=post.url).post-media 17 + p.self-text !{post.selftext} 18 + hr 19 + 20 + div.comments-container 21 + each child in comments 22 + include comment 23 + +comment(child, true) 24 + 25 + script(src='https://unpkg.com/htmx.org@1.9.10')
+19
src/views/index.pug
··· 1 + doctype html 2 + html 3 + head 4 + meta(charset='UTF-8') 5 + title reddit 6 + link(rel='stylesheet', href='/styles.css') 7 + body 8 + main#content 9 + div.header 10 + a(href=`/r/#{subreddit}`) 11 + h1 r/#{subreddit} 12 + if about 13 + p #{about.public_description} 14 + 15 + each child in posts.posts 16 + include post 17 + +post(child.data) 18 + 19 + script(src='https://unpkg.com/htmx.org@1.9.10')
+21
src/views/post.pug
··· 1 + include utils 2 + mixin post(p) 3 + article.post 4 + div.post-container 5 + div.post-text 6 + div.title-container !{p.title} 7 + div.info-container 8 + div.info-item by u/#{p.author} 9 + div.info-item ↑ #{fmtnum(p.ups)} 10 + div.info-item #{p.domain} 11 + div.info-item 12 + a(href=`/r/${p.subreddit}`) r/#{p.subreddit} 13 + div.info-item 14 + a(href=`/comments/${p.id}`) #{fmtnum (p.num_comments)} #{fmttxt(p.num_comments, 'comment')} 15 + div.media-preview 16 + if p.post_hint == "image" || p.post_hint == "link" 17 + if p.thumbnail && p.thumbnail != "self" || p.thumbnail != "default" 18 + a(href=p.url) 19 + img(src=p.thumbnail width='100px') 20 + else if p.post_hint == "hosted:video" 21 + video(src=p.secure_media.reddit_video.scrubber_media_url width='100px')
+3
src/views/utils.pug
··· 1 + - var fmtnum = (n)=>n>=1000?(n/1000).toFixed(1)+'k':n; 2 + - var fmttxt = (n,t)=>`${t}${n==1?'':'s'}` 3 +