├── .prettierignore ├── .env.development ├── src ├── react-app-env.d.ts ├── components │ ├── Diff │ │ ├── Diff.css │ │ └── Diff.tsx │ ├── Chunk │ │ ├── Chunk.css │ │ └── Chunk.tsx │ ├── Nav │ │ ├── Nav.css │ │ └── Nav.tsx │ ├── File │ │ ├── File.css │ │ └── File.tsx │ ├── Change │ │ ├── Change.css │ │ └── Change.tsx │ ├── Home │ │ ├── Home.css │ │ └── Home.tsx │ └── App │ │ └── App.tsx ├── index.css ├── index.tsx ├── move.svg └── registerServiceWorker.js ├── .env.production ├── server ├── migrations │ ├── 001_add_diffs.sql │ └── 002_add_users.sql └── index.js ├── public ├── manifest.json └── index.html ├── .gitignore ├── tsconfig.json ├── .eslintrc ├── package.json └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | REACT_APP_SERVER_URL=http://localhost:3500 2 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/components/Diff/Diff.css: -------------------------------------------------------------------------------- 1 | .diff { 2 | margin: 0 1rem 1rem; 3 | } 4 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_SERVER_URL=https://api.narrated-diffs.thomasbroadley.com 2 | -------------------------------------------------------------------------------- /server/migrations/001_add_diffs.sql: -------------------------------------------------------------------------------- 1 | create table diffs 2 | ( 3 | id uuid primary key, 4 | diff text not null default '[]' 5 | ) 6 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Narrative Diffs", 3 | "name": "Narrative Diffs", 4 | "start_url": "./index.html", 5 | "display": "standalone", 6 | "theme_color": "#333333", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /server/migrations/002_add_users.sql: -------------------------------------------------------------------------------- 1 | create table users 2 | ( 3 | id bigserial primary key, 4 | github_id bigint not null, 5 | github_username varchar(39) not null, 6 | github_token varchar(100) not null, 7 | constraint unq_github_id unique (github_id) 8 | ) 9 | -------------------------------------------------------------------------------- /src/components/Chunk/Chunk.css: -------------------------------------------------------------------------------- 1 | .chunk { 2 | margin: 1rem 0 0; 3 | } 4 | 5 | .chunk__content { 6 | display: inline-block; 7 | padding-bottom: 0.25rem; 8 | margin: 0 0 1rem; 9 | font-family: monospace, monospace; 10 | border-bottom: 1px solid #333333; 11 | } 12 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | height: 100%; 9 | font-family: sans-serif; 10 | font-size: 14px; 11 | color: #333333; 12 | } 13 | 14 | div#root { 15 | height: 100%; 16 | } 17 | 18 | * { 19 | box-sizing: border-box; 20 | } 21 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import "./index.css"; 5 | import { App } from "./components/App/App"; 6 | import registerServiceWorker from "./registerServiceWorker"; 7 | 8 | ReactDOM.render(, document.getElementById("root")); 9 | registerServiceWorker(); 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | .env 24 | -------------------------------------------------------------------------------- /src/components/Nav/Nav.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | width: 100%; 3 | height: 50px; 4 | position: sticky; 5 | top: 0; 6 | 7 | padding: 1rem; 8 | 9 | display: flex; 10 | align-items: center; 11 | 12 | background-color: white; 13 | border-bottom: 1px solid black; 14 | 15 | z-index: 100; 16 | } 17 | 18 | .nav > *:not(:last-child) { 19 | padding-right: 1rem; 20 | } 21 | 22 | .nav__horizontal-fill { 23 | flex-grow: 1; 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"] 19 | } 20 | -------------------------------------------------------------------------------- /src/components/File/File.css: -------------------------------------------------------------------------------- 1 | .file { 2 | margin: 1rem 0; 3 | padding: 1rem; 4 | border: 1px solid #333333; 5 | background-color: #ffffff; 6 | } 7 | 8 | .file__controls { 9 | display: flex; 10 | align-items: center; 11 | margin: 0 0 1rem; 12 | } 13 | 14 | .file__controls > *:not(:last-child) { 15 | margin-right: 0.5rem; 16 | } 17 | 18 | .file__user-text { 19 | margin: 0 0 1rem; 20 | } 21 | 22 | .file__drag-handle { 23 | width: 1.5rem; 24 | } 25 | 26 | .file__description { 27 | margin: 0 0 1rem; 28 | line-height: 1.5; 29 | } 30 | 31 | .file__name, 32 | .file__from-name, 33 | .file__to-name { 34 | display: inline-block; 35 | padding: 0 0.25rem; 36 | font-family: monospace, monospace; 37 | background-color: #d7d7d7; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Change/Change.css: -------------------------------------------------------------------------------- 1 | .change { 2 | display: flex; 3 | padding: 0 0.25rem; 4 | line-height: 1.5; 5 | } 6 | 7 | .change--added { 8 | background-color: #d7ffd7; 9 | } 10 | 11 | .change--deleted { 12 | background-color: #ffd7d7; 13 | } 14 | 15 | .change__addition-or-deletion, 16 | .change__line-number, 17 | .change__content { 18 | font-family: monospace, monospace; 19 | } 20 | 21 | .change__addition-or-deletion { 22 | display: inline-block; 23 | flex-shrink: 0; 24 | width: 1rem; 25 | } 26 | 27 | .change__line-number { 28 | display: inline-block; 29 | flex-shrink: 0; 30 | width: 2rem; 31 | text-align: right; 32 | } 33 | 34 | .change__content { 35 | margin-left: 1rem; 36 | white-space: pre-wrap; 37 | flex-grow: 0; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Chunk/Chunk.tsx: -------------------------------------------------------------------------------- 1 | import parseDiff from "parse-diff"; 2 | import React from "react"; 3 | 4 | import { Change } from "../Change/Change"; 5 | import "./Chunk.css"; 6 | 7 | export function Chunk({ 8 | baseKey, 9 | content, 10 | changes, 11 | }: { 12 | baseKey: string; 13 | content: string; 14 | changes: parseDiff.Change[]; 15 | }) { 16 | return ( 17 |
18 |

{content}

19 | {changes.map((change) => { 20 | const line = 21 | change.type === "normal" ? `${change.ln1}-${change.ln2}` : change.ln; 22 | const key = `${baseKey}-${change.type}-${line}`; 23 | return ; 24 | })} 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Change/Change.tsx: -------------------------------------------------------------------------------- 1 | import parseDiff from "parse-diff"; 2 | import React from "react"; 3 | import "./Change.css"; 4 | 5 | export function Change({ change }: { change: parseDiff.Change }) { 6 | const { type, content } = change; 7 | const lineNumber = change.type === "normal" ? change.ln2 : change.ln; 8 | 9 | return ( 10 |
15 |
16 | {type === "add" ? "+" : ""} 17 | {type === "del" ? "-" : ""} 18 |
19 |
{lineNumber}
20 |
{content.slice(1)}
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "env": { "browser": true, "node": true }, 4 | "plugins": ["@typescript-eslint"], 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:@typescript-eslint/recommended", 8 | "plugin:react/recommended", 9 | "plugin:import/errors", 10 | "plugin:import/warnings", 11 | "plugin:import/typescript", 12 | "prettier", 13 | "prettier/react" 14 | ], 15 | "rules": { 16 | "react/prop-types": "off", 17 | "import/order": ["error", { "newlines-between": "always" }], 18 | "import/no-default-export": "error", 19 | "@typescript-eslint/explicit-module-boundary-types": "off" 20 | }, 21 | "settings": { 22 | "react": { 23 | "version": "detect" 24 | }, 25 | "linkComponents": [{ "name": "Link", "linkAttribute": "to" }], 26 | "import/extensions": [".js", ".jsx", ".ts", ".tsx"] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Nav/Nav.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | import "./Nav.css"; 5 | 6 | const { REACT_APP_SERVER_URL } = process.env; 7 | 8 | export const Nav = ({ 9 | username, 10 | id, 11 | readOnly, 12 | }: { 13 | username?: string; 14 | id?: string; 15 | readOnly?: boolean; 16 | }) => ( 17 |
18 | {id ? ( 19 | readOnly ? ( 20 | <> 21 | Home 22 | Edit this diff 23 | 24 | ) : ( 25 | <> 26 | Home 27 | Link to this diff for reviewers 28 | 29 | ) 30 | ) : null} 31 |
32 | {username ? ( 33 | <> 34 |

Hello, {username}

35 | Logout 36 | 37 | ) : ( 38 | Login with GitHub 39 | )} 40 |
41 | ); 42 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | Narrated Diffs 16 | 17 | 18 | 19 |
20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/Home/Home.css: -------------------------------------------------------------------------------- 1 | .home { 2 | height: calc(100% - 50px); 3 | } 4 | 5 | .paste-diff { 6 | height: 100%; 7 | width: 500px; 8 | max-width: 100%; 9 | 10 | margin: auto; 11 | 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | } 16 | 17 | @media (max-width: 767px) { 18 | .paste-diff { 19 | justify-content: start; 20 | } 21 | } 22 | 23 | .paste-diff__tabs { 24 | width: 100%; 25 | display: flex; 26 | } 27 | 28 | .paste-diff__tab { 29 | height: 48px; 30 | width: 72px; 31 | 32 | font-size: 24px; 33 | 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | 38 | border: 1px solid #aaaaaa; 39 | border-radius: 5px; 40 | border-bottom: none; 41 | 42 | cursor: pointer; 43 | } 44 | 45 | .paste-diff__tab--active { 46 | background-color: #f3f3f3; 47 | } 48 | 49 | .paste-diff__tab:not(:last-child) { 50 | border-right: none; 51 | } 52 | 53 | .paste-diff__tab-body { 54 | height: 500px; 55 | max-height: 100%; 56 | 57 | padding: 0 1rem; 58 | 59 | border: 1px solid #aaaaaa; 60 | border-radius: 5px; 61 | } 62 | 63 | .paste-diff__tab-body input, 64 | .paste-diff__tab-body textarea { 65 | width: 100%; 66 | } 67 | 68 | .paste-diff__tab-body textarea { 69 | height: 200px; 70 | box-sizing: border-box; 71 | resize: none; 72 | } 73 | 74 | .paste-diff__error { 75 | color: red; 76 | } 77 | -------------------------------------------------------------------------------- /src/components/App/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Route, BrowserRouter as Router, Switch } from "react-router-dom"; 3 | 4 | import { Home } from "../Home/Home"; 5 | import { Diff } from "../Diff/Diff"; 6 | import { Nav } from "../Nav/Nav"; 7 | 8 | const { REACT_APP_SERVER_URL } = process.env; 9 | 10 | export function App() { 11 | const [username, setUsername] = React.useState(undefined); 12 | 13 | React.useEffect(() => { 14 | (async () => { 15 | const response = await fetch(`${REACT_APP_SERVER_URL}/users/current`, { 16 | credentials: "include", 17 | }); 18 | const { githubUsername } = await response.json(); 19 | setUsername(githubUsername); 20 | })(); 21 | }, []); 22 | 23 | return ( 24 | 25 | 26 | 27 | {({ match }) => ( 28 |
29 |
32 | )} 33 |
34 | 35 | {({ match }) => ( 36 |
37 |
40 | )} 41 |
42 | 43 |