├── .dockerignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierignore ├── Dockerfile ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── HN-Clone-Architecture-overview.png ├── HN-Demo.gif ├── HN-Demo.jpg ├── HN-Demo.psd ├── README.md └── seal.jpg ├── healthcheck.js ├── jest-setup.js ├── jest.config.js ├── next-env.d.ts ├── next.config.js ├── nodemon.json ├── package-lock.json ├── package.json ├── pages ├── active.tsx ├── ask.tsx ├── best.tsx ├── bestcomments.tsx ├── bookmarklet.tsx ├── changepw.tsx ├── dmca.tsx ├── favorites.tsx ├── forgot.tsx ├── formatdoc.tsx ├── front.tsx ├── hidden.tsx ├── index.tsx ├── item.tsx ├── jobs.tsx ├── leaders.tsx ├── lists.tsx ├── login.tsx ├── newcomments.tsx ├── newest.tsx ├── newpoll.tsx ├── newsfaq.tsx ├── newsguidelines.tsx ├── newswelcome.tsx ├── noobcomments.tsx ├── noobstories.tsx ├── reply.tsx ├── security.tsx ├── show.tsx ├── showhn.tsx ├── shownew.tsx ├── submit.tsx ├── submitted.tsx ├── threads.tsx ├── upvoted.tsx ├── user.tsx └── x.tsx ├── prettier.config.js ├── public ├── robots.txt └── static │ ├── README.md │ ├── dmca.css │ ├── favicon.ico │ ├── grayarrow.gif │ ├── grayarrow2x.gif │ ├── news.css │ ├── s.gif │ ├── y18.gif │ ├── yc.css │ └── yc500.gif ├── server ├── database │ ├── cache-warmer.ts │ ├── cache.ts │ └── database.ts ├── graphql-resolvers.spec.ts ├── graphql-resolvers.ts ├── graphql-schema.ts ├── server.ts └── services │ ├── comment-service.spec.ts │ ├── comment-service.ts │ ├── feed-service.ts │ ├── news-item-service.spec.ts │ ├── news-item-service.ts │ └── user-service.ts ├── src ├── @types │ └── global.ts ├── README.md ├── __tests__ │ ├── active.spec.tsx │ ├── ask.spec.tsx │ ├── best.spec.tsx │ ├── bestcomments.spec.tsx │ ├── bookmarklet.spec.tsx │ ├── changepw.spec.tsx │ ├── dmca.spec.tsx │ ├── formatdoc.spec.tsx │ ├── front.spec.tsx │ ├── hidden.spec.tsx │ ├── index.spec.tsx │ ├── item.spec.tsx │ ├── jobs.spec.tsx │ ├── leaders.spec.tsx │ ├── lists.spec.tsx │ ├── login.spec.tsx │ ├── newcomments.spec.tsx │ ├── newest.spec.tsx │ ├── newpoll.spec.tsx │ ├── newsfaq.spec.tsx │ ├── newsguidelines.spec.tsx │ ├── newswelcome.spec.tsx │ ├── noobcomments.spec.tsx │ ├── noobstories.spec.tsx │ ├── reply.spec.tsx │ ├── security.spec.tsx │ ├── show.spec.tsx │ ├── showhn.spec.tsx │ ├── shownew.spec.tsx │ ├── submit.spec.tsx │ ├── submitted.spec.tsx │ ├── threads.spec.tsx │ └── user.spec.tsx ├── components │ ├── __snapshots__ │ │ ├── comment-box.spec.tsx.snap │ │ ├── comment.spec.tsx.snap │ │ ├── news-detail.spec.tsx.snap │ │ ├── news-feed.spec.tsx.snap │ │ └── news-title.spec.tsx.snap │ ├── comment-box.spec.tsx │ ├── comment-box.tsx │ ├── comment.spec.tsx │ ├── comment.tsx │ ├── comments.tsx │ ├── footer.tsx │ ├── header-nav.tsx │ ├── header.tsx │ ├── loading-spinner.tsx │ ├── news-detail.spec.tsx │ ├── news-detail.tsx │ ├── news-feed.spec.tsx │ ├── news-feed.tsx │ ├── news-item-with-comments.tsx │ ├── news-title.spec.tsx │ └── news-title.tsx ├── config.ts ├── data │ ├── models │ │ ├── comment-model.ts │ │ ├── feed.ts │ │ ├── index.ts │ │ ├── news-item-model.ts │ │ └── user-model.ts │ ├── mutations │ │ ├── hide-news-item-mutation.ts │ │ ├── submit-news-item-mutation.ts │ │ └── upvote-news-item-mutation.ts │ ├── queries │ │ └── me-query.ts │ ├── sample-data.ts │ └── validation │ │ ├── user.ts │ │ └── validation-error.ts ├── helpers │ ├── convert-number-to-time-ago.spec.ts │ ├── convert-number-to-time-ago.ts │ ├── hash-password.ts │ ├── init-apollo.tsx │ ├── is-valid-url.ts │ ├── user-login-error-code.ts │ └── with-data.tsx └── layouts │ ├── blank-layout.tsx │ ├── main-layout.tsx │ └── notice-layout.tsx ├── tsconfig-server.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .next 3 | build 4 | dist 5 | docs 6 | node_modules 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | }, 7 | ignorePatterns: ['node_modules', '**/__mocks__*', '**/__tests__*', '**/*.spec.*'], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | project: 'tsconfig.json', 11 | sourceType: 'module', 12 | }, 13 | plugins: ['react', 'prettier', '@typescript-eslint', 'import'], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:react/recommended', 17 | 'plugin:@typescript-eslint/eslint-recommended', 18 | 'plugin:@typescript-eslint/recommended', 19 | 'plugin:@typescript-eslint/recommended-requiring-type-checking', 20 | 'prettier', 21 | 'plugin:prettier/recommended', 22 | 'plugin:import/recommended', 23 | 'plugin:import/typescript' 24 | ], 25 | rules: { 26 | '@typescript-eslint/interface-name-prefix': ['warn', { prefixWithI: 'always' }], 27 | 'import/prefer-default-export': 'off', 28 | 'import/no-cycle': 'warn', 29 | 'react/prop-types': 'off', 30 | 'react/state-in-constructor': 'off', 31 | 'react/static-property-placement': ['error', 'static public field'], 32 | 'react/destructuring-assignment': ['warn', 'always', { ignoreClassFields: true }], 33 | 'no-nested-ternary': 'off', 34 | '@typescript-eslint/restrict-template-expressions': 'warn', 35 | // '@typescript-eslint/adjacent-overload-signatures': 'warn', 36 | // '@typescript-eslint/array-type': 'warn', 37 | // '@typescript-eslint/ban-types': 'warn', 38 | // '@typescript-eslint/class-name-casing': 'warn', 39 | // '@typescript-eslint/consistent-type-assertions': 'warn', 40 | // '@typescript-eslint/indent': ['off', 2], 41 | // '@typescript-eslint/member-delimiter-style': [ 42 | // 'off', 43 | // { 44 | // multiline: { 45 | // delimiter: 'none', 46 | // requireLast: true, 47 | // }, 48 | // singleline: { 49 | // delimiter: 'semi', 50 | // requireLast: false, 51 | // }, 52 | // }, 53 | // ], 54 | // '@typescript-eslint/member-ordering': 'off', 55 | // '@typescript-eslint/no-empty-function': 'warn', 56 | // '@typescript-eslint/no-empty-interface': 'warn', 57 | '@typescript-eslint/no-explicit-any': 'off', 58 | // '@typescript-eslint/no-misused-new': 'warn', 59 | // '@typescript-eslint/no-namespace': 'warn', 60 | // '@typescript-eslint/no-parameter-properties': 'off', 61 | // '@typescript-eslint/no-this-alias': 'warn', 62 | // '@typescript-eslint/no-use-before-define': 'off', 63 | // '@typescript-eslint/no-var-requires': 'warn', 64 | // '@typescript-eslint/prefer-for-of': 'warn', 65 | // '@typescript-eslint/prefer-function-type': 'warn', 66 | // '@typescript-eslint/prefer-namespace-keyword': 'warn', 67 | // '@typescript-eslint/quotes': ['warn', 'single'], 68 | // '@typescript-eslint/semi': ['off', null], 69 | // '@typescript-eslint/triple-slash-reference': 'warn', 70 | // '@typescript-eslint/type-annotation-spacing': 'off', 71 | // '@typescript-eslint/unified-signatures': 'warn', 72 | // 'arrow-parens': ['off', 'as-needed'], 73 | }, 74 | settings: { 75 | react: { 76 | version: 'detect', 77 | }, 78 | }, 79 | }; 80 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | .env 3 | .next 4 | .vscode 5 | *.DS_Store 6 | *.log 7 | build 8 | coverage 9 | dist 10 | logs 11 | node_modules 12 | npm-debug.log 13 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | build 3 | docs 4 | public 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # DEV BUILD STEP 2 | FROM node:14.17-alpine3.13 as devBuild 3 | WORKDIR /app 4 | 5 | # Log the npm config and env variables 6 | ENV NODE_ENV=production 7 | RUN npm config ls 8 | RUN env 9 | 10 | # Install dependencies first so docker caches them 11 | COPY package.json package-lock.json /app/ 12 | RUN ls -a 13 | RUN npm install --production=false 14 | 15 | # Copy the source code and build 16 | COPY . . 17 | RUN npm run build:prod 18 | RUN ls -a 19 | 20 | # PROD BUILD STEP 21 | FROM node:14.17-alpine3.13 22 | 23 | # Create an app directory on the container 24 | WORKDIR /app 25 | ENV NODE_ENV=production 26 | 27 | # Project copy build, install only prod dependencies 28 | COPY --from=devBuild /app/dist ./dist 29 | COPY --from=devBuild /app/.next ./.next 30 | COPY --from=devBuild /app/public ./public 31 | COPY --from=devBuild /app/next.config.js ./next.config.js 32 | COPY package.json package-lock.json healthcheck.js ./ 33 | 34 | RUN ls -a 35 | 36 | RUN npm install --only=prod 37 | 38 | RUN npx next telemetry disable 39 | 40 | # Expose the container port to the OS 41 | # docker run takes -p argument to forward this port to network 42 | EXPOSE 3000 43 | 44 | # Start the application 45 | CMD npm run start:prod 46 | 47 | HEALTHCHECK --interval=30s --timeout=12s --start-period=30s \ 48 | CMD node /healthcheck.js 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017, Clinton D'Annolfo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Hacker News Clone React/GraphQL

2 | 3 |

4 | GitHub Stars 5 | GitHub Followers 6 | GitHub Issues 7 | GitHub Pull Requests 8 |

9 | 10 | This project is a clone of hacker news rewritten with universal JavaScript, using React and GraphQL. It is intended to be an example or boilerplate to help you structure your projects using production-ready technologies. 11 | 12 |

13 | 14 | Hacker News Clone Demo 15 | 16 |

17 |

18 | Live Demo 19 |

20 | 21 | ## Overview 22 | 23 | ### Featuring 24 | 25 | - React - (UI Framework) 26 | - GraphQL - (Web Data API) 27 | - Apollo - (GraphQL Client/Server) 28 | - Next - (Routing, SSR, Hot Module Reloading, Code Splitting, Build tool uses Webpack) 29 | - TypeScript - (Static Types) 30 | - Webpack - (Module Bundler) 31 | - PostCSS - (CSS Processing) 32 | - Node.js - (Web Server) 33 | - Express - (Web App Server) 34 | - Passport - (Authentication) 35 | - ESLint - (Coding Best Practices/Code Highlighting) 36 | - Jest - (Tests) 37 | - Docker - (Container Deployment) 38 | 39 | - Optional - Yarn or Pnpm Package Manager - (Better Dependencies) 40 | 41 | ### Benefits 42 | 43 | **Front End** 44 | 45 | - Declarative UI - (`react`) 46 | - Static Typing (`typescript`) 47 | - GraphQL Fragment Colocation - (`@apollo/client`) 48 | - Prefetch Page Assets - (`next`) 49 | 50 | **Server** 51 | 52 | - Universal JS - (`node` & `express`) 53 | - Declarative GraphQL Schema - (`apollo-server`) 54 | - GraphQL Query Batching - (`apollo-server-express`) 55 | - GraphQL Stored Queries - (`apollo-server-express`) 56 | - Easy GraphiQL Include - (`apollo-server-express`) 57 | - Local Authentication Strategy - (`passport`) 58 | - Server Side Rendering - (`next`) 59 | - Code Splitting - (`next`) 60 | - Build to Static Website - (`next`) 61 | - Container Based Runtime - (`docker`) 62 | 63 | **Dev/Test** 64 | 65 | - Hot Module Reloading - (`next`) 66 | - Snapshot Testing - (`jest`) 67 | - GraphQL Playground - (`apollo-server-express`) 68 | - Faster Package Install - (`pnpm`/`yarn`) 69 | - JS/TS Best Practices - (`eslint`) 70 | 71 | ### Architecture Overview 72 | 73 |

74 | Hacker News Clone Architecture Overview 75 |

76 | 77 | `server.ts` is the entry point. It uses Express and passes requests to Next. Next SSR renders the pages using `getServerSideProps()` hook from Apollo helper. Therefore the app makes GraphQL requests on the client or server. 78 | 79 | When the client loads the page it preloads next pages code from any ``. When the client navigates to the next page it only needs to make one GraphQL query to render. _Great!_ 80 | 81 | See more: Next.js, 82 | Apollo GraphQL Client 83 | 84 | GraphQL: GraphQL-Tools by Apollo 85 | or 86 | GraphQL docs 87 | 88 | ### Directory Structure 89 | 90 | Each web page has a React component in `pages`. Server code is in `server`. Shared code that runs on client or server is in `src`. Do not import from `server` or `pages` in `src` to avoid running code in the wrong environment. 91 | 92 | The project root contains config files such as TypeScript, Babel, ESLint, Docker, Flow, NPM, Yarn, Git. 93 | 94 | ## How To Start 95 | 96 | ### One Click Download & Run 97 | 98 | You can download and run the repo with one command to rule them all: 99 | 100 | `git clone https://github.com/clintonwoo/hackernews-react-graphql.git && cd hackernews-react-graphql && npm install && npm start` 101 | 102 | ### Setup 103 | 104 | Running the app in dev mode is fully featured including _hot module reloading_: 105 | 106 | `npm install` 107 | 108 | `npm start` 109 | 110 | To run in production mode: 111 | 112 | `npm run build:prod && npm run start:prod` 113 | 114 | ### Configuration 115 | 116 | The project runs out of the box with default settings (`/src/config.ts`). You can include a .env file in your project root to configure settings (this is the '_dotenv_' npm package). The _.env_ file is included in _.gitignore_. 117 | 118 | ## How To Test 119 | 120 | ### Jest 121 | 122 | `npm test` 123 | 124 | This project uses Jest and can do snapshot testing of React components. Whenever a component is changed, please update the snapshots using `npm test -- -u` or `npx jest --updateSnapshot`. 125 | 126 | ## How To Build For Deployment 127 | 128 | `npm run build:prod`: NextJS app with entry point `server.ts` that uses Node.js/Express. Uses TypeScript compiler to transpile project src to build. 129 | 130 | OR 131 | 132 | `npm run build-docker` 133 | Docker Container: Builds a docker container using Dockerfile. 134 | 135 | #### Static Website (Optional) 136 | 137 | NextJS lets us make a powerful static website but you need to consider if you need server side rendering. 138 | 139 | `npm run build-static-website`: Builds static website to `/build/static`. Use a static web server _eg._ NGINX/Github Pages. 140 | 141 | ## Contributing 142 | 143 | Pull requests are welcome. File an issue for ideas, conversation or feedback. 144 | 145 | ### Community 146 | 147 | After you ★Star this project, follow [@ClintonDAnnolfo](https://twitter.com/clintondannolfo) on Twitter. 148 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // Next.JS expects babel.config.js in app root, we want /pages in /src folder so we have it here 2 | module.exports = (api) => { 3 | api.cache(false); 4 | 5 | return { 6 | presets: ['next/babel'], 7 | plugins: [], 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /docs/HN-Clone-Architecture-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clintonwoo/hackernews-react-graphql/6875c7233c2e0d82a910f8427c8d230f0858d048/docs/HN-Clone-Architecture-overview.png -------------------------------------------------------------------------------- /docs/HN-Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clintonwoo/hackernews-react-graphql/6875c7233c2e0d82a910f8427c8d230f0858d048/docs/HN-Demo.gif -------------------------------------------------------------------------------- /docs/HN-Demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clintonwoo/hackernews-react-graphql/6875c7233c2e0d82a910f8427c8d230f0858d048/docs/HN-Demo.jpg -------------------------------------------------------------------------------- /docs/HN-Demo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clintonwoo/hackernews-react-graphql/6875c7233c2e0d82a910f8427c8d230f0858d048/docs/HN-Demo.psd -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ### Directory Structure 2 | 3 | *docs* - Contains extra documentation. -------------------------------------------------------------------------------- /docs/seal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clintonwoo/hackernews-react-graphql/6875c7233c2e0d82a910f8427c8d230f0858d048/docs/seal.jpg -------------------------------------------------------------------------------- /healthcheck.js: -------------------------------------------------------------------------------- 1 | // This file is used to run health checks with Node in the Docker container 2 | 3 | var http = require('http'); 4 | 5 | var options = { 6 | host: 'localhost', 7 | port: '3000', 8 | timeout: 2000, 9 | }; 10 | 11 | var request = http.request(options, (res) => { 12 | console.log(`STATUS: ${res.statusCode}`); 13 | 14 | if (res.statusCode == 200) { 15 | process.exit(0); 16 | } else { 17 | process.exit(1); 18 | } 19 | }); 20 | 21 | request.on('error', function (err) { 22 | console.log('ERROR'); 23 | process.exit(1); 24 | }); 25 | 26 | request.end(); 27 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | // Fail tests on any warning 2 | // console.error = (message) => { 3 | // throw new Error(message); 4 | // }; 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | setupFilesAfterEnv: ['/jest-setup.js'], 5 | transform: { '^.+\\.tsx?$': 'ts-jest' }, 6 | testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec).ts?(x)'], 7 | verbose: true, 8 | globals: { 9 | 'ts-jest': { 10 | tsconfig: 'tsconfig-server.json', 11 | }, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | future: { 3 | webpack5: true, 4 | }, 5 | webpack: (config) => { 6 | // Perform customizations to webpack config 7 | 8 | return config; 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["server"], 3 | "exec": "ts-node --log-error --project tsconfig-server.json server/server.ts", 4 | "ext": "js ts tsx" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackernews", 3 | "version": "0.9.0", 4 | "description": "A hacker news clone built from the ground up to demonstrate React and GraphQL", 5 | "engines": { 6 | "node": ">=14.16.0" 7 | }, 8 | "scripts": { 9 | "clean": "rm -rf dist && rm -rf ./.next", 10 | "build": "npm run clean && next build && tsc --project tsconfig-server.json", 11 | "build:docker": "docker build --tag 'clintonwoo/hackernews-react-graphql:latest' --rm . && docker run --rm -p 80:3000 --name hackernews-react-graphql clintonwoo/hackernews-react-graphql", 12 | "build:prod": "cross-env NODE_ENV=production npm run build", 13 | "build:static-website": "rm -rf build/static && cross-env NODE_ENV=production next build && next export -o build/static", 14 | "debug": "cross-env DEBUG=app:* npm start", 15 | "debug:all": "cross-env DEBUG=* npm start", 16 | "debug:inspect": "cross-env DEBUG=app* TS_NODE_COMPILER_OPTIONS={\\\"module\\\":\\\"commonjs\\\"} node --inspect -r ts-node/register server/server.ts --project tsconfig-server.json", 17 | "debug:inspect-prod": "cross-env DEBUG=app* NODE_ENV=production node --inspect dist/server/server.js", 18 | "lint": "eslint \"src/**/*.ts?(x)\" --ext .js,.jsx,.ts,.tsx", 19 | "prettier:check": "prettier --check .", 20 | "prettier:format": "prettier --write .", 21 | "test": "jest --runInBand --config jest.config.js", 22 | "tsc:check": "tsc --noEmit", 23 | "start": "nodemon", 24 | "start:prod": "cross-env NODE_ENV=production node dist/server/server.js" 25 | }, 26 | "author": "Clinton D'Annolfo", 27 | "license": "Apache-2.0", 28 | "keywords": [ 29 | "hacker-news", 30 | "clone", 31 | "react", 32 | "graphql" 33 | ], 34 | "dependencies": { 35 | "@apollo/client": "3.5.7", 36 | "@graphql-tools/utils": "8.6.1", 37 | "apollo-server-express": "3.6.1", 38 | "body-parser": "1.19.1", 39 | "cookie": "0.4.1", 40 | "cookie-parser": "1.4.6", 41 | "cross-env": "7.0.3", 42 | "debug": "4.3.3", 43 | "dotenv": "12.0.3", 44 | "express": "4.17.2", 45 | "express-session": "1.17.2", 46 | "firebase": "9.6.3", 47 | "graphql": "16.2.0", 48 | "graphql-tag": "2.12.6", 49 | "isomorphic-unfetch": "3.1.0", 50 | "lru-cache": "6.0.0", 51 | "next": "12.0.8", 52 | "passport": "0.5.2", 53 | "passport-local": "1.0.0", 54 | "react": "17.0.2", 55 | "react-apollo": "3.1.5", 56 | "react-dom": "17.0.2", 57 | "react-render-html": "0.6.0", 58 | "url": "0.11.0" 59 | }, 60 | "devDependencies": { 61 | "@testing-library/jest-dom": "5.16.1", 62 | "@testing-library/react": "12.1.2", 63 | "@types/async": "3.2.12", 64 | "@types/body-parser": "1.19.2", 65 | "@types/cookie": "0.4.1", 66 | "@types/cookie-parser": "1.4.2", 67 | "@types/debug": "4.1.7", 68 | "@types/express": "4.17.13", 69 | "@types/express-session": "1.17.4", 70 | "@types/isomorphic-fetch": "0.0.35", 71 | "@types/jest": "27.4.0", 72 | "@types/lru-cache": "5.1.1", 73 | "@types/node": "17.0.8", 74 | "@types/passport": "1.0.7", 75 | "@types/passport-local": "1.0.34", 76 | "@types/react": "17.0.38", 77 | "@typescript-eslint/eslint-plugin": "5.9.1", 78 | "@typescript-eslint/eslint-plugin-tslint": "5.9.1", 79 | "@typescript-eslint/parser": "5.9.1", 80 | "babel-loader": "8.2.3", 81 | "babel-plugin-transform-define": "2.0.1", 82 | "eslint": "8.6.0", 83 | "eslint-config-prettier": "8.3.0", 84 | "eslint-plugin-import": "2.25.4", 85 | "eslint-plugin-jsx-a11y": "6.5.1", 86 | "eslint-plugin-prettier": "4.0.0", 87 | "eslint-plugin-react": "7.28.0", 88 | "jest": "27.4.7", 89 | "mockdate": "3.0.5", 90 | "nodemon": "2.0.15", 91 | "postcss": "8.4.5", 92 | "postcss-preset-env": "7.2.3", 93 | "prettier": "2.5.1", 94 | "react-test-renderer": "17.0.2", 95 | "rimraf": "3.0.2", 96 | "ts-jest": "27.1.3", 97 | "ts-node": "10.4.0", 98 | "tslint-config-prettier": "1.18.0", 99 | "typescript": "4.5.4" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /pages/active.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { NewsFeedView } from '../src/components/news-feed'; 4 | import { sampleData } from '../src/data/sample-data'; 5 | import { withDataAndRouter } from '../src/helpers/with-data'; 6 | import { MainLayout } from '../src/layouts/main-layout'; 7 | 8 | export function ActivePage(props): JSX.Element { 9 | const { router } = props; 10 | const pageNumber = (router.query && +router.query.p) || 0; 11 | 12 | return ( 13 | 14 | 20 | 21 | ); 22 | } 23 | 24 | export default withDataAndRouter(ActivePage); 25 | -------------------------------------------------------------------------------- /pages/ask.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import * as React from 'react'; 3 | import { useQuery } from '@apollo/client'; 4 | 5 | import { NewsFeed, newsFeedNewsItemFragment } from '../src/components/news-feed'; 6 | import { FeedType } from '../src/data/models'; 7 | import { withDataAndRouter } from '../src/helpers/with-data'; 8 | import { MainLayout } from '../src/layouts/main-layout'; 9 | import { POSTS_PER_PAGE } from '../src/config'; 10 | 11 | const query = gql` 12 | query topNewsItems($type: FeedType!, $first: Int!, $skip: Int!) { 13 | feed(type: $type, first: $first, skip: $skip) { 14 | ...NewsFeed 15 | } 16 | } 17 | ${newsFeedNewsItemFragment} 18 | `; 19 | 20 | export interface IAskPageProps { 21 | options: { 22 | currentUrl: string; 23 | first: number; 24 | skip: number; 25 | }; 26 | } 27 | 28 | export function AskPage(props): JSX.Element { 29 | const { router } = props; 30 | const pageNumber = (router.query && +router.query.p) || 0; 31 | 32 | const first = POSTS_PER_PAGE; 33 | const skip = POSTS_PER_PAGE * pageNumber; 34 | 35 | const { data } = useQuery(query, { variables: { first, skip, type: FeedType.ASK } }); 36 | 37 | return ( 38 | 39 | 40 | 41 | ); 42 | } 43 | 44 | export default withDataAndRouter(AskPage); 45 | -------------------------------------------------------------------------------- /pages/best.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import * as React from 'react'; 3 | import { useQuery } from '@apollo/client'; 4 | 5 | import { NewsFeed, newsFeedNewsItemFragment } from '../src/components/news-feed'; 6 | import { FeedType } from '../src/data/models'; 7 | import { withDataAndRouter } from '../src/helpers/with-data'; 8 | import { MainLayout } from '../src/layouts/main-layout'; 9 | import { POSTS_PER_PAGE } from '../src/config'; 10 | 11 | const query = gql` 12 | query bestNewsItems($type: FeedType!, $first: Int!, $skip: Int!) { 13 | feed(type: $type, first: $first, skip: $skip) { 14 | ...NewsFeed 15 | } 16 | } 17 | ${newsFeedNewsItemFragment} 18 | `; 19 | 20 | export interface IBestNewsFeedProps { 21 | options: { 22 | currentUrl: string; 23 | first: number; 24 | skip: number; 25 | }; 26 | } 27 | 28 | export function BestPage(props): JSX.Element { 29 | const { router } = props; 30 | 31 | const pageNumber = (router.query && +router.query.p) || 0; 32 | 33 | const first = POSTS_PER_PAGE; 34 | const skip = POSTS_PER_PAGE * pageNumber; 35 | 36 | const { data } = useQuery(query, { variables: { first, skip, type: FeedType.BEST } }); 37 | 38 | return ( 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default withDataAndRouter(BestPage); 46 | -------------------------------------------------------------------------------- /pages/bestcomments.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { NewsFeedView } from '../src/components/news-feed'; 4 | import { sampleData } from '../src/data/sample-data'; 5 | import { withDataAndRouter } from '../src/helpers/with-data'; 6 | import { MainLayout } from '../src/layouts/main-layout'; 7 | 8 | export function BestCommentsPage(props): JSX.Element { 9 | const { router } = props; 10 | 11 | const pageNumber = (router.query && +router.query.p) || 0; 12 | 13 | return ( 14 | 15 | 21 | 22 | ); 23 | } 24 | 25 | export default withDataAndRouter(BestCommentsPage); 26 | -------------------------------------------------------------------------------- /pages/bookmarklet.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import * as React from 'react'; 3 | 4 | import { NoticeLayout } from '../src/layouts/notice-layout'; 5 | 6 | export function BookmarkletPage(props): JSX.Element { 7 | return ( 8 | 9 | Bookmarklet 10 |
11 |
12 |
13 |

14 | Thanks to Phil Kast for writing this bookmarklet for submitting links to{' '} 15 | 16 | Hacker News 17 | 18 | . When you click on the bookmarklet, it will submit the page you're on. To install, 19 | drag this link to your browser toolbar: 20 |
21 |
22 |

23 | 31 |
32 |
33 | 34 | 35 | 36 | 38 | 39 |
37 |
40 |

41 | 42 |
43 |
44 |
45 |

46 |
47 |
48 | ); 49 | } 50 | 51 | export default BookmarkletPage; 52 | -------------------------------------------------------------------------------- /pages/changepw.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { withDataAndRouter } from '../src/helpers/with-data'; 4 | import { MainLayout } from '../src/layouts/main-layout'; 5 | 6 | export function ChangePasswordPage(props): JSX.Element { 7 | const { router } = props; 8 | 9 | return ( 10 | 11 |

Change PW

12 |
13 | ); 14 | } 15 | 16 | export default withDataAndRouter(ChangePasswordPage); 17 | -------------------------------------------------------------------------------- /pages/favorites.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import * as React from 'react'; 3 | import { useQuery } from '@apollo/client'; 4 | 5 | import { NewsFeed, newsFeedNewsItemFragment } from '../src/components/news-feed'; 6 | import { withDataAndRouter } from '../src/helpers/with-data'; 7 | import { MainLayout } from '../src/layouts/main-layout'; 8 | import { FeedType } from '../src/data/models'; 9 | import { POSTS_PER_PAGE } from '../src/config'; 10 | 11 | const query = gql` 12 | query NewestFeed($type: FeedType!, $first: Int!, $skip: Int!) { 13 | feed(type: $type, first: $first, skip: $skip) { 14 | ...NewsFeed 15 | } 16 | } 17 | ${newsFeedNewsItemFragment} 18 | `; 19 | 20 | export interface INewestNewsFeedOwnProps { 21 | options: { 22 | currentUrl: string; 23 | first: number; 24 | skip: number; 25 | }; 26 | } 27 | 28 | export function FavoritesPage(props): JSX.Element { 29 | const { router } = props; 30 | 31 | const pageNumber = (router.query && +router.query.p) || 0; 32 | 33 | const first = POSTS_PER_PAGE; 34 | const skip = POSTS_PER_PAGE * pageNumber; 35 | 36 | const { data } = useQuery(query, { variables: { first, skip, type: FeedType.NEW } }); 37 | 38 | return ( 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default withDataAndRouter(FavoritesPage); 46 | -------------------------------------------------------------------------------- /pages/forgot.tsx: -------------------------------------------------------------------------------- 1 | import { NextRouter } from 'next/router'; 2 | import React, { useState } from 'react'; 3 | 4 | import { withDataAndRouter } from '../src/helpers/with-data'; 5 | import { BlankLayout } from '../src/layouts/blank-layout'; 6 | 7 | export interface IForgotPageProps { 8 | router: NextRouter; // { how: UserLoginErrorCode } 9 | } 10 | 11 | function ForgotPage(props: IForgotPageProps): JSX.Element { 12 | const [username, setUsername] = useState(''); 13 | 14 | return ( 15 | 16 | Reset your password 17 |
18 |
19 |
20 | 21 | 22 | username:{' '} 23 | setUsername(e.target.value)} /> 24 |
25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | 32 | export default withDataAndRouter(ForgotPage); 33 | -------------------------------------------------------------------------------- /pages/formatdoc.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { MainLayout } from '../src/layouts/main-layout'; 4 | import { withDataAndRouter } from '../src/helpers/with-data'; 5 | 6 | export function FormatDocPage(props): JSX.Element { 7 | const { router } = props; 8 | 9 | return ( 10 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 39 | 40 | 41 |
24 | Blank lines separate paragraphs. 25 |

26 | Text after a blank line that is indented by two or more spaces is reproduced 27 | verbatim. (This is intended for code.) 28 |

29 |

30 | Text surrounded by asterisks is italicized, if the character after the first 31 | asterisk isn't whitespace. 32 |

33 |

34 | Urls become links, except in the text field of a submission. 35 |
36 |
37 |

38 |
42 |
43 |
44 |
45 |
46 | 47 | 48 |
49 | ); 50 | } 51 | 52 | export default withDataAndRouter(FormatDocPage); 53 | -------------------------------------------------------------------------------- /pages/front.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { withDataAndRouter } from '../src/helpers/with-data'; 4 | import { MainLayout } from '../src/layouts/main-layout'; 5 | 6 | export function FrontPage(props): JSX.Element { 7 | const { router } = props; 8 | return ( 9 | 10 | total 11 | 12 | ); 13 | } 14 | 15 | export default withDataAndRouter(FrontPage); 16 | -------------------------------------------------------------------------------- /pages/hidden.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { NewsFeedView } from '../src/components/news-feed'; 4 | import { sampleData } from '../src/data/sample-data'; 5 | import { withDataAndRouter } from '../src/helpers/with-data'; 6 | import { MainLayout } from '../src/layouts/main-layout'; 7 | 8 | export function HiddenPage(props): JSX.Element { 9 | const { router } = props; 10 | 11 | const pageNumber = (router.query && +router.query.p) || 0; 12 | 13 | return ( 14 | 15 | 21 | 22 | ); 23 | } 24 | 25 | export default withDataAndRouter(HiddenPage); 26 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import * as React from 'react'; 3 | import { useQuery } from '@apollo/client'; 4 | 5 | import { NewsFeed, newsFeedNewsItemFragment } from '../src/components/news-feed'; 6 | import { withDataAndRouter } from '../src/helpers/with-data'; 7 | import { MainLayout } from '../src/layouts/main-layout'; 8 | import { FeedType } from '../src/data/models'; 9 | import { POSTS_PER_PAGE } from '../src/config'; 10 | 11 | const query = gql` 12 | query topNewsItems($type: FeedType!, $first: Int!, $skip: Int!) { 13 | feed(type: $type, first: $first, skip: $skip) { 14 | ...NewsFeed 15 | } 16 | } 17 | ${newsFeedNewsItemFragment} 18 | `; 19 | 20 | export interface ITopNewsFeedProps { 21 | options: { 22 | currentUrl: string; 23 | first: number; 24 | skip: number; 25 | }; 26 | } 27 | 28 | export function IndexPage(props): JSX.Element { 29 | const { router } = props; 30 | 31 | const pageNumber = (router.query && +router.query.p) || 0; 32 | 33 | const first = POSTS_PER_PAGE; 34 | const skip = POSTS_PER_PAGE * pageNumber; 35 | 36 | const { data } = useQuery(query, { variables: { first, skip, type: FeedType.TOP } }); 37 | 38 | return ( 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default withDataAndRouter(IndexPage); 46 | -------------------------------------------------------------------------------- /pages/item.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import * as React from 'react'; 3 | import { useQuery } from '@apollo/client'; 4 | 5 | import { commentsFragment } from '../src/components/comments'; 6 | import { newsDetailNewsItemFragment } from '../src/components/news-detail'; 7 | import { NewsItemWithComments } from '../src/components/news-item-with-comments'; 8 | import { newsTitleFragment } from '../src/components/news-title'; 9 | import { NewsItemModel } from '../src/data/models'; 10 | import { withDataAndRouter } from '../src/helpers/with-data'; 11 | import { MainLayout } from '../src/layouts/main-layout'; 12 | 13 | export interface INewsItemWithCommentsQuery { 14 | newsItem: NewsItemModel; 15 | } 16 | 17 | const newsItemWithCommentsQuery = gql` 18 | query NewsItemWithComments($id: Int!) { 19 | newsItem(id: $id) { 20 | id 21 | comments { 22 | ...Comments 23 | } 24 | ...NewsTitle 25 | ...NewsDetail 26 | } 27 | } 28 | ${newsTitleFragment} 29 | ${newsDetailNewsItemFragment} 30 | ${commentsFragment} 31 | `; 32 | 33 | export interface INewsItemWithCommentsWithGraphQLOwnProps { 34 | id: number; 35 | } 36 | 37 | export function ItemPage(props): JSX.Element { 38 | const { router } = props; 39 | 40 | const { data } = useQuery(newsItemWithCommentsQuery, { 41 | variables: { id: (router.query && +router.query.id) || 0 }, 42 | }); 43 | 44 | return ( 45 | 46 | 47 | 48 | ); 49 | } 50 | 51 | export default withDataAndRouter(ItemPage); 52 | -------------------------------------------------------------------------------- /pages/jobs.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/client'; 2 | import gql from 'graphql-tag'; 3 | import * as React from 'react'; 4 | 5 | import { NewsFeed, newsFeedNewsItemFragment } from '../src/components/news-feed'; 6 | import { withDataAndRouter } from '../src/helpers/with-data'; 7 | import { MainLayout } from '../src/layouts/main-layout'; 8 | import { FeedType } from '../src/data/models'; 9 | import { POSTS_PER_PAGE } from '../src/config'; 10 | 11 | const query = gql` 12 | query topNewsItems($type: FeedType!, $first: Int!, $skip: Int!) { 13 | feed(type: $type, first: $first, skip: $skip) { 14 | ...NewsFeed 15 | } 16 | } 17 | ${newsFeedNewsItemFragment} 18 | `; 19 | 20 | export interface IJobsPageOwnProps { 21 | options: { 22 | currentUrl: string; 23 | first: number; 24 | isJobListing: boolean; 25 | isRankVisible: boolean; 26 | isUpvoteVisible: boolean; 27 | notice: JSX.Element; 28 | skip: number; 29 | }; 30 | } 31 | 32 | export function JobsPage(props): JSX.Element { 33 | const { router } = props; 34 | 35 | const pageNumber = (router.query && +router.query.p) || 0; 36 | 37 | const first = POSTS_PER_PAGE; 38 | const skip = POSTS_PER_PAGE * pageNumber; 39 | 40 | const { data } = useQuery(query, { variables: { first, skip, type: FeedType.JOB } }); 41 | 42 | return ( 43 | 44 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | These are jobs at startups that were funded by Y Combinator. You can also get a job 61 | at a YC startup through{' '} 62 | 63 | Triplebyte 64 | 65 | . 66 | 67 | 68 | 69 | 70 | } 71 | skip={POSTS_PER_PAGE * pageNumber} 72 | /> 73 | 74 | ); 75 | } 76 | 77 | export default withDataAndRouter(JobsPage); 78 | -------------------------------------------------------------------------------- /pages/leaders.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { withDataAndRouter } from '../src/helpers/with-data'; 4 | import { MainLayout } from '../src/layouts/main-layout'; 5 | 6 | export function LeadersPage(props): JSX.Element { 7 | const { router } = props; 8 | 9 | return ( 10 | 11 | total 12 | 13 | ); 14 | } 15 | 16 | export default withDataAndRouter(LeadersPage); 17 | -------------------------------------------------------------------------------- /pages/lists.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import * as React from 'react'; 3 | 4 | import { withDataAndRouter } from '../src/helpers/with-data'; 5 | import { MainLayout } from '../src/layouts/main-layout'; 6 | 7 | export function ListsPage(props): JSX.Element { 8 | const { router } = props; 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 30 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 73 | 74 | 75 | 76 |
18 | 19 | leaders 20 | 21 | Users with most karma.
26 | 27 | front 28 | 29 | 31 | Front page submissions for a given day (e.g.{' '} 32 | 2016-06-20), ordered by time spent there. 33 |
37 | 38 | best 39 | 40 | Highest-voted recent links.
45 | 46 | active 47 | 48 | Most active current discussions.
53 | 54 | bestcomments 55 | 56 | Highest-voted recent comments.
61 | 62 | noobstories 63 | 64 | Submissions from new accounts.
69 | 70 | noobcomments 71 | 72 | Comments from new accounts.
77 | 78 | 79 |
80 | ); 81 | } 82 | 83 | export default withDataAndRouter(ListsPage); 84 | -------------------------------------------------------------------------------- /pages/login.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/client'; 2 | import Link from 'next/link'; 3 | import Router, { NextRouter } from 'next/router'; 4 | import React, { useState } from 'react'; 5 | 6 | import { IMeQuery, ME_QUERY } from '../src/data/queries/me-query'; 7 | import { validateNewUser } from '../src/data/validation/user'; 8 | import { 9 | getErrorMessageForLoginErrorCode, 10 | UserLoginErrorCode, 11 | } from '../src/helpers/user-login-error-code'; 12 | import { withDataAndRouter } from '../src/helpers/with-data'; 13 | import { BlankLayout } from '../src/layouts/blank-layout'; 14 | 15 | export interface ILoginPageProps { 16 | router?: NextRouter; 17 | } 18 | 19 | function LoginPage(props: ILoginPageProps): JSX.Element { 20 | const { data } = useQuery(ME_QUERY); 21 | 22 | const { router } = props; 23 | 24 | const routerQuery = router!.query as { how: UserLoginErrorCode; goto: string }; 25 | const message = routerQuery.how ? getErrorMessageForLoginErrorCode(routerQuery.how) : undefined; 26 | 27 | const [loginId, setLoginId] = useState(''); 28 | const [loginPassword, setLoginPassword] = useState(''); 29 | const [registerId, setRegisterId] = useState(''); 30 | const [registerPassword, setRegisterPassword] = useState(''); 31 | const [validationMessage, setValidationMessage] = useState(''); 32 | 33 | const validateLogin = (e: React.FormEvent): void => { 34 | if (data?.me) { 35 | e.preventDefault(); 36 | Router.push('/login?how=loggedin'); 37 | } else { 38 | try { 39 | validateNewUser({ id: loginId, password: loginPassword }); 40 | } catch (err: any) { 41 | e.preventDefault(); 42 | setValidationMessage(err.message); 43 | } 44 | } 45 | }; 46 | 47 | const validateRegister = (e: React.FormEvent): void => { 48 | if (data?.me) { 49 | e.preventDefault(); 50 | Router.push('/login?how=loggedin'); 51 | } else { 52 | try { 53 | validateNewUser({ id: registerId, password: registerPassword }); 54 | } catch (err: any) { 55 | e.preventDefault(); 56 | setValidationMessage(err.message); 57 | } 58 | } 59 | }; 60 | 61 | return ( 62 | 63 | {message &&

{message}

} 64 | {validationMessage &&

{validationMessage}

} 65 | Login 66 |
67 |
68 |
validateLogin(e)} 72 | style={{ marginBottom: '1em' }} 73 | > 74 | 75 | 76 | 77 | 78 | 79 | 90 | 91 | 92 | 93 | 101 | 102 | 103 |
username: 80 | setLoginId(e.target.value)} 85 | size={20} 86 | spellCheck={false} 87 | type="text" 88 | /> 89 |
password: 94 | setLoginPassword(e.target.value)} 98 | size={20} 99 | /> 100 |
104 |
105 | 106 |
107 | 108 | Forgot your password? 109 | 110 |
111 |
112 | Create Account 113 |
114 |
115 |
validateRegister(e)} 119 | style={{ marginBottom: '1em' }} 120 | > 121 | 122 | 123 | 124 | 125 | 136 | 137 | 138 | 139 | 147 | 148 | 149 |
username: 126 | setRegisterId(e.target.value)} 130 | size={20} 131 | autoCorrect="off" 132 | spellCheck={false} 133 | autoCapitalize="off" 134 | /> 135 |
password: 140 | setRegisterPassword(e.target.value)} 144 | size={20} 145 | /> 146 |
150 |
151 | 152 |
153 |
154 | ); 155 | } 156 | 157 | export default withDataAndRouter(LoginPage); 158 | -------------------------------------------------------------------------------- /pages/newcomments.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { NewsFeedView } from '../src/components/news-feed'; 4 | import { sampleData } from '../src/data/sample-data'; 5 | import { withDataAndRouter } from '../src/helpers/with-data'; 6 | import { MainLayout } from '../src/layouts/main-layout'; 7 | 8 | export function NewCommentsPage(props): JSX.Element { 9 | const { router } = props; 10 | 11 | const pageNumber = (router.query && +router.query.p) || 0; 12 | 13 | return ( 14 | 15 | 21 | 22 | ); 23 | } 24 | 25 | export default withDataAndRouter(NewCommentsPage); 26 | -------------------------------------------------------------------------------- /pages/newest.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from '@apollo/client'; 2 | import gql from 'graphql-tag'; 3 | import * as React from 'react'; 4 | 5 | import { NewsFeed, newsFeedNewsItemFragment } from '../src/components/news-feed'; 6 | import { withDataAndRouter } from '../src/helpers/with-data'; 7 | import { MainLayout } from '../src/layouts/main-layout'; 8 | import { FeedType } from '../src/data/models'; 9 | import { POSTS_PER_PAGE } from '../src/config'; 10 | 11 | const query = gql` 12 | query NewestFeed($type: FeedType!, $first: Int!, $skip: Int!) { 13 | feed(type: $type, first: $first, skip: $skip) { 14 | ...NewsFeed 15 | } 16 | } 17 | ${newsFeedNewsItemFragment} 18 | `; 19 | 20 | export interface INewestNewsFeedProps { 21 | options: { 22 | currentUrl: string; 23 | first: number; 24 | skip: number; 25 | }; 26 | } 27 | 28 | export function NewestPage(props): JSX.Element { 29 | const { router } = props; 30 | 31 | const pageNumber = (router.query && +router.query.p) || 0; 32 | 33 | const first = POSTS_PER_PAGE; 34 | const skip = POSTS_PER_PAGE * pageNumber; 35 | 36 | const { data } = useQuery(query, { variables: { first, skip, type: FeedType.NEW } }); 37 | 38 | return ( 39 | 40 | 41 | 42 | ); 43 | } 44 | 45 | export default withDataAndRouter(NewestPage); 46 | -------------------------------------------------------------------------------- /pages/newpoll.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { NewsFeedView } from '../src/components/news-feed'; 4 | import { sampleData } from '../src/data/sample-data'; 5 | import { withDataAndRouter } from '../src/helpers/with-data'; 6 | import { MainLayout } from '../src/layouts/main-layout'; 7 | 8 | export function NewPollPage(props): JSX.Element { 9 | const { router } = props; 10 | 11 | const pageNumber = (router.query && +router.query.p) || 0; 12 | 13 | return ( 14 | 15 | 21 | 22 | ); 23 | } 24 | 25 | export default withDataAndRouter(NewPollPage); 26 | -------------------------------------------------------------------------------- /pages/newsguidelines.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import * as React from 'react'; 3 | 4 | import { NoticeLayout } from '../src/layouts/notice-layout'; 5 | 6 | export function NewsGuidelinesPage(): JSX.Element { 7 | return ( 8 | 9 | Hacker News Guidelines 10 |
11 |
12 | What to Submit 13 |

14 | On-Topic: Anything that good hackers would find interesting. That includes more than hacking 15 | and startups. If you had to reduce it to a sentence, the answer might be: anything that 16 | gratifies one's intellectual curiosity. 17 |

18 |

19 | Off-Topic: Most stories about politics, or crime, or sports, unless they're evidence of 20 | some interesting new phenomenon. Ideological or political battle or talking points. Videos 21 | of pratfalls or disasters, or cute animal pictures. If they'd cover it on TV news, 22 | it's probably off-topic. 23 |

24 |

25 | In Submissions 26 |

27 |

28 | Please don't do things to make titles stand out, like using uppercase or exclamation 29 | points, or adding a parenthetical remark saying how great an article is. It's implicit 30 | in submitting something that you think it's important. 31 |

32 |

33 | If you submit a link to a video or pdf, please warn us by appending [video] or [pdf] to the 34 | title. 35 |

36 |

37 | Please submit the original source. If a post reports on something found on another site, 38 | submit the latter. 39 |

40 |

41 | If the original title includes the name of the site, please take it out, because the site 42 | name will be displayed after the link. 43 |

44 |

45 | If the original title begins with a number or number + gratuitous adjective, we'd 46 | appreciate it if you'd crop it. E.g. translate "10 Ways To Do X" to "How 47 | To Do X," and "14 Amazing Ys" to "Ys." Exception: when the number 48 | is meaningful, e.g. "The 5 Platonic Solids." 49 |

50 |

Otherwise please use the original title, unless it is misleading or linkbait.

51 |

52 | Please don't post on HN to ask or tell us something. Instead, please send it to 53 | hn@ycombinator.com. Similarly, please don't use HN posts to ask YC-funded companies 54 | questions that you could ask by emailing them. 55 |

56 |

57 | Please don't submit so many links at once that the new page is dominated by your 58 | submissions. 59 |

60 |

61 | In Comments 62 |

63 |

64 | Be civil. Don't say things you wouldn't say face-to-face. Don't be snarky. 65 | Comments should get more civil and substantive, not less, as a topic gets more divisive. 66 |

67 |

68 | When disagreeing, please reply to the argument instead of calling names. "That is 69 | idiotic; 1 + 1 is 2, not 3" can be shortened to "1 + 1 is 2, not 3." 70 |

71 |

72 | Please respond to the strongest plausible interpretation of what someone says, not a weaker 73 | one that's easier to criticize. 74 |

75 |

76 | Eschew flamebait. Don't introduce flamewar topics unless you have something genuinely 77 | new to say. Avoid unrelated controversies and generic tangents. 78 |

79 |

80 | Please don't insinuate that someone hasn't read an article. "Did you even read 81 | the article? It mentions that" can be shortened to "The article mentions 82 | that." 83 |

84 |

85 | Please don't use uppercase for emphasis. If you want to emphasize a word or phrase, put 86 | *asterisks* around it and it will get italicized. 87 |

88 |

89 | Please don't accuse others of astroturfing or shillage. Email us instead and we'll 90 | look into it. 91 |

92 |

93 | Please don't complain that a submission is inappropriate. If a story is spam or 94 | off-topic, flag it. Don't feed egregious comments by replying;{' '} 95 | 96 | flag 97 | {' '} 98 | them instead. When you flag something, please don't also comment that you did. 99 |

100 |

101 | Please don't comment about the voting on comments. It never does any good, and it makes 102 | boring reading. 103 |

104 |

105 | Throwaway accounts are ok for sensitive information, but please don't create them 106 | routinely. On HN, users need an identity that others can relate to. 107 |

108 |

109 | We ban accounts that use Hacker News primarily for political or ideological battle, 110 | regardless of which politics they favor. 111 |
112 |
113 |
114 | 115 | 116 | 117 | 119 | 120 |
118 |
121 |

122 |

123 | 124 |
125 |
126 |
127 |

128 |
129 | ); 130 | } 131 | 132 | export default NewsGuidelinesPage; 133 | -------------------------------------------------------------------------------- /pages/newswelcome.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import * as React from 'react'; 3 | 4 | import { NoticeLayout } from '../src/layouts/notice-layout'; 5 | 6 | export function NewsWelcomePage(): JSX.Element { 7 | return ( 8 | 9 | Welcome to Hacker News 10 |
11 |
12 |

13 | 14 | Hacker News 15 | {' '} 16 | is a bit different from other community sites, and we'd appreciate it if you'd take 17 | a minute to read the following as well as the{' '} 18 | 19 | official guidelines 20 | 21 | . 22 |

23 |

24 | HN is an experiment. As a rule, a community site that becomes popular will decline in 25 | quality. Our hypothesis is that this is not inevitable—that by making a conscious effort to 26 | resist decline, we can keep it from happening. 27 |

28 |

29 | Essentially there are two rules here: don't post or upvote crap links, and don't be 30 | rude or dumb in comment threads. 31 |

32 |

33 | A crap link is one that's only superficially interesting. Stories on HN don't have 34 | to be about hacking, because good hackers aren't only interested in hacking, but they do 35 | have to be deeply interesting. 36 |

37 |

38 | What does "deeply interesting" mean? It means stuff that teaches you about the 39 | world. A story about a robbery, for example, would probably not be deeply interesting. But 40 | if this robbery was a sign of some bigger, underlying trend, perhaps it could be. 41 |

42 |

43 | The worst thing to post or upvote is something that's intensely but shallowly 44 | interesting: gossip about famous people, funny or cute pictures or videos, partisan 45 | political articles, etc. If you let{' '} 46 | 47 | that sort of thing onto a news site, it will push aside the deeply interesting stuff, 48 | which tends to be quieter. 49 | 50 |

51 |

52 | The most important principle on HN, though, is to make thoughtful comments. Thoughtful in 53 | both senses: civil and substantial. 54 |

55 |

56 | The test for substance is a lot like it is for links. Does your comment teach us anything? 57 | There are two ways to do that: by pointing out some consideration that hadn't previously 58 | been mentioned, and by giving more information about the topic, perhaps from personal 59 | experience. Whereas comments like "LOL!" or worse still, "That's 60 | retarded!" teach us nothing. 61 |

62 |

63 | Empty comments can be ok if they're positive. There's nothing wrong with submitting 64 | a comment saying just "Thanks." What we especially discourage are comments that 65 | are empty and negative—comments that are mere name-calling. 66 |

67 |

68 | Which brings us to the most important principle on HN: civility. Since long before the web, 69 | the anonymity of online conversation has lured people into being much ruder than they'd 70 | be in person. So the principle here is: don't say anything you wouldn't say face to 71 | face. This doesn't mean you can't disagree. But disagree without calling names. If 72 | you're right, your argument will be more convincing without them. 73 |
74 |
75 |
76 | 77 | 78 | 79 | 81 | 82 |
80 |
83 |

84 |

85 | 86 |
87 |
88 |
89 |

90 |
91 | ); 92 | } 93 | 94 | export default NewsWelcomePage; 95 | -------------------------------------------------------------------------------- /pages/noobcomments.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { NewsFeedView } from '../src/components/news-feed'; 4 | import { sampleData } from '../src/data/sample-data'; 5 | import { withDataAndRouter } from '../src/helpers/with-data'; 6 | import { MainLayout } from '../src/layouts/main-layout'; 7 | 8 | export function NoobCommentsPage(props): JSX.Element { 9 | const { router } = props; 10 | 11 | const pageNumber = (router.query && +router.query.p) || 0; 12 | 13 | return ( 14 | 15 | 21 | 22 | ); 23 | } 24 | 25 | export default withDataAndRouter(NoobCommentsPage); 26 | -------------------------------------------------------------------------------- /pages/noobstories.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { NewsFeedView } from '../src/components/news-feed'; 4 | import { sampleData } from '../src/data/sample-data'; 5 | import { withDataAndRouter } from '../src/helpers/with-data'; 6 | import { MainLayout } from '../src/layouts/main-layout'; 7 | 8 | export function NoobStoriesPage(props): JSX.Element { 9 | const { router } = props; 10 | 11 | const pageNumber = (router.query && +router.query.p) || 0; 12 | 13 | return ( 14 | 15 | 21 | 22 | ); 23 | } 24 | 25 | export default withDataAndRouter(NoobStoriesPage); 26 | -------------------------------------------------------------------------------- /pages/reply.tsx: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import * as React from 'react'; 3 | import { useQuery } from '@apollo/client'; 4 | 5 | import { commentFragment } from '../src/components/comment'; 6 | import { withDataAndRouter } from '../src/helpers/with-data'; 7 | import { MainLayout } from '../src/layouts/main-layout'; 8 | 9 | const query = gql` 10 | query Comment($id: Int!) { 11 | comment(id: $id) { 12 | id 13 | ...Comment 14 | } 15 | } 16 | ${commentFragment} 17 | `; 18 | 19 | export interface IReplyPageProps { 20 | router; 21 | } 22 | 23 | function ReplyPage(props: IReplyPageProps): JSX.Element { 24 | const { router } = props; 25 | 26 | const { data } = useQuery(query, { 27 | variables: { id: (router.query && +router.query.id) || 0 }, 28 | }); 29 | 30 | const vote = (): void => { 31 | console.log('onclick'); 32 | }; 33 | 34 | const toggle = (): void => { 35 | console.log('toggle'); 36 | }; 37 | 38 | return ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 57 | 102 | 103 | 104 | 105 |
46 | 47 |
48 | 53 |
54 | 55 |
56 |
58 | 81 |
82 |
83 | 84 | 85 | Because the vehicle is electric, there is no need to “heat up” the brakes 86 | when descending. This is because the enormous electric engine acts as a 87 | generator and recharges the battery pack. That same energy is then used to 88 | help the vehicle travel back up the hill. Phys reports, “If all goes as 89 | planned, the electric dumper truck will even harvest more electricity while 90 | traveling downhill than it needs for the ascent. Instead of consuming fossil 91 | fuels, it would then feed surplus electricity into the grid.” 92 | 93 |

94 | Clever. It can do this because it travels uphill empty and comes downhill 95 | full. 96 | 97 |

98 |
99 | 100 |
101 |
106 | 107 |
108 | 109 | 110 | 115 |