[
  {
    "path": ".all-contributorsrc",
    "content": "{\n  \"projectName\": \"takenote\",\n  \"projectOwner\": \"taniarascia\",\n  \"repoType\": \"github\",\n  \"repoHost\": \"https://github.com\",\n  \"files\": [\n    \"README.md\"\n  ],\n  \"imageSize\": 50,\n  \"commit\": true,\n  \"commitConvention\": \"none\",\n  \"contributors\": [\n    {\n      \"login\": \"taniarascia\",\n      \"name\": \"Tania Rascia\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/11951801?v=4\",\n      \"profile\": \"https://www.taniarascia.com\",\n      \"contributions\": [\n        \"code\",\n        \"ideas\",\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"hankolsen\",\n      \"name\": \"hankolsen\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/1008390?v=4\",\n      \"profile\": \"https://github.com/hankolsen\",\n      \"contributions\": [\n        \"code\",\n        \"bug\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"joseph-perez\",\n      \"name\": \"Joseph Perez\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/7772649?v=4\",\n      \"profile\": \"https://github.com/joseph-perez\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"dagda1\",\n      \"name\": \"Paul\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/118328?v=4\",\n      \"profile\": \"https://cutting.scot\",\n      \"contributions\": [\n        \"code\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"MartinRosenberg\",\n      \"name\": \"Martin Rosenberg\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/2382147?v=4\",\n      \"profile\": \"https://martinbrosenberg.com/\",\n      \"contributions\": [\n        \"code\",\n        \"bug\",\n        \"maintenance\"\n      ]\n    },\n    {\n      \"login\": \"meowwwls\",\n      \"name\": \"Melissa\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/16426195?v=4\",\n      \"profile\": \"http://codepen.io/meowwwls\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"jjtowle\",\n      \"name\": \"Jason Towle\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/41359068?v=4\",\n      \"profile\": \"https://github.com/jjtowle\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"markerikson\",\n      \"name\": \"Mark Erikson\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/1128784?v=4\",\n      \"profile\": \"http://blog.isquaredsoftware.com\",\n      \"contributions\": [\n        \"ideas\"\n      ]\n    },\n    {\n      \"login\": \"alphonseb\",\n      \"name\": \"Alphonse Bouy\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/32797759?v=4\",\n      \"profile\": \"http://www.alphonsebouy.fr\",\n      \"contributions\": [\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"dave2kb\",\n      \"name\": \"dave2kb\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/30696030?v=4\",\n      \"profile\": \"https://github.com/dave2kb\",\n      \"contributions\": [\n        \"design\",\n        \"ideas\"\n      ]\n    },\n    {\n      \"login\": \"Dantaro\",\n      \"name\": \"Devin McIntyre\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/2750903?v=4\",\n      \"profile\": \"https://github.com/Dantaro\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"jeffslofish\",\n      \"name\": \"Jeffrey Fisher\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/1240484?v=4\",\n      \"profile\": \"http://slofish.io\",\n      \"contributions\": [\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"dong-alex\",\n      \"name\": \"Alex Dong\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/23242741?v=4\",\n      \"profile\": \"https://github.com/dong-alex\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Publicker\",\n      \"name\": \"Publicker\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/52673485?v=4\",\n      \"profile\": \"https://github.com/Publicker\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"kleyu\",\n      \"name\": \"Jakub Naskręski\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/36169811?v=4\",\n      \"profile\": \"https://github.com/kleyu\",\n      \"contributions\": [\n        \"code\",\n        \"bug\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"opw0011\",\n      \"name\": \"Benny O\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/10897048?v=4\",\n      \"profile\": \"https://opw0011.github.io/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"justDOindev\",\n      \"name\": \"Justin Payne\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/44042682?v=4\",\n      \"profile\": \"https://github.com/justDOindev\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"yikjin\",\n      \"name\": \"marshmallow\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/34995304?v=4\",\n      \"profile\": \"https://yikjin.github.io\",\n      \"contributions\": [\n        \"maintenance\"\n      ]\n    },\n    {\n      \"login\": \"Jfelix61\",\n      \"name\": \"Jose Felix \",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/21092519?v=4\",\n      \"profile\": \"http://jfelix.info\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"xboston\",\n      \"name\": \"Nikolay Kirsh\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/201306?v=4\",\n      \"profile\": \"https://xboston.dev\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Mudassar045\",\n      \"name\": \"Mudassar Ali\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/24487349?v=4\",\n      \"profile\": \"https://github.com/Mudassar045\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"NathanBland\",\n      \"name\": \"Nathan Bland\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/926111?v=4\",\n      \"profile\": \"https://nathanbland.github.io/\",\n      \"contributions\": [\n        \"bug\",\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"siliconeidolon\",\n      \"name\": \"Craig Lam\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/8170456?v=4\",\n      \"profile\": \"http://craiglam.com\",\n      \"contributions\": [\n        \"code\",\n        \"bug\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"ashinzekene\",\n      \"name\": \"Ashinze Ekene\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/20991583?v=4\",\n      \"profile\": \"https://twitter.com/ashinzekene\",\n      \"contributions\": [\n        \"bug\",\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"harrySullivan\",\n      \"name\": \"Harry Sullivan\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/38230536?v=4\",\n      \"profile\": \"https://adityasriram.ga\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"moudev\",\n      \"name\": \"Mauricio Martínez\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/13499566?v=4\",\n      \"profile\": \"https://github.com/moudev\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"BlackHole1\",\n      \"name\": \"Black-Hole\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/8198408?v=4\",\n      \"profile\": \"http://www.bugs.cc/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"yogan\",\n      \"name\": \"Frank Blendinger\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/122564?v=4\",\n      \"profile\": \"https://zogan.de/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"osiux\",\n      \"name\": \"Eduardo Reveles\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/204463?v=4\",\n      \"profile\": \"https://www.osiux.ws\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"leofrozenyogurt\",\n      \"name\": \"Leo Royzengurt\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/2198384?v=4\",\n      \"profile\": \"https://github.com/leofrozenyogurt\",\n      \"contributions\": [\n        \"code\",\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"kcvgan\",\n      \"name\": \"kcvgan\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/13578888?v=4\",\n      \"profile\": \"https://github.com/kcvgan\",\n      \"contributions\": [\n        \"code\",\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"codytowstik\",\n      \"name\": \"Cody Towstik\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/10625608?v=4\",\n      \"profile\": \"https://github.com/codytowstik\",\n      \"contributions\": [\n        \"code\",\n        \"test\",\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"vincentdoerig\",\n      \"name\": \"Vincent Dörig\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/24668338?v=4\",\n      \"profile\": \"https://github.com/vincentdoerig\",\n      \"contributions\": [\n        \"test\",\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"miqh\",\n      \"name\": \"Michael Huynh\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/43751307?v=4\",\n      \"profile\": \"https://github.com/miqh\",\n      \"contributions\": [\n        \"code\",\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"code128\",\n      \"name\": \"Joshua Bloom\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/43435?v=4\",\n      \"profile\": \"https://github.com/code128\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Mxchaeltrxn\",\n      \"name\": \"Mxchaeltrxn\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/34886045?v=4\",\n      \"profile\": \"https://github.com/Mxchaeltrxn\",\n      \"contributions\": [\n        \"code\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"KonradStanski\",\n      \"name\": \"Konrad Staniszewski\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/38778413?v=4\",\n      \"profile\": \"https://konradstaniszewski.com\",\n      \"contributions\": [\n        \"doc\"\n      ]\n    },\n    {\n      \"login\": \"yohix\",\n      \"name\": \"Yohix\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/61746440?v=4\",\n      \"profile\": \"https://github.com/yohix\",\n      \"contributions\": [\n        \"maintenance\"\n      ]\n    },\n    {\n      \"login\": \"jackson-elfers\",\n      \"name\": \"Jackson Elfers\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/55408089?v=4\",\n      \"profile\": \"https://github.com/jackson-elfers\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"vamshi-tg\",\n      \"name\": \"Vamshi\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/32225088?v=4\",\n      \"profile\": \"https://github.com/vamshi-tg\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"pavlakissimos\",\n      \"name\": \"Simos\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/19609475?v=4\",\n      \"profile\": \"https://github.com/pavlakissimos\",\n      \"contributions\": [\n        \"code\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"ggonza89\",\n      \"name\": \"Yankee\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/5530647?v=4\",\n      \"profile\": \"https://github.com/ggonza89\",\n      \"contributions\": [\n        \"code\",\n        \"ideas\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"G-Milevski\",\n      \"name\": \"G-Milevski\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/25174255?v=4\",\n      \"profile\": \"https://github.com/G-Milevski\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"kodyclemens\",\n      \"name\": \"Kody Clemens\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/43357615?v=4\",\n      \"profile\": \"https://kodyclemens.com\",\n      \"contributions\": [\n        \"code\",\n        \"test\",\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"qpeela\",\n      \"name\": \"Vladimir Yamshikov\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/5824914?v=4\",\n      \"profile\": \"https://github.com/qpeela\",\n      \"contributions\": [\n        \"code\",\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"ronan696\",\n      \"name\": \"Ronan D'Souza\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/13074003?v=4\",\n      \"profile\": \"https://about.me/ronan696\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"ModProg\",\n      \"name\": \"Roland Fredenhagen\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/11978847?v=4\",\n      \"profile\": \"http://modprog.de\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"PranjaliPatil14\",\n      \"name\": \"Pranjali Pramod Patil\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/31987627?v=4\",\n      \"profile\": \"https://github.com/PranjaliPatil14\",\n      \"contributions\": [\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"cbrgm\",\n      \"name\": \"Chris Bargmann\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/24737434?v=4\",\n      \"profile\": \"https://cbrgm.net\",\n      \"contributions\": [\n        \"ideas\",\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Jadhielv\",\n      \"name\": \"Jadhiel Vélez\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/24376900?v=4\",\n      \"profile\": \"https://www.linkedin.com/in/jadhielv\",\n      \"contributions\": [\n        \"code\",\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"machadolucasvp\",\n      \"name\": \"Lucas Machado\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/44952113?v=4\",\n      \"profile\": \"https://github.com/machadolucasvp\",\n      \"contributions\": [\n        \"code\",\n        \"bug\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"xsteadybcgo\",\n      \"name\": \"xsteadybcgo\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/19681921?v=4\",\n      \"profile\": \"https://github.com/xsteadybcgo\",\n      \"contributions\": [\n        \"bug\"\n      ]\n    },\n    {\n      \"login\": \"Rwandarushya\",\n      \"name\": \"Marius Robert RWANDARUSHYA\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/49269745?v=4\",\n      \"profile\": \"https://github.com/Rwandarushya\",\n      \"contributions\": [\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"Isaackomeza\",\n      \"name\": \"Isaac Komezusenge\",\n      \"avatar_url\": \"https://avatars1.githubusercontent.com/u/66563235?v=4\",\n      \"profile\": \"https://github.com/Isaackomeza\",\n      \"contributions\": [\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"maximeish\",\n      \"name\": \"Maxime Ishimwe\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/54126307?v=4\",\n      \"profile\": \"https://github.com/maximeish\",\n      \"contributions\": [\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"marcosspn\",\n      \"name\": \"Marcos Spanholi\",\n      \"avatar_url\": \"https://avatars3.githubusercontent.com/u/2171424?v=4\",\n      \"profile\": \"https://github.com/marcosspn\",\n      \"contributions\": [\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"roshanrajeev\",\n      \"name\": \"Roshan Rajeev\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/52269241?v=4\",\n      \"profile\": \"http://roshanrajeev.xyz\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"fistonhn\",\n      \"name\": \"fistonhn\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/55746279?v=4\",\n      \"profile\": \"https://github.com/fistonhn\",\n      \"contributions\": [\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"raffaeleferri\",\n      \"name\": \"Raffaele Ferri\",\n      \"avatar_url\": \"https://avatars0.githubusercontent.com/u/75796924?v=4\",\n      \"profile\": \"https://github.com/raffaeleferri\",\n      \"contributions\": [\n        \"maintenance\"\n      ]\n    },\n    {\n      \"login\": \"joshwambere\",\n      \"name\": \"Dusabe Johnson\",\n      \"avatar_url\": \"https://avatars2.githubusercontent.com/u/59834399?v=4\",\n      \"profile\": \"https://github.com/joshwambere\",\n      \"contributions\": [\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"tomasvn\",\n      \"name\": \"tomasvn\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/17225564?v=4\",\n      \"profile\": \"https://github.com/tomasvn\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"lucasvribeiro\",\n      \"name\": \"Lucas Ribeiro\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/12684816?v=4\",\n      \"profile\": \"http://www.lucasribeiro.dev\",\n      \"contributions\": [\n        \"code\",\n        \"test\"\n      ]\n    },\n    {\n      \"login\": \"Bartek532\",\n      \"name\": \"Bartosz Zagrodzki\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/57185551?v=4\",\n      \"profile\": \"http://bartek532.github.io/portfolio\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"mookkiah\",\n      \"name\": \"Mahendran Mookkiah\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/8975264?v=4\",\n      \"profile\": \"https://www.linkedin.com/in/mookkiah/\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"hkhattabii\",\n      \"name\": \"hkhattabii\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/54418529?v=4\",\n      \"profile\": \"https://github.com/hkhattabii\",\n      \"contributions\": [\n        \"code\"\n      ]\n    },\n    {\n      \"login\": \"Federico-Pomponii\",\n      \"name\": \"Federico Pomponii\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/6978411?v=4\",\n      \"profile\": \"https://github.com/Federico-Pomponii\",\n      \"contributions\": [\n        \"code\"\n      ]\n    }\n  ],\n  \"contributorsPerLine\": 7,\n  \"skipCi\": true\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": "node_modules\ndist\n.git\n.vscode\n!src\n!public\n!docs\n!config/*\n!package.json\n!package-lock.json"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "/**\n * ESLint Configuration\n */\nmodule.exports = {\n  parser: '@typescript-eslint/parser',\n  parserOptions: {\n    ecmaVersion: 2020,\n    sourceType: 'module',\n    ecmaFeatures: {\n      modules: true,\n    },\n  },\n  extends: [\n    'plugin:react/recommended',\n    'plugin:import/typescript',\n    'plugin:import/errors',\n    'plugin:import/warnings',\n    'plugin:prettier/recommended',\n    'prettier/react',\n  ],\n  plugins: ['import'],\n  rules: {\n    // Separate import groups with newline by section\n    'import/order': [\n      'error',\n      {\n        groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'unknown'],\n        'newlines-between': 'always',\n      },\n    ],\n    'no-console': 1, // Warning to reduce console logs used throughout app\n    'react/prop-types': 0, // Not using prop-types because we have TypeScript\n    'newline-before-return': 1,\n    'no-useless-return': 1,\n    'prefer-const': 1,\n    'no-useless-return': 1,\n    'no-unused-vars': 0,\n  },\n  settings: {\n    'import/resolver': {\n      // Allow `@/` to map to `src/client/`\n      alias: {\n        map: [\n          ['@', './src/client'],\n          ['@resources', './src/resources'],\n        ],\n        extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],\n      },\n    },\n    react: {\n      version: 'detect',\n    },\n  },\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: '[Bug]'\nlabels: 'Type: Bug'\nassignees: ''\n---\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Actual behavior**\nWhat actually happened.\n\n**Notes**\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: '[Feature]'\nlabels: 'Type: Feature'\nassignees: ''\n---\n\n**Problem**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Solution**\nA clear and concise description of what you want to happen.\n\n**Notes**\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## Description\n\nPlease read the [Contribution Guidelines](../CONTRIBUTING.md) before opening a pull request. Include a summary of the change and which issue is fixed.\n\nCloses # <-- link the issue number here\n\n### Browser checklist\n\nThis PR has been tested in the following browsers:\n\n- [ ] Chrome\n- [ ] Firefox\n- [ ] Safari\n\n### Testing checklist\n\n- [ ] End-to-end tests have been created if necessary\n"
  },
  {
    "path": ".gitignore",
    "content": "/node_modules\n/coverage\n/dist\n/cypress\n.idea\n.DS_Store\n.env\n.vscode\n\nnpm-debug.log*\n.coveralls.yml\n**/*/videos\ntests/e2e/videos"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"bracketSpacing\": true,\n  \"printWidth\": 100,\n  \"semi\": false,\n  \"singleQuote\": true,\n  \"tabWidth\": 2,\n  \"trailingComma\": \"es5\"\n}\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\n\nnode_js:\n  - '12'\n\naddons:\n  apt:\n    packages:\n      # Ubuntu 16+ does not install this dependency by default, so we need to install it ourselves\n      - libgconf-2-4\n      - snapd\n\ncache:\n  # Caches $HOME/.npm when npm ci is default script command\n  # Caches node_modules in all other cases\n  npm: false\n  # directories:\n    # we also need to cache folder with Cypress binary\n    # - ~/.cache\n\nservices:\n  # Use Docker command line\n  - docker\n\ninstall:\n  # Install dependencies for tests\n  - echo \"MTY1LjIyNy42MC4zMyBlY2RzYS1zaGEyLW5pc3RwMjU2IEFBQUFFMlZqWkhOaExYTm9ZVEl0Ym1semRIQXlOVFlBQUFBSWJtbHpkSEF5TlRZQUFBQkJCTVVPRlVVT3BxSzNmWkMzUUxJNmsrL2Vlc1l5YVVaNGZXbkRUaWNia1pjMmJIR1ltMG4wVk9RaW5mK0NYY2xhWmZTaVBNQ0xZakJUUzkrUWxWSFpPZ009\" | base64 -d >> $HOME/.ssh/known_hosts\n  - sudo snap install doctl\n  - npm ci\n\nbefore_script:\n  # Start server and client for tests\n  - echo -e \"CLIENT_ID=abc\\nDEMO=true\" > .env\n  - npm run client &\n\nscript:\n  # Run unit, component, and e2e tests\n  - npm run test:coverage:ci && npm run test:e2e\n\n# Commenting out the deploy phase as the demo is static and no longer requires a server\n# So Travis is currently only being used for running tests, not deploying\n# deploy:\n#   # Build Docker container and push to Docker Hub\n#   # Pull into DigitalOcean container and start\n#   provider: script\n#   script: bash deploy.sh\n#   on:\n#     branch: master\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n## v0.7.2 10/27/2020\n\nRefactoring.\n\n- Add SonarQube via SonarCloud (https://sonarcloud.io/dashboard?id=taniarascia_takenote)\n- Fix bugs and code smells\n- Fix #422 /app redirect\n- Add demo environment variable\n- Add compression\n- Remove prettier and associated massive Webpack bundle\n\n## v0.7.1 10/25/2020\n\nAdd GitHub integration.\n\n### Changed\n\n- Add code for integrating GitHub sync (https://github.com/taniarascia/takenote/pull/389)\n- Create demo mode for takenote.dev deployment\n- Add tests for selectors\n- Hide note list in Scratchpad view\n- Localized dates\n- Various bug fixes\n- Additional settings\n\n## v0.6.1 10/19/2020\n\nAdd top menu bar.\n\n### Changed\n\n- Add top menu bar for preview, sync, settings, and other note options\n- Update settings UI\n- Add sync test back\n- Organize end-to-end tests\n\n## v0.6.0 10/16/2020\n\nUpgrade to webpack 5.\n\n### Changed\n\n- Updated all packages, notable webpack 4 to webpack 5\n- Updated webpack config to reflect breaking changes\n- Manually brought in polyfills for Node packages that are no longer polyfilled by webpack\n- Moved settings to bottom of app sidebar\n- Removed sync button\n- Removed unnecessary patches\n\n## v0.5.0 02/22/2020\n\nGitHub authentication.\n\n### Added\n\n- Log in/log out functionality implemented using GitHub OAuth\n\n### Changed\n\n- Refactored large files into smaller components\n- Added folder structure and technologies to README\n- Modify deployment scripts and Dockerfile to allow local development with GitHub authentication\n- Prompt to confirm exit added when notes have not yet been synced\n\n## v0.4.0 02/03/2020\n\nInitial release.\n\n### Added\n\n- Created `CHANGELOG.md`\n- Added Node/TypeScript backend for REST API calls\n- Created CI/CD pipeline with `deploy.sh`, `.travis.yml` and `Dockerfile`\n\n### Changed\n\n- Migrated website from Netlify to DigitalOcean\n- Added instructions for new local development to README\n- Removed Netlify badge from README\n- Removed Create React App and replaced with custom Webpack setup\n\n### Removed\n\n- Removed Service Worker due to the application no longer being fully client side\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contribution Guidelines\n\nTakeNote is an open source project, and contributions of any kind are welcome and appreciated. Open issues, bugs, and enhancements are all listed on the [issues](https://github.com/taniarascia/takenote/issues) tab and labeled accordingly. Feel free to open bug tickets and make feature requests. Easy bugs and features will be tagged with the `good first issue` label.\n\n## Issues\n\nIf you encounter a bug, please file a bug report. If you have a feature to request, please open a feature request. If you would like to work on an issue or feature, there is no need to request permission. Please add tests to any new features.\n\n## Pull Requests\n\nIn order to create a pull request for TakeNote, follow the GitHub instructions for [Creating a pull request from a fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork). Please link your pull request to an existing issue.\n\n## Folder Structure\n\nDescription of the project files and directories.\n\n```bash\n├── config/                    # Configuration\n│   ├── cypress.config.js      # Cypress end-to-end test configuration\n│   ├── jest.config.js         # Jest unit/component test configuration\n│   ├── nodemon.config.json    # Nodemon configuration\n│   ├── webpack.common.js      # Webpack shared configuration\n│   ├── webpack.dev.js         # Webpack development configuration (dev server)\n│   └── webpack.prod.js        # Webpack productuon configuration (dist output)\n├── assets/                    # Supplemental assets\n├── public/                    # Files that will write to dist on build\n├── src/                       # All TakeNote app source files\n│   ├── resources/             # Shared resources\n│   ├── client/                # React client side code\n│   │   ├── api/               # Temporary placeholders for mock API calls\n│   │   ├── components/        # React components that are not connected to Redux\n│   │   ├── containers/        # React Redux connected containers\n│   │   ├── contexts/          # React context global state without Redux\n│   │   ├── router/            # React private and public routes\n│   │   ├── sagas/             # Redux sagas\n│   │   ├── selectors/         # Redux Toolkit selectors\n│   │   ├── slices/            # Redux Toolkit slices\n│   │   ├── styles/            # Sass style files\n│   │   ├── types/             # TypeScript types\n│   │   ├── utils/             # Utility functions\n│   │   └── index.tsx          # Client side entry point\n│   └── server/                # Node/Express server side code\n│       ├── handlers/          # Functions for API endpoints\n│       ├── middleware/        # Middleware for API endpoints\n│       ├── router/            # Route API endpoints\n│       ├── utils/             # Backend utilities\n│       └── index.ts           # Server entrypoint\n├── tests/                     # Test suites\n│   ├── e2e/                   # Cypress end-to-end tests\n│   └── unit/                  # React Testing Library and Jest tests\n├── .dockerignore              # Files ignored by Docker\n├── .editorconfig              # Configures editor rules\n├── .gitignore                 # Files ignored by git\n├── .prettierrc                # Code convention enforced by Prettier\n├── .travis.yml                # Continuous integration and deployment config\n├── CHANGELOG.md               # List of significant changes\n├── deploy.sh                  # Deployment script for Docker in production\n├── Dockerfile                 # Docker build instructions\n├── LICENSE                    # License for this open source project\n├── package-lock.json          # Package lockfile\n├── package.json               # Dependencies and additional information\n├── README.md\n├── seed.js                    # Seed the app with data for testing\n└── tsconfig.json              # Typescript configuration\n```\n\n## Scripts\n\nAn explanation of the `package.json` scripts.\n\n| Command         | Description                                 |\n| --------------- | ------------------------------------------- |\n| `dev`           | Run TakeNote in a development environment   |\n| `dev:test`      | Run TakeNote in a testing environment       |\n| `client`        | Start a webpack dev server for the frontend |\n| `server`        | Start a nodemon dev server for the backend  |\n| `build`         | Create a production build of TakeNote       |\n| `start`         | Start a production server for TakeNote      |\n| `test`          | Run unit and component tests                |\n| `test:e2e`      | Run end-to-end tests in the command line    |\n| `test:e2e:open` | Open end-to-end tests in a browser          |\n| `test:coverage` | Get test coverage                           |\n\n## Technologies\n\nThis project is possible thanks to all these open source languages, libraries, and frameworks.\n\n| Tech                                          | Description                               |\n| --------------------------------------------- | ----------------------------------------- |\n| [Codemirror](https://codemirror.net/)         | Browser-based text editor                 |\n| [TypeScript](https://www.typescriptlang.org/) | Static type-checking programming language |\n| [Node.js](https://nodejs.org/en/)             | JavaScript runtime for the backend        |\n| [Express](https://expressjs.com/)             | Server framework                          |\n| [React](https://reactjs.org/)                 | Front end user interface                  |\n| [Redux](https://redux.js.org/)                | Global state management                   |\n| [Webpack](https://webpack.js.org/)            | Asset bundler                             |\n| [Sass](https://sass-lang.com/)                | Style preprocessor                        |\n| [OAuth](https://oauth.net/)                   | Protocol for secure authorization         |\n| [ESLint](https://eslint.org/)                 | TypeScript linting                        |\n| [Jest](https://jestjs.io/)                    | Unit testing framework                    |\n| [Cypress](https://www.cypress.io/)            | End-to-end testing framework              |\n\n## Styleguide\n\nCoding conventions are enforced by [ESLint](.eslintrc.js) and [Prettier](.prettierrc).\n\n- No semicolons\n- Single quotes\n- Two space indentation\n- Trailing commas in arrays and objects\n- [Non-default exports](https://humanwhocodes.com/blog/2019/01/stop-using-default-exports-javascript-module/) are preferred for components\n- Module imports are ordered and separated: **built-in** -> **external** -> **internal** -> **css/assets/other**\n- TypeScript: strict mode, with no implicitly any\n- React: functional style with Hooks (no classes)\n- `const` preferred over `let`\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Use small Alpine Linux image\nFROM node:12-alpine\n\n# Set environment variables\nENV PORT=5000\nARG CLIENT_ID\n\nCOPY . app/\n\nWORKDIR app/\n\n# Make sure dependencies exist for Webpack loaders\nRUN apk add --no-cache \\\n  autoconf \\\n  automake \\\n  bash \\\n  g++ \\\n  libc6-compat \\\n  libjpeg-turbo-dev \\\n  libpng-dev \\\n  make \\\n  nasm \nRUN npm ci --only-production --silent\n\n# Build production client side React application\nRUN npm run build\n\n# Expose port for Node\nEXPOSE $PORT\n\n# Start Node server\nENTRYPOINT npm run prod"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2020 Tania Rascia\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"./assets/logo.png\">\n</p>\n\n<p align=\"center\">\n <img src=\"https://img.shields.io/badge/License-MIT-blue.svg\">\n   <a href=\"https://app.netlify.com/sites/tnote/deploys\"><img src=\"https://api.netlify.com/api/v1/badges/a0e055de-cab8-4217-80dd-5bd769b7d478/deploy-status\"></a>\n   <a href='https://coveralls.io/github/taniarascia/takenote'><img src='https://coveralls.io/repos/github/taniarascia/takenote/badge.svg' alt='Coverage Status' /></a>\n </p>\n <p align=\"center\">\n   <a href=\"https://sonarcloud.io/dashboard?id=taniarascia_takenote\"><img src=\"https://sonarcloud.io/api/project_badges/measure?project=taniarascia_takenote&metric=sqale_rating\"></a>\n   <a href=\"https://sonarcloud.io/dashboard?id=taniarascia_takenote\"><img src=\"https://sonarcloud.io/api/project_badges/measure?project=taniarascia_takenote&metric=reliability_rating\"></a>\n   <a href=\"https://sonarcloud.io/api/project_badges/measure?project=taniarascia_takenote&metric=security_rating\"><img src=\"https://sonarcloud.io/api/project_badges/measure?project=taniarascia_takenote&metric=security_rating\"></a>\n   \n</p>\n\n<p align=\"center\">A web-based notes app for developers. (Demo only)</p>\n\n![Screenshot](./assets/takenote-light.png)\n\n## Features\n\n- **Plain text notes** - take notes in an IDE-like environment that makes no assumptions\n- **Markdown preview** - view rendered HTML\n- **Linked notes** - use `{{uuid}}` syntax to link to notes within other notes\n- **Syntax highlighting** - light and dark mode available (based on the beautiful [New Moon theme](https://taniarascia.github.io/new-moon/))\n- **Keyboard shortcuts** - use the keyboard for all common tasks - creating notes and categories, toggling settings, and other options\n- **Drag and drop** - drag a note or multiple notes to categories, favorites, or trash\n- **Multi-cursor editing** - supports multiple cursors and other [Codemirror](https://codemirror.net/) options\n- **Search notes** - easily search all notes, or notes within a category\n- **Prettify notes** - use Prettier on the fly for your Markdown\n- **No WYSIWYG** - made for developers, by developers\n- **No database** - notes are only stored in the browser's local storage and are available for download and export to you alone\n- **No tracking or analytics** - 'nuff said\n- **GitHub integration** - self-hosted option is available for auto-syncing to a GitHub repository (not available in the demo)\n\n## About\n\nTakeNote is a note-taking app for the web. You can use the demo app at [takenote.dev](https://takenote.dev). It is a static site without a database and does not sync your notes to the cloud. The notes are persisted temporarily in local storage, but you can download all notes in markdown format as a zip.\n\nHidden within the code is an alternate version that contain a Node/Express server and integration with GitHub. This version involves creating an OAuth application for GitHub and signing up to it with private repository permissions. Instead of backing up to local storage, your notes will back up to a private repository in your account called `takenote-data`. Due to the following reasons I'm choosing not to deploy or maintain this portion of the application:\n\n- I do not want to maintain a free app with users alongside my career and other commitments\n- I do not want to request private repository permissions from users\n- I do not want to maintain an active server\n- I do not want to worry about GitHub rate limiting from the server\n- There is no way to batch create many files from the GitHub API, leading to a suboptimal GitHub storage solution\n\nHowever, I'm leaving the code available so you can feel free to host your own TakeNote instance or study the code for learning purposes. I do not provide support or guidance for these purposes.\n\nTakeNote was created with TypeScript, React, Redux, Node, Express, Codemirror, Webpack, Jest, Cypress, Feather Icons, ESLint, and Mousetrap, among other awesome open-source software.\n\n## Reviews\n\n> _\"I think the lack of extra crap is a feature.\"_ — Craig Lam\n\n## Demo Development\n\nClone and install.\n\n```bash\ngit clone git@github.com:taniarascia/takenote\ncd takenote\nnpm i\n```\n\nRun a development server.\n\n```bash\nnpm run client\n```\n\n## Full Application Development (self-hosted)\n\n### Pre-Installation\n\nBefore working on TakeNote locally, you must create a GitHub OAuth app for development.\n\nGo to your GitHub profile settings, and click on **Developer Settings**.\n\nClick the **New OAuth App** button.\n\n- **Application name**: TakeNote Development\n- **Homepage URL**: `http://localhost:3000`\n- **Authorization callback URL**: `http://localhost:3000/api/auth/callback`\n\nCreate a `.env` file in the root of the project, and add the app's client ID and secret. Remove `DEMO` variable to enable GitHub integration.\n\n```bash\nCLIENT_ID=xxx\nCLIENT_SECRET=xxxx\nDEMO=true\n```\n\n> Change the URLs to port `5000` in production mode or Docker.\n\n### Installation\n\n```bash\ngit clone git@github.com:taniarascia/takenote\ncd takenote\nnpm i\n```\n\n#### Development mode\n\nIn the development environment, an Express server is running on port `5000` to handle all API calls, and a hot Webpack dev server is running on port `3000` for the React frontend. To run both of these servers concurrently, run the `dev` command.\n\n```bash\nnpm run dev\n```\n\nGo to `localhost:3000` to view the app.\n\n> API requests will be proxied to port `5000` automatically.\n\n#### Production mode\n\nIn the production environment, the React app is built, and Express redirects all incoming requests to the `dist` directory on port `5000`.\n\n```bash\nnpm run build && npm run start\n```\n\nGo to `localhost:5000` to view the app.\n\n#### Run in Docker\n\nFollow these instructions to build an image and run a container.\n\n```bash\n# Build Docker image\ndocker build --build-arg CLIENT_ID=xxx -t takenote:mytag .\n\n# Run Docker container in port 5000\ndocker run \\\n-e CLIENT_ID=xxx \\\n-e CLIENT_SECRET=xxxx \\\n-e NODE_ENV=development \\\n-p 5000:5000 \\\ntakenote:mytag\n```\n\nGo to `localhost:5000` to view the app.\n\n> Note: You will see some errors during the installation phase, but these are simply warnings that unnecessary packages do not exist, since the Node Alpine base image is minimal.\n\n### Seed data\n\nTo seed the app with some test data, paste the contents of `seed.js` into your browser console.\n\n## Testing\n\nRun unit and component/integration tests.\n\n```bash\nnpm run test\n```\n\n> If using Jest Runner in VSCode, add `\"jestrunner.configPath\": \"config/jest.config.js\"` to your settings\n\nRun Cypress end-to-end tests.\n\n```bash\n# In one window, run the application\nnpm run client\n\n# In another window, run the end-to-end tests\nnpm run test:e2e:open\n```\n\n## Contributing\n\nTakeNote is an open source project, and contributions of any kind are welcome and appreciated. Open issues, bugs, and feature requests are all listed on the [issues](https://github.com/taniarascia/takenote/issues) tab and labeled accordingly. Feel free to open bug tickets and make feature requests. Easy bugs and features will be tagged with the `good first issue` label.\n\nView [CONTRIBUTING.md](CONTRIBUTING.md) to learn about the style guide, folder structure, scripts, and how to contribute.\n\n## Contributors\n\nThanks goes to these wonderful people:\n\n<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->\n<!-- prettier-ignore-start -->\n<!-- markdownlint-disable -->\n<table>\n  <tr>\n    <td align=\"center\"><a href=\"https://www.taniarascia.com\"><img src=\"https://avatars3.githubusercontent.com/u/11951801?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Tania Rascia</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=taniarascia\" title=\"Code\">💻</a> <a href=\"#ideas-taniarascia\" title=\"Ideas, Planning, & Feedback\">🤔</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Ataniarascia\" title=\"Bug reports\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/hankolsen\"><img src=\"https://avatars3.githubusercontent.com/u/1008390?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>hankolsen</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=hankolsen\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Ahankolsen\" title=\"Bug reports\">🐛</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=hankolsen\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://github.com/joseph-perez\"><img src=\"https://avatars0.githubusercontent.com/u/7772649?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Joseph Perez</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=joseph-perez\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://cutting.scot\"><img src=\"https://avatars0.githubusercontent.com/u/118328?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Paul</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=dagda1\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=dagda1\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://martinbrosenberg.com/\"><img src=\"https://avatars2.githubusercontent.com/u/2382147?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Martin Rosenberg</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=MartinRosenberg\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3AMartinRosenberg\" title=\"Bug reports\">🐛</a> <a href=\"#maintenance-MartinRosenberg\" title=\"Maintenance\">🚧</a></td>\n    <td align=\"center\"><a href=\"http://codepen.io/meowwwls\"><img src=\"https://avatars3.githubusercontent.com/u/16426195?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Melissa</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=meowwwls\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/jjtowle\"><img src=\"https://avatars0.githubusercontent.com/u/41359068?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Jason Towle</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=jjtowle\" title=\"Code\">💻</a></td>\n  </tr>\n  <tr>\n    <td align=\"center\"><a href=\"http://blog.isquaredsoftware.com\"><img src=\"https://avatars1.githubusercontent.com/u/1128784?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Mark Erikson</b></sub></a><br /><a href=\"#ideas-markerikson\" title=\"Ideas, Planning, & Feedback\">🤔</a></td>\n    <td align=\"center\"><a href=\"http://www.alphonsebouy.fr\"><img src=\"https://avatars2.githubusercontent.com/u/32797759?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Alphonse Bouy</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Aalphonseb\" title=\"Bug reports\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/dave2kb\"><img src=\"https://avatars1.githubusercontent.com/u/30696030?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>dave2kb</b></sub></a><br /><a href=\"#design-dave2kb\" title=\"Design\">🎨</a> <a href=\"#ideas-dave2kb\" title=\"Ideas, Planning, & Feedback\">🤔</a></td>\n    <td align=\"center\"><a href=\"https://github.com/Dantaro\"><img src=\"https://avatars3.githubusercontent.com/u/2750903?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Devin McIntyre</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=Dantaro\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"http://slofish.io\"><img src=\"https://avatars0.githubusercontent.com/u/1240484?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Jeffrey Fisher</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Ajeffslofish\" title=\"Bug reports\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/dong-alex\"><img src=\"https://avatars2.githubusercontent.com/u/23242741?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Alex Dong</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=dong-alex\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/Publicker\"><img src=\"https://avatars2.githubusercontent.com/u/52673485?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Publicker</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=Publicker\" title=\"Code\">💻</a></td>\n  </tr>\n  <tr>\n    <td align=\"center\"><a href=\"https://github.com/kleyu\"><img src=\"https://avatars2.githubusercontent.com/u/36169811?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Jakub Naskręski</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=kleyu\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Akleyu\" title=\"Bug reports\">🐛</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=kleyu\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://opw0011.github.io/\"><img src=\"https://avatars2.githubusercontent.com/u/10897048?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Benny O</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=opw0011\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/justDOindev\"><img src=\"https://avatars3.githubusercontent.com/u/44042682?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Justin Payne</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=justDOindev\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://yikjin.github.io\"><img src=\"https://avatars2.githubusercontent.com/u/34995304?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>marshmallow</b></sub></a><br /><a href=\"#maintenance-yikjin\" title=\"Maintenance\">🚧</a></td>\n    <td align=\"center\"><a href=\"http://jfelix.info\"><img src=\"https://avatars2.githubusercontent.com/u/21092519?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Jose Felix </b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=Jfelix61\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://xboston.dev\"><img src=\"https://avatars1.githubusercontent.com/u/201306?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Nikolay Kirsh</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=xboston\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/Mudassar045\"><img src=\"https://avatars0.githubusercontent.com/u/24487349?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Mudassar Ali</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=Mudassar045\" title=\"Code\">💻</a></td>\n  </tr>\n  <tr>\n    <td align=\"center\"><a href=\"https://nathanbland.github.io/\"><img src=\"https://avatars1.githubusercontent.com/u/926111?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Nathan Bland</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/issues?q=author%3ANathanBland\" title=\"Bug reports\">🐛</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=NathanBland\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"http://craiglam.com\"><img src=\"https://avatars1.githubusercontent.com/u/8170456?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Craig Lam</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=siliconeidolon\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Asiliconeidolon\" title=\"Bug reports\">🐛</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=siliconeidolon\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://twitter.com/ashinzekene\"><img src=\"https://avatars2.githubusercontent.com/u/20991583?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Ashinze Ekene</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Aashinzekene\" title=\"Bug reports\">🐛</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=ashinzekene\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://adityasriram.ga\"><img src=\"https://avatars0.githubusercontent.com/u/38230536?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Harry Sullivan</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=harrySullivan\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/moudev\"><img src=\"https://avatars2.githubusercontent.com/u/13499566?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Mauricio Martínez</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=moudev\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"http://www.bugs.cc/\"><img src=\"https://avatars0.githubusercontent.com/u/8198408?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Black-Hole</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=BlackHole1\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://zogan.de/\"><img src=\"https://avatars0.githubusercontent.com/u/122564?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Frank Blendinger</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=yogan\" title=\"Code\">💻</a></td>\n  </tr>\n  <tr>\n    <td align=\"center\"><a href=\"https://www.osiux.ws\"><img src=\"https://avatars2.githubusercontent.com/u/204463?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Eduardo Reveles</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=osiux\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/leofrozenyogurt\"><img src=\"https://avatars2.githubusercontent.com/u/2198384?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Leo Royzengurt</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=leofrozenyogurt\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Aleofrozenyogurt\" title=\"Bug reports\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/kcvgan\"><img src=\"https://avatars1.githubusercontent.com/u/13578888?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>kcvgan</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=kcvgan\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Akcvgan\" title=\"Bug reports\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/codytowstik\"><img src=\"https://avatars1.githubusercontent.com/u/10625608?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Cody Towstik</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=codytowstik\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=codytowstik\" title=\"Tests\">⚠️</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Acodytowstik\" title=\"Bug reports\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/vincentdoerig\"><img src=\"https://avatars3.githubusercontent.com/u/24668338?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Vincent Dörig</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=vincentdoerig\" title=\"Tests\">⚠️</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=vincentdoerig\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/miqh\"><img src=\"https://avatars3.githubusercontent.com/u/43751307?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Michael Huynh</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=miqh\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Amiqh\" title=\"Bug reports\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/code128\"><img src=\"https://avatars0.githubusercontent.com/u/43435?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Joshua Bloom</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=code128\" title=\"Code\">💻</a></td>\n  </tr>\n  <tr>\n    <td align=\"center\"><a href=\"https://github.com/Mxchaeltrxn\"><img src=\"https://avatars3.githubusercontent.com/u/34886045?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Mxchaeltrxn</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=Mxchaeltrxn\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=Mxchaeltrxn\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://konradstaniszewski.com\"><img src=\"https://avatars2.githubusercontent.com/u/38778413?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Konrad Staniszewski</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=KonradStanski\" title=\"Documentation\">📖</a></td>\n    <td align=\"center\"><a href=\"https://github.com/yohix\"><img src=\"https://avatars3.githubusercontent.com/u/61746440?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Yohix</b></sub></a><br /><a href=\"#maintenance-yohix\" title=\"Maintenance\">🚧</a></td>\n    <td align=\"center\"><a href=\"https://github.com/jackson-elfers\"><img src=\"https://avatars1.githubusercontent.com/u/55408089?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Jackson Elfers</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=jackson-elfers\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/vamshi-tg\"><img src=\"https://avatars2.githubusercontent.com/u/32225088?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Vamshi</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=vamshi-tg\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/pavlakissimos\"><img src=\"https://avatars1.githubusercontent.com/u/19609475?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Simos</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=pavlakissimos\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=pavlakissimos\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://github.com/ggonza89\"><img src=\"https://avatars0.githubusercontent.com/u/5530647?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Yankee</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=ggonza89\" title=\"Code\">💻</a> <a href=\"#ideas-ggonza89\" title=\"Ideas, Planning, & Feedback\">🤔</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=ggonza89\" title=\"Tests\">⚠️</a></td>\n  </tr>\n  <tr>\n    <td align=\"center\"><a href=\"https://github.com/G-Milevski\"><img src=\"https://avatars2.githubusercontent.com/u/25174255?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>G-Milevski</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=G-Milevski\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://kodyclemens.com\"><img src=\"https://avatars0.githubusercontent.com/u/43357615?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Kody Clemens</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=kodyclemens\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=kodyclemens\" title=\"Tests\">⚠️</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Akodyclemens\" title=\"Bug reports\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/qpeela\"><img src=\"https://avatars3.githubusercontent.com/u/5824914?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Vladimir Yamshikov</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=qpeela\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Aqpeela\" title=\"Bug reports\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://about.me/ronan696\"><img src=\"https://avatars1.githubusercontent.com/u/13074003?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Ronan D'Souza</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=ronan696\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"http://modprog.de\"><img src=\"https://avatars0.githubusercontent.com/u/11978847?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Roland Fredenhagen</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=ModProg\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/PranjaliPatil14\"><img src=\"https://avatars2.githubusercontent.com/u/31987627?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Pranjali Pramod Patil</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=PranjaliPatil14\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://cbrgm.net\"><img src=\"https://avatars1.githubusercontent.com/u/24737434?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Chris Bargmann</b></sub></a><br /><a href=\"#ideas-cbrgm\" title=\"Ideas, Planning, & Feedback\">🤔</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=cbrgm\" title=\"Code\">💻</a></td>\n  </tr>\n  <tr>\n    <td align=\"center\"><a href=\"https://www.linkedin.com/in/jadhielv\"><img src=\"https://avatars3.githubusercontent.com/u/24376900?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Jadhiel Vélez</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=Jadhielv\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3AJadhielv\" title=\"Bug reports\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/machadolucasvp\"><img src=\"https://avatars0.githubusercontent.com/u/44952113?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Lucas Machado</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=machadolucasvp\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Amachadolucasvp\" title=\"Bug reports\">🐛</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=machadolucasvp\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://github.com/xsteadybcgo\"><img src=\"https://avatars3.githubusercontent.com/u/19681921?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>xsteadybcgo</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/issues?q=author%3Axsteadybcgo\" title=\"Bug reports\">🐛</a></td>\n    <td align=\"center\"><a href=\"https://github.com/Rwandarushya\"><img src=\"https://avatars2.githubusercontent.com/u/49269745?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Marius Robert RWANDARUSHYA</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=Rwandarushya\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://github.com/Isaackomeza\"><img src=\"https://avatars1.githubusercontent.com/u/66563235?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Isaac Komezusenge</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=Isaackomeza\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://github.com/maximeish\"><img src=\"https://avatars0.githubusercontent.com/u/54126307?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Maxime Ishimwe</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=maximeish\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://github.com/marcosspn\"><img src=\"https://avatars3.githubusercontent.com/u/2171424?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Marcos Spanholi</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=marcosspn\" title=\"Tests\">⚠️</a></td>\n  </tr>\n  <tr>\n    <td align=\"center\"><a href=\"http://roshanrajeev.xyz\"><img src=\"https://avatars2.githubusercontent.com/u/52269241?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Roshan Rajeev</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=roshanrajeev\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/fistonhn\"><img src=\"https://avatars0.githubusercontent.com/u/55746279?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>fistonhn</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=fistonhn\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://github.com/raffaeleferri\"><img src=\"https://avatars0.githubusercontent.com/u/75796924?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Raffaele Ferri</b></sub></a><br /><a href=\"#maintenance-raffaeleferri\" title=\"Maintenance\">🚧</a></td>\n    <td align=\"center\"><a href=\"https://github.com/joshwambere\"><img src=\"https://avatars2.githubusercontent.com/u/59834399?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Dusabe Johnson</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=joshwambere\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"https://github.com/tomasvn\"><img src=\"https://avatars.githubusercontent.com/u/17225564?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>tomasvn</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=tomasvn\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"http://www.lucasribeiro.dev\"><img src=\"https://avatars.githubusercontent.com/u/12684816?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Lucas Ribeiro</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=lucasvribeiro\" title=\"Code\">💻</a> <a href=\"https://github.com/taniarascia/takenote/commits?author=lucasvribeiro\" title=\"Tests\">⚠️</a></td>\n    <td align=\"center\"><a href=\"http://bartek532.github.io/portfolio\"><img src=\"https://avatars.githubusercontent.com/u/57185551?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Bartosz Zagrodzki</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=Bartek532\" title=\"Code\">💻</a></td>\n  </tr>\n  <tr>\n    <td align=\"center\"><a href=\"https://www.linkedin.com/in/mookkiah/\"><img src=\"https://avatars.githubusercontent.com/u/8975264?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Mahendran Mookkiah</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=mookkiah\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/hkhattabii\"><img src=\"https://avatars.githubusercontent.com/u/54418529?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>hkhattabii</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=hkhattabii\" title=\"Code\">💻</a></td>\n    <td align=\"center\"><a href=\"https://github.com/Federico-Pomponii\"><img src=\"https://avatars.githubusercontent.com/u/6978411?v=4?s=50\" width=\"50px;\" alt=\"\"/><br /><sub><b>Federico Pomponii</b></sub></a><br /><a href=\"https://github.com/taniarascia/takenote/commits?author=Federico-Pomponii\" title=\"Code\">💻</a></td>\n  </tr>\n</table>\n\n<!-- markdownlint-restore -->\n<!-- prettier-ignore-end -->\n\n<!-- ALL-CONTRIBUTORS-LIST:END -->\n\n## Acknowledgements\n\n- A big thank you to [David Bock](https://dkbock.com/) for logo design.\n\n## Author\n\n- [Tania Rascia](https://www.taniarascia.com)\n\n## License\n\nThis project is open source and available under the [MIT License](LICENSE).\n"
  },
  {
    "path": "config/cypress.config.json",
    "content": "{\n  \"baseUrl\": \"http://localhost:3000\",\n  \"integrationFolder\": \"tests/e2e/integration\",\n  \"pluginsFile\": \"tests/e2e/plugins/index.js\",\n  \"supportFile\": \"tests/e2e/support/index.js\",\n  \"fixturesFolder\": \"tests/e2e/fixtures\",\n  \"videosFolder\": \"tests/e2e/videos\",\n  \"video\": false\n}\n"
  },
  {
    "path": "config/jest.config.js",
    "content": "module.exports = {\n  // Setting the root to the actual root, since this file is in root/config\n  preset: 'ts-jest',\n  rootDir: '../',\n  roots: ['<rootDir>/src', '<rootDir>/tests/unit'],\n  transform: {\n    '^.+\\\\.tsx?$': 'ts-jest',\n    '\\\\.(html|xml|txt|md)$': 'jest-raw-loader',\n  },\n  setupFilesAfterEnv: ['@testing-library/jest-dom', 'jest-extended'],\n  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],\n  moduleNameMapper: {\n    // Allow `@/` to map to `src/client/` in Jest tests\n    '@/(.*)$': '<rootDir>/src/client/$1',\n    '@resources/(.*)$': '<rootDir>/src/resources/$1',\n    '\\\\.(css|less)$': '<rootDir>/tests/__mocks__/styleMock.ts',\n  },\n  globals: {\n    'ts-jest': {\n      diagnostics: false,\n    },\n  },\n}\n"
  },
  {
    "path": "config/nodemon.config.json",
    "content": "{\n  \"ignore\": [\n    \".git\",\n    \".eslintrc\",\n    \"node_modules/**\",\n    \"src/client/**\",\n    \"test/**\",\n    \"public/**\",\n    \"*.test.js\",\n    \"webpack.*.js\"\n  ],\n  \"watch\": [\"src/server/\"],\n  \"ext\": \"ts\",\n  \"exec\": \"cross-env NODE_ENV=development node --inspect -r ts-node/register ./src/server/index.ts\"\n}\n"
  },
  {
    "path": "config/webpack.common.js",
    "content": "const path = require('path')\n\nconst dotenv = require('dotenv')\nconst webpack = require('webpack')\nconst { CleanWebpackPlugin } = require('clean-webpack-plugin')\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\n\n/**\n * Obtain client id for OAuth link in React\n *\n * If in development mode or local production mode, search the .env file for\n * client id. If using Docker, pass a build arg.\n */\nconst getEnvFromDotEnvFile = dotenv.config()\nlet envKeys\n\nif (getEnvFromDotEnvFile.error) {\n  console.log('Getting environment variables from build args for production') // eslint-disable-line\n  envKeys = {\n    'process.env.CLIENT_ID': JSON.stringify(process.env.CLIENT_ID),\n    'process.env.DEMO': JSON.stringify(process.env.DEMO),\n    'process.env.NODE_ENV': JSON.stringify('production'),\n  }\n} else {\n  envKeys = {\n    'process.env.CLIENT_ID': JSON.stringify(getEnvFromDotEnvFile.parsed['CLIENT_ID']),\n    'process.env.DEMO': JSON.stringify(getEnvFromDotEnvFile.parsed['DEMO']),\n  }\n}\n\nmodule.exports = {\n  entry: ['./src/client/index.tsx'],\n  output: {\n    path: path.resolve(__dirname, '../dist'),\n    filename: '[name].[fullhash].bundle.js',\n    publicPath: '/',\n  },\n  module: {\n    rules: [\n      /**\n       * TypeScript (.ts/.tsx files)\n       *\n       * The TypeScript loader will compile all .ts/.tsx files to .js. Babel is\n       * not necessary here since TypeScript is taking care of all transpiling.\n       */\n      {\n        test: /\\.ts(x?)$/,\n        loader: 'ts-loader',\n        exclude: /node_modules/,\n      },\n      // Fonts\n      {\n        test: /\\.(woff(2)?|eot|ttf|otf)$/,\n        type: 'asset/inline',\n      },\n      // Markdown\n      {\n        test: /\\.md$/,\n        type: 'asset/source',\n      },\n      // Images\n      {\n        test: /\\.(?:ico|gif|png|jpg|jpeg|webp|svg)$/i,\n        type: 'asset/resource',\n      },\n    ],\n  },\n  resolve: {\n    // Resolve in this order\n    extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.md'],\n    // Allow `@/` to map to `src/client/`\n    alias: {\n      '@': path.resolve(__dirname, '../src/client'),\n      '@resources': path.resolve(__dirname, '../src/resources'),\n      stream: 'stream-browserify',\n      path: 'path-browserify',\n    },\n  },\n  plugins: [\n    // Get environment variables in React\n    new webpack.DefinePlugin(envKeys),\n    new CleanWebpackPlugin(),\n    new CopyWebpackPlugin({\n      patterns: [\n        {\n          from: path.resolve(__dirname, '../public'),\n          globOptions: {\n            ignore: ['*.DS_Store', 'favicon.ico', 'template.html'],\n          },\n        },\n      ],\n    }),\n    new webpack.ProvidePlugin({\n      process: 'process/browser',\n    }),\n  ],\n}\n"
  },
  {
    "path": "config/webpack.dev.js",
    "content": "const webpack = require('webpack')\nconst { merge } = require('webpack-merge')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\n\nconst common = require('./webpack.common.js')\n\nmodule.exports = merge(common, {\n  mode: 'development',\n  devtool: 'eval-source-map',\n  module: {\n    rules: [\n      // Styles\n      {\n        test: /\\.(scss|css)$/,\n        use: [\n          'style-loader',\n          {\n            loader: 'css-loader',\n            options: { sourceMap: true, importLoaders: 1 },\n          },\n          { loader: 'sass-loader', options: { sourceMap: true } },\n        ],\n      },\n    ],\n  },\n  devServer: {\n    historyApiFallback: true,\n    proxy: {\n      '/api': 'http://localhost:5000',\n    },\n    open: true,\n    compress: true,\n    hot: true,\n    port: 3000,\n  },\n  plugins: [\n    new webpack.HotModuleReplacementPlugin(),\n    new HtmlWebpackPlugin({\n      template: './public/template.html',\n      favicon: './public/favicon.ico',\n    }),\n  ],\n})\n"
  },
  {
    "path": "config/webpack.prod.js",
    "content": "const webpack = require('webpack')\nconst { merge } = require('webpack-merge')\nconst MiniCssExtractPlugin = require('mini-css-extract-plugin')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\nconst CssMinimizerPlugin = require('css-minimizer-webpack-plugin')\n\nconst common = require('./webpack.common.js')\n\n// Disable React DevTools in production\nconst disableReactDevtools = `\n<script>\nif (typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') {\n   __REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function() {};\n}\n</script>\n`\n\nmodule.exports = merge(common, {\n  mode: 'production',\n  devtool: false,\n  module: {\n    rules: [\n      // Styles\n      {\n        test: /\\.(scss|css)$/,\n        use: [\n          MiniCssExtractPlugin.loader,\n          {\n            loader: 'css-loader',\n            options: {\n              importLoaders: 1,\n            },\n          },\n          'sass-loader',\n        ],\n      },\n    ],\n  },\n  plugins: [\n    new MiniCssExtractPlugin({\n      filename: 'styles/[name].[contenthash].css',\n      chunkFilename: 'styles/[name].[id].[contenthash].css',\n      ignoreOrder: false,\n    }),\n    new HtmlWebpackPlugin({\n      template: './public/template.html',\n      favicon: './public/favicon.ico',\n      hash: true,\n      disableReactDevtools,\n    }),\n    new webpack.SourceMapDevToolPlugin({\n      exclude: ['/node_modules/'],\n    }),\n  ],\n  performance: {\n    hints: 'warning',\n    maxEntrypointSize: 512000,\n    maxAssetSize: 512000,\n  },\n  optimization: {\n    minimizer: [new CssMinimizerPlugin(), '...'],\n    runtimeChunk: 'multiple',\n    splitChunks: {\n      // Cache vendors since this code won't change very often\n      cacheGroups: {\n        vendor: {\n          test: /[\\\\/]node_modules[\\\\/](react|react-dom|axios|redux|react-redux)[\\\\/]/,\n          name: 'vendors',\n          chunks: 'all',\n          enforce: true,\n        },\n      },\n    },\n  },\n})\n"
  },
  {
    "path": "deploy.sh",
    "content": "#!/bin/sh\n\n# Stop script from running if there are any errors\nset -e\n\n# Docker image\nIMAGE=\"taniarascia/takenote\"\n\n# Git version with git hash and tags (if they exist) to be used as Docker tag\nGIT_VERSION=$(git describe --always --abbrev --tags --long)\n\n# Build and tag new Docker image and push up to Docker Hub\necho \"Building and tagging new Docker image: ${IMAGE}:${GIT_VERSION}\"\n\ndocker build --build-arg DEMO=true CLIENT_ID=${CLIENT_ID} -t ${IMAGE}:${GIT_VERSION} .\ndocker tag ${IMAGE}:${GIT_VERSION} ${IMAGE}:latest\n\n# Login to Docker Hub and push newest build\necho \"Logging into Docker and pushing ${IMAGE}:${GIT_VERSION}\"\n\necho \"${DOCKER_PASSWORD}\" | docker login -u \"${DOCKER_USERNAME}\" --password-stdin\ndocker push ${IMAGE}:${GIT_VERSION}\ndocker push ${IMAGE}:latest\n\n# Login to DigitalOcean command line\necho \"Authorizing DigitalOcean\"\n\ndoctl auth init -t \"${DO_ACCESS_TOKEN}\"\n\n# Decode SSH key\necho ${DO_SSH_KEY} | base64 -d > deploy_key\nchmod 600 deploy_key\n\n# Log into Droplet, stop the currently running container and start the new one\necho \"Stopping container name current and starting ${IMAGE}:${GIT_VERSION}\"\n\ndoctl compute ssh ${DROPLET} --ssh-key-path deploy_key --ssh-command \"docker pull ${IMAGE}:${GIT_VERSION} && \ndocker stop current && \ndocker rm current && \ndocker run --name=current --restart unless-stopped -e DEMO=true CLIENT_ID=${CLIENT_ID} -e CLIENT_SECRET=${CLIENT_SECRET} -d -p 80:5000 ${IMAGE}:${GIT_VERSION} &&\ndocker system prune -a -f &&\ndocker image prune -a -f\"\n"
  },
  {
    "path": "kubernetes.yml",
    "content": "apiVersion: v1\ndata:\n  client_id: // base64 encoded string\n  client_secret: // base64 encoded string\nkind: Secret\nmetadata:\n  name: takenote-pwd\n  labels:\n    app: takenote\n    component: server\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: takenote\n  labels:\n    app: takenote\n    component: server  \nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: takenote\n      component: server\n  template:\n    metadata:\n      labels:\n        app: takenote\n        component: server\n    spec:\n      containers:\n      - image: <your_alias/takenote:latest> \n        name: takenote         \n        env:\n        - name: CLIENT_ID\n          valueFrom:\n            secretKeyRef:\n              name: takenote-pwd\n              key: client_id\n        - name: CLIENT_SECRET\n          valueFrom:\n            secretKeyRef:\n              name: takenote-pwd\n              key: client_secret\n        - name: NODE_ENV\n          value: development\n        ports:\n        - containerPort: 5000\n          name: web\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: takenote\n  labels:\n    app: takenote\n    component: server\nspec:\n  ports:\n    - protocol: TCP\n      port: 8080\n      targetPort: 5000\n      name: web\n  selector:\n    app: takenote\n    component: server\n  type: ClusterIP\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"takenote\",\n  \"version\": \"0.7.2\",\n  \"description\": \"A web-based notes app for developers.\",\n  \"author\": \"Tania Rascia\",\n  \"license\": \"MIT\",\n  \"private\": false,\n  \"main\": \"src/server/index.ts\",\n  \"scripts\": {\n    \"dev\": \"concurrently \\\"npm run server\\\" \\\"npm run client\\\"\",\n    \"client\": \"cross-env NODE_ENV=development webpack serve --config config/webpack.dev.js\",\n    \"server\": \"nodemon --config config/nodemon.config.json\",\n    \"build\": \"cross-env NODE_ENV=production webpack --config config/webpack.prod.js\",\n    \"prod\": \"node -r ts-node/register/transpile-only src/server/index.ts\",\n    \"start\": \"npm run client\",\n    \"test\": \"jest --config config/jest.config.js\",\n    \"test:e2e\": \"cypress run --config-file config/cypress.config.json\",\n    \"test:e2e:open\": \"cypress open --config-file config/cypress.config.json\",\n    \"test:coverage\": \"jest --config config/jest.config.js --coverage --watchAll=false\",\n    \"test:coverage:ci\": \"jest --config config/jest.config.js --ci --coverage --watchAll=false && cat ./coverage/lcov.info | coveralls\",\n    \"format\": \"prettier --write \\\"./**/*.{js,jsx,ts,tsx,css,scss,md}\\\"\",\n    \"eslint\": \"eslint src/**/*.{ts,tsx}\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/taniarascia/takenote\"\n  },\n  \"keywords\": [\n    \"notes\",\n    \"notes-app\",\n    \"note-taking\",\n    \"markdown\",\n    \"markdown-editor\",\n    \"redux\",\n    \"react\",\n    \"typescript\",\n    \"react-hooks\",\n    \"react-hooks-redux\",\n    \"github\"\n  ],\n  \"bugs\": {\n    \"url\": \"https://github.com/taniarascia/takenote/issues\"\n  },\n  \"homepage\": \"https://takenote.dev\",\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"lint-staged\"\n    }\n  },\n  \"lint-staged\": {\n    \"**/*.{js,jsx,ts,tsx}\": [\n      \"eslint --fix\"\n    ],\n    \"**/*.{json,css,scss,md}\": [\n      \"prettier --write\"\n    ]\n  },\n  \"dependencies\": {\n    \"@reduxjs/toolkit\": \"^1.4.0\",\n    \"axios\": \"^0.21.1\",\n    \"clipboard-polyfill\": \"^3.0.1\",\n    \"codemirror\": \"^5.58.1\",\n    \"compression\": \"^1.7.4\",\n    \"cookie-parser\": \"^1.4.5\",\n    \"cors\": \"^2.8.5\",\n    \"dayjs\": \"^1.9.3\",\n    \"express\": \"^4.17.1\",\n    \"helmet\": \"^4.1.1\",\n    \"jszip\": \"^3.5.0\",\n    \"mousetrap\": \"^1.6.5\",\n    \"mousetrap-global-bind\": \"^1.1.0\",\n    \"path-browserify\": \"^1.0.1\",\n    \"prettier\": \"^2.1.2\",\n    \"process\": \"^0.11.10\",\n    \"react\": \"^16.14.0\",\n    \"react-beautiful-dnd\": \"^13.0.0\",\n    \"react-codemirror2\": \"^7.2.1\",\n    \"react-device-detect\": \"^1.14.0\",\n    \"react-dom\": \"^16.14.0\",\n    \"react-feather\": \"^2.0.8\",\n    \"react-helmet-async\": \"^1.0.7\",\n    \"react-markdown\": \"^4.3.1\",\n    \"react-redux\": \"^7.2.1\",\n    \"react-router-dom\": \"^5.2.0\",\n    \"react-split-pane\": \"^0.1.92\",\n    \"redux\": \"^4.0.5\",\n    \"redux-saga\": \"^1.1.3\",\n    \"stream-browserify\": \"^3.0.0\",\n    \"unist-util-visit\": \"^2.0.3\",\n    \"uuid\": \"^8.3.1\"\n  },\n  \"devDependencies\": {\n    \"@cypress/webpack-preprocessor\": \"^5.4.8\",\n    \"@testing-library/cypress\": \"^7.0.1\",\n    \"@testing-library/jest-dom\": \"^5.11.4\",\n    \"@testing-library/react\": \"^11.1.0\",\n    \"@types/axios\": \"^0.14.0\",\n    \"@types/codemirror\": \"0.0.98\",\n    \"@types/compression\": \"^1.7.0\",\n    \"@types/cookie-parser\": \"^1.4.2\",\n    \"@types/cors\": \"^2.8.8\",\n    \"@types/express\": \"^4.17.8\",\n    \"@types/faker\": \"^5.1.2\",\n    \"@types/helmet\": \"0.0.48\",\n    \"@types/jest\": \"^26.0.14\",\n    \"@types/jszip\": \"^3.4.1\",\n    \"@types/lodash\": \"^4.14.162\",\n    \"@types/node\": \"^14.11.8\",\n    \"@types/prettier\": \"^2.1.3\",\n    \"@types/react\": \"^16.9.52\",\n    \"@types/react-beautiful-dnd\": \"^13.0.0\",\n    \"@types/react-dom\": \"^16.9.8\",\n    \"@types/react-helmet-async\": \"^1.0.3\",\n    \"@types/react-redux\": \"^7.1.9\",\n    \"@types/react-router\": \"^5.1.8\",\n    \"@types/react-router-dom\": \"^5.1.6\",\n    \"@types/testing-library__cypress\": \"^5.0.8\",\n    \"@types/uuid\": \"^8.3.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^4.4.1\",\n    \"@typescript-eslint/parser\": \"^4.4.1\",\n    \"clean-webpack-plugin\": \"^3.0.0\",\n    \"clipboardy\": \"^2.3.0\",\n    \"concurrently\": \"^5.3.0\",\n    \"copy-webpack-plugin\": \"^6.2.1\",\n    \"coveralls\": \"^3.1.0\",\n    \"cross-env\": \"^7.0.2\",\n    \"css-loader\": \"^5.0.0\",\n    \"css-minimizer-webpack-plugin\": \"^1.1.5\",\n    \"cypress\": \"^5.4.0\",\n    \"cypress-file-upload\": \"^4.1.1\",\n    \"dotenv\": \"^8.2.0\",\n    \"eslint\": \"^7.11.0\",\n    \"eslint-config-prettier\": \"^6.13.0\",\n    \"eslint-import-resolver-alias\": \"^1.1.2\",\n    \"eslint-plugin-import\": \"^2.22.1\",\n    \"eslint-plugin-prettier\": \"^3.1.4\",\n    \"eslint-plugin-react\": \"^7.21.4\",\n    \"faker\": \"^5.1.0\",\n    \"html-webpack-plugin\": \"^5.0.0-alpha.7\",\n    \"husky\": \"^4.3.0\",\n    \"image-webpack-loader\": \"^7.0.1\",\n    \"jest\": \"^26.5.3\",\n    \"jest-extended\": \"^0.11.5\",\n    \"jest-raw-loader\": \"^1.0.1\",\n    \"lint-staged\": \"^10.4.1\",\n    \"mini-css-extract-plugin\": \"^1.0.0\",\n    \"node-sass\": \"^4.14.1\",\n    \"nodemon\": \"^2.0.5\",\n    \"sass-loader\": \"^10.0.3\",\n    \"style-loader\": \"^2.0.0\",\n    \"ts-jest\": \"^26.4.1\",\n    \"ts-loader\": \"^8.0.5\",\n    \"ts-node\": \"^9.0.0\",\n    \"typescript\": \"^4.0.3\",\n    \"webpack\": \"^5.1.3\",\n    \"webpack-cli\": \"^4.0.0\",\n    \"webpack-dev-server\": \"^3.11.0\",\n    \"webpack-merge\": \"^5.2.0\"\n  }\n}\n"
  },
  {
    "path": "public/_redirects",
    "content": "/*    /index.html   200"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n  \"short_name\": \"TakeNote\",\n  \"name\": \"A web-based notes app for developers.\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#5183f5\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "public/robots.txt",
    "content": "User-agent: *\n"
  },
  {
    "path": "public/template.html",
    "content": "<!DOCTYPE html>\n\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\" />\n    <title>TakeNote</title>\n  </head>\n\n  <body>\n    <%= htmlWebpackPlugin.options.disableReactDevtools %>\n    <div id=\"root\"></div>\n    <div id=\"context-menu\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "seed.js",
    "content": "const categories = [\n  {\n    id: 'goals',\n    name: 'goals',\n  },\n  {\n    id: 'health',\n    name: 'health',\n  },\n  {\n    id: 'design',\n    name: 'design',\n  },\n  {\n    id: 'development',\n    name: 'development',\n  },\n  {\n    id: 'personal',\n    name: 'personal',\n  },\n  {\n    id: 'recipes',\n    name: 'recipes',\n  },\n]\n\nconst notes = [\n  {\n    id: 'e0196fd9-d644-4ca8-aa58-467b8082993e',\n    text:\n      '## How Strings are Indexed\\n\\nEach of the characters in a string correspond to an index number, starting with `0`.\\n\\nTo demonstrate, we will create a string with the value `How are you?`.\\n\\n| H   | o   | w   |     | a   | r   | e   |     | y   | o   | u   | ?   |\\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\\n| 0   | 1   | 2   | 3   | 4   | 5   | 6   | 7   | 8   | 9   | 10  | 11  |',\n    created: '2019-10-14T17:03:27-05:00',\n    lastUpdated: '2019-10-14T17:03:27-05:00',\n    category: '',\n    favorite: false,\n  },\n  {\n    id: '6a2923a6-8fed-4277-9286-49125c91d876',\n    text:\n      \"Writing a Simple MVC App in Plain JavaScript\\n\\n---\\ndate: 2019-07-30\\ntitle: 'Writing a Simple MVC App in Plain JavaScript'\\ntemplate: post\\nthumbnail: '../thumbnails/triangle.png'\\nslug: javascript-mvc-todo-app\\ncategories:\\n  - Popular\\n  - Code\\ntags:\\n  - javascript\\n  - mvc\\n  - architecture\\n---\\n\\nI wanted to write a simple application in plain JavaScript using the [model-view-controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) architectural pattern. So I did, and here it is. Hopefully it helps you understand MVC, as it's a difficult concept to wrap your head around when you're first starting out.\\n\\nI made [this todo app](https://taniarascia.github.io/mvc), which is a simple little browser app that allows you to CRUD (create, read, update, and delete) todos. It just consists of an `index.html`, `style.css`, and `script.js`, so nice and simple and dependency/framework-free for learning purposes.\\n\\n#### Prerequisites\\n\\n- Basic JavaScript and HTML\\n- Familiarity with [the latest JavaScript syntax](https://www.taniarascia.com/es6-syntax-and-feature-overview/)\\n\\n#### Goals\\n\\nCreate a todo app in the browser with plain JavaScript, and get familiar with the concepts of MVC (and OOP - object-oriented programming).\\n\\n- [View demo](https://taniarascia.github.io/mvc)\\n- [View source](https://github.com/taniarascia/mvc)\\n\\n> **Note:** Since this app uses the latest JavaScript features (ES2017), it won't work as-is on some browsers like Safari without using Babel to compile to backwards-compatible JavaScript syntax.\\n\\n## What is Model View Controller?\\n\\nMVC is one possible pattern for organizing your code. It's a popular one.\\n\\n- **Model** - Manages the data of an application\\n- **View** - A visual representation of the model\\n- **Controller** - Links the user and the system\\n\\nThe **model** is the data. In this todo application, that'll be the actual todos, and the methods that will add, edit, or delete them.\\n\\nThe **view** is how the data is displayed. In this todo application, that will be the rendered HTML in the DOM and CSS.\\n\\nThe **controller** connects the model and the view. It takes user input, such as clicking or typing, and handles callbacks for user interactions.\\n\\nThe model never touches the view. The view never touches the model. The controller connects them.\\n\\n> I'd like to mention that doing MVC for a simple todo app is actually a ton of boilerplate. It would really be overcomplicating things if this was the app you wanted to create and you made this whole system. The point is to try to understand it on a small level so you can understand why a scaled system might use it.\",\n    created: '2019-10-14T17:01:39-05:00',\n    lastUpdated: '2019-10-14T17:01:49-05:00',\n    category: 'development',\n    favorite: false,\n  },\n  {\n    id: 'a4c5ea72-34ed-496d-aa90-096df7e1ffbd',\n    text:\n      \"# Create and Deploy a Node JS Server\\n\\nRecently, I wanted to create and host a Node server, and discovered that [Heroku](https://heroku.com) is an excellent cloud platform service that has free hobby hosting for Node and PostgreSQL, among many other languages and databases.\\n\\nThis tutorial walks through creating a local REST API with Node using an Express server and PostgreSQL database. It also lists the instructions for deploying to Heroku.\\n\\n#### Prerequisites\\n\\nThis guide uses installation instructions for macOS and assumes a prior knowledge of:\\n\\n- [Command line usage](/how-to-use-the-command-line-for-apple-macos-and-linux/)\\n- [Basic JavaScript](/javascript-day-one/)\\n- [Basic Node.js and npm](/how-to-install-and-use-node-js-and-npm-mac-and-windows/)\\n- [SQL](/overview-of-sql-commands-and-pdo-operations/) and [PostgreSQL](https://blog.logrocket.com/setting-up-a-restful-api-with-node-js-and-postgresql-d96d6fc892d8/)\\n- [Understanding REST/REST APIs](https://code.tutsplus.com/tutorials/code-your-first-api-with-nodejs-and-express-understanding-rest-apis--cms-31697)\\n\\n#### Goals\\n\\nThis walkthrough will have three parts:\\n\\n- [Setting up a local **PostgreSQL database**](#set-up-postgresql-database)\\n- [Setting up a local **Node/Express API server**](#create-express-api)\\n- [Deploying the Node, Express, PostgreSQL API to **Heroku**](#deploy-app-to-heroku)\\n\\nWe'll create a local, simple REST API in Node.js that runs on an Express server and utilizes PostgreSQL for a database. Then we'll deploy it to Heroku.\\n\\nI also have a few production tips for validation and rate limiting.\\n\\n- [4. Production tips](#production-tips)\\n\\n## Set Up PostgreSQL Database\\n\\nWe're going to:\\n\\n- Install PostgreSQL\\n- Create a user\\n- Create a database, table, and entry to the table\\n\\nThis will be a very quick runthrough - if it's your first time using PostgreSQL, or Express, I recommend reading [Setting up a RESTful API with Node.js and PostgreSQL](https://blog.logrocket.com/setting-up-a-restful-api-with-node-js-and-postgresql-d96d6fc892d8/).\\n\\nInstall and start PostgreSQL.\\n\\n```bash\\nbrew install postgresql\\nbrew services start postgresql\\n```\\n\\nLogin to `postgres`.\\n\\n```bash\\npsql postgres\\n```\\n\\nCreate a user and password and give them create database access.\\n\\n```bash\\nCREATE ROLE api_user WITH LOGIN PASSWORD 'password';\\nALTER ROLE api_user CREATEDB;\\n```\\n\\nLog out of the root user and log in to the newly created user.\\n\\n```bash\\n\\\\q\\npsql -d postgres -U api_user\\n```\\n\\nCreate a `books_api` database and connect to it.\\n\\n```sql\\nCREATE DATABASE books_api;\\n\\\\c books_api\\n```\\n\\nCreate a `books` table with `ID`, `author`, and `title`.\\n\\n```sql\\nCREATE TABLE books (\\n  ID SERIAL PRIMARY KEY,\\n  author VARCHAR(255) NOT NULL,\\n  title VARCHAR(255) NOT NULL\\n);\\n```\\n\\nInsert one entry into the new table.\\n\\n```sql\\nINSERT INTO books (author, title)\\nVALUES  ('J.K. Rowling', 'Harry Potter');\\n```\\n\\n## Create Express API\\n\\nThe Express API will set up an Express server and route to two endpoints, `GET` and `POST`.\\n\\nCreate the following files:\\n\\n- `.env` - file containing environment variables (does not get version controlled)\\n- `package.json` - information about the project and dependencies\\n- `init.sql` - file to initialize PostgreSQL table\\n- `config.js` - will create the database connection\\n- `index.js` - the Express server\\n\\n```bash\\ntouch .env package.json init.sql config.js index.js\\n```\",\n    created: '2019-10-14T17:00:31-05:00',\n    lastUpdated: '2019-10-14T17:00:55-05:00',\n    category: '',\n    favorite: false,\n    trash: true,\n  },\n  {\n    id: '645fdc64-8511-469d-a2db-c7f04a36a9af',\n    text:\n      \"# Roll Your Own Comment System\\n\\nA while ago, I [migrated my site from WordPress to Gatsby](/migrating-from-wordpress-to-gatsby/), a static site generator that runs on JavaScript/React. Gatsby [recommends Disqus](https://www.gatsbyjs.org/docs/adding-comments/) as an option for comments, and I briefly migrated all my comments over to it...until I looked at my site on a browser window without adblocker installed. I could see dozens of scripts injected into the site and even worse - truly egregious buzzfeed-esque ads embedded between all the comments. I decided it immediately had to go.\\n\\nI had no comments for a bit, but I felt like I had no idea what the reception of my articles was without having any place for people to leave comments. Occasionally people will leave useful critiques or tips on tutorials that can help future visitors as well, so I wanted to try adding something very simple back in.\\n\\nI looked at all the options, but I really didn't want to invest in setting up some third party code that I couldn't rely on, or something with ads. So I figured I'd set one up myself. I designed the simplest possible comment system in a day, which this blog now runs on.\\n\\nHere's some pros and cons to rolling your own comment system:\\n\\n#### Pros\\n\\n- Free\\n- No ads\\n- No third party scripts injected into your site\\n- Complete control over functionality and design\\n- Can be as simple or complicated as you want\\n- Little to no spam because spambots aren't set up to spam your custom content\\n- Easy to migrate - it all exists in one Heroku + Postgres server\\n\\n#### Cons\\n\\n- More work to set up\\n- Less features\\n\\nIf you've also struggled with this and wondered if there could be an easier way, or are just intrigued to see one person's implementation, read on!\\n \",\n    created: '2019-10-14T16:58:09-05:00',\n    lastUpdated: '2019-10-14T17:01:53-05:00',\n    category: 'development',\n    favorite: false,\n  },\n  {\n    id: 'b2808149-a40f-4f3c-83bc-94db34881241',\n    text:\n      \"# Developer Blogs to Follow\\n\\nI recently discovered that I ended up on the Hacker Noon awards for [Personal Developer Blog of the Year 2019](https://hackernoon.com/personal-developer-blog-of-the-year-hacker-noon-noonies-awards-2019-hz2tu32ql), which is an amazing honor! I got third place. I thought that was pretty neat, so I figured I'd mention it. Thank you all for reading, subscribing, and sharing my content!\\n\\nIn 2017, [I wrote a list](/web-developers-and-bloggers-i-follow-2017/) of some bloggers I follow, though much of the list wasn't actually web development related. I have a few favorites blogs I keep an eye on right now, so I'll share them with you.\\n\\nEveryone on this list has their own personal website/blog that isn't hosted on some third party like Medium, most of them have no ads, and I think they're all cool people in general.\\n\\n## Robin Wieruch\\n\\n- [robinwieruch.de](https://www.robinwieruch.de/)\\n\\nChances are, if you're looking for something about Firebase or React/Redux, you've probably ended up on Robin's blog. With good reason - he has tons of great tutorials.\\n\\n## Khalil Stemmler\\n\\n- [khalilstemmler.com](https://khalilstemmler.com/)\\n\\nKhalil is filling an all-too-rare niche in web development blogs, which is how to build large scale applications properly, specifically with TypeScript and Node. He's bridging the gap between intermediate and advanced, which is a difficult area to cover. Check it out if you're looking for something beyond \\\"Hello, World\\\"!\\n\\n## Flavio Copes\\n\\n- [flaviocopes.com](https://flaviocopes.com/)\\n\\nNo one is more prolific than Flavio. I honestly don't know how he has time to breathe, much less write all these tutorials. Not only does he write a blog post _every single day_, but he has endless handbooks, courses, and tutorials. Some of the posts are more like snippets, but you'll find nice, succint helpful stuff on there.\\n\\n## Dan Abramov\\n\\n- [overreacted.io](https://overreacted.io/)\\n\\nIf you're into JavaScript, and especially React, I'm sure you already know and love our React Overlord, Dan Abramov. Dan is known for his amazing contributions to JavaScript - Create React App and Redux - and his blog is known for long, insightful posts that cover unique areas of JavaScript and development in general. His [Things I Don't Know](https://overreacted.io/things-i-dont-know-as-of-2018/) and [Things I Know](https://overreacted.io/the-elements-of-ui-engineering/) have inspired many spinoff articles, including my [Everything I Know as a Software Developer Without a Degree](/everything-i-know-as-a-software-developer-without-a-degree/) post.\\n\\n## Swyx\",\n    created: '2019-10-14T16:57:24-05:00',\n    lastUpdated: '2019-10-14T16:57:42-05:00',\n    category: '',\n    favorite: true,\n  },\n  {\n    id: 'fa23b58e-2c2e-4c67-b6cd-4f7817ba7e89',\n    text:\n      \"# This, Bind, Call, and Apply\\n\\nThe [`this`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this) keyword is a very important concept in JavaScript, and also a particularly confusing one to both new developers and those who have experience in other programming languages. In JavaScript, `this` is a reference to an object. The object that `this` refers to can vary, implicitly based on whether it is global, on an object, or in a constructor, and can also vary explicitly based on usage of the `Function` prototype methods `bind`, `call`, and `apply`.\\n\\nAlthough `this` is a bit of a complex topic, it is also one that appears as soon as you begin writing your first JavaScript programs. Whether you're trying to access an element or event in [the Document Object Model (DOM)](https://www.digitalocean.com/community/tutorial_series/understanding-the-dom-document-object-model), building classes for writing in the object-oriented programming style, or using the properties and methods of regular objects, you will encounter `this`.\\n\\nIn this article, you'll learn what `this` refers to implicitly based on context, and you'll learn how to use the `bind`, `call`, and `apply` methods to explicitly determine the value of `this`.\\n\\n## Implicit Context\\n\\nThere are four main contexts in which the value of `this` can be implicitly inferred:\\n\\n- the global context\\n- as a method within an object\\n- as a constructor on a function or class\\n- as a DOM event handler\\n\\n### Global\\n\\nIn the global context, `this` refers to the [global object](https://developer.mozilla.org/en-US/docs/Glossary/Global_object). When you're working in a browser, the global context is would be `window`. When you're working in Node.js, the global context is `global`.\\n\\n> **Note:** If you are not yet familiar with the concept of scope in JavaScript, please review [Understanding Variables, Scope, and Hoisting in JavaScript](/understanding-variables-scope-hoisting-in-javascript).\\n\\nFor the examples, you will practice the code in the browser's Developer Tools console. Read [How to Use the JavaScript Developer Console](/how-to-use-the-javascript-developer-console) if you are not familiar with running JavaScript code in the browser.\\n\\nIf you log the value of `this` without any other code, you will see what object `this` refers to.\\n\\n```js\\nconsole.log(this)\\n```\\n\\n```terminal\\nWindow {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …}\\n```\\n\\nYou can see that `this` is `window`, which is the global object of a browser.\\n\\nIn [Understanding Variables, Scope, and Hoisting in JavaScript](/understanding-variables-scope-hoisting-in-javascript), you learned that functions have their own context for variables. You might be tempted to think that `this` would follow the same rules inside a function, but it does not. A top-level function will still retain the `this` reference of the global object.\\n\\nYou write a top-level function, or a function that is not associated with any object, like this:\\n\\n```js\\nfunction printThis() {\\n  console.log(this)\\n}\",\n    created: '2019-10-14T16:54:08-05:00',\n    lastUpdated: '2019-10-14T16:57:48-05:00',\n    category: '',\n    favorite: false,\n  },\n]\n\nlocalStorage.setItem('categories', JSON.stringify(categories))\nlocalStorage.setItem('notes', JSON.stringify(notes))\nwindow.location.reload()\n"
  },
  {
    "path": "src/client/api/index.ts",
    "content": "import { v4 as uuid } from 'uuid'\nimport dayjs from 'dayjs'\n\nimport { NoteItem, SyncPayload, SettingsState } from '@/types'\n\nconst scratchpadNote = {\n  id: uuid(),\n  text: `# Scratchpad\n\nThe easiest note to find.`,\n  category: '',\n  scratchpad: true,\n  favorite: false,\n  created: dayjs().format(),\n  lastUpdated: dayjs().format(),\n}\n\nconst markdown = `# Welcome to Takenote!\n\nTakeNote is a free, open-source notes app for the web. It is a demo project only, and does not integrate with any database or cloud. Your notes are saved in local storage and will not be permanently persisted, but are available for download.\n\nView the source on [Github](https://github.com/taniarascia/takenote).\n\n## Features\n\n- **Plain text notes** - take notes in an IDE-like environment that makes no assumptions\n- **Markdown preview** - view rendered HTML\n- **Linked notes** - use \\`{{uuid}}\\` syntax to link to notes within other notes\n- **Syntax highlighting** - light and dark mode available (based on the beautiful [New Moon theme](https://taniarascia.github.io/new-moon/))\n- **Keyboard shortcuts** - use the keyboard for all common tasks - creating notes and categories, toggling settings, and other options\n- **Drag and drop** - drag a note or multiple notes to categories, favorites, or trash\n- **Multi-cursor editing** - supports multiple cursors and other [Codemirror](https://codemirror.net/) options\n- **Search notes** - easily search all notes, or notes within a category\n- **Prettify notes** - use Prettier on the fly for your Markdown\n- **No WYSIWYG** - made for developers, by developers\n- **No database** - notes are only stored in the browser's local storage and are available for download and export to you alone\n- **No tracking or analytics** - 'nuff said\n- **GitHub integration** - self-hosted option is available for auto-syncing to a GitHub repository (not available in the demo)\n`\n\nconst welcomeNote = {\n  id: uuid(),\n  text: markdown,\n  category: '',\n  favorite: false,\n  created: dayjs().format(),\n  lastUpdated: dayjs().format(),\n}\n\ntype PromiseCallback = (value?: any) => void\ntype GetLocalStorage = (\n  key: string,\n  errorMessage?: string\n) => (resolve: PromiseCallback, reject: PromiseCallback) => void\n\nconst getLocalStorage: GetLocalStorage = (key, errorMessage = 'Something went wrong') => (\n  resolve,\n  reject\n) => {\n  const data = localStorage.getItem(key)\n\n  if (data) {\n    resolve(JSON.parse(data))\n  } else {\n    reject({\n      message: errorMessage,\n    })\n  }\n}\n\nconst getUserNotes = () => (resolve: PromiseCallback, reject: PromiseCallback) => {\n  const notes: any = localStorage.getItem('notes')\n\n  // check if there is any data in localstorage\n  if (!notes) {\n    // if there is none (i.e. new user), create the welcomeNote and scratchpadNote\n    resolve([scratchpadNote, welcomeNote])\n  } else if (Array.isArray(JSON.parse(notes))) {\n    // if there is (existing user), show the user's notes\n    resolve(\n      // find does not work if the array is empty.\n      JSON.parse(notes).length === 0 || !JSON.parse(notes).find((note: NoteItem) => note.scratchpad)\n        ? [scratchpadNote, ...JSON.parse(notes)]\n        : JSON.parse(notes)\n    )\n  } else {\n    reject({\n      message: 'Something went wrong',\n    })\n  }\n}\n\nexport const saveState = ({ categories, notes }: SyncPayload) =>\n  new Promise((resolve) => {\n    localStorage.setItem('categories', JSON.stringify(categories))\n    localStorage.setItem('notes', JSON.stringify(notes))\n\n    resolve({\n      categories: JSON.parse(localStorage.getItem('categories') || '[]'),\n      notes: JSON.parse(localStorage.getItem('notes') || '[]'),\n    })\n  })\n\nexport const saveSettings = ({ isOpen, ...settings }: SettingsState) =>\n  Promise.resolve(localStorage.setItem('settings', JSON.stringify(settings)))\n\nexport const requestNotes = () => new Promise(getUserNotes())\nexport const requestCategories = () => new Promise(getLocalStorage('categories'))\nexport const requestSettings = () => new Promise(getLocalStorage('settings'))\n"
  },
  {
    "path": "src/client/components/AppSidebar/ActionButton.tsx",
    "content": "import React, { MouseEventHandler } from 'react'\nimport { Icon } from 'react-feather'\n\nimport { iconColor } from '@/utils/constants'\n\nexport interface ActionButtonProps {\n  dataTestID: string\n  disabled?: boolean\n  handler: MouseEventHandler\n  icon: Icon\n  label: string\n  text: string\n}\n\nexport const ActionButton: React.FC<ActionButtonProps> = ({\n  dataTestID,\n  disabled = false,\n  handler,\n  icon: IconCmp,\n  label,\n  text,\n}) => {\n  return (\n    <button\n      data-testid={dataTestID}\n      className=\"action-button\"\n      aria-label={label}\n      onClick={handler}\n      disabled={disabled}\n      title={label}\n    >\n      <IconCmp\n        size={18}\n        className=\"action-button-icon\"\n        color={iconColor}\n        aria-hidden=\"true\"\n        focusable=\"false\"\n      />\n      <span>{text}</span>\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/client/components/AppSidebar/AddCategoryButton.tsx",
    "content": "import React from 'react'\nimport { Plus } from 'react-feather'\n\nimport { iconColor } from '@/utils/constants'\n\nexport interface AddCategoryButtonProps {\n  dataTestID: string\n  handler: (adding: boolean) => void\n  label: string\n}\n\nexport const AddCategoryButton: React.FC<AddCategoryButtonProps> = ({\n  dataTestID,\n  handler,\n  label,\n}) => {\n  return (\n    <button\n      data-testid={dataTestID}\n      className=\"category-button\"\n      onClick={() => handler(true)}\n      aria-label={label}\n    >\n      <Plus size={16} color={iconColor} />\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/client/components/AppSidebar/AddCategoryForm.tsx",
    "content": "import React from 'react'\n\nimport { TestID } from '@resources/TestID'\nimport { ReactSubmitEvent } from '@/types'\n\nexport interface AddCategoryFormProps {\n  dataTestID: string\n  submitHandler: (event: ReactSubmitEvent) => void\n  changeHandler: (editingCategoryId: string, value: string) => void\n  resetHandler: () => void\n  editingCategoryId: string\n  tempCategoryName: string\n}\n\nexport const AddCategoryForm: React.FC<AddCategoryFormProps> = ({\n  dataTestID,\n  submitHandler,\n  changeHandler,\n  resetHandler,\n  editingCategoryId,\n  tempCategoryName,\n}) => {\n  return (\n    <form data-testid={dataTestID} className=\"category-form\" onSubmit={submitHandler}>\n      <input\n        data-testid={TestID.NEW_CATEGORY_INPUT}\n        aria-label=\"Category name\"\n        type=\"text\"\n        autoFocus\n        maxLength={20}\n        placeholder=\"New category...\"\n        onChange={(event) => {\n          changeHandler(editingCategoryId, event.target.value)\n        }}\n        onBlur={(event) => {\n          if (!tempCategoryName || tempCategoryName.trim() === '') {\n            resetHandler()\n          } else {\n            submitHandler(event)\n          }\n        }}\n      />\n    </form>\n  )\n}\n"
  },
  {
    "path": "src/client/components/AppSidebar/CollapseCategoryButton.tsx",
    "content": "import React from 'react'\nimport { ChevronDown, ChevronRight, Layers } from 'react-feather'\n\nexport interface CollapseCategoryListButton {\n  dataTestID: string\n  handler: () => void\n  label: string\n  isCategoryListOpen: boolean\n  showIcon: boolean\n}\n\nexport const CollapseCategoryListButton: React.FC<CollapseCategoryListButton> = ({\n  dataTestID,\n  handler,\n  label,\n  isCategoryListOpen,\n  showIcon,\n}) => {\n  return (\n    <button\n      data-testid={dataTestID}\n      className=\"collapse-button\"\n      onClick={handler}\n      aria-label={label}\n    >\n      {showIcon ? (\n        isCategoryListOpen ? (\n          <ChevronDown size={16} />\n        ) : (\n          <ChevronRight size={16} />\n        )\n      ) : (\n        <Layers size={16} />\n      )}\n      <h2>Categories</h2>\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/client/components/AppSidebar/FolderOption.tsx",
    "content": "import React, { useState } from 'react'\nimport { Book, Star, Trash2 } from 'react-feather'\n\nimport { Folder } from '@/utils/enums'\nimport { iconColor } from '@/utils/constants'\nimport { ReactDragEvent } from '@/types'\n\nexport interface FolderOptionProps {\n  text: string\n  active: boolean\n  dataTestID: string\n  folder: Folder\n  swapFolder: (folder: Folder) => void\n  addNoteType: (noteId: string) => void\n}\n\nexport const FolderOption: React.FC<FolderOptionProps> = ({\n  text,\n  active,\n  dataTestID,\n  folder,\n  swapFolder,\n  addNoteType,\n}) => {\n  const [mainSectionDragState, setMainSectionDragState] = useState({\n    [Folder.ALL]: false,\n    [Folder.FAVORITES]: false,\n    [Folder.SCRATCHPAD]: false,\n    [Folder.TRASH]: false,\n    [Folder.CATEGORY]: false,\n  })\n  const dragEnterHandler = () => {\n    setMainSectionDragState({ ...mainSectionDragState, [folder]: true })\n  }\n  const dragLeaveHandler = () => {\n    setMainSectionDragState({ ...mainSectionDragState, [folder]: false })\n  }\n  const noteHandler = (event: ReactDragEvent) => {\n    event.preventDefault()\n\n    addNoteType(event.dataTransfer.getData('text'))\n    dragLeaveHandler()\n  }\n\n  const determineClass = () => {\n    if (active) {\n      return 'app-sidebar-link active'\n    } else if (mainSectionDragState[folder]) {\n      return 'app-sidebar-link dragged-over'\n    } else {\n      return 'app-sidebar-link'\n    }\n  }\n\n  const renderIcon = () => {\n    if (folder === 'FAVORITES') {\n      return <Star size={15} className=\"app-sidebar-icon\" color={iconColor} />\n    } else if (folder === 'ALL') {\n      return <Book size={15} className=\"app-sidebar-icon\" color={iconColor} />\n    } else {\n      return <Trash2 size={15} className=\"app-sidebar-icon\" color={iconColor} />\n    }\n  }\n\n  return (\n    <button\n      onClick={() => {\n        swapFolder(folder)\n      }}\n      className=\"app-sidebar-wrapper\"\n    >\n      <div\n        data-testid={dataTestID}\n        className={determineClass()}\n        onDrop={noteHandler}\n        onDragOver={(event: ReactDragEvent) => {\n          event.preventDefault()\n        }}\n        onDragEnter={dragEnterHandler}\n        onDragLeave={dragLeaveHandler}\n      >\n        {renderIcon()}\n        {text}\n      </div>\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/client/components/AppSidebar/ScratchpadOption.tsx",
    "content": "import React from 'react'\nimport { Edit } from 'react-feather'\n\nimport { TestID } from '@resources/TestID'\nimport { LabelText } from '@resources/LabelText'\nimport { Folder } from '@/utils/enums'\nimport { iconColor } from '@/utils/constants'\n\nexport interface ScratchpadOptionProps {\n  active: boolean\n  swapFolder: (folder: Folder) => {}\n}\n\nexport const ScratchpadOption: React.FC<ScratchpadOptionProps> = ({ active, swapFolder }) => {\n  return (\n    <button\n      onClick={() => {\n        swapFolder(Folder.SCRATCHPAD)\n      }}\n      className=\"app-sidebar-wrapper\"\n    >\n      <div data-testid={TestID.SCRATCHPAD} className={`app-sidebar-link ${active ? 'active' : ''}`}>\n        <Edit size={15} className=\"app-sidebar-icon\" color={iconColor} />\n        {LabelText.SCRATCHPAD}\n      </div>\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/client/components/Editor/EmptyEditor.tsx",
    "content": "import React from 'react'\n\nexport const EmptyEditor: React.FC = () => {\n  return (\n    <div className=\"empty-editor v-center\" data-testid=\"empty-editor\">\n      <div className=\"text-center\">\n        <p>\n          <strong>Create a note</strong>\n        </p>\n        <p>\n          <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>N</kbd>\n        </p>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/client/components/Editor/NoteLink.tsx",
    "content": "import React from 'react'\n\nimport { NoteItem } from '@/types'\nimport { Errors } from '@/utils/enums'\nimport { TestID } from '@resources/TestID'\n\nimport { getNoteTitle, getActiveNoteFromShortUuid } from '../../utils/helpers'\n\nexport interface NoteLinkProps {\n  uuid: string\n  notes: NoteItem[]\n  handleNoteLinkClick: (e: React.SyntheticEvent, note: NoteItem) => void\n}\n\nconst NoteLink: React.FC<NoteLinkProps> = ({ notes, uuid, handleNoteLinkClick }) => {\n  const note = getActiveNoteFromShortUuid(notes, uuid)\n  const title = note !== undefined ? getNoteTitle(note.text) : null\n\n  if (note && title)\n    return (\n      <a data-testid={TestID.NOTE_LINK_SUCCESS} onClick={(e) => handleNoteLinkClick(e, note)}>\n        {title}\n      </a>\n    )\n\n  return (\n    <span data-testid={TestID.NOTE_LINK_ERROR} className=\"error\">\n      {Errors.INVALID_LINKED_NOTE_ID}\n    </span>\n  )\n}\n\nexport default NoteLink\n"
  },
  {
    "path": "src/client/components/Editor/PreviewEditor.tsx",
    "content": "import React from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { useDispatch } from 'react-redux'\n\nimport { Folder } from '@/utils/enums'\nimport { updateActiveNote, updateSelectedNotes, pruneNotes, swapFolder } from '@/slices/note'\nimport { NoteItem } from '@/types'\n\nimport { uuidPlugin } from '../../utils/reactMarkdownPlugins'\n\nimport NoteLink from './NoteLink'\n\nexport interface PreviewEditorProps {\n  noteText: string\n  directionText: string\n  notes: NoteItem[]\n}\n\nexport const PreviewEditor: React.FC<PreviewEditorProps> = ({ noteText, directionText, notes }) => {\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _updateSelectedNotes = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateSelectedNotes({ noteId, multiSelect }))\n\n  const _updateActiveNote = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateActiveNote({ noteId, multiSelect }))\n\n  const _pruneNotes = () => dispatch(pruneNotes())\n\n  const _swapFolder = (folder: Folder) => dispatch(swapFolder({ folder }))\n\n  // ===========================================================================\n  // Handlers\n  // ===========================================================================\n\n  const handleNoteLinkClick = (e: React.SyntheticEvent, note: NoteItem) => {\n    e.preventDefault()\n\n    if (note) {\n      _updateActiveNote(note.id, false)\n      _updateSelectedNotes(note.id, false)\n      _pruneNotes()\n\n      if (note?.favorite) return _swapFolder(Folder.FAVORITES)\n      if (note?.scratchpad) return _swapFolder(Folder.SCRATCHPAD)\n      if (note?.trash) return _swapFolder(Folder.TRASH)\n\n      return _swapFolder(Folder.ALL)\n    }\n  }\n\n  const returnNoteLink = (value: string) => {\n    return <NoteLink uuid={value} notes={notes} handleNoteLinkClick={handleNoteLinkClick} />\n  }\n\n  return (\n    <ReactMarkdown\n      plugins={[uuidPlugin]}\n      renderers={{\n        uuid: ({ value }) => returnNoteLink(value),\n      }}\n      linkTarget=\"_blank\"\n      className={`previewer previewer_direction-${directionText}`}\n      source={noteText}\n    />\n  )\n}\n"
  },
  {
    "path": "src/client/components/LandingPage.tsx",
    "content": "import React from 'react'\nimport { isMobile } from 'react-device-detect'\n\nimport lightScreen from '@resources/assets/screenshot-light.png'\nimport darkScreen from '@resources/assets/screenshot-dark.png'\nimport squareLogo from '@resources/assets/logo-square-white.svg'\nimport logo from '@resources/assets/logo-square-color.svg'\nimport githubLogo from '@resources/assets/github-logo.png'\n\nconst clientId = process.env.CLIENT_ID\nconst isDemo = process.env.DEMO\n\nconst loginButton = (text: string) => (\n  <a\n    className=\"button github-button\"\n    href={`https://github.com/login/oauth/authorize?client_id=${clientId}&scope=repo`}\n  >\n    <img src={githubLogo} />\n    {text}\n  </a>\n)\n\nexport const LandingPage: React.FC = () => {\n  return (\n    <section className=\"landing-page\">\n      <section className=\"content\">\n        <div className=\"container-small\">\n          <div className=\"lead\">\n            <img src={logo} height=\"200\" width=\"200\" alt=\"TakeNote\" />\n            <h1>\n              The Note Taking App\n              <br /> for Developers\n            </h1>\n            <p className=\"subtitle\">A web-based notes app for developers.</p>\n            {isMobile ? (\n              <p className=\"p-mobile\">\n                TakeNote is not currently supported for tablet and mobile devices.\n              </p>\n            ) : isDemo ? (\n              <div className=\"new-signup\">\n                <div>\n                  <p>\n                    TakeNote is only available as a demo. Your notes will be saved to local storage\n                    and <b>not</b> persisted in any database or cloud.\n                  </p>\n                  <a className=\"button\" href=\"/app\">\n                    View Demo\n                  </a>\n                </div>\n              </div>\n            ) : (\n              <div className=\"new-signup\">\n                <div>\n                  <p>\n                    TakeNote does not have a database or users. It simply links with your GitHub\n                    account for authentication, and stores the data in a private{' '}\n                    <code>takenotes-data</code> repo.\n                  </p>\n                  <div className=\"cta\">{loginButton('Sign Up with GitHub')}</div>\n                </div>\n              </div>\n            )}\n          </div>\n        </div>\n        <div className=\"container\">\n          <img src={lightScreen} alt=\"TakeNote App\" className=\"screenshot\" />\n        </div>\n      </section>\n\n      <section className=\"content\">\n        <div className=\"container-small\">\n          <div className=\"features\">\n            <h2 className=\"text-center\">Features</h2>\n            <ul>\n              <li>\n                <strong>Plain text notes</strong> - take notes in an IDE-like environment that makes\n                no assumptions\n              </li>\n              <li>\n                <strong>Markdown preview</strong> - view rendered HTML\n              </li>\n              <li>\n                <strong>Linked notes</strong> - use <code>{`{{uuid}}`}</code> syntax to link to\n                notes within other notes\n              </li>\n              <li>\n                <strong>Syntax highlighting</strong> - light and dark mode available (based on the\n                beautiful <a href=\"https://taniarascia.github.io/new-moon/\">New Moon theme</a>)\n              </li>\n              <li>\n                <strong>Keyboard shortcuts</strong> - use the keyboard for all common tasks -\n                creating notes and categories, toggling settings, and other options\n              </li>\n              <li>\n                <strong>Drag and drop</strong> - drag a note or multiple notes to categories,\n                favorites, or trash\n              </li>\n              <li>\n                <strong>Multi-cursor editing</strong> - supports multiple cursors and other{' '}\n                <a href=\"https://codemirror.net/\">Codemirror</a> options\n              </li>\n              <li>\n                <strong>Search notes</strong> - easily search all notes, or notes within a category\n              </li>\n              <li>\n                <strong>Prettify notes</strong> - use Prettier on the fly for your Markdown\n              </li>\n              <li>\n                <strong>No WYSIWYG</strong> - made for developers, by developers\n              </li>\n              <li>\n                <strong>No database</strong> - notes are only stored in the browser&#39;s local\n                storage and are available for download and export to you alone\n              </li>\n              <li>\n                <strong>No tracking or analytics</strong> - &#39;nuff said\n              </li>\n              <li>\n                <strong>GitHub integration</strong> - self-hosted option is available for\n                auto-syncing to a GitHub repository (not available in the demo)\n              </li>\n            </ul>\n          </div>\n        </div>\n        <div className=\"container\">\n          <img src={darkScreen} alt=\"TakeNote App\" className=\"screenshot\" />\n        </div>\n      </section>\n\n      <footer className=\"footer\">\n        <div className=\"container-small\">\n          <img src={squareLogo} alt=\"TakeNote App\" className=\"logo\" />\n          <p>\n            <strong>TakeNote</strong>\n          </p>\n          <nav>\n            <a\n              href=\"https://github.com/taniarascia/takenote\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              Source\n            </a>\n            <a\n              href=\"https://github.com/taniarascia/takenote/issues\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              Issues\n            </a>\n            <a\n              href=\"https://github.com/taniarascia/takenote/graphs/contributors\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              Contributors\n            </a>\n          </nav>\n        </div>\n      </footer>\n    </section>\n  )\n}\n"
  },
  {
    "path": "src/client/components/LastSyncedNotification.tsx",
    "content": "import React from 'react'\nimport dayjs from 'dayjs'\n\nimport { TestID } from '@resources/TestID'\n\nexport interface LastSyncedNotificationProps {\n  datetime: string\n  pending: boolean\n  syncing: boolean\n}\n\nexport const LastSyncedNotification: React.FC<LastSyncedNotificationProps> = ({\n  datetime,\n  pending,\n  syncing,\n}) => {\n  const renderLastSynced = () => {\n    if (syncing) {\n      return <i data-testid={TestID.LAST_SYNCED_NOTIFICATION_SYNCING}>Syncing...</i>\n    }\n\n    if (pending) {\n      return <i data-testid={TestID.LAST_SYNCED_NOTIFICATION_UNSAVED}>Unsaved changes</i>\n    }\n\n    if (datetime) {\n      return (\n        <span data-testid={TestID.LAST_SYNCED_NOTIFICATION_DATE}>\n          {dayjs(datetime).format('LT on L')}\n        </span>\n      )\n    }\n  }\n\n  return <div className=\"last-synced\">{renderLastSynced()}</div>\n}\n"
  },
  {
    "path": "src/client/components/NoteList/ContextMenuOption.tsx",
    "content": "import React, { KeyboardEventHandler, MouseEventHandler, useContext } from 'react'\nimport { Icon } from 'react-feather'\n\nimport { MenuUtilitiesContext } from '@/containers/ContextMenu'\n\nexport interface ContextMenuOptionProps {\n  dataTestID: string\n  handler: MouseEventHandler & KeyboardEventHandler\n  icon: Icon\n  text: string\n  optionType?: string\n}\n\nexport const ContextMenuOption: React.FC<ContextMenuOptionProps> = ({\n  dataTestID,\n  handler,\n  optionType,\n  icon: IconCmp,\n  text,\n  ...rest\n}) => {\n  // ===========================================================================\n  // Context\n  // ===========================================================================\n\n  const { setOptionsId } = useContext(MenuUtilitiesContext)\n\n  // ===========================================================================\n  // Handlers\n  // ===========================================================================\n\n  const optionHandler: MouseEventHandler & KeyboardEventHandler = (\n    event: React.MouseEvent<Element, MouseEvent> & React.KeyboardEvent<Element>\n  ) => {\n    handler(event)\n    setOptionsId('')\n  }\n\n  return (\n    <div\n      data-testid={dataTestID}\n      className={optionType === 'delete' ? 'nav-item delete-option' : 'nav-item'}\n      role=\"button\"\n      onClick={optionHandler}\n      onKeyPress={optionHandler}\n      tabIndex={0}\n      {...rest}\n    >\n      <IconCmp className=\"nav-item-icon\" size={18} />\n      {text}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/client/components/NoteList/NoteListButton.tsx",
    "content": "import React, { MouseEventHandler } from 'react'\n\nexport interface NoteListButtonProps {\n  dataTestID: string\n  disabled?: boolean\n  handler: MouseEventHandler\n  label: string\n}\n\nexport const NoteListButton: React.FC<NoteListButtonProps> = ({\n  dataTestID,\n  disabled = false,\n  handler,\n  label,\n}) => {\n  return (\n    <button\n      data-testid={dataTestID}\n      className=\"list-button\"\n      aria-label={label}\n      onClick={handler}\n      disabled={disabled}\n      title={label}\n    >\n      {label}\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/client/components/NoteList/SearchBar.tsx",
    "content": "import React from 'react'\n\nimport { TestID } from '@resources/TestID'\n\nexport interface SearchBarProps {\n  searchRef: React.MutableRefObject<HTMLInputElement>\n  searchNotes: (searchValue: string) => void\n}\n\nexport const SearchBar: React.FC<SearchBarProps> = ({ searchRef, searchNotes }) => {\n  return (\n    <input\n      ref={searchRef}\n      data-testid={TestID.NOTE_SEARCH}\n      type=\"search\"\n      onChange={(event) => {\n        event.preventDefault()\n        searchNotes(event.target.value)\n      }}\n      placeholder=\"Search for notes\"\n      onDragOver={(e) => {\n        e.preventDefault()\n      }}\n    />\n  )\n}\n"
  },
  {
    "path": "src/client/components/NoteList/SelectCategory.tsx",
    "content": "import React from 'react'\n\nimport { TestID } from '@resources/TestID'\nimport { NoteItem, CategoryItem } from '@/types'\nimport { isDraftNote } from '@/utils/helpers'\n\nexport interface SelectCategoryProps {\n  onChange: (selectedOption: any) => void\n  categories: CategoryItem[]\n  note: NoteItem\n  activeCategoryId: string\n}\n\nexport const SelectCategory: React.FC<SelectCategoryProps> = ({\n  onChange,\n  categories,\n  note,\n  activeCategoryId,\n}) => {\n  const filteredCategories = categories\n    .filter(({ id }) => id !== activeCategoryId)\n    .filter((category) => category.id !== note.category)\n\n  return (\n    <>\n      {!note.trash && !isDraftNote(note) && filteredCategories.length > 0 && (\n        <select\n          data-testid={TestID.MOVE_TO_CATEGORY}\n          defaultValue=\"\"\n          className=\"nav-item move-to-category-select\"\n          onChange={onChange}\n        >\n          <option disabled value=\"\">\n            Move to category...\n          </option>\n          {filteredCategories\n            .filter((category) => category.id !== note.category)\n            .map((category) => (\n              <option key={category.id} value={category.id}>\n                {category.name}\n              </option>\n            ))}\n        </select>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "src/client/components/Select.tsx",
    "content": "import React from 'react'\n\nexport interface SelectOption {\n  label: string\n  value: string\n}\n\nexport interface SelectProps {\n  options: SelectOption[]\n  onChange: (selectedOption: SelectOption) => void\n  selectedValue: string\n  testId: string\n}\n\nexport const Select: React.FC<SelectProps> = ({ options, selectedValue, onChange, testId }) => {\n  const getSelectedOption = (options: SelectOption[], value: string): SelectOption => {\n    return options.filter((option: SelectOption) => {\n      return value === option.value\n    })[0]\n  }\n\n  return (\n    <select\n      onChange={(event) => onChange(getSelectedOption(options, event.target.value))}\n      value={selectedValue}\n      data-testid={testId}\n    >\n      {options.map((selectOption) => (\n        <option key={selectOption.value} value={selectOption.value}>\n          {selectOption.label}\n        </option>\n      ))}\n    </select>\n  )\n}\n"
  },
  {
    "path": "src/client/components/SettingsModal/IconButton.tsx",
    "content": "import React, { MouseEventHandler } from 'react'\nimport { Icon } from 'react-feather'\n\nimport { iconColor } from '@/utils/constants'\n\nexport interface IconButtonProps {\n  dataTestID?: string\n  disabled?: boolean\n  handler: MouseEventHandler\n  icon: Icon\n  text: string\n}\n\nexport const IconButton: React.FC<IconButtonProps> = ({\n  dataTestID,\n  disabled = false,\n  handler,\n  icon: IconCmp,\n  text,\n}) => {\n  return (\n    <button\n      data-testid={dataTestID}\n      aria-label={text}\n      onClick={handler}\n      disabled={disabled}\n      title={text}\n      className=\"icon-button\"\n    >\n      <IconCmp\n        size={18}\n        className=\"button-icon\"\n        color={iconColor}\n        aria-hidden=\"true\"\n        focusable=\"false\"\n      />\n      {text}\n    </button>\n  )\n}\n"
  },
  {
    "path": "src/client/components/SettingsModal/IconButtonUploader.tsx",
    "content": "import React, { ChangeEvent, useRef } from 'react'\nimport { Icon } from 'react-feather'\n\nimport { iconColor } from '@/utils/constants'\n\nexport interface IconButtonUploaderProps {\n  dataTestID?: string\n  disabled?: boolean\n  handler: (file: File) => void\n  icon: Icon\n  text: string\n  accept: string\n}\n\nexport const IconButtonUploader: React.FC<IconButtonUploaderProps> = ({\n  dataTestID,\n  disabled = false,\n  handler,\n  icon: IconCmp,\n  text,\n  accept,\n}) => {\n  const inputRef = useRef<HTMLInputElement>(null)\n\n  const handleClick = () => {\n    if (inputRef.current) {\n      inputRef.current.click()\n    }\n  }\n\n  const handleFileInput = (e: ChangeEvent<HTMLInputElement>) => {\n    if (e.target.files) {\n      handler(e.target.files[0])\n    }\n  }\n\n  return (\n    <div>\n      <input\n        data-testid={dataTestID}\n        accept={accept}\n        tabIndex={-1}\n        autoComplete=\"off\"\n        ref={inputRef}\n        type=\"file\"\n        onChange={handleFileInput}\n        className=\"hidden\"\n      />\n      <button\n        onClick={handleClick}\n        aria-label={text}\n        disabled={disabled}\n        title={text}\n        className=\"icon-button\"\n      >\n        <IconCmp\n          size={18}\n          className=\"button-icon\"\n          color={iconColor}\n          aria-hidden=\"true\"\n          focusable=\"false\"\n        />\n        {text}\n      </button>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/client/components/SettingsModal/Option.tsx",
    "content": "import React from 'react'\n\nimport { Switch } from '@/components/Switch'\n\nexport interface OptionProps {\n  title: string\n  description: string\n  toggle: () => void\n  checked: boolean\n  testId: string\n}\n\nexport const Option: React.FC<OptionProps> = ({ title, description, toggle, checked, testId }) => {\n  return (\n    <div className=\"settings-option\">\n      <div>\n        <h3>{title}</h3>\n        <p className=\"description\">{description}</p>\n      </div>\n      <Switch toggle={toggle} checked={checked} testId={testId} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/client/components/SettingsModal/SelectOptions.tsx",
    "content": "import React from 'react'\n\nimport { Select } from '../Select'\n\nexport interface OptionProps {\n  title: string\n  description: string\n  onChange: (selectedOption: any) => void\n  selectedValue: string\n  options: Array<{ value: string; label: string }>\n  testId: string\n}\n\nexport const SelectOptions: React.FC<OptionProps> = ({\n  title,\n  description,\n  onChange,\n  selectedValue,\n  options,\n  testId,\n}) => {\n  return (\n    <div className=\"settings-option\">\n      <div>\n        <h3>{title}</h3>\n        <p className=\"description\">{description}</p>\n      </div>\n      <Select options={options} onChange={onChange} selectedValue={selectedValue} testId={testId} />\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/client/components/SettingsModal/Shortcut.tsx",
    "content": "import React from 'react'\n\nexport interface ShortcutProps {\n  action: string\n  letter: string\n}\n\nexport const Shortcut: React.FC<ShortcutProps> = ({ action, letter }) => {\n  return (\n    <div className=\"settings-shortcut\">\n      <div>{action}</div>\n      <div className=\"keys\">\n        <kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>{letter}</kbd>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/client/components/Switch.tsx",
    "content": "import React from 'react'\n\nexport interface SwitchProps {\n  toggle: () => void\n  checked: boolean\n  testId: string\n}\n\nexport const Switch: React.FC<SwitchProps> = ({ toggle, checked, testId }) => {\n  return (\n    <label className=\"switch\" data-testid={testId}>\n      <input type=\"checkbox\" onChange={toggle} checked={checked} />\n      <span className=\"slider\" />\n    </label>\n  )\n}\n"
  },
  {
    "path": "src/client/components/Tabs/Tab.tsx",
    "content": "import React from 'react'\nimport { Icon } from 'react-feather'\n\nexport interface TabProps {\n  label: string\n  activeTab: string\n  onClick: (label: string) => void\n  icon: Icon\n}\n\nexport const Tab: React.FC<TabProps> = ({ activeTab, label, icon: IconCmp, onClick }) => {\n  const className = activeTab === label ? 'tab active' : 'tab'\n\n  return (\n    <div role=\"button\" key={label} className={className} onClick={() => onClick(label)}>\n      <IconCmp size={18} className=\"mr-1\" aria-hidden=\"true\" focusable=\"false\" /> {label}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/client/components/Tabs/TabPanel.tsx",
    "content": "import React from 'react'\nimport { Icon } from 'react-feather'\n\nexport interface TabPanelProps {\n  label: string\n  icon: Icon\n  children: JSX.Element[] | JSX.Element\n}\n\nexport const TabPanel: React.FC<TabPanelProps> = ({ children }) => {\n  return <section>{children}</section>\n}\n"
  },
  {
    "path": "src/client/components/Tabs/Tabs.tsx",
    "content": "import React, { Fragment, useState } from 'react'\n\nimport { Tab } from './Tab'\n\nexport interface TabsProps {\n  children: JSX.Element[]\n}\n\nexport const Tabs: React.FC<TabsProps> = ({ children }) => {\n  const [activeTab, setActiveTab] = useState('Preferences')\n\n  return (\n    <div className=\"tabs\">\n      <nav className=\"tab-list\">\n        {children.map((child) => {\n          const { label, icon } = child.props\n\n          return (\n            <Tab\n              icon={icon}\n              activeTab={activeTab}\n              key={label}\n              label={label}\n              onClick={setActiveTab}\n            />\n          )\n        })}\n      </nav>\n      <div className=\"tab-content\">\n        {children.map((child) => {\n          if (child.props.label !== activeTab) return\n\n          return (\n            <Fragment key={`${child.props.label}-panel`}>\n              <h3>{child.props.label}</h3>\n              {child.props.children}\n            </Fragment>\n          )\n        })}\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/client/containers/App.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { Route, Switch, Redirect } from 'react-router-dom'\nimport { Helmet, HelmetProvider } from 'react-helmet-async'\n\nimport { LandingPage } from '@/components/LandingPage'\nimport { TakeNoteApp } from '@/containers/TakeNoteApp'\nimport { PublicRoute } from '@/router/PublicRoute'\nimport { PrivateRoute } from '@/router/PrivateRoute'\nimport { getAuth } from '@/selectors'\nimport { login } from '@/slices/auth'\n\nconst isDemo = process.env.DEMO\n\nexport const App: React.FC = () => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { loading } = useSelector(getAuth)\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _login = () => dispatch(login())\n\n  // ===========================================================================\n  // Hooks\n  // ===========================================================================\n\n  useEffect(() => {\n    _login()\n  }, [])\n\n  if (loading) {\n    return (\n      <div className=\"loading\">\n        <div className=\"la-ball-beat\">\n          <div />\n          <div />\n          <div />\n        </div>\n      </div>\n    )\n  }\n\n  return (\n    <HelmetProvider>\n      <Helmet>\n        <meta charSet=\"utf-8\" />\n        <title>TakeNote</title>\n        <link rel=\"canonical\" href=\"https://takenote.dev\" />\n      </Helmet>\n\n      <Switch>\n        {isDemo ? (\n          <>\n            <Route exact path=\"/\" component={LandingPage} />\n            <Route path=\"/app\" component={TakeNoteApp} />\n          </>\n        ) : (\n          <>\n            <PublicRoute exact path=\"/\" component={LandingPage} />\n            <PrivateRoute path=\"/app\" component={TakeNoteApp} />\n          </>\n        )}\n\n        <Redirect to=\"/\" />\n      </Switch>\n    </HelmetProvider>\n  )\n}\n"
  },
  {
    "path": "src/client/containers/AppSidebar.tsx",
    "content": "import React from 'react'\nimport { Plus } from 'react-feather'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { LabelText } from '@resources/LabelText'\nimport { TestID } from '@resources/TestID'\nimport { ActionButton } from '@/components/AppSidebar/ActionButton'\nimport { FolderOption } from '@/components/AppSidebar/FolderOption'\nimport { ScratchpadOption } from '@/components/AppSidebar/ScratchpadOption'\nimport { Folder, NotesSortKey } from '@/utils/enums'\nimport { CategoryList } from '@/containers/CategoryList'\nimport {\n  addNote,\n  swapFolder,\n  updateActiveNote,\n  assignFavoriteToNotes,\n  assignTrashToNotes,\n  updateSelectedNotes,\n  unassignTrashFromNotes,\n} from '@/slices/note'\nimport { togglePreviewMarkdown } from '@/slices/settings'\nimport { getSettings, getNotes } from '@/selectors'\nimport { NoteItem } from '@/types'\nimport { newNoteHandlerHelper, getActiveNote } from '@/utils/helpers'\n\nexport const AppSidebar: React.FC = () => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { activeCategoryId, activeFolder, activeNoteId, notes } = useSelector(getNotes)\n  const { previewMarkdown, notesSortKey } = useSelector(getSettings)\n\n  const activeNote = getActiveNote(notes, activeNoteId)\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _addNote = (note: NoteItem) => dispatch(addNote(note))\n  const _updateActiveNote = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateActiveNote({ noteId, multiSelect }))\n  const _updateSelectedNotes = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateSelectedNotes({ noteId, multiSelect }))\n  const _swapFolder = (sortOrderKey: NotesSortKey) => (folder: Folder) =>\n    dispatch(swapFolder({ folder, sortOrderKey }))\n  const _togglePreviewMarkdown = () => dispatch(togglePreviewMarkdown())\n  const _assignTrashToNotes = (noteId: string) => dispatch(assignTrashToNotes(noteId))\n  const _unassignTrashFromNotes = (noteId: string) => dispatch(unassignTrashFromNotes(noteId))\n  const _assignFavoriteToNotes = (noteId: string) => dispatch(assignFavoriteToNotes(noteId))\n\n  // ===========================================================================\n  // Handlers\n  // ===========================================================================\n\n  const newNoteHandler = () =>\n    newNoteHandlerHelper(\n      activeFolder,\n      previewMarkdown,\n      activeNote,\n      activeCategoryId,\n      swapFolderHandler,\n      _togglePreviewMarkdown,\n      _addNote,\n      _updateActiveNote,\n      _updateSelectedNotes\n    )\n  const swapFolderHandler = _swapFolder(notesSortKey)\n\n  return (\n    <aside className=\"app-sidebar\">\n      <ActionButton\n        dataTestID={TestID.SIDEBAR_ACTION_CREATE_NEW_NOTE}\n        handler={newNoteHandler}\n        icon={Plus}\n        label={LabelText.CREATE_NEW_NOTE}\n        text={LabelText.NEW_NOTE}\n      />\n      <section className=\"app-sidebar-main\">\n        <ScratchpadOption\n          active={activeFolder === Folder.SCRATCHPAD}\n          swapFolder={swapFolderHandler}\n        />\n        <FolderOption\n          active={activeFolder === Folder.ALL}\n          swapFolder={swapFolderHandler}\n          text={LabelText.NOTES}\n          dataTestID={TestID.FOLDER_NOTES}\n          folder={Folder.ALL}\n          addNoteType={_unassignTrashFromNotes}\n        />\n        <FolderOption\n          active={activeFolder === Folder.FAVORITES}\n          text={LabelText.FAVORITES}\n          dataTestID={TestID.FOLDER_FAVORITES}\n          folder={Folder.FAVORITES}\n          swapFolder={swapFolderHandler}\n          addNoteType={_assignFavoriteToNotes}\n        />\n        <FolderOption\n          active={activeFolder === Folder.TRASH}\n          text={LabelText.TRASH}\n          dataTestID={TestID.FOLDER_TRASH}\n          folder={Folder.TRASH}\n          swapFolder={swapFolderHandler}\n          addNoteType={_assignTrashToNotes}\n        />\n        <CategoryList />\n      </section>\n    </aside>\n  )\n}\n"
  },
  {
    "path": "src/client/containers/CategoryList.tsx",
    "content": "import React, { useRef, useState, useEffect } from 'react'\nimport { useSelector, useDispatch } from 'react-redux'\nimport { v4 as uuid } from 'uuid'\nimport { Droppable } from 'react-beautiful-dnd'\n\nimport { LabelText } from '@resources/LabelText'\nimport { TestID } from '@resources/TestID'\nimport { CategoryOption } from '@/containers/CategoryOption'\nimport { getCategories } from '@/selectors'\nimport { shouldOpenContextMenu } from '@/utils/helpers'\nimport { ReactMouseEvent, ReactSubmitEvent, CategoryItem } from '@/types'\nimport { useTempState } from '@/contexts/TempStateContext'\nimport { setCategoryEdit, updateCategory, addCategory } from '@/slices/category'\nimport { AddCategoryForm } from '@/components/AppSidebar/AddCategoryForm'\nimport { AddCategoryButton } from '@/components/AppSidebar/AddCategoryButton'\nimport { CollapseCategoryListButton } from '@/components/AppSidebar/CollapseCategoryButton'\n\nexport const CategoryList: React.FC = () => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const {\n    categories,\n    editingCategory: { id: editingCategoryId, tempName: tempCategoryName },\n  } = useSelector(getCategories)\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _setCategoryEdit = (categoryId: string, tempName: string) =>\n    dispatch(setCategoryEdit({ id: categoryId, tempName }))\n  const _updateCategory = (category: CategoryItem) => dispatch(updateCategory(category))\n  const _addCategory = (category: CategoryItem) => dispatch(addCategory(category))\n\n  // ===========================================================================\n  // Refs\n  // ===========================================================================\n\n  const contextMenuRef = useRef<HTMLDivElement>(null)\n\n  // ===========================================================================\n  // State\n  // ===========================================================================\n\n  const [optionsId, setOptionsId] = useState('')\n  const [optionsPosition, setOptionsPosition] = useState({ x: 0, y: 0 })\n  const [isCategoryListOpen, setCategoryListOpen] = useState(true)\n\n  // ===========================================================================\n  // Context\n  // ===========================================================================\n\n  const { addingTempCategory, setAddingTempCategory } = useTempState()\n\n  // ===========================================================================\n  // Handlers\n  // ===========================================================================\n\n  const onAddCategory = (adding: boolean) => {\n    setCategoryListOpen(true)\n    setAddingTempCategory(adding)\n  }\n\n  const handleCategoryMenuClick = (\n    event: React.MouseEvent<HTMLDivElement, MouseEvent> | ReactMouseEvent,\n    categoryId: string = ''\n  ) => {\n    const clicked = event.target\n\n    // Make sure we aren't getting any null values .. any element clicked should be a sub-class of element\n    if (!clicked) return\n\n    if (shouldOpenContextMenu(clicked as Element)) {\n      if ('clientX' in event && 'clientY' in event) {\n        setOptionsPosition({ x: event.clientX, y: event.clientY })\n      }\n    }\n\n    event.stopPropagation()\n\n    if (!contextMenuRef?.current?.contains(clicked as HTMLDivElement)) {\n      setOptionsId(!optionsId || optionsId !== categoryId ? categoryId : '')\n    }\n  }\n\n  const handleCategoryRightClick = (\n    event: React.MouseEvent<HTMLDivElement, MouseEvent> | ReactMouseEvent,\n    categoryId: string = ''\n  ) => {\n    event.preventDefault()\n    handleCategoryMenuClick(event, categoryId)\n  }\n\n  const resetTempCategory = () => {\n    setAddingTempCategory(false)\n    _setCategoryEdit('', '')\n  }\n\n  const onSubmitUpdateCategory = (event: ReactSubmitEvent): void => {\n    event.preventDefault()\n\n    const category = { id: editingCategoryId, name: tempCategoryName.trim(), draggedOver: false }\n\n    if (categories.find((cat) => cat.name === category.name) || category.name === '') {\n      resetTempCategory()\n    } else {\n      _updateCategory(category)\n      resetTempCategory()\n    }\n  }\n\n  const onSubmitNewCategory = (event: ReactSubmitEvent): void => {\n    event.preventDefault()\n\n    const category = { id: uuid(), name: tempCategoryName.trim(), draggedOver: false }\n\n    if (categories.find((cat) => cat.name === category.name) || category.name === '') {\n      resetTempCategory()\n    } else {\n      _addCategory(category)\n      resetTempCategory()\n    }\n  }\n\n  // ===========================================================================\n  // Hooks\n  // ===========================================================================\n\n  useEffect(() => {\n    document.addEventListener('mousedown', handleCategoryMenuClick)\n\n    return () => {\n      document.removeEventListener('mousedown', handleCategoryMenuClick)\n    }\n  })\n\n  return (\n    <>\n      <div className=\"category-title\">\n        <CollapseCategoryListButton\n          dataTestID={TestID.CATEGORY_COLLAPSE_BUTTON}\n          handler={() => setCategoryListOpen(!isCategoryListOpen)}\n          label={LabelText.COLLAPSE_CATEGORY}\n          isCategoryListOpen={isCategoryListOpen}\n          showIcon={categories.length > 0}\n        />\n        <AddCategoryButton\n          dataTestID={TestID.ADD_CATEGORY_BUTTON}\n          handler={onAddCategory}\n          label={LabelText.ADD_CATEGORY}\n        />\n      </div>\n      {isCategoryListOpen && (\n        <>\n          <Droppable type=\"CATEGORY\" droppableId=\"Category list\">\n            {(droppableProvided) => (\n              <div\n                {...droppableProvided.droppableProps}\n                ref={droppableProvided.innerRef}\n                className=\"category-list\"\n                aria-label=\"Category list\"\n              >\n                {categories.map((category, index) => (\n                  <CategoryOption\n                    key={category.id}\n                    index={index}\n                    category={category}\n                    contextMenuRef={contextMenuRef}\n                    handleCategoryMenuClick={handleCategoryMenuClick}\n                    handleCategoryRightClick={handleCategoryRightClick}\n                    onSubmitUpdateCategory={onSubmitUpdateCategory}\n                    optionsId={optionsId}\n                    setOptionsId={setOptionsId}\n                    optionsPosition={optionsPosition}\n                  />\n                ))}\n                {droppableProvided.placeholder}\n              </div>\n            )}\n          </Droppable>\n          {addingTempCategory && (\n            <AddCategoryForm\n              dataTestID={TestID.NEW_CATEGORY_FORM}\n              submitHandler={onSubmitNewCategory}\n              changeHandler={_setCategoryEdit}\n              resetHandler={resetTempCategory}\n              editingCategoryId={editingCategoryId}\n              tempCategoryName={tempCategoryName}\n            />\n          )}\n        </>\n      )}\n    </>\n  )\n}\n"
  },
  {
    "path": "src/client/containers/CategoryOption.tsx",
    "content": "import React from 'react'\nimport { useSelector, useDispatch } from 'react-redux'\nimport { Draggable } from 'react-beautiful-dnd'\nimport { Folder as FolderIcon, MoreHorizontal } from 'react-feather'\n\nimport { TestID } from '@resources/TestID'\nimport { CategoryItem, ReactDragEvent, ReactMouseEvent, ReactSubmitEvent } from '@/types'\nimport { determineCategoryClass } from '@/utils/helpers'\nimport { getNotes, getCategories, getSettings } from '@/selectors'\nimport {\n  updateActiveCategoryId,\n  updateActiveNote,\n  updateSelectedNotes,\n  addCategoryToNote,\n} from '@/slices/note'\nimport { setCategoryEdit, categoryDragLeave, categoryDragEnter } from '@/slices/category'\nimport { iconColor } from '@/utils/constants'\nimport { ContextMenuEnum } from '@/utils/enums'\nimport { getNotesSorter } from '@/utils/notesSortStrategies'\nimport { ContextMenu } from '@/containers/ContextMenu'\n\ninterface CategoryOptionProps {\n  category: CategoryItem\n  index: number\n  contextMenuRef: React.RefObject<HTMLDivElement>\n  handleCategoryMenuClick: (\n    event: React.MouseEvent<HTMLDivElement, MouseEvent> | ReactMouseEvent,\n    categoryId?: string\n  ) => void\n  handleCategoryRightClick: (\n    event: React.MouseEvent<HTMLDivElement, MouseEvent> | ReactMouseEvent,\n    categoryId?: string\n  ) => void\n  onSubmitUpdateCategory: (event: ReactSubmitEvent) => void\n  optionsPosition: { x: number; y: number }\n  optionsId: string\n  setOptionsId: React.Dispatch<React.SetStateAction<string>>\n}\n\nexport const CategoryOption: React.FC<CategoryOptionProps> = ({\n  category,\n  index,\n  contextMenuRef,\n  handleCategoryMenuClick,\n  handleCategoryRightClick,\n  onSubmitUpdateCategory,\n  optionsPosition,\n  optionsId,\n  setOptionsId,\n}) => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { activeCategoryId, notes } = useSelector(getNotes)\n  const {\n    editingCategory: { id: editingCategoryId, tempName: tempCategoryName },\n  } = useSelector(getCategories)\n  const { notesSortKey } = useSelector(getSettings)\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _updateActiveCategoryId = (categoryId: string) =>\n    dispatch(updateActiveCategoryId(categoryId))\n  const _updateActiveNote = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateActiveNote({ noteId, multiSelect }))\n  const _updateSelectedNotes = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateSelectedNotes({ noteId, multiSelect }))\n  const _setCategoryEdit = (categoryId: string, tempName: string) =>\n    dispatch(setCategoryEdit({ id: categoryId, tempName }))\n  const _addCategoryToNote = (categoryId: string, noteId: string) =>\n    dispatch(addCategoryToNote({ categoryId, noteId }))\n  const _categoryDragEnter = (category: CategoryItem) => dispatch(categoryDragEnter(category))\n  const _categoryDragLeave = (category: CategoryItem) => dispatch(categoryDragLeave(category))\n\n  return (\n    <Draggable draggableId={category.id} index={index}>\n      {(draggableProvided, snapshot) => (\n        <div\n          {...draggableProvided.dragHandleProps}\n          {...draggableProvided.draggableProps}\n          ref={draggableProvided.innerRef}\n          data-testid={TestID.CATEGORY_LIST_DIV}\n          className={determineCategoryClass(category, snapshot.isDragging, activeCategoryId)}\n          onClick={() => {\n            const notesForNewCategory = notes\n              .filter((note) => !note.trash && note.category === category.id)\n              .sort(getNotesSorter(notesSortKey))\n\n            const defaultActiveNoteId =\n              notesForNewCategory.length > 0 ? notesForNewCategory[0].id : ''\n\n            if (category.id !== activeCategoryId) {\n              _updateActiveCategoryId(category.id)\n              _updateActiveNote(defaultActiveNoteId, false)\n              _updateSelectedNotes(defaultActiveNoteId, false)\n            }\n          }}\n          onDoubleClick={() => {\n            _setCategoryEdit(category.id, category.name)\n          }}\n          onBlur={() => {\n            _setCategoryEdit('', '')\n          }}\n          onDrop={(event) => {\n            event.preventDefault()\n\n            _addCategoryToNote(category.id, event.dataTransfer.getData('text'))\n            _categoryDragLeave(category)\n          }}\n          onDragOver={(event: ReactDragEvent) => event.preventDefault()}\n          onDragEnter={() => _categoryDragEnter(category)}\n          onDragLeave={() => _categoryDragLeave(category)}\n          onContextMenu={(event) => handleCategoryRightClick(event, category.id)}\n        >\n          <form\n            className=\"category-list-name\"\n            onSubmit={(event) => {\n              event.preventDefault()\n              _setCategoryEdit('', '')\n              onSubmitUpdateCategory(event)\n\n              if (optionsId) setOptionsId('')\n            }}\n          >\n            <FolderIcon size={15} className=\"app-sidebar-icon\" color={iconColor} />\n            {editingCategoryId === category.id ? (\n              <input\n                data-testid={TestID.CATEGORY_EDIT}\n                className=\"category-edit\"\n                type=\"text\"\n                autoFocus\n                maxLength={20}\n                value={tempCategoryName}\n                onChange={(event) => {\n                  _setCategoryEdit(editingCategoryId, event.target.value)\n                }}\n                onBlur={(event) => onSubmitUpdateCategory(event)}\n              />\n            ) : (\n              category.name\n            )}\n          </form>\n          <div\n            data-testid={TestID.MOVE_CATEGORY}\n            className={optionsId === category.id ? 'category-options active' : 'category-options'}\n            onClick={(event) => handleCategoryMenuClick(event, category.id)}\n          >\n            <MoreHorizontal size={15} className=\"context-menu-action\" />\n          </div>\n          {optionsId === category.id && (\n            <ContextMenu\n              contextMenuRef={contextMenuRef}\n              item={category}\n              optionsPosition={optionsPosition}\n              setOptionsId={setOptionsId}\n              type={ContextMenuEnum.CATEGORY}\n            />\n          )}\n        </div>\n      )}\n    </Draggable>\n  )\n}\n"
  },
  {
    "path": "src/client/containers/ContextMenu.tsx",
    "content": "import ReactDOM from 'react-dom'\nimport React, { useEffect, useState, createContext } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { SelectCategory } from '@/components/NoteList/SelectCategory'\nimport { ContextMenuOptions } from '@/containers/ContextMenuOptions'\nimport { addCategoryToNote, updateActiveCategoryId, updateActiveNote } from '@/slices/note'\nimport { NoteItem, CategoryItem } from '@/types'\nimport { getNotes, getCategories, getSettings } from '@/selectors'\nimport { ContextMenuEnum } from '@/utils/enums'\nimport { isDraftNote } from '@/utils/helpers'\n\nexport const MenuUtilitiesContext = createContext({\n  setOptionsId: (id: string) => {},\n})\ninterface Position {\n  x: number\n  y: number\n}\n\nexport interface ContextMenuProps {\n  item: NoteItem | CategoryItem\n  optionsPosition: Position\n  contextMenuRef: React.RefObject<HTMLDivElement> | null\n  setOptionsId: (id: string) => void\n  type: ContextMenuEnum\n}\n\nexport const ContextMenu: React.FC<ContextMenuProps> = ({\n  item,\n  optionsPosition,\n  contextMenuRef,\n  setOptionsId,\n  type,\n}) => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { darkTheme } = useSelector(getSettings)\n\n  // ===========================================================================\n  // State\n  // ===========================================================================\n\n  const [elementDimensions, setElementDimensions] = useState<{\n    offsetHeight: number | null\n    offsetWidth: number | null\n  }>({ offsetHeight: null, offsetWidth: null })\n\n  // ===========================================================================\n  // Hooks\n  // ===========================================================================\n\n  useEffect(() => {\n    if (contextMenuRef?.current) {\n      const { offsetHeight, offsetWidth } = contextMenuRef.current\n      setElementDimensions({ offsetHeight, offsetWidth })\n    }\n  }, [contextMenuRef])\n\n  // ===========================================================================\n  // Other\n  // ===========================================================================\n\n  const contextValues = {\n    setOptionsId,\n  }\n\n  const getOptionsYPosition = (): Number => {\n    if (elementDimensions.offsetHeight || elementDimensions.offsetWidth) {\n      // get the max window frame\n      const MaxY = window.innerHeight\n      const optionsSize = elementDimensions.offsetHeight as number\n\n      // if window position - noteOptions position isn't bigger than options, flip it.\n      return MaxY - optionsPosition.y > optionsSize\n        ? optionsPosition.y\n        : optionsPosition.y - optionsSize\n    }\n\n    return 0\n  }\n\n  return ReactDOM.createPortal(\n    <div className={type === ContextMenuEnum.CATEGORY || darkTheme ? 'dark' : ''}>\n      <div\n        ref={contextMenuRef}\n        className=\"options-context-menu\"\n        style={{\n          visibility: getOptionsYPosition() ? 'visible' : 'hidden',\n          position: 'absolute',\n          top: getOptionsYPosition() + 'px',\n          left: optionsPosition.x + 'px',\n        }}\n        onClick={(event) => {\n          event.stopPropagation()\n        }}\n      >\n        <MenuUtilitiesContext.Provider value={contextValues}>\n          {type === ContextMenuEnum.CATEGORY ? (\n            <CategoryMenu category={item as CategoryItem} />\n          ) : (\n            <NotesMenu note={item as NoteItem} setOptionsId={setOptionsId} />\n          )}\n        </MenuUtilitiesContext.Provider>\n      </div>\n    </div>,\n    document.getElementById('context-menu') as HTMLElement\n  )\n}\n\ninterface CategoryMenuProps {\n  category: CategoryItem\n}\n\nconst CategoryMenu: React.FC<CategoryMenuProps> = ({ category }) => {\n  return <ContextMenuOptions clickedItem={category} type={ContextMenuEnum.CATEGORY} />\n}\n\ninterface NotesMenuProps {\n  note: NoteItem\n  setOptionsId: (id: string) => void\n}\n\nconst NotesMenu: React.FC<NotesMenuProps> = ({ note, setOptionsId }) => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { categories } = useSelector(getCategories)\n  const { activeCategoryId } = useSelector(getNotes)\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _addCategoryToNote = (categoryId: string, noteId: string) =>\n    dispatch(addCategoryToNote({ categoryId, noteId }))\n  const _updateActiveNote = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateActiveNote({ noteId, multiSelect }))\n  const _updateActiveCategoryId = (categoryId: string) =>\n    dispatch(updateActiveCategoryId(categoryId))\n\n  return !isDraftNote(note) ? (\n    <>\n      {!note.scratchpad && (\n        <SelectCategory\n          onChange={(event) => {\n            _addCategoryToNote(event.target.value, note.id)\n\n            if (event.target.value !== activeCategoryId) {\n              _updateActiveCategoryId(event.target.value)\n              _updateActiveNote(note.id, false)\n            }\n\n            setOptionsId('')\n          }}\n          categories={categories}\n          activeCategoryId={activeCategoryId}\n          note={note}\n        />\n      )}\n      <ContextMenuOptions type={ContextMenuEnum.NOTE} clickedItem={note} />\n    </>\n  ) : null\n}\n"
  },
  {
    "path": "src/client/containers/ContextMenuOptions.tsx",
    "content": "import React, { useContext } from 'react'\nimport { ArrowUp, Download, Star, Trash, X, Edit2, Clipboard } from 'react-feather'\nimport { useDispatch, useSelector } from 'react-redux'\n\nimport { LabelText } from '@resources/LabelText'\nimport { TestID } from '@resources/TestID'\nimport { ContextMenuOption } from '@/components/NoteList/ContextMenuOption'\nimport { downloadNotes, isDraftNote, getShortUuid, copyToClipboard } from '@/utils/helpers'\nimport {\n  deleteNotes,\n  toggleFavoriteNotes,\n  toggleTrashNotes,\n  addCategoryToNote,\n  updateActiveNote,\n  swapFolder,\n  removeCategoryFromNotes,\n} from '@/slices/note'\nimport { getCategories, getNotes } from '@/selectors'\nimport { Folder, ContextMenuEnum } from '@/utils/enums'\nimport { CategoryItem, NoteItem } from '@/types'\nimport category, { setCategoryEdit, deleteCategory } from '@/slices/category'\nimport { MenuUtilitiesContext } from '@/containers/ContextMenu'\n\nexport interface ContextMenuOptionsProps {\n  clickedItem: NoteItem | CategoryItem\n  type: ContextMenuEnum\n}\n\nexport const ContextMenuOptions: React.FC<ContextMenuOptionsProps> = ({ clickedItem, type }) => {\n  if (type === 'CATEGORY') {\n    return <CategoryOptions clickedCategory={clickedItem as CategoryItem} />\n  } else {\n    return <NotesOptions clickedNote={clickedItem as NoteItem} />\n  }\n}\n\ninterface CategoryOptionsProps {\n  clickedCategory: CategoryItem\n}\n\nconst CategoryOptions: React.FC<CategoryOptionsProps> = ({ clickedCategory }) => {\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _deleteCategory = (categoryId: string) => dispatch(deleteCategory(categoryId))\n  const _removeCategoryFromNotes = (categoryId: string) =>\n    dispatch(removeCategoryFromNotes(categoryId))\n  const _swapFolder = (folder: Folder) => dispatch(swapFolder({ folder }))\n  const _setCategoryEdit = (categoryId: string, tempName: string) =>\n    dispatch(setCategoryEdit({ id: categoryId, tempName }))\n\n  // ===========================================================================\n  // Context\n  // ===========================================================================\n\n  const { setOptionsId } = useContext(MenuUtilitiesContext)\n\n  // ===========================================================================\n  // Handlers\n  // ===========================================================================\n\n  const startRenameHandler = () => {\n    _setCategoryEdit(clickedCategory.id, clickedCategory.name)\n    setOptionsId('')\n  }\n  const removeCategoryHandler = () => {\n    _deleteCategory(clickedCategory.id)\n    _removeCategoryFromNotes(clickedCategory.id)\n    _swapFolder(Folder.ALL)\n  }\n\n  return (\n    <nav className=\"options-nav\" data-testid={TestID.CATEGORY_OPTIONS_NAV}>\n      <ContextMenuOption\n        dataTestID={TestID.CATEGORY_OPTION_RENAME}\n        handler={startRenameHandler}\n        icon={Edit2}\n        text={LabelText.RENAME}\n      />\n      <ContextMenuOption\n        dataTestID={TestID.CATEGORY_OPTION_DELETE_PERMANENTLY}\n        handler={removeCategoryHandler}\n        icon={X}\n        text={LabelText.DELETE_PERMANENTLY}\n        optionType=\"delete\"\n      />\n    </nav>\n  )\n}\n\ninterface NotesOptionsProps {\n  clickedNote: NoteItem\n}\n\nconst NotesOptions: React.FC<NotesOptionsProps> = ({ clickedNote }) => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { selectedNotesIds, notes } = useSelector(getNotes)\n  const { categories } = useSelector(getCategories)\n\n  const selectedNotes = notes.filter((note) => selectedNotesIds.includes(note.id))\n  const isSelectedNotesDiffFavor = Boolean(\n    selectedNotes.find((note) => note.favorite) && selectedNotes.find((note) => !note.favorite)\n  )\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _deleteNotes = (noteIds: string[]) => dispatch(deleteNotes(noteIds))\n  const _toggleTrashNotes = (noteId: string) => dispatch(toggleTrashNotes(noteId))\n  const _toggleFavoriteNotes = (noteId: string) => dispatch(toggleFavoriteNotes(noteId))\n  const _addCategoryToNote = (categoryId: string, noteId: string) =>\n    dispatch(addCategoryToNote({ categoryId, noteId }))\n  const _updateActiveNote = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateActiveNote({ noteId, multiSelect }))\n\n  // ===========================================================================\n  // Handlers\n  // ===========================================================================\n\n  const deleteNotesHandler = () => _deleteNotes(selectedNotesIds)\n  const downloadNotesHandler = () =>\n    downloadNotes(\n      selectedNotesIds.includes(clickedNote.id) ? selectedNotes : [clickedNote],\n      categories\n    )\n  const favoriteNoteHandler = () => _toggleFavoriteNotes(clickedNote.id)\n  const trashNoteHandler = () => _toggleTrashNotes(clickedNote.id)\n  const removeCategoryFromNoteHandler = () => {\n    _addCategoryToNote('', clickedNote.id)\n    _updateActiveNote(clickedNote.id, false)\n  }\n  const copyLinkedNoteMarkdownHandler = (e: React.SyntheticEvent, note: NoteItem) => {\n    e.preventDefault()\n\n    const shortNoteUuid = getShortUuid(note.id)\n    copyToClipboard(`{{${shortNoteUuid}}}`)\n  }\n\n  return !isDraftNote(clickedNote) ? (\n    <nav className=\"options-nav\" data-testid={TestID.NOTE_OPTIONS_NAV}>\n      {clickedNote.trash && (\n        <>\n          <ContextMenuOption\n            dataTestID={TestID.NOTE_OPTION_DELETE_PERMANENTLY}\n            handler={deleteNotesHandler}\n            icon={X}\n            text={LabelText.DELETE_PERMANENTLY}\n            optionType=\"delete\"\n          />\n          <ContextMenuOption\n            dataTestID={TestID.NOTE_OPTION_RESTORE_FROM_TRASH}\n            handler={trashNoteHandler}\n            icon={ArrowUp}\n            text={LabelText.RESTORE_FROM_TRASH}\n          />\n        </>\n      )}\n      {!clickedNote.scratchpad && !clickedNote.trash && (\n        <>\n          <ContextMenuOption\n            dataTestID={TestID.NOTE_OPTION_FAVORITE}\n            handler={favoriteNoteHandler}\n            icon={Star}\n            text={\n              isSelectedNotesDiffFavor\n                ? LabelText.TOGGLE_FAVORITE\n                : clickedNote.favorite\n                ? LabelText.REMOVE_FAVORITE\n                : LabelText.MARK_AS_FAVORITE\n            }\n          />\n          <ContextMenuOption\n            dataTestID={TestID.NOTE_OPTION_TRASH}\n            handler={trashNoteHandler}\n            icon={Trash}\n            text={LabelText.MOVE_TO_TRASH}\n            optionType=\"delete\"\n          />\n        </>\n      )}\n      {clickedNote.category && !clickedNote.trash && (\n        <ContextMenuOption\n          dataTestID={TestID.NOTE_OPTION_REMOVE_CATEGORY}\n          handler={removeCategoryFromNoteHandler}\n          icon={X}\n          text={LabelText.REMOVE_CATEGORY}\n        />\n      )}\n      <ContextMenuOption\n        dataTestID={TestID.NOTE_OPTION_DOWNLOAD}\n        handler={downloadNotesHandler}\n        icon={Download}\n        text={LabelText.DOWNLOAD}\n      />\n      <ContextMenuOption\n        dataTestID={TestID.COPY_REFERENCE_TO_NOTE}\n        handler={(e: React.SyntheticEvent) => copyLinkedNoteMarkdownHandler(e, clickedNote)}\n        icon={Clipboard}\n        text={LabelText.COPY_REFERENCE_TO_NOTE}\n      />\n    </nav>\n  ) : null\n}\n"
  },
  {
    "path": "src/client/containers/KeyboardShortcuts.tsx",
    "content": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport prettier from 'prettier/standalone'\nimport parserMarkdown from 'prettier/parser-markdown'\n\nimport { useTempState } from '@/contexts/TempStateContext'\nimport { Folder, Shortcuts } from '@/utils/enums'\nimport { downloadNotes, getActiveNote, newNoteHandlerHelper } from '@/utils/helpers'\nimport { useKey } from '@/utils/hooks'\nimport {\n  addNote,\n  swapFolder,\n  toggleTrashNotes,\n  updateActiveNote,\n  updateSelectedNotes,\n  updateNote,\n} from '@/slices/note'\nimport { sync } from '@/slices/sync'\nimport { getCategories, getNotes, getSettings } from '@/selectors'\nimport { CategoryItem, NoteItem } from '@/types'\nimport { toggleDarkTheme, togglePreviewMarkdown, updateCodeMirrorOption } from '@/slices/settings'\n\nexport const KeyboardShortcuts: React.FC = () => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { categories } = useSelector(getCategories)\n  const { activeCategoryId, activeFolder, activeNoteId, notes, selectedNotesIds } = useSelector(\n    getNotes\n  )\n  const { darkTheme, previewMarkdown } = useSelector(getSettings)\n\n  const activeNote = getActiveNote(notes, activeNoteId)\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _addNote = (note: NoteItem) => dispatch(addNote(note))\n  const _updateActiveNote = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateActiveNote({ noteId, multiSelect }))\n  const _updateSelectedNotes = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateSelectedNotes({ noteId, multiSelect }))\n  const _swapFolder = (folder: Folder) => dispatch(swapFolder({ folder }))\n  const _toggleTrashNotes = (noteId: string) => dispatch(toggleTrashNotes(noteId))\n  const _sync = (notes: NoteItem[], categories: CategoryItem[]) =>\n    dispatch(sync({ notes, categories }))\n  const _togglePreviewMarkdown = () => dispatch(togglePreviewMarkdown())\n  const _toggleDarkTheme = () => dispatch(toggleDarkTheme())\n  const _updateCodeMirrorOption = (key: string, value: string) =>\n    dispatch(updateCodeMirrorOption({ key, value }))\n\n  // ===========================================================================\n  // State\n  // ===========================================================================\n\n  const { addingTempCategory, setAddingTempCategory } = useTempState()\n\n  // ===========================================================================\n  // Handlers\n  // ===========================================================================\n\n  const newNoteHandler = () =>\n    newNoteHandlerHelper(\n      activeFolder,\n      previewMarkdown,\n      activeNote,\n      activeCategoryId,\n      _swapFolder,\n      _togglePreviewMarkdown,\n      _addNote,\n      _updateActiveNote,\n      _updateSelectedNotes\n    )\n  const newTempCategoryHandler = () => !addingTempCategory && setAddingTempCategory(true)\n  const trashNoteHandler = () => _toggleTrashNotes(activeNote!.id)\n  const syncNotesHandler = () => _sync(notes, categories)\n  const downloadNotesHandler = () => {\n    if (!activeNote || selectedNotesIds.length === 0) return\n    downloadNotes(\n      selectedNotesIds.includes(activeNote.id)\n        ? notes.filter((note) => selectedNotesIds.includes(note.id))\n        : [activeNote],\n      categories\n    )\n  }\n  const togglePreviewMarkdownHandler = () => _togglePreviewMarkdown()\n  const toggleDarkThemeHandler = () => {\n    _toggleDarkTheme()\n    _updateCodeMirrorOption('theme', darkTheme ? 'base16-light' : 'new-moon')\n  }\n  const prettifyNoteHandler = () => {\n    // format current note with prettier\n    if (activeNote && activeNote.text) {\n      const formattedText = prettier.format(activeNote.text, {\n        parser: 'markdown',\n        plugins: [parserMarkdown],\n      })\n\n      const updatedNote = {\n        ...activeNote,\n        text: formattedText,\n      }\n\n      dispatch(updateNote(updatedNote))\n    }\n  }\n\n  // ===========================================================================\n  // Hooks\n  // ===========================================================================\n\n  useKey(Shortcuts.NEW_NOTE, () => newNoteHandler())\n  useKey(Shortcuts.NEW_CATEGORY, () => newTempCategoryHandler())\n  useKey(Shortcuts.DELETE_NOTE, () => trashNoteHandler())\n  useKey(Shortcuts.SYNC_NOTES, () => syncNotesHandler())\n  useKey(Shortcuts.DOWNLOAD_NOTES, () => downloadNotesHandler())\n  useKey(Shortcuts.PREVIEW, () => togglePreviewMarkdownHandler())\n  useKey(Shortcuts.TOGGLE_THEME, () => toggleDarkThemeHandler())\n  useKey(Shortcuts.PRETTIFY, () => prettifyNoteHandler())\n\n  return null\n}\n"
  },
  {
    "path": "src/client/containers/NoteEditor.tsx",
    "content": "import dayjs from 'dayjs'\nimport React from 'react'\nimport { Controlled as CodeMirror } from 'react-codemirror2'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { Editor } from 'codemirror'\n\nimport { getActiveNote } from '@/utils/helpers'\nimport { updateNote } from '@/slices/note'\nimport { NoteItem } from '@/types'\nimport { NoteMenuBar } from '@/containers/NoteMenuBar'\nimport { EmptyEditor } from '@/components/Editor/EmptyEditor'\nimport { PreviewEditor } from '@/components/Editor/PreviewEditor'\nimport { getNotes, getSettings, getSync } from '@/selectors'\nimport { setPendingSync } from '@/slices/sync'\n\nimport 'codemirror/lib/codemirror.css'\nimport 'codemirror/theme/base16-light.css'\nimport 'codemirror/mode/gfm/gfm'\nimport 'codemirror/addon/selection/active-line'\nimport 'codemirror/addon/scroll/scrollpastend'\n\nexport const NoteEditor: React.FC = () => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { pendingSync } = useSelector(getSync)\n  const { activeNoteId, loading, notes } = useSelector(getNotes)\n  const { codeMirrorOptions, previewMarkdown } = useSelector(getSettings)\n\n  const activeNote = getActiveNote(notes, activeNoteId)\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _updateNote = (note: NoteItem) => {\n    !pendingSync && dispatch(setPendingSync())\n    dispatch(updateNote(note))\n  }\n\n  const setEditorOverlay = (editor: Editor) => {\n    const query = /\\{\\{[^}]*}}/g\n    editor.addOverlay({\n      token: function (stream: any) {\n        query.lastIndex = stream.pos\n        var match = query.exec(stream.string)\n        if (match && match.index == stream.pos) {\n          stream.pos += match[0].length || 1\n\n          return 'notelink'\n        } else if (match) {\n          stream.pos = match.index\n        } else {\n          stream.skipToEnd()\n        }\n      },\n    })\n  }\n\n  const renderEditor = () => {\n    if (loading) {\n      return <div className=\"empty-editor v-center\">Loading...</div>\n    } else if (!activeNote) {\n      return <EmptyEditor />\n    } else if (previewMarkdown) {\n      return (\n        <PreviewEditor\n          directionText={codeMirrorOptions.direction}\n          noteText={activeNote.text}\n          notes={notes}\n        />\n      )\n    }\n\n    return (\n      <CodeMirror\n        data-testid=\"codemirror-editor\"\n        className=\"editor mousetrap\"\n        value={activeNote.text}\n        options={codeMirrorOptions}\n        editorDidMount={(editor) => {\n          setTimeout(() => {\n            editor.focus()\n          }, 0)\n          editor.setCursor(0)\n          setEditorOverlay(editor)\n        }}\n        onBeforeChange={(editor, data, value) => {\n          _updateNote({\n            id: activeNote.id,\n            text: value,\n            created: activeNote.created,\n            lastUpdated: dayjs().format(),\n          })\n        }}\n        onChange={(editor, data, value) => {\n          if (!value) {\n            editor.focus()\n          }\n        }}\n        onPaste={(editor, event: any) => {\n          // Get around pasting issue\n          // https://github.com/scniro/react-codemirror2/issues/77\n          if (!event.clipboardData || !event.clipboardData.items || !event.clipboardData.items[0])\n            return\n          event.clipboardData.items[0].getAsString((pasted: any) => {\n            if (editor.getSelection() !== pasted) return\n            const { anchor, head } = editor.listSelections()[0]\n            editor.setCursor({\n              line: Math.max(anchor.line, head.line),\n              ch: Math.max(anchor.ch, head.ch),\n            })\n          })\n        }}\n      />\n    )\n  }\n\n  return (\n    <main className=\"note-editor\">\n      <NoteMenuBar />\n      {renderEditor()}\n    </main>\n  )\n}\n"
  },
  {
    "path": "src/client/containers/NoteList.tsx",
    "content": "import React, { useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { MoreHorizontal, Book, Star, Folder as FolderIcon } from 'react-feather'\n\nimport { TestID } from '@resources/TestID'\nimport { Folder, Shortcuts, ContextMenuEnum } from '@/utils/enums'\nimport { NoteListButton } from '@/components/NoteList/NoteListButton'\nimport { SearchBar } from '@/components/NoteList/SearchBar'\nimport { ContextMenu } from '@/containers/ContextMenu'\nimport { getNoteTitle, shouldOpenContextMenu, debounceEvent, isDraftNote } from '@/utils/helpers'\nimport { useKey } from '@/utils/hooks'\nimport {\n  permanentlyEmptyTrash,\n  pruneNotes,\n  updateActiveNote,\n  searchNotes,\n  updateSelectedNotes,\n} from '@/slices/note'\nimport { NoteItem, ReactDragEvent, ReactMouseEvent } from '@/types'\nimport { getNotes, getSettings, getCategories } from '@/selectors'\nimport { getNotesSorter } from '@/utils/notesSortStrategies'\n\nexport const NoteList: React.FC = () => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { notesSortKey } = useSelector(getSettings)\n  const { activeCategoryId, activeFolder, selectedNotesIds, notes, searchValue } =\n    useSelector(getNotes)\n  const { categories } = useSelector(getCategories)\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _updateSelectedNotes = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateSelectedNotes({ noteId, multiSelect }))\n  const _permanentlyEmptyTrash = () => dispatch(permanentlyEmptyTrash())\n  const _pruneNotes = () => dispatch(pruneNotes())\n  const _updateActiveNote = (noteId: string, multiSelect: boolean) =>\n    dispatch(updateActiveNote({ noteId, multiSelect }))\n  const _searchNotes = debounceEvent(\n    (searchValue: string) => dispatch(searchNotes(searchValue)),\n    100\n  )\n\n  // ===========================================================================\n  // Refs\n  // ===========================================================================\n\n  const contextMenuRef = useRef<HTMLDivElement>(null)\n  const searchRef = React.useRef() as React.MutableRefObject<HTMLInputElement>\n\n  // ===========================================================================\n  // State\n  // ===========================================================================\n\n  const [optionsId, setOptionsId] = useState('')\n  const [optionsPosition, setOptionsPosition] = useState({ x: 0, y: 0 })\n\n  const re = new RegExp(searchValue.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'i')\n  const isMatch = (result: NoteItem) => re.test(result.text)\n\n  const filter: Record<Folder, (note: NoteItem) => boolean> = {\n    [Folder.CATEGORY]: (note) => !note.trash && note.category === activeCategoryId,\n    [Folder.SCRATCHPAD]: (note) => !!note.scratchpad,\n    [Folder.FAVORITES]: (note) => !note.trash && !!note.favorite,\n    [Folder.TRASH]: (note) => !!note.trash,\n    [Folder.ALL]: (note) => !note.trash && !note.scratchpad,\n  }\n\n  const filteredNotes: NoteItem[] = notes\n    .filter(filter[activeFolder])\n    .filter(isMatch)\n    .sort(getNotesSorter(notesSortKey))\n\n  // ===========================================================================\n  // Handlers\n  // ===========================================================================\n\n  const focusSearchHandler = () => searchRef.current.focus()\n\n  const handleDragStart = (event: ReactDragEvent, noteId: string = '') => {\n    event.stopPropagation()\n\n    event.dataTransfer.setData('text/plain', noteId)\n  }\n\n  const handleNoteOptionsClick = (event: ReactMouseEvent, noteId: string = '') => {\n    const clicked = event.target\n\n    // Make sure we aren't getting any null values. Any element clicked should be a sub-class of element\n    if (!clicked) return\n\n    // Ensure the clicked target is supposed to open the context menu\n    if (shouldOpenContextMenu(clicked as Element)) {\n      // note: don't check for MouseEvent because Cypress MouseEvent !== Window.MouseEvent\n      if ('pageX' in event && 'pageY' in event) {\n        setOptionsPosition({ x: event.pageX, y: event.pageY })\n      }\n    }\n\n    event.stopPropagation()\n\n    if (!contextMenuRef.current || !contextMenuRef.current.contains(clicked as HTMLDivElement)) {\n      setOptionsId(!optionsId || optionsId !== noteId ? noteId : '')\n    }\n  }\n\n  const handleNoteRightClick = (\n    event: React.MouseEvent<HTMLDivElement, MouseEvent>,\n    noteId: string = ''\n  ) => {\n    event.preventDefault()\n    const clicked = event.target\n    const RIGHT_CLICK = 2\n\n    // Make sure we aren't getting any null values .. any element clicked should be a sub-class of element\n    if (!clicked) return\n\n    // FIXME: This feels hacky\n    if (event.ctrlKey) return\n\n    // Make sure we are not right clicking on the menu\n    if (optionsId && event.button == RIGHT_CLICK) return\n\n    if ('clientX' in event && 'clientY' in event) {\n      setOptionsPosition({ x: event.clientX, y: event.clientY })\n    }\n\n    event.stopPropagation()\n\n    if (!contextMenuRef.current || contextMenuRef.current.contains(clicked as HTMLDivElement)) {\n      setOptionsId(!optionsId || optionsId !== noteId ? noteId : '')\n    }\n  }\n\n  const showEmptyTrash = activeFolder === Folder.TRASH && filteredNotes.length > 0\n\n  // ===========================================================================\n  // Hooks\n  // ===========================================================================\n\n  useEffect(() => {\n    document.addEventListener('mousedown', handleNoteOptionsClick)\n\n    return () => {\n      document.removeEventListener('mousedown', handleNoteOptionsClick)\n    }\n  })\n\n  useKey(Shortcuts.SEARCH, () => focusSearchHandler())\n\n  return (\n    <aside className=\"note-sidebar\">\n      <div className=\"note-sidebar-header\">\n        <SearchBar searchRef={searchRef} searchNotes={_searchNotes} />\n        {showEmptyTrash && (\n          <NoteListButton\n            dataTestID={TestID.EMPTY_TRASH_BUTTON}\n            label=\"Empty\"\n            handler={() => _permanentlyEmptyTrash()}\n          >\n            Empty Trash\n          </NoteListButton>\n        )}\n      </div>\n      <div data-testid={TestID.NOTE_LIST} className=\"note-list\">\n        {filteredNotes.map((note: NoteItem, index: number) => {\n          let noteTitle: string | React.ReactElement = getNoteTitle(note.text)\n          const noteCategory = categories.find((category) => category.id === note.category)\n\n          if (searchValue) {\n            const highlightStart = noteTitle.search(re)\n\n            if (highlightStart !== -1) {\n              const highlightEnd = highlightStart + searchValue.length\n\n              noteTitle = (\n                <>\n                  {noteTitle.slice(0, highlightStart)}\n                  <strong className=\"highlighted\">\n                    {noteTitle.slice(highlightStart, highlightEnd)}\n                  </strong>\n                  {noteTitle.slice(highlightEnd)}\n                </>\n              )\n            }\n          }\n\n          return (\n            <div\n              data-testid={TestID.NOTE_LIST_ITEM + index}\n              className={\n                selectedNotesIds.includes(note.id) ? 'note-list-each selected' : 'note-list-each'\n              }\n              key={note.id}\n              onClick={(event) => {\n                event.stopPropagation()\n\n                _updateSelectedNotes(note.id, event.metaKey)\n                _updateActiveNote(note.id, event.metaKey)\n                _pruneNotes()\n              }}\n              onContextMenu={(event) => handleNoteRightClick(event, note.id)}\n              draggable={note.text !== ''}\n              onDragStart={(event) => handleDragStart(event, note.id)}\n            >\n              <div className=\"note-list-outer\">\n                <div data-testid={'note-title-' + index} className=\"note-title\">\n                  {note.favorite ? (\n                    <>\n                      <div className=\"icon\">\n                        <Star aria-hidden=\"true\" className=\"note-favorite\" size={12} />\n                        <span className=\"sr-only\">Favorite note</span>\n                      </div>\n                      <div className=\"truncate-text\">{noteTitle}</div>\n                    </>\n                  ) : (\n                    <>\n                      <div className=\"icon\" />\n                      <div className=\"truncate-text\"> {noteTitle}</div>\n                    </>\n                  )}\n                </div>\n                {!isDraftNote(note) ? (\n                  <div\n                    // TODO: make testID based off of index when we add that to a NoteItem object\n                    data-testid={TestID.NOTE_OPTIONS_DIV + index}\n                    className={optionsId === note.id ? 'note-options selected' : 'note-options'}\n                    onClick={(event) => handleNoteOptionsClick(event, note.id)}\n                  >\n                    <MoreHorizontal aria-hidden=\"true\" size={15} className=\"context-menu-action\" />\n                    <span className=\"sr-only\">Note options</span>\n                  </div>\n                ) : (\n                  <div className=\"note-options\">&nbsp;</div>\n                )}\n              </div>\n              {(activeFolder === Folder.ALL || activeFolder === Folder.FAVORITES) && (\n                <div className=\"note-category\">\n                  {!!noteCategory ? (\n                    <>\n                      <FolderIcon size={12} className=\"context-menu-action\" />\n                      {noteCategory?.name}\n                    </>\n                  ) : (\n                    <>\n                      <Book size={12} className=\"context-menu-action\" />\n                      Notes\n                    </>\n                  )}\n                </div>\n              )}\n              {optionsId === note.id && !isDraftNote(note) && (\n                <ContextMenu\n                  contextMenuRef={contextMenuRef}\n                  item={note}\n                  optionsPosition={optionsPosition}\n                  setOptionsId={setOptionsId}\n                  type={ContextMenuEnum.NOTE}\n                />\n              )}\n            </div>\n          )\n        })}\n      </div>\n    </aside>\n  )\n}\n"
  },
  {
    "path": "src/client/containers/NoteMenuBar.tsx",
    "content": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  Eye,\n  Edit,\n  Star,\n  Trash2,\n  Download,\n  RefreshCw,\n  Loader,\n  Settings,\n  Sun,\n  Moon,\n  Clipboard as ClipboardCmp,\n} from 'react-feather'\n\nimport { TestID } from '@resources/TestID'\nimport { LastSyncedNotification } from '@/components/LastSyncedNotification'\nimport { NoteItem, CategoryItem } from '@/types'\nimport {\n  toggleSettingsModal,\n  togglePreviewMarkdown,\n  toggleDarkTheme,\n  updateCodeMirrorOption,\n} from '@/slices/settings'\nimport { toggleFavoriteNotes, toggleTrashNotes } from '@/slices/note'\nimport { getCategories, getNotes, getSync, getSettings } from '@/selectors'\nimport { downloadNotes, isDraftNote, getShortUuid, copyToClipboard } from '@/utils/helpers'\nimport { sync } from '@/slices/sync'\n\nexport const NoteMenuBar = () => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { notes, activeNoteId } = useSelector(getNotes)\n  const { categories } = useSelector(getCategories)\n  const { syncing, lastSynced, pendingSync } = useSelector(getSync)\n  const { darkTheme } = useSelector(getSettings)\n\n  // ===========================================================================\n  // Other\n  // ===========================================================================\n\n  const copyNoteIcon = <ClipboardCmp size={18} aria-hidden=\"true\" focusable=\"false\" />\n  const successfulCopyMessage = 'Note copied!'\n  const activeNote = notes.find((note) => note.id === activeNoteId)!\n  const shortNoteUuid = getShortUuid(activeNoteId)\n\n  // ===========================================================================\n  // State\n  // ===========================================================================\n\n  const [uuidCopiedText, setUuidCopiedText] = useState<string>('')\n  const [isToggled, togglePreviewIcon] = useState<boolean>(false)\n\n  // ===========================================================================\n  // Hooks\n  // ===========================================================================\n\n  useEffect(() => {\n    if (uuidCopiedText === successfulCopyMessage) {\n      const timer = setTimeout(() => {\n        setUuidCopiedText('')\n      }, 3000)\n\n      return () => clearTimeout(timer)\n    }\n  }, [uuidCopiedText])\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _togglePreviewMarkdown = () => dispatch(togglePreviewMarkdown())\n  const _toggleTrashNotes = (noteId: string) => dispatch(toggleTrashNotes(noteId))\n  const _toggleFavoriteNotes = (noteId: string) => dispatch(toggleFavoriteNotes(noteId))\n  const _sync = (notes: NoteItem[], categories: CategoryItem[]) =>\n    dispatch(sync({ notes, categories }))\n  const _toggleSettingsModal = () => dispatch(toggleSettingsModal())\n  const _toggleDarkTheme = () => dispatch(toggleDarkTheme())\n  const _updateCodeMirrorOption = (key: string, value: any) =>\n    dispatch(updateCodeMirrorOption({ key, value }))\n\n  // ===========================================================================\n  // Handlers\n  // ===========================================================================\n\n  const downloadNotesHandler = () => downloadNotes([activeNote], categories)\n  const favoriteNoteHandler = () => _toggleFavoriteNotes(activeNoteId)\n  const trashNoteHandler = () => _toggleTrashNotes(activeNoteId)\n  const syncNotesHandler = () => _sync(notes, categories)\n  const settingsHandler = () => _toggleSettingsModal()\n  const toggleDarkThemeHandler = () => {\n    _toggleDarkTheme()\n    _updateCodeMirrorOption('theme', darkTheme ? 'base16-light' : 'new-moon')\n  }\n  const togglePreviewHandler = () => {\n    togglePreviewIcon(!isToggled)\n    _togglePreviewMarkdown()\n  }\n\n  return (\n    <section className=\"note-menu-bar\">\n      {activeNote && !isDraftNote(activeNote) ? (\n        <nav>\n          <button\n            className=\"note-menu-bar-button\"\n            onClick={togglePreviewHandler}\n            data-testid={TestID.PREVIEW_MODE}\n          >\n            {isToggled ? (\n              <Edit aria-hidden=\"true\" size={18} />\n            ) : (\n              <Eye aria-hidden=\"true\" size={18} />\n            )}\n            <span className=\"sr-only\">{isToggled ? 'Edit note' : 'Preview note'}</span>\n          </button>\n          {!activeNote.scratchpad && (\n            <>\n              <button className=\"note-menu-bar-button\" onClick={favoriteNoteHandler}>\n                <Star aria-hidden=\"true\" size={18} />\n                <span className=\"sr-only\">Add note to favorites</span>\n              </button>\n              <button className=\"note-menu-bar-button trash\" onClick={trashNoteHandler}>\n                <Trash2 aria-hidden=\"true\" size={18} />\n                <span className=\"sr-only\">Delete note</span>\n              </button>\n            </>\n          )}\n          <button className=\"note-menu-bar-button\">\n            <Download aria-hidden=\"true\" size={18} onClick={downloadNotesHandler} />\n            <span className=\"sr-only\">Download note</span>\n          </button>\n          <button\n            className=\"note-menu-bar-button uuid\"\n            onClick={() => {\n              copyToClipboard(`{{${shortNoteUuid}}}`)\n              setUuidCopiedText(successfulCopyMessage)\n            }}\n            data-testid={TestID.UUID_MENU_BAR_COPY_ICON}\n          >\n            {copyNoteIcon}\n            {uuidCopiedText && <span className=\"uuid-copied-text\">{uuidCopiedText}</span>}\n            <span className=\"sr-only\">Copy note</span>\n          </button>\n        </nav>\n      ) : (\n        <div />\n      )}\n      <nav>\n        <LastSyncedNotification datetime={lastSynced} pending={pendingSync} syncing={syncing} />\n        <button\n          className=\"note-menu-bar-button\"\n          onClick={syncNotesHandler}\n          data-testid={TestID.TOPBAR_ACTION_SYNC_NOTES}\n        >\n          {syncing ? (\n            <Loader aria-hidden=\"true\" size={18} className=\"rotating-svg\" />\n          ) : (\n            <RefreshCw aria-hidden=\"true\" size={18} />\n          )}\n          <span className=\"sr-only\">Sync notes</span>\n        </button>\n        <button className=\"note-menu-bar-button\" onClick={toggleDarkThemeHandler}>\n          {darkTheme ? <Sun aria-hidden=\"true\" size={18} /> : <Moon aria-hidden=\"true\" size={18} />}\n          <span className=\"sr-only\">Themes</span>\n        </button>\n\n        <button className=\"note-menu-bar-button\" onClick={settingsHandler}>\n          <Settings aria-hidden=\"true\" size={18} />\n          <span className=\"sr-only\">Settings</span>\n        </button>\n      </nav>\n    </section>\n  )\n}\n"
  },
  {
    "path": "src/client/containers/SettingsModal.tsx",
    "content": "import React, { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n  X,\n  Command,\n  Settings,\n  Archive,\n  Edit2,\n  Download,\n  DownloadCloud,\n  UploadCloud,\n} from 'react-feather'\n\nimport {\n  toggleSettingsModal,\n  updateCodeMirrorOption,\n  togglePreviewMarkdown,\n  toggleDarkTheme,\n  updateNotesSortStrategy,\n} from '@/slices/settings'\nimport { updateNotes, importNotes } from '@/slices/note'\nimport { logout } from '@/slices/auth'\nimport { importCategories } from '@/slices/category'\nimport { shortcutMap, notesSortOptions, directionTextOptions } from '@/utils/constants'\nimport { CategoryItem, NoteItem, ReactMouseEvent } from '@/types'\nimport { getSettings, getAuth, getNotes, getCategories } from '@/selectors'\nimport { Option } from '@/components/SettingsModal/Option'\nimport { Shortcut } from '@/components/SettingsModal/Shortcut'\nimport { SelectOptions } from '@/components/SettingsModal/SelectOptions'\nimport { IconButton } from '@/components/SettingsModal/IconButton'\nimport { NotesSortKey } from '@/utils/enums'\nimport { backupNotes, downloadNotes } from '@/utils/helpers'\nimport { Tabs } from '@/components/Tabs/Tabs'\nimport { TabPanel } from '@/components/Tabs/TabPanel'\nimport { LabelText } from '@resources/LabelText'\nimport { TestID } from '@resources/TestID'\nimport { IconButtonUploader } from '@/components/SettingsModal/IconButtonUploader'\n\nexport const SettingsModal: React.FC = () => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { codeMirrorOptions, isOpen, previewMarkdown, darkTheme, notesSortKey } = useSelector(\n    getSettings\n  )\n  const { currentUser } = useSelector(getAuth)\n  const { notes, activeFolder, activeCategoryId } = useSelector(getNotes)\n  const { categories } = useSelector(getCategories)\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _logout = () => dispatch(logout())\n  const _toggleSettingsModal = () => dispatch(toggleSettingsModal())\n  const _togglePreviewMarkdown = () => dispatch(togglePreviewMarkdown())\n  const _toggleDarkTheme = () => dispatch(toggleDarkTheme())\n  const _updateNotesSortStrategy = (sortBy: NotesSortKey) =>\n    dispatch(updateNotesSortStrategy(sortBy))\n  const _updateCodeMirrorOption = (key: string, value: any) =>\n    dispatch(updateCodeMirrorOption({ key, value }))\n  const _updateNotes = (sortOrderKey: NotesSortKey) =>\n    dispatch(updateNotes({ notes, activeFolder, activeCategoryId, sortOrderKey }))\n  const _importBackup = (notes: NoteItem[], categories: CategoryItem[]) => {\n    dispatch(importNotes(notes))\n    dispatch(importCategories(categories))\n  }\n\n  // ===========================================================================\n  // Refs\n  // ===========================================================================\n\n  const node = useRef<HTMLDivElement>(null)\n\n  // ===========================================================================\n  // Handlers\n  // ===========================================================================\n\n  const handleDomClick = (event: ReactMouseEvent) => {\n    event.stopPropagation()\n\n    if (node.current && node.current.contains(event.target as HTMLDivElement)) return\n    if (isOpen) {\n      _toggleSettingsModal()\n    }\n  }\n\n  const togglePreviewMarkdownHandler = () => _togglePreviewMarkdown()\n  const toggleDarkThemeHandler = () => {\n    _toggleDarkTheme()\n    _updateCodeMirrorOption('theme', darkTheme ? 'base16-light' : 'new-moon')\n  }\n  const toggleLineHighlight = () =>\n    _updateCodeMirrorOption('styleActiveLine', !codeMirrorOptions.styleActiveLine)\n  const toggleScrollPastEnd = () =>\n    _updateCodeMirrorOption('scrollPastEnd', !codeMirrorOptions.scrollPastEnd)\n  const toggleLineNumbersHandler = () =>\n    _updateCodeMirrorOption('lineNumbers', !codeMirrorOptions.lineNumbers)\n  const handleEscPress = (event: KeyboardEvent) => {\n    event.stopPropagation()\n    if (event.key === 'Escape' && isOpen) {\n      _toggleSettingsModal()\n    }\n  }\n  const updateNotesSortStrategyHandler = (selectedOption: any) => {\n    _updateNotesSortStrategy(selectedOption.value)\n    _updateNotes(selectedOption.value)\n  }\n  const updateNotesDirectionHandler = (selectedOption: any) => {\n    _updateCodeMirrorOption('direction', selectedOption.value)\n  }\n  const downloadNotesHandler = () => downloadNotes(notes, categories)\n  const backupHandler = () => backupNotes(notes, categories)\n  const importBackupHandler = async (json: File) => {\n    const content = await json.text()\n    const { notes, categories } = JSON.parse(content) as {\n      notes: NoteItem[]\n      categories: CategoryItem[]\n    }\n\n    if (!notes || !categories) return\n\n    _importBackup(notes, categories)\n  }\n  // ===========================================================================\n  // Hooks\n  // ===========================================================================\n\n  useEffect(() => {\n    document.addEventListener('mousedown', handleDomClick)\n    document.addEventListener('keydown', handleEscPress)\n\n    return () => {\n      document.removeEventListener('mousedown', handleDomClick)\n      document.removeEventListener('keydown', handleEscPress)\n    }\n  })\n\n  return isOpen ? (\n    <div className=\"dimmer\">\n      <aside ref={node} className=\"settings-modal\">\n        <header className=\"settings-modal-header\">\n          <div\n            className=\"close-button\"\n            onClick={() => {\n              if (isOpen) _toggleSettingsModal()\n            }}\n          >\n            <X size={20} />\n          </div>\n\n          <section className=\"profile flex\">\n            <div>\n              {currentUser.avatar_url && (\n                <img src={currentUser.avatar_url} alt=\"Profile\" className=\"profile-picture\" />\n              )}\n            </div>\n            <div className=\"profile-details\">\n              <h3>{currentUser.name}</h3>\n              <div className=\"subtitle\">{currentUser.bio}</div>\n            </div>\n            <button\n              onClick={() => {\n                _logout()\n              }}\n            >\n              Log out\n            </button>\n          </section>\n        </header>\n\n        <section className=\"settings-content\">\n          <Tabs>\n            <TabPanel label=\"Preferences\" icon={Settings}>\n              <Option\n                title=\"Active line highlight\"\n                description=\"Controls whether the editor should highlight the active line\"\n                toggle={toggleLineHighlight}\n                checked={codeMirrorOptions.styleActiveLine}\n                testId={TestID.ACTIVE_LINE_HIGHLIGHT_TOGGLE}\n              />\n              <Option\n                title=\"Display line numbers\"\n                description=\"Controls whether the editor should display line numbers\"\n                toggle={toggleLineNumbersHandler}\n                checked={codeMirrorOptions.lineNumbers}\n                testId={TestID.DISPLAY_LINE_NUMS_TOGGLE}\n              />\n              <Option\n                title=\"Scroll past end\"\n                description=\"Controls whether the editor will add blank space to the end of all files\"\n                toggle={toggleScrollPastEnd}\n                checked={codeMirrorOptions.scrollPastEnd}\n                testId={TestID.SCROLL_PAST_END_TOGGLE}\n              />\n              <Option\n                title=\"Markdown preview\"\n                description=\"Controls whether markdown preview mode is enabled\"\n                toggle={togglePreviewMarkdownHandler}\n                checked={previewMarkdown}\n                testId={TestID.MARKDOWN_PREVIEW_TOGGLE}\n              />\n              <Option\n                title=\"Dark mode\"\n                description=\"Controls the theme of the application and editor\"\n                toggle={toggleDarkThemeHandler}\n                checked={darkTheme}\n                testId={TestID.DARK_MODE_TOGGLE}\n              />\n              <SelectOptions\n                title=\"Sort By\"\n                description=\"Controls the sort strategy of the notes\"\n                onChange={updateNotesSortStrategyHandler}\n                options={notesSortOptions}\n                selectedValue={notesSortKey}\n                testId={TestID.SORT_BY_DROPDOWN}\n              />\n              <SelectOptions\n                title=\"Text direction\"\n                description=\"Controls the direction of the text\"\n                onChange={updateNotesDirectionHandler}\n                options={directionTextOptions}\n                selectedValue={codeMirrorOptions.direction}\n                testId={TestID.TEXT_DIRECTION_DROPDOWN}\n              />\n            </TabPanel>\n            <TabPanel label=\"Keyboard shortcuts\" icon={Command}>\n              {shortcutMap.map((shortcut) => (\n                <Shortcut action={shortcut.action} letter={shortcut.key} key={shortcut.key} />\n              ))}\n            </TabPanel>\n            <TabPanel label=\"Data management\" icon={Archive}>\n              <p>Download all notes as Markdown files in a zip.</p>\n              <IconButton\n                dataTestID={TestID.SETTINGS_MODAL_DOWNLOAD_NOTES}\n                handler={downloadNotesHandler}\n                icon={Download}\n                text={LabelText.DOWNLOAD_ALL_NOTES}\n              />\n              <p>Export TakeNote data as JSON.</p>\n              <IconButton\n                handler={backupHandler}\n                icon={DownloadCloud}\n                text={LabelText.BACKUP_ALL_NOTES}\n              />\n              <p>Import TakeNote JSON file.</p>\n              <IconButtonUploader\n                dataTestID={TestID.UPLOAD_SETTINGS_BACKUP}\n                accept=\".json\"\n                handler={importBackupHandler}\n                icon={UploadCloud}\n                text={LabelText.IMPORT_BACKUP}\n              />\n            </TabPanel>\n            <TabPanel label=\"About TakeNote\" icon={Edit2}>\n              <p>\n                TakeNote is a minimalist note-taking web app for developers. Write in plain text or\n                Markdown in an IDE-like environment.\n              </p>\n              <p>\n                This app has no tracking or analytics and does not retain any user data. Notes are\n                persisted in local storage and can be downloaded as Markdown files from the data\n                management tab.\n              </p>\n              <p>\n                TakeNote was created by{' '}\n                <a href=\"https://www.taniarascia.com\" target=\"_blank\" rel=\"noreferrer\">\n                  Tania Rascia\n                </a>{' '}\n                with the help of{' '}\n                <a\n                  href=\"https://github.com/taniarascia/takenote/graphs/contributors\"\n                  target=\"_blank\"\n                  rel=\"noreferrer\"\n                >\n                  the open-source community\n                </a>\n                .\n              </p>\n              <p>\n                <a\n                  className=\"button\"\n                  href=\"https://github.com/taniarascia/takenote\"\n                  target=\"_blank\"\n                  rel=\"noreferrer\"\n                >\n                  View source\n                </a>\n              </p>\n            </TabPanel>\n          </Tabs>\n        </section>\n      </aside>\n    </div>\n  ) : null\n}\n"
  },
  {
    "path": "src/client/containers/TakeNoteApp.tsx",
    "content": "import React, { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { Helmet, HelmetProvider } from 'react-helmet-async'\nimport { DragDropContext, DropResult } from 'react-beautiful-dnd'\nimport SplitPane from 'react-split-pane'\nimport dayjs from 'dayjs'\nimport localizedFormat from 'dayjs/plugin/localizedFormat'\n\nimport { AppSidebar } from '@/containers/AppSidebar'\nimport { KeyboardShortcuts } from '@/containers/KeyboardShortcuts'\nimport { NoteEditor } from '@/containers/NoteEditor'\nimport { NoteList } from '@/containers/NoteList'\nimport { SettingsModal } from '@/containers/SettingsModal'\nimport { TempStateProvider } from '@/contexts/TempStateContext'\nimport { useInterval, useBeforeUnload } from '@/utils/hooks'\nimport {\n  getWebsiteTitle,\n  determineAppClass,\n  getActiveCategory,\n  getDayJsLocale,\n  getNoteBarConf,\n} from '@/utils/helpers'\nimport { loadCategories, swapCategories } from '@/slices/category'\nimport { sync } from '@/slices/sync'\nimport { NoteItem, CategoryItem } from '@/types'\nimport { loadNotes } from '@/slices/note'\nimport { loadSettings } from '@/slices/settings'\nimport { getSettings, getNotes, getCategories, getSync } from '@/selectors'\n\ndayjs.extend(localizedFormat)\ndayjs.locale(getDayJsLocale(navigator.language))\n\nexport const TakeNoteApp: React.FC = () => {\n  // ===========================================================================\n  // Selectors\n  // ===========================================================================\n\n  const { darkTheme, sidebarVisible } = useSelector(getSettings)\n  const { activeFolder, activeCategoryId, notes } = useSelector(getNotes)\n  const { categories } = useSelector(getCategories)\n  const { pendingSync } = useSelector(getSync)\n\n  const activeCategory = getActiveCategory(categories, activeCategoryId)\n\n  // ===========================================================================\n  // Dispatch\n  // ===========================================================================\n\n  const dispatch = useDispatch()\n\n  const _loadNotes = () => dispatch(loadNotes())\n  const _loadCategories = () => dispatch(loadCategories())\n  const _loadSettings = () => dispatch(loadSettings())\n  const _swapCategories = (categoryId: number, destinationId: number) =>\n    dispatch(swapCategories({ categoryId, destinationId }))\n  const _sync = (notes: NoteItem[], categories: CategoryItem[]) =>\n    dispatch(sync({ notes, categories }))\n\n  // ===========================================================================\n  // Handlers\n  // ===========================================================================\n\n  const onDragEnd = (result: DropResult) => {\n    const { destination, source } = result\n\n    if (!destination) return\n\n    if (destination.droppableId === source.droppableId && destination.index === source.index) return\n\n    if (result.type === 'CATEGORY') {\n      _swapCategories(source.index, destination.index)\n    }\n  }\n\n  // ===========================================================================\n  // Hooks\n  // ===========================================================================\n\n  useEffect(() => {\n    _loadNotes()\n    _loadCategories()\n    _loadSettings()\n  }, [])\n\n  useInterval(() => {\n    _sync(notes, categories)\n  }, 50000)\n\n  useBeforeUnload((event: BeforeUnloadEvent) => (pendingSync ? event.preventDefault() : null))\n\n  return (\n    <HelmetProvider>\n      <Helmet>\n        <meta charSet=\"utf-8\" />\n        <title>{getWebsiteTitle(activeFolder, activeCategory)}</title>\n        <link rel=\"canonical\" href=\"https://takenote.dev\" />\n      </Helmet>\n\n      <TempStateProvider>\n        <div className={determineAppClass(darkTheme, sidebarVisible, activeFolder)}>\n          <DragDropContext onDragEnd={onDragEnd}>\n            <SplitPane split=\"vertical\" minSize={150} maxSize={500} defaultSize={240}>\n              <AppSidebar />\n              <SplitPane split=\"vertical\" {...getNoteBarConf(activeFolder)}>\n                <NoteList />\n                <NoteEditor />\n              </SplitPane>\n            </SplitPane>\n          </DragDropContext>\n          <KeyboardShortcuts />\n          <SettingsModal />\n        </div>\n      </TempStateProvider>\n    </HelmetProvider>\n  )\n}\n"
  },
  {
    "path": "src/client/contexts/TempStateContext.tsx",
    "content": "import React, { createContext, FunctionComponent, useContext, useState } from 'react'\n\ninterface TempStateContextInterface {\n  addingTempCategory: boolean\n  setAddingTempCategory(adding: boolean): void\n}\n\nconst initialContextValue = {\n  addingTempCategory: false,\n  setAddingTempCategory: (adding: boolean) => undefined,\n}\n\nconst TempStateContext = createContext<TempStateContextInterface>(initialContextValue)\n\nconst useTempState = () => {\n  const context = useContext(TempStateContext)\n\n  if (!context) {\n    throw new Error('useTempState must be used within a TempStateContext')\n  }\n\n  return context\n}\n\nconst TempStateProvider: FunctionComponent = ({ children }) => {\n  const [addingTempCategory, setAddingTempCategory] = useState(false)\n\n  const value: TempStateContextInterface = {\n    addingTempCategory,\n    setAddingTempCategory,\n  }\n\n  return <TempStateContext.Provider value={value}>{children}</TempStateContext.Provider>\n}\n\nexport { TempStateProvider, useTempState }\n"
  },
  {
    "path": "src/client/global.d.ts",
    "content": "declare module '*.png' {\n  const value: any\n  export default value\n}\n\ndeclare module '*.svg' {\n  const value: any\n  export default value\n}\n\ndeclare module '*.md' {\n  const content: string\n  export = content\n}\n\ndeclare module 'mousetrap'\n"
  },
  {
    "path": "src/client/index.tsx",
    "content": "import React from 'react'\nimport { render } from 'react-dom'\nimport { Provider } from 'react-redux'\nimport { Router } from 'react-router-dom'\nimport createSagaMiddleware from 'redux-saga'\nimport { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'\n\nimport { App } from '@/containers/App'\nimport rootSaga from '@/sagas'\nimport rootReducer from '@/slices'\nimport history from '@/utils/history'\n\nimport '@/styles/index.scss'\n\nconst sagaMiddleware = createSagaMiddleware()\nconst store = configureStore({\n  reducer: rootReducer,\n  middleware: [sagaMiddleware, ...getDefaultMiddleware({ thunk: false })],\n  devTools: process.env.NODE_ENV !== 'production',\n})\n\nsagaMiddleware.run(rootSaga)\n\nrender(\n  <Provider store={store}>\n    <Router history={history}>\n      <App />\n    </Router>\n  </Provider>,\n  document.getElementById('root')\n)\n"
  },
  {
    "path": "src/client/router/PrivateRoute.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { Redirect, Route, RouteProps } from 'react-router-dom'\n\nimport { getAuth } from '@/selectors'\n\ninterface PrivateRouteProps extends RouteProps {\n  component: any\n}\n\nexport const PrivateRoute: React.FC<PrivateRouteProps> = ({ component: Component, ...rest }) => {\n  const { isAuthenticated } = useSelector(getAuth)\n\n  return (\n    <Route\n      render={(props) =>\n        isAuthenticated === true ? <Component {...props} /> : <Redirect to=\"/\" />\n      }\n      {...rest}\n    />\n  )\n}\n"
  },
  {
    "path": "src/client/router/PublicRoute.tsx",
    "content": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { Redirect, Route, RouteProps } from 'react-router-dom'\n\nimport { getAuth } from '@/selectors'\n\ninterface PublicRouteProps extends RouteProps {\n  component: any\n}\n\nexport const PublicRoute: React.FC<PublicRouteProps> = ({ component: Component, ...rest }) => {\n  const { isAuthenticated } = useSelector(getAuth)\n\n  return (\n    <Route\n      render={(props) =>\n        isAuthenticated === false ? <Component {...props} /> : <Redirect to=\"/app\" />\n      }\n      {...rest}\n    />\n  )\n}\n"
  },
  {
    "path": "src/client/sagas/index.ts",
    "content": "import { all, put, takeLatest, select } from 'redux-saga/effects'\nimport dayjs from 'dayjs'\nimport axios from 'axios'\n\nimport { requestCategories, requestNotes, requestSettings, saveState, saveSettings } from '@/api'\nimport { loadCategories, loadCategoriesError, loadCategoriesSuccess } from '@/slices/category'\nimport { loadNotes, loadNotesError, loadNotesSuccess } from '@/slices/note'\nimport { sync, syncError, syncSuccess } from '@/slices/sync'\nimport { login, loginSuccess, loginError, logout, logoutSuccess } from '@/slices/auth'\nimport {\n  updateCodeMirrorOption,\n  loadSettingsSuccess,\n  loadSettingsError,\n  loadSettings,\n  toggleDarkTheme,\n  togglePreviewMarkdown,\n  toggleSettingsModal,\n  updateNotesSortStrategy,\n} from '@/slices/settings'\nimport { SyncAction } from '@/types'\nimport { getSettings } from '@/selectors'\n\nconst isDemo = process.env.DEMO\n\n// Hit the Express endpoint to get the current GitHub user from the cookie\nfunction* loginUser() {\n  try {\n    if (isDemo) {\n      yield put(loginSuccess({ name: 'Demo User' }))\n    } else {\n      const { data } = yield axios('/api/auth/login')\n\n      yield put(loginSuccess(data))\n    }\n  } catch (error) {\n    yield put(loginError(error.message))\n  }\n}\n\n// Remove the access token cookie from Express\nfunction* logoutUser() {\n  try {\n    if (isDemo) {\n      yield put(logoutSuccess())\n    } else {\n      yield axios('/api/auth/logout')\n    }\n\n    yield put(logoutSuccess())\n  } catch (error) {\n    yield put(logoutSuccess())\n  }\n}\n\n// Get notes from API\nfunction* fetchNotes() {\n  let data\n  try {\n    if (isDemo) {\n      data = yield requestNotes()\n    } else {\n      data = (yield axios('/api/sync/notes')).data\n    }\n    const { notesSortKey } = yield select(getSettings)\n\n    yield put(loadNotesSuccess({ notes: data, sortOrderKey: notesSortKey }))\n  } catch (error) {\n    yield put(loadNotesError(error.message))\n  }\n}\n\n// Get categories from API\nfunction* fetchCategories() {\n  let data\n  try {\n    if (isDemo) {\n      data = yield requestCategories()\n    } else {\n      data = (yield axios('/api/sync/categories')).data\n    }\n\n    yield put(loadCategoriesSuccess(data))\n  } catch (error) {\n    yield put(loadCategoriesError(error.message))\n  }\n}\n\n// Get settings from API\nfunction* fetchSettings() {\n  let data\n  try {\n    data = yield requestSettings()\n\n    yield put(loadSettingsSuccess(data))\n  } catch (error) {\n    yield put(loadSettingsError())\n  }\n}\n\nfunction* syncData({ payload }: SyncAction) {\n  try {\n    if (isDemo) {\n      yield saveState(payload)\n    } else {\n      yield axios.post('/api/sync', payload)\n    }\n    yield put(syncSuccess(dayjs().format()))\n  } catch (error) {\n    yield put(syncError(error.message))\n  }\n}\n\nfunction* syncSettings() {\n  try {\n    const settings = yield select(getSettings)\n\n    yield saveSettings(settings)\n  } catch (error) {}\n}\n\n// If any of these functions are dispatched, invoke the appropriate saga\nfunction* rootSaga() {\n  yield all([\n    takeLatest(login.type, loginUser),\n    takeLatest(logout.type, logoutUser),\n    takeLatest(loadNotes.type, fetchNotes),\n    takeLatest(loadCategories.type, fetchCategories),\n    takeLatest(loadSettings.type, fetchSettings),\n    takeLatest(sync.type, syncData),\n    takeLatest(\n      [\n        toggleDarkTheme.type,\n        togglePreviewMarkdown.type,\n        updateCodeMirrorOption.type,\n        toggleSettingsModal.type,\n        updateNotesSortStrategy.type,\n      ],\n      syncSettings\n    ),\n  ])\n}\n\nexport default rootSaga\n"
  },
  {
    "path": "src/client/selectors/index.ts",
    "content": "import { RootState } from '@/types'\n\nexport const getSettings = (state: RootState) => state.settingsState\nexport const getCategories = (state: RootState) => state.categoryState\nexport const getNotes = (state: RootState) => state.noteState\nexport const getSync = (state: RootState) => state.syncState\nexport const getAuth = (state: RootState) => state.authState\n"
  },
  {
    "path": "src/client/serviceWorker.ts",
    "content": "const isLocalhost = Boolean(\n  window.location.hostname === 'localhost' ||\n    // [::1] is the IPv6 localhost address.\n    window.location.hostname === '[::1]' ||\n    // 127.0.0.0/8 are considered localhost for IPv4.\n    window.location.hostname.match(/^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)\n)\n\ntype Config = {\n  onSuccess?: (registration: ServiceWorkerRegistration) => void\n  onUpdate?: (registration: ServiceWorkerRegistration) => void\n}\n\nexport function register(config?: Config) {\n  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n    const publicUrl = new URL(process.env.PUBLIC_URL!, window.location.href)\n    if (publicUrl.origin !== window.location.origin) {\n      return\n    }\n\n    window.addEventListener('load', () => {\n      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`\n\n      if (isLocalhost) {\n        // This is running on localhost. Let's check if a service worker still exists or not.\n        checkValidServiceWorker(swUrl, config)\n      } else {\n        // Is not localhost. Just register service worker\n        registerValidSW(swUrl, config)\n      }\n    })\n  }\n}\n\nfunction registerValidSW(swUrl: string, config?: Config) {\n  navigator.serviceWorker\n    .register(swUrl)\n    .then((registration) => {\n      registration.onupdatefound = () => {\n        const installingWorker = registration.installing\n        if (installingWorker == null) {\n          return\n        }\n        installingWorker.onstatechange = () => {\n          if (installingWorker.state === 'installed') {\n            if (config && config.onSuccess) {\n              config.onSuccess(registration)\n            }\n          }\n        }\n      }\n    })\n    .catch((error) => {\n      console.error('Error during service worker registration:', error) // eslint-disable-line\n    })\n}\n\nfunction checkValidServiceWorker(swUrl: string, config?: Config) {\n  // Check if the service worker can be found. If it can't reload the page.\n  fetch(swUrl, {\n    headers: { 'Service-Worker': 'script' },\n  })\n    .then((response) => {\n      // Ensure service worker exists, and that we really are getting a JS file.\n      const contentType = response.headers.get('content-type')\n      if (\n        response.status === 404 ||\n        (contentType != null && contentType.indexOf('javascript') === -1)\n      ) {\n        // No service worker found. Probably a different app. Reload the page.\n        navigator.serviceWorker.ready.then((registration) => {\n          registration.unregister().then(() => {\n            window.location.reload()\n          })\n        })\n      } else {\n        // Service worker found. Proceed as normal.\n        registerValidSW(swUrl, config)\n      }\n    })\n    .catch(() => {\n      console.log('No internet connection found. App is running in offline mode.') // eslint-disable-line\n    })\n}\n\nexport function unregister() {\n  if ('serviceWorker' in navigator) {\n    navigator.serviceWorker.ready.then((registration) => {\n      registration.unregister()\n    })\n  }\n}\n"
  },
  {
    "path": "src/client/setupTests.ts",
    "content": "import '@testing-library/jest-dom/extend-expect'\n"
  },
  {
    "path": "src/client/slices/auth.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\nimport { AuthState } from '@/types'\n\nexport const initialState: AuthState = {\n  currentUser: {},\n  isAuthenticated: false,\n  error: '',\n  loading: true,\n}\n\nconst authSlice = createSlice({\n  name: 'auth',\n  initialState,\n  reducers: {\n    login: (state) => {\n      state.loading = true\n    },\n\n    loginSuccess: (state, { payload }: PayloadAction<any>) => {\n      state.currentUser = payload\n      state.isAuthenticated = true\n      state.loading = false\n    },\n\n    loginError: (state, { payload }: PayloadAction<string>) => {\n      state.error = payload\n      state.isAuthenticated = false\n      state.loading = false\n    },\n\n    logout: (state) => {\n      state.loading = true\n    },\n\n    logoutSuccess: (state) => {\n      state.isAuthenticated = false\n      state.currentUser = {}\n      state.error = ''\n      state.loading = false\n    },\n  },\n})\n\nexport const { login, loginSuccess, loginError, logout, logoutSuccess } = authSlice.actions\n\nexport default authSlice.reducer\n"
  },
  {
    "path": "src/client/slices/category.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\nimport { CategoryItem, CategoryState } from '@/types'\n\nconst _swapCategories = (categories: CategoryItem[], categoryId: number, destinationId: number) => {\n  const newCategories = [...categories]\n  newCategories.splice(categoryId, 1)\n  newCategories.splice(destinationId, 0, categories[categoryId])\n\n  return newCategories\n}\n\nexport const initialState: CategoryState = {\n  categories: [],\n  editingCategory: {\n    id: '',\n    tempName: '',\n  },\n  error: '',\n  loading: true,\n}\n\nconst categorySlice = createSlice({\n  name: 'category',\n  initialState,\n  reducers: {\n    addCategory: (state, { payload }: PayloadAction<CategoryItem>) => {\n      state.categories.push(payload)\n    },\n\n    importCategories: (state, { payload }: PayloadAction<CategoryItem[]>) => {\n      const categoryNames = new Map<string, string>()\n      state.categories.forEach(({ name }) => categoryNames.set(name, name))\n\n      // Make sure duplicate category is not added\n      const toAdd = payload.filter(({ name }) => !categoryNames.has(name))\n\n      state.categories.push(...toAdd)\n    },\n\n    updateCategory: (state, { payload }: PayloadAction<CategoryItem>) => {\n      state.categories = state.categories.map((category) =>\n        category.id === payload.id ? { ...category, name: payload.name } : category\n      )\n    },\n\n    deleteCategory: (state, { payload }: PayloadAction<string>) => {\n      state.categories = state.categories.filter((category) => category.id !== payload)\n    },\n\n    categoryDragEnter: (state, { payload }: PayloadAction<CategoryItem>) => {\n      state.categories = state.categories.map((category) =>\n        category.id === payload.id ? { ...category, draggedOver: true } : category\n      )\n    },\n\n    categoryDragLeave: (state, { payload }: PayloadAction<CategoryItem>) => {\n      state.categories = state.categories.map((category) =>\n        category.id === payload.id ? { ...category, draggedOver: false } : category\n      )\n    },\n\n    swapCategories: (\n      state,\n      { payload }: PayloadAction<{ categoryId: number; destinationId: number }>\n    ) => {\n      state.categories = _swapCategories(\n        state.categories,\n        payload.categoryId,\n        payload.destinationId\n      )\n    },\n\n    setCategoryEdit: (state, { payload }: PayloadAction<{ id: string; tempName: string }>) => {\n      state.editingCategory = payload\n    },\n\n    loadCategories: (state) => {\n      state.loading = true\n    },\n\n    loadCategoriesError: (state, { payload }: PayloadAction<string>) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    loadCategoriesSuccess: (state, { payload }: PayloadAction<CategoryItem[]>) => {\n      state.categories = payload\n      state.loading = false\n    },\n  },\n})\n\nexport const {\n  addCategory,\n  categoryDragEnter,\n  categoryDragLeave,\n  swapCategories,\n  deleteCategory,\n  loadCategories,\n  loadCategoriesError,\n  loadCategoriesSuccess,\n  updateCategory,\n  setCategoryEdit,\n  importCategories,\n} = categorySlice.actions\n\nexport default categorySlice.reducer\n"
  },
  {
    "path": "src/client/slices/index.ts",
    "content": "import { combineReducers, Reducer } from 'redux'\n\nimport authReducer from '@/slices/auth'\nimport categoryReducer from '@/slices/category'\nimport noteReducer from '@/slices/note'\nimport settingsReducer from '@/slices/settings'\nimport syncReducer from '@/slices/sync'\nimport { RootState } from '@/types'\n\nconst rootReducer: Reducer<RootState> = combineReducers<RootState>({\n  authState: authReducer,\n  categoryState: categoryReducer,\n  noteState: noteReducer,\n  settingsState: settingsReducer,\n  syncState: syncReducer,\n})\n\nexport default rootReducer\n"
  },
  {
    "path": "src/client/slices/note.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { v4 as uuid } from 'uuid'\n\nimport { Folder, NotesSortKey } from '@/utils/enums'\nimport { NoteItem, NoteState } from '@/types'\nimport { isDraftNote } from '@/utils/helpers'\nimport { getNotesSorter } from '@/utils/notesSortStrategies'\n\nconst getNewActiveNoteId = (\n  notes: NoteItem[],\n  oldNoteId: string,\n  activeCategoryId: string,\n  activeFolder: Folder\n): string => {\n  const newActiveNotes = notes\n    .filter((note) => !note.scratchpad) // filter out all scratchpad notes\n    .filter((note) => (activeFolder !== Folder.TRASH ? !note.trash : note.trash)) // trash or not trash\n    .filter((note) => (activeCategoryId ? note.category === activeCategoryId : true)) // filter category if necessary\n  const trashedNoteIndex = newActiveNotes.findIndex((note) => note.id === oldNoteId)\n\n  if (trashedNoteIndex === 0 && newActiveNotes[1]) return newActiveNotes[1].id\n  if (newActiveNotes[trashedNoteIndex - 1]) return newActiveNotes[trashedNoteIndex - 1].id\n\n  return ''\n}\n\nexport const getFirstNoteId = (\n  folder: Folder,\n  notes: NoteItem[],\n  categoryId?: string,\n  sortOrderKey?: NotesSortKey\n): string => {\n  const availableNotes = !sortOrderKey\n    ? notes.filter((note) => !note.trash)\n    : notes.filter((note) => !note.trash).sort(getNotesSorter(sortOrderKey))\n\n  const firstNote = {\n    [Folder.ALL]: () => availableNotes.find((note) => !note.scratchpad),\n    [Folder.CATEGORY]: () => availableNotes.find((note) => note.category === categoryId),\n    [Folder.FAVORITES]: () => availableNotes.find((note) => note.favorite),\n    [Folder.SCRATCHPAD]: () => availableNotes.find((note) => note.scratchpad),\n    [Folder.TRASH]: () => notes.find((note) => note.trash),\n  }[folder]()\n\n  return firstNote ? firstNote.id : ''\n}\n\nexport const initialState: NoteState = {\n  notes: [],\n  activeCategoryId: '',\n  activeFolder: Folder.ALL,\n  activeNoteId: '',\n  selectedNotesIds: [],\n  searchValue: '',\n  error: '',\n  loading: true,\n}\n\nconst noteSlice = createSlice({\n  name: 'note',\n  initialState,\n  reducers: {\n    addNote: (state, { payload }: PayloadAction<NoteItem>) => {\n      const draftNote = state.notes.find((note) => isDraftNote(note))\n\n      if (!draftNote) {\n        state.notes.push(payload)\n      }\n    },\n\n    importNotes: (state, { payload }: PayloadAction<NoteItem[]>) => {\n      const toAdd = payload.map((note) => {\n        note.id = uuid()\n\n        return note\n      })\n\n      state.notes.push(...toAdd)\n    },\n\n    updateNote: (state, { payload }: PayloadAction<NoteItem>) => {\n      state.notes = state.notes.map((note) =>\n        note.id === payload.id\n          ? { ...note, text: payload.text, lastUpdated: payload.lastUpdated }\n          : note\n      )\n    },\n\n    updateNotes: (\n      state,\n      {\n        payload,\n      }: PayloadAction<{\n        notes: NoteItem[]\n        activeFolder: Folder\n        activeCategoryId?: string\n        sortOrderKey?: NotesSortKey\n      }>\n    ) => {\n      state.notes = payload.notes\n      state.activeNoteId = getFirstNoteId(\n        payload.activeFolder,\n        payload.notes,\n        payload.activeCategoryId,\n        payload.sortOrderKey\n      )\n      state.selectedNotesIds = [\n        getFirstNoteId(\n          payload.activeFolder,\n          payload.notes,\n          payload.activeCategoryId,\n          payload.sortOrderKey\n        ),\n      ]\n    },\n\n    deleteNotes: (state, { payload }: PayloadAction<string[]>) => {\n      state.notes = state.notes.filter((note) => !payload.includes(note.id))\n      state.activeNoteId = getNewActiveNoteId(\n        state.notes,\n        state.activeNoteId,\n        state.activeCategoryId,\n        state.activeFolder\n      )\n      state.selectedNotesIds = [\n        getNewActiveNoteId(\n          state.notes,\n          state.activeNoteId,\n          state.activeCategoryId,\n          state.activeFolder\n        ),\n      ]\n    },\n\n    addCategoryToNote: (\n      state,\n      { payload }: PayloadAction<{ categoryId: string; noteId: string }>\n    ) => {\n      state.notes = state.selectedNotesIds.includes(payload.noteId)\n        ? // Multi\n          state.notes.map((note) =>\n            state.selectedNotesIds.includes(note.id)\n              ? { ...note, category: payload.categoryId }\n              : note\n          )\n        : // Single\n          state.notes.map((note) =>\n            note.id === payload.noteId ? { ...note, category: payload.categoryId } : note\n          )\n    },\n\n    removeCategoryFromNotes: (state, { payload }: PayloadAction<string>) => {\n      state.notes.map((note) => {\n        if (note.category === payload) {\n          note.category = ''\n        }\n      })\n    },\n\n    updateActiveNote: (\n      state,\n      { payload: { noteId, multiSelect } }: PayloadAction<{ noteId: string; multiSelect: boolean }>\n    ) => {\n      state.activeNoteId = multiSelect\n        ? state.notes.filter(({ id }) => state.selectedNotesIds.includes(id)).slice(-1)[0].id\n        : noteId\n    },\n\n    updateActiveCategoryId: (state, { payload }: PayloadAction<string>) => {\n      state.activeCategoryId = payload\n      state.activeFolder = Folder.CATEGORY\n      state.activeNoteId = getFirstNoteId(Folder.CATEGORY, state.notes, payload)\n      state.selectedNotesIds = [getFirstNoteId(Folder.CATEGORY, state.notes, payload)]\n      state.notes = state.notes.filter((note) => note.text !== '')\n    },\n\n    swapFolder: (\n      state,\n      { payload }: PayloadAction<{ folder: Folder; sortOrderKey?: NotesSortKey }>\n    ) => {\n      state.activeFolder = payload.folder\n      state.activeCategoryId = ''\n      state.activeNoteId = getFirstNoteId(\n        payload.folder,\n        state.notes,\n        undefined,\n        payload.sortOrderKey\n      )\n      state.selectedNotesIds = [\n        getFirstNoteId(payload.folder, state.notes, undefined, payload.sortOrderKey),\n      ]\n      state.notes = state.notes.filter((note) => note.scratchpad || note.text !== '')\n    },\n\n    assignFavoriteToNotes: (state, { payload }: PayloadAction<string>) => {\n      state.notes = state.notes.map((note) => {\n        if (state.selectedNotesIds.includes(payload)) {\n          return state.selectedNotesIds.includes(note.id) ? { ...note, favorite: true } : note\n        }\n        if (note.id === payload) {\n          return { ...note, favorite: true }\n        }\n\n        return note\n      })\n    },\n\n    toggleFavoriteNotes: (state, { payload }: PayloadAction<string>) => {\n      state.notes = state.selectedNotesIds.includes(payload)\n        ? state.notes.map((note) =>\n            state.selectedNotesIds.includes(note.id) ? { ...note, favorite: !note.favorite } : note\n          )\n        : state.notes.map((note) =>\n            note.id === payload ? { ...note, favorite: !note.favorite } : note\n          )\n    },\n\n    assignTrashToNotes: (state, { payload }: PayloadAction<string>) => {\n      state.notes = state.notes.map((note) => {\n        if (state.selectedNotesIds.includes(payload)) {\n          return state.selectedNotesIds.includes(note.id) ? { ...note, trash: true } : note\n        }\n        if (note.id === payload) {\n          return { ...note, trash: true }\n        }\n\n        return note\n      })\n      state.activeNoteId = getNewActiveNoteId(\n        state.notes,\n        payload,\n        state.activeCategoryId,\n        state.activeFolder\n      )\n      state.selectedNotesIds = [\n        getNewActiveNoteId(state.notes, payload, state.activeCategoryId, state.activeFolder),\n      ]\n    },\n\n    toggleTrashNotes: (state, { payload }: PayloadAction<string>) => {\n      state.notes = state.selectedNotesIds.includes(payload)\n        ? state.notes.map((note) =>\n            state.selectedNotesIds.includes(note.id) ? { ...note, trash: !note.trash } : note\n          )\n        : state.notes.map((note) => (note.id === payload ? { ...note, trash: !note.trash } : note))\n      state.activeNoteId = getNewActiveNoteId(\n        state.notes,\n        payload,\n        state.activeCategoryId,\n        state.activeFolder\n      )\n      state.selectedNotesIds = [\n        getNewActiveNoteId(state.notes, payload, state.activeCategoryId, state.activeFolder),\n      ]\n    },\n\n    unassignTrashFromNotes: (state, { payload }: PayloadAction<string>) => {\n      state.notes = state.notes.map((note) => {\n        if (state.selectedNotesIds.includes(payload)) {\n          return state.selectedNotesIds.includes(note.id) && note.trash\n            ? { ...note, trash: false }\n            : note\n        }\n        if (note.id === payload) {\n          return { ...note, trash: false }\n        }\n\n        return note\n      })\n    },\n\n    updateSelectedNotes: (\n      state,\n      { payload: { noteId, multiSelect } }: PayloadAction<{ noteId: string; multiSelect: boolean }>\n    ) => {\n      state.selectedNotesIds = multiSelect\n        ? state.selectedNotesIds.length === 1 && state.selectedNotesIds[0] === noteId\n          ? state.selectedNotesIds\n          : state.selectedNotesIds.includes(noteId)\n          ? state.selectedNotesIds.filter((selectedNoteId) => selectedNoteId !== noteId)\n          : [...state.selectedNotesIds, noteId]\n        : [noteId]\n    },\n\n    permanentlyEmptyTrash: (state) => {\n      state.notes = state.notes.filter((note) => !note.trash)\n    },\n\n    pruneNotes: (state) => {\n      state.notes = state.notes.filter(\n        (note) => note.scratchpad || note.text !== '' || state.selectedNotesIds.includes(note.id)\n      )\n    },\n\n    searchNotes: (state, { payload }: PayloadAction<string>) => {\n      state.searchValue = payload\n    },\n\n    loadNotes: (state) => {\n      state.loading = true\n    },\n\n    loadNotesError: (state, { payload }: PayloadAction<string>) => {\n      state.loading = false\n      state.error = payload\n    },\n\n    loadNotesSuccess: (\n      state,\n      { payload }: PayloadAction<{ notes: NoteItem[]; sortOrderKey?: NotesSortKey }>\n    ) => {\n      state.notes = payload.notes\n      state.activeNoteId = getFirstNoteId(\n        Folder.ALL,\n        payload.notes,\n        undefined,\n        payload.sortOrderKey\n      )\n      state.selectedNotesIds = [\n        getFirstNoteId(Folder.ALL, payload.notes, undefined, payload.sortOrderKey),\n      ]\n      state.loading = false\n    },\n  },\n})\n\nexport const {\n  addNote,\n  updateNote,\n  updateNotes,\n  deleteNotes,\n  addCategoryToNote,\n  removeCategoryFromNotes,\n  updateActiveNote,\n  updateActiveCategoryId,\n  swapFolder,\n  assignFavoriteToNotes,\n  toggleFavoriteNotes,\n  assignTrashToNotes,\n  toggleTrashNotes,\n  unassignTrashFromNotes,\n  updateSelectedNotes,\n  permanentlyEmptyTrash,\n  pruneNotes,\n  searchNotes,\n  loadNotes,\n  loadNotesError,\n  loadNotesSuccess,\n  importNotes,\n} = noteSlice.actions\n\nexport default noteSlice.reducer\n"
  },
  {
    "path": "src/client/slices/settings.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\nimport { SettingsState } from '@/types'\nimport { NotesSortKey, DirectionText } from '@/utils/enums'\n\nexport const initialState: SettingsState = {\n  previewMarkdown: false,\n  darkTheme: false,\n  sidebarVisible: true,\n  notesSortKey: NotesSortKey.LAST_UPDATED,\n  codeMirrorOptions: {\n    mode: 'gfm',\n    theme: 'base16-light',\n    lineNumbers: false,\n    lineWrapping: true,\n    styleActiveLine: { nonEmpty: true },\n    viewportMargin: Infinity,\n    keyMap: 'default',\n    dragDrop: false,\n    direction: DirectionText.LEFT_TO_RIGHT,\n    scrollPastEnd: false,\n  },\n  isOpen: false,\n  loading: false,\n}\n\nconst settingsSlice = createSlice({\n  name: 'settings',\n  initialState,\n  reducers: {\n    toggleSettingsModal: (state) => {\n      state.isOpen = !state.isOpen\n    },\n\n    updateCodeMirrorOption: (state, { payload }: PayloadAction<{ key: string; value: string }>) => {\n      state.codeMirrorOptions[payload.key] = payload.value\n    },\n\n    togglePreviewMarkdown: (state) => {\n      state.previewMarkdown = !state.previewMarkdown\n    },\n\n    toggleDarkTheme: (state) => {\n      state.darkTheme = !state.darkTheme\n    },\n\n    updateNotesSortStrategy: (state, { payload }: PayloadAction<NotesSortKey>) => {\n      state.notesSortKey = payload\n    },\n\n    loadSettings: (state) => {\n      state.loading = true\n    },\n\n    loadSettingsError: (state) => {\n      state.loading = false\n    },\n\n    loadSettingsSuccess: (state, { payload }: PayloadAction<SettingsState>) => {\n      return { ...payload, loading: false }\n    },\n  },\n})\n\nexport const {\n  toggleSettingsModal,\n  updateCodeMirrorOption,\n  toggleDarkTheme,\n  togglePreviewMarkdown,\n  updateNotesSortStrategy,\n  loadSettings,\n  loadSettingsError,\n  loadSettingsSuccess,\n} = settingsSlice.actions\n\nexport default settingsSlice.reducer\n"
  },
  {
    "path": "src/client/slices/sync.ts",
    "content": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\nimport { SyncState, SyncPayload } from '@/types'\n\nexport const initialState: SyncState = {\n  pendingSync: false,\n  lastSynced: '',\n  error: '',\n  syncing: false,\n}\n\nconst syncSlice = createSlice({\n  name: 'sync',\n  initialState,\n  reducers: {\n    setPendingSync: (state) => {\n      state.pendingSync = true\n    },\n\n    sync: (state, { payload }: PayloadAction<SyncPayload>) => {\n      state.syncing = true\n    },\n\n    syncError: (state, { payload }: PayloadAction<string>) => {\n      state.syncing = false\n      state.error = payload\n    },\n\n    syncSuccess: (state, { payload }: PayloadAction<string>) => {\n      state.syncing = false\n      state.lastSynced = payload\n      state.pendingSync = false\n    },\n  },\n})\n\nexport const { sync, syncError, syncSuccess, setPendingSync } = syncSlice.actions\n\nexport default syncSlice.reducer\n"
  },
  {
    "path": "src/client/styles/_app-sidebar.scss",
    "content": ".app-sidebar {\n  background: $app-sidebar-color;\n  color: $light-font-color;\n  display: flex;\n  flex-direction: column;\n  border-right: 1px solid darken($app-sidebar-color, 10%);\n  z-index: 3;\n\n  &-settings {\n    cursor: pointer;\n    background: $app-sidebar-color;\n    border-width: 0 !important;\n    display: flex;\n    align-items: center;\n    text-align: left;\n    padding: 0.75rem;\n    border-radius: 0;\n    border-top: 1px solid lighten($app-sidebar-color, 8%) !important;\n    border-right: 1px solid darken($app-sidebar-color, 10%);\n    margin: 0;\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    width: 100%;\n    &:focus {\n      background: $app-sidebar-color;\n    }\n    &:hover {\n      background: lighten($app-sidebar-color, 5%);\n      .user-settings-icon {\n        margin-left: auto;\n        margin-right: 0.5rem;\n        color: lighten($app-sidebar-color, 60%);\n      }\n    }\n    .user-avatar {\n      border-radius: 50%;\n      max-width: 40px;\n      margin-right: 0.75rem;\n    }\n    .user-name {\n      font-weight: 600;\n      font-size: 0.95rem;\n      margin-bottom: 0.25rem;\n    }\n    .user-subtitle {\n      color: lighten($app-sidebar-color, 30%);\n      font-size: 0.85rem;\n    }\n    .user-settings-icon {\n      margin-left: auto;\n      margin-right: 0.5rem;\n      color: lighten($app-sidebar-color, 40%);\n    }\n  }\n\n  &-wrapper {\n    background-color: transparent;\n    border: 0 none;\n    padding: 0;\n    width: 100%;\n    font-family: inherit;\n    margin: 0;\n    font-size: 0.95rem;\n    line-height: inherit;\n    color: inherit;\n    border-radius: 0;\n\n    &:hover,\n    &:focus {\n      border: 0 none;\n    }\n\n    &:focus {\n      background: lighten($app-sidebar-color, 5%);\n      color: white;\n\n      svg {\n        stroke: darken($light-font-color, 10%);\n      }\n    }\n  }\n\n  &-link {\n    display: flex;\n    align-items: center;\n    padding: 0.5rem 1rem;\n    cursor: pointer;\n    font-size: 0.95rem;\n    font-weight: 600;\n    border: 1px solid transparent;\n\n    &:hover {\n      background: lighten($app-sidebar-color, 5%);\n      color: white;\n\n      svg {\n        stroke: darken($light-font-color, 10%);\n      }\n    }\n\n    &.active {\n      color: white;\n      background: darken($app-sidebar-color, 5%);\n\n      svg {\n        stroke: $primary;\n      }\n    }\n\n    &.dragged-over {\n      border: 1px dashed $primary;\n      color: white;\n      background: darken($app-sidebar-color, 5%);\n\n      svg {\n        stroke: $primary;\n      }\n    }\n  }\n\n  &-main {\n    flex: 1;\n    position: relative;\n    padding-bottom: 1rem;\n\n    p {\n      padding: 0 0.5rem;\n    }\n\n    h2 {\n      margin: 0;\n      color: lighten($app-sidebar-color, 30%);\n      text-transform: uppercase;\n      font-size: 0.8rem;\n    }\n  }\n\n  &-icon {\n    margin-right: 0.75rem;\n  }\n\n  &-actions {\n    box-sizing: content-box;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    margin-bottom: 1rem;\n  }\n\n  .action-button {\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: flex-start;\n    padding: 1rem;\n    margin: 0.5rem 0%;\n    border: none !important;\n    text-align: left;\n    font-size: 0.95rem;\n    border-radius: 0;\n\n    svg {\n      margin-right: 0.75rem;\n    }\n\n    &:hover,\n    &:focus {\n      background: $primary;\n      outline: none;\n\n      .action-button-icon {\n        stroke: white;\n      }\n    }\n\n    .action-button-icon {\n      stroke: white;\n      background: $primary;\n      border-radius: 0.3rem;\n    }\n  }\n\n  .category {\n    &-title {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      margin-top: 1rem;\n      padding: 0.5rem 0 0.5rem 1rem;\n\n      .collapse-button {\n        cursor: pointer;\n        -webkit-appearance: none;\n        display: flex;\n        align-items: center;\n        padding: 0;\n        color: darken($light-font-color, 25%);\n        background: transparent;\n        font-size: 0.8rem;\n        border: none;\n        line-height: 1;\n        margin: 0;\n\n        &:hover {\n          color: white;\n\n          svg {\n            stroke: white;\n          }\n        }\n\n        h2 {\n          padding-left: 0.75rem;\n        }\n      }\n    }\n\n    &-error-message {\n      display: flex;\n      margin: 0.5rem;\n      color: $error;\n      font-size: 0.85rem;\n    }\n\n    &-list {\n      font-size: 0.9rem;\n\n      &-each {\n        cursor: pointer;\n        padding: 0.5rem 1rem;\n        display: flex;\n        align-items: center;\n        justify-content: space-between;\n        border: 1px dashed transparent;\n\n        &:focus {\n          outline: none;\n        }\n\n        &:hover {\n          background: lighten($app-sidebar-color, 5%);\n          color: white;\n\n          .category-options {\n            color: lighten($app-sidebar-color, 30%);\n          }\n\n          .category-list-name {\n            svg {\n              stroke: darken($light-font-color, 10%);\n            }\n          }\n        }\n\n        &.active {\n          background: darken($app-sidebar-color, 5%);\n          color: white;\n\n          .category-list-name {\n            svg {\n              stroke: $primary;\n            }\n          }\n        }\n\n        &.dragged-over {\n          border: 1px dashed $primary;\n          color: white;\n          background: darken($app-sidebar-color, 5%);\n\n          svg {\n            stroke: $primary;\n          }\n        }\n\n        &.dragging {\n          background: darken($app-sidebar-color, 10%);\n          box-shadow: 2px 3px 10px rgba(0, 0, 0, 0.15);\n        }\n      }\n\n      &-name {\n        display: flex;\n        align-items: center;\n      }\n\n      .category-options {\n        color: transparent;\n        z-index: 1;\n        display: flex;\n        cursor: pointer;\n\n        & > *:not(:last-child) {\n          margin-right: 0.3rem;\n        }\n        &.active {\n          color: lighten($app-sidebar-color, 20%);\n        }\n      }\n    }\n\n    &-button {\n      cursor: pointer;\n      -webkit-appearance: none;\n      display: flex;\n      align-items: center;\n      color: darken($light-font-color, 25%);\n      background: transparent;\n      font-size: 0.8rem;\n      border: none;\n      line-height: 1;\n      margin: 0;\n      padding: 0.5rem 1rem;\n\n      &:hover {\n        color: white;\n\n        svg {\n          stroke: white;\n        }\n      }\n    }\n  }\n\n  [type='text'] {\n    -webkit-appearance: none;\n    border-radius: 0;\n    background: darken($app-sidebar-color, 5%);\n    border: none;\n    padding: 0.5rem;\n    font-size: 0.9rem;\n    color: #eee;\n    line-height: 1;\n    margin: 0.5rem 0.5rem 0.5rem 1rem;\n    width: 150px;\n\n    &.category-edit {\n      padding: 0;\n      width: auto;\n      max-width: 100px;\n      margin: 0;\n    }\n\n    &:hover,\n    &:focus {\n      border: none;\n    }\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_buttons.scss",
    "content": "%buttons {\n  -webkit-appearance: none;\n  display: inline-block;\n  border: 2px solid $primary;\n  border-radius: 0.3rem;\n  background: $primary;\n  color: white;\n  font-weight: 600;\n  font-size: 1rem;\n  padding: 0.5rem 0.75rem;\n  margin: 0 0 0.5rem 0;\n  vertical-align: middle;\n  text-align: center;\n  cursor: pointer;\n  text-decoration: none;\n  line-height: 1;\n}\n\n%buttons-hover {\n  border: 2px solid darken($primary, 5%);\n  background: darken($primary, 5%);\n  text-decoration: none;\n}\n\n%buttons-focus {\n  border: 2px solid darken($primary, 10%);\n  background: darken($primary, 10%);\n  text-decoration: none;\n  outline: none;\n}\n\n#{$buttons} {\n  @extend %buttons;\n\n  &.secondary {\n    background: darken($note-sidebar-color, 8%);\n    border: 2px solid darken($note-sidebar-color, 8%);\n    color: #666;\n\n    &:hover {\n      background: darken($note-sidebar-color, 12%);\n      border: 2px solid darken($note-sidebar-color, 12%);\n      color: #555;\n    }\n  }\n\n  &.icon-button {\n    display: flex;\n    align-items: center;\n    svg {\n      margin-right: 0.75rem;\n      stroke: rgba(255, 255, 255, 0.7);\n    }\n  }\n\n  &.github-button {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    background-color: #28a745;\n    background-image: linear-gradient(-180deg, #34d058, #28a745 90%);\n    border: 1px solid rgba(27, 31, 35, 0.2);\n    border-radius: 0.3rem;\n    font-size: 1.1rem;\n    padding: 0.75rem 0.85rem;\n\n    &:hover {\n      background-image: linear-gradient(-180deg, #2fcb53, #269f42 90%);\n    }\n\n    img {\n      margin-right: 0.5rem;\n      height: 20px;\n      width: 20px;\n      max-width: 20px;\n    }\n  }\n\n  &::-moz-focus-inner {\n    border: 0;\n    padding: 0;\n  }\n\n  &:hover,\n  &:active {\n    @extend %buttons-hover;\n  }\n\n  &:focus {\n    @extend %buttons-focus;\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_dark.scss",
    "content": "$dark-sidebar: #333;\n$dark-editor: #3f3f3f;\n\n.dark {\n  a {\n    color: lighten($primary, 8%);\n    &:hover {\n      color: lighten($primary, 15%);\n    }\n    &.button {\n      color: white;\n    }\n  }\n\n  .loading {\n    background: #222;\n  }\n\n  .app-sidebar {\n    border-right: 1px solid darken($dark-sidebar, 8%);\n  }\n\n  .note-menu-bar {\n    background: darken($dark-sidebar, 6%);\n    border-color: darken($dark-sidebar, 12%);\n\n    button {\n      color: rgba(255, 255, 255, 0.5);\n      &.trash {\n        &:hover {\n          color: darken($error, 8%);\n        }\n      }\n      &:hover {\n        color: rgba(255, 255, 255, 0.7);\n        background: darken($dark-sidebar, 10%);\n      }\n    }\n  }\n\n  .note-sidebar {\n    background: darken($dark-sidebar, 6%);\n    border-right: 1px solid darken($dark-sidebar, 8%);\n\n    .note-sidebar-header {\n      color: $light-font-color;\n      background: darken($dark-sidebar, 4%);\n      border-color: darken($dark-sidebar, 12%);\n\n      input {\n        background: lighten($dark-sidebar, 5%);\n        color: $light-font-color;\n        border: 1px solid darken($dark-sidebar, 10%);\n      }\n    }\n\n    .list-button {\n      color: $light-font-color;\n      background: darken($dark-sidebar, 8%);\n      border: 1px solid darken($dark-sidebar, 1%);\n\n      &:hover,\n      &:focus {\n        color: white;\n        background: darken($variable, 10%);\n      }\n    }\n\n    .note-list {\n      .note-list-each {\n        border-bottom: 1px solid darken($dark-sidebar, 10%);\n        color: darken($light-font-color, 10%);\n\n        &.active:hover .note-options {\n          color: white;\n        }\n\n        .note-category {\n          color: rgba(255, 255, 255, 0.3);\n        }\n\n        &:hover {\n          background: $dark-sidebar;\n          .note-options {\n            color: #888;\n          }\n        }\n\n        &.selected {\n          background: $primary;\n          color: white;\n          border-bottom: 1px solid darken($primary, 5%);\n\n          &:hover {\n            .note-options {\n              color: white;\n            }\n          }\n\n          .note-options {\n            &.active {\n              color: white;\n            }\n          }\n          .note-category {\n            color: rgba(255, 255, 255, 0.7);\n          }\n        }\n\n        .note-options {\n          &.active {\n            color: #888;\n          }\n        }\n      }\n    }\n  }\n\n  .options-context-menu {\n    border: 1px solid darken($dark-sidebar, 8%);\n    background: $app-sidebar-color;\n    color: $light-font-color;\n    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2), 0 5px 5px rgba(0, 0, 0, 0.25);\n\n    svg {\n      color: rgba(255, 255, 255, 0.5);\n    }\n\n    select {\n      background: lighten($dark-sidebar, 8%);\n      color: $light-font-color;\n      border: 1px solid darken($dark-sidebar, 5%);\n    }\n  }\n\n  .options-nav {\n    .nav-item {\n      &:hover {\n        color: white;\n        background: lighten($app-sidebar-color, 8%);\n\n        svg {\n          color: rgba(255, 255, 255, 0.8);\n        }\n      }\n      &.delete-option {\n        &:hover {\n          background: darken($error, 10%);\n          color: white;\n        }\n      }\n    }\n  }\n\n  .empty-editor {\n    background: $dark-editor;\n    color: $light-font-color;\n  }\n\n  .editor .CodeMirror-activeline-background {\n    background: #222 !important;\n  }\n\n  .previewer {\n    background: darken($dark-editor, 5%);\n    color: $light-font-color;\n\n    a {\n      color: $tag;\n    }\n\n    h1,\n    h2,\n    h3,\n    h4,\n    h5 {\n      color: $attribute;\n    }\n\n    table,\n    thead th,\n    tfoot th,\n    td {\n      border-color: darken($dark-editor, 20%);\n    }\n\n    code {\n      background: rgba(0, 0, 0, 0.2);\n      border: 1px solid $dark-editor;\n    }\n\n    pre {\n      background: darken($dark-editor, 10%);\n      border: 1px solid $dark-editor;\n\n      code {\n        color: $code-font-color;\n        background: transparent;\n        border-width: 0;\n      }\n    }\n\n    hr {\n      height: 0;\n      border: 0;\n      border-top: 2px solid darken($dark-editor, 5%);\n    }\n\n    blockquote {\n      border-color: darken($dark-editor, 10%);\n    }\n  }\n\n  .preview-button {\n    background: $app-sidebar-color;\n    color: #ccc;\n\n    &:hover {\n      background: darken($app-sidebar-color, 5%);\n    }\n  }\n\n  .settings-modal {\n    background: $dark-sidebar;\n    color: $light-font-color;\n    .settings-modal-header {\n      border-color: darken($dark-sidebar, 5%);\n    }\n    .subtitle {\n      color: darken($light-font-color, 20%);\n    }\n    .settings-option {\n      border-color: darken($dark-sidebar, 5%);\n      h3 {\n        color: rgba(255, 255, 255, 0.8);\n      }\n      .description {\n        color: rgba(255, 255, 255, 0.5);\n      }\n    }\n\n    .close-button {\n      &:hover,\n      &:active {\n        background: darken($dark-sidebar, 5%);\n      }\n    }\n\n    select {\n      background: lighten($dark-sidebar, 8%);\n      color: $light-font-color;\n      border: 1px solid darken($dark-sidebar, 5%);\n\n      &:active &:focus {\n        border: 1px solid darken($primary, 20%);\n        box-shadow: 0 0 0.2rem darken($primary, 20%);\n      }\n    }\n  }\n\n  kbd {\n    background: darken($dark-sidebar, 5%);\n    border: 1px solid darken($dark-sidebar, 10%);\n    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.2), 0 0 0 2px #222 inset;\n    color: darken($light-font-color, 10%);\n    text-shadow: 0 1px 0 #000;\n  }\n\n  .cache {\n    border: 1px solid black;\n  }\n\n  ::-webkit-scrollbar {\n    width: 8px;\n    height: 8px;\n    background: $app-sidebar-color;\n  }\n\n  ::-webkit-scrollbar-thumb {\n    background: lighten($app-sidebar-color, 8%);\n    border-radius: 0;\n  }\n  .Resizer {\n    opacity: 1;\n  }\n\n  .uuid-menu-bar {\n    color: rgba(255, 255, 255, 0.4);\n  }\n\n  .last-synced {\n    color: rgba(255, 255, 255, 0.4);\n  }\n\n  .tab-list {\n    border-color: darken($dark-sidebar, 10%);\n  }\n\n  .tabs {\n    p {\n      color: rgba(255, 255, 255, 0.7);\n    }\n    h3 {\n      color: white;\n    }\n  }\n\n  .tab {\n    svg {\n      color: rgba(255, 255, 255, 0.4);\n    }\n    &.active {\n      background: darken($dark-sidebar, 6%) !important;\n      color: white;\n      svg {\n        color: $primary !important;\n      }\n    }\n    &:hover {\n      background: lighten($dark-sidebar, 6%);\n      color: white;\n      svg {\n        color: rgba(255, 255, 255, 0.7);\n      }\n    }\n  }\n\n  .slider {\n    background-color: darken($dark-sidebar, 10%);\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_editor.scss",
    "content": ".empty-editor {\n  background: $light-theme-background;\n  width: 100%;\n}\n\n.editor {\n  overflow-y: auto;\n}\n\n.CodeMirror {\n  -webkit-font-smoothing: subpixel-antialiased;\n  padding: 1rem;\n  height: 100%;\n  font-family: Menlo, Monaco, monospace;\n  font-weight: 500;\n  font-size: 15px;\n  line-height: 1.5;\n}\n\n.CodeMirror-lines {\n  padding: 0;\n}\n\n.CodeMirror-linenumber {\n  padding-right: 15px;\n}\n\n.CodeMirror-gutter-background {\n  color: #333;\n}\n\n.CodeMirror-activeline-background {\n  background: rgba(0, 0, 0, 0.05) !important;\n}\n\n.cm-notelink {\n  font-style: italic;\n  font-weight: bold;\n  color: #0daba3;\n}\n"
  },
  {
    "path": "src/client/styles/_forms.scss",
    "content": "%forms {\n  display: block;\n  border-radius: 0.3rem;\n  border: 1px solid $accent-gray;\n  padding: 0.75rem;\n  outline: none;\n  margin-bottom: 0.5rem;\n  font-size: 1rem;\n  width: 100%;\n  max-width: 100%;\n}\n\n%forms-focus {\n  outline: 0;\n  border: 1px solid lighten($primary, 15%);\n  box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n#{$forms} {\n  @extend %forms;\n\n  &:focus,\n  &:active {\n    @extend %forms-focus;\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_helpers.scss",
    "content": ".sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border-width: 0;\n}\n\n.hidden {\n  display: none;\n}\n\n.icon {\n  color: rgba(255, 255, 255, 0.7);\n}\n\n.v-center {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.v-between {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.flex {\n  display: flex;\n}\n\n.mb-1 {\n  margin-bottom: 1rem;\n}\n\n.mt-1 {\n  margin-top: 1rem;\n}\n\n.ml-1 {\n  margin-left: 1rem;\n}\n.mr-1 {\n  margin-right: 1rem;\n}\n\n.text-center {\n  text-align: center;\n}\n\n.switch {\n  position: relative;\n  display: inline-block;\n  width: 50px;\n  height: 24px;\n\n  input {\n    opacity: 0;\n    width: 0;\n    height: 0;\n\n    &:checked + .slider {\n      background: #72ce6e;\n    }\n\n    &:focus + .slider {\n      box-shadow: 0 0 1px #72ce6e;\n    }\n\n    &:checked + .slider:before {\n      transform: translateX(26px);\n    }\n  }\n}\n\n.slider {\n  position: absolute;\n  cursor: pointer;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background-color: $accent-gray;\n  transition: 0.4s;\n  border-radius: 34px;\n\n  &:before {\n    position: absolute;\n    content: '';\n    height: 20px;\n    width: 20px;\n    left: 2px;\n    bottom: 2px;\n    background: white;\n    transition: 0.4s;\n    border-radius: 50%;\n    box-shadow: 2px 3px 5px rgba(0, 0, 0, 0.07), 2px 3px 2px rgba(0, 0, 0, 0.2);\n  }\n}\n\nkbd {\n  background-color: #f7f7f7;\n  border: 1px solid #ccc;\n  border-radius: 3px;\n  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px #fff inset;\n  color: #333;\n  display: inline-block;\n  font-family: Helvetica, Arial, sans-serif;\n  font-size: 12px;\n  line-height: 1.4;\n  margin: 0 0.1em;\n  padding: 0.1em 0.6em;\n  text-shadow: 0 1px 0 #fff;\n}\n\n.action-button {\n  cursor: pointer;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  background: transparent;\n  padding: 0.8rem;\n  margin: 0 0.5rem;\n  border: none !important;\n}\n\n::-webkit-scrollbar-corner {\n  width: 8px;\n  height: 8px;\n  background: transparent;\n}\n\n::-webkit-scrollbar {\n  width: 8px;\n  height: 8px;\n  background: transparent;\n}\n\n::-webkit-scrollbar-thumb {\n  background: darken($note-sidebar-color, 10%);\n  border-radius: 0;\n}\n\n/*!\n * Load Awesome v1.1.0 (http://github.danielcardoso.net/load-awesome/)\n * Copyright 2015 Daniel Cardoso <@DanielCardoso>\n * Licensed under MIT\n */\n.la-ball-beat,\n.la-ball-beat > div {\n  position: relative;\n  -webkit-box-sizing: border-box;\n  -moz-box-sizing: border-box;\n  box-sizing: border-box;\n}\n\n.la-ball-beat {\n  display: block;\n  font-size: 0;\n  color: $primary;\n  width: 54px;\n  height: 18px;\n}\n\n.la-ball-beat.la-dark {\n  color: #333;\n}\n\n.la-ball-beat > div {\n  display: inline-block;\n  float: none;\n  background-color: currentColor;\n  border: 0 solid currentColor;\n  width: 10px;\n  height: 10px;\n  margin: 4px;\n  border-radius: 50%;\n  animation: ball-beat 0.7s -0.15s infinite linear;\n}\n\n.la-ball-beat > div:nth-child(2n-1) {\n  animation-delay: -0.5s;\n}\n\n.la-ball-beat.la-sm {\n  width: 26px;\n  height: 8px;\n}\n\n.la-ball-beat.la-sm > div {\n  width: 4px;\n  height: 4px;\n  margin: 2px;\n}\n\n.la-ball-beat.la-2x {\n  width: 108px;\n  height: 36px;\n}\n\n.la-ball-beat.la-2x > div {\n  width: 20px;\n  height: 20px;\n  margin: 8px;\n}\n\n.la-ball-beat.la-3x {\n  width: 162px;\n  height: 54px;\n}\n\n.la-ball-beat.la-3x > div {\n  width: 30px;\n  height: 30px;\n  margin: 12px;\n}\n\n@keyframes ball-beat {\n  50% {\n    opacity: 0.2;\n    transform: scale(0.75);\n  }\n\n  100% {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n.rotating-svg {\n  animation-name: rotating;\n  animation-duration: 15.5s;\n  animation-iteration-count: infinite;\n  transform-origin: 50% 50%;\n  display: inline-block;\n}\n\n@keyframes rotating {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_landing-page.scss",
    "content": ".landing-page {\n  a {\n    color: $primary;\n    text-decoration: none;\n    font-weight: 600;\n    &.button {\n      font-size: 1.1rem;\n      color: white !important;\n      margin: 0;\n      font-weight: 600;\n    }\n    &:hover {\n      color: darken($primary, 15%);\n    }\n  }\n\n  p,\n  ul,\n  li {\n    color: #404040;\n    line-height: 1.6;\n    font-size: 1.1rem;\n  }\n\n  code {\n    font-size: 0.9rem;\n    background: #f0f0f0;\n  }\n\n  .new-signup {\n    background: white;\n    padding: 1.5rem;\n    border-radius: 0.3rem;\n    border: 1px solid $note-sidebar-color;\n    margin: 2rem 0;\n  }\n\n  .p-mobile {\n    color: $primary !important;\n    font-weight: 600;\n    margin: 1rem auto 1.5rem;\n  }\n\n  .content {\n    padding: 1rem 0;\n    background: lighten($note-sidebar-color, 5%);\n\n    .lead {\n      text-align: center;\n    }\n\n    h1 {\n      margin-top: 2rem;\n      font-weight: 700;\n      font-size: 2rem;\n      line-height: 1.1;\n      letter-spacing: -0.03rem;\n      margin-bottom: 0;\n\n      @include small-breakpoint {\n        font-size: 3rem;\n      }\n    }\n\n    .subtitle {\n      color: #888;\n      line-height: 1.6;\n      font-size: 1.2rem;\n      margin: 1rem auto 1.5rem;\n\n      @include small-breakpoint {\n        font-size: 1.4rem;\n      }\n    }\n  }\n\n  .container {\n    max-width: 1200px;\n    margin-left: auto;\n    margin-right: auto;\n    padding: 0 1.5rem;\n    &-small {\n      @extend .container;\n      max-width: 800px;\n    }\n  }\n\n  .screenshot {\n    max-width: 100%;\n    height: auto;\n  }\n\n  h2 {\n    margin-top: 0;\n    font-size: 1.6rem;\n    @include small-breakpoint {\n      font-size: 2rem;\n    }\n  }\n\n  .footer {\n    background: $primary;\n    padding: 2rem 0;\n    text-align: center;\n    color: white;\n\n    p {\n      color: white;\n    }\n\n    @include small-breakpoint {\n      padding: 4rem 0;\n    }\n\n    .logo {\n      display: block;\n      margin: 0 auto;\n      height: 50px;\n      width: 50px;\n      margin-bottom: 2rem;\n\n      @include small-breakpoint {\n        height: 100px;\n        width: 100px;\n        margin-bottom: 4rem;\n      }\n    }\n\n    a {\n      color: rgba(255, 255, 255, 0.8);\n      &:hover {\n        color: white;\n      }\n    }\n\n    nav {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      margin-bottom: 1.5rem;\n\n      a {\n        margin: 0 0.75rem;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_layout.scss",
    "content": ".loading {\n  height: 100vh;\n  width: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background: lighten($note-sidebar-color, 5%);\n\n  svg {\n    max-width: 200px;\n  }\n}\n\n.app {\n  height: 100vh;\n  position: relative;\n  overflow-y: hidden;\n}\n\n.app-sidebar {\n  height: 100%;\n  overflow-y: hidden;\n  overflow-x: hidden;\n\n  &::-webkit-scrollbar-thumb {\n    background: lighten($app-sidebar-color, 15%);\n    border-radius: 0;\n  }\n\n  &:hover {\n    overflow-y: auto;\n  }\n}\n\n.note-sidebar {\n  overflow-y: hidden;\n  overflow-x: hidden;\n\n  &::-webkit-scrollbar-thumb {\n    background: darken($note-sidebar-color, 10%);\n    border-radius: 0;\n  }\n}\n\n.note-editor {\n  position: relative;\n  min-width: 300px;\n  .empty-editor {\n    height: calc(100vh);\n  }\n  .editor,\n  .previewer {\n    padding-bottom: 38px;\n    height: 100vh;\n  }\n}\n\n.empty-editor {\n  display: flex !important;\n}\n\n.editor,\n.previewer {\n  display: block !important;\n}\n\n.options-context-menu {\n  cursor: default;\n  border-radius: 4px;\n  position: absolute;\n  color: $font-color;\n  top: 32px;\n  left: 200px;\n  min-width: 250px;\n  background: white;\n  padding: 0.25rem 0;\n  border: 1px solid darken($accent-gray, 5%);\n  z-index: 5;\n  box-shadow: 0 10px 15px rgba(0, 0, 0, 0.07), 0 5px 5px rgba(0, 0, 0, 0.2);\n\n  .move-to-category-select {\n    // This would be better as a right arrow new context menu than a dropdown\n    -webkit-appearance: none;\n    border-radius: 4px;\n    font-size: 0.9rem;\n    padding: 0.5rem;\n    width: 220px;\n    margin-left: auto;\n    margin-right: auto;\n    margin-top: 0.25rem;\n    margin-bottom: 0.5rem;\n  }\n}\n\n.options-nav {\n  font-size: 0.9rem;\n\n  .nav-item {\n    cursor: pointer;\n    display: flex;\n    padding: 0.5rem 1rem;\n    border-radius: 0;\n\n    &:hover {\n      background: #f0f0f0;\n      color: black;\n\n      svg {\n        color: rgba(0, 0, 0, 0.8);\n      }\n    }\n\n    &.delete-option {\n      &:hover {\n        background: $error;\n        color: white;\n\n        svg {\n          color: white;\n        }\n      }\n    }\n  }\n\n  svg {\n    pointer-events: none;\n    margin-right: 1rem;\n    color: rgba(0, 0, 0, 0.5);\n  }\n}\n\n.Resizer {\n  background: #000;\n  opacity: 0.2;\n  z-index: 1;\n  box-sizing: border-box;\n  background-clip: padding-box;\n}\n\n.Resizer:hover {\n  transition: all 2s ease;\n}\n\n.Resizer.horizontal {\n  height: 11px;\n  margin: -5px 0;\n  border-top: 5px solid rgba(255, 255, 255, 0);\n  border-bottom: 5px solid rgba(255, 255, 255, 0);\n  cursor: row-resize;\n  width: 100%;\n}\n\n.Resizer.horizontal:hover {\n  border-top: 5px solid rgba(0, 0, 0, 0.5);\n  border-bottom: 5px solid rgba(0, 0, 0, 0.5);\n}\n\n.Resizer.vertical {\n  width: 11px;\n  margin: 0 -5px;\n  border-left: 5px solid rgba(255, 255, 255, 0);\n  border-right: 5px solid rgba(255, 255, 255, 0);\n  cursor: col-resize;\n}\n\n.Resizer.vertical:hover {\n  border-left: 5px solid rgba(0, 0, 0, 0.5);\n  border-right: 5px solid rgba(0, 0, 0, 0.5);\n}\n.Resizer.disabled {\n  cursor: not-allowed;\n}\n.Resizer.disabled:hover {\n  border-color: transparent;\n}\n"
  },
  {
    "path": "src/client/styles/_light-theme.scss",
    "content": ".cm-s-base16-light {\n  span.cm-string {\n    color: #90a959;\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_mixins.scss",
    "content": "@mixin small-breakpoint {\n  @media (min-width: #{$tablet}) {\n    @content;\n  }\n}\n\n@mixin large-breakpoint {\n  @media (min-width: #{$desktop}) {\n    @content;\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_modal.scss",
    "content": ".dimmer {\n  position: fixed;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  text-align: center;\n  background: $dimmer-background;\n  line-height: 1;\n  user-select: none;\n  z-index: 99;\n}\n\n.settings-modal {\n  position: relative;\n  border-radius: 0.3rem;\n  background: white;\n  box-shadow: 0 10px 15px rgba(0, 0, 0, 0.07), 0 5px 5px rgba(0, 0, 0, 0.2);\n  text-align: left;\n  width: 850px;\n  max-width: 90%;\n  user-select: text;\n  z-index: 100;\n\n  h2 {\n    margin: 0;\n  }\n\n  .settings-modal-header {\n    position: relative;\n    width: 100%;\n    padding: 1rem 2rem 0.5rem;\n    border-bottom: 1px solid $note-sidebar-color;\n  }\n\n  .settings {\n    &-label {\n      font-weight: 600;\n      font-size: 1.3rem;\n      margin: 0.5rem 0 1rem 0;\n    }\n\n    &-option {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      padding: 1rem 0;\n      border-bottom: 1px solid $note-sidebar-color;\n      &:last-of-type {\n        border-bottom: none;\n      }\n      h3 {\n        margin-top: 0;\n        margin-bottom: 0.5rem;\n        font-weight: 500;\n        font-size: 1rem;\n      }\n      .description {\n        font-size: 0.9rem;\n        color: rgba(0, 0, 0, 0.5);\n        margin: 0;\n        line-height: 1.3;\n      }\n    }\n\n    &-shortcut {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      padding: 0.3rem 0;\n      font-size: 0.95rem;\n      .keys {\n        width: 180px;\n      }\n    }\n\n    &-content {\n      padding: 0 2rem 2rem 2rem;\n      height: 685px;\n      max-height: calc(80vh - 10rem);\n      overflow: auto;\n      select {\n        font-size: 1rem;\n        padding: 0.25rem 0.5rem;\n        margin: 0;\n        height: 30px;\n        max-width: 130px;\n        border-radius: 0.3rem;\n        -webkit-appearance: none;\n\n        &:active &:focus {\n          border: 1px solid lighten($primary, 15%);\n          box-shadow: 0 0 0.2rem lighten($primary, 15%);\n        }\n      }\n    }\n  }\n\n  .close-button {\n    cursor: pointer;\n    position: absolute;\n    top: 1rem;\n    right: 1rem;\n    margin: 0;\n    border-radius: 0.3rem;\n    padding: 0.5rem;\n\n    &:hover,\n    &:active {\n      background: darken($light-theme-background, 3%);\n    }\n  }\n\n  .profile {\n    align-items: center;\n  }\n\n  .profile-picture {\n    height: 100px;\n    width: 100px;\n    border-radius: 0;\n    border-radius: 50%;\n    margin-right: 1rem;\n  }\n\n  .profile-details {\n    h3 {\n      font-size: 1.2rem;\n      margin: 0;\n      margin-right: 1rem;\n    }\n  }\n\n  .subtitle {\n    color: lighten($font-color, 15%);\n    margin-bottom: 0.75rem;\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_new-moon.scss",
    "content": ".cm-s-new-moon .CodeMirror-gutters {\n  background: #333333 !important;\n  // Remove white border when line numbers are displayed\n  border-right: 0px;\n}\n\n.cm-s-new-moon .CodeMirror-linenumber {\n  // Adjust color of line number\n  color: #4f4f4f;\n}\n\n.cm-s-new-moon .CodeMirror-foldgutter-open,\n.CodeMirror-foldgutter-folded {\n  color: #999;\n}\n\n.cm-s-new-moon .CodeMirror-cursor {\n  border-left: 1px solid white;\n}\n\n.cm-s-new-moon {\n  background-color: #333333;\n  color: $code-font-color;\n}\n\n.cm-s-new-moon span.cm-builtin {\n  color: $attribute;\n  font-weight: bold;\n}\n\n.cm-s-new-moon span.cm-comment {\n  color: lighten($comment, 5%);\n}\n\n.cm-s-new-moon span.cm-keyword {\n  color: $keyword;\n  font-weight: bold;\n}\n\n.cm-s-new-moon span.cm-atom {\n  color: #bfebbf;\n}\n\n.cm-s-new-moon span.cm-def {\n  color: $attribute;\n}\n\n.cm-s-new-moon span.cm-variable {\n  color: $variable;\n}\n\n.cm-s-new-moon span.cm-variable-2 {\n  color: $tag;\n}\n\n.cm-s-new-moon span.cm-string {\n  color: $string;\n}\n\n.cm-s-new-moon span.cm-string-2 {\n  color: $string;\n}\n\n.cm-s-new-moon span.cm-number {\n  color: $number;\n}\n\n.cm-s-new-moon span.cm-tag {\n  color: $tag;\n}\n\n.cm-s-new-moon span.cm-property {\n  color: $punctuation;\n}\n\n.cm-s-new-moon span.cm-attribute {\n  color: $attribute;\n}\n\n.cm-s-new-moon span.cm-qualifier {\n  color: $function;\n}\n\n.cm-s-new-moon span.cm-meta {\n  color: $keyword;\n}\n\n.cm-s-new-moon span.cm-header {\n  color: $attribute;\n}\n\n.cm-s-new-moon span.cm-quote {\n  color: $class;\n}\n\n.cm-s-new-moon span.cm-strong {\n  color: $function;\n}\n\n.cm-s-new-moon span.cm-operator {\n  color: $operator;\n}\n\n.cm-s-new-moon span.CodeMirror-matchingbracket {\n  box-sizing: border-box;\n  background: transparent;\n  border-bottom: 1px solid;\n}\n\n.cm-s-new-moon span.CodeMirror-nonmatchingbracket {\n  border-bottom: 1px solid;\n  background: none;\n}\n\n.cm-s-new-moon .CodeMirror-activeline {\n  background: #000000;\n}\n\n.cm-s-new-moon .CodeMirror-activeline-background {\n  background: #000000;\n}\n\n.cm-s-new-moon div.CodeMirror-selected {\n  background: rgba(255, 255, 255, 0.15);\n}\n\n.cm-s-new-moon .CodeMirror-focused div.CodeMirror-selected {\n  background: rgba(255, 255, 255, 0.15);\n}\n"
  },
  {
    "path": "src/client/styles/_note-menu-bar.scss",
    "content": ".note-menu-bar {\n  height: 39px;\n  border-top: 1px solid darken($note-sidebar-color, 5%);\n  background: $note-sidebar-color;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  width: 100%;\n  z-index: 99;\n\n  nav {\n    display: flex;\n    align-items: stretch;\n    justify-content: flex-end;\n    align-self: stretch;\n  }\n\n  .uuid-menu-bar {\n    font-size: 0.7rem;\n    padding: 0 0.5rem;\n    align-self: center;\n  }\n\n  .last-synced {\n    font-size: 0.7rem;\n    padding: 0 0.5rem;\n    align-self: center;\n  }\n\n  .note-menu-bar-button {\n    background: transparent;\n    border: none;\n    border-radius: 0;\n    color: lighten($font-color, 15%);\n    padding: 0 0.75rem;\n    margin: 0;\n    &.trash {\n      &:hover {\n        color: darken($error, 20%);\n      }\n    }\n    &:hover {\n      color: $font-color;\n      background: darken($note-sidebar-color, 5%);\n    }\n    &:active svg {\n      transform: scale(1.1);\n    }\n    &.uuid {\n      display: flex;\n      align-items: center;\n    }\n  }\n\n  .uuid-copied-text {\n    padding-left: 0.5rem;\n    font-size: 0.7rem;\n    font-weight: 400;\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_note-sidebar.scss",
    "content": ".note-sidebar {\n  background: $note-sidebar-color;\n  border-right: 1px solid darken($note-sidebar-color, 10%);\n  height: 100%;\n\n  &:hover {\n    overflow-y: auto;\n  }\n\n  &-header {\n    display: flex;\n    align-items: center;\n    text-align: center;\n    font-weight: 700;\n    background: $note-sidebar-color;\n    height: 49px;\n    padding: 0 0.5rem;\n    border-bottom: 1px solid darken($note-sidebar-color, 10%);\n    input {\n      -webkit-appearance: none;\n      margin: 0;\n      min-width: 0;\n      padding: 0.5rem;\n    }\n  }\n\n  .list-button {\n    cursor: pointer;\n    align-items: center;\n    justify-content: space-between;\n    color: $font-color;\n    background: darken($note-sidebar-color, 8%);\n    padding: 0.7rem;\n    margin: 0 0 0 0.5rem;\n    font-size: 0.85rem;\n    font-weight: 500;\n    border: none !important;\n\n    &:hover,\n    &:focus {\n      background: $variable;\n      color: white;\n      outline: none;\n    }\n  }\n\n  .note-list {\n    &-outer {\n      display: flex;\n      align-items: center;\n      width: 100%;\n    }\n\n    &-each {\n      cursor: pointer;\n      padding: 0.5rem;\n      border-bottom: 1px solid darken($note-sidebar-color, 8%);\n      font-weight: 500;\n      font-size: 0.85rem;\n      line-height: 1.3;\n\n      .highlighted {\n        color: #3e64ff;\n      }\n\n      .note-category {\n        display: flex;\n        align-items: center;\n        font-size: 0.8rem;\n        color: rgba(0, 0, 0, 0.4);\n        margin-left: 25px;\n\n        svg {\n          margin-right: 0.5rem;\n        }\n      }\n\n      .note-title {\n        display: flex;\n        align-items: center;\n        width: 100%;\n\n        .icon {\n          display: flex;\n          flex: 0 0 25px;\n        }\n\n        .note-favorite {\n          stroke: $primary;\n          margin: 0.25rem;\n        }\n\n        .truncate-text {\n          overflow: hidden;\n          text-overflow: clip;\n        }\n      }\n\n      &:hover {\n        background: darken($note-sidebar-color, 5%);\n\n        .note-options {\n          color: $font-color;\n        }\n      }\n\n      &.selected {\n        background: $primary;\n        color: white;\n        border-bottom: 1px solid darken($primary, 5%);\n\n        .note-category {\n          color: rgba(255, 255, 255, 0.6);\n        }\n\n        &:hover {\n          .note-options {\n            color: white;\n          }\n        }\n\n        .highlighted {\n          background: white;\n        }\n\n        .note-options {\n          &.selected {\n            color: white;\n          }\n        }\n\n        .note-favorite {\n          stroke: white;\n        }\n      }\n\n      .note-options {\n        display: block;\n        font-size: 1rem;\n        color: transparent;\n        padding: 0.4rem;\n        z-index: 1;\n        cursor: pointer;\n\n        &.selected {\n          color: $font-color;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_previewer.scss",
    "content": ".previewer {\n  position: relative;\n  max-height: calc(100vh);\n  overflow-y: auto;\n  background: #fafafa;\n  color: #404040;\n  padding: 1rem;\n  -webkit-font-smoothing: subpixel-antialiased;\n\n  padding-bottom: 0px;\n  top: 0px;\n  bottom: 39px;\n\n  &_direction-ltr {\n    direction: ltr;\n  }\n\n  &_direction-rtl {\n    direction: rtl;\n  }\n\n  a {\n    color: $primary;\n    text-decoration: none;\n    font-weight: 600;\n\n    &:hover {\n      cursor: pointer;\n      text-decoration: underline;\n    }\n  }\n\n  .error {\n    color: $error;\n  }\n\n  p,\n  ol,\n  ul,\n  dl,\n  table {\n    font-size: 1.1rem;\n    line-height: 1.7;\n    margin: 0 0 1.5rem 0;\n  }\n\n  ul li ul {\n    margin-bottom: 0;\n  }\n\n  ol li ol {\n    margin-bottom: 0;\n  }\n\n  ul li [type='checkbox'] {\n    margin-right: 0.75rem;\n  }\n\n  h1,\n  h2,\n  h3,\n  h4,\n  h5 {\n    margin: 0 0 1.5rem 0;\n    font-weight: 600;\n    line-height: 1.2;\n\n    &:not(:first-child) {\n      margin: 1.5rem 0;\n    }\n  }\n\n  // Increased margin on additional headings\n  h1:not(:first-child),\n  h2:not(:first-child),\n  h3:not(:first-child) {\n    margin-top: 2rem;\n  }\n\n  // Heading individual styles\n  h1 {\n    margin-top: 0.5rem;\n    font-size: 2rem;\n  }\n\n  h2 {\n    font-size: 1.6rem;\n  }\n\n  h3 {\n    font-size: 1.4rem;\n  }\n\n  h4 {\n    font-size: 1.2rem;\n  }\n\n  h5 {\n    font-size: 1rem;\n  }\n\n  // Blockquote\n  blockquote {\n    margin: 0 0 1.5rem 0;\n    border-left: 4px solid $light-font-color;\n    padding: 0.5rem 1.5rem;\n\n    p {\n      font-size: 1.1rem;\n\n      &:last-of-type {\n        margin-bottom: 0;\n      }\n    }\n\n    cite {\n      display: block;\n      margin-top: 1.5rem;\n      font-size: 1rem;\n      text-align: right;\n    }\n  }\n\n  // Code block styling\n  pre {\n    background: lighten($note-sidebar-color, 8%);\n    padding: 1rem;\n    tab-size: 2;\n    color: #404040;\n    margin: 0 0 1.5rem 0;\n    white-space: pre-wrap;\n    word-spacing: normal;\n    word-break: normal;\n    border-radius: 0.3rem;\n    border: 1px solid darken($note-sidebar-color, 3%);\n    font-size: 0.9rem;\n    line-height: 1.4rem;\n\n    code {\n      padding: 0;\n      background: transparent;\n      line-height: 1.2;\n      border-width: 0;\n    }\n  }\n\n  code {\n    padding: 2px 3px;\n    background: lighten($note-sidebar-color, 8%);\n    border-radius: 0.3rem;\n    border: 1px solid darken($note-sidebar-color, 3%);\n  }\n\n  hr {\n    height: 0;\n    border: 0;\n    border-top: 2px solid lighten($light-font-color, 10%);\n  }\n\n  img {\n    max-width: 100%;\n    max-height: 20rem;\n    object-fit: cover;\n  }\n\n  table {\n    border: 1px solid $note-sidebar-color;\n    border-collapse: collapse;\n    border-spacing: 0;\n    max-width: 100%;\n  }\n\n  thead th {\n    border-bottom: 2px solid $note-sidebar-color;\n  }\n\n  tfoot th {\n    border-top: 2px solid $note-sidebar-color;\n  }\n\n  td {\n    border-bottom: 1px solid $note-sidebar-color;\n  }\n\n  th,\n  td {\n    text-align: left;\n    padding: 0.5rem;\n  }\n}\n\n.preview-button {\n  display: flex;\n  align-items: center;\n  position: absolute;\n  color: $font-color;\n  top: 0;\n  right: 1rem;\n  border: none;\n  background-color: $note-sidebar-color;\n  font-weight: 500;\n  padding: 0.5rem;\n  font-size: 0.8rem;\n  z-index: 2;\n  box-shadow: $box-shadow;\n\n  &:hover,\n  &:focus {\n    border: none;\n    color: darken($font-color, 10%);\n    background-color: darken($note-sidebar-color, 5%);\n  }\n\n  .invalid-note-uuid {\n    color: green;\n  }\n}\n"
  },
  {
    "path": "src/client/styles/_scaffolding.scss",
    "content": "html {\n  box-sizing: border-box;\n  font-size: 1rem;\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: inherit;\n}\n\nbody {\n  margin: 0;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  font-family: $sans-serif;\n  color: $font-color;\n}\n\ncode {\n  font-family: $monospace;\n}\n\na {\n  color: $primary;\n  text-decoration: none;\n  font-weight: 600;\n  &:hover {\n    color: lighten($primary, 10%);\n  }\n}\n\np {\n  line-height: 1.4;\n}\n\n::selection {\n  background: $primary;\n  color: white;\n}\n"
  },
  {
    "path": "src/client/styles/_tabs.scss",
    "content": ".tabs {\n  margin-top: 1.5rem;\n  display: flex;\n  flex-direction: row;\n  align-items: flex-start;\n  justify-content: flex-start;\n  width: 100%;\n\n  h3 {\n    margin-top: 0.5rem;\n  }\n\n  p {\n    font-size: 0.95rem;\n    color: lighten($font-color, 8%);\n  }\n}\n\n.tab-list {\n  flex: 0 0 220px;\n  height: 100%;\n  padding-right: 1rem;\n  margin-right: 1rem;\n  border-right: 1px solid $note-sidebar-color;\n}\n\n.tab {\n  display: flex;\n  align-items: center;\n  padding: 0.75rem;\n  margin: 0.25rem 0;\n  cursor: pointer;\n  border-radius: 0.3rem;\n  font-weight: 600;\n  font-size: 0.95rem;\n\n  svg {\n    color: rgba(0, 0, 0, 0.5);\n  }\n\n  &:hover {\n    color: darken($font-color, 10%);\n    background: lighten($note-sidebar-color, 5%);\n    svg {\n      color: rgba(0, 0, 0, 0.6);\n    }\n  }\n  &.active {\n    color: darken($font-color, 15%);\n    background: lighten($note-sidebar-color, 3%) !important;\n    svg {\n      color: rgba(0, 0, 0, 0.8);\n    }\n  }\n}\n\n.tab-content {\n  flex: 1;\n  height: 100%;\n}\n"
  },
  {
    "path": "src/client/styles/_variables.scss",
    "content": "// Sizes\n$app-sidebar-width: 240px;\n$note-sidebar-width: 340px;\n\n$mobile: 575px;\n$tablet: 768px;\n$desktop: 991px;\n\n$note-header-height: 60px;\n\n// Colors\n$primary: #5183f5;\n$font-color: #404040;\n$light-font-color: #d0d0d0;\n$app-sidebar-color: #2d2d2d;\n$note-sidebar-color: #e5e5e5;\n$light-theme-background: #f5f5f5;\n$accent-gray: #d0d0d0;\n$accent-lightgray: lighten($accent-gray, 12%);\n$dimmer-background: rgba(0, 0, 0, 0.6);\n$error: #f2777a;\n\n$box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.1), 0 2px 4px 0 rgba(0, 0, 0, 0.08);\n\n// Font\n$sans-serif: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',\n  'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;\n\n$monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;\n\n// Elements\n$buttons: ('.button, a.button, button, [type=submit], [type=reset], [type=button]');\n$forms: (\n  '[type=color], [type=date], [type=datetime], [type=datetime-local], [type=email], [type=month], [type=number], [type=password], [type=search], [type=tel], [type=text], [type=url], [type=week], [type=time], select, textarea'\n);\n\n// New Moon\n\n$code-font-color: #b3b9c5;\n$string: #92d192;\n$variable: #f2777a;\n$property: #abb2bf;\n$number: #fca369;\n$operator: #ac8d58;\n$punctuation: #d5d8df;\n$comment: #777c85;\n$function: #62cfcf;\n$keyword: #ffeead;\n$attribute: #ffd479;\n$class: #e1a6f2;\n$tag: #6ab0f3;\n$error: #f2777a;\n"
  },
  {
    "path": "src/client/styles/index.scss",
    "content": "@import 'variables';\n@import 'mixins';\n@import 'scaffolding';\n@import 'layout';\n@import 'app-sidebar';\n@import 'note-sidebar';\n@import 'note-menu-bar';\n@import 'editor';\n@import 'previewer';\n@import 'light-theme';\n@import 'new-moon';\n@import 'buttons';\n@import 'forms';\n@import 'modal';\n@import 'tabs';\n@import 'helpers';\n@import 'landing-page';\n\n@import 'dark';\n"
  },
  {
    "path": "src/client/types/index.ts",
    "content": "import React from 'react'\n\nimport { Folder, NotesSortKey } from '@/utils/enums'\nimport { sync } from '@/slices/sync'\n\n//==============================================================================\n// Items\n//==============================================================================\n\nexport interface NoteItem {\n  id: string\n  text: string\n  created: string\n  lastUpdated: string\n  /**\n   * Refers to the category UUID and not the actual name.\n   */\n  category?: string\n  scratchpad?: boolean\n  trash?: boolean\n  favorite?: boolean\n}\n\nexport interface CategoryItem {\n  id: string\n  name: string\n  draggedOver: boolean\n}\n\nexport interface GithubUser {\n  [anyProp: string]: any\n}\n\n//==============================================================================\n// State\n//==============================================================================\n\nexport interface AuthState {\n  loading: boolean\n  currentUser: GithubUser\n  isAuthenticated: boolean\n  error?: string\n}\n\nexport interface CategoryState {\n  categories: CategoryItem[]\n  error: string\n  loading: boolean\n  editingCategory: {\n    id: string\n    tempName: string\n  }\n}\n\nexport interface NoteState {\n  notes: NoteItem[]\n  activeFolder: Folder\n  activeNoteId: string\n  selectedNotesIds: string[]\n  activeCategoryId: string\n  error: string\n  loading: boolean\n  searchValue: string\n}\n\nexport interface SettingsState {\n  isOpen: boolean\n  previewMarkdown: boolean\n  loading: boolean\n  darkTheme: boolean\n  sidebarVisible: boolean\n  notesSortKey: NotesSortKey\n  codeMirrorOptions: { [key: string]: any }\n}\n\nexport interface SyncState {\n  syncing: boolean\n  lastSynced: string\n  error: string\n  pendingSync: boolean\n}\n\nexport interface RootState {\n  authState: AuthState\n  categoryState: CategoryState\n  noteState: NoteState\n  settingsState: SettingsState\n  syncState: SyncState\n}\n\n//==============================================================================\n// API\n//==============================================================================\n\nexport interface SyncPayload {\n  categories: CategoryItem[]\n  notes: NoteItem[]\n}\n\nexport interface SyncAction {\n  type: typeof sync.type\n  payload: SyncPayload\n}\n\n//==============================================================================\n// Events\n//==============================================================================\n\nexport type ReactDragEvent = React.DragEvent<HTMLDivElement>\n\nexport type ReactMouseEvent =\n  | MouseEvent\n  | React.MouseEvent<HTMLDivElement>\n  | React.ChangeEvent<HTMLSelectElement>\n\nexport type ReactSubmitEvent = React.FormEvent<HTMLFormElement> | React.FocusEvent<HTMLInputElement>\n\n//==============================================================================\n// Default Types\n//==============================================================================\n\n// Taken from TypeScript private declared type within Actions\nexport type WithPayload<P, T> = T & {\n  payload: P\n}\n"
  },
  {
    "path": "src/client/utils/constants.ts",
    "content": "import { Folder, NotesSortKey, DirectionText } from '@/utils/enums'\n\nexport const folderMap: Record<Folder, string> = {\n  [Folder.ALL]: 'All Notes',\n  [Folder.FAVORITES]: 'Favorites',\n  [Folder.SCRATCHPAD]: 'Scratchpad',\n  [Folder.TRASH]: 'Trash',\n  [Folder.CATEGORY]: 'Category',\n}\n\nexport const iconColor = 'rgba(255, 255, 255, 0.25)'\n\nexport const shortcutMap = [\n  { action: 'Create a new note', key: 'N' },\n  { action: 'Delete a note', key: 'U' },\n  { action: 'Create a category', key: 'C' },\n  { action: 'Download a note', key: 'O' },\n  { action: 'Sync all notes', key: 'L' },\n  { action: 'Markdown preview', key: 'P' },\n  { action: 'Toggle theme', key: 'K' },\n  { action: 'Search notes', key: 'F' },\n  { action: 'Prettify a note', key: 'I' },\n]\n\nexport const notesSortOptions = [\n  { value: NotesSortKey.TITLE, label: 'Title' },\n  { value: NotesSortKey.CREATED_DATE, label: 'Date Created' },\n  { value: NotesSortKey.LAST_UPDATED, label: 'Last Updated' },\n]\n\nexport const directionTextOptions = [\n  { value: DirectionText.LEFT_TO_RIGHT, label: 'Left to right' },\n  { value: DirectionText.RIGHT_TO_LEFT, label: 'Right to left' },\n]\n"
  },
  {
    "path": "src/client/utils/enums.ts",
    "content": "export enum Folder {\n  ALL = 'ALL',\n  CATEGORY = 'CATEGORY',\n  FAVORITES = 'FAVORITES',\n  SCRATCHPAD = 'SCRATCHPAD',\n  TRASH = 'TRASH',\n}\n\nexport enum Shortcuts {\n  NEW_NOTE = 'ctrl+alt+n',\n  NEW_CATEGORY = 'ctrl+alt+c',\n  DELETE_NOTE = 'ctrl+alt+u',\n  SYNC_NOTES = 'ctrl+alt+l',\n  DOWNLOAD_NOTES = 'ctrl+alt+o',\n  PREVIEW = 'alt+ctrl+p',\n  TOGGLE_THEME = 'alt+ctrl+k',\n  SEARCH = 'alt+ctrl+f',\n  PRETTIFY = 'ctrl+alt+i',\n}\n\nexport enum ContextMenuEnum {\n  CATEGORY = 'CATEGORY',\n  NOTE = 'NOTE',\n}\n\nexport enum NotesSortKey {\n  LAST_UPDATED = 'lastUpdated',\n  TITLE = 'title',\n  CREATED_DATE = 'created_date',\n}\n\nexport enum DirectionText {\n  LEFT_TO_RIGHT = 'ltr',\n  RIGHT_TO_LEFT = 'rtl',\n}\n\nexport enum Errors {\n  INVALID_LINKED_NOTE_ID = '<invalid note id provided>',\n}\n"
  },
  {
    "path": "src/client/utils/helpers.ts",
    "content": "import dayjs from 'dayjs'\nimport { v4 as uuid } from 'uuid'\nimport JSZip from 'jszip'\nimport { Action } from 'redux'\nimport * as clipboard from 'clipboard-polyfill/text'\n\nimport { LabelText } from '@resources/LabelText'\nimport { Folder, NotesSortKey } from '@/utils/enums'\nimport { folderMap } from '@/utils/constants'\nimport { NoteItem, CategoryItem, WithPayload } from '@/types'\n\nexport const getActiveNote = (notes: NoteItem[], activeNoteId: string) =>\n  notes.find((note) => note.id === activeNoteId)\n\nexport const getShortUuid = (uuid: string) => {\n  return uuid.substr(0, 6)\n}\n\nexport const getActiveNoteFromShortUuid = (notes: NoteItem[], shortUuid: string) => {\n  const uuidWithoutHash = shortUuid.replace('{{', '').replace('}}', '')\n\n  return notes.find((note) => note.id.startsWith(uuidWithoutHash))\n}\n\nexport const getActiveCategory = (categories: CategoryItem[], activeCategoryId: string) =>\n  categories.find(({ id }) => id === activeCategoryId)\n\nexport const getNoteTitle = (text: string): string => {\n  // Remove whitespace from both ends\n  // Get the first n characters\n  // Remove # from the title in the case of using markdown headers in your title\n  const noteText = text.trim().match(/[^#]{1,45}/)\n\n  // Get the first line of text after any newlines\n  // In the future, this should break on a full word\n  return noteText ? noteText[0].trim().split(/\\r?\\n/)[0] : LabelText.NEW_NOTE\n}\n\nexport const noteWithFrontmatter = (note: NoteItem, category?: CategoryItem): string =>\n  `---\ntitle: ${getNoteTitle(note.text)}\ncreated: ${note.created}\nlastUpdated: ${note.lastUpdated}\ncategory: ${category?.name ?? ''}\n---\n\n${note.text}`\n\n// Downloads a single note as a markdown file or a group of notes as a zip file.\nexport const downloadNotes = (notes: NoteItem[], categories: CategoryItem[]): void => {\n  if (notes.length === 1) {\n    const pom = document.createElement('a')\n\n    pom.setAttribute(\n      'href',\n      `data:text/plain;charset=utf-8,${encodeURIComponent(\n        noteWithFrontmatter(\n          notes[0],\n          categories.find((category: CategoryItem) => category.id === notes[0].category)\n        )\n      )}`\n    )\n    pom.setAttribute('download', `${getNoteTitle(notes[0].text)}.md`)\n\n    if (document.createEvent) {\n      const event = document.createEvent('MouseEvents')\n      event.initEvent('click', true, true)\n      pom.dispatchEvent(event)\n    } else {\n      pom.click()\n    }\n  } else {\n    const zip = new JSZip()\n    notes.forEach((note) =>\n      zip.file(\n        `${getNoteTitle(note.text)} (${note.id.substring(0, 6)}).md`,\n        noteWithFrontmatter(\n          note,\n          categories.find((category: CategoryItem) => category.id === note.category)\n        )\n      )\n    )\n\n    zip.generateAsync({ type: 'blob' }).then(\n      (content) => {\n        var downloadUrl = window.URL.createObjectURL(content)\n        var a = document.createElement('a')\n        a.href = downloadUrl\n        a.download = 'notes.zip'\n        document.body.appendChild(a)\n        a.click()\n        URL.revokeObjectURL(downloadUrl)\n      },\n      (err) => {\n        // TODO: error generating zip file.\n        // Generate a popup?\n      }\n    )\n  }\n}\n\nexport const backupNotes = (notes: NoteItem[], categories: CategoryItem[]) => {\n  const pom = document.createElement('a')\n\n  const json = JSON.stringify({ notes, categories })\n  const blob = new Blob([json], { type: 'application/json' })\n\n  const downloadUrl = window.URL.createObjectURL(blob)\n  pom.href = downloadUrl\n  pom.download = `takenote-backup-${dayjs().format('YYYY-MM-DD')}.json`\n  document.body.appendChild(pom)\n\n  // @ts-ignore\n  if (!window.Cypress) {\n    pom.click()\n    URL.revokeObjectURL(downloadUrl)\n  }\n}\n\nconst newNote = (categoryId?: string, folder?: Folder): NoteItem => ({\n  id: uuid(),\n  text: '',\n  created: dayjs().format(),\n  lastUpdated: dayjs().format(),\n  category: categoryId,\n  favorite: folder === Folder.FAVORITES,\n})\n\nexport const newNoteHandlerHelper = (\n  activeFolder: Folder,\n  previewMarkdown: boolean,\n  activeNote: NoteItem | undefined,\n  activeCategoryId: string,\n  swapFolder: (\n    folder: Folder\n  ) => WithPayload<{ folder: string; sortOrderKey?: NotesSortKey }, Action<string>>,\n  togglePreviewMarkdown: () => WithPayload<undefined, Action<string>>,\n  addNote: (note: NoteItem) => WithPayload<NoteItem, Action<string>>,\n  updateActiveNote: (\n    noteId: string,\n    multiSelect: boolean\n  ) => WithPayload<\n    {\n      noteId: string\n      multiSelect: boolean\n    },\n    Action<string>\n  >,\n  updateSelectedNotes: (\n    noteId: string,\n    multiSelect: boolean\n  ) => WithPayload<\n    {\n      noteId: string\n      multiSelect: boolean\n    },\n    Action<string>\n  >\n) => {\n  if ([Folder.TRASH, Folder.SCRATCHPAD].indexOf(activeFolder) !== -1) {\n    swapFolder(Folder.ALL)\n  }\n\n  if (previewMarkdown) {\n    togglePreviewMarkdown()\n  }\n\n  if ((activeNote && activeNote.text !== '') || !activeNote) {\n    const note = newNote(\n      activeCategoryId,\n      activeFolder === Folder.TRASH ? Folder.ALL : activeFolder\n    )\n    addNote(note)\n    updateSelectedNotes(note.id, false)\n    updateActiveNote(note.id, false)\n  }\n}\n\nexport const shouldOpenContextMenu = (clicked: Element) => {\n  if (!clicked.parentElement) return\n\n  const elementContainsClass = (className: string) => clicked.classList.contains(className)\n\n  const parentContainsClass = (className: string) =>\n    clicked.parentElement!.classList.contains(className)\n\n  return (\n    (clicked instanceof Element &&\n      // If the element is explicitly a context menu action\n      elementContainsClass('context-menu-action')) ||\n    // If the element is an item of the context menu\n    (!elementContainsClass('nav-item') &&\n      !elementContainsClass('options-context-menu') &&\n      !elementContainsClass('nav-item-icon') &&\n      !parentContainsClass('nav-item-icon')) ||\n    // Or if it's a sub-element of the context menu\n    (clicked.tagName === 'circle' && parentContainsClass('context-menu-action'))\n  )\n}\n\nexport const getWebsiteTitle = (activeFolder: Folder, activeCategory?: CategoryItem) => {\n  // Show category name if category is active\n  if (activeFolder === Folder.CATEGORY && activeCategory) {\n    return `${activeCategory.name} | TakeNote`\n  } else {\n    // Show main folder name otherwise\n    return `${folderMap[activeFolder]} | TakeNote`\n  }\n}\n\nexport const determineAppClass = (\n  darkTheme: boolean,\n  sidebarVisible: boolean,\n  activeFolder: Folder\n) => {\n  let className = 'app'\n\n  if (darkTheme) className += ' dark'\n\n  return className\n}\n\nexport const determineCategoryClass = (\n  category: CategoryItem,\n  isDragging: boolean,\n  activeCategoryId: string\n) => {\n  if (category.draggedOver) {\n    return 'category-list-each dragged-over'\n  } else if (category.id === activeCategoryId) {\n    return 'category-list-each active'\n  } else if (isDragging) {\n    return 'category-list-each dragging'\n  } else {\n    return 'category-list-each'\n  }\n}\n\nexport const debounceEvent = <T extends Function>(cb: T, wait = 20) => {\n  let h = 0\n  const callable = (...args: any) => {\n    clearTimeout(h)\n    h = window.setTimeout(() => cb(...args), wait)\n  }\n\n  return <T>(<any>callable)\n}\n\nexport const isDraftNote = (note: NoteItem) => {\n  return !note.scratchpad && note.text === ''\n}\n\nexport const getDayJsLocale = (languagetoken: string): string => {\n  try {\n    require('dayjs/locale/' + languagetoken + '.js')\n\n    return languagetoken\n  } catch (error) {\n    if (languagetoken.includes('-'))\n      return getDayJsLocale(languagetoken.substring(0, languagetoken.lastIndexOf('-')))\n\n    return 'en'\n  }\n}\n\nexport const getNoteBarConf = (\n  activeFolder: Folder\n): {\n  minSize?: number\n  maxSize?: number\n  defaultSize?: number\n  allowResize?: boolean\n  resizerStyle?: React.CSSProperties\n} => {\n  switch (activeFolder) {\n    case Folder.SCRATCHPAD:\n      return {\n        minSize: 0,\n        maxSize: 0,\n        defaultSize: 0,\n        allowResize: false,\n        resizerStyle: { display: 'none' },\n      }\n\n    default:\n      return {\n        minSize: 200,\n        maxSize: 600,\n        defaultSize: 330,\n      }\n  }\n}\n\nexport const copyToClipboard = (text: string) => {\n  clipboard.writeText(text)\n}\n"
  },
  {
    "path": "src/client/utils/history.ts",
    "content": "import { createBrowserHistory } from 'history'\n\nexport default createBrowserHistory()\n"
  },
  {
    "path": "src/client/utils/hooks.ts",
    "content": "import mousetrap from 'mousetrap'\nimport { useEffect, useRef } from 'react'\n\nimport 'mousetrap-global-bind'\n\nconst noop = () => {}\n\nexport function useInterval(callback: () => void, delay: number | null) {\n  const savedCallback = useRef(noop)\n\n  // Remember the latest callback\n  useEffect(() => {\n    savedCallback.current = callback\n  }, [callback])\n\n  // Set up the interval\n  useEffect(() => {\n    const tick = () => savedCallback.current()\n    if (delay) {\n      const id = setInterval(tick, delay)\n\n      return () => clearInterval(id)\n    }\n  }, [delay])\n}\n\nexport function useKey(key: string, action: () => void) {\n  const actionRef = useRef(noop)\n  actionRef.current = action\n\n  useEffect(() => {\n    mousetrap.bindGlobal(key, (event: Event) => {\n      event.preventDefault()\n      if (actionRef.current) {\n        actionRef.current()\n      }\n    })\n\n    return () => mousetrap.unbind(key)\n  }, [key])\n}\n\nexport function useBeforeUnload(handler: Function = () => {}) {\n  if (process.env.NODE_ENV !== 'production' && typeof handler !== 'function') {\n    throw new TypeError(`Expected \"handler\" to be a function, not ${typeof handler}.`)\n  }\n\n  const handlerRef = useRef(handler)\n\n  // Remember the latest callback\n  useEffect(() => {\n    handlerRef.current = handler\n  }, [handler])\n\n  // Set up the before unload event\n  useEffect(() => {\n    const handleBeforeunload = (event: BeforeUnloadEvent) => {\n      let returnValue\n\n      if (typeof handlerRef.current === 'function') {\n        returnValue = handlerRef.current(event)\n      }\n\n      if (event.defaultPrevented) {\n        event.returnValue = ''\n      }\n\n      if (typeof returnValue === 'string') {\n        event.returnValue = returnValue\n\n        return returnValue\n      }\n    }\n\n    window.addEventListener('beforeunload', handleBeforeunload)\n\n    return () => {\n      window.removeEventListener('beforeunload', handleBeforeunload)\n    }\n  }, [])\n}\n"
  },
  {
    "path": "src/client/utils/notesSortStrategies.ts",
    "content": "import { NoteItem } from '@/types'\n\nimport { getNoteTitle } from './helpers'\nimport { NotesSortKey } from './enums'\n\nexport interface NotesSortStrategy {\n  sort: (a: NoteItem, b: NoteItem) => number\n}\n\nconst withFavorites = (sortFunction: NotesSortStrategy['sort']) => (a: NoteItem, b: NoteItem) => {\n  if (a.favorite && !b.favorite) return -1\n  if (!a.favorite && b.favorite) return 1\n\n  return sortFunction(a, b)\n}\n\nconst createdDate: NotesSortStrategy = {\n  sort: (a: NoteItem, b: NoteItem): number => {\n    const dateA = new Date(a.created)\n    const dateB = new Date(b.created)\n\n    return dateA < dateB ? 1 : -1\n  },\n}\n\nconst lastUpdated: NotesSortStrategy = {\n  sort: (a: NoteItem, b: NoteItem): number => {\n    const dateA = new Date(a.lastUpdated)\n    const dateB = new Date(b.lastUpdated)\n\n    // the first note in the list should consistently sort after if it is created at the same time\n    return dateA < dateB ? 1 : -1\n  },\n}\n\nconst title: NotesSortStrategy = {\n  sort: (a: NoteItem, b: NoteItem): number => {\n    const titleA = getNoteTitle(a.text)\n    const titleB = getNoteTitle(b.text)\n\n    if (titleA === titleB) return 0\n\n    return titleA > titleB ? 1 : -1\n  },\n}\n\nexport const sortStrategyMap: { [key in NotesSortKey]: NotesSortStrategy } = {\n  [NotesSortKey.LAST_UPDATED]: lastUpdated,\n  [NotesSortKey.TITLE]: title,\n  [NotesSortKey.CREATED_DATE]: createdDate,\n}\n\nexport const getNotesSorter = (notesSortKey: NotesSortKey) =>\n  withFavorites(sortStrategyMap[notesSortKey].sort)\n"
  },
  {
    "path": "src/client/utils/reactMarkdownPlugins.ts",
    "content": "import visit from 'unist-util-visit'\n\n// This regexp will match any string starting with a # followed by 6 alphanumeric chars\n// #k5b4m3, #j4n7k3, etc (substring of a note's UUID)\nconst noteUuidRegexp = /\\{\\{[a-z0-9]{6}\\}\\}/\n\nconst extractText = (string: string, start: number, end: number) => {\n  const startLine = string.slice(0, start).split('\\n')\n  const endLine = string.slice(0, end).split('\\n')\n\n  return {\n    type: 'text',\n    value: string.slice(start, end),\n    position: {\n      start: {\n        line: startLine.length,\n        column: startLine[startLine.length - 1].length + 1,\n      },\n      end: {\n        line: endLine.length,\n        column: endLine[endLine.length - 1].length + 1,\n      },\n    },\n  }\n}\n\nexport const uuidPlugin = () => {\n  function transformer(tree: any) {\n    visit(tree, 'text', (node: any, position: any, parent: any) => {\n      const definition = []\n      let lastIndex = 0\n      let match\n\n      if ((match = noteUuidRegexp.exec(node.value)) !== null) {\n        const value = match[0]\n        const type = 'uuid'\n\n        if (match.index !== lastIndex) {\n          definition.push(extractText(node.value, lastIndex, match.index))\n        }\n\n        definition.push({\n          type,\n          value,\n        })\n\n        lastIndex = match.index + value.length\n      }\n\n      if (lastIndex !== node.value.length) {\n        const text = extractText(node.value, lastIndex, node.value.length)\n        definition.push(text)\n      }\n\n      if (!parent) return\n      const last = parent.children.slice(position + 1)\n      parent.children = parent.children.slice(0, position)\n      parent.children = parent.children.concat(definition)\n      parent.children = parent.children.concat(last)\n    })\n  }\n\n  return transformer\n}\n"
  },
  {
    "path": "src/resources/LabelText.ts",
    "content": "// Default Labels\nexport enum LabelText {\n  ADD_CATEGORY = 'Add category',\n  COLLAPSE_CATEGORY = 'Collapse Category List',\n  NOTES = 'Notes',\n  CREATE_NEW_NOTE = 'Create new note',\n  DELETE_PERMANENTLY = 'Delete permanently',\n  DOWNLOAD = 'Download',\n  FAVORITES = 'Favorites',\n  SCRATCHPAD = 'Scratchpad',\n  MARK_AS_FAVORITE = 'Mark as favorite',\n  MOVE_TO_TRASH = 'Move to trash',\n  NEW_CATEGORY = 'New category...',\n  NEW_NOTE = 'New note',\n  REMOVE_CATEGORY = 'Remove category',\n  REMOVE_FAVORITE = 'Remove favorite',\n  MOVE_CATEGORY = 'Move category',\n  RESTORE_FROM_TRASH = 'Restore from trash',\n  SETTINGS = 'Settings',\n  SYNC_NOTES = 'Sync notes',\n  TRASH = 'Trash',\n  WELCOME_TO_TAKENOTE = 'Welcome to Takenote!',\n  RENAME = 'Rename category',\n  ADD_CONTENT_NOTE = 'Please add content to this new note to access the menu options.',\n  DOWNLOAD_ALL_NOTES = 'Download all notes',\n  BACKUP_ALL_NOTES = 'Export backup',\n  IMPORT_BACKUP = 'Import backup',\n  TOGGLE_FAVORITE = 'Toggle favorite',\n  COPY_REFERENCE_TO_NOTE = 'Copy reference to note',\n}\n"
  },
  {
    "path": "src/resources/TestID.ts",
    "content": "// data-testid\nexport enum TestID {\n  ACTION_BUTTON = 'action-button',\n  ADD_CATEGORY_BUTTON = 'add-category-button',\n  CATEGORY_EDIT = 'category-edit',\n  CATEGORY_COLLAPSE_BUTTON = 'category-collapse-button',\n  CATEGORY_LIST_DIV = 'category-list-div',\n  CATEGORY_OPTIONS_NAV = 'category-options-nav',\n  CATEGORY_OPTION_RENAME = 'category-options-rename',\n  CATEGORY_OPTION_DELETE_PERMANENTLY = 'category-option-delete-permanently',\n  EDIT_CATEGORY = 'edit-category',\n  EMPTY_TRASH_BUTTON = 'empty-trash-button',\n  FOLDER_NOTES = 'notes',\n  FOLDER_FAVORITES = 'favorites',\n  FOLDER_TRASH = 'trash',\n  NEW_CATEGORY_FORM = 'new-category-form',\n  NEW_CATEGORY_INPUT = 'new-category-label',\n  NOTE_LIST = 'note-list',\n  NOTE_LINK_ERROR = 'note-link-error',\n  NOTE_LINK_SUCCESS = 'note-link-success',\n  NOTE_LIST_ITEM = 'note-list-item-',\n  NOTE_OPTIONS_DIV = 'note-options-div-',\n  NOTE_OPTIONS_NAV = 'note-options-nav',\n  NOTE_OPTION_DELETE_PERMANENTLY = 'note-option-delete-permanently',\n  NOTE_OPTION_DOWNLOAD = 'note-option-download',\n  NOTE_OPTION_FAVORITE = 'note-option-favorite',\n  NOTE_OPTION_REMOVE_CATEGORY = 'note-option-remove-category',\n  NOTE_OPTION_RESTORE_FROM_TRASH = 'note-option-restore-from-trash',\n  NOTE_OPTION_TRASH = 'note-option-trash',\n  NOTE_SEARCH = 'note-search',\n  NOTE_TITLE = 'note-title-',\n  MOVE_TO_CATEGORY = 'note-options-move-to-category-select',\n  REMOVE_CATEGORY = 'remove-category',\n  MOVE_CATEGORY = 'move-category',\n  SIDEBAR_ACTION_CREATE_NEW_NOTE = 'sidebar-action-create-new-note',\n  SIDEBAR_ACTION_SETTINGS = 'sidebar-action-settings',\n  TOPBAR_ACTION_SYNC_NOTES = 'topbar-action-sync-notes',\n  SCRATCHPAD = 'scratchpad',\n  NOTE_OPTION_ADD_CONTENT_NOTE = 'note-option-add-content-note',\n  SETTINGS_MODAL_DOWNLOAD_NOTES = 'settings-modal-download-notes',\n  DARK_MODE_TOGGLE = 'dark-mode-toggle',\n  MARKDOWN_PREVIEW_TOGGLE = 'markdown-preview-toggle',\n  ACTIVE_LINE_HIGHLIGHT_TOGGLE = 'active-line-highlight-toggle',\n  DISPLAY_LINE_NUMS_TOGGLE = 'display-line-nums-toggle',\n  SCROLL_PAST_END_TOGGLE = 'scroll-past-end-toggle',\n  SORT_BY_DROPDOWN = 'sort-by-dropdown',\n  TEXT_DIRECTION_DROPDOWN = 'text-direction-dropdown',\n  UPLOAD_SETTINGS_BACKUP = 'upload-settings-backup',\n  UUID_MENU_BAR_COPY_ICON = 'uuid-menu-bar-copy-icon',\n  PREVIEW_MODE = 'preview-mode',\n  COPY_REFERENCE_TO_NOTE = 'copy-reference-to-note',\n  EMPTY_EDITOR = 'empty-editor',\n  ICON_BUTTON = 'icon-button',\n  ICON_BUTTON_UPLOADER = 'icon-button-uploader',\n  LAST_SYNCED_NOTIFICATION_SYNCING = 'last-synced-notification-syncing',\n  LAST_SYNCED_NOTIFICATION_UNSAVED = 'last-synced-notification-unsaved',\n  LAST_SYNCED_NOTIFICATION_DATE = 'last-synced-notification-date',\n}\n"
  },
  {
    "path": "src/server/handlers/auth.ts",
    "content": "import { Request, Response } from 'express'\nimport axios from 'axios'\nimport * as dotenv from 'dotenv'\n\nimport { welcomeNote } from '../utils/data/welcomeNote'\nimport { scratchpadNote } from '../utils/data/scratchpadNote'\nimport { thirtyDayCookie } from '../utils/constants'\nimport { SDK } from '../utils/helpers'\nimport { Method } from '../utils/enums'\n\ndotenv.config()\n\nconst clientId = process.env.CLIENT_ID\nconst clientSecret = process.env.CLIENT_SECRET\n\nexport default {\n  /**\n   * GitHub OAuth flow\n   * @url https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/\n   *\n   * 1. When the user hits the `/authorize` endpoint on GitHub, they\n   * will be prompted to log in (if not already logged in) and redirected to\n   * this `/callback` endpoint with a code.\n   *\n   * 2. The code will be exchanged for an access token on the `/access_token`\n   * endpoint and the user will be redirected to the app.\n   *\n   * Client-side persistence\n   * @url https://www.taniarascia.com/full-stack-cookies-localstorage-react-express/\n   *\n   * A thirty day, secure, HTTP only, and same-site cookie will be set on the app\n   * containing the access token with repo scope for accessing any GitHub data\n   * and determining login state.\n   *\n   * Refresh tokens\n   * @url https://developer.github.com/apps/migrating-oauth-apps-to-github-apps/\n   *\n   * From the GitHub docs:\n   * > An OAuth token does not expire until the person who authorized the OAuth\n   * > App revokes the token.\n   *\n   * Therefore, there is no refresh token set up and the only option is to\n   * log in again.\n   */\n  callback: async (request: Request, response: Response) => {\n    const { code } = request.query\n\n    try {\n      // Obtain access token\n      const { data } = await axios({\n        method: 'post',\n        url: `https://github.com/login/oauth/access_token?client_id=${clientId}&client_secret=${clientSecret}&code=${code}`,\n        headers: {\n          accept: 'application/json',\n        },\n      })\n\n      const accessToken = data.access_token\n\n      // Set cookie\n      response.cookie('githubAccessToken', accessToken, thirtyDayCookie)\n\n      // Redirect to the app when logged in\n      response.redirect('/app')\n    } catch (error) {\n      console.log(error) // eslint-disable-line\n      // Redirect to the main page if something went wrong\n      response.redirect('/')\n    }\n  },\n\n  /**\n   * Authentication\n   *\n   * If an access token cookie exists, attempt to determine the currently logged\n   * in user. If the access token is in some way incorrect, expired, etc., throw\n   * an error.\n   *\n   * After successful login, check if it's the first time logging in by seeing if a repo\n   * named `takenote-data` exists. If it doesn't, create it.\n   */\n  login: async (request: Request, response: Response) => {\n    const { accessToken } = response.locals\n\n    try {\n      const { data } = await SDK(Method.GET, '/user', accessToken)\n      const username = data.login\n\n      const isFirstTimeLoggingIn = await firstTimeLoginCheck(username, accessToken)\n\n      if (isFirstTimeLoggingIn) {\n        await createTakeNoteDataRepo(username, accessToken)\n        await createInitialCommit(username, accessToken)\n      }\n\n      response.status(200).send(data)\n    } catch (error) {\n      response.status(400).send({ message: error.message })\n    }\n  },\n\n  logout: async (request: Request, response: Response) => {\n    response.clearCookie('githubAccessToken')\n\n    response.status(200).send({ message: 'Logged out' })\n  },\n}\n\nasync function firstTimeLoginCheck(username: string, accessToken: string): Promise<boolean> {\n  try {\n    await SDK(Method.GET, `/repos/${username}/takenote-data`, accessToken)\n\n    // If repo already exists, we assume it's the takenote data repo and can move on\n    return false\n  } catch (error) {\n    // If repo doesn't exist, we'll try to create it\n    return true\n  }\n}\n\nasync function createTakeNoteDataRepo(username: string, accessToken: string): Promise<void> {\n  const takenoteDataRepo = {\n    name: 'takenote-data',\n    description: 'Database of notes for TakeNote',\n    private: true,\n    visibility: 'private',\n    has_issues: false,\n    has_projects: false,\n    has_wiki: false,\n    is_template: false,\n    auto_init: false,\n    allow_squash_merge: false,\n    allow_rebase_merge: false,\n  }\n  try {\n    await SDK(Method.POST, `/user/repos`, accessToken, takenoteDataRepo)\n  } catch (error) {\n    throw new Error(error)\n  }\n}\n\nasync function createInitialCommit(username: string, accessToken: string): Promise<void> {\n  const noteCommit = {\n    message: 'Initial commit',\n    content: Buffer.from(JSON.stringify([scratchpadNote, welcomeNote], null, 2)).toString('base64'),\n    branch: 'master',\n  }\n  try {\n    await SDK(\n      Method.PUT,\n      `/repos/${username}/takenote-data/contents/notes.json`,\n      accessToken,\n      noteCommit\n    )\n  } catch (error) {\n    throw new Error(error)\n  }\n}\n"
  },
  {
    "path": "src/server/handlers/sync.ts",
    "content": "import { Request, Response } from 'express'\nimport dayjs from 'dayjs'\n\nimport { SDK } from '../utils/helpers'\nimport { Method } from '../utils/enums'\n\nexport default {\n  sync: async (request: Request, response: Response) => {\n    const { accessToken, userData } = response.locals\n    const {\n      body: { notes, categories },\n    } = request\n    const username = userData.login\n    const repo = 'takenote-data'\n\n    try {\n      // Get a reference\n      // https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference\n      const ref = await SDK(\n        Method.GET,\n        `/repos/${username}/${repo}/git/refs/heads/master`,\n        accessToken\n      )\n\n      // Create blobs\n      // https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-blob\n      const [noteBlob, categoryBlob] = await Promise.all([\n        SDK(Method.POST, `/repos/${username}/${repo}/git/blobs`, accessToken, {\n          content: JSON.stringify(notes, null, 2),\n        }),\n        SDK(Method.POST, `/repos/${username}/${repo}/git/blobs`, accessToken, {\n          content: JSON.stringify(categories, null, 2),\n        }),\n      ])\n\n      // Create tree path\n      const treeItems = [\n        {\n          path: 'notes.json',\n          sha: noteBlob.data.sha,\n          mode: '100644',\n          type: 'blob',\n        },\n        {\n          path: 'categories.json',\n          sha: categoryBlob.data.sha,\n          mode: '100644',\n          type: 'blob',\n        },\n      ]\n\n      // Create tree\n      // https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-tree\n      const tree = await SDK(Method.POST, `/repos/${username}/${repo}/git/trees`, accessToken, {\n        tree: treeItems,\n        base_tree: ref.data.object.sha,\n      })\n\n      // Create commit\n      // https://docs.github.com/en/free-pro-team@latest/rest/reference/git#create-a-commit\n      const commit = await SDK(Method.POST, `/repos/${username}/${repo}/git/commits`, accessToken, {\n        message: 'TakeNote update ' + dayjs(Date.now()).format('h:mm A M/D/YYYY'),\n        tree: tree.data.sha,\n        parents: [ref.data.object.sha],\n      })\n\n      // Update a reference\n      // https://docs.github.com/en/free-pro-team@latest/rest/reference/git#update-a-reference\n      await SDK(Method.POST, `/repos/${username}/${repo}/git/refs/heads/master`, accessToken, {\n        sha: commit.data.sha,\n        force: true,\n      })\n\n      response.status(200).send({ message: 'Successly commited to takenote-data' })\n    } catch (error) {\n      response\n        .status(400)\n        .send({ message: error.message || 'Something went wrong while syncing data' })\n    }\n  },\n\n  getNotes: async (request: Request, response: Response) => {\n    const { accessToken, userData } = response.locals\n    const username = userData.login\n    const repo = 'takenote-data'\n\n    try {\n      const { data } = await SDK(\n        Method.GET,\n        `/repos/${username}/${repo}/contents/notes.json`,\n        accessToken\n      )\n\n      const notes = Buffer.from(data.content, 'base64').toString()\n\n      try {\n        JSON.parse(notes)\n      } catch (error) {\n        response.status(400).send({ message: error.message || 'Must be valid JSON.' })\n      }\n\n      response.status(200).send(notes)\n    } catch (error) {\n      response\n        .status(400)\n        .send({ message: error.message || 'Something went wrong while fetching note data' })\n    }\n  },\n\n  getCategories: async (request: Request, response: Response) => {\n    const { accessToken, userData } = response.locals\n    const username = userData.login\n    const repo = 'takenote-data'\n\n    try {\n      const { data } = await SDK(\n        Method.GET,\n        `/repos/${username}/${repo}/contents/categories.json`,\n        accessToken\n      )\n\n      const categories = Buffer.from(data.content, 'base64').toString()\n\n      try {\n        JSON.parse(categories)\n      } catch (error) {\n        response.status(400).send({ message: error.message || 'Must be valid JSON.' })\n      }\n\n      response.status(200).send(categories)\n    } catch (error) {\n      response\n        .status(400)\n        .send({ message: error.message || 'Something went wrong while fetching category data' })\n    }\n  },\n}\n"
  },
  {
    "path": "src/server/index.ts",
    "content": "import initializeServer from './initializeServer'\nimport router from './router'\n\nconst app = initializeServer(router)\n\napp.listen(5000, () => console.log(`Listening on port ${5000}`)) // eslint-disable-line\n"
  },
  {
    "path": "src/server/initializeServer.ts",
    "content": "import path from 'path'\n\nimport express, { Router } from 'express'\nimport cookieParser from 'cookie-parser'\nimport cors from 'cors'\nimport helmet from 'helmet'\nimport compression from 'compression'\n\nexport default function initializeServer(router: Router) {\n  const app = express()\n  const isProduction = process.env.NODE_ENV === 'production'\n  const origin = { origin: isProduction ? false : '*' }\n\n  app.set('trust proxy', 1)\n  app.use(express.json())\n  app.use(cookieParser())\n  app.use(cors(origin))\n  app.use(helmet())\n  app.use(compression())\n\n  app.use((request, response, next) => {\n    response.header('Content-Security-Policy', \"img-src 'self' *.githubusercontent.com\")\n\n    return next()\n  })\n\n  app.use(express.static(path.join(__dirname, '../../dist/')))\n  app.use('/api', router)\n  app.get('*', (request, response) => {\n    response.sendFile(path.join(__dirname, '../../dist/index.html'))\n  })\n\n  return app\n}\n"
  },
  {
    "path": "src/server/middleware/checkAuth.ts",
    "content": "import { Request, Response, NextFunction } from 'express'\n\nconst checkAuth = async (request: Request, response: Response, next: NextFunction) => {\n  const accessToken = request.cookies?.githubAccessToken\n\n  if (accessToken) {\n    response.locals.accessToken = accessToken\n\n    next()\n  } else {\n    response.status(403).send({ message: 'Forbidden Resource', status: 403 })\n  }\n}\n\nexport default checkAuth\n"
  },
  {
    "path": "src/server/middleware/getUser.ts",
    "content": "import { Request, Response, NextFunction } from 'express'\n\nimport { SDK } from '../utils/helpers'\nimport { Method } from '../utils/enums'\n\nconst getUser = async (request: Request, response: Response, next: NextFunction) => {\n  const { accessToken } = response.locals\n\n  try {\n    const { data } = await SDK(Method.GET, '/user', accessToken)\n    response.locals.userData = data\n\n    next()\n  } catch (error) {\n    response.status(403).send({ message: 'Forbidden Resource', status: 403 })\n  }\n}\n\nexport default getUser\n"
  },
  {
    "path": "src/server/router/auth.ts",
    "content": "import express from 'express'\nimport * as dotenv from 'dotenv'\n\nimport authHandler from '../handlers/auth'\nimport checkAuth from '../middleware/checkAuth'\n\nconst router = express.Router()\ndotenv.config()\n\nrouter.get('/callback', authHandler.callback)\nrouter.get('/login', checkAuth, authHandler.login)\nrouter.get('/logout', authHandler.logout)\n\nexport default router\n"
  },
  {
    "path": "src/server/router/index.ts",
    "content": "import express from 'express'\n\nimport authRoutes from './auth'\nimport syncRoutes from './sync'\n\nconst router = express.Router()\n\nrouter.use('/auth', authRoutes)\nrouter.use('/sync', syncRoutes)\n\nexport default router\n"
  },
  {
    "path": "src/server/router/sync.ts",
    "content": "import express from 'express'\n\nimport syncHandler from '../handlers/sync'\nimport checkAuth from '../middleware/checkAuth'\nimport getUser from '../middleware/getUser'\n\nconst router = express.Router()\n\nrouter.post('/', checkAuth, getUser, syncHandler.sync)\nrouter.get('/notes', checkAuth, getUser, syncHandler.getNotes)\nrouter.get('/categories', checkAuth, getUser, syncHandler.getCategories)\n\nexport default router\n"
  },
  {
    "path": "src/server/utils/constants.ts",
    "content": "const isProduction = process.env.NODE_ENV === 'production'\n\nexport const thirtyDayCookie = {\n  maxAge: 60 * 60 * 1000 * 24 * 30,\n  secure: isProduction,\n  httpOnly: true,\n  sameSite: true,\n}\n"
  },
  {
    "path": "src/server/utils/data/scratchpadNote.ts",
    "content": "import { v4 as uuid } from 'uuid'\nimport dayjs from 'dayjs'\n\nexport const scratchpadNote = {\n  id: uuid(),\n  text: `# Scratchpad\n\nThe easiest note to find.`,\n  category: '',\n  scratchpad: true,\n  favorite: false,\n  created: dayjs().format(),\n  lastUpdated: dayjs().format(),\n}\n"
  },
  {
    "path": "src/server/utils/data/welcomeNote.ts",
    "content": "import { v4 as uuid } from 'uuid'\nimport dayjs from 'dayjs'\n\nconst markdown = `# Welcome to Takenote!\n\nTakeNote is a free, open-source notes app for the web. It is a demo project only, and does not integrate with any database or cloud. Your notes are saved in local storage and will not be permanently persisted, but are available for download.\n\nView the source on [Github](https://github.com/taniarascia/takenote).\n\n## Features\n\n- **Plain text notes** - take notes in an IDE-like environment that makes no assumptions\n- **Markdown preview** - view rendered HTML of the notes\n- **Linked notes** - use \\`{{uuid}}\\` syntax to link to notes within other notes\n- **Syntax highlighting** - light and dark mode available (based on the beautiful [New Moon theme](https://taniarascia.github.io/new-moon/))\n- **Keyboard shortcuts** - use the keyboard for all common tasks - creating notes and categories, toggling settings, and other options\n- **Drag and drop** - drag a note or multiple notes to categories, favorites, or trash\n- **Multi-cursor editing** - supports multiple cursors and other [Codemirror](https://codemirror.net/) options\n- **Search notes** - easily search all notes, or notes within a category\n- **Prettify notes** - use Prettier on the fly for your Markdown\n- **No WYSIWYG** - made for developers, by developers\n- **No database** - notes are only stored in the browser's local storage and are available for download and export to you alone\n- **No tracking or analytics** - 'nuff said\n- **GitHub integration** - self-hosted option is available for auto-syncing to a GitHub repository (not available in the demo)\n`\n\nexport const welcomeNote = {\n  id: uuid(),\n  text: markdown,\n  category: '',\n  favorite: false,\n  created: dayjs().format(),\n  lastUpdated: dayjs().format(),\n}\n"
  },
  {
    "path": "src/server/utils/enums.ts",
    "content": "export enum Method {\n  GET = 'GET',\n  POST = 'POST',\n  PUT = 'PUT',\n  PATCH = 'PATCH',\n  DELETE = 'DELETE',\n}\n"
  },
  {
    "path": "src/server/utils/helpers.ts",
    "content": "import axios from 'axios'\n\nimport { Method } from './enums'\n\nexport function SDK(method: Method, path: string, accessToken: string, data?: Object) {\n  const apiHost = 'https://api.github.com'\n\n  return axios({\n    method,\n    url: `${apiHost}${path}`,\n    data,\n    headers: {\n      Authorization: `token ${accessToken}`,\n    },\n  })\n}\n"
  },
  {
    "path": "tests/__mocks__/styleMock.ts",
    "content": "// __mocks__/styleMock.js\n\n// @ts-ignore\nmodule.exports = {}\n"
  },
  {
    "path": "tests/e2e/integration/category.test.ts",
    "content": "// category.spec.ts\n// Tests for manipulating note categories\n\nimport {\n  addCategory,\n  assertCategoryDoesNotExist,\n  assertCategoryExists,\n  assertCategoryOptionsOpened,\n  assertCategoryOrder,\n  navigateToCategory,\n  selectMoveToCategoryOption,\n  startEditingCategory,\n  renameCategory,\n  defocusCategory,\n  moveCategory,\n  openCategoryContextMenu,\n  clickCategoryOptionRename,\n  clickCategoryOptionDelete,\n  collapseCategoryList,\n  assertCategoryListExists,\n  assertCategoryListDoesNotExists,\n} from '../utils/testCategoryHelperUtils'\nimport { dynamicTimeCategoryName } from '../utils/testHelperEnums'\nimport {\n  defaultInit,\n  navigateToNotes,\n  assertCurrentFolderOrCategory,\n} from '../utils/testHelperUtils'\nimport {\n  assertNoteListLengthEquals,\n  clickNoteOptions,\n  createXUniqueNotes,\n} from '../utils/testNotesHelperUtils'\n\ndescribe('Categories', () => {\n  defaultInit()\n\n  it('should hide the category list on click of category', () => {\n    addCategory(dynamicTimeCategoryName)\n\n    collapseCategoryList()\n\n    assertCategoryListDoesNotExists()\n  })\n\n  it('should show category list on add new category', () => {\n    collapseCategoryList()\n\n    addCategory(dynamicTimeCategoryName)\n\n    assertCategoryListExists()\n  })\n\n  it('creates a new category with the current time', () => {\n    // Skipping for now due to\n    addCategory(dynamicTimeCategoryName)\n  })\n\n  it('should add a note to new category', () => {\n    // add a category\n    addCategory(dynamicTimeCategoryName)\n\n    // navigate back to All Notes create a new note, and move it to that category\n    navigateToNotes()\n    createXUniqueNotes(1)\n    clickNoteOptions()\n    selectMoveToCategoryOption(dynamicTimeCategoryName)\n\n    // make sure it ended up in the category\n    navigateToCategory(dynamicTimeCategoryName)\n    assertNoteListLengthEquals(1)\n  })\n\n  it('should rename existing category after defocusing edit state', () => {\n    const originalCategoryName = 'Category'\n    const newCategoryName = 'Renamed Category'\n\n    addCategory(originalCategoryName)\n    startEditingCategory(originalCategoryName)\n    renameCategory(originalCategoryName, newCategoryName)\n    defocusCategory(newCategoryName)\n\n    assertCategoryExists(newCategoryName)\n  })\n\n  it('should change category order', () => {\n    const firstCategory = 'Source Category'\n    const secondCategory = 'Destination Category'\n\n    addCategory(firstCategory)\n    addCategory(secondCategory)\n    moveCategory(firstCategory, secondCategory)\n    assertCategoryOrder(firstCategory, 3)\n    moveCategory(secondCategory, firstCategory)\n    assertCategoryOrder(secondCategory, 3)\n  })\n\n  it('should open context menu with right click', () => {\n    const categoryName = 'Context Menu'\n\n    addCategory(categoryName)\n    openCategoryContextMenu(categoryName)\n    assertCategoryOptionsOpened()\n  })\n\n  it('should allow category rename through context menu', () => {\n    const originalCategoryName = 'Category CM'\n    const newCategoryName = 'Renamed Category CM'\n\n    addCategory(originalCategoryName)\n    openCategoryContextMenu(originalCategoryName)\n    clickCategoryOptionRename()\n    renameCategory(originalCategoryName, newCategoryName)\n    defocusCategory(newCategoryName)\n\n    assertCategoryExists(newCategoryName)\n  })\n\n  it('should allow category permanent delete through context menu', () => {\n    addCategory(dynamicTimeCategoryName)\n\n    openCategoryContextMenu(dynamicTimeCategoryName)\n    clickCategoryOptionDelete()\n\n    assertCategoryDoesNotExist(dynamicTimeCategoryName)\n  })\n\n  it('should redirect to notes after deleting the category you are in', () => {\n    addCategory(dynamicTimeCategoryName)\n\n    navigateToCategory(dynamicTimeCategoryName)\n    openCategoryContextMenu(dynamicTimeCategoryName)\n    clickCategoryOptionDelete()\n\n    assertCurrentFolderOrCategory('Notes')\n  })\n})\n"
  },
  {
    "path": "tests/e2e/integration/note.test.ts",
    "content": "// note.test.ts\n// Tests for managing notes (create, trash, favorite, etc)\n\nimport { LabelText } from '@resources/LabelText'\nimport { TestID } from '@resources/TestID'\nimport { Errors } from '@/utils/enums'\n\nimport {\n  defaultInit,\n  getNoteCount,\n  navigateToNotes,\n  navigateToFavorites,\n  navigateToTrash,\n  testIDShouldContain,\n  testIDShouldNotExist,\n  clickDynamicTestID,\n} from '../utils/testHelperUtils'\nimport {\n  assertNewNoteCreated,\n  assertNoteEditorCharacterCount,\n  assertNoteEditorLineCount,\n  assertNoteListLengthEquals,\n  assertNoteListLengthGTE,\n  assertNoteListTitleAtIndex,\n  assertNoteOptionsOpened,\n  assertNotesSelected,\n  clickCreateNewNote,\n  createXUniqueNotes,\n  clickEmptyTrash,\n  clickNoteOptionDeleteNotePermanently,\n  clickNoteOptionFavorite,\n  clickNoteOptionRestoreFromTrash,\n  clickNoteOptionTrash,\n  clickNoteOptions,\n  clickNoteOptionCopyLinkedNoteMarkdown,\n  clickSyncNotes,\n  typeNoteEditor,\n  typeNoteSearch,\n  clearNoteSearch,\n  openNoteContextMenu,\n  holdKeyAndClickNoteAtIndex,\n  trashAllNotes,\n  dragAndDrop,\n} from '../utils/testNotesHelperUtils'\nimport {\n  addCategory,\n  selectMoveToCategoryOption,\n  navigateToCategory,\n} from '../utils/testCategoryHelperUtils'\nimport { dynamicTimeCategoryName } from '../utils/testHelperEnums'\n\ndescribe('Manage notes test', () => {\n  defaultInit()\n\n  before(() => {\n    // Delete welcome note\n    clickNoteOptions()\n    clickNoteOptionTrash()\n  })\n\n  beforeEach(() => {\n    navigateToNotes()\n    createXUniqueNotes(1)\n    trashAllNotes()\n    clearNoteSearch()\n    createXUniqueNotes(1)\n  })\n\n  it('should try to create a few new notes', () => {\n    clickCreateNewNote()\n    assertNoteListLengthEquals(2)\n    assertNewNoteCreated()\n\n    clickCreateNewNote()\n    assertNoteListLengthEquals(2)\n    assertNewNoteCreated()\n\n    clickCreateNewNote()\n    assertNoteListLengthEquals(2)\n    assertNewNoteCreated()\n  })\n\n  it('should link to another vote if a valid uuid is provided', () => {\n    createXUniqueNotes(3)\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    clickDynamicTestID(TestID.UUID_MENU_BAR_COPY_ICON)\n    const id = cy.task('getClipboard')\n\n    clickCreateNewNote()\n    cy.get('.CodeMirror textarea').invoke('val', `test ${id}`)\n\n    clickDynamicTestID(TestID.PREVIEW_MODE)\n    cy.get('a').should('exist')\n  })\n\n  it('should not link to another vote if an invalid uuid is provided', () => {\n    createXUniqueNotes(3)\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    clickCreateNewNote()\n    cy.get('.CodeMirror textarea').invoke('val', 'test {{z1x2c3}}')\n\n    clickDynamicTestID(TestID.PREVIEW_MODE)\n    cy.get('span.error').should('contain', Errors.INVALID_LINKED_NOTE_ID)\n  })\n\n  it('should copy note linking syntax from context menu', () => {\n    createXUniqueNotes(1)\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    openNoteContextMenu()\n    clickNoteOptionCopyLinkedNoteMarkdown()\n\n    cy.task('getClipboard').should('match', /\\{\\{[a-z0-9]{6}\\}\\}/)\n  })\n\n  it('should update a note', () => {\n    const sampleText = 'Sample note text.'\n\n    clickCreateNewNote()\n    // add some text to the editor\n    typeNoteEditor(sampleText)\n    assertNoteEditorLineCount(1)\n    assertNoteEditorCharacterCount(sampleText.length)\n\n    typeNoteEditor('{enter}123')\n    assertNoteEditorLineCount(2)\n    assertNoteEditorCharacterCount(sampleText.length + 3)\n\n    typeNoteEditor('{backspace}{backspace}{backspace}{backspace}')\n    assertNoteEditorLineCount(1)\n    assertNoteEditorCharacterCount(sampleText.length)\n\n    // // clean up state\n    // clickNoteOptions()\n    // clickNoteOptionTrash()\n    // clickCreateNewNote()\n    // navigateToTrash()\n    // clickEmptyTrash()\n  })\n\n  it('should open options', () => {\n    clickNoteOptions()\n    assertNoteOptionsOpened()\n  })\n\n  it('should open context menu through right click', () => {\n    openNoteContextMenu()\n    assertNoteOptionsOpened()\n  })\n\n  it('should add a note to favorites', () => {\n    // make sure favorites is empty\n    navigateToFavorites()\n    assertNoteListLengthEquals(0)\n\n    // favorite the note in All Notes\n    navigateToNotes()\n    clickNoteOptions()\n    testIDShouldContain(TestID.NOTE_OPTION_FAVORITE, LabelText.MARK_AS_FAVORITE)\n    clickNoteOptionFavorite()\n\n    // assert there is 1 favorited note\n    navigateToFavorites()\n    assertNoteListLengthEquals(1)\n\n    // assert button now says 'Remove'\n    clickNoteOptions()\n    testIDShouldContain(TestID.NOTE_OPTION_FAVORITE, LabelText.REMOVE_FAVORITE)\n    clickNoteOptionFavorite()\n\n    // assert favorites is empty\n    assertNoteListLengthEquals(0)\n  })\n\n  it('should add a note to favorites through context menu', () => {\n    // make sure favorites is empty\n    navigateToFavorites()\n    assertNoteListLengthEquals(0)\n\n    // favorite the note in All Notes\n    navigateToNotes()\n    openNoteContextMenu()\n    testIDShouldContain(TestID.NOTE_OPTION_FAVORITE, LabelText.MARK_AS_FAVORITE)\n    clickNoteOptionFavorite()\n\n    // assert there is 1 favorited note\n    navigateToFavorites()\n    assertNoteListLengthEquals(1)\n\n    // assert button now says 'Remove'\n    openNoteContextMenu()\n    testIDShouldContain(TestID.NOTE_OPTION_FAVORITE, LabelText.REMOVE_FAVORITE)\n    clickNoteOptionFavorite()\n\n    // assert favorites is empty\n    assertNoteListLengthEquals(0)\n  })\n\n  it('should send a note to trash', () => {\n    // make sure trash is currently empty\n    navigateToTrash()\n    assertNoteListLengthEquals(0)\n\n    // navigate back to All Notes and move the note to trash\n    navigateToNotes()\n    clickNoteOptions()\n    testIDShouldContain(TestID.NOTE_OPTION_TRASH, LabelText.MOVE_TO_TRASH)\n    clickNoteOptionTrash()\n    testIDShouldNotExist(TestID.NOTE_OPTION_TRASH)\n\n    // make sure the new note is in the trash\n    navigateToTrash()\n    assertNoteListLengthEquals(1)\n    clickEmptyTrash()\n  })\n\n  it('should empty notes in trash', () => {\n    // move note to trash\n    clickNoteOptions()\n    clickNoteOptionTrash()\n\n    // make sure there is a note in the trash and empty it\n    navigateToTrash()\n    assertNoteListLengthGTE(1)\n    clickEmptyTrash()\n    assertNoteListLengthEquals(0)\n\n    // assert the empty trash button is gone\n    testIDShouldNotExist(TestID.EMPTY_TRASH_BUTTON)\n  })\n\n  it('should send a note to trash through context menu', () => {\n    // make sure trash is currently empty\n    navigateToTrash()\n    assertNoteListLengthEquals(0)\n\n    // navigate back to All Notes and move the note to trash\n    navigateToNotes()\n    openNoteContextMenu()\n    testIDShouldContain(TestID.NOTE_OPTION_TRASH, LabelText.MOVE_TO_TRASH)\n    clickNoteOptionTrash()\n    testIDShouldNotExist(TestID.NOTE_OPTION_TRASH)\n\n    // make sure there is a note in the trash and empty it\n    navigateToTrash()\n    assertNoteListLengthEquals(1)\n    clickEmptyTrash()\n    assertNoteListLengthEquals(0)\n  })\n\n  it('should delete the active note in the trash permanently', () => {\n    // move note to trash\n    clickNoteOptions()\n    clickNoteOptionTrash()\n\n    // navigate to trash and delete the active note permanently\n    navigateToTrash()\n    clickNoteOptions()\n    testIDShouldContain(TestID.NOTE_OPTION_DELETE_PERMANENTLY, LabelText.DELETE_PERMANENTLY)\n    clickNoteOptionDeleteNotePermanently()\n    assertNoteListLengthEquals(0)\n\n    // assert the empty trash button is gone\n    testIDShouldNotExist(TestID.EMPTY_TRASH_BUTTON)\n  })\n\n  it('should delete the active note in the trash permanently through context menu', () => {\n    // move note to trash\n    openNoteContextMenu()\n    clickNoteOptionTrash()\n\n    // navigate to trash and delete the active note permanently\n    navigateToTrash()\n    openNoteContextMenu()\n    testIDShouldContain(TestID.NOTE_OPTION_DELETE_PERMANENTLY, LabelText.DELETE_PERMANENTLY)\n    clickNoteOptionDeleteNotePermanently()\n    assertNoteListLengthEquals(0)\n\n    // assert the empty trash button is gone\n    testIDShouldNotExist(TestID.EMPTY_TRASH_BUTTON)\n  })\n\n  it('should restore the active note in the trash', function () {\n    getNoteCount('allNoteStartCount')\n\n    // move note to trash\n    clickNoteOptions()\n    clickNoteOptionTrash()\n    cy.then(() => assertNoteListLengthEquals(this.allNoteStartCount - 1))\n\n    // navigate to trash and restore the active note\n    navigateToTrash()\n    getNoteCount('trashStartCount')\n    clickNoteOptions()\n    testIDShouldContain(TestID.NOTE_OPTION_RESTORE_FROM_TRASH, LabelText.RESTORE_FROM_TRASH)\n    clickNoteOptionRestoreFromTrash()\n    cy.then(() => assertNoteListLengthEquals(this.trashStartCount - 1))\n\n    // assert the empty trash button is gone\n    testIDShouldNotExist(TestID.EMPTY_TRASH_BUTTON)\n\n    // make sure the note is back in All Notes\n    navigateToNotes()\n    cy.then(() => assertNoteListLengthEquals(this.allNoteStartCount))\n  })\n\n  it('should restore the active note in the trash through context menu', function () {\n    getNoteCount('allNoteStartCount')\n\n    // move note to trash\n    openNoteContextMenu()\n    clickNoteOptionTrash()\n    cy.then(() => assertNoteListLengthEquals(this.allNoteStartCount - 1))\n\n    // navigate to trash and restore the active note\n    navigateToTrash()\n    getNoteCount('trashStartCount')\n    openNoteContextMenu()\n    testIDShouldContain(TestID.NOTE_OPTION_RESTORE_FROM_TRASH, LabelText.RESTORE_FROM_TRASH)\n    clickNoteOptionRestoreFromTrash()\n    cy.then(() => assertNoteListLengthEquals(this.trashStartCount - 1))\n\n    // assert the empty trash button is gone\n    testIDShouldNotExist(TestID.EMPTY_TRASH_BUTTON)\n\n    // make sure the note is back in All Notes\n    navigateToNotes()\n    cy.then(() => assertNoteListLengthEquals(this.allNoteStartCount))\n  })\n\n  it('should sync some notes', function () {\n    const noteOneTitle = 'note 1'\n    const noteTwoTitle = 'same note title'\n    const noteThreeTitle = 'same note title'\n    const noteFourTitle = 'note 4'\n\n    Cypress.on('window:before:unload', (event: BeforeUnloadEvent) =>\n      expect(event.returnValue).to.equal('')\n    )\n\n    // start with a refresh so we know our current saved state\n    cy.reload()\n    getNoteCount('allNoteStartCount')\n\n    // create a new note and refresh without syncing\n    clickCreateNewNote()\n    typeNoteEditor(noteOneTitle)\n    cy.reload()\n    cy.then(() => assertNoteListLengthEquals(this.allNoteStartCount))\n\n    // create a few new notes\n    clickCreateNewNote()\n    typeNoteEditor(noteOneTitle)\n    clickCreateNewNote()\n    typeNoteEditor(noteTwoTitle)\n    clickCreateNewNote()\n    typeNoteEditor(noteThreeTitle)\n    clickCreateNewNote()\n    typeNoteEditor(noteFourTitle)\n    clickSyncNotes()\n\n    // make sure notes persisted\n    cy.reload()\n    cy.then(() => assertNoteListLengthEquals(this.allNoteStartCount + 4))\n\n    // make sure order is correct\n    assertNoteListTitleAtIndex(3, noteOneTitle)\n    assertNoteListTitleAtIndex(2, noteTwoTitle)\n    assertNoteListTitleAtIndex(1, noteThreeTitle)\n    assertNoteListTitleAtIndex(0, noteFourTitle)\n  })\n\n  it('should search some notes', function () {\n    const noteOneTitle = 'note 1'\n    const noteTwoTitle = 'same note title'\n    const noteThreeTitle = 'same note title'\n    const noteFourTitle = 'note 4'\n\n    // start with a refresh so we know our current saved state\n    cy.reload()\n    getNoteCount('allNoteStartCount')\n\n    // create a few new notes\n    clickCreateNewNote()\n    typeNoteEditor(noteOneTitle)\n    clickCreateNewNote()\n    typeNoteEditor(noteTwoTitle)\n    clickCreateNewNote()\n    typeNoteEditor(noteThreeTitle)\n    clickCreateNewNote()\n    typeNoteEditor(noteFourTitle)\n\n    // make sure notes are filtered\n    typeNoteSearch('note title')\n    cy.then(() => assertNoteListLengthEquals(2))\n  })\n\n  it('should select multiple notes via ctrl/cmd + click', () => {\n    createXUniqueNotes(2)\n\n    // Select notes\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    assertNotesSelected(2)\n  })\n\n  it('should send multiple selected notes to trash via context menu', () => {\n    createXUniqueNotes(1)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n    openNoteContextMenu()\n    clickNoteOptionTrash()\n    assertNoteListLengthEquals(0)\n\n    navigateToTrash()\n    assertNoteListLengthEquals(2)\n  })\n\n  it('should send multiple selected notes to favorites via context menu', () => {\n    createXUniqueNotes(1)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    openNoteContextMenu()\n    clickNoteOptionFavorite()\n    assertNoteListLengthEquals(2)\n\n    navigateToFavorites()\n    assertNoteListLengthEquals(2)\n  })\n\n  it('should remove multiple selected notes from favorites via context menu', () => {\n    createXUniqueNotes(1)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    openNoteContextMenu()\n    clickNoteOptionFavorite()\n    assertNoteListLengthEquals(2)\n\n    navigateToFavorites()\n    assertNoteListLengthEquals(2)\n\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    openNoteContextMenu()\n    clickNoteOptionFavorite()\n    assertNoteListLengthEquals(0)\n\n    navigateToNotes()\n    assertNoteListLengthEquals(2)\n  })\n\n  it('should send multiple selected notes to favorites when clicking on an already favorited selected note via context menu', () => {})\n\n  it('should remove multiple selected notes from favorites when clicking on a not yet favorited selected note via context menu', () => {})\n\n  it('should send multiple selected notes to a category via context menu', () => {\n    // add a category\n    addCategory(dynamicTimeCategoryName)\n\n    // navigate back to All Notes create a new note, and move it to that category\n    navigateToNotes()\n    createXUniqueNotes(1)\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n    openNoteContextMenu()\n    selectMoveToCategoryOption(dynamicTimeCategoryName)\n    assertNoteListLengthEquals(2)\n\n    navigateToCategory(dynamicTimeCategoryName)\n    assertNoteListLengthEquals(2)\n  })\n\n  it('should restore multiple selected notes from trash', () => {\n    createXUniqueNotes(1)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n    openNoteContextMenu()\n    clickNoteOptionTrash()\n\n    navigateToTrash()\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    openNoteContextMenu()\n    clickNoteOptionRestoreFromTrash()\n    assertNoteListLengthEquals(0)\n\n    navigateToNotes()\n    assertNoteListLengthEquals(2)\n  })\n\n  it('should permanently delete multiple selected notes from trash', () => {\n    createXUniqueNotes(1)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n    openNoteContextMenu()\n    clickNoteOptionTrash()\n\n    navigateToTrash()\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    openNoteContextMenu()\n    clickNoteOptionDeleteNotePermanently()\n    assertNoteListLengthEquals(0)\n\n    navigateToNotes()\n    assertNoteListLengthEquals(0)\n  })\n\n  it('should send a not selected note to favorites with drag & drop', () => {\n    createXUniqueNotes(2)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-2]', '[data-testid=favorites]')\n\n    cy.get('[data-testid=favorites]').click()\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 1)\n    })\n  })\n\n  it('should send a not selected note to trash with drag & drop', () => {\n    createXUniqueNotes(2)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-2]', '[data-testid=trash]')\n\n    cy.get('[data-testid=trash]').click()\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 1)\n    })\n  })\n\n  it('should send a not selected note to a category with drag & drop', () => {\n    createXUniqueNotes(2)\n    addCategory(dynamicTimeCategoryName)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-2]', '[data-testid=category-list-div]')\n\n    cy.get('[data-testid=category-list-div]').click()\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 1)\n    })\n  })\n\n  it('should send multiple notes to favorites with drag & drop', () => {\n    createXUniqueNotes(2)\n    addCategory(dynamicTimeCategoryName)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-0]', '[data-testid=favorites]')\n\n    cy.get('[data-testid=favorites]').click()\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 2)\n    })\n  })\n\n  it('should send multiple notes to favorites with drag & drop', () => {\n    createXUniqueNotes(2)\n    addCategory(dynamicTimeCategoryName)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-0]', '[data-testid=trash]')\n\n    cy.get('[data-testid=trash]').click()\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 2)\n    })\n  })\n\n  it('should send multiple notes to a category with drag & drop', () => {\n    createXUniqueNotes(2)\n    addCategory(dynamicTimeCategoryName)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-0]', '[data-testid=category-list-div]')\n\n    cy.get('[data-testid=category-list-div]').click()\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 2)\n    })\n  })\n\n  it('should send a not selected trashed note to notes with drag & drop', () => {\n    createXUniqueNotes(2)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-0]', '[data-testid=trash]')\n\n    cy.get('[data-testid=trash]').click()\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 1)\n    })\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-0]', '[data-testid=notes]')\n\n    cy.get('[data-testid=notes]').click()\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 3)\n    })\n  })\n\n  it('should send multiple not selected trashed notes to notes with drag & drop', () => {\n    createXUniqueNotes(2)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(1, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-0]', '[data-testid=trash]')\n\n    cy.get('[data-testid=trash]').click()\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 2)\n    })\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-0]', '[data-testid=notes]')\n\n    cy.get('[data-testid=notes]').click()\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 3)\n    })\n  })\n\n  it('should not move a note that is already in Notes when dragged & dropped on Notes', () => {\n    createXUniqueNotes(2)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-0]', '[data-testid=notes]')\n\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 3)\n    })\n  })\n\n  it('should not move multiple notes that are already in Notes when dragged & dropped on Notes', () => {\n    createXUniqueNotes(2)\n\n    holdKeyAndClickNoteAtIndex(0, 'meta')\n    holdKeyAndClickNoteAtIndex(2, 'meta')\n\n    dragAndDrop('[data-testid=note-list-item-0]', '[data-testid=notes]')\n\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 3)\n    })\n  })\n\n  it('should not create a new draft note if one already exists', () => {\n    clickCreateNewNote()\n\n    cy.get('[data-testid=trash]').click()\n\n    clickCreateNewNote()\n\n    cy.get('[data-testid=note-list]').within(() => {\n      cy.get('.note-list-each').should('have.length', 2)\n    })\n  })\n})\n"
  },
  {
    "path": "tests/e2e/integration/settings.test.ts",
    "content": "// settings.test.ts\n// Tests for functionality available in the settings menu\n\nimport { TestID } from '@resources/TestID'\nimport { getNoteTitle } from '@/utils/helpers'\nimport { NoteItem, CategoryItem } from '@/types'\n\nimport {\n  addCategory,\n  assertCategoryExists,\n  clickCategoryOptionDelete,\n  openCategoryContextMenu,\n} from '../utils/testCategoryHelperUtils'\nimport { defaultInit, assertNoteContainsText } from '../utils/testHelperUtils'\nimport {\n  clickCreateNewNote,\n  createXUniqueNotes,\n  clickNoteOptionFavorite,\n  typeNoteEditor,\n  openNoteContextMenu,\n  holdKeyAndClickNoteAtIndex,\n  trashAllNotes,\n  assertNoteListLengthEquals,\n} from '../utils/testNotesHelperUtils'\nimport {\n  navigateToSettings,\n  assertSettingsMenuIsOpen,\n  assertSettingsMenuIsClosed,\n  closeSettingsByClickingX,\n  closeSettingsByClickingOutsideWindow,\n  toggleDarkMode,\n  toggleMarkdownPreview,\n  toggleLineNumbers,\n  toggleLineHighlight,\n  assertDarkModeActive,\n  assertDarkModeInactive,\n  assertMarkdownPreviewActive,\n  assertMarkdownPreviewInactive,\n  assertLineNumbersActive,\n  assertLineNumbersInactive,\n  selectOptionInSortByDropdown,\n  assertLineHighlightActive,\n  assertLineHighlightInactive,\n  clickSettingsTab,\n  getDownloadedBackup,\n} from '../utils/testSettingsUtils'\n\ndescribe('Settings', () => {\n  defaultInit()\n\n  before(() => {})\n\n  beforeEach(() => {\n    navigateToSettings()\n  })\n\n  afterEach(() => {\n    closeSettingsByClickingOutsideWindow()\n  })\n\n  describe('Preferences', () => {\n    const generateAndConfigureSomeNotes = () => {\n      const noteTitle = 'note 10'\n      const noteTitleAbc = 'B'\n\n      createXUniqueNotes(5)\n      holdKeyAndClickNoteAtIndex(0, 'meta')\n      holdKeyAndClickNoteAtIndex(1, 'meta')\n      openNoteContextMenu()\n      clickNoteOptionFavorite()\n\n      clickCreateNewNote()\n      typeNoteEditor(noteTitleAbc)\n\n      clickCreateNewNote()\n      typeNoteEditor(noteTitle)\n    }\n\n    it('should open settings menu', () => {\n      assertSettingsMenuIsOpen()\n    })\n\n    it('should close settings menu on clicking X', () => {\n      closeSettingsByClickingX()\n      assertSettingsMenuIsClosed()\n      navigateToSettings()\n    })\n\n    it('should close settings menu on clicking outside of window', () => {\n      closeSettingsByClickingOutsideWindow()\n      assertSettingsMenuIsClosed()\n      navigateToSettings()\n    })\n\n    it('should toggle preferences: active line highlight [off]', () => {\n      toggleLineHighlight()\n      closeSettingsByClickingOutsideWindow()\n      assertLineHighlightInactive()\n      navigateToSettings()\n    })\n\n    it('should toggle preferences: active line highlight [on]', () => {\n      toggleLineHighlight()\n      closeSettingsByClickingOutsideWindow()\n      assertLineHighlightActive()\n      navigateToSettings()\n    })\n\n    it('should toggle preferences: dark mode [on]', () => {\n      toggleDarkMode()\n      assertDarkModeActive()\n    })\n\n    it('should toggle preferences: dark mode [off]', () => {\n      toggleDarkMode()\n      assertDarkModeInactive()\n    })\n\n    it('should toggle preferences: markdown preview [on]', () => {\n      toggleMarkdownPreview()\n      assertMarkdownPreviewActive()\n    })\n\n    it('should toggle preferences: markdown preview [off]', () => {\n      toggleMarkdownPreview()\n      assertMarkdownPreviewInactive()\n    })\n\n    it('should toggle preferences: line numbers [on]', () => {\n      toggleLineNumbers()\n      assertLineNumbersActive()\n    })\n\n    it('should toggle preferences: line numbers [off]', () => {\n      toggleLineNumbers()\n      assertLineNumbersInactive()\n      closeSettingsByClickingX()\n      generateAndConfigureSomeNotes()\n      navigateToSettings()\n    })\n\n    it('should change sort order: last updated', () => {\n      selectOptionInSortByDropdown('Last Updated')\n      closeSettingsByClickingX()\n      assertNoteContainsText('note-list-item-2', 'note 10')\n      navigateToSettings()\n    })\n\n    it('should change sort order: title (alphabetical)', () => {\n      selectOptionInSortByDropdown('Title')\n      closeSettingsByClickingX()\n      assertNoteContainsText('note-list-item-2', 'B')\n      navigateToSettings()\n    })\n\n    it('should change sort order: date created', () => {\n      selectOptionInSortByDropdown('Date Created')\n      closeSettingsByClickingX()\n      assertNoteContainsText('note-list-item-2', 'note 10')\n      navigateToSettings()\n    })\n  })\n\n  describe('Data Management', () => {\n    before(() => {\n      trashAllNotes()\n    })\n\n    beforeEach(() => {\n      clickSettingsTab('data management')\n    })\n\n    it('should download backup', () => {\n      cy.findByRole('button', { name: /export backup/i }).click()\n\n      getDownloadedBackup().then((result) => {\n        const data = JSON.parse(result as string)\n\n        expect(data.notes).to.have.length(1)\n        expect(data.notes[0].text).to.include('Scratchpad')\n      })\n    })\n\n    it('should import backup', () => {\n      const categories = ['test_category_1', 'test_category_2']\n\n      closeSettingsByClickingOutsideWindow()\n      createXUniqueNotes(2)\n      categories.forEach((category) => {\n        addCategory(category)\n      })\n      navigateToSettings()\n      clickSettingsTab('data management')\n      cy.findByRole('button', { name: /export backup/i }).click()\n\n      getDownloadedBackup().then((result) => {\n        closeSettingsByClickingOutsideWindow()\n        trashAllNotes()\n        categories.forEach((category) => {\n          openCategoryContextMenu(category)\n          clickCategoryOptionDelete()\n        })\n        navigateToSettings()\n        clickSettingsTab('data management')\n\n        cy.findByTestId(TestID.UPLOAD_SETTINGS_BACKUP).attachFile({\n          fileContent: result as Blob,\n          filePath: '',\n          fileName: 'backup',\n          mimeType: 'application/json',\n          encoding: 'utf-8',\n        })\n\n        const backupData = JSON.parse(result as string) as {\n          categories: CategoryItem[]\n          notes: NoteItem[]\n        }\n\n        clickSettingsTab('preferences')\n        selectOptionInSortByDropdown('Title')\n        clickSettingsTab('data management')\n        closeSettingsByClickingX()\n\n        backupData.categories.forEach(({ name }) => {\n          assertCategoryExists(name)\n        })\n\n        assertNoteListLengthEquals(2)\n        backupData.notes.slice(1).forEach(({ text }, index) => {\n          assertNoteContainsText(TestID.NOTE_LIST_ITEM + index, getNoteTitle(text))\n        })\n\n        navigateToSettings()\n      })\n    })\n  })\n})\n"
  },
  {
    "path": "tests/e2e/plugins/cy-ts-preprocessor.js",
    "content": "const path = require('path')\n\nconst wp = require('@cypress/webpack-preprocessor')\n\nconst webpackOptions = {\n  resolve: {\n    extensions: ['.tsx', '.ts', '.js'],\n    alias: {\n      '@': path.resolve(__dirname, '../../../src/client'),\n      '@resources': path.resolve(__dirname, '../../../src/resources'),\n    },\n    // Polyfills\n    fallback: {\n      path: require.resolve('path-browserify'), // Needed for cypress-file-upload\n      stream: require.resolve('stream-browserify'), // Needed to import utils from client folder\n    },\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.ts(x?)$/,\n        exclude: [/node_modules/],\n        use: [\n          {\n            loader: 'ts-loader',\n            options: {\n              transpileOnly: true,\n            },\n          },\n        ],\n      },\n    ],\n  },\n}\n\nconst options = { webpackOptions }\n\nmodule.exports = wp(options)\n"
  },
  {
    "path": "tests/e2e/plugins/index.js",
    "content": "const clipboardy = require('clipboardy')\n\nconst cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor')\n\nmodule.exports = (on) => {\n  on('file:preprocessor', cypressTypeScriptPreprocessor)\n\n  on('task', {\n    getClipboard() {\n      return clipboardy.readSync()\n    },\n  })\n}\n"
  },
  {
    "path": "tests/e2e/support/commands.js",
    "content": "import '@testing-library/cypress/add-commands'\nimport 'cypress-file-upload'\n\nCypress.Commands.add('dragAndDrop', { prevSubject: 'element' }, (subject, target) => {\n  Cypress.log({\n    name: 'DRAGNDROP',\n    message: `Dragging element ${subject} to ${target}`,\n    consoleProps: () => {\n      return {\n        subject: subject,\n        target: target,\n      }\n    },\n  })\n  const BUTTON_INDEX = 0\n  const SLOPPY_CLICK_THRESHOLD = 10\n  cy.contains(target).then(($target) => {\n    const coordsDrop = $target[0].getBoundingClientRect()\n    cy.get(subject)\n      .first()\n      .then((subject) => {\n        const coordsDrag = subject[0].getBoundingClientRect()\n        cy.wrap(subject)\n          .trigger('mousedown', {\n            button: BUTTON_INDEX,\n            clientX: coordsDrag.x,\n            clientY: coordsDrag.y,\n            force: true,\n          })\n          .trigger('mousemove', {\n            button: BUTTON_INDEX,\n            clientX: coordsDrag.x + SLOPPY_CLICK_THRESHOLD,\n            clientY: coordsDrag.y,\n            force: true,\n          })\n\n        cy.get('body')\n          .trigger('mousemove', {\n            button: BUTTON_INDEX,\n            clientX: coordsDrag.x,\n            clientY: coordsDrop.y,\n            force: true,\n          })\n          .wait(0.2 * 1000)\n          .trigger('mouseup')\n      })\n  })\n})\n"
  },
  {
    "path": "tests/e2e/support/index.js",
    "content": "import './commands'\n\n// Since before unload alert hangs console tests, the alert has to be disabled\n// Check https://github.com/cypress-io/cypress/issues/2118 for more info\nCypress.on('window:before:load', function (win) {\n  const original = win.EventTarget.prototype.addEventListener\n\n  win.EventTarget.prototype.addEventListener = function () {\n    if (arguments && arguments[0] === 'beforeunload') return\n\n    return original.apply(this, arguments)\n  }\n\n  Object.defineProperty(win, 'onbeforeunload', {\n    get: function () {},\n    set: function () {},\n  })\n})\n"
  },
  {
    "path": "tests/e2e/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"../node_modules/cypress\", \"**/*.ts\"],\n  \"compilerOptions\": {\n    \"types\": [\"cypress\", \"@types/testing-library__cypress\", \"cypress-file-upload\"],\n    \"isolatedModules\": false,\n    \"noEmit\": false\n  }\n}\n"
  },
  {
    "path": "tests/e2e/utils/testCategoryHelperUtils.ts",
    "content": "// testCategoryHelperUtils.ts\n// Utility functions for use in category tests\n\nimport { TestID } from '@resources/TestID'\n\nimport {\n  getTestID,\n  wrapWithTestIDTag,\n  testIDShouldExist,\n  clickTestID,\n  testIDShouldNotExist,\n} from './testHelperUtils'\n\nconst addCategory = (categoryName: string) => {\n  getTestID(TestID.ADD_CATEGORY_BUTTON).click()\n  getTestID(TestID.NEW_CATEGORY_INPUT).type(categoryName)\n  getTestID(TestID.NEW_CATEGORY_FORM).submit()\n\n  cy.contains(categoryName)\n}\n\nconst collapseCategoryList = () => {\n  getTestID(TestID.CATEGORY_COLLAPSE_BUTTON).click()\n}\n\nconst assertCategoryDoesNotExist = (categoryName: string) => {\n  cy.findByText(categoryName).should('not.exist')\n}\n\nconst assertCategoryExists = (categoryName: string) => {\n  cy.contains(wrapWithTestIDTag(TestID.CATEGORY_LIST_DIV), categoryName).should('exist')\n}\n\nconst assertCategoryListExists = () => {\n  testIDShouldExist(TestID.CATEGORY_LIST_DIV)\n}\n\nconst assertCategoryListDoesNotExists = () => {\n  testIDShouldNotExist(TestID.CATEGORY_LIST_DIV)\n}\n\nconst assertCategoryOrder = (categoryName: string, position: number) => {\n  cy.get('.category-list > div').eq(position).contains(categoryName)\n}\n\nconst assertCategoryOptionsOpened = () => {\n  testIDShouldExist(TestID.CATEGORY_OPTIONS_NAV)\n}\n\nconst defocusCategory = (categoryName: string) => {\n  getTestID(TestID.CATEGORY_EDIT).blur()\n}\n\nconst navigateToCategory = (categoryName: string) => {\n  cy.get('.category-list').contains(categoryName).click()\n}\n\nconst moveCategory = (categoryName: string, targetName: string) => {\n  cy.contains(categoryName)\n    .parent()\n    .find(wrapWithTestIDTag(TestID.MOVE_CATEGORY))\n    // @ts-ignore\n    .dragAndDrop(targetName)\n    .wait(1 * 1000)\n}\n\nconst renameCategory = (oldCategoryName: string, newCategoryName: string) => {\n  getTestID(TestID.CATEGORY_EDIT).focus().clear().type(newCategoryName)\n}\n\nconst openCategoryContextMenu = (categoryName: string) => {\n  cy.contains(categoryName).parent().rightclick()\n}\n\nconst selectMoveToCategoryOption = (categoryName: string) => {\n  getTestID(TestID.MOVE_TO_CATEGORY).select(categoryName)\n}\n\nconst startEditingCategory = (categoryName: string) => {\n  cy.get('.category-list')\n    .contains(wrapWithTestIDTag(TestID.CATEGORY_LIST_DIV), categoryName)\n    .dblclick()\n}\n\nconst clickCategoryOptionRename = () => {\n  clickTestID(TestID.CATEGORY_OPTION_RENAME)\n}\n\nconst clickCategoryOptionDelete = () => {\n  clickTestID(TestID.CATEGORY_OPTION_DELETE_PERMANENTLY)\n}\n\nexport {\n  addCategory,\n  assertCategoryExists,\n  assertCategoryDoesNotExist,\n  assertCategoryOrder,\n  assertCategoryOptionsOpened,\n  defocusCategory,\n  navigateToCategory,\n  moveCategory,\n  renameCategory,\n  selectMoveToCategoryOption,\n  startEditingCategory,\n  openCategoryContextMenu,\n  clickCategoryOptionRename,\n  clickCategoryOptionDelete,\n  collapseCategoryList,\n  assertCategoryListExists,\n  assertCategoryListDoesNotExists,\n}\n"
  },
  {
    "path": "tests/e2e/utils/testHelperEnums.ts",
    "content": "// testHelperEnums.ts\n// Default enumerated values that can be used in tests\n\nconst entryPoint = '/app'\nconst dynamicTimeCategoryName = `Cy${Date.now()}`\n\nexport { entryPoint, dynamicTimeCategoryName }\n"
  },
  {
    "path": "tests/e2e/utils/testHelperUtils.ts",
    "content": "// testHelperUtils.ts\n// Utility functions used by all test specs\n\nimport { LabelText } from '@resources/LabelText'\nimport { TestID } from '@resources/TestID'\n\nimport { entryPoint } from './testHelperEnums'\n\nconst assertCurrentFolderOrCategory = (folderOrCategoryName: string) => {\n  cy.get('.active').should('have.text', folderOrCategoryName)\n}\n\nconst assertNoteContainsText = (testID: string, text: string) => {\n  cy.get(wrapWithTestIDTag(testID)).click().should('contain.text', text)\n}\n\n// takes a built string instead of a TestID .. prefer clickTestID() when possible\nconst clickDynamicTestID = (dynamicTestID: string) => {\n  cy.get(wrapWithTestIDTag(dynamicTestID)).click()\n}\n\n// optional second parameter to click at supported areas (e.g. 'right' 'left') default is 'center'\nconst clickTestID = (testIDEnum: TestID) => {\n  cy.get(wrapWithTestIDTag(testIDEnum)).click()\n}\n\nconst selectOptionTestID = (testIDEnum: TestID, text: string) => {\n  cy.get(wrapWithTestIDTag(testIDEnum)).select(text)\n}\n\nconst defaultInit = () => {\n  before(() => {\n    cy.visit(entryPoint)\n\n    // wait for things to settle .. like waiting for Welcome Note to resolve\n    // increasing due to occasional flaky starts\n    cy.wait(200)\n  })\n}\n\nconst getDynamicTestID = (testID: string) => {\n  return cy.get(wrapWithTestIDTag(testID))\n}\n\nconst getTestID = (testIDEnum: TestID) => {\n  return cy.get(wrapWithTestIDTag(testIDEnum))\n}\n\n// sets the specified alias for the current folder note count, must be accessed\n// through 'this' asynchronously (for example, .then())\n// note: test retrieving aliased variable must use regular 'function(){}' syntax for proper 'this' scope\nconst getNoteCount = (noteCountAlias: string) => {\n  getTestID(TestID.NOTE_LIST).children().its('length').as(noteCountAlias)\n}\n\nconst navigateToNotes = () => {\n  clickTestID(TestID.FOLDER_NOTES)\n}\n\nconst navigateToFavorites = () => {\n  clickTestID(TestID.FOLDER_FAVORITES)\n}\n\nconst navigateToTrash = () => {\n  clickTestID(TestID.FOLDER_TRASH)\n}\n\nconst testIDShouldContain = (testIDEnum: TestID, textEnum: LabelText) => {\n  cy.get(wrapWithTestIDTag(testIDEnum)).should('contain', textEnum)\n}\n\nconst testIDShouldExist = (testIDEnum: TestID) => {\n  cy.get(wrapWithTestIDTag(testIDEnum)).should('exist')\n}\n\nconst testIDShouldNotExist = (testIDEnum: TestID) => {\n  cy.get(wrapWithTestIDTag(testIDEnum)).should('not.exist')\n}\n\nconst wrapWithTestIDTag = (testIDEnum: TestID | string) => {\n  return '[data-testid=\"' + testIDEnum + '\"]'\n}\n\nexport {\n  clickDynamicTestID,\n  clickTestID,\n  getDynamicTestID,\n  getNoteCount,\n  getTestID,\n  defaultInit,\n  navigateToNotes,\n  navigateToFavorites,\n  navigateToTrash,\n  testIDShouldContain,\n  testIDShouldExist,\n  testIDShouldNotExist,\n  wrapWithTestIDTag,\n  assertCurrentFolderOrCategory,\n  selectOptionTestID,\n  assertNoteContainsText,\n}\n"
  },
  {
    "path": "tests/e2e/utils/testNotesHelperUtils.ts",
    "content": "// testNotesHelperUtils.ts\n// Utility functions for use in note tests\n\nimport { LabelText } from '@resources/LabelText'\nimport { TestID } from '@resources/TestID'\n\nimport {\n  clickDynamicTestID,\n  clickTestID,\n  getDynamicTestID,\n  getTestID,\n  testIDShouldExist,\n  navigateToTrash,\n} from './testHelperUtils'\n\nconst assertNewNoteCreated = () => {\n  getDynamicTestID(TestID.NOTE_LIST_ITEM + '0').should('contain', LabelText.NEW_NOTE)\n}\n\nconst assertNoteEditorCharacterCount = (expectedCharacterCount: number) => {\n  // all lines in the code editor should be descendants of the CodeMirror-code class\n  cy.get('.CodeMirror-code').each((element) => {\n    expect(element.text().length).to.equal(expectedCharacterCount)\n  })\n}\n\nconst assertNoteEditorLineCount = (expectedLineCount: number) => {\n  cy.get('.CodeMirror-code').children().should('have.length', expectedLineCount)\n}\n\nconst assertNoteListLengthEquals = (expectedLength: number) => {\n  getTestID(TestID.NOTE_LIST).children().should('have.length', expectedLength)\n}\n\nconst assertNoteListLengthGTE = (expectedLength: number) => {\n  getTestID(TestID.NOTE_LIST).children().should('have.length.gte', expectedLength)\n}\n\nconst assertNoteListTitleAtIndex = (noteIndex: number, expectedTitle: string) => {\n  getDynamicTestID(TestID.NOTE_TITLE + noteIndex)\n    .children()\n    .contains(expectedTitle)\n}\n\nconst assertNoteOptionsOpened = () => {\n  testIDShouldExist(TestID.NOTE_OPTIONS_NAV)\n}\n\nconst assertNotesSelected = (expectedSelectedNotesCount: number) => {\n  cy.get('.selected').should('have.length', expectedSelectedNotesCount)\n}\n\nconst trashAllNotes = () => {\n  getTestID(TestID.NOTE_LIST)\n    .children()\n    .each((el, noteIndex) => {\n      if (el.hasClass('selected')) return\n      cy.get('body').type(`{meta}`, { release: false })\n      getDynamicTestID(TestID.NOTE_LIST_ITEM + noteIndex).click()\n    })\n  openNoteContextMenu()\n  clickNoteOptionTrash()\n\n  navigateToTrash()\n  clickEmptyTrash()\n}\n\nconst createXUniqueNotes = (numberOfUniqueNotes: number) => {\n  for (let i = 0; i < numberOfUniqueNotes; i++) {\n    clickCreateNewNote()\n    typeNoteEditor(`note ${i}`)\n  }\n}\n\nconst clickCreateNewNote = () => {\n  clickTestID(TestID.SIDEBAR_ACTION_CREATE_NEW_NOTE)\n}\n\nconst clickEmptyTrash = () => {\n  clickTestID(TestID.EMPTY_TRASH_BUTTON)\n}\n\nconst clickNoteAtIndex = (noteIndex: number) => {\n  getDynamicTestID(TestID.NOTE_LIST_ITEM + noteIndex).click()\n}\n\nconst holdKeyAndClickNoteAtIndex = (\n  noteIndex: number,\n  key: 'alt' | 'ctrl' | 'meta' | 'shift' | null = null\n) => {\n  key && cy.get('body').type(`{${key}}`, { release: false })\n  getDynamicTestID(TestID.NOTE_LIST_ITEM + noteIndex).click()\n}\n\n// click a note with the specified index\nconst clickNoteOptions = (noteIndex: number = 0) => {\n  clickDynamicTestID(TestID.NOTE_OPTIONS_DIV + noteIndex)\n}\n\nconst openNoteContextMenu = (noteIndex: number = 0) => {\n  cy.get('.note-list > div').eq(noteIndex).rightclick()\n}\n\nconst clickNoteOptionDeleteNotePermanently = () => {\n  clickTestID(TestID.NOTE_OPTION_DELETE_PERMANENTLY)\n}\n\nconst clickNoteOptionFavorite = () => {\n  clickTestID(TestID.NOTE_OPTION_FAVORITE)\n}\n\nconst clickNoteOptionRestoreFromTrash = () => {\n  clickTestID(TestID.NOTE_OPTION_RESTORE_FROM_TRASH)\n}\n\nconst clickNoteOptionTrash = () => {\n  clickTestID(TestID.NOTE_OPTION_TRASH)\n}\n\nconst clickNoteOptionCopyLinkedNoteMarkdown = () => {\n  clickTestID(TestID.COPY_REFERENCE_TO_NOTE)\n}\n\nconst clickSyncNotes = () => {\n  clickTestID(TestID.TOPBAR_ACTION_SYNC_NOTES)\n}\n\nconst typeNoteEditor = (contentToType: string) => {\n  // force = true, cypress doesn't support typing in hidden elements\n  cy.get('.CodeMirror textarea').type(contentToType, { force: true })\n}\n\nconst typeNoteSearch = (contentToType: string) => {\n  getTestID(TestID.NOTE_SEARCH).type(contentToType, { force: true })\n}\n\nconst clearNoteSearch = () => {\n  getTestID(TestID.NOTE_SEARCH).clear()\n}\n\nconst dragAndDrop = (subject: string, element: string) => {\n  const dt = new DataTransfer()\n\n  cy.get(subject).trigger('dragstart', { dataTransfer: dt })\n  cy.get(element).trigger('drop', { dataTransfer: dt })\n}\n\nexport {\n  assertNewNoteCreated,\n  assertNoteEditorCharacterCount,\n  assertNoteEditorLineCount,\n  assertNoteListLengthEquals,\n  assertNoteListLengthGTE,\n  assertNoteListTitleAtIndex,\n  assertNoteOptionsOpened,\n  assertNotesSelected,\n  clickCreateNewNote,\n  createXUniqueNotes,\n  clickEmptyTrash,\n  clickNoteAtIndex,\n  clickNoteOptionDeleteNotePermanently,\n  clickNoteOptionFavorite,\n  clickNoteOptionRestoreFromTrash,\n  clickNoteOptionTrash,\n  clickNoteOptions,\n  clickNoteOptionCopyLinkedNoteMarkdown,\n  clickSyncNotes,\n  typeNoteEditor,\n  typeNoteSearch,\n  clearNoteSearch,\n  openNoteContextMenu,\n  holdKeyAndClickNoteAtIndex,\n  trashAllNotes,\n  dragAndDrop,\n}\n"
  },
  {
    "path": "tests/e2e/utils/testSettingsUtils.ts",
    "content": "// testHelperUtils.ts\n// Utility functions for use in settings tests\n\nimport { TestID } from '@resources/TestID'\n\nimport { clickTestID, selectOptionTestID } from './testHelperUtils'\n\nconst clickSettingsTab = (name: string) => {\n  cy.findByRole('button', { name: new RegExp(name, 'i') }).click()\n}\n\nconst navigateToSettings = () => {\n  cy.findByRole('button', { name: /settings/i }).click()\n}\n\nconst assertSettingsMenuIsOpen = () => {\n  cy.get('.settings-modal').should('exist')\n}\n\nconst assertSettingsMenuIsClosed = () => {\n  cy.get('.settings-modal').should('not.exist')\n}\n\nconst closeSettingsByClickingX = () => {\n  cy.get('.close-button').click()\n}\n\nconst closeSettingsByClickingOutsideWindow = () => {\n  cy.get('.dimmer').click('topLeft')\n}\n\nconst toggleDarkMode = () => {\n  clickTestID(TestID.DARK_MODE_TOGGLE)\n}\n\nconst toggleMarkdownPreview = () => {\n  clickTestID(TestID.MARKDOWN_PREVIEW_TOGGLE)\n}\n\nconst toggleLineNumbers = () => {\n  clickTestID(TestID.DISPLAY_LINE_NUMS_TOGGLE)\n}\n\nconst toggleLineHighlight = () => {\n  clickTestID(TestID.ACTIVE_LINE_HIGHLIGHT_TOGGLE)\n}\n\nconst selectOptionInSortByDropdown = (text: string) => {\n  selectOptionTestID(TestID.SORT_BY_DROPDOWN, text)\n}\n\nconst assertDarkModeActive = () => {\n  cy.get('.settings-modal').should('have.css', 'background-color', 'rgb(51, 51, 51)')\n}\n\nconst assertDarkModeInactive = () => {\n  cy.get('.settings-modal').should('have.css', 'background-color', 'rgb(255, 255, 255)')\n}\n\nconst assertMarkdownPreviewActive = () => {\n  cy.get('h1').should('exist')\n}\n\nconst assertMarkdownPreviewInactive = () => {\n  cy.get('h1').should('not.exist')\n}\n\nconst assertLineNumbersActive = () => {\n  cy.get('.CodeMirror-activeline > .CodeMirror-gutter-wrapper > .CodeMirror-linenumber').should(\n    'exist'\n  )\n}\n\nconst assertLineNumbersInactive = () => {\n  cy.get('.CodeMirror-activeline > .CodeMirror-gutter-wrapper > .CodeMirror-linenumber').should(\n    'not.exist'\n  )\n}\n\nconst assertLineHighlightActive = () => {\n  cy.get('div.CodeMirror-activeline').should('exist')\n}\n\nconst assertLineHighlightInactive = () => {\n  cy.get('div.CodeMirror-activeline').should('not.exist')\n}\n\nconst getDownloadedBackup = () => {\n  return cy.get('a[download]:last-child').then(\n    (anchor) =>\n      new Cypress.Promise((resolve) => {\n        // Use XHR to get the blob that corresponds to the object URL.\n        const xhr = new XMLHttpRequest()\n        xhr.open('GET', anchor.prop('href'), true)\n        xhr.responseType = 'blob'\n\n        // Once loaded, use FileReader to get the string back from the blob.\n        xhr.onload = () => {\n          if (xhr.status === 200) {\n            const blob = xhr.response\n            const reader = new FileReader()\n            reader.onload = () => {\n              // Once we have a string, resolve the promise to let\n              // the Cypress chain continue, e.g. to assert on the result.\n              resolve(reader.result)\n            }\n            reader.readAsText(blob)\n          }\n        }\n        xhr.send()\n      })\n  )\n}\n\nexport {\n  navigateToSettings,\n  assertSettingsMenuIsOpen,\n  assertSettingsMenuIsClosed,\n  closeSettingsByClickingX,\n  closeSettingsByClickingOutsideWindow,\n  toggleDarkMode,\n  toggleMarkdownPreview,\n  toggleLineNumbers,\n  toggleLineHighlight,\n  assertDarkModeActive,\n  assertDarkModeInactive,\n  assertMarkdownPreviewActive,\n  assertMarkdownPreviewInactive,\n  assertLineNumbersActive,\n  assertLineNumbersInactive,\n  selectOptionInSortByDropdown,\n  assertLineHighlightActive,\n  assertLineHighlightInactive,\n  clickSettingsTab,\n  getDownloadedBackup,\n}\n"
  },
  {
    "path": "tests/unit/client/components/AppSidebar/ActionButton.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\nimport { Camera } from 'react-feather'\n\nimport { TestID } from '@resources/TestID'\nimport { ActionButton, ActionButtonProps } from '@/components/AppSidebar/ActionButton'\n\ntest('Sample test', () => {\n  expect(true).toBeTruthy()\n})\n\ndescribe('<ActionButton />', () => {\n  it('renders the ActionButton component', () => {\n    const enabledProps: ActionButtonProps = {\n      handler: jest.fn,\n      label: 'Test',\n      dataTestID: TestID.ACTION_BUTTON,\n      text: 'text',\n      icon: Camera,\n    }\n\n    const component = render(<ActionButton {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/AppSidebar/AddCategoryButton.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\n\nimport { TestID } from '@resources/TestID'\nimport {\n  AddCategoryButton,\n  AddCategoryButtonProps,\n} from '@/components/AppSidebar/AddCategoryButton'\n\ndescribe('<AddCategoryButton />', () => {\n  it('renders the AddCategoryButton component', () => {\n    const enabledProps: AddCategoryButtonProps = {\n      handler: jest.fn,\n      label: 'Test',\n      dataTestID: TestID.ADD_CATEGORY_BUTTON,\n    }\n\n    const component = render(<AddCategoryButton {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/AppSidebar/AddCategoryForm.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\n\nimport { TestID } from '@resources/TestID'\nimport { AddCategoryForm, AddCategoryFormProps } from '@/components/AppSidebar/AddCategoryForm'\n\ndescribe('<AddCategoryForm />', () => {\n  it('renders the AddCategoryForm component', () => {\n    const enabledProps: AddCategoryFormProps = {\n      submitHandler: jest.fn,\n      changeHandler: jest.fn,\n      resetHandler: jest.fn,\n      editingCategoryId: 'Category-id',\n      tempCategoryName: 'Category-id',\n      dataTestID: TestID.NEW_CATEGORY_INPUT,\n    }\n\n    const component = render(<AddCategoryForm {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/AppSidebar/CollapseCategoryButton.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\n\nimport { TestID } from '@resources/TestID'\nimport { CollapseCategoryListButton } from '@/components/AppSidebar/CollapseCategoryButton'\n\ndescribe('<CollapseCategoryButton />', () => {\n  it('renders the CollapseCategoryButton component', () => {\n    const enabledProps: CollapseCategoryListButton = {\n      handler: jest.fn,\n      label: 'Test',\n      dataTestID: TestID.CATEGORY_COLLAPSE_BUTTON,\n      isCategoryListOpen: true,\n      showIcon: true,\n    }\n\n    const component = render(<CollapseCategoryListButton {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/AppSidebar/FolderOption.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\n\nimport { TestID } from '@resources/TestID'\nimport { FolderOption, FolderOptionProps } from '@/components/AppSidebar/FolderOption'\nimport { Folder } from '@/utils/enums'\n\ndescribe('<FolderOption />', () => {\n  it('renders the FolderOption component', () => {\n    const enabledProps: FolderOptionProps = {\n      swapFolder: jest.fn,\n      addNoteType: jest.fn,\n      text: 'Test',\n      dataTestID: TestID.FOLDER_NOTES,\n      active: true,\n      folder: Folder.CATEGORY,\n    }\n\n    const component = render(<FolderOption {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/AppSidebar/ScratchpadOption.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\n\nimport { ScratchpadOption, ScratchpadOptionProps } from '@/components/AppSidebar/ScratchpadOption'\n\ndescribe('<ScratchpadOption />', () => {\n  it('renders the ScratchpadOption component', () => {\n    const enabledProps: ScratchpadOptionProps = {\n      swapFolder: jest.fn,\n      active: true,\n    }\n\n    const component = render(<ScratchpadOption {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/LastSyncedNotification.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport dayjs from 'dayjs'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\n\nimport { TestID } from '@resources/TestID'\nimport {\n  LastSyncedNotification,\n  LastSyncedNotificationProps,\n} from '@/components/LastSyncedNotification'\n\ndescribe('<LastSyncedNotification />', () => {\n  it('renders the Tab component', () => {\n    const enabledProps: LastSyncedNotificationProps = {\n      datetime: '',\n      pending: false,\n      syncing: true,\n    }\n\n    const component = render(<LastSyncedNotification {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n  it('Should display syncing ', () => {\n    const enabledProps: LastSyncedNotificationProps = {\n      datetime: '',\n      pending: false,\n      syncing: true,\n    }\n\n    const { getByTestId } = render(<LastSyncedNotification {...enabledProps} />)\n\n    expect(getByTestId(TestID.LAST_SYNCED_NOTIFICATION_SYNCING).innerHTML).toBe('Syncing...')\n  })\n\n  it('Should display Unsaved change ', () => {\n    const enabledProps: LastSyncedNotificationProps = {\n      datetime: '',\n      pending: true,\n      syncing: false,\n    }\n\n    const { getByTestId } = render(<LastSyncedNotification {...enabledProps} />)\n\n    expect(getByTestId(TestID.LAST_SYNCED_NOTIFICATION_UNSAVED).innerHTML).toBe('Unsaved changes')\n  })\n\n  it('Should display date ', () => {\n    const enabledProps: LastSyncedNotificationProps = {\n      datetime: Date(),\n      pending: false,\n      syncing: false,\n    }\n\n    const { getByTestId } = render(<LastSyncedNotification {...enabledProps} />)\n    expect(getByTestId(TestID.LAST_SYNCED_NOTIFICATION_DATE).innerHTML).toBe(\n      dayjs(Date()).format('LT on L')\n    )\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/NoteList/NoteListButton.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\n\nimport { TestID } from '@resources/TestID'\nimport { NoteListButton, NoteListButtonProps } from '@/components/NoteList/NoteListButton'\n\ndescribe('<NoteListButton />', () => {\n  it('renders the NoteListButton component', () => {\n    const enabledProps: NoteListButtonProps = {\n      handler: jest.fn,\n      label: 'Test',\n      dataTestID: TestID.EMPTY_TRASH_BUTTON,\n    }\n\n    const component = render(<NoteListButton {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n\n  it('renders the NoteListButton component as disabled', () => {\n    const disabledProps: NoteListButtonProps = {\n      handler: jest.fn,\n      label: 'Test',\n      disabled: true,\n      dataTestID: TestID.EMPTY_TRASH_BUTTON,\n    }\n\n    const component = render(<NoteListButton {...disabledProps} />)\n    const button = component.queryByTestId(TestID.EMPTY_TRASH_BUTTON)\n\n    expect(button).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/NoteList/SearchBar.test.tsx",
    "content": "import React, { createRef } from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\n\nimport { TestID } from '@resources/TestID'\nimport { SearchBar, SearchBarProps } from '@/components/NoteList/SearchBar'\n\ndescribe('<SearchBar />', () => {\n  it('renders the SearchBar component', () => {\n    const enabledProps: SearchBarProps = {\n      searchRef: createRef() as React.MutableRefObject<HTMLInputElement>,\n      searchNotes: jest.fn,\n    }\n\n    const component = render(<SearchBar {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n\n  it('renders the SearchBar and searches for text', () => {\n    const enabledProps: SearchBarProps = {\n      searchRef: createRef() as React.MutableRefObject<HTMLInputElement>,\n      searchNotes: jest.fn,\n    }\n\n    const component = render(<SearchBar {...enabledProps} />)\n    expect(component).toBeTruthy()\n\n    const { getByTestId } = component\n\n    fireEvent.change(getByTestId(TestID.NOTE_SEARCH), {\n      target: { value: 'welcome' },\n    })\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/SettingsModal/IconButton.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\nimport { Camera } from 'react-feather'\n\nimport { TestID } from '@resources/TestID'\nimport { IconButton, IconButtonProps } from '@/components/SettingsModal/IconButton'\n\ndescribe('<IconButton />', () => {\n  it('renders the IconButton component', () => {\n    const enabledProps: IconButtonProps = {\n      handler: jest.fn,\n      dataTestID: TestID.ICON_BUTTON,\n      icon: Camera,\n      text: 'takeNote',\n    }\n\n    const component = render(<IconButton {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n\n  it('renders the IconButton component as disabled', () => {\n    const disabledProps: IconButtonProps = {\n      handler: jest.fn,\n      dataTestID: TestID.ICON_BUTTON,\n      disabled: true,\n      icon: Camera,\n      text: 'takeNote',\n    }\n\n    const component = render(<IconButton {...disabledProps} />)\n    const button = component.queryByTestId(TestID.ICON_BUTTON)\n\n    expect(button).toBeDisabled()\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/SettingsModal/IconButtonUploader.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\nimport { Camera } from 'react-feather'\n\nimport { TestID } from '@resources/TestID'\nimport {\n  IconButtonUploader,\n  IconButtonUploaderProps,\n} from '@/components/SettingsModal/IconButtonUploader'\n\ndescribe('<IconButtonUploader />', () => {\n  it('renders the IconButtonUploader component', () => {\n    const enabledProps: IconButtonUploaderProps = {\n      handler: jest.fn,\n      dataTestID: TestID.ICON_BUTTON_UPLOADER,\n      icon: Camera,\n      text: 'takeNote',\n      accept: 'takeNote',\n    }\n\n    const component = render(<IconButtonUploader {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/Switch.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\n\nimport { Switch, SwitchProps } from '@/components/Switch'\n\ndescribe('<Switch />', () => {\n  it('renders the Switch component', () => {\n    const enabledProps: SwitchProps = {\n      toggle: jest.fn(),\n      checked: false,\n      testId: 'fake-test-id-for-testing',\n    }\n\n    const component = render(<Switch {...enabledProps} />)\n\n    expect(component).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/editor/EditorEmpty.test.tsx",
    "content": "import React from 'react'\nimport { render, screen } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\n\nimport { TestID } from '@resources/TestID'\nimport { EmptyEditor } from '@/components/Editor/EmptyEditor'\n\ndescribe('<EmptyEditor />', () => {\n  it('renders the EmptyEditor component', () => {\n    const component = render(<EmptyEditor />)\n\n    expect(component).toBeTruthy()\n  })\n\n  it('renders the EmptyEditor component and its texts', () => {\n    const component = render(<EmptyEditor />)\n\n    const createNoteText = component.queryByTestId(TestID.EMPTY_EDITOR)\n\n    expect(createNoteText).toBeValid()\n    expect(component.getByText('Create a note')).toBeInstanceOf(Node)\n    expect(component.getByText('CTRL')).toBeInstanceOf(Node)\n    expect(component.getByText('ALT')).toBeInstanceOf(Node)\n    expect(component.getByText('N')).toBeInstanceOf(Node)\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/components/editor/PreviewEditor.test.tsx",
    "content": "import React from 'react'\nimport { render } from '@testing-library/react'\n\nimport '@testing-library/jest-dom'\nimport 'jest-extended'\nimport { PreviewEditor, PreviewEditorProps } from '@/components/Editor/PreviewEditor'\nimport NoteLink, { NoteLinkProps } from '@/components/Editor/NoteLink'\nimport { NoteItem } from '@/types'\nimport { Errors } from '@/utils/enums'\nimport { TestID } from '@resources/TestID'\nimport { TempStateProvider } from '@/contexts/TempStateContext'\n\nimport { renderWithRouter } from '../../testHelpers'\n\nconst wrap = (props: PreviewEditorProps) => renderWithRouter(<PreviewEditor {...props} />)\n\ndescribe('<PreviewEditor />', () => {\n  it('renders the PreviewEditor component', () => {\n    const props: PreviewEditorProps = {\n      noteText: 'texts for testing',\n      directionText: 'testing',\n      notes: [],\n    }\n    const component = wrap(props)\n\n    expect(component).toBeTruthy()\n  })\n\n  it('test', () => {\n    const noteItemProps: NoteItem = {\n      id: 'test-note',\n      text: 'Test note',\n      created: Date(),\n      lastUpdated: Date(),\n    }\n\n    const props: NoteLinkProps = {\n      uuid: '{{test-note}}',\n      notes: [noteItemProps],\n      handleNoteLinkClick: jest.fn,\n    }\n\n    const component = render(\n      <TempStateProvider>\n        <NoteLink {...props} />\n      </TempStateProvider>\n    )\n    expect(component).toBeTruthy()\n\n    const { getByTestId } = component\n\n    expect(getByTestId(TestID.NOTE_LINK_SUCCESS).innerHTML).toMatch(noteItemProps.text)\n  })\n\n  it('test2', () => {\n    const noteItemProps: NoteItem = {\n      id: '2',\n      text: 'Test note',\n      created: Date(),\n      lastUpdated: Date(),\n    }\n\n    const props: NoteLinkProps = {\n      uuid: 'test-note',\n      notes: [noteItemProps],\n      handleNoteLinkClick: jest.fn,\n    }\n\n    const component = render(\n      <TempStateProvider>\n        <NoteLink {...props} />\n      </TempStateProvider>\n    )\n    expect(component).toBeTruthy()\n\n    const { getByTestId } = component\n\n    expect(getByTestId(TestID.NOTE_LINK_ERROR).innerHTML).toMatch(\n      '&lt;invalid note id provided&gt;'\n    )\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/containers/ContextMenuOptions.test.tsx",
    "content": "import React from 'react'\n\nimport { TestID } from '@resources/TestID'\nimport { ContextMenuOptions, ContextMenuOptionsProps } from '@/containers/ContextMenuOptions'\nimport { ContextMenuEnum } from '@/utils/enums'\n\nimport { renderWithRouter } from '../testHelpers'\n\nconst wrap = (props: ContextMenuOptionsProps) => renderWithRouter(<ContextMenuOptions {...props} />)\n\ndescribe('<ContextMenuOptions />', () => {\n  it('renders the ContextMenuOptions', () => {\n    const props: ContextMenuOptionsProps = {\n      clickedItem: {\n        id: '1',\n        text: 'text',\n        created: '01/02/2019',\n        lastUpdated: '01/02/2019',\n      },\n      type: ContextMenuEnum.NOTE,\n    }\n\n    const component = wrap(props)\n    const nav = component.getByTestId('note-options-nav')\n\n    expect(nav).toBeTruthy()\n  })\n\n  it('displays correct default options', () => {\n    const props: ContextMenuOptionsProps = {\n      clickedItem: {\n        id: '1',\n        text: 'text',\n        created: '01/02/2019',\n        lastUpdated: '01/02/2019',\n      },\n      type: ContextMenuEnum.NOTE,\n    }\n\n    const component = wrap(props)\n    const addToFavorites = component.queryByTestId(TestID.NOTE_OPTION_FAVORITE)\n    const removeCategory = component.queryByTestId(TestID.NOTE_OPTION_REMOVE_CATEGORY)\n    const download = component.queryByTestId(TestID.NOTE_OPTION_DOWNLOAD)\n    const deletePermanently = component.queryByTestId(TestID.NOTE_OPTION_DELETE_PERMANENTLY)\n    const restoreFromTrash = component.queryByTestId(TestID.NOTE_OPTION_RESTORE_FROM_TRASH)\n\n    expect(addToFavorites).toBeTruthy()\n    expect(download).toBeTruthy()\n    expect(removeCategory).toBeFalsy()\n    expect(deletePermanently).toBeFalsy()\n    expect(restoreFromTrash).toBeFalsy()\n  })\n\n  it('displays correct trash options', () => {\n    const props: ContextMenuOptionsProps = {\n      clickedItem: {\n        id: '1',\n        text: 'text',\n        created: '01/02/2019',\n        lastUpdated: '01/02/2019',\n        trash: true,\n      },\n      type: ContextMenuEnum.NOTE,\n    }\n\n    const component = wrap(props)\n    const addToFavorites = component.queryByTestId(TestID.NOTE_OPTION_FAVORITE)\n    const removeCategory = component.queryByTestId(TestID.NOTE_OPTION_REMOVE_CATEGORY)\n    const download = component.queryByTestId(TestID.NOTE_OPTION_DOWNLOAD)\n    const deletePermanently = component.queryByTestId(TestID.NOTE_OPTION_DELETE_PERMANENTLY)\n    const restoreFromTrash = component.queryByTestId(TestID.NOTE_OPTION_RESTORE_FROM_TRASH)\n\n    expect(addToFavorites).toBeFalsy()\n    expect(deletePermanently).toBeTruthy()\n    expect(restoreFromTrash).toBeTruthy()\n    expect(removeCategory).toBeFalsy()\n    expect(download).toBeTruthy()\n  })\n\n  it('displays correct category options', () => {\n    const props: ContextMenuOptionsProps = {\n      clickedItem: {\n        id: '1',\n        text: 'text',\n        created: '01/02/2019',\n        lastUpdated: '01/02/2019',\n        category: '2',\n      },\n      type: ContextMenuEnum.NOTE,\n    }\n\n    const component = wrap(props)\n    const addToFavorites = component.queryByTestId(TestID.NOTE_OPTION_FAVORITE)\n    const removeCategory = component.queryByTestId(TestID.NOTE_OPTION_REMOVE_CATEGORY)\n    const download = component.queryByTestId(TestID.NOTE_OPTION_DOWNLOAD)\n    const deletePermanently = component.queryByTestId(TestID.NOTE_OPTION_DELETE_PERMANENTLY)\n    const restoreFromTrash = component.queryByTestId(TestID.NOTE_OPTION_RESTORE_FROM_TRASH)\n\n    expect(addToFavorites).toBeTruthy()\n    expect(deletePermanently).toBeFalsy()\n    expect(restoreFromTrash).toBeFalsy()\n    expect(removeCategory).toBeTruthy()\n    expect(download).toBeTruthy()\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/containers/TakeNoteApp.test.tsx",
    "content": "import React from 'react'\nimport { mocked } from 'ts-jest/utils'\nimport { waitFor } from '@testing-library/react'\nimport { name, internet, lorem } from 'faker'\n\nimport { getAuth, getCategories, getSettings, getNotes, getSync } from '@/selectors'\nimport { Folder, NotesSortKey } from '@/utils/enums'\nimport { TakeNoteApp } from '@/containers/TakeNoteApp'\n\nimport { renderWithRouter } from '../testHelpers'\n\njest.mock('@/selectors')\n\nconst mockedGetNotes = mocked(getNotes, true)\nconst mockedGetSettings = mocked(getSettings, true)\nconst mockedGetCategories = mocked(getCategories, true)\nconst mockedGetSync = mocked(getSync, true)\nconst mockedGetAuth = mocked(getAuth, true)\n\nconst wrap = () => renderWithRouter(<TakeNoteApp />)\n\ndescribe('<TakeNoteApp />', () => {\n  test('should see empty editor if there are no active notes', async () => {\n    mockedGetNotes.mockImplementation(() => {\n      return {\n        activeCategoryId: '',\n        activeFolder: Folder.ALL,\n        activeNoteId: '',\n        selectedNotesIds: [],\n        error: '',\n        loading: false,\n        notes: [],\n        searchValue: '',\n      }\n    })\n    mockedGetSettings.mockImplementation(() => {\n      return {\n        isOpen: false,\n        loading: false,\n        previewMarkdown: false,\n        darkTheme: false,\n        sidebarVisible: true,\n        notesSortKey: NotesSortKey.LAST_UPDATED,\n        codeMirrorOptions: {\n          mode: 'gfm',\n          theme: 'base16-light',\n          lineNumbers: false,\n          lineWrapping: true,\n          styleActiveLine: { nonEmpty: true },\n          viewportMargin: Infinity,\n          keyMap: 'default',\n          dragDrop: false,\n          scrollPastEnd: true,\n        },\n      }\n    })\n    mockedGetCategories.mockImplementation(() => {\n      return {\n        categories: [],\n        error: '',\n        loading: false,\n        editingCategory: {\n          id: '',\n          tempName: '',\n        },\n      }\n    })\n    mockedGetSync.mockImplementation(() => {\n      return {\n        error: '',\n        syncing: false,\n        lastSynced: '',\n        pendingSync: false,\n      }\n    })\n    mockedGetAuth.mockImplementation(() => {\n      return {\n        loading: false,\n        currentUser: {\n          bio: lorem.words(),\n          name: name.findName(),\n          avatar_url: internet.url(),\n        },\n        isAuthenticated: true,\n        error: '',\n      }\n    })\n\n    const component = wrap()\n\n    await waitFor(() => component.getByTestId('empty-editor'))\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/slices/auth.test.ts",
    "content": "import { PayloadAction } from '@reduxjs/toolkit'\n\nimport reducer, {\n  initialState,\n  login,\n  loginError,\n  loginSuccess,\n  logout,\n  logoutSuccess,\n} from '@/slices/auth'\n\ndescribe('authSlice', () => {\n  it('should return the initial state on first run', () => {\n    const nextState = initialState\n    const action = {} as PayloadAction\n    const result = reducer(undefined, action)\n\n    expect(result).toEqual(nextState)\n  })\n\n  it('should set the loading true on login', () => {\n    const nextState = { ...initialState, loading: true }\n\n    const result = reducer(initialState, login())\n\n    expect(result).toEqual(nextState)\n  })\n\n  it('should set the currentUser to payload, isAuthenticated to true and loading to false on loginSuccess', () => {\n    const payload = { currentUserName: 'test' }\n    const nextState = {\n      ...initialState,\n      loading: false,\n      isAuthenticated: true,\n      currentUser: payload,\n    }\n\n    const result = reducer(initialState, loginSuccess(payload))\n\n    expect(result).toEqual(nextState)\n  })\n\n  it('should set the error to payload, isAuthenticated to false and loading to false on loginError', () => {\n    const payload = 'error text'\n    const nextState = {\n      ...initialState,\n      loading: false,\n      isAuthenticated: false,\n      error: payload,\n    }\n\n    const result = reducer(initialState, loginError(payload))\n\n    expect(result).toEqual(nextState)\n  })\n\n  it('should set the loading true on logout', () => {\n    const nextState = { ...initialState, loading: true }\n\n    const result = reducer(initialState, logout())\n\n    expect(result).toEqual(nextState)\n  })\n\n  it('should set isAuthenticated to false, currentUser to empty object and loading to false on logoutSuccess', () => {\n    const initialStateBeforeLogout = {\n      isAuthenticated: true,\n      loading: false,\n      currentUser: { name: 'test' },\n      error: '',\n    }\n\n    const nextState = {\n      ...initialState,\n      loading: false,\n    }\n\n    const result = reducer(initialStateBeforeLogout, logoutSuccess())\n\n    expect(result).toEqual(nextState)\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/slices/category.test.ts",
    "content": "import { PayloadAction } from '@reduxjs/toolkit'\n\nimport reducer, {\n  initialState,\n  addCategory,\n  updateCategory,\n  deleteCategory,\n  categoryDragEnter,\n  categoryDragLeave,\n  setCategoryEdit,\n  loadCategories,\n  loadCategoriesError,\n  loadCategoriesSuccess,\n  swapCategories,\n} from '@/slices/category'\n\ndescribe('categorySlice', () => {\n  test('should return initial state on first run', () => {\n    const nextState = initialState\n    const action = {} as PayloadAction\n    const result = reducer(undefined, action)\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should add passed payload in the existing category list on addCategory', () => {\n    const payload = { id: '123', name: 'note 0', draggedOver: false }\n    const nextState = { ...initialState, categories: [payload] }\n\n    const result = reducer(initialState, addCategory(payload))\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should update the category name on updateCategory', () => {\n    const payload = { id: '123', name: 'note 0 renamed', draggedOver: false }\n    const nextState = { ...initialState, categories: [payload] }\n    const initialStateBeforeUpdateCategory = {\n      ...initialState,\n      categories: [\n        {\n          id: '123',\n          name: 'note 0',\n          draggedOver: false,\n        },\n      ],\n    }\n    const result = reducer(initialStateBeforeUpdateCategory, updateCategory(payload))\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should delete the category name on deleteCategory', () => {\n    const initialStateBeforeDeleteCategory = {\n      ...initialState,\n      categories: [\n        {\n          id: '123',\n          name: 'note 0',\n          draggedOver: false,\n        },\n        {\n          id: '456',\n          name: 'note 1',\n          draggedOver: false,\n        },\n      ],\n    }\n\n    const nextState = {\n      ...initialState,\n      categories: [\n        {\n          id: '123',\n          name: 'note 0',\n          draggedOver: false,\n        },\n      ],\n    }\n    const result = reducer(initialStateBeforeDeleteCategory, deleteCategory('456'))\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set draggedOver to true on categoryDragEnter', () => {\n    const payload = { id: '123', name: 'note 0 renamed', draggedOver: false }\n    const initialStateBeforeCategoryDragEnter = {\n      ...initialState,\n      categories: [\n        {\n          id: '123',\n          name: 'note 0',\n          draggedOver: false,\n        },\n      ],\n    }\n    const nextState = {\n      ...initialState,\n      categories: [\n        {\n          id: '123',\n          name: 'note 0',\n          draggedOver: true,\n        },\n      ],\n    }\n\n    const result = reducer(initialStateBeforeCategoryDragEnter, categoryDragEnter(payload))\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set draggedOver to false on categoryDragLeave', () => {\n    const payload = { id: '123', name: 'note 0 renamed', draggedOver: false }\n    const initialStateBeforeCategoryDragLeave = {\n      ...initialState,\n      categories: [\n        {\n          id: '123',\n          name: 'note 0',\n          draggedOver: true,\n        },\n      ],\n    }\n    const nextState = {\n      ...initialState,\n      categories: [\n        {\n          id: '123',\n          name: 'note 0',\n          draggedOver: false,\n        },\n      ],\n    }\n\n    const result = reducer(initialStateBeforeCategoryDragLeave, categoryDragLeave(payload))\n    expect(result).toEqual(nextState)\n  })\n\n  test('should swap categories', () => {\n    const payload = {\n      categoryId: 0,\n      destinationId: 2,\n    }\n    const initialStateBeforeSwapCategories = {\n      ...initialState,\n      categories: [\n        {\n          id: '1',\n          name: 'note 0',\n          draggedOver: false,\n        },\n        {\n          id: '2',\n          name: 'note 1',\n          draggedOver: false,\n        },\n        {\n          id: '3',\n          name: 'note 2',\n          draggedOver: false,\n        },\n      ],\n    }\n\n    const nextState = {\n      ...initialState,\n      categories: [\n        {\n          id: '2',\n          name: 'note 1',\n          draggedOver: false,\n        },\n        {\n          id: '3',\n          name: 'note 2',\n          draggedOver: false,\n        },\n        {\n          id: '1',\n          name: 'note 0',\n          draggedOver: false,\n        },\n      ],\n    }\n    const result = reducer(initialStateBeforeSwapCategories, swapCategories(payload))\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set editing category to payload on setCategoryEdit', () => {\n    const payload = {\n      id: '123',\n      tempName: 'tempName',\n    }\n    const nextState = {\n      ...initialState,\n      editingCategory: payload,\n    }\n    const result = reducer(initialState, setCategoryEdit(payload))\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set loading true on loadCategories', () => {\n    const nextState = {\n      ...initialState,\n      loading: true,\n    }\n    const result = reducer(initialState, loadCategories())\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set loading false and error to payload on loadCategoriesError', () => {\n    const payload = 'test error'\n    const nextState = {\n      ...initialState,\n      loading: false,\n      error: payload,\n    }\n    const result = reducer(initialState, loadCategoriesError(payload))\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set loading false and categories to payload on loadCategoriesSuccess', () => {\n    const payload = [{ id: '123', name: 'note 0', draggedOver: false }]\n    const nextState = {\n      ...initialState,\n      loading: false,\n      categories: payload,\n    }\n    const result = reducer(initialState, loadCategoriesSuccess(payload))\n    expect(result).toEqual(nextState)\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/slices/note.test.ts",
    "content": "import { PayloadAction } from '@reduxjs/toolkit'\nimport dayjs from 'dayjs'\n\nimport reducer, {\n  addNote,\n  initialState,\n  updateNote,\n  deleteNotes,\n  addCategoryToNote,\n  removeCategoryFromNotes,\n  updateActiveNote,\n  updateActiveCategoryId,\n  swapFolder,\n  assignFavoriteToNotes,\n  toggleFavoriteNotes,\n  assignTrashToNotes,\n  toggleTrashNotes,\n  unassignTrashFromNotes,\n  updateSelectedNotes,\n  permanentlyEmptyTrash,\n  pruneNotes,\n  searchNotes,\n  loadNotes,\n  loadNotesError,\n  loadNotesSuccess,\n} from '@/slices/note'\nimport { Folder } from '@/utils/enums'\n\nfunction createNote({\n  id,\n  category,\n  text,\n  favorite,\n  scratchpad,\n  trash,\n}: {\n  id: string\n  category?: string\n  text?: string\n  favorite?: boolean\n  scratchpad?: boolean\n  trash?: boolean\n}) {\n  return {\n    id,\n    text: text ?? `sample note - ${id}`,\n    created: dayjs().format(),\n    lastUpdated: dayjs().format(),\n    category,\n    favorite,\n    scratchpad,\n    trash,\n  }\n}\n\ndescribe('noteSlice', () => {\n  test('should return initial state on first run', () => {\n    const nextState = initialState\n    const action = {} as PayloadAction\n    const result = reducer(undefined, action)\n\n    expect(result).toEqual(nextState)\n  })\n\n  describe('addNote', () => {\n    test('should add note if there are no notes in draft state on addNote', () => {\n      const payload = createNote({ id: '1', category: '1' })\n      const nextState = { ...initialState, notes: [payload] }\n      const result = reducer(initialState, addNote(payload))\n\n      expect(result).toEqual(nextState)\n    })\n\n    test('should not add note if there is any note in draft state on addNote', () => {\n      const payload = createNote({ id: '1', category: '1' })\n      const initialStateBeforeAddNote = {\n        ...initialState,\n        notes: [\n          createNote({ id: '1', category: '1' }),\n          createNote({ id: '1', category: '1', text: '' }),\n        ],\n      }\n      const nextState = { ...initialStateBeforeAddNote }\n      const result = reducer(initialStateBeforeAddNote, addNote(payload))\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  test('should update note content and lastUpdated on updateNote', () => {\n    const payload = createNote({ id: '1', category: '1' })\n    const initialStateBeforeUpdateNote = {\n      ...initialState,\n      notes: [createNote({ id: '1', category: '1' })],\n    }\n    const nextState = { ...initialStateBeforeUpdateNote, notes: [payload] }\n    const result = reducer(initialStateBeforeUpdateNote, updateNote(payload))\n\n    expect(result).toEqual(nextState)\n  })\n\n  describe('deleteNotes', () => {\n    test('should deleteNotes from notes list and set activeNoteId and selectedNotesIds on deleteNotes', () => {\n      const payload = ['1', '4']\n      const notes = [\n        createNote({ id: '1', category: '1' }),\n        createNote({ id: '2', category: '1' }),\n        createNote({ id: '3' }),\n        createNote({ id: '4', category: '1' }),\n      ]\n      const initialStateBeforeDeleteNotes = {\n        ...initialState,\n        notes: notes,\n        activeCategoryId: '1',\n      }\n      const nextState = {\n        ...initialStateBeforeDeleteNotes,\n        notes: [notes[1], notes[2]],\n        activeNoteId: '',\n        selectedNotesIds: [''],\n      }\n      const result = reducer(initialStateBeforeDeleteNotes, deleteNotes(payload))\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('addCategory', () => {\n    test('should add Category to the existing single note', () => {\n      const payload = {\n        categoryId: '3',\n        noteId: '2',\n      }\n      const note = createNote({ id: '2' })\n      const initialStateBeforeAddingCategoryToNote = {\n        ...initialState,\n        notes: [note],\n      }\n\n      const nextState = {\n        ...initialStateBeforeAddingCategoryToNote,\n        notes: [\n          {\n            ...note,\n            category: '3',\n          },\n        ],\n      }\n      const result = reducer(initialStateBeforeAddingCategoryToNote, addCategoryToNote(payload))\n\n      expect(result).toEqual(nextState)\n    })\n\n    test('should add Category to the requested note and selected notes', () => {\n      const payload = {\n        categoryId: '3',\n        noteId: '2',\n      }\n      const notes = [createNote({ id: '1' }), createNote({ id: '2' }), createNote({ id: '3' })]\n      const initialStateBeforeAddingCategoryToNote = {\n        ...initialState,\n        notes,\n        selectedNotesIds: ['1', '2'],\n      }\n\n      const nextState = {\n        ...initialStateBeforeAddingCategoryToNote,\n        notes: [\n          {\n            ...notes[0],\n            category: '3',\n          },\n          {\n            ...notes[1],\n            category: '3',\n          },\n          notes[2],\n        ],\n      }\n      const result = reducer(initialStateBeforeAddingCategoryToNote, addCategoryToNote(payload))\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('removeCategory', () => {\n    test('should remove Category from the notes', () => {\n      const payload = '1'\n      const notes = [\n        createNote({ id: '1', category: '1' }),\n        createNote({ id: '2', category: '1' }),\n        createNote({ id: '3', category: '2' }),\n        createNote({ id: '4', category: '3' }),\n      ]\n      const initialStateBeforeRemovingCategoryFromNotes = {\n        ...initialState,\n        notes: notes,\n      }\n      const nextState = {\n        ...initialStateBeforeRemovingCategoryFromNotes,\n        notes: [\n          {\n            ...notes[0],\n            category: '',\n          },\n          {\n            ...notes[1],\n            category: '',\n          },\n          notes[2],\n          notes[3],\n        ],\n      }\n      const result = reducer(\n        initialStateBeforeRemovingCategoryFromNotes,\n        removeCategoryFromNotes(payload)\n      )\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('updateActiveNote', () => {\n    const notes = [createNote({ id: '1', category: '3' }), createNote({ id: '2' })]\n    test('should update active note id on updateActiveNote when multiSelect is false', () => {\n      const payload = {\n        multiSelect: false,\n        noteId: '2',\n      }\n      const initialStateBeforeUpdatingActiveNote = {\n        ...initialState,\n        notes,\n      }\n\n      const nextState = { ...initialStateBeforeUpdatingActiveNote, activeNoteId: '2' }\n      const result = reducer(initialStateBeforeUpdatingActiveNote, updateActiveNote(payload))\n\n      expect(result).toEqual(nextState)\n    })\n    test('should update active note id on updateActiveNote when multiSelect is true', () => {\n      const payload = {\n        multiSelect: true,\n        noteId: '2',\n      }\n      const initialStateBeforeUpdatingActiveNote = {\n        ...initialState,\n        notes,\n        selectedNotesIds: ['1', '2'],\n      }\n\n      const nextState = { ...initialStateBeforeUpdatingActiveNote, activeNoteId: '2' }\n      const result = reducer(initialStateBeforeUpdatingActiveNote, updateActiveNote(payload))\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('updateActiveCategory', () => {\n    test('should update active category id , active note id, selected note ids and filter the draft notes on updateActiveCategoryId', () => {\n      const payload = '3'\n      const initialStateBeforeUpdatingActiveCategoryId = {\n        ...initialState,\n        notes: [\n          createNote({ id: '1', category: '3' }),\n          createNote({ id: '4', category: '3', text: '' }),\n          createNote({ id: '7', category: '3' }),\n        ],\n      }\n      const nextState = {\n        ...initialStateBeforeUpdatingActiveCategoryId,\n        activeFolder: Folder.CATEGORY,\n        activeCategoryId: '3',\n        activeNoteId: '1',\n        selectedNotesIds: ['1'],\n        notes: [createNote({ id: '1', category: '3' }), createNote({ id: '7', category: '3' })],\n      }\n      const result = reducer(\n        initialStateBeforeUpdatingActiveCategoryId,\n        updateActiveCategoryId(payload)\n      )\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('swapFolder', () => {\n    test('should swap folders and set FAVORITES folder as active folder', () => {\n      const payload = Folder.FAVORITES\n      const notes = [\n        createNote({ id: '1', category: '3' }),\n        createNote({ id: '2', favorite: true }),\n        createNote({ id: '4', category: '3', text: '' }),\n        createNote({ id: '7', category: '3', favorite: true }),\n      ]\n      const initialStateBeforeUpdatingActiveFolder = {\n        ...initialState,\n        notes: notes,\n      }\n      const nextState = {\n        ...initialStateBeforeUpdatingActiveFolder,\n        activeFolder: Folder.FAVORITES,\n        activeCategoryId: '',\n        activeNoteId: '2',\n        selectedNotesIds: ['2'],\n        notes: [notes[0], notes[1], notes[3]],\n      }\n      const result = reducer(\n        initialStateBeforeUpdatingActiveFolder,\n        swapFolder({ folder: payload })\n      )\n\n      expect(result).toEqual(nextState)\n    })\n\n    test('should swap folders and set SCRATCHPAD folder as active folder', () => {\n      const payload = Folder.SCRATCHPAD\n      const notes = [\n        createNote({ id: '1', category: '3' }),\n        createNote({ id: '2', scratchpad: true }),\n        createNote({ id: '4', category: '3', text: '' }),\n        createNote({ id: '7', category: '3', scratchpad: true }),\n      ]\n      const initialStateBeforeUpdatingActiveFolder = {\n        ...initialState,\n        notes: notes,\n      }\n      const nextState = {\n        ...initialStateBeforeUpdatingActiveFolder,\n        activeFolder: Folder.SCRATCHPAD,\n        activeCategoryId: '',\n        activeNoteId: '2',\n        selectedNotesIds: ['2'],\n        notes: [notes[0], notes[1], notes[3]],\n      }\n      const result = reducer(\n        initialStateBeforeUpdatingActiveFolder,\n        swapFolder({ folder: payload })\n      )\n\n      expect(result).toEqual(nextState)\n    })\n\n    test('should swap folders and set TRASH folder as active folder', () => {\n      const payload = Folder.TRASH\n      const notes = [\n        createNote({ id: '1', category: '3' }),\n        createNote({ id: '2', trash: true }),\n        createNote({ id: '4', category: '3', text: '' }),\n        createNote({ id: '7', category: '3', trash: true }),\n      ]\n      const initialStateBeforeUpdatingActiveFolder = {\n        ...initialState,\n        notes: notes,\n      }\n      const nextState = {\n        ...initialStateBeforeUpdatingActiveFolder,\n        activeFolder: Folder.TRASH,\n        activeCategoryId: '',\n        activeNoteId: '2',\n        selectedNotesIds: ['2'],\n        notes: [notes[0], notes[1], notes[3]],\n      }\n      const result = reducer(\n        initialStateBeforeUpdatingActiveFolder,\n        swapFolder({ folder: payload })\n      )\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('assignFavorite', () => {\n    test('should assign Favorite To Notes', () => {\n      const payload = '2'\n      const notes = [\n        createNote({ id: '1', category: '3', favorite: true }),\n        createNote({ id: '2', category: '3' }),\n      ]\n      const initialStateBeforeAssigningFavoriteToNotes = {\n        ...initialState,\n        notes: notes,\n      }\n      const nextState = {\n        ...initialStateBeforeAssigningFavoriteToNotes,\n        notes: [notes[0], { ...notes[1], favorite: true }],\n      }\n      const result = reducer(\n        initialStateBeforeAssigningFavoriteToNotes,\n        assignFavoriteToNotes(payload)\n      )\n\n      expect(result).toEqual(nextState)\n    })\n\n    test('should assign Favorite To Notes which are in selectedNotesIds', () => {\n      const payload = '2'\n      const notes = [createNote({ id: '1', category: '3' }), createNote({ id: '2', category: '3' })]\n      const initialStateBeforeAssigningFavoriteToNotes = {\n        ...initialState,\n        notes,\n        selectedNotesIds: ['2', '1'],\n      }\n      const nextState = {\n        ...initialStateBeforeAssigningFavoriteToNotes,\n        notes: [\n          { ...notes[0], favorite: true },\n          {\n            ...notes[1],\n            favorite: true,\n          },\n        ],\n        selectedNotesIds: ['2', '1'],\n      }\n      const result = reducer(\n        initialStateBeforeAssigningFavoriteToNotes,\n        assignFavoriteToNotes(payload)\n      )\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('toggleFavoriteNotes', () => {\n    test('should toggle Favorite notes for selected ids', () => {\n      const payload = '1'\n      const notes = [\n        createNote({ id: '1', category: '3', favorite: true }),\n        createNote({ id: '2' }),\n        createNote({ id: '3' }),\n      ]\n      const initialStateBeforeTogglingFavoriteToNotes = {\n        ...initialState,\n        notes: notes,\n        selectedNotesIds: ['2', '1'],\n      }\n      const nextState = {\n        ...initialStateBeforeTogglingFavoriteToNotes,\n        notes: [\n          {\n            ...notes[0],\n            favorite: false,\n          },\n          {\n            ...notes[1],\n            favorite: true,\n          },\n          notes[2],\n        ],\n        selectedNotesIds: ['2', '1'],\n      }\n      const result = reducer(\n        initialStateBeforeTogglingFavoriteToNotes,\n        toggleFavoriteNotes(payload)\n      )\n\n      expect(result).toEqual(nextState)\n    })\n    test('should toggle Favorite notes only for passed id when there are no selectedNotesIds', () => {\n      const payload = '1'\n      const notes = [\n        createNote({ id: '1', category: '3', favorite: true }),\n        createNote({ id: '2' }),\n        createNote({ id: '3' }),\n      ]\n      const initialStateBeforeTogglingFavoriteToNotes = {\n        ...initialState,\n        notes: notes,\n        selectedNotesIds: [],\n      }\n      const nextState = {\n        ...initialStateBeforeTogglingFavoriteToNotes,\n        notes: [\n          {\n            ...notes[0],\n            favorite: false,\n          },\n          notes[1],\n          notes[2],\n        ],\n        selectedNotesIds: [],\n      }\n      const result = reducer(\n        initialStateBeforeTogglingFavoriteToNotes,\n        toggleFavoriteNotes(payload)\n      )\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('assignTrash', () => {\n    const notes = [\n      createNote({ id: '1', category: '3', favorite: true }),\n      createNote({ id: '2' }),\n      createNote({ id: '3' }),\n    ]\n    test('should assign trash to all selected ids', () => {\n      const payload = '1'\n\n      const initialStateBeforeAssigningTrashToNotes = {\n        ...initialState,\n        notes,\n        selectedNotesIds: ['2', '1'],\n      }\n      const nextState = {\n        ...initialStateBeforeAssigningTrashToNotes,\n        notes: [\n          {\n            ...notes[0],\n            trash: true,\n          },\n          {\n            ...notes[1],\n            trash: true,\n          },\n          {\n            ...notes[2],\n          },\n        ],\n        selectedNotesIds: [''],\n        activeNoteId: '',\n      }\n      const result = reducer(initialStateBeforeAssigningTrashToNotes, assignTrashToNotes(payload))\n\n      expect(result).toEqual(nextState)\n    })\n\n    test('should assign trash to given payload id', () => {\n      const payload = '3'\n      const initialStateBeforeAssigningTrashToNotes = {\n        ...initialState,\n        notes,\n        selectedNotesIds: ['2', '1'],\n      }\n      const nextState = {\n        ...initialStateBeforeAssigningTrashToNotes,\n        notes: [\n          notes[0],\n          notes[1],\n          {\n            ...notes[2],\n            trash: true,\n          },\n        ],\n        selectedNotesIds: [''],\n        activeNoteId: '',\n      }\n      const result = reducer(initialStateBeforeAssigningTrashToNotes, assignTrashToNotes(payload))\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('toggleTrash', () => {\n    test('should toggle all selected trash notes', () => {\n      const payload = '2'\n      const notes = [\n        createNote({ id: '1', category: '3', favorite: true }),\n        createNote({ id: '2', trash: true }),\n        createNote({ id: '3', trash: false }),\n      ]\n      const initialStateBeforeTogglingTrashToNotes = {\n        ...initialState,\n        notes,\n        selectedNotesIds: ['2', '3'],\n      }\n      const nextState = {\n        ...initialStateBeforeTogglingTrashToNotes,\n        notes: [notes[0], { ...notes[1], trash: false }, { ...notes[2], trash: true }],\n        selectedNotesIds: ['1'],\n        activeNoteId: '1',\n      }\n      const result = reducer(initialStateBeforeTogglingTrashToNotes, toggleTrashNotes(payload))\n\n      expect(result).toEqual(nextState)\n    })\n\n    test('should toggle only payload id trash notes', () => {})\n\n    const payload = '2'\n    const notes = [\n      createNote({ id: '1', category: '3', favorite: true }),\n      createNote({ id: '2', trash: false }),\n      createNote({ id: '3' }),\n    ]\n    const initialStateBeforeTogglingTrashToNotes = {\n      ...initialState,\n      notes,\n      selectedNotesIds: ['3'],\n    }\n    const nextState = {\n      ...initialStateBeforeTogglingTrashToNotes,\n      notes: [\n        notes[0],\n        {\n          ...notes[1],\n          trash: true,\n        },\n        notes[2],\n      ],\n      selectedNotesIds: [''],\n      activeNoteId: '',\n    }\n    const result = reducer(initialStateBeforeTogglingTrashToNotes, toggleTrashNotes(payload))\n\n    expect(result).toEqual(nextState)\n  })\n\n  describe('unassignTrash', () => {\n    test('should unassign all selected notes from trash', () => {\n      const payload = '2'\n      const notes = [\n        createNote({ id: '1', category: '3', favorite: true }),\n        createNote({ id: '2', trash: true }),\n        createNote({ id: '3', trash: false }),\n      ]\n      const initialStateBeforeUnassigningTrashFromNotes = {\n        ...initialState,\n        notes,\n        selectedNotesIds: ['2', '3'],\n      }\n      const nextState = {\n        ...initialStateBeforeUnassigningTrashFromNotes,\n        notes: [\n          notes[0],\n          {\n            ...notes[1],\n            trash: false,\n          },\n          notes[2],\n        ],\n        selectedNotesIds: ['2', '3'],\n        activeNoteId: '',\n      }\n      const result = reducer(\n        initialStateBeforeUnassigningTrashFromNotes,\n        unassignTrashFromNotes(payload)\n      )\n\n      expect(result).toEqual(nextState)\n    })\n\n    test('should unassign only payload id trash note', () => {\n      const payload = '2'\n      const notes = [\n        createNote({ id: '1', category: '3', favorite: true }),\n        createNote({ id: '2', trash: true }),\n        createNote({ id: '3', trash: false }),\n      ]\n      const initialStateBeforeUnassigningTrashFromNotes = {\n        ...initialState,\n        notes,\n        selectedNotesIds: ['3'],\n      }\n      const nextState = {\n        ...initialStateBeforeUnassigningTrashFromNotes,\n        notes: [\n          notes[0],\n          {\n            ...notes[1],\n            trash: false,\n          },\n          notes[2],\n        ],\n        selectedNotesIds: ['3'],\n        activeNoteId: '',\n      }\n      const result = reducer(\n        initialStateBeforeUnassigningTrashFromNotes,\n        unassignTrashFromNotes(payload)\n      )\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  describe('updateSelectedNotes', () => {\n    test('should update selected notes ids when multiSelect is false', () => {\n      const payload = {\n        noteId: '2',\n        multiSelect: false,\n      }\n      const initialStateBeforeUpdatingSelectedNotes = {\n        ...initialState,\n        selectedNotesIds: [],\n        notes: [createNote({ id: '2' })],\n      }\n      const nextState = {\n        ...initialStateBeforeUpdatingSelectedNotes,\n        selectedNotesIds: ['2'],\n      }\n\n      const result = reducer(initialStateBeforeUpdatingSelectedNotes, updateSelectedNotes(payload))\n\n      expect(result).toEqual(nextState)\n    })\n\n    test('should update selected notes ids when multiSelect is true', () => {\n      const payload = {\n        noteId: '3',\n        multiSelect: true,\n      }\n      const initialStateBeforeUpdatingSelectedNotes = {\n        ...initialState,\n        selectedNotesIds: ['2'],\n        notes: [createNote({ id: '2' }), createNote({ id: '3' })],\n      }\n      const nextState = {\n        ...initialStateBeforeUpdatingSelectedNotes,\n        selectedNotesIds: ['2', '3'],\n      }\n\n      const result = reducer(initialStateBeforeUpdatingSelectedNotes, updateSelectedNotes(payload))\n\n      expect(result).toEqual(nextState)\n    })\n\n    test('should update selected notes ids when multiSelect is true and selectedNotesIds are more than 1', () => {\n      const payload = {\n        noteId: '4',\n        multiSelect: true,\n      }\n      const initialStateBeforeUpdatingSelectedNotes = {\n        ...initialState,\n        selectedNotesIds: ['2', '3'],\n        notes: [createNote({ id: '2' }), createNote({ id: '3' }), createNote({ id: '4' })],\n      }\n      const nextState = {\n        ...initialStateBeforeUpdatingSelectedNotes,\n        selectedNotesIds: ['2', '3', '4'],\n      }\n\n      const result = reducer(initialStateBeforeUpdatingSelectedNotes, updateSelectedNotes(payload))\n\n      expect(result).toEqual(nextState)\n    })\n  })\n\n  test('should permanently empty trash', () => {\n    const initialStateBeforeEmptyingTRash = {\n      ...initialState,\n      notes: [createNote({ id: '2', trash: true }), createNote({ id: '3' })],\n    }\n    const nextState = {\n      ...initialStateBeforeEmptyingTRash,\n      notes: [createNote({ id: '3' })],\n    }\n\n    const result = reducer(initialStateBeforeEmptyingTRash, permanentlyEmptyTrash())\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should prune notes', () => {\n    const initialStateBeforePrune = {\n      ...initialState,\n      notes: [\n        createNote({ id: '1', trash: true, text: '' }),\n        createNote({ id: '2', trash: true, text: '' }),\n        createNote({ id: '3' }),\n      ],\n      selectedNotesIds: ['2'],\n    }\n    const nextState = {\n      ...initialStateBeforePrune,\n      notes: [createNote({ id: '2', trash: true, text: '' }), createNote({ id: '3' })],\n    }\n    const result = reducer(initialStateBeforePrune, pruneNotes())\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set searchValue to payload on searchNotes', () => {\n    const payload = 'searchText'\n    const nextState = {\n      ...initialState,\n      searchValue: payload,\n    }\n    const result = reducer(initialState, searchNotes(payload))\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set loading true on loadNotes', () => {\n    const nextState = {\n      ...initialState,\n      loading: true,\n    }\n    const result = reducer(initialState, loadNotes())\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set loading false and error to payload on loadNotesError', () => {\n    const payload = 'error'\n    const nextState = {\n      ...initialState,\n      loading: false,\n      error: payload,\n    }\n    const result = reducer(initialState, loadNotesError(payload))\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set value for notes, activeNoteId and selectedNotesIds on loadNotesSuccess', () => {\n    const payload = [createNote({ id: '2', text: '' })]\n    const nextState = {\n      ...initialState,\n      loading: false,\n      notes: payload,\n      activeNoteId: '2',\n      selectedNotesIds: ['2'],\n    }\n    const result = reducer(initialState, loadNotesSuccess({ notes: payload }))\n\n    expect(result).toEqual(nextState)\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/slices/settings.test.ts",
    "content": "import { PayloadAction } from '@reduxjs/toolkit'\n\nimport reducer, {\n  initialState,\n  toggleSettingsModal,\n  togglePreviewMarkdown,\n  updateCodeMirrorOption,\n  toggleDarkTheme,\n} from '@/slices/settings'\n\ndescribe('settings slice', () => {\n  it('should return the initial state on first run', () => {\n    const nextState = initialState\n    const action = {} as PayloadAction\n    const result = reducer(undefined, action)\n\n    expect(result).toEqual(nextState)\n  })\n\n  it('should toggle open state', () => {\n    const nextState = { ...initialState, isOpen: true }\n    const result = reducer(initialState, toggleSettingsModal())\n\n    expect(result).toEqual(nextState)\n  })\n\n  it('should update code mirror option', () => {\n    const payload = { key: 'key123', value: 'mirror' }\n    const state = {\n      ...initialState,\n      codeMirrorOptions: {\n        ...initialState.codeMirrorOptions,\n        [payload.key]: payload.value,\n      },\n    }\n    const result = reducer(initialState, updateCodeMirrorOption(payload))\n\n    expect(result).toEqual(state)\n  })\n\n  it('should toggle preview markdown state', () => {\n    const nextState = { ...initialState, previewMarkdown: !initialState.previewMarkdown }\n    const result = reducer(initialState, togglePreviewMarkdown())\n\n    expect(result).toEqual(nextState)\n  })\n\n  it('should toggle dark theme state', () => {\n    const nextState = { ...initialState, darkTheme: !initialState.darkTheme }\n    const result = reducer(initialState, toggleDarkTheme())\n\n    expect(result).toEqual(nextState)\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/slices/sync.test.ts",
    "content": "import { PayloadAction } from '@reduxjs/toolkit'\n\nimport reducer, { initialState, setPendingSync, sync, syncError, syncSuccess } from '@/slices/sync'\n\ndescribe('SycSlice', () => {\n  test('should return initial state on first run', () => {\n    const nextState = initialState\n    const action = {} as PayloadAction\n    const result = reducer(undefined, action)\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set pendingSync to true on setPendingSync', () => {\n    const nextState = { ...initialState, pendingSync: true }\n    const result = reducer(undefined, setPendingSync())\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set syncing to true on sync', () => {\n    const payload = {\n      categories: [],\n      notes: [],\n    }\n    const nextState = { ...initialState, syncing: true }\n    const result = reducer(initialState, sync(payload))\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set syncing to false and error to payload on syncError', () => {\n    const payload = 'test error'\n    const nextState = { ...initialState, syncing: false, error: payload }\n    const result = reducer(initialState, syncError(payload))\n\n    expect(result).toEqual(nextState)\n  })\n\n  test('should set syncing to false, pendingSync to false and lastSynced to payload on syncSuccess', () => {\n    const payload = 'lastUpdated'\n    const nextState = {\n      ...initialState,\n      syncing: false,\n      lastSynced: payload,\n      pendingSync: false,\n      error: '',\n    }\n    const result = reducer(initialState, syncSuccess(payload))\n\n    expect(result).toEqual(nextState)\n  })\n})\n"
  },
  {
    "path": "tests/unit/client/testHelpers.tsx",
    "content": "import { render } from '@testing-library/react'\nimport { createMemoryHistory, MemoryHistory } from 'history'\nimport React, { ReactNode } from 'react'\nimport { Provider } from 'react-redux'\nimport { MemoryRouter } from 'react-router-dom'\nimport createSagaMiddleware from 'redux-saga'\nimport { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'\n\nimport rootSaga from '@/sagas'\nimport rootReducer from '@/slices'\n\ninterface RenderWithRouterOptions {\n  route: string\n  history: MemoryHistory\n}\n\nexport const renderWithRouter = (\n  ui: ReactNode,\n  {\n    route = '/',\n    history = createMemoryHistory({ initialEntries: [route] }),\n  }: RenderWithRouterOptions = {} as RenderWithRouterOptions\n) => {\n  const sagaMiddleware = createSagaMiddleware()\n\n  const store = configureStore({\n    reducer: rootReducer,\n    middleware: [sagaMiddleware, ...getDefaultMiddleware({ thunk: false })],\n  })\n\n  sagaMiddleware.run(rootSaga)\n\n  return {\n    ...render(\n      <Provider store={store}>\n        <MemoryRouter>{ui}</MemoryRouter>\n      </Provider>\n    ),\n    history,\n  }\n}\n"
  },
  {
    "path": "tests/unit/client/utils/index.test.ts",
    "content": "import dayjs from 'dayjs'\n\nimport { getNoteTitle, getWebsiteTitle, getActiveNoteFromShortUuid } from '@/utils/helpers'\nimport { Folder } from '@/utils/enums'\nimport { NoteItem, CategoryItem } from '@/types'\n\ndescribe('Utilities', () => {\n  describe('getNoteTitle', () => {\n    test(`should return 45 characters`, () => {\n      const note = `This is your world. This is gonna be a happy little seascape. I'm gonna start with a little Alizarin crimson and a touch of Prussian blue`\n      expect(getNoteTitle(note)).toEqual(note.slice(0, 45).trim())\n    })\n\n    test(`should trim both ends`, () => {\n      const note = ` This is your world. This is gonna be a happy `\n      expect(getNoteTitle(note)).toEqual(`This is your world. This is gonna be a happy`)\n    })\n\n    test(`should only return the first line`, () => {\n      const note = `Something\n      \n      and something else`\n      expect(getNoteTitle(note)).toEqual(`Something`)\n    })\n\n    test(`should not display a hash`, () => {\n      const note = `# Something\n      \n  and something else`\n      expect(getNoteTitle(note)).toEqual(`Something`)\n    })\n\n    test(`should ignore newlines in the beginning`, () => {\n      const note = `\n      \n  Something\n      \n  and something else`\n      expect(getNoteTitle(note)).toEqual(`Something`)\n    })\n  })\n\n  describe('getWebsiteTitle', () => {\n    test(`should display the folder name followed by the app name`, () => {\n      expect(getWebsiteTitle(Folder.ALL)).toEqual(`All Notes | TakeNote`)\n      expect(getWebsiteTitle(Folder.FAVORITES)).toEqual(`Favorites | TakeNote`)\n      expect(getWebsiteTitle(Folder.TRASH)).toEqual(`Trash | TakeNote`)\n    })\n\n    test(`should display the category name followed by the app name`, () => {\n      const category = {\n        id: '123',\n        name: 'Recipes',\n        draggedOver: false,\n      }\n\n      expect(getWebsiteTitle(Folder.CATEGORY, category)).toEqual(`Recipes | TakeNote`)\n    })\n  })\n\n  const newNote = (id: string): NoteItem => ({\n    id: id,\n    text: '',\n    created: dayjs().format(),\n    lastUpdated: dayjs().format(),\n  })\n  describe('getActiveNoteFromShortUuid', () => {\n    test(`should get active note from short`, () => {\n      const activeNoteId = '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b'\n      const shortActiveNoteId = '6ec0bd'\n      const othernoteId = '710b962e-041c-11e1-9234-0123456789ab'\n\n      const activenote: NoteItem = newNote(activeNoteId)\n\n      const othernote: NoteItem = newNote(othernoteId)\n\n      const notes = [activenote, othernote]\n\n      expect(getActiveNoteFromShortUuid(notes, shortActiveNoteId)).toEqual(activenote)\n    })\n\n    test(`should get active note from short with braces`, () => {\n      const activeNoteId = '6ec0bd7f-11c0-43da-975e-2a8ad9ebae0b'\n      const shortActiveNoteId = '{{6ec0bd}}'\n      const otherNoteId = '710b962e-041c-11e1-9234-0123456789ab'\n\n      const activeNote: NoteItem = newNote(activeNoteId)\n\n      const otherNote: NoteItem = newNote(otherNoteId)\n\n      const notes = [activeNote, otherNote]\n\n      expect(getActiveNoteFromShortUuid(notes, shortActiveNoteId)).toEqual(activeNote)\n    })\n\n    test(`should not get active note if not present`, () => {\n      const shortActiveNoteId = '6ec0bd'\n      const oneNoteId = '109156be-c4fb-41ea-b1b4-efe1671c5836'\n      const otherNoteId = '710b962e-041c-11e1-9234-0123456789ab'\n\n      const oneNote: NoteItem = newNote(oneNoteId)\n\n      const otherNote: NoteItem = newNote(otherNoteId)\n\n      const notes = [oneNote, otherNote]\n\n      expect(getActiveNoteFromShortUuid(notes, shortActiveNoteId)).toEqual(undefined)\n    })\n  })\n})\n"
  },
  {
    "path": "tests/unit/server/middleware/checkAuth.test.ts",
    "content": "import checkAuth from '../../../../src/server/middleware/checkAuth'\n\ndescribe(`checkAuth middleware`, () => {\n  let requestMock: any\n  let responseMock: any\n  const nextMock = jest.fn()\n  const statusSend = jest.fn()\n\n  beforeEach(() => {\n    responseMock = {\n      locals: {},\n      status: jest.fn(() => {\n        return { send: statusSend }\n      }),\n      clearCookie: jest.fn(),\n    }\n  })\n\n  afterEach(() => jest.resetAllMocks())\n\n  test(`should pass saved cookies to locals`, async () => {\n    requestMock = {\n      cookies: {\n        githubAccessToken: 'test access token',\n      },\n    }\n\n    await checkAuth(requestMock, responseMock, nextMock)\n\n    expect(responseMock.locals.accessToken).toEqual('test access token')\n  })\n\n  test(`should exit with an error if no access token cookie`, async () => {\n    requestMock = {\n      cookies: {},\n    }\n\n    await checkAuth(requestMock, responseMock, nextMock)\n\n    expect(statusSend).toBeCalledWith({ message: 'Forbidden Resource', status: 403 })\n  })\n})\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"outDir\": \"./dist\",\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noImplicitAny\": true,\n    \"allowJs\": false,\n    \"target\": \"es5\",\n    \"module\": \"commonjs\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"jsx\": \"react\",\n    \"allowSyntheticDefaultImports\": true,\n    \"esModuleInterop\": true,\n    \"sourceMap\": true,\n    \"paths\": {\n      // Allow `@/` to map to `src/client/`\n      \"@/*\": [\"./src/client/*\"],\n      \"@resources/*\": [\"./src/resources/*\"]\n    }\n  },\n  \"include\": [\"./src/**/*\", \"./tests/unit/**/*\"],\n  \"exclude\": [\"./dist\", \"node_modules\", \"./config\"]\n}\n"
  }
]