Showing preview only (443K chars total). Download the full file or copy to clipboard to get everything.
Repository: taniarascia/takenote
Branch: master
Commit: e0eddbb9a21a
Files: 164
Total size: 404.2 KB
Directory structure:
gitextract_n64ay1gg/
├── .all-contributorsrc
├── .dockerignore
├── .editorconfig
├── .eslintrc.js
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── config/
│ ├── cypress.config.json
│ ├── jest.config.js
│ ├── nodemon.config.json
│ ├── webpack.common.js
│ ├── webpack.dev.js
│ └── webpack.prod.js
├── deploy.sh
├── kubernetes.yml
├── package.json
├── public/
│ ├── _redirects
│ ├── manifest.json
│ ├── robots.txt
│ └── template.html
├── seed.js
├── src/
│ ├── client/
│ │ ├── api/
│ │ │ └── index.ts
│ │ ├── components/
│ │ │ ├── AppSidebar/
│ │ │ │ ├── ActionButton.tsx
│ │ │ │ ├── AddCategoryButton.tsx
│ │ │ │ ├── AddCategoryForm.tsx
│ │ │ │ ├── CollapseCategoryButton.tsx
│ │ │ │ ├── FolderOption.tsx
│ │ │ │ └── ScratchpadOption.tsx
│ │ │ ├── Editor/
│ │ │ │ ├── EmptyEditor.tsx
│ │ │ │ ├── NoteLink.tsx
│ │ │ │ └── PreviewEditor.tsx
│ │ │ ├── LandingPage.tsx
│ │ │ ├── LastSyncedNotification.tsx
│ │ │ ├── NoteList/
│ │ │ │ ├── ContextMenuOption.tsx
│ │ │ │ ├── NoteListButton.tsx
│ │ │ │ ├── SearchBar.tsx
│ │ │ │ └── SelectCategory.tsx
│ │ │ ├── Select.tsx
│ │ │ ├── SettingsModal/
│ │ │ │ ├── IconButton.tsx
│ │ │ │ ├── IconButtonUploader.tsx
│ │ │ │ ├── Option.tsx
│ │ │ │ ├── SelectOptions.tsx
│ │ │ │ └── Shortcut.tsx
│ │ │ ├── Switch.tsx
│ │ │ └── Tabs/
│ │ │ ├── Tab.tsx
│ │ │ ├── TabPanel.tsx
│ │ │ └── Tabs.tsx
│ │ ├── containers/
│ │ │ ├── App.tsx
│ │ │ ├── AppSidebar.tsx
│ │ │ ├── CategoryList.tsx
│ │ │ ├── CategoryOption.tsx
│ │ │ ├── ContextMenu.tsx
│ │ │ ├── ContextMenuOptions.tsx
│ │ │ ├── KeyboardShortcuts.tsx
│ │ │ ├── NoteEditor.tsx
│ │ │ ├── NoteList.tsx
│ │ │ ├── NoteMenuBar.tsx
│ │ │ ├── SettingsModal.tsx
│ │ │ └── TakeNoteApp.tsx
│ │ ├── contexts/
│ │ │ └── TempStateContext.tsx
│ │ ├── global.d.ts
│ │ ├── index.tsx
│ │ ├── router/
│ │ │ ├── PrivateRoute.tsx
│ │ │ └── PublicRoute.tsx
│ │ ├── sagas/
│ │ │ └── index.ts
│ │ ├── selectors/
│ │ │ └── index.ts
│ │ ├── serviceWorker.ts
│ │ ├── setupTests.ts
│ │ ├── slices/
│ │ │ ├── auth.ts
│ │ │ ├── category.ts
│ │ │ ├── index.ts
│ │ │ ├── note.ts
│ │ │ ├── settings.ts
│ │ │ └── sync.ts
│ │ ├── styles/
│ │ │ ├── _app-sidebar.scss
│ │ │ ├── _buttons.scss
│ │ │ ├── _dark.scss
│ │ │ ├── _editor.scss
│ │ │ ├── _forms.scss
│ │ │ ├── _helpers.scss
│ │ │ ├── _landing-page.scss
│ │ │ ├── _layout.scss
│ │ │ ├── _light-theme.scss
│ │ │ ├── _mixins.scss
│ │ │ ├── _modal.scss
│ │ │ ├── _new-moon.scss
│ │ │ ├── _note-menu-bar.scss
│ │ │ ├── _note-sidebar.scss
│ │ │ ├── _previewer.scss
│ │ │ ├── _scaffolding.scss
│ │ │ ├── _tabs.scss
│ │ │ ├── _variables.scss
│ │ │ └── index.scss
│ │ ├── types/
│ │ │ └── index.ts
│ │ └── utils/
│ │ ├── constants.ts
│ │ ├── enums.ts
│ │ ├── helpers.ts
│ │ ├── history.ts
│ │ ├── hooks.ts
│ │ ├── notesSortStrategies.ts
│ │ └── reactMarkdownPlugins.ts
│ ├── resources/
│ │ ├── LabelText.ts
│ │ └── TestID.ts
│ └── server/
│ ├── handlers/
│ │ ├── auth.ts
│ │ └── sync.ts
│ ├── index.ts
│ ├── initializeServer.ts
│ ├── middleware/
│ │ ├── checkAuth.ts
│ │ └── getUser.ts
│ ├── router/
│ │ ├── auth.ts
│ │ ├── index.ts
│ │ └── sync.ts
│ └── utils/
│ ├── constants.ts
│ ├── data/
│ │ ├── scratchpadNote.ts
│ │ └── welcomeNote.ts
│ ├── enums.ts
│ └── helpers.ts
├── tests/
│ ├── __mocks__/
│ │ └── styleMock.ts
│ ├── e2e/
│ │ ├── integration/
│ │ │ ├── category.test.ts
│ │ │ ├── note.test.ts
│ │ │ └── settings.test.ts
│ │ ├── plugins/
│ │ │ ├── cy-ts-preprocessor.js
│ │ │ └── index.js
│ │ ├── support/
│ │ │ ├── commands.js
│ │ │ └── index.js
│ │ ├── tsconfig.json
│ │ └── utils/
│ │ ├── testCategoryHelperUtils.ts
│ │ ├── testHelperEnums.ts
│ │ ├── testHelperUtils.ts
│ │ ├── testNotesHelperUtils.ts
│ │ └── testSettingsUtils.ts
│ └── unit/
│ ├── client/
│ │ ├── components/
│ │ │ ├── AppSidebar/
│ │ │ │ ├── ActionButton.test.tsx
│ │ │ │ ├── AddCategoryButton.test.tsx
│ │ │ │ ├── AddCategoryForm.test.tsx
│ │ │ │ ├── CollapseCategoryButton.test.tsx
│ │ │ │ ├── FolderOption.test.tsx
│ │ │ │ └── ScratchpadOption.test.tsx
│ │ │ ├── LastSyncedNotification.test.tsx
│ │ │ ├── NoteList/
│ │ │ │ ├── NoteListButton.test.tsx
│ │ │ │ └── SearchBar.test.tsx
│ │ │ ├── SettingsModal/
│ │ │ │ ├── IconButton.test.tsx
│ │ │ │ └── IconButtonUploader.test.tsx
│ │ │ ├── Switch.test.tsx
│ │ │ └── editor/
│ │ │ ├── EditorEmpty.test.tsx
│ │ │ └── PreviewEditor.test.tsx
│ │ ├── containers/
│ │ │ ├── ContextMenuOptions.test.tsx
│ │ │ └── TakeNoteApp.test.tsx
│ │ ├── slices/
│ │ │ ├── auth.test.ts
│ │ │ ├── category.test.ts
│ │ │ ├── note.test.ts
│ │ │ ├── settings.test.ts
│ │ │ └── sync.test.ts
│ │ ├── testHelpers.tsx
│ │ └── utils/
│ │ └── index.test.ts
│ └── server/
│ └── middleware/
│ └── checkAuth.test.ts
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .all-contributorsrc
================================================
{
"projectName": "takenote",
"projectOwner": "taniarascia",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 50,
"commit": true,
"commitConvention": "none",
"contributors": [
{
"login": "taniarascia",
"name": "Tania Rascia",
"avatar_url": "https://avatars3.githubusercontent.com/u/11951801?v=4",
"profile": "https://www.taniarascia.com",
"contributions": [
"code",
"ideas",
"bug"
]
},
{
"login": "hankolsen",
"name": "hankolsen",
"avatar_url": "https://avatars3.githubusercontent.com/u/1008390?v=4",
"profile": "https://github.com/hankolsen",
"contributions": [
"code",
"bug",
"test"
]
},
{
"login": "joseph-perez",
"name": "Joseph Perez",
"avatar_url": "https://avatars0.githubusercontent.com/u/7772649?v=4",
"profile": "https://github.com/joseph-perez",
"contributions": [
"code"
]
},
{
"login": "dagda1",
"name": "Paul",
"avatar_url": "https://avatars0.githubusercontent.com/u/118328?v=4",
"profile": "https://cutting.scot",
"contributions": [
"code",
"test"
]
},
{
"login": "MartinRosenberg",
"name": "Martin Rosenberg",
"avatar_url": "https://avatars2.githubusercontent.com/u/2382147?v=4",
"profile": "https://martinbrosenberg.com/",
"contributions": [
"code",
"bug",
"maintenance"
]
},
{
"login": "meowwwls",
"name": "Melissa",
"avatar_url": "https://avatars3.githubusercontent.com/u/16426195?v=4",
"profile": "http://codepen.io/meowwwls",
"contributions": [
"code"
]
},
{
"login": "jjtowle",
"name": "Jason Towle",
"avatar_url": "https://avatars0.githubusercontent.com/u/41359068?v=4",
"profile": "https://github.com/jjtowle",
"contributions": [
"code"
]
},
{
"login": "markerikson",
"name": "Mark Erikson",
"avatar_url": "https://avatars1.githubusercontent.com/u/1128784?v=4",
"profile": "http://blog.isquaredsoftware.com",
"contributions": [
"ideas"
]
},
{
"login": "alphonseb",
"name": "Alphonse Bouy",
"avatar_url": "https://avatars2.githubusercontent.com/u/32797759?v=4",
"profile": "http://www.alphonsebouy.fr",
"contributions": [
"bug"
]
},
{
"login": "dave2kb",
"name": "dave2kb",
"avatar_url": "https://avatars1.githubusercontent.com/u/30696030?v=4",
"profile": "https://github.com/dave2kb",
"contributions": [
"design",
"ideas"
]
},
{
"login": "Dantaro",
"name": "Devin McIntyre",
"avatar_url": "https://avatars3.githubusercontent.com/u/2750903?v=4",
"profile": "https://github.com/Dantaro",
"contributions": [
"code"
]
},
{
"login": "jeffslofish",
"name": "Jeffrey Fisher",
"avatar_url": "https://avatars0.githubusercontent.com/u/1240484?v=4",
"profile": "http://slofish.io",
"contributions": [
"bug"
]
},
{
"login": "dong-alex",
"name": "Alex Dong",
"avatar_url": "https://avatars2.githubusercontent.com/u/23242741?v=4",
"profile": "https://github.com/dong-alex",
"contributions": [
"code"
]
},
{
"login": "Publicker",
"name": "Publicker",
"avatar_url": "https://avatars2.githubusercontent.com/u/52673485?v=4",
"profile": "https://github.com/Publicker",
"contributions": [
"code"
]
},
{
"login": "kleyu",
"name": "Jakub Naskręski",
"avatar_url": "https://avatars2.githubusercontent.com/u/36169811?v=4",
"profile": "https://github.com/kleyu",
"contributions": [
"code",
"bug",
"test"
]
},
{
"login": "opw0011",
"name": "Benny O",
"avatar_url": "https://avatars2.githubusercontent.com/u/10897048?v=4",
"profile": "https://opw0011.github.io/",
"contributions": [
"code"
]
},
{
"login": "justDOindev",
"name": "Justin Payne",
"avatar_url": "https://avatars3.githubusercontent.com/u/44042682?v=4",
"profile": "https://github.com/justDOindev",
"contributions": [
"code"
]
},
{
"login": "yikjin",
"name": "marshmallow",
"avatar_url": "https://avatars2.githubusercontent.com/u/34995304?v=4",
"profile": "https://yikjin.github.io",
"contributions": [
"maintenance"
]
},
{
"login": "Jfelix61",
"name": "Jose Felix ",
"avatar_url": "https://avatars2.githubusercontent.com/u/21092519?v=4",
"profile": "http://jfelix.info",
"contributions": [
"code"
]
},
{
"login": "xboston",
"name": "Nikolay Kirsh",
"avatar_url": "https://avatars1.githubusercontent.com/u/201306?v=4",
"profile": "https://xboston.dev",
"contributions": [
"code"
]
},
{
"login": "Mudassar045",
"name": "Mudassar Ali",
"avatar_url": "https://avatars0.githubusercontent.com/u/24487349?v=4",
"profile": "https://github.com/Mudassar045",
"contributions": [
"code"
]
},
{
"login": "NathanBland",
"name": "Nathan Bland",
"avatar_url": "https://avatars1.githubusercontent.com/u/926111?v=4",
"profile": "https://nathanbland.github.io/",
"contributions": [
"bug",
"code"
]
},
{
"login": "siliconeidolon",
"name": "Craig Lam",
"avatar_url": "https://avatars1.githubusercontent.com/u/8170456?v=4",
"profile": "http://craiglam.com",
"contributions": [
"code",
"bug",
"test"
]
},
{
"login": "ashinzekene",
"name": "Ashinze Ekene",
"avatar_url": "https://avatars2.githubusercontent.com/u/20991583?v=4",
"profile": "https://twitter.com/ashinzekene",
"contributions": [
"bug",
"code"
]
},
{
"login": "harrySullivan",
"name": "Harry Sullivan",
"avatar_url": "https://avatars0.githubusercontent.com/u/38230536?v=4",
"profile": "https://adityasriram.ga",
"contributions": [
"code"
]
},
{
"login": "moudev",
"name": "Mauricio Martínez",
"avatar_url": "https://avatars2.githubusercontent.com/u/13499566?v=4",
"profile": "https://github.com/moudev",
"contributions": [
"code"
]
},
{
"login": "BlackHole1",
"name": "Black-Hole",
"avatar_url": "https://avatars0.githubusercontent.com/u/8198408?v=4",
"profile": "http://www.bugs.cc/",
"contributions": [
"code"
]
},
{
"login": "yogan",
"name": "Frank Blendinger",
"avatar_url": "https://avatars0.githubusercontent.com/u/122564?v=4",
"profile": "https://zogan.de/",
"contributions": [
"code"
]
},
{
"login": "osiux",
"name": "Eduardo Reveles",
"avatar_url": "https://avatars2.githubusercontent.com/u/204463?v=4",
"profile": "https://www.osiux.ws",
"contributions": [
"code"
]
},
{
"login": "leofrozenyogurt",
"name": "Leo Royzengurt",
"avatar_url": "https://avatars2.githubusercontent.com/u/2198384?v=4",
"profile": "https://github.com/leofrozenyogurt",
"contributions": [
"code",
"bug"
]
},
{
"login": "kcvgan",
"name": "kcvgan",
"avatar_url": "https://avatars1.githubusercontent.com/u/13578888?v=4",
"profile": "https://github.com/kcvgan",
"contributions": [
"code",
"bug"
]
},
{
"login": "codytowstik",
"name": "Cody Towstik",
"avatar_url": "https://avatars1.githubusercontent.com/u/10625608?v=4",
"profile": "https://github.com/codytowstik",
"contributions": [
"code",
"test",
"bug"
]
},
{
"login": "vincentdoerig",
"name": "Vincent Dörig",
"avatar_url": "https://avatars3.githubusercontent.com/u/24668338?v=4",
"profile": "https://github.com/vincentdoerig",
"contributions": [
"test",
"code"
]
},
{
"login": "miqh",
"name": "Michael Huynh",
"avatar_url": "https://avatars3.githubusercontent.com/u/43751307?v=4",
"profile": "https://github.com/miqh",
"contributions": [
"code",
"bug"
]
},
{
"login": "code128",
"name": "Joshua Bloom",
"avatar_url": "https://avatars0.githubusercontent.com/u/43435?v=4",
"profile": "https://github.com/code128",
"contributions": [
"code"
]
},
{
"login": "Mxchaeltrxn",
"name": "Mxchaeltrxn",
"avatar_url": "https://avatars3.githubusercontent.com/u/34886045?v=4",
"profile": "https://github.com/Mxchaeltrxn",
"contributions": [
"code",
"test"
]
},
{
"login": "KonradStanski",
"name": "Konrad Staniszewski",
"avatar_url": "https://avatars2.githubusercontent.com/u/38778413?v=4",
"profile": "https://konradstaniszewski.com",
"contributions": [
"doc"
]
},
{
"login": "yohix",
"name": "Yohix",
"avatar_url": "https://avatars3.githubusercontent.com/u/61746440?v=4",
"profile": "https://github.com/yohix",
"contributions": [
"maintenance"
]
},
{
"login": "jackson-elfers",
"name": "Jackson Elfers",
"avatar_url": "https://avatars1.githubusercontent.com/u/55408089?v=4",
"profile": "https://github.com/jackson-elfers",
"contributions": [
"code"
]
},
{
"login": "vamshi-tg",
"name": "Vamshi",
"avatar_url": "https://avatars2.githubusercontent.com/u/32225088?v=4",
"profile": "https://github.com/vamshi-tg",
"contributions": [
"code"
]
},
{
"login": "pavlakissimos",
"name": "Simos",
"avatar_url": "https://avatars1.githubusercontent.com/u/19609475?v=4",
"profile": "https://github.com/pavlakissimos",
"contributions": [
"code",
"test"
]
},
{
"login": "ggonza89",
"name": "Yankee",
"avatar_url": "https://avatars0.githubusercontent.com/u/5530647?v=4",
"profile": "https://github.com/ggonza89",
"contributions": [
"code",
"ideas",
"test"
]
},
{
"login": "G-Milevski",
"name": "G-Milevski",
"avatar_url": "https://avatars2.githubusercontent.com/u/25174255?v=4",
"profile": "https://github.com/G-Milevski",
"contributions": [
"code"
]
},
{
"login": "kodyclemens",
"name": "Kody Clemens",
"avatar_url": "https://avatars0.githubusercontent.com/u/43357615?v=4",
"profile": "https://kodyclemens.com",
"contributions": [
"code",
"test",
"bug"
]
},
{
"login": "qpeela",
"name": "Vladimir Yamshikov",
"avatar_url": "https://avatars3.githubusercontent.com/u/5824914?v=4",
"profile": "https://github.com/qpeela",
"contributions": [
"code",
"bug"
]
},
{
"login": "ronan696",
"name": "Ronan D'Souza",
"avatar_url": "https://avatars1.githubusercontent.com/u/13074003?v=4",
"profile": "https://about.me/ronan696",
"contributions": [
"code"
]
},
{
"login": "ModProg",
"name": "Roland Fredenhagen",
"avatar_url": "https://avatars0.githubusercontent.com/u/11978847?v=4",
"profile": "http://modprog.de",
"contributions": [
"code"
]
},
{
"login": "PranjaliPatil14",
"name": "Pranjali Pramod Patil",
"avatar_url": "https://avatars2.githubusercontent.com/u/31987627?v=4",
"profile": "https://github.com/PranjaliPatil14",
"contributions": [
"test"
]
},
{
"login": "cbrgm",
"name": "Chris Bargmann",
"avatar_url": "https://avatars1.githubusercontent.com/u/24737434?v=4",
"profile": "https://cbrgm.net",
"contributions": [
"ideas",
"code"
]
},
{
"login": "Jadhielv",
"name": "Jadhiel Vélez",
"avatar_url": "https://avatars3.githubusercontent.com/u/24376900?v=4",
"profile": "https://www.linkedin.com/in/jadhielv",
"contributions": [
"code",
"bug"
]
},
{
"login": "machadolucasvp",
"name": "Lucas Machado",
"avatar_url": "https://avatars0.githubusercontent.com/u/44952113?v=4",
"profile": "https://github.com/machadolucasvp",
"contributions": [
"code",
"bug",
"test"
]
},
{
"login": "xsteadybcgo",
"name": "xsteadybcgo",
"avatar_url": "https://avatars3.githubusercontent.com/u/19681921?v=4",
"profile": "https://github.com/xsteadybcgo",
"contributions": [
"bug"
]
},
{
"login": "Rwandarushya",
"name": "Marius Robert RWANDARUSHYA",
"avatar_url": "https://avatars2.githubusercontent.com/u/49269745?v=4",
"profile": "https://github.com/Rwandarushya",
"contributions": [
"test"
]
},
{
"login": "Isaackomeza",
"name": "Isaac Komezusenge",
"avatar_url": "https://avatars1.githubusercontent.com/u/66563235?v=4",
"profile": "https://github.com/Isaackomeza",
"contributions": [
"test"
]
},
{
"login": "maximeish",
"name": "Maxime Ishimwe",
"avatar_url": "https://avatars0.githubusercontent.com/u/54126307?v=4",
"profile": "https://github.com/maximeish",
"contributions": [
"test"
]
},
{
"login": "marcosspn",
"name": "Marcos Spanholi",
"avatar_url": "https://avatars3.githubusercontent.com/u/2171424?v=4",
"profile": "https://github.com/marcosspn",
"contributions": [
"test"
]
},
{
"login": "roshanrajeev",
"name": "Roshan Rajeev",
"avatar_url": "https://avatars2.githubusercontent.com/u/52269241?v=4",
"profile": "http://roshanrajeev.xyz",
"contributions": [
"code"
]
},
{
"login": "fistonhn",
"name": "fistonhn",
"avatar_url": "https://avatars0.githubusercontent.com/u/55746279?v=4",
"profile": "https://github.com/fistonhn",
"contributions": [
"test"
]
},
{
"login": "raffaeleferri",
"name": "Raffaele Ferri",
"avatar_url": "https://avatars0.githubusercontent.com/u/75796924?v=4",
"profile": "https://github.com/raffaeleferri",
"contributions": [
"maintenance"
]
},
{
"login": "joshwambere",
"name": "Dusabe Johnson",
"avatar_url": "https://avatars2.githubusercontent.com/u/59834399?v=4",
"profile": "https://github.com/joshwambere",
"contributions": [
"test"
]
},
{
"login": "tomasvn",
"name": "tomasvn",
"avatar_url": "https://avatars.githubusercontent.com/u/17225564?v=4",
"profile": "https://github.com/tomasvn",
"contributions": [
"code"
]
},
{
"login": "lucasvribeiro",
"name": "Lucas Ribeiro",
"avatar_url": "https://avatars.githubusercontent.com/u/12684816?v=4",
"profile": "http://www.lucasribeiro.dev",
"contributions": [
"code",
"test"
]
},
{
"login": "Bartek532",
"name": "Bartosz Zagrodzki",
"avatar_url": "https://avatars.githubusercontent.com/u/57185551?v=4",
"profile": "http://bartek532.github.io/portfolio",
"contributions": [
"code"
]
},
{
"login": "mookkiah",
"name": "Mahendran Mookkiah",
"avatar_url": "https://avatars.githubusercontent.com/u/8975264?v=4",
"profile": "https://www.linkedin.com/in/mookkiah/",
"contributions": [
"code"
]
},
{
"login": "hkhattabii",
"name": "hkhattabii",
"avatar_url": "https://avatars.githubusercontent.com/u/54418529?v=4",
"profile": "https://github.com/hkhattabii",
"contributions": [
"code"
]
},
{
"login": "Federico-Pomponii",
"name": "Federico Pomponii",
"avatar_url": "https://avatars.githubusercontent.com/u/6978411?v=4",
"profile": "https://github.com/Federico-Pomponii",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true
}
================================================
FILE: .dockerignore
================================================
node_modules
dist
.git
.vscode
!src
!public
!docs
!config/*
!package.json
!package-lock.json
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .eslintrc.js
================================================
/**
* ESLint Configuration
*/
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
modules: true,
},
},
extends: [
'plugin:react/recommended',
'plugin:import/typescript',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:prettier/recommended',
'prettier/react',
],
plugins: ['import'],
rules: {
// Separate import groups with newline by section
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'unknown'],
'newlines-between': 'always',
},
],
'no-console': 1, // Warning to reduce console logs used throughout app
'react/prop-types': 0, // Not using prop-types because we have TypeScript
'newline-before-return': 1,
'no-useless-return': 1,
'prefer-const': 1,
'no-useless-return': 1,
'no-unused-vars': 0,
},
settings: {
'import/resolver': {
// Allow `@/` to map to `src/client/`
alias: {
map: [
['@', './src/client'],
['@resources', './src/resources'],
],
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
},
},
react: {
version: 'detect',
},
},
}
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: '[Bug]'
labels: 'Type: Bug'
assignees: ''
---
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Actual behavior**
What actually happened.
**Notes**
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: '[Feature]'
labels: 'Type: Feature'
assignees: ''
---
**Problem**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Solution**
A clear and concise description of what you want to happen.
**Notes**
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Description
Please read the [Contribution Guidelines](../CONTRIBUTING.md) before opening a pull request. Include a summary of the change and which issue is fixed.
Closes # <-- link the issue number here
### Browser checklist
This PR has been tested in the following browsers:
- [ ] Chrome
- [ ] Firefox
- [ ] Safari
### Testing checklist
- [ ] End-to-end tests have been created if necessary
================================================
FILE: .gitignore
================================================
/node_modules
/coverage
/dist
/cypress
.idea
.DS_Store
.env
.vscode
npm-debug.log*
.coveralls.yml
**/*/videos
tests/e2e/videos
================================================
FILE: .prettierrc
================================================
{
"bracketSpacing": true,
"printWidth": 100,
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5"
}
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- '12'
addons:
apt:
packages:
# Ubuntu 16+ does not install this dependency by default, so we need to install it ourselves
- libgconf-2-4
- snapd
cache:
# Caches $HOME/.npm when npm ci is default script command
# Caches node_modules in all other cases
npm: false
# directories:
# we also need to cache folder with Cypress binary
# - ~/.cache
services:
# Use Docker command line
- docker
install:
# Install dependencies for tests
- echo "MTY1LjIyNy42MC4zMyBlY2RzYS1zaGEyLW5pc3RwMjU2IEFBQUFFMlZqWkhOaExYTm9ZVEl0Ym1semRIQXlOVFlBQUFBSWJtbHpkSEF5TlRZQUFBQkJCTVVPRlVVT3BxSzNmWkMzUUxJNmsrL2Vlc1l5YVVaNGZXbkRUaWNia1pjMmJIR1ltMG4wVk9RaW5mK0NYY2xhWmZTaVBNQ0xZakJUUzkrUWxWSFpPZ009" | base64 -d >> $HOME/.ssh/known_hosts
- sudo snap install doctl
- npm ci
before_script:
# Start server and client for tests
- echo -e "CLIENT_ID=abc\nDEMO=true" > .env
- npm run client &
script:
# Run unit, component, and e2e tests
- npm run test:coverage:ci && npm run test:e2e
# Commenting out the deploy phase as the demo is static and no longer requires a server
# So Travis is currently only being used for running tests, not deploying
# deploy:
# # Build Docker container and push to Docker Hub
# # Pull into DigitalOcean container and start
# provider: script
# script: bash deploy.sh
# on:
# branch: master
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
## v0.7.2 10/27/2020
Refactoring.
- Add SonarQube via SonarCloud (https://sonarcloud.io/dashboard?id=taniarascia_takenote)
- Fix bugs and code smells
- Fix #422 /app redirect
- Add demo environment variable
- Add compression
- Remove prettier and associated massive Webpack bundle
## v0.7.1 10/25/2020
Add GitHub integration.
### Changed
- Add code for integrating GitHub sync (https://github.com/taniarascia/takenote/pull/389)
- Create demo mode for takenote.dev deployment
- Add tests for selectors
- Hide note list in Scratchpad view
- Localized dates
- Various bug fixes
- Additional settings
## v0.6.1 10/19/2020
Add top menu bar.
### Changed
- Add top menu bar for preview, sync, settings, and other note options
- Update settings UI
- Add sync test back
- Organize end-to-end tests
## v0.6.0 10/16/2020
Upgrade to webpack 5.
### Changed
- Updated all packages, notable webpack 4 to webpack 5
- Updated webpack config to reflect breaking changes
- Manually brought in polyfills for Node packages that are no longer polyfilled by webpack
- Moved settings to bottom of app sidebar
- Removed sync button
- Removed unnecessary patches
## v0.5.0 02/22/2020
GitHub authentication.
### Added
- Log in/log out functionality implemented using GitHub OAuth
### Changed
- Refactored large files into smaller components
- Added folder structure and technologies to README
- Modify deployment scripts and Dockerfile to allow local development with GitHub authentication
- Prompt to confirm exit added when notes have not yet been synced
## v0.4.0 02/03/2020
Initial release.
### Added
- Created `CHANGELOG.md`
- Added Node/TypeScript backend for REST API calls
- Created CI/CD pipeline with `deploy.sh`, `.travis.yml` and `Dockerfile`
### Changed
- Migrated website from Netlify to DigitalOcean
- Added instructions for new local development to README
- Removed Netlify badge from README
- Removed Create React App and replaced with custom Webpack setup
### Removed
- Removed Service Worker due to the application no longer being fully client side
================================================
FILE: CONTRIBUTING.md
================================================
# Contribution Guidelines
TakeNote 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.
## Issues
If 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.
## Pull Requests
In 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.
## Folder Structure
Description of the project files and directories.
```bash
├── config/ # Configuration
│ ├── cypress.config.js # Cypress end-to-end test configuration
│ ├── jest.config.js # Jest unit/component test configuration
│ ├── nodemon.config.json # Nodemon configuration
│ ├── webpack.common.js # Webpack shared configuration
│ ├── webpack.dev.js # Webpack development configuration (dev server)
│ └── webpack.prod.js # Webpack productuon configuration (dist output)
├── assets/ # Supplemental assets
├── public/ # Files that will write to dist on build
├── src/ # All TakeNote app source files
│ ├── resources/ # Shared resources
│ ├── client/ # React client side code
│ │ ├── api/ # Temporary placeholders for mock API calls
│ │ ├── components/ # React components that are not connected to Redux
│ │ ├── containers/ # React Redux connected containers
│ │ ├── contexts/ # React context global state without Redux
│ │ ├── router/ # React private and public routes
│ │ ├── sagas/ # Redux sagas
│ │ ├── selectors/ # Redux Toolkit selectors
│ │ ├── slices/ # Redux Toolkit slices
│ │ ├── styles/ # Sass style files
│ │ ├── types/ # TypeScript types
│ │ ├── utils/ # Utility functions
│ │ └── index.tsx # Client side entry point
│ └── server/ # Node/Express server side code
│ ├── handlers/ # Functions for API endpoints
│ ├── middleware/ # Middleware for API endpoints
│ ├── router/ # Route API endpoints
│ ├── utils/ # Backend utilities
│ └── index.ts # Server entrypoint
├── tests/ # Test suites
│ ├── e2e/ # Cypress end-to-end tests
│ └── unit/ # React Testing Library and Jest tests
├── .dockerignore # Files ignored by Docker
├── .editorconfig # Configures editor rules
├── .gitignore # Files ignored by git
├── .prettierrc # Code convention enforced by Prettier
├── .travis.yml # Continuous integration and deployment config
├── CHANGELOG.md # List of significant changes
├── deploy.sh # Deployment script for Docker in production
├── Dockerfile # Docker build instructions
├── LICENSE # License for this open source project
├── package-lock.json # Package lockfile
├── package.json # Dependencies and additional information
├── README.md
├── seed.js # Seed the app with data for testing
└── tsconfig.json # Typescript configuration
```
## Scripts
An explanation of the `package.json` scripts.
| Command | Description |
| --------------- | ------------------------------------------- |
| `dev` | Run TakeNote in a development environment |
| `dev:test` | Run TakeNote in a testing environment |
| `client` | Start a webpack dev server for the frontend |
| `server` | Start a nodemon dev server for the backend |
| `build` | Create a production build of TakeNote |
| `start` | Start a production server for TakeNote |
| `test` | Run unit and component tests |
| `test:e2e` | Run end-to-end tests in the command line |
| `test:e2e:open` | Open end-to-end tests in a browser |
| `test:coverage` | Get test coverage |
## Technologies
This project is possible thanks to all these open source languages, libraries, and frameworks.
| Tech | Description |
| --------------------------------------------- | ----------------------------------------- |
| [Codemirror](https://codemirror.net/) | Browser-based text editor |
| [TypeScript](https://www.typescriptlang.org/) | Static type-checking programming language |
| [Node.js](https://nodejs.org/en/) | JavaScript runtime for the backend |
| [Express](https://expressjs.com/) | Server framework |
| [React](https://reactjs.org/) | Front end user interface |
| [Redux](https://redux.js.org/) | Global state management |
| [Webpack](https://webpack.js.org/) | Asset bundler |
| [Sass](https://sass-lang.com/) | Style preprocessor |
| [OAuth](https://oauth.net/) | Protocol for secure authorization |
| [ESLint](https://eslint.org/) | TypeScript linting |
| [Jest](https://jestjs.io/) | Unit testing framework |
| [Cypress](https://www.cypress.io/) | End-to-end testing framework |
## Styleguide
Coding conventions are enforced by [ESLint](.eslintrc.js) and [Prettier](.prettierrc).
- No semicolons
- Single quotes
- Two space indentation
- Trailing commas in arrays and objects
- [Non-default exports](https://humanwhocodes.com/blog/2019/01/stop-using-default-exports-javascript-module/) are preferred for components
- Module imports are ordered and separated: **built-in** -> **external** -> **internal** -> **css/assets/other**
- TypeScript: strict mode, with no implicitly any
- React: functional style with Hooks (no classes)
- `const` preferred over `let`
================================================
FILE: Dockerfile
================================================
# Use small Alpine Linux image
FROM node:12-alpine
# Set environment variables
ENV PORT=5000
ARG CLIENT_ID
COPY . app/
WORKDIR app/
# Make sure dependencies exist for Webpack loaders
RUN apk add --no-cache \
autoconf \
automake \
bash \
g++ \
libc6-compat \
libjpeg-turbo-dev \
libpng-dev \
make \
nasm
RUN npm ci --only-production --silent
# Build production client side React application
RUN npm run build
# Expose port for Node
EXPOSE $PORT
# Start Node server
ENTRYPOINT npm run prod
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2020 Tania Rascia
Permission 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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE 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.
================================================
FILE: README.md
================================================
<p align="center">
<img src="./assets/logo.png">
</p>
<p align="center">
<img src="https://img.shields.io/badge/License-MIT-blue.svg">
<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>
<a href='https://coveralls.io/github/taniarascia/takenote'><img src='https://coveralls.io/repos/github/taniarascia/takenote/badge.svg' alt='Coverage Status' /></a>
</p>
<p align="center">
<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>
<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>
<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>
</p>
<p align="center">A web-based notes app for developers. (Demo only)</p>

## Features
- **Plain text notes** - take notes in an IDE-like environment that makes no assumptions
- **Markdown preview** - view rendered HTML
- **Linked notes** - use `{{uuid}}` syntax to link to notes within other notes
- **Syntax highlighting** - light and dark mode available (based on the beautiful [New Moon theme](https://taniarascia.github.io/new-moon/))
- **Keyboard shortcuts** - use the keyboard for all common tasks - creating notes and categories, toggling settings, and other options
- **Drag and drop** - drag a note or multiple notes to categories, favorites, or trash
- **Multi-cursor editing** - supports multiple cursors and other [Codemirror](https://codemirror.net/) options
- **Search notes** - easily search all notes, or notes within a category
- **Prettify notes** - use Prettier on the fly for your Markdown
- **No WYSIWYG** - made for developers, by developers
- **No database** - notes are only stored in the browser's local storage and are available for download and export to you alone
- **No tracking or analytics** - 'nuff said
- **GitHub integration** - self-hosted option is available for auto-syncing to a GitHub repository (not available in the demo)
## About
TakeNote 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.
Hidden 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:
- I do not want to maintain a free app with users alongside my career and other commitments
- I do not want to request private repository permissions from users
- I do not want to maintain an active server
- I do not want to worry about GitHub rate limiting from the server
- There is no way to batch create many files from the GitHub API, leading to a suboptimal GitHub storage solution
However, 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.
TakeNote was created with TypeScript, React, Redux, Node, Express, Codemirror, Webpack, Jest, Cypress, Feather Icons, ESLint, and Mousetrap, among other awesome open-source software.
## Reviews
> _"I think the lack of extra crap is a feature."_ — Craig Lam
## Demo Development
Clone and install.
```bash
git clone git@github.com:taniarascia/takenote
cd takenote
npm i
```
Run a development server.
```bash
npm run client
```
## Full Application Development (self-hosted)
### Pre-Installation
Before working on TakeNote locally, you must create a GitHub OAuth app for development.
Go to your GitHub profile settings, and click on **Developer Settings**.
Click the **New OAuth App** button.
- **Application name**: TakeNote Development
- **Homepage URL**: `http://localhost:3000`
- **Authorization callback URL**: `http://localhost:3000/api/auth/callback`
Create 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.
```bash
CLIENT_ID=xxx
CLIENT_SECRET=xxxx
DEMO=true
```
> Change the URLs to port `5000` in production mode or Docker.
### Installation
```bash
git clone git@github.com:taniarascia/takenote
cd takenote
npm i
```
#### Development mode
In 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.
```bash
npm run dev
```
Go to `localhost:3000` to view the app.
> API requests will be proxied to port `5000` automatically.
#### Production mode
In the production environment, the React app is built, and Express redirects all incoming requests to the `dist` directory on port `5000`.
```bash
npm run build && npm run start
```
Go to `localhost:5000` to view the app.
#### Run in Docker
Follow these instructions to build an image and run a container.
```bash
# Build Docker image
docker build --build-arg CLIENT_ID=xxx -t takenote:mytag .
# Run Docker container in port 5000
docker run \
-e CLIENT_ID=xxx \
-e CLIENT_SECRET=xxxx \
-e NODE_ENV=development \
-p 5000:5000 \
takenote:mytag
```
Go to `localhost:5000` to view the app.
> 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.
### Seed data
To seed the app with some test data, paste the contents of `seed.js` into your browser console.
## Testing
Run unit and component/integration tests.
```bash
npm run test
```
> If using Jest Runner in VSCode, add `"jestrunner.configPath": "config/jest.config.js"` to your settings
Run Cypress end-to-end tests.
```bash
# In one window, run the application
npm run client
# In another window, run the end-to-end tests
npm run test:e2e:open
```
## Contributing
TakeNote 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.
View [CONTRIBUTING.md](CONTRIBUTING.md) to learn about the style guide, folder structure, scripts, and how to contribute.
## Contributors
Thanks goes to these wonderful people:
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</tr>
<tr>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</tr>
<tr>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</tr>
<tr>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</tr>
<tr>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</tr>
<tr>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</tr>
<tr>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</tr>
<tr>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</tr>
<tr>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</tr>
<tr>
<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>
<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>
<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>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
## Acknowledgements
- A big thank you to [David Bock](https://dkbock.com/) for logo design.
## Author
- [Tania Rascia](https://www.taniarascia.com)
## License
This project is open source and available under the [MIT License](LICENSE).
================================================
FILE: config/cypress.config.json
================================================
{
"baseUrl": "http://localhost:3000",
"integrationFolder": "tests/e2e/integration",
"pluginsFile": "tests/e2e/plugins/index.js",
"supportFile": "tests/e2e/support/index.js",
"fixturesFolder": "tests/e2e/fixtures",
"videosFolder": "tests/e2e/videos",
"video": false
}
================================================
FILE: config/jest.config.js
================================================
module.exports = {
// Setting the root to the actual root, since this file is in root/config
preset: 'ts-jest',
rootDir: '../',
roots: ['<rootDir>/src', '<rootDir>/tests/unit'],
transform: {
'^.+\\.tsx?$': 'ts-jest',
'\\.(html|xml|txt|md)$': 'jest-raw-loader',
},
setupFilesAfterEnv: ['@testing-library/jest-dom', 'jest-extended'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
moduleNameMapper: {
// Allow `@/` to map to `src/client/` in Jest tests
'@/(.*)$': '<rootDir>/src/client/$1',
'@resources/(.*)$': '<rootDir>/src/resources/$1',
'\\.(css|less)$': '<rootDir>/tests/__mocks__/styleMock.ts',
},
globals: {
'ts-jest': {
diagnostics: false,
},
},
}
================================================
FILE: config/nodemon.config.json
================================================
{
"ignore": [
".git",
".eslintrc",
"node_modules/**",
"src/client/**",
"test/**",
"public/**",
"*.test.js",
"webpack.*.js"
],
"watch": ["src/server/"],
"ext": "ts",
"exec": "cross-env NODE_ENV=development node --inspect -r ts-node/register ./src/server/index.ts"
}
================================================
FILE: config/webpack.common.js
================================================
const path = require('path')
const dotenv = require('dotenv')
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
/**
* Obtain client id for OAuth link in React
*
* If in development mode or local production mode, search the .env file for
* client id. If using Docker, pass a build arg.
*/
const getEnvFromDotEnvFile = dotenv.config()
let envKeys
if (getEnvFromDotEnvFile.error) {
console.log('Getting environment variables from build args for production') // eslint-disable-line
envKeys = {
'process.env.CLIENT_ID': JSON.stringify(process.env.CLIENT_ID),
'process.env.DEMO': JSON.stringify(process.env.DEMO),
'process.env.NODE_ENV': JSON.stringify('production'),
}
} else {
envKeys = {
'process.env.CLIENT_ID': JSON.stringify(getEnvFromDotEnvFile.parsed['CLIENT_ID']),
'process.env.DEMO': JSON.stringify(getEnvFromDotEnvFile.parsed['DEMO']),
}
}
module.exports = {
entry: ['./src/client/index.tsx'],
output: {
path: path.resolve(__dirname, '../dist'),
filename: '[name].[fullhash].bundle.js',
publicPath: '/',
},
module: {
rules: [
/**
* TypeScript (.ts/.tsx files)
*
* The TypeScript loader will compile all .ts/.tsx files to .js. Babel is
* not necessary here since TypeScript is taking care of all transpiling.
*/
{
test: /\.ts(x?)$/,
loader: 'ts-loader',
exclude: /node_modules/,
},
// Fonts
{
test: /\.(woff(2)?|eot|ttf|otf)$/,
type: 'asset/inline',
},
// Markdown
{
test: /\.md$/,
type: 'asset/source',
},
// Images
{
test: /\.(?:ico|gif|png|jpg|jpeg|webp|svg)$/i,
type: 'asset/resource',
},
],
},
resolve: {
// Resolve in this order
extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.md'],
// Allow `@/` to map to `src/client/`
alias: {
'@': path.resolve(__dirname, '../src/client'),
'@resources': path.resolve(__dirname, '../src/resources'),
stream: 'stream-browserify',
path: 'path-browserify',
},
},
plugins: [
// Get environment variables in React
new webpack.DefinePlugin(envKeys),
new CleanWebpackPlugin(),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'),
globOptions: {
ignore: ['*.DS_Store', 'favicon.ico', 'template.html'],
},
},
],
}),
new webpack.ProvidePlugin({
process: 'process/browser',
}),
],
}
================================================
FILE: config/webpack.dev.js
================================================
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
module: {
rules: [
// Styles
{
test: /\.(scss|css)$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: { sourceMap: true, importLoaders: 1 },
},
{ loader: 'sass-loader', options: { sourceMap: true } },
],
},
],
},
devServer: {
historyApiFallback: true,
proxy: {
'/api': 'http://localhost:5000',
},
open: true,
compress: true,
hot: true,
port: 3000,
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
template: './public/template.html',
favicon: './public/favicon.ico',
}),
],
})
================================================
FILE: config/webpack.prod.js
================================================
const webpack = require('webpack')
const { merge } = require('webpack-merge')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const common = require('./webpack.common.js')
// Disable React DevTools in production
const disableReactDevtools = `
<script>
if (typeof window.__REACT_DEVTOOLS_GLOBAL_HOOK__ === 'object') {
__REACT_DEVTOOLS_GLOBAL_HOOK__.inject = function() {};
}
</script>
`
module.exports = merge(common, {
mode: 'production',
devtool: false,
module: {
rules: [
// Styles
{
test: /\.(scss|css)$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 1,
},
},
'sass-loader',
],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'styles/[name].[contenthash].css',
chunkFilename: 'styles/[name].[id].[contenthash].css',
ignoreOrder: false,
}),
new HtmlWebpackPlugin({
template: './public/template.html',
favicon: './public/favicon.ico',
hash: true,
disableReactDevtools,
}),
new webpack.SourceMapDevToolPlugin({
exclude: ['/node_modules/'],
}),
],
performance: {
hints: 'warning',
maxEntrypointSize: 512000,
maxAssetSize: 512000,
},
optimization: {
minimizer: [new CssMinimizerPlugin(), '...'],
runtimeChunk: 'multiple',
splitChunks: {
// Cache vendors since this code won't change very often
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/](react|react-dom|axios|redux|react-redux)[\\/]/,
name: 'vendors',
chunks: 'all',
enforce: true,
},
},
},
},
})
================================================
FILE: deploy.sh
================================================
#!/bin/sh
# Stop script from running if there are any errors
set -e
# Docker image
IMAGE="taniarascia/takenote"
# Git version with git hash and tags (if they exist) to be used as Docker tag
GIT_VERSION=$(git describe --always --abbrev --tags --long)
# Build and tag new Docker image and push up to Docker Hub
echo "Building and tagging new Docker image: ${IMAGE}:${GIT_VERSION}"
docker build --build-arg DEMO=true CLIENT_ID=${CLIENT_ID} -t ${IMAGE}:${GIT_VERSION} .
docker tag ${IMAGE}:${GIT_VERSION} ${IMAGE}:latest
# Login to Docker Hub and push newest build
echo "Logging into Docker and pushing ${IMAGE}:${GIT_VERSION}"
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
docker push ${IMAGE}:${GIT_VERSION}
docker push ${IMAGE}:latest
# Login to DigitalOcean command line
echo "Authorizing DigitalOcean"
doctl auth init -t "${DO_ACCESS_TOKEN}"
# Decode SSH key
echo ${DO_SSH_KEY} | base64 -d > deploy_key
chmod 600 deploy_key
# Log into Droplet, stop the currently running container and start the new one
echo "Stopping container name current and starting ${IMAGE}:${GIT_VERSION}"
doctl compute ssh ${DROPLET} --ssh-key-path deploy_key --ssh-command "docker pull ${IMAGE}:${GIT_VERSION} &&
docker stop current &&
docker rm current &&
docker 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} &&
docker system prune -a -f &&
docker image prune -a -f"
================================================
FILE: kubernetes.yml
================================================
apiVersion: v1
data:
client_id: // base64 encoded string
client_secret: // base64 encoded string
kind: Secret
metadata:
name: takenote-pwd
labels:
app: takenote
component: server
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: takenote
labels:
app: takenote
component: server
spec:
replicas: 1
selector:
matchLabels:
app: takenote
component: server
template:
metadata:
labels:
app: takenote
component: server
spec:
containers:
- image: <your_alias/takenote:latest>
name: takenote
env:
- name: CLIENT_ID
valueFrom:
secretKeyRef:
name: takenote-pwd
key: client_id
- name: CLIENT_SECRET
valueFrom:
secretKeyRef:
name: takenote-pwd
key: client_secret
- name: NODE_ENV
value: development
ports:
- containerPort: 5000
name: web
---
apiVersion: v1
kind: Service
metadata:
name: takenote
labels:
app: takenote
component: server
spec:
ports:
- protocol: TCP
port: 8080
targetPort: 5000
name: web
selector:
app: takenote
component: server
type: ClusterIP
================================================
FILE: package.json
================================================
{
"name": "takenote",
"version": "0.7.2",
"description": "A web-based notes app for developers.",
"author": "Tania Rascia",
"license": "MIT",
"private": false,
"main": "src/server/index.ts",
"scripts": {
"dev": "concurrently \"npm run server\" \"npm run client\"",
"client": "cross-env NODE_ENV=development webpack serve --config config/webpack.dev.js",
"server": "nodemon --config config/nodemon.config.json",
"build": "cross-env NODE_ENV=production webpack --config config/webpack.prod.js",
"prod": "node -r ts-node/register/transpile-only src/server/index.ts",
"start": "npm run client",
"test": "jest --config config/jest.config.js",
"test:e2e": "cypress run --config-file config/cypress.config.json",
"test:e2e:open": "cypress open --config-file config/cypress.config.json",
"test:coverage": "jest --config config/jest.config.js --coverage --watchAll=false",
"test:coverage:ci": "jest --config config/jest.config.js --ci --coverage --watchAll=false && cat ./coverage/lcov.info | coveralls",
"format": "prettier --write \"./**/*.{js,jsx,ts,tsx,css,scss,md}\"",
"eslint": "eslint src/**/*.{ts,tsx}"
},
"repository": {
"type": "git",
"url": "git+https://github.com/taniarascia/takenote"
},
"keywords": [
"notes",
"notes-app",
"note-taking",
"markdown",
"markdown-editor",
"redux",
"react",
"typescript",
"react-hooks",
"react-hooks-redux",
"github"
],
"bugs": {
"url": "https://github.com/taniarascia/takenote/issues"
},
"homepage": "https://takenote.dev",
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": [
"eslint --fix"
],
"**/*.{json,css,scss,md}": [
"prettier --write"
]
},
"dependencies": {
"@reduxjs/toolkit": "^1.4.0",
"axios": "^0.21.1",
"clipboard-polyfill": "^3.0.1",
"codemirror": "^5.58.1",
"compression": "^1.7.4",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"dayjs": "^1.9.3",
"express": "^4.17.1",
"helmet": "^4.1.1",
"jszip": "^3.5.0",
"mousetrap": "^1.6.5",
"mousetrap-global-bind": "^1.1.0",
"path-browserify": "^1.0.1",
"prettier": "^2.1.2",
"process": "^0.11.10",
"react": "^16.14.0",
"react-beautiful-dnd": "^13.0.0",
"react-codemirror2": "^7.2.1",
"react-device-detect": "^1.14.0",
"react-dom": "^16.14.0",
"react-feather": "^2.0.8",
"react-helmet-async": "^1.0.7",
"react-markdown": "^4.3.1",
"react-redux": "^7.2.1",
"react-router-dom": "^5.2.0",
"react-split-pane": "^0.1.92",
"redux": "^4.0.5",
"redux-saga": "^1.1.3",
"stream-browserify": "^3.0.0",
"unist-util-visit": "^2.0.3",
"uuid": "^8.3.1"
},
"devDependencies": {
"@cypress/webpack-preprocessor": "^5.4.8",
"@testing-library/cypress": "^7.0.1",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@types/axios": "^0.14.0",
"@types/codemirror": "0.0.98",
"@types/compression": "^1.7.0",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.8",
"@types/express": "^4.17.8",
"@types/faker": "^5.1.2",
"@types/helmet": "0.0.48",
"@types/jest": "^26.0.14",
"@types/jszip": "^3.4.1",
"@types/lodash": "^4.14.162",
"@types/node": "^14.11.8",
"@types/prettier": "^2.1.3",
"@types/react": "^16.9.52",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "^16.9.8",
"@types/react-helmet-async": "^1.0.3",
"@types/react-redux": "^7.1.9",
"@types/react-router": "^5.1.8",
"@types/react-router-dom": "^5.1.6",
"@types/testing-library__cypress": "^5.0.8",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^4.4.1",
"@typescript-eslint/parser": "^4.4.1",
"clean-webpack-plugin": "^3.0.0",
"clipboardy": "^2.3.0",
"concurrently": "^5.3.0",
"copy-webpack-plugin": "^6.2.1",
"coveralls": "^3.1.0",
"cross-env": "^7.0.2",
"css-loader": "^5.0.0",
"css-minimizer-webpack-plugin": "^1.1.5",
"cypress": "^5.4.0",
"cypress-file-upload": "^4.1.1",
"dotenv": "^8.2.0",
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.13.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-react": "^7.21.4",
"faker": "^5.1.0",
"html-webpack-plugin": "^5.0.0-alpha.7",
"husky": "^4.3.0",
"image-webpack-loader": "^7.0.1",
"jest": "^26.5.3",
"jest-extended": "^0.11.5",
"jest-raw-loader": "^1.0.1",
"lint-staged": "^10.4.1",
"mini-css-extract-plugin": "^1.0.0",
"node-sass": "^4.14.1",
"nodemon": "^2.0.5",
"sass-loader": "^10.0.3",
"style-loader": "^2.0.0",
"ts-jest": "^26.4.1",
"ts-loader": "^8.0.5",
"ts-node": "^9.0.0",
"typescript": "^4.0.3",
"webpack": "^5.1.3",
"webpack-cli": "^4.0.0",
"webpack-dev-server": "^3.11.0",
"webpack-merge": "^5.2.0"
}
}
================================================
FILE: public/_redirects
================================================
/* /index.html 200
================================================
FILE: public/manifest.json
================================================
{
"short_name": "TakeNote",
"name": "A web-based notes app for developers.",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#5183f5",
"background_color": "#ffffff"
}
================================================
FILE: public/robots.txt
================================================
User-agent: *
================================================
FILE: public/template.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>TakeNote</title>
</head>
<body>
<%= htmlWebpackPlugin.options.disableReactDevtools %>
<div id="root"></div>
<div id="context-menu"></div>
</body>
</html>
================================================
FILE: seed.js
================================================
const categories = [
{
id: 'goals',
name: 'goals',
},
{
id: 'health',
name: 'health',
},
{
id: 'design',
name: 'design',
},
{
id: 'development',
name: 'development',
},
{
id: 'personal',
name: 'personal',
},
{
id: 'recipes',
name: 'recipes',
},
]
const notes = [
{
id: 'e0196fd9-d644-4ca8-aa58-467b8082993e',
text:
'## 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 |',
created: '2019-10-14T17:03:27-05:00',
lastUpdated: '2019-10-14T17:03:27-05:00',
category: '',
favorite: false,
},
{
id: '6a2923a6-8fed-4277-9286-49125c91d876',
text:
"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.",
created: '2019-10-14T17:01:39-05:00',
lastUpdated: '2019-10-14T17:01:49-05:00',
category: 'development',
favorite: false,
},
{
id: 'a4c5ea72-34ed-496d-aa90-096df7e1ffbd',
text:
"# 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```",
created: '2019-10-14T17:00:31-05:00',
lastUpdated: '2019-10-14T17:00:55-05:00',
category: '',
favorite: false,
trash: true,
},
{
id: '645fdc64-8511-469d-a2db-c7f04a36a9af',
text:
"# 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 ",
created: '2019-10-14T16:58:09-05:00',
lastUpdated: '2019-10-14T17:01:53-05:00',
category: 'development',
favorite: false,
},
{
id: 'b2808149-a40f-4f3c-83bc-94db34881241',
text:
"# 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",
created: '2019-10-14T16:57:24-05:00',
lastUpdated: '2019-10-14T16:57:42-05:00',
category: '',
favorite: true,
},
{
id: 'fa23b58e-2c2e-4c67-b6cd-4f7817ba7e89',
text:
"# 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}",
created: '2019-10-14T16:54:08-05:00',
lastUpdated: '2019-10-14T16:57:48-05:00',
category: '',
favorite: false,
},
]
localStorage.setItem('categories', JSON.stringify(categories))
localStorage.setItem('notes', JSON.stringify(notes))
window.location.reload()
================================================
FILE: src/client/api/index.ts
================================================
import { v4 as uuid } from 'uuid'
import dayjs from 'dayjs'
import { NoteItem, SyncPayload, SettingsState } from '@/types'
const scratchpadNote = {
id: uuid(),
text: `# Scratchpad
The easiest note to find.`,
category: '',
scratchpad: true,
favorite: false,
created: dayjs().format(),
lastUpdated: dayjs().format(),
}
const markdown = `# Welcome to Takenote!
TakeNote 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.
View the source on [Github](https://github.com/taniarascia/takenote).
## Features
- **Plain text notes** - take notes in an IDE-like environment that makes no assumptions
- **Markdown preview** - view rendered HTML
- **Linked notes** - use \`{{uuid}}\` syntax to link to notes within other notes
- **Syntax highlighting** - light and dark mode available (based on the beautiful [New Moon theme](https://taniarascia.github.io/new-moon/))
- **Keyboard shortcuts** - use the keyboard for all common tasks - creating notes and categories, toggling settings, and other options
- **Drag and drop** - drag a note or multiple notes to categories, favorites, or trash
- **Multi-cursor editing** - supports multiple cursors and other [Codemirror](https://codemirror.net/) options
- **Search notes** - easily search all notes, or notes within a category
- **Prettify notes** - use Prettier on the fly for your Markdown
- **No WYSIWYG** - made for developers, by developers
- **No database** - notes are only stored in the browser's local storage and are available for download and export to you alone
- **No tracking or analytics** - 'nuff said
- **GitHub integration** - self-hosted option is available for auto-syncing to a GitHub repository (not available in the demo)
`
const welcomeNote = {
id: uuid(),
text: markdown,
category: '',
favorite: false,
created: dayjs().format(),
lastUpdated: dayjs().format(),
}
type PromiseCallback = (value?: any) => void
type GetLocalStorage = (
key: string,
errorMessage?: string
) => (resolve: PromiseCallback, reject: PromiseCallback) => void
const getLocalStorage: GetLocalStorage = (key, errorMessage = 'Something went wrong') => (
resolve,
reject
) => {
const data = localStorage.getItem(key)
if (data) {
resolve(JSON.parse(data))
} else {
reject({
message: errorMessage,
})
}
}
const getUserNotes = () => (resolve: PromiseCallback, reject: PromiseCallback) => {
const notes: any = localStorage.getItem('notes')
// check if there is any data in localstorage
if (!notes) {
// if there is none (i.e. new user), create the welcomeNote and scratchpadNote
resolve([scratchpadNote, welcomeNote])
} else if (Array.isArray(JSON.parse(notes))) {
// if there is (existing user), show the user's notes
resolve(
// find does not work if the array is empty.
JSON.parse(notes).length === 0 || !JSON.parse(notes).find((note: NoteItem) => note.scratchpad)
? [scratchpadNote, ...JSON.parse(notes)]
: JSON.parse(notes)
)
} else {
reject({
message: 'Something went wrong',
})
}
}
export const saveState = ({ categories, notes }: SyncPayload) =>
new Promise((resolve) => {
localStorage.setItem('categories', JSON.stringify(categories))
localStorage.setItem('notes', JSON.stringify(notes))
resolve({
categories: JSON.parse(localStorage.getItem('categories') || '[]'),
notes: JSON.parse(localStorage.getItem('notes') || '[]'),
})
})
export const saveSettings = ({ isOpen, ...settings }: SettingsState) =>
Promise.resolve(localStorage.setItem('settings', JSON.stringify(settings)))
export const requestNotes = () => new Promise(getUserNotes())
export const requestCategories = () => new Promise(getLocalStorage('categories'))
export const requestSettings = () => new Promise(getLocalStorage('settings'))
================================================
FILE: src/client/components/AppSidebar/ActionButton.tsx
================================================
import React, { MouseEventHandler } from 'react'
import { Icon } from 'react-feather'
import { iconColor } from '@/utils/constants'
export interface ActionButtonProps {
dataTestID: string
disabled?: boolean
handler: MouseEventHandler
icon: Icon
label: string
text: string
}
export const ActionButton: React.FC<ActionButtonProps> = ({
dataTestID,
disabled = false,
handler,
icon: IconCmp,
label,
text,
}) => {
return (
<button
data-testid={dataTestID}
className="action-button"
aria-label={label}
onClick={handler}
disabled={disabled}
title={label}
>
<IconCmp
size={18}
className="action-button-icon"
color={iconColor}
aria-hidden="true"
focusable="false"
/>
<span>{text}</span>
</button>
)
}
================================================
FILE: src/client/components/AppSidebar/AddCategoryButton.tsx
================================================
import React from 'react'
import { Plus } from 'react-feather'
import { iconColor } from '@/utils/constants'
export interface AddCategoryButtonProps {
dataTestID: string
handler: (adding: boolean) => void
label: string
}
export const AddCategoryButton: React.FC<AddCategoryButtonProps> = ({
dataTestID,
handler,
label,
}) => {
return (
<button
data-testid={dataTestID}
className="category-button"
onClick={() => handler(true)}
aria-label={label}
>
<Plus size={16} color={iconColor} />
</button>
)
}
================================================
FILE: src/client/components/AppSidebar/AddCategoryForm.tsx
================================================
import React from 'react'
import { TestID } from '@resources/TestID'
import { ReactSubmitEvent } from '@/types'
export interface AddCategoryFormProps {
dataTestID: string
submitHandler: (event: ReactSubmitEvent) => void
changeHandler: (editingCategoryId: string, value: string) => void
resetHandler: () => void
editingCategoryId: string
tempCategoryName: string
}
export const AddCategoryForm: React.FC<AddCategoryFormProps> = ({
dataTestID,
submitHandler,
changeHandler,
resetHandler,
editingCategoryId,
tempCategoryName,
}) => {
return (
<form data-testid={dataTestID} className="category-form" onSubmit={submitHandler}>
<input
data-testid={TestID.NEW_CATEGORY_INPUT}
aria-label="Category name"
type="text"
autoFocus
maxLength={20}
placeholder="New category..."
onChange={(event) => {
changeHandler(editingCategoryId, event.target.value)
}}
onBlur={(event) => {
if (!tempCategoryName || tempCategoryName.trim() === '') {
resetHandler()
} else {
submitHandler(event)
}
}}
/>
</form>
)
}
================================================
FILE: src/client/components/AppSidebar/CollapseCategoryButton.tsx
================================================
import React from 'react'
import { ChevronDown, ChevronRight, Layers } from 'react-feather'
export interface CollapseCategoryListButton {
dataTestID: string
handler: () => void
label: string
isCategoryListOpen: boolean
showIcon: boolean
}
export const CollapseCategoryListButton: React.FC<CollapseCategoryListButton> = ({
dataTestID,
handler,
label,
isCategoryListOpen,
showIcon,
}) => {
return (
<button
data-testid={dataTestID}
className="collapse-button"
onClick={handler}
aria-label={label}
>
{showIcon ? (
isCategoryListOpen ? (
<ChevronDown size={16} />
) : (
<ChevronRight size={16} />
)
) : (
<Layers size={16} />
)}
<h2>Categories</h2>
</button>
)
}
================================================
FILE: src/client/components/AppSidebar/FolderOption.tsx
================================================
import React, { useState } from 'react'
import { Book, Star, Trash2 } from 'react-feather'
import { Folder } from '@/utils/enums'
import { iconColor } from '@/utils/constants'
import { ReactDragEvent } from '@/types'
export interface FolderOptionProps {
text: string
active: boolean
dataTestID: string
folder: Folder
swapFolder: (folder: Folder) => void
addNoteType: (noteId: string) => void
}
export const FolderOption: React.FC<FolderOptionProps> = ({
text,
active,
dataTestID,
folder,
swapFolder,
addNoteType,
}) => {
const [mainSectionDragState, setMainSectionDragState] = useState({
[Folder.ALL]: false,
[Folder.FAVORITES]: false,
[Folder.SCRATCHPAD]: false,
[Folder.TRASH]: false,
[Folder.CATEGORY]: false,
})
const dragEnterHandler = () => {
setMainSectionDragState({ ...mainSectionDragState, [folder]: true })
}
const dragLeaveHandler = () => {
setMainSectionDragState({ ...mainSectionDragState, [folder]: false })
}
const noteHandler = (event: ReactDragEvent) => {
event.preventDefault()
addNoteType(event.dataTransfer.getData('text'))
dragLeaveHandler()
}
const determineClass = () => {
if (active) {
return 'app-sidebar-link active'
} else if (mainSectionDragState[folder]) {
return 'app-sidebar-link dragged-over'
} else {
return 'app-sidebar-link'
}
}
const renderIcon = () => {
if (folder === 'FAVORITES') {
return <Star size={15} className="app-sidebar-icon" color={iconColor} />
} else if (folder === 'ALL') {
return <Book size={15} className="app-sidebar-icon" color={iconColor} />
} else {
return <Trash2 size={15} className="app-sidebar-icon" color={iconColor} />
}
}
return (
<button
onClick={() => {
swapFolder(folder)
}}
className="app-sidebar-wrapper"
>
<div
data-testid={dataTestID}
className={determineClass()}
onDrop={noteHandler}
onDragOver={(event: ReactDragEvent) => {
event.preventDefault()
}}
onDragEnter={dragEnterHandler}
onDragLeave={dragLeaveHandler}
>
{renderIcon()}
{text}
</div>
</button>
)
}
================================================
FILE: src/client/components/AppSidebar/ScratchpadOption.tsx
================================================
import React from 'react'
import { Edit } from 'react-feather'
import { TestID } from '@resources/TestID'
import { LabelText } from '@resources/LabelText'
import { Folder } from '@/utils/enums'
import { iconColor } from '@/utils/constants'
export interface ScratchpadOptionProps {
active: boolean
swapFolder: (folder: Folder) => {}
}
export const ScratchpadOption: React.FC<ScratchpadOptionProps> = ({ active, swapFolder }) => {
return (
<button
onClick={() => {
swapFolder(Folder.SCRATCHPAD)
}}
className="app-sidebar-wrapper"
>
<div data-testid={TestID.SCRATCHPAD} className={`app-sidebar-link ${active ? 'active' : ''}`}>
<Edit size={15} className="app-sidebar-icon" color={iconColor} />
{LabelText.SCRATCHPAD}
</div>
</button>
)
}
================================================
FILE: src/client/components/Editor/EmptyEditor.tsx
================================================
import React from 'react'
export const EmptyEditor: React.FC = () => {
return (
<div className="empty-editor v-center" data-testid="empty-editor">
<div className="text-center">
<p>
<strong>Create a note</strong>
</p>
<p>
<kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>N</kbd>
</p>
</div>
</div>
)
}
================================================
FILE: src/client/components/Editor/NoteLink.tsx
================================================
import React from 'react'
import { NoteItem } from '@/types'
import { Errors } from '@/utils/enums'
import { TestID } from '@resources/TestID'
import { getNoteTitle, getActiveNoteFromShortUuid } from '../../utils/helpers'
export interface NoteLinkProps {
uuid: string
notes: NoteItem[]
handleNoteLinkClick: (e: React.SyntheticEvent, note: NoteItem) => void
}
const NoteLink: React.FC<NoteLinkProps> = ({ notes, uuid, handleNoteLinkClick }) => {
const note = getActiveNoteFromShortUuid(notes, uuid)
const title = note !== undefined ? getNoteTitle(note.text) : null
if (note && title)
return (
<a data-testid={TestID.NOTE_LINK_SUCCESS} onClick={(e) => handleNoteLinkClick(e, note)}>
{title}
</a>
)
return (
<span data-testid={TestID.NOTE_LINK_ERROR} className="error">
{Errors.INVALID_LINKED_NOTE_ID}
</span>
)
}
export default NoteLink
================================================
FILE: src/client/components/Editor/PreviewEditor.tsx
================================================
import React from 'react'
import ReactMarkdown from 'react-markdown'
import { useDispatch } from 'react-redux'
import { Folder } from '@/utils/enums'
import { updateActiveNote, updateSelectedNotes, pruneNotes, swapFolder } from '@/slices/note'
import { NoteItem } from '@/types'
import { uuidPlugin } from '../../utils/reactMarkdownPlugins'
import NoteLink from './NoteLink'
export interface PreviewEditorProps {
noteText: string
directionText: string
notes: NoteItem[]
}
export const PreviewEditor: React.FC<PreviewEditorProps> = ({ noteText, directionText, notes }) => {
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _updateSelectedNotes = (noteId: string, multiSelect: boolean) =>
dispatch(updateSelectedNotes({ noteId, multiSelect }))
const _updateActiveNote = (noteId: string, multiSelect: boolean) =>
dispatch(updateActiveNote({ noteId, multiSelect }))
const _pruneNotes = () => dispatch(pruneNotes())
const _swapFolder = (folder: Folder) => dispatch(swapFolder({ folder }))
// ===========================================================================
// Handlers
// ===========================================================================
const handleNoteLinkClick = (e: React.SyntheticEvent, note: NoteItem) => {
e.preventDefault()
if (note) {
_updateActiveNote(note.id, false)
_updateSelectedNotes(note.id, false)
_pruneNotes()
if (note?.favorite) return _swapFolder(Folder.FAVORITES)
if (note?.scratchpad) return _swapFolder(Folder.SCRATCHPAD)
if (note?.trash) return _swapFolder(Folder.TRASH)
return _swapFolder(Folder.ALL)
}
}
const returnNoteLink = (value: string) => {
return <NoteLink uuid={value} notes={notes} handleNoteLinkClick={handleNoteLinkClick} />
}
return (
<ReactMarkdown
plugins={[uuidPlugin]}
renderers={{
uuid: ({ value }) => returnNoteLink(value),
}}
linkTarget="_blank"
className={`previewer previewer_direction-${directionText}`}
source={noteText}
/>
)
}
================================================
FILE: src/client/components/LandingPage.tsx
================================================
import React from 'react'
import { isMobile } from 'react-device-detect'
import lightScreen from '@resources/assets/screenshot-light.png'
import darkScreen from '@resources/assets/screenshot-dark.png'
import squareLogo from '@resources/assets/logo-square-white.svg'
import logo from '@resources/assets/logo-square-color.svg'
import githubLogo from '@resources/assets/github-logo.png'
const clientId = process.env.CLIENT_ID
const isDemo = process.env.DEMO
const loginButton = (text: string) => (
<a
className="button github-button"
href={`https://github.com/login/oauth/authorize?client_id=${clientId}&scope=repo`}
>
<img src={githubLogo} />
{text}
</a>
)
export const LandingPage: React.FC = () => {
return (
<section className="landing-page">
<section className="content">
<div className="container-small">
<div className="lead">
<img src={logo} height="200" width="200" alt="TakeNote" />
<h1>
The Note Taking App
<br /> for Developers
</h1>
<p className="subtitle">A web-based notes app for developers.</p>
{isMobile ? (
<p className="p-mobile">
TakeNote is not currently supported for tablet and mobile devices.
</p>
) : isDemo ? (
<div className="new-signup">
<div>
<p>
TakeNote is only available as a demo. Your notes will be saved to local storage
and <b>not</b> persisted in any database or cloud.
</p>
<a className="button" href="/app">
View Demo
</a>
</div>
</div>
) : (
<div className="new-signup">
<div>
<p>
TakeNote does not have a database or users. It simply links with your GitHub
account for authentication, and stores the data in a private{' '}
<code>takenotes-data</code> repo.
</p>
<div className="cta">{loginButton('Sign Up with GitHub')}</div>
</div>
</div>
)}
</div>
</div>
<div className="container">
<img src={lightScreen} alt="TakeNote App" className="screenshot" />
</div>
</section>
<section className="content">
<div className="container-small">
<div className="features">
<h2 className="text-center">Features</h2>
<ul>
<li>
<strong>Plain text notes</strong> - take notes in an IDE-like environment that makes
no assumptions
</li>
<li>
<strong>Markdown preview</strong> - view rendered HTML
</li>
<li>
<strong>Linked notes</strong> - use <code>{`{{uuid}}`}</code> syntax to link to
notes within other notes
</li>
<li>
<strong>Syntax highlighting</strong> - light and dark mode available (based on the
beautiful <a href="https://taniarascia.github.io/new-moon/">New Moon theme</a>)
</li>
<li>
<strong>Keyboard shortcuts</strong> - use the keyboard for all common tasks -
creating notes and categories, toggling settings, and other options
</li>
<li>
<strong>Drag and drop</strong> - drag a note or multiple notes to categories,
favorites, or trash
</li>
<li>
<strong>Multi-cursor editing</strong> - supports multiple cursors and other{' '}
<a href="https://codemirror.net/">Codemirror</a> options
</li>
<li>
<strong>Search notes</strong> - easily search all notes, or notes within a category
</li>
<li>
<strong>Prettify notes</strong> - use Prettier on the fly for your Markdown
</li>
<li>
<strong>No WYSIWYG</strong> - made for developers, by developers
</li>
<li>
<strong>No database</strong> - notes are only stored in the browser's local
storage and are available for download and export to you alone
</li>
<li>
<strong>No tracking or analytics</strong> - 'nuff said
</li>
<li>
<strong>GitHub integration</strong> - self-hosted option is available for
auto-syncing to a GitHub repository (not available in the demo)
</li>
</ul>
</div>
</div>
<div className="container">
<img src={darkScreen} alt="TakeNote App" className="screenshot" />
</div>
</section>
<footer className="footer">
<div className="container-small">
<img src={squareLogo} alt="TakeNote App" className="logo" />
<p>
<strong>TakeNote</strong>
</p>
<nav>
<a
href="https://github.com/taniarascia/takenote"
target="_blank"
rel="noopener noreferrer"
>
Source
</a>
<a
href="https://github.com/taniarascia/takenote/issues"
target="_blank"
rel="noopener noreferrer"
>
Issues
</a>
<a
href="https://github.com/taniarascia/takenote/graphs/contributors"
target="_blank"
rel="noopener noreferrer"
>
Contributors
</a>
</nav>
</div>
</footer>
</section>
)
}
================================================
FILE: src/client/components/LastSyncedNotification.tsx
================================================
import React from 'react'
import dayjs from 'dayjs'
import { TestID } from '@resources/TestID'
export interface LastSyncedNotificationProps {
datetime: string
pending: boolean
syncing: boolean
}
export const LastSyncedNotification: React.FC<LastSyncedNotificationProps> = ({
datetime,
pending,
syncing,
}) => {
const renderLastSynced = () => {
if (syncing) {
return <i data-testid={TestID.LAST_SYNCED_NOTIFICATION_SYNCING}>Syncing...</i>
}
if (pending) {
return <i data-testid={TestID.LAST_SYNCED_NOTIFICATION_UNSAVED}>Unsaved changes</i>
}
if (datetime) {
return (
<span data-testid={TestID.LAST_SYNCED_NOTIFICATION_DATE}>
{dayjs(datetime).format('LT on L')}
</span>
)
}
}
return <div className="last-synced">{renderLastSynced()}</div>
}
================================================
FILE: src/client/components/NoteList/ContextMenuOption.tsx
================================================
import React, { KeyboardEventHandler, MouseEventHandler, useContext } from 'react'
import { Icon } from 'react-feather'
import { MenuUtilitiesContext } from '@/containers/ContextMenu'
export interface ContextMenuOptionProps {
dataTestID: string
handler: MouseEventHandler & KeyboardEventHandler
icon: Icon
text: string
optionType?: string
}
export const ContextMenuOption: React.FC<ContextMenuOptionProps> = ({
dataTestID,
handler,
optionType,
icon: IconCmp,
text,
...rest
}) => {
// ===========================================================================
// Context
// ===========================================================================
const { setOptionsId } = useContext(MenuUtilitiesContext)
// ===========================================================================
// Handlers
// ===========================================================================
const optionHandler: MouseEventHandler & KeyboardEventHandler = (
event: React.MouseEvent<Element, MouseEvent> & React.KeyboardEvent<Element>
) => {
handler(event)
setOptionsId('')
}
return (
<div
data-testid={dataTestID}
className={optionType === 'delete' ? 'nav-item delete-option' : 'nav-item'}
role="button"
onClick={optionHandler}
onKeyPress={optionHandler}
tabIndex={0}
{...rest}
>
<IconCmp className="nav-item-icon" size={18} />
{text}
</div>
)
}
================================================
FILE: src/client/components/NoteList/NoteListButton.tsx
================================================
import React, { MouseEventHandler } from 'react'
export interface NoteListButtonProps {
dataTestID: string
disabled?: boolean
handler: MouseEventHandler
label: string
}
export const NoteListButton: React.FC<NoteListButtonProps> = ({
dataTestID,
disabled = false,
handler,
label,
}) => {
return (
<button
data-testid={dataTestID}
className="list-button"
aria-label={label}
onClick={handler}
disabled={disabled}
title={label}
>
{label}
</button>
)
}
================================================
FILE: src/client/components/NoteList/SearchBar.tsx
================================================
import React from 'react'
import { TestID } from '@resources/TestID'
export interface SearchBarProps {
searchRef: React.MutableRefObject<HTMLInputElement>
searchNotes: (searchValue: string) => void
}
export const SearchBar: React.FC<SearchBarProps> = ({ searchRef, searchNotes }) => {
return (
<input
ref={searchRef}
data-testid={TestID.NOTE_SEARCH}
type="search"
onChange={(event) => {
event.preventDefault()
searchNotes(event.target.value)
}}
placeholder="Search for notes"
onDragOver={(e) => {
e.preventDefault()
}}
/>
)
}
================================================
FILE: src/client/components/NoteList/SelectCategory.tsx
================================================
import React from 'react'
import { TestID } from '@resources/TestID'
import { NoteItem, CategoryItem } from '@/types'
import { isDraftNote } from '@/utils/helpers'
export interface SelectCategoryProps {
onChange: (selectedOption: any) => void
categories: CategoryItem[]
note: NoteItem
activeCategoryId: string
}
export const SelectCategory: React.FC<SelectCategoryProps> = ({
onChange,
categories,
note,
activeCategoryId,
}) => {
const filteredCategories = categories
.filter(({ id }) => id !== activeCategoryId)
.filter((category) => category.id !== note.category)
return (
<>
{!note.trash && !isDraftNote(note) && filteredCategories.length > 0 && (
<select
data-testid={TestID.MOVE_TO_CATEGORY}
defaultValue=""
className="nav-item move-to-category-select"
onChange={onChange}
>
<option disabled value="">
Move to category...
</option>
{filteredCategories
.filter((category) => category.id !== note.category)
.map((category) => (
<option key={category.id} value={category.id}>
{category.name}
</option>
))}
</select>
)}
</>
)
}
================================================
FILE: src/client/components/Select.tsx
================================================
import React from 'react'
export interface SelectOption {
label: string
value: string
}
export interface SelectProps {
options: SelectOption[]
onChange: (selectedOption: SelectOption) => void
selectedValue: string
testId: string
}
export const Select: React.FC<SelectProps> = ({ options, selectedValue, onChange, testId }) => {
const getSelectedOption = (options: SelectOption[], value: string): SelectOption => {
return options.filter((option: SelectOption) => {
return value === option.value
})[0]
}
return (
<select
onChange={(event) => onChange(getSelectedOption(options, event.target.value))}
value={selectedValue}
data-testid={testId}
>
{options.map((selectOption) => (
<option key={selectOption.value} value={selectOption.value}>
{selectOption.label}
</option>
))}
</select>
)
}
================================================
FILE: src/client/components/SettingsModal/IconButton.tsx
================================================
import React, { MouseEventHandler } from 'react'
import { Icon } from 'react-feather'
import { iconColor } from '@/utils/constants'
export interface IconButtonProps {
dataTestID?: string
disabled?: boolean
handler: MouseEventHandler
icon: Icon
text: string
}
export const IconButton: React.FC<IconButtonProps> = ({
dataTestID,
disabled = false,
handler,
icon: IconCmp,
text,
}) => {
return (
<button
data-testid={dataTestID}
aria-label={text}
onClick={handler}
disabled={disabled}
title={text}
className="icon-button"
>
<IconCmp
size={18}
className="button-icon"
color={iconColor}
aria-hidden="true"
focusable="false"
/>
{text}
</button>
)
}
================================================
FILE: src/client/components/SettingsModal/IconButtonUploader.tsx
================================================
import React, { ChangeEvent, useRef } from 'react'
import { Icon } from 'react-feather'
import { iconColor } from '@/utils/constants'
export interface IconButtonUploaderProps {
dataTestID?: string
disabled?: boolean
handler: (file: File) => void
icon: Icon
text: string
accept: string
}
export const IconButtonUploader: React.FC<IconButtonUploaderProps> = ({
dataTestID,
disabled = false,
handler,
icon: IconCmp,
text,
accept,
}) => {
const inputRef = useRef<HTMLInputElement>(null)
const handleClick = () => {
if (inputRef.current) {
inputRef.current.click()
}
}
const handleFileInput = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
handler(e.target.files[0])
}
}
return (
<div>
<input
data-testid={dataTestID}
accept={accept}
tabIndex={-1}
autoComplete="off"
ref={inputRef}
type="file"
onChange={handleFileInput}
className="hidden"
/>
<button
onClick={handleClick}
aria-label={text}
disabled={disabled}
title={text}
className="icon-button"
>
<IconCmp
size={18}
className="button-icon"
color={iconColor}
aria-hidden="true"
focusable="false"
/>
{text}
</button>
</div>
)
}
================================================
FILE: src/client/components/SettingsModal/Option.tsx
================================================
import React from 'react'
import { Switch } from '@/components/Switch'
export interface OptionProps {
title: string
description: string
toggle: () => void
checked: boolean
testId: string
}
export const Option: React.FC<OptionProps> = ({ title, description, toggle, checked, testId }) => {
return (
<div className="settings-option">
<div>
<h3>{title}</h3>
<p className="description">{description}</p>
</div>
<Switch toggle={toggle} checked={checked} testId={testId} />
</div>
)
}
================================================
FILE: src/client/components/SettingsModal/SelectOptions.tsx
================================================
import React from 'react'
import { Select } from '../Select'
export interface OptionProps {
title: string
description: string
onChange: (selectedOption: any) => void
selectedValue: string
options: Array<{ value: string; label: string }>
testId: string
}
export const SelectOptions: React.FC<OptionProps> = ({
title,
description,
onChange,
selectedValue,
options,
testId,
}) => {
return (
<div className="settings-option">
<div>
<h3>{title}</h3>
<p className="description">{description}</p>
</div>
<Select options={options} onChange={onChange} selectedValue={selectedValue} testId={testId} />
</div>
)
}
================================================
FILE: src/client/components/SettingsModal/Shortcut.tsx
================================================
import React from 'react'
export interface ShortcutProps {
action: string
letter: string
}
export const Shortcut: React.FC<ShortcutProps> = ({ action, letter }) => {
return (
<div className="settings-shortcut">
<div>{action}</div>
<div className="keys">
<kbd>CTRL</kbd> + <kbd>ALT</kbd> + <kbd>{letter}</kbd>
</div>
</div>
)
}
================================================
FILE: src/client/components/Switch.tsx
================================================
import React from 'react'
export interface SwitchProps {
toggle: () => void
checked: boolean
testId: string
}
export const Switch: React.FC<SwitchProps> = ({ toggle, checked, testId }) => {
return (
<label className="switch" data-testid={testId}>
<input type="checkbox" onChange={toggle} checked={checked} />
<span className="slider" />
</label>
)
}
================================================
FILE: src/client/components/Tabs/Tab.tsx
================================================
import React from 'react'
import { Icon } from 'react-feather'
export interface TabProps {
label: string
activeTab: string
onClick: (label: string) => void
icon: Icon
}
export const Tab: React.FC<TabProps> = ({ activeTab, label, icon: IconCmp, onClick }) => {
const className = activeTab === label ? 'tab active' : 'tab'
return (
<div role="button" key={label} className={className} onClick={() => onClick(label)}>
<IconCmp size={18} className="mr-1" aria-hidden="true" focusable="false" /> {label}
</div>
)
}
================================================
FILE: src/client/components/Tabs/TabPanel.tsx
================================================
import React from 'react'
import { Icon } from 'react-feather'
export interface TabPanelProps {
label: string
icon: Icon
children: JSX.Element[] | JSX.Element
}
export const TabPanel: React.FC<TabPanelProps> = ({ children }) => {
return <section>{children}</section>
}
================================================
FILE: src/client/components/Tabs/Tabs.tsx
================================================
import React, { Fragment, useState } from 'react'
import { Tab } from './Tab'
export interface TabsProps {
children: JSX.Element[]
}
export const Tabs: React.FC<TabsProps> = ({ children }) => {
const [activeTab, setActiveTab] = useState('Preferences')
return (
<div className="tabs">
<nav className="tab-list">
{children.map((child) => {
const { label, icon } = child.props
return (
<Tab
icon={icon}
activeTab={activeTab}
key={label}
label={label}
onClick={setActiveTab}
/>
)
})}
</nav>
<div className="tab-content">
{children.map((child) => {
if (child.props.label !== activeTab) return
return (
<Fragment key={`${child.props.label}-panel`}>
<h3>{child.props.label}</h3>
{child.props.children}
</Fragment>
)
})}
</div>
</div>
)
}
================================================
FILE: src/client/containers/App.tsx
================================================
import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Route, Switch, Redirect } from 'react-router-dom'
import { Helmet, HelmetProvider } from 'react-helmet-async'
import { LandingPage } from '@/components/LandingPage'
import { TakeNoteApp } from '@/containers/TakeNoteApp'
import { PublicRoute } from '@/router/PublicRoute'
import { PrivateRoute } from '@/router/PrivateRoute'
import { getAuth } from '@/selectors'
import { login } from '@/slices/auth'
const isDemo = process.env.DEMO
export const App: React.FC = () => {
// ===========================================================================
// Selectors
// ===========================================================================
const { loading } = useSelector(getAuth)
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _login = () => dispatch(login())
// ===========================================================================
// Hooks
// ===========================================================================
useEffect(() => {
_login()
}, [])
if (loading) {
return (
<div className="loading">
<div className="la-ball-beat">
<div />
<div />
<div />
</div>
</div>
)
}
return (
<HelmetProvider>
<Helmet>
<meta charSet="utf-8" />
<title>TakeNote</title>
<link rel="canonical" href="https://takenote.dev" />
</Helmet>
<Switch>
{isDemo ? (
<>
<Route exact path="/" component={LandingPage} />
<Route path="/app" component={TakeNoteApp} />
</>
) : (
<>
<PublicRoute exact path="/" component={LandingPage} />
<PrivateRoute path="/app" component={TakeNoteApp} />
</>
)}
<Redirect to="/" />
</Switch>
</HelmetProvider>
)
}
================================================
FILE: src/client/containers/AppSidebar.tsx
================================================
import React from 'react'
import { Plus } from 'react-feather'
import { useDispatch, useSelector } from 'react-redux'
import { LabelText } from '@resources/LabelText'
import { TestID } from '@resources/TestID'
import { ActionButton } from '@/components/AppSidebar/ActionButton'
import { FolderOption } from '@/components/AppSidebar/FolderOption'
import { ScratchpadOption } from '@/components/AppSidebar/ScratchpadOption'
import { Folder, NotesSortKey } from '@/utils/enums'
import { CategoryList } from '@/containers/CategoryList'
import {
addNote,
swapFolder,
updateActiveNote,
assignFavoriteToNotes,
assignTrashToNotes,
updateSelectedNotes,
unassignTrashFromNotes,
} from '@/slices/note'
import { togglePreviewMarkdown } from '@/slices/settings'
import { getSettings, getNotes } from '@/selectors'
import { NoteItem } from '@/types'
import { newNoteHandlerHelper, getActiveNote } from '@/utils/helpers'
export const AppSidebar: React.FC = () => {
// ===========================================================================
// Selectors
// ===========================================================================
const { activeCategoryId, activeFolder, activeNoteId, notes } = useSelector(getNotes)
const { previewMarkdown, notesSortKey } = useSelector(getSettings)
const activeNote = getActiveNote(notes, activeNoteId)
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _addNote = (note: NoteItem) => dispatch(addNote(note))
const _updateActiveNote = (noteId: string, multiSelect: boolean) =>
dispatch(updateActiveNote({ noteId, multiSelect }))
const _updateSelectedNotes = (noteId: string, multiSelect: boolean) =>
dispatch(updateSelectedNotes({ noteId, multiSelect }))
const _swapFolder = (sortOrderKey: NotesSortKey) => (folder: Folder) =>
dispatch(swapFolder({ folder, sortOrderKey }))
const _togglePreviewMarkdown = () => dispatch(togglePreviewMarkdown())
const _assignTrashToNotes = (noteId: string) => dispatch(assignTrashToNotes(noteId))
const _unassignTrashFromNotes = (noteId: string) => dispatch(unassignTrashFromNotes(noteId))
const _assignFavoriteToNotes = (noteId: string) => dispatch(assignFavoriteToNotes(noteId))
// ===========================================================================
// Handlers
// ===========================================================================
const newNoteHandler = () =>
newNoteHandlerHelper(
activeFolder,
previewMarkdown,
activeNote,
activeCategoryId,
swapFolderHandler,
_togglePreviewMarkdown,
_addNote,
_updateActiveNote,
_updateSelectedNotes
)
const swapFolderHandler = _swapFolder(notesSortKey)
return (
<aside className="app-sidebar">
<ActionButton
dataTestID={TestID.SIDEBAR_ACTION_CREATE_NEW_NOTE}
handler={newNoteHandler}
icon={Plus}
label={LabelText.CREATE_NEW_NOTE}
text={LabelText.NEW_NOTE}
/>
<section className="app-sidebar-main">
<ScratchpadOption
active={activeFolder === Folder.SCRATCHPAD}
swapFolder={swapFolderHandler}
/>
<FolderOption
active={activeFolder === Folder.ALL}
swapFolder={swapFolderHandler}
text={LabelText.NOTES}
dataTestID={TestID.FOLDER_NOTES}
folder={Folder.ALL}
addNoteType={_unassignTrashFromNotes}
/>
<FolderOption
active={activeFolder === Folder.FAVORITES}
text={LabelText.FAVORITES}
dataTestID={TestID.FOLDER_FAVORITES}
folder={Folder.FAVORITES}
swapFolder={swapFolderHandler}
addNoteType={_assignFavoriteToNotes}
/>
<FolderOption
active={activeFolder === Folder.TRASH}
text={LabelText.TRASH}
dataTestID={TestID.FOLDER_TRASH}
folder={Folder.TRASH}
swapFolder={swapFolderHandler}
addNoteType={_assignTrashToNotes}
/>
<CategoryList />
</section>
</aside>
)
}
================================================
FILE: src/client/containers/CategoryList.tsx
================================================
import React, { useRef, useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { v4 as uuid } from 'uuid'
import { Droppable } from 'react-beautiful-dnd'
import { LabelText } from '@resources/LabelText'
import { TestID } from '@resources/TestID'
import { CategoryOption } from '@/containers/CategoryOption'
import { getCategories } from '@/selectors'
import { shouldOpenContextMenu } from '@/utils/helpers'
import { ReactMouseEvent, ReactSubmitEvent, CategoryItem } from '@/types'
import { useTempState } from '@/contexts/TempStateContext'
import { setCategoryEdit, updateCategory, addCategory } from '@/slices/category'
import { AddCategoryForm } from '@/components/AppSidebar/AddCategoryForm'
import { AddCategoryButton } from '@/components/AppSidebar/AddCategoryButton'
import { CollapseCategoryListButton } from '@/components/AppSidebar/CollapseCategoryButton'
export const CategoryList: React.FC = () => {
// ===========================================================================
// Selectors
// ===========================================================================
const {
categories,
editingCategory: { id: editingCategoryId, tempName: tempCategoryName },
} = useSelector(getCategories)
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _setCategoryEdit = (categoryId: string, tempName: string) =>
dispatch(setCategoryEdit({ id: categoryId, tempName }))
const _updateCategory = (category: CategoryItem) => dispatch(updateCategory(category))
const _addCategory = (category: CategoryItem) => dispatch(addCategory(category))
// ===========================================================================
// Refs
// ===========================================================================
const contextMenuRef = useRef<HTMLDivElement>(null)
// ===========================================================================
// State
// ===========================================================================
const [optionsId, setOptionsId] = useState('')
const [optionsPosition, setOptionsPosition] = useState({ x: 0, y: 0 })
const [isCategoryListOpen, setCategoryListOpen] = useState(true)
// ===========================================================================
// Context
// ===========================================================================
const { addingTempCategory, setAddingTempCategory } = useTempState()
// ===========================================================================
// Handlers
// ===========================================================================
const onAddCategory = (adding: boolean) => {
setCategoryListOpen(true)
setAddingTempCategory(adding)
}
const handleCategoryMenuClick = (
event: React.MouseEvent<HTMLDivElement, MouseEvent> | ReactMouseEvent,
categoryId: string = ''
) => {
const clicked = event.target
// Make sure we aren't getting any null values .. any element clicked should be a sub-class of element
if (!clicked) return
if (shouldOpenContextMenu(clicked as Element)) {
if ('clientX' in event && 'clientY' in event) {
setOptionsPosition({ x: event.clientX, y: event.clientY })
}
}
event.stopPropagation()
if (!contextMenuRef?.current?.contains(clicked as HTMLDivElement)) {
setOptionsId(!optionsId || optionsId !== categoryId ? categoryId : '')
}
}
const handleCategoryRightClick = (
event: React.MouseEvent<HTMLDivElement, MouseEvent> | ReactMouseEvent,
categoryId: string = ''
) => {
event.preventDefault()
handleCategoryMenuClick(event, categoryId)
}
const resetTempCategory = () => {
setAddingTempCategory(false)
_setCategoryEdit('', '')
}
const onSubmitUpdateCategory = (event: ReactSubmitEvent): void => {
event.preventDefault()
const category = { id: editingCategoryId, name: tempCategoryName.trim(), draggedOver: false }
if (categories.find((cat) => cat.name === category.name) || category.name === '') {
resetTempCategory()
} else {
_updateCategory(category)
resetTempCategory()
}
}
const onSubmitNewCategory = (event: ReactSubmitEvent): void => {
event.preventDefault()
const category = { id: uuid(), name: tempCategoryName.trim(), draggedOver: false }
if (categories.find((cat) => cat.name === category.name) || category.name === '') {
resetTempCategory()
} else {
_addCategory(category)
resetTempCategory()
}
}
// ===========================================================================
// Hooks
// ===========================================================================
useEffect(() => {
document.addEventListener('mousedown', handleCategoryMenuClick)
return () => {
document.removeEventListener('mousedown', handleCategoryMenuClick)
}
})
return (
<>
<div className="category-title">
<CollapseCategoryListButton
dataTestID={TestID.CATEGORY_COLLAPSE_BUTTON}
handler={() => setCategoryListOpen(!isCategoryListOpen)}
label={LabelText.COLLAPSE_CATEGORY}
isCategoryListOpen={isCategoryListOpen}
showIcon={categories.length > 0}
/>
<AddCategoryButton
dataTestID={TestID.ADD_CATEGORY_BUTTON}
handler={onAddCategory}
label={LabelText.ADD_CATEGORY}
/>
</div>
{isCategoryListOpen && (
<>
<Droppable type="CATEGORY" droppableId="Category list">
{(droppableProvided) => (
<div
{...droppableProvided.droppableProps}
ref={droppableProvided.innerRef}
className="category-list"
aria-label="Category list"
>
{categories.map((category, index) => (
<CategoryOption
key={category.id}
index={index}
category={category}
contextMenuRef={contextMenuRef}
handleCategoryMenuClick={handleCategoryMenuClick}
handleCategoryRightClick={handleCategoryRightClick}
onSubmitUpdateCategory={onSubmitUpdateCategory}
optionsId={optionsId}
setOptionsId={setOptionsId}
optionsPosition={optionsPosition}
/>
))}
{droppableProvided.placeholder}
</div>
)}
</Droppable>
{addingTempCategory && (
<AddCategoryForm
dataTestID={TestID.NEW_CATEGORY_FORM}
submitHandler={onSubmitNewCategory}
changeHandler={_setCategoryEdit}
resetHandler={resetTempCategory}
editingCategoryId={editingCategoryId}
tempCategoryName={tempCategoryName}
/>
)}
</>
)}
</>
)
}
================================================
FILE: src/client/containers/CategoryOption.tsx
================================================
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { Draggable } from 'react-beautiful-dnd'
import { Folder as FolderIcon, MoreHorizontal } from 'react-feather'
import { TestID } from '@resources/TestID'
import { CategoryItem, ReactDragEvent, ReactMouseEvent, ReactSubmitEvent } from '@/types'
import { determineCategoryClass } from '@/utils/helpers'
import { getNotes, getCategories, getSettings } from '@/selectors'
import {
updateActiveCategoryId,
updateActiveNote,
updateSelectedNotes,
addCategoryToNote,
} from '@/slices/note'
import { setCategoryEdit, categoryDragLeave, categoryDragEnter } from '@/slices/category'
import { iconColor } from '@/utils/constants'
import { ContextMenuEnum } from '@/utils/enums'
import { getNotesSorter } from '@/utils/notesSortStrategies'
import { ContextMenu } from '@/containers/ContextMenu'
interface CategoryOptionProps {
category: CategoryItem
index: number
contextMenuRef: React.RefObject<HTMLDivElement>
handleCategoryMenuClick: (
event: React.MouseEvent<HTMLDivElement, MouseEvent> | ReactMouseEvent,
categoryId?: string
) => void
handleCategoryRightClick: (
event: React.MouseEvent<HTMLDivElement, MouseEvent> | ReactMouseEvent,
categoryId?: string
) => void
onSubmitUpdateCategory: (event: ReactSubmitEvent) => void
optionsPosition: { x: number; y: number }
optionsId: string
setOptionsId: React.Dispatch<React.SetStateAction<string>>
}
export const CategoryOption: React.FC<CategoryOptionProps> = ({
category,
index,
contextMenuRef,
handleCategoryMenuClick,
handleCategoryRightClick,
onSubmitUpdateCategory,
optionsPosition,
optionsId,
setOptionsId,
}) => {
// ===========================================================================
// Selectors
// ===========================================================================
const { activeCategoryId, notes } = useSelector(getNotes)
const {
editingCategory: { id: editingCategoryId, tempName: tempCategoryName },
} = useSelector(getCategories)
const { notesSortKey } = useSelector(getSettings)
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _updateActiveCategoryId = (categoryId: string) =>
dispatch(updateActiveCategoryId(categoryId))
const _updateActiveNote = (noteId: string, multiSelect: boolean) =>
dispatch(updateActiveNote({ noteId, multiSelect }))
const _updateSelectedNotes = (noteId: string, multiSelect: boolean) =>
dispatch(updateSelectedNotes({ noteId, multiSelect }))
const _setCategoryEdit = (categoryId: string, tempName: string) =>
dispatch(setCategoryEdit({ id: categoryId, tempName }))
const _addCategoryToNote = (categoryId: string, noteId: string) =>
dispatch(addCategoryToNote({ categoryId, noteId }))
const _categoryDragEnter = (category: CategoryItem) => dispatch(categoryDragEnter(category))
const _categoryDragLeave = (category: CategoryItem) => dispatch(categoryDragLeave(category))
return (
<Draggable draggableId={category.id} index={index}>
{(draggableProvided, snapshot) => (
<div
{...draggableProvided.dragHandleProps}
{...draggableProvided.draggableProps}
ref={draggableProvided.innerRef}
data-testid={TestID.CATEGORY_LIST_DIV}
className={determineCategoryClass(category, snapshot.isDragging, activeCategoryId)}
onClick={() => {
const notesForNewCategory = notes
.filter((note) => !note.trash && note.category === category.id)
.sort(getNotesSorter(notesSortKey))
const defaultActiveNoteId =
notesForNewCategory.length > 0 ? notesForNewCategory[0].id : ''
if (category.id !== activeCategoryId) {
_updateActiveCategoryId(category.id)
_updateActiveNote(defaultActiveNoteId, false)
_updateSelectedNotes(defaultActiveNoteId, false)
}
}}
onDoubleClick={() => {
_setCategoryEdit(category.id, category.name)
}}
onBlur={() => {
_setCategoryEdit('', '')
}}
onDrop={(event) => {
event.preventDefault()
_addCategoryToNote(category.id, event.dataTransfer.getData('text'))
_categoryDragLeave(category)
}}
onDragOver={(event: ReactDragEvent) => event.preventDefault()}
onDragEnter={() => _categoryDragEnter(category)}
onDragLeave={() => _categoryDragLeave(category)}
onContextMenu={(event) => handleCategoryRightClick(event, category.id)}
>
<form
className="category-list-name"
onSubmit={(event) => {
event.preventDefault()
_setCategoryEdit('', '')
onSubmitUpdateCategory(event)
if (optionsId) setOptionsId('')
}}
>
<FolderIcon size={15} className="app-sidebar-icon" color={iconColor} />
{editingCategoryId === category.id ? (
<input
data-testid={TestID.CATEGORY_EDIT}
className="category-edit"
type="text"
autoFocus
maxLength={20}
value={tempCategoryName}
onChange={(event) => {
_setCategoryEdit(editingCategoryId, event.target.value)
}}
onBlur={(event) => onSubmitUpdateCategory(event)}
/>
) : (
category.name
)}
</form>
<div
data-testid={TestID.MOVE_CATEGORY}
className={optionsId === category.id ? 'category-options active' : 'category-options'}
onClick={(event) => handleCategoryMenuClick(event, category.id)}
>
<MoreHorizontal size={15} className="context-menu-action" />
</div>
{optionsId === category.id && (
<ContextMenu
contextMenuRef={contextMenuRef}
item={category}
optionsPosition={optionsPosition}
setOptionsId={setOptionsId}
type={ContextMenuEnum.CATEGORY}
/>
)}
</div>
)}
</Draggable>
)
}
================================================
FILE: src/client/containers/ContextMenu.tsx
================================================
import ReactDOM from 'react-dom'
import React, { useEffect, useState, createContext } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { SelectCategory } from '@/components/NoteList/SelectCategory'
import { ContextMenuOptions } from '@/containers/ContextMenuOptions'
import { addCategoryToNote, updateActiveCategoryId, updateActiveNote } from '@/slices/note'
import { NoteItem, CategoryItem } from '@/types'
import { getNotes, getCategories, getSettings } from '@/selectors'
import { ContextMenuEnum } from '@/utils/enums'
import { isDraftNote } from '@/utils/helpers'
export const MenuUtilitiesContext = createContext({
setOptionsId: (id: string) => {},
})
interface Position {
x: number
y: number
}
export interface ContextMenuProps {
item: NoteItem | CategoryItem
optionsPosition: Position
contextMenuRef: React.RefObject<HTMLDivElement> | null
setOptionsId: (id: string) => void
type: ContextMenuEnum
}
export const ContextMenu: React.FC<ContextMenuProps> = ({
item,
optionsPosition,
contextMenuRef,
setOptionsId,
type,
}) => {
// ===========================================================================
// Selectors
// ===========================================================================
const { darkTheme } = useSelector(getSettings)
// ===========================================================================
// State
// ===========================================================================
const [elementDimensions, setElementDimensions] = useState<{
offsetHeight: number | null
offsetWidth: number | null
}>({ offsetHeight: null, offsetWidth: null })
// ===========================================================================
// Hooks
// ===========================================================================
useEffect(() => {
if (contextMenuRef?.current) {
const { offsetHeight, offsetWidth } = contextMenuRef.current
setElementDimensions({ offsetHeight, offsetWidth })
}
}, [contextMenuRef])
// ===========================================================================
// Other
// ===========================================================================
const contextValues = {
setOptionsId,
}
const getOptionsYPosition = (): Number => {
if (elementDimensions.offsetHeight || elementDimensions.offsetWidth) {
// get the max window frame
const MaxY = window.innerHeight
const optionsSize = elementDimensions.offsetHeight as number
// if window position - noteOptions position isn't bigger than options, flip it.
return MaxY - optionsPosition.y > optionsSize
? optionsPosition.y
: optionsPosition.y - optionsSize
}
return 0
}
return ReactDOM.createPortal(
<div className={type === ContextMenuEnum.CATEGORY || darkTheme ? 'dark' : ''}>
<div
ref={contextMenuRef}
className="options-context-menu"
style={{
visibility: getOptionsYPosition() ? 'visible' : 'hidden',
position: 'absolute',
top: getOptionsYPosition() + 'px',
left: optionsPosition.x + 'px',
}}
onClick={(event) => {
event.stopPropagation()
}}
>
<MenuUtilitiesContext.Provider value={contextValues}>
{type === ContextMenuEnum.CATEGORY ? (
<CategoryMenu category={item as CategoryItem} />
) : (
<NotesMenu note={item as NoteItem} setOptionsId={setOptionsId} />
)}
</MenuUtilitiesContext.Provider>
</div>
</div>,
document.getElementById('context-menu') as HTMLElement
)
}
interface CategoryMenuProps {
category: CategoryItem
}
const CategoryMenu: React.FC<CategoryMenuProps> = ({ category }) => {
return <ContextMenuOptions clickedItem={category} type={ContextMenuEnum.CATEGORY} />
}
interface NotesMenuProps {
note: NoteItem
setOptionsId: (id: string) => void
}
const NotesMenu: React.FC<NotesMenuProps> = ({ note, setOptionsId }) => {
// ===========================================================================
// Selectors
// ===========================================================================
const { categories } = useSelector(getCategories)
const { activeCategoryId } = useSelector(getNotes)
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _addCategoryToNote = (categoryId: string, noteId: string) =>
dispatch(addCategoryToNote({ categoryId, noteId }))
const _updateActiveNote = (noteId: string, multiSelect: boolean) =>
dispatch(updateActiveNote({ noteId, multiSelect }))
const _updateActiveCategoryId = (categoryId: string) =>
dispatch(updateActiveCategoryId(categoryId))
return !isDraftNote(note) ? (
<>
{!note.scratchpad && (
<SelectCategory
onChange={(event) => {
_addCategoryToNote(event.target.value, note.id)
if (event.target.value !== activeCategoryId) {
_updateActiveCategoryId(event.target.value)
_updateActiveNote(note.id, false)
}
setOptionsId('')
}}
categories={categories}
activeCategoryId={activeCategoryId}
note={note}
/>
)}
<ContextMenuOptions type={ContextMenuEnum.NOTE} clickedItem={note} />
</>
) : null
}
================================================
FILE: src/client/containers/ContextMenuOptions.tsx
================================================
import React, { useContext } from 'react'
import { ArrowUp, Download, Star, Trash, X, Edit2, Clipboard } from 'react-feather'
import { useDispatch, useSelector } from 'react-redux'
import { LabelText } from '@resources/LabelText'
import { TestID } from '@resources/TestID'
import { ContextMenuOption } from '@/components/NoteList/ContextMenuOption'
import { downloadNotes, isDraftNote, getShortUuid, copyToClipboard } from '@/utils/helpers'
import {
deleteNotes,
toggleFavoriteNotes,
toggleTrashNotes,
addCategoryToNote,
updateActiveNote,
swapFolder,
removeCategoryFromNotes,
} from '@/slices/note'
import { getCategories, getNotes } from '@/selectors'
import { Folder, ContextMenuEnum } from '@/utils/enums'
import { CategoryItem, NoteItem } from '@/types'
import category, { setCategoryEdit, deleteCategory } from '@/slices/category'
import { MenuUtilitiesContext } from '@/containers/ContextMenu'
export interface ContextMenuOptionsProps {
clickedItem: NoteItem | CategoryItem
type: ContextMenuEnum
}
export const ContextMenuOptions: React.FC<ContextMenuOptionsProps> = ({ clickedItem, type }) => {
if (type === 'CATEGORY') {
return <CategoryOptions clickedCategory={clickedItem as CategoryItem} />
} else {
return <NotesOptions clickedNote={clickedItem as NoteItem} />
}
}
interface CategoryOptionsProps {
clickedCategory: CategoryItem
}
const CategoryOptions: React.FC<CategoryOptionsProps> = ({ clickedCategory }) => {
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _deleteCategory = (categoryId: string) => dispatch(deleteCategory(categoryId))
const _removeCategoryFromNotes = (categoryId: string) =>
dispatch(removeCategoryFromNotes(categoryId))
const _swapFolder = (folder: Folder) => dispatch(swapFolder({ folder }))
const _setCategoryEdit = (categoryId: string, tempName: string) =>
dispatch(setCategoryEdit({ id: categoryId, tempName }))
// ===========================================================================
// Context
// ===========================================================================
const { setOptionsId } = useContext(MenuUtilitiesContext)
// ===========================================================================
// Handlers
// ===========================================================================
const startRenameHandler = () => {
_setCategoryEdit(clickedCategory.id, clickedCategory.name)
setOptionsId('')
}
const removeCategoryHandler = () => {
_deleteCategory(clickedCategory.id)
_removeCategoryFromNotes(clickedCategory.id)
_swapFolder(Folder.ALL)
}
return (
<nav className="options-nav" data-testid={TestID.CATEGORY_OPTIONS_NAV}>
<ContextMenuOption
dataTestID={TestID.CATEGORY_OPTION_RENAME}
handler={startRenameHandler}
icon={Edit2}
text={LabelText.RENAME}
/>
<ContextMenuOption
dataTestID={TestID.CATEGORY_OPTION_DELETE_PERMANENTLY}
handler={removeCategoryHandler}
icon={X}
text={LabelText.DELETE_PERMANENTLY}
optionType="delete"
/>
</nav>
)
}
interface NotesOptionsProps {
clickedNote: NoteItem
}
const NotesOptions: React.FC<NotesOptionsProps> = ({ clickedNote }) => {
// ===========================================================================
// Selectors
// ===========================================================================
const { selectedNotesIds, notes } = useSelector(getNotes)
const { categories } = useSelector(getCategories)
const selectedNotes = notes.filter((note) => selectedNotesIds.includes(note.id))
const isSelectedNotesDiffFavor = Boolean(
selectedNotes.find((note) => note.favorite) && selectedNotes.find((note) => !note.favorite)
)
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _deleteNotes = (noteIds: string[]) => dispatch(deleteNotes(noteIds))
const _toggleTrashNotes = (noteId: string) => dispatch(toggleTrashNotes(noteId))
const _toggleFavoriteNotes = (noteId: string) => dispatch(toggleFavoriteNotes(noteId))
const _addCategoryToNote = (categoryId: string, noteId: string) =>
dispatch(addCategoryToNote({ categoryId, noteId }))
const _updateActiveNote = (noteId: string, multiSelect: boolean) =>
dispatch(updateActiveNote({ noteId, multiSelect }))
// ===========================================================================
// Handlers
// ===========================================================================
const deleteNotesHandler = () => _deleteNotes(selectedNotesIds)
const downloadNotesHandler = () =>
downloadNotes(
selectedNotesIds.includes(clickedNote.id) ? selectedNotes : [clickedNote],
categories
)
const favoriteNoteHandler = () => _toggleFavoriteNotes(clickedNote.id)
const trashNoteHandler = () => _toggleTrashNotes(clickedNote.id)
const removeCategoryFromNoteHandler = () => {
_addCategoryToNote('', clickedNote.id)
_updateActiveNote(clickedNote.id, false)
}
const copyLinkedNoteMarkdownHandler = (e: React.SyntheticEvent, note: NoteItem) => {
e.preventDefault()
const shortNoteUuid = getShortUuid(note.id)
copyToClipboard(`{{${shortNoteUuid}}}`)
}
return !isDraftNote(clickedNote) ? (
<nav className="options-nav" data-testid={TestID.NOTE_OPTIONS_NAV}>
{clickedNote.trash && (
<>
<ContextMenuOption
dataTestID={TestID.NOTE_OPTION_DELETE_PERMANENTLY}
handler={deleteNotesHandler}
icon={X}
text={LabelText.DELETE_PERMANENTLY}
optionType="delete"
/>
<ContextMenuOption
dataTestID={TestID.NOTE_OPTION_RESTORE_FROM_TRASH}
handler={trashNoteHandler}
icon={ArrowUp}
text={LabelText.RESTORE_FROM_TRASH}
/>
</>
)}
{!clickedNote.scratchpad && !clickedNote.trash && (
<>
<ContextMenuOption
dataTestID={TestID.NOTE_OPTION_FAVORITE}
handler={favoriteNoteHandler}
icon={Star}
text={
isSelectedNotesDiffFavor
? LabelText.TOGGLE_FAVORITE
: clickedNote.favorite
? LabelText.REMOVE_FAVORITE
: LabelText.MARK_AS_FAVORITE
}
/>
<ContextMenuOption
dataTestID={TestID.NOTE_OPTION_TRASH}
handler={trashNoteHandler}
icon={Trash}
text={LabelText.MOVE_TO_TRASH}
optionType="delete"
/>
</>
)}
{clickedNote.category && !clickedNote.trash && (
<ContextMenuOption
dataTestID={TestID.NOTE_OPTION_REMOVE_CATEGORY}
handler={removeCategoryFromNoteHandler}
icon={X}
text={LabelText.REMOVE_CATEGORY}
/>
)}
<ContextMenuOption
dataTestID={TestID.NOTE_OPTION_DOWNLOAD}
handler={downloadNotesHandler}
icon={Download}
text={LabelText.DOWNLOAD}
/>
<ContextMenuOption
dataTestID={TestID.COPY_REFERENCE_TO_NOTE}
handler={(e: React.SyntheticEvent) => copyLinkedNoteMarkdownHandler(e, clickedNote)}
icon={Clipboard}
text={LabelText.COPY_REFERENCE_TO_NOTE}
/>
</nav>
) : null
}
================================================
FILE: src/client/containers/KeyboardShortcuts.tsx
================================================
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import prettier from 'prettier/standalone'
import parserMarkdown from 'prettier/parser-markdown'
import { useTempState } from '@/contexts/TempStateContext'
import { Folder, Shortcuts } from '@/utils/enums'
import { downloadNotes, getActiveNote, newNoteHandlerHelper } from '@/utils/helpers'
import { useKey } from '@/utils/hooks'
import {
addNote,
swapFolder,
toggleTrashNotes,
updateActiveNote,
updateSelectedNotes,
updateNote,
} from '@/slices/note'
import { sync } from '@/slices/sync'
import { getCategories, getNotes, getSettings } from '@/selectors'
import { CategoryItem, NoteItem } from '@/types'
import { toggleDarkTheme, togglePreviewMarkdown, updateCodeMirrorOption } from '@/slices/settings'
export const KeyboardShortcuts: React.FC = () => {
// ===========================================================================
// Selectors
// ===========================================================================
const { categories } = useSelector(getCategories)
const { activeCategoryId, activeFolder, activeNoteId, notes, selectedNotesIds } = useSelector(
getNotes
)
const { darkTheme, previewMarkdown } = useSelector(getSettings)
const activeNote = getActiveNote(notes, activeNoteId)
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _addNote = (note: NoteItem) => dispatch(addNote(note))
const _updateActiveNote = (noteId: string, multiSelect: boolean) =>
dispatch(updateActiveNote({ noteId, multiSelect }))
const _updateSelectedNotes = (noteId: string, multiSelect: boolean) =>
dispatch(updateSelectedNotes({ noteId, multiSelect }))
const _swapFolder = (folder: Folder) => dispatch(swapFolder({ folder }))
const _toggleTrashNotes = (noteId: string) => dispatch(toggleTrashNotes(noteId))
const _sync = (notes: NoteItem[], categories: CategoryItem[]) =>
dispatch(sync({ notes, categories }))
const _togglePreviewMarkdown = () => dispatch(togglePreviewMarkdown())
const _toggleDarkTheme = () => dispatch(toggleDarkTheme())
const _updateCodeMirrorOption = (key: string, value: string) =>
dispatch(updateCodeMirrorOption({ key, value }))
// ===========================================================================
// State
// ===========================================================================
const { addingTempCategory, setAddingTempCategory } = useTempState()
// ===========================================================================
// Handlers
// ===========================================================================
const newNoteHandler = () =>
newNoteHandlerHelper(
activeFolder,
previewMarkdown,
activeNote,
activeCategoryId,
_swapFolder,
_togglePreviewMarkdown,
_addNote,
_updateActiveNote,
_updateSelectedNotes
)
const newTempCategoryHandler = () => !addingTempCategory && setAddingTempCategory(true)
const trashNoteHandler = () => _toggleTrashNotes(activeNote!.id)
const syncNotesHandler = () => _sync(notes, categories)
const downloadNotesHandler = () => {
if (!activeNote || selectedNotesIds.length === 0) return
downloadNotes(
selectedNotesIds.includes(activeNote.id)
? notes.filter((note) => selectedNotesIds.includes(note.id))
: [activeNote],
categories
)
}
const togglePreviewMarkdownHandler = () => _togglePreviewMarkdown()
const toggleDarkThemeHandler = () => {
_toggleDarkTheme()
_updateCodeMirrorOption('theme', darkTheme ? 'base16-light' : 'new-moon')
}
const prettifyNoteHandler = () => {
// format current note with prettier
if (activeNote && activeNote.text) {
const formattedText = prettier.format(activeNote.text, {
parser: 'markdown',
plugins: [parserMarkdown],
})
const updatedNote = {
...activeNote,
text: formattedText,
}
dispatch(updateNote(updatedNote))
}
}
// ===========================================================================
// Hooks
// ===========================================================================
useKey(Shortcuts.NEW_NOTE, () => newNoteHandler())
useKey(Shortcuts.NEW_CATEGORY, () => newTempCategoryHandler())
useKey(Shortcuts.DELETE_NOTE, () => trashNoteHandler())
useKey(Shortcuts.SYNC_NOTES, () => syncNotesHandler())
useKey(Shortcuts.DOWNLOAD_NOTES, () => downloadNotesHandler())
useKey(Shortcuts.PREVIEW, () => togglePreviewMarkdownHandler())
useKey(Shortcuts.TOGGLE_THEME, () => toggleDarkThemeHandler())
useKey(Shortcuts.PRETTIFY, () => prettifyNoteHandler())
return null
}
================================================
FILE: src/client/containers/NoteEditor.tsx
================================================
import dayjs from 'dayjs'
import React from 'react'
import { Controlled as CodeMirror } from 'react-codemirror2'
import { useDispatch, useSelector } from 'react-redux'
import { Editor } from 'codemirror'
import { getActiveNote } from '@/utils/helpers'
import { updateNote } from '@/slices/note'
import { NoteItem } from '@/types'
import { NoteMenuBar } from '@/containers/NoteMenuBar'
import { EmptyEditor } from '@/components/Editor/EmptyEditor'
import { PreviewEditor } from '@/components/Editor/PreviewEditor'
import { getNotes, getSettings, getSync } from '@/selectors'
import { setPendingSync } from '@/slices/sync'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/base16-light.css'
import 'codemirror/mode/gfm/gfm'
import 'codemirror/addon/selection/active-line'
import 'codemirror/addon/scroll/scrollpastend'
export const NoteEditor: React.FC = () => {
// ===========================================================================
// Selectors
// ===========================================================================
const { pendingSync } = useSelector(getSync)
const { activeNoteId, loading, notes } = useSelector(getNotes)
const { codeMirrorOptions, previewMarkdown } = useSelector(getSettings)
const activeNote = getActiveNote(notes, activeNoteId)
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _updateNote = (note: NoteItem) => {
!pendingSync && dispatch(setPendingSync())
dispatch(updateNote(note))
}
const setEditorOverlay = (editor: Editor) => {
const query = /\{\{[^}]*}}/g
editor.addOverlay({
token: function (stream: any) {
query.lastIndex = stream.pos
var match = query.exec(stream.string)
if (match && match.index == stream.pos) {
stream.pos += match[0].length || 1
return 'notelink'
} else if (match) {
stream.pos = match.index
} else {
stream.skipToEnd()
}
},
})
}
const renderEditor = () => {
if (loading) {
return <div className="empty-editor v-center">Loading...</div>
} else if (!activeNote) {
return <EmptyEditor />
} else if (previewMarkdown) {
return (
<PreviewEditor
directionText={codeMirrorOptions.direction}
noteText={activeNote.text}
notes={notes}
/>
)
}
return (
<CodeMirror
data-testid="codemirror-editor"
className="editor mousetrap"
value={activeNote.text}
options={codeMirrorOptions}
editorDidMount={(editor) => {
setTimeout(() => {
editor.focus()
}, 0)
editor.setCursor(0)
setEditorOverlay(editor)
}}
onBeforeChange={(editor, data, value) => {
_updateNote({
id: activeNote.id,
text: value,
created: activeNote.created,
lastUpdated: dayjs().format(),
})
}}
onChange={(editor, data, value) => {
if (!value) {
editor.focus()
}
}}
onPaste={(editor, event: any) => {
// Get around pasting issue
// https://github.com/scniro/react-codemirror2/issues/77
if (!event.clipboardData || !event.clipboardData.items || !event.clipboardData.items[0])
return
event.clipboardData.items[0].getAsString((pasted: any) => {
if (editor.getSelection() !== pasted) return
const { anchor, head } = editor.listSelections()[0]
editor.setCursor({
line: Math.max(anchor.line, head.line),
ch: Math.max(anchor.ch, head.ch),
})
})
}}
/>
)
}
return (
<main className="note-editor">
<NoteMenuBar />
{renderEditor()}
</main>
)
}
================================================
FILE: src/client/containers/NoteList.tsx
================================================
import React, { useEffect, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { MoreHorizontal, Book, Star, Folder as FolderIcon } from 'react-feather'
import { TestID } from '@resources/TestID'
import { Folder, Shortcuts, ContextMenuEnum } from '@/utils/enums'
import { NoteListButton } from '@/components/NoteList/NoteListButton'
import { SearchBar } from '@/components/NoteList/SearchBar'
import { ContextMenu } from '@/containers/ContextMenu'
import { getNoteTitle, shouldOpenContextMenu, debounceEvent, isDraftNote } from '@/utils/helpers'
import { useKey } from '@/utils/hooks'
import {
permanentlyEmptyTrash,
pruneNotes,
updateActiveNote,
searchNotes,
updateSelectedNotes,
} from '@/slices/note'
import { NoteItem, ReactDragEvent, ReactMouseEvent } from '@/types'
import { getNotes, getSettings, getCategories } from '@/selectors'
import { getNotesSorter } from '@/utils/notesSortStrategies'
export const NoteList: React.FC = () => {
// ===========================================================================
// Selectors
// ===========================================================================
const { notesSortKey } = useSelector(getSettings)
const { activeCategoryId, activeFolder, selectedNotesIds, notes, searchValue } =
useSelector(getNotes)
const { categories } = useSelector(getCategories)
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _updateSelectedNotes = (noteId: string, multiSelect: boolean) =>
dispatch(updateSelectedNotes({ noteId, multiSelect }))
const _permanentlyEmptyTrash = () => dispatch(permanentlyEmptyTrash())
const _pruneNotes = () => dispatch(pruneNotes())
const _updateActiveNote = (noteId: string, multiSelect: boolean) =>
dispatch(updateActiveNote({ noteId, multiSelect }))
const _searchNotes = debounceEvent(
(searchValue: string) => dispatch(searchNotes(searchValue)),
100
)
// ===========================================================================
// Refs
// ===========================================================================
const contextMenuRef = useRef<HTMLDivElement>(null)
const searchRef = React.useRef() as React.MutableRefObject<HTMLInputElement>
// ===========================================================================
// State
// ===========================================================================
const [optionsId, setOptionsId] = useState('')
const [optionsPosition, setOptionsPosition] = useState({ x: 0, y: 0 })
const re = new RegExp(searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i')
const isMatch = (result: NoteItem) => re.test(result.text)
const filter: Record<Folder, (note: NoteItem) => boolean> = {
[Folder.CATEGORY]: (note) => !note.trash && note.category === activeCategoryId,
[Folder.SCRATCHPAD]: (note) => !!note.scratchpad,
[Folder.FAVORITES]: (note) => !note.trash && !!note.favorite,
[Folder.TRASH]: (note) => !!note.trash,
[Folder.ALL]: (note) => !note.trash && !note.scratchpad,
}
const filteredNotes: NoteItem[] = notes
.filter(filter[activeFolder])
.filter(isMatch)
.sort(getNotesSorter(notesSortKey))
// ===========================================================================
// Handlers
// ===========================================================================
const focusSearchHandler = () => searchRef.current.focus()
const handleDragStart = (event: ReactDragEvent, noteId: string = '') => {
event.stopPropagation()
event.dataTransfer.setData('text/plain', noteId)
}
const handleNoteOptionsClick = (event: ReactMouseEvent, noteId: string = '') => {
const clicked = event.target
// Make sure we aren't getting any null values. Any element clicked should be a sub-class of element
if (!clicked) return
// Ensure the clicked target is supposed to open the context menu
if (shouldOpenContextMenu(clicked as Element)) {
// note: don't check for MouseEvent because Cypress MouseEvent !== Window.MouseEvent
if ('pageX' in event && 'pageY' in event) {
setOptionsPosition({ x: event.pageX, y: event.pageY })
}
}
event.stopPropagation()
if (!contextMenuRef.current || !contextMenuRef.current.contains(clicked as HTMLDivElement)) {
setOptionsId(!optionsId || optionsId !== noteId ? noteId : '')
}
}
const handleNoteRightClick = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
noteId: string = ''
) => {
event.preventDefault()
const clicked = event.target
const RIGHT_CLICK = 2
// Make sure we aren't getting any null values .. any element clicked should be a sub-class of element
if (!clicked) return
// FIXME: This feels hacky
if (event.ctrlKey) return
// Make sure we are not right clicking on the menu
if (optionsId && event.button == RIGHT_CLICK) return
if ('clientX' in event && 'clientY' in event) {
setOptionsPosition({ x: event.clientX, y: event.clientY })
}
event.stopPropagation()
if (!contextMenuRef.current || contextMenuRef.current.contains(clicked as HTMLDivElement)) {
setOptionsId(!optionsId || optionsId !== noteId ? noteId : '')
}
}
const showEmptyTrash = activeFolder === Folder.TRASH && filteredNotes.length > 0
// ===========================================================================
// Hooks
// ===========================================================================
useEffect(() => {
document.addEventListener('mousedown', handleNoteOptionsClick)
return () => {
document.removeEventListener('mousedown', handleNoteOptionsClick)
}
})
useKey(Shortcuts.SEARCH, () => focusSearchHandler())
return (
<aside className="note-sidebar">
<div className="note-sidebar-header">
<SearchBar searchRef={searchRef} searchNotes={_searchNotes} />
{showEmptyTrash && (
<NoteListButton
dataTestID={TestID.EMPTY_TRASH_BUTTON}
label="Empty"
handler={() => _permanentlyEmptyTrash()}
>
Empty Trash
</NoteListButton>
)}
</div>
<div data-testid={TestID.NOTE_LIST} className="note-list">
{filteredNotes.map((note: NoteItem, index: number) => {
let noteTitle: string | React.ReactElement = getNoteTitle(note.text)
const noteCategory = categories.find((category) => category.id === note.category)
if (searchValue) {
const highlightStart = noteTitle.search(re)
if (highlightStart !== -1) {
const highlightEnd = highlightStart + searchValue.length
noteTitle = (
<>
{noteTitle.slice(0, highlightStart)}
<strong className="highlighted">
{noteTitle.slice(highlightStart, highlightEnd)}
</strong>
{noteTitle.slice(highlightEnd)}
</>
)
}
}
return (
<div
data-testid={TestID.NOTE_LIST_ITEM + index}
className={
selectedNotesIds.includes(note.id) ? 'note-list-each selected' : 'note-list-each'
}
key={note.id}
onClick={(event) => {
event.stopPropagation()
_updateSelectedNotes(note.id, event.metaKey)
_updateActiveNote(note.id, event.metaKey)
_pruneNotes()
}}
onContextMenu={(event) => handleNoteRightClick(event, note.id)}
draggable={note.text !== ''}
onDragStart={(event) => handleDragStart(event, note.id)}
>
<div className="note-list-outer">
<div data-testid={'note-title-' + index} className="note-title">
{note.favorite ? (
<>
<div className="icon">
<Star aria-hidden="true" className="note-favorite" size={12} />
<span className="sr-only">Favorite note</span>
</div>
<div className="truncate-text">{noteTitle}</div>
</>
) : (
<>
<div className="icon" />
<div className="truncate-text"> {noteTitle}</div>
</>
)}
</div>
{!isDraftNote(note) ? (
<div
// TODO: make testID based off of index when we add that to a NoteItem object
data-testid={TestID.NOTE_OPTIONS_DIV + index}
className={optionsId === note.id ? 'note-options selected' : 'note-options'}
onClick={(event) => handleNoteOptionsClick(event, note.id)}
>
<MoreHorizontal aria-hidden="true" size={15} className="context-menu-action" />
<span className="sr-only">Note options</span>
</div>
) : (
<div className="note-options"> </div>
)}
</div>
{(activeFolder === Folder.ALL || activeFolder === Folder.FAVORITES) && (
<div className="note-category">
{!!noteCategory ? (
<>
<FolderIcon size={12} className="context-menu-action" />
{noteCategory?.name}
</>
) : (
<>
<Book size={12} className="context-menu-action" />
Notes
</>
)}
</div>
)}
{optionsId === note.id && !isDraftNote(note) && (
<ContextMenu
contextMenuRef={contextMenuRef}
item={note}
optionsPosition={optionsPosition}
setOptionsId={setOptionsId}
type={ContextMenuEnum.NOTE}
/>
)}
</div>
)
})}
</div>
</aside>
)
}
================================================
FILE: src/client/containers/NoteMenuBar.tsx
================================================
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import {
Eye,
Edit,
Star,
Trash2,
Download,
RefreshCw,
Loader,
Settings,
Sun,
Moon,
Clipboard as ClipboardCmp,
} from 'react-feather'
import { TestID } from '@resources/TestID'
import { LastSyncedNotification } from '@/components/LastSyncedNotification'
import { NoteItem, CategoryItem } from '@/types'
import {
toggleSettingsModal,
togglePreviewMarkdown,
toggleDarkTheme,
updateCodeMirrorOption,
} from '@/slices/settings'
import { toggleFavoriteNotes, toggleTrashNotes } from '@/slices/note'
import { getCategories, getNotes, getSync, getSettings } from '@/selectors'
import { downloadNotes, isDraftNote, getShortUuid, copyToClipboard } from '@/utils/helpers'
import { sync } from '@/slices/sync'
export const NoteMenuBar = () => {
// ===========================================================================
// Selectors
// ===========================================================================
const { notes, activeNoteId } = useSelector(getNotes)
const { categories } = useSelector(getCategories)
const { syncing, lastSynced, pendingSync } = useSelector(getSync)
const { darkTheme } = useSelector(getSettings)
// ===========================================================================
// Other
// ===========================================================================
const copyNoteIcon = <ClipboardCmp size={18} aria-hidden="true" focusable="false" />
const successfulCopyMessage = 'Note copied!'
const activeNote = notes.find((note) => note.id === activeNoteId)!
const shortNoteUuid = getShortUuid(activeNoteId)
// ===========================================================================
// State
// ===========================================================================
const [uuidCopiedText, setUuidCopiedText] = useState<string>('')
const [isToggled, togglePreviewIcon] = useState<boolean>(false)
// ===========================================================================
// Hooks
// ===========================================================================
useEffect(() => {
if (uuidCopiedText === successfulCopyMessage) {
const timer = setTimeout(() => {
setUuidCopiedText('')
}, 3000)
return () => clearTimeout(timer)
}
}, [uuidCopiedText])
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _togglePreviewMarkdown = () => dispatch(togglePreviewMarkdown())
const _toggleTrashNotes = (noteId: string) => dispatch(toggleTrashNotes(noteId))
const _toggleFavoriteNotes = (noteId: string) => dispatch(toggleFavoriteNotes(noteId))
const _sync = (notes: NoteItem[], categories: CategoryItem[]) =>
dispatch(sync({ notes, categories }))
const _toggleSettingsModal = () => dispatch(toggleSettingsModal())
const _toggleDarkTheme = () => dispatch(toggleDarkTheme())
const _updateCodeMirrorOption = (key: string, value: any) =>
dispatch(updateCodeMirrorOption({ key, value }))
// ===========================================================================
// Handlers
// ===========================================================================
const downloadNotesHandler = () => downloadNotes([activeNote], categories)
const favoriteNoteHandler = () => _toggleFavoriteNotes(activeNoteId)
const trashNoteHandler = () => _toggleTrashNotes(activeNoteId)
const syncNotesHandler = () => _sync(notes, categories)
const settingsHandler = () => _toggleSettingsModal()
const toggleDarkThemeHandler = () => {
_toggleDarkTheme()
_updateCodeMirrorOption('theme', darkTheme ? 'base16-light' : 'new-moon')
}
const togglePreviewHandler = () => {
togglePreviewIcon(!isToggled)
_togglePreviewMarkdown()
}
return (
<section className="note-menu-bar">
{activeNote && !isDraftNote(activeNote) ? (
<nav>
<button
className="note-menu-bar-button"
onClick={togglePreviewHandler}
data-testid={TestID.PREVIEW_MODE}
>
{isToggled ? (
<Edit aria-hidden="true" size={18} />
) : (
<Eye aria-hidden="true" size={18} />
)}
<span className="sr-only">{isToggled ? 'Edit note' : 'Preview note'}</span>
</button>
{!activeNote.scratchpad && (
<>
<button className="note-menu-bar-button" onClick={favoriteNoteHandler}>
<Star aria-hidden="true" size={18} />
<span className="sr-only">Add note to favorites</span>
</button>
<button className="note-menu-bar-button trash" onClick={trashNoteHandler}>
<Trash2 aria-hidden="true" size={18} />
<span className="sr-only">Delete note</span>
</button>
</>
)}
<button className="note-menu-bar-button">
<Download aria-hidden="true" size={18} onClick={downloadNotesHandler} />
<span className="sr-only">Download note</span>
</button>
<button
className="note-menu-bar-button uuid"
onClick={() => {
copyToClipboard(`{{${shortNoteUuid}}}`)
setUuidCopiedText(successfulCopyMessage)
}}
data-testid={TestID.UUID_MENU_BAR_COPY_ICON}
>
{copyNoteIcon}
{uuidCopiedText && <span className="uuid-copied-text">{uuidCopiedText}</span>}
<span className="sr-only">Copy note</span>
</button>
</nav>
) : (
<div />
)}
<nav>
<LastSyncedNotification datetime={lastSynced} pending={pendingSync} syncing={syncing} />
<button
className="note-menu-bar-button"
onClick={syncNotesHandler}
data-testid={TestID.TOPBAR_ACTION_SYNC_NOTES}
>
{syncing ? (
<Loader aria-hidden="true" size={18} className="rotating-svg" />
) : (
<RefreshCw aria-hidden="true" size={18} />
)}
<span className="sr-only">Sync notes</span>
</button>
<button className="note-menu-bar-button" onClick={toggleDarkThemeHandler}>
{darkTheme ? <Sun aria-hidden="true" size={18} /> : <Moon aria-hidden="true" size={18} />}
<span className="sr-only">Themes</span>
</button>
<button className="note-menu-bar-button" onClick={settingsHandler}>
<Settings aria-hidden="true" size={18} />
<span className="sr-only">Settings</span>
</button>
</nav>
</section>
)
}
================================================
FILE: src/client/containers/SettingsModal.tsx
================================================
import React, { useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import {
X,
Command,
Settings,
Archive,
Edit2,
Download,
DownloadCloud,
UploadCloud,
} from 'react-feather'
import {
toggleSettingsModal,
updateCodeMirrorOption,
togglePreviewMarkdown,
toggleDarkTheme,
updateNotesSortStrategy,
} from '@/slices/settings'
import { updateNotes, importNotes } from '@/slices/note'
import { logout } from '@/slices/auth'
import { importCategories } from '@/slices/category'
import { shortcutMap, notesSortOptions, directionTextOptions } from '@/utils/constants'
import { CategoryItem, NoteItem, ReactMouseEvent } from '@/types'
import { getSettings, getAuth, getNotes, getCategories } from '@/selectors'
import { Option } from '@/components/SettingsModal/Option'
import { Shortcut } from '@/components/SettingsModal/Shortcut'
import { SelectOptions } from '@/components/SettingsModal/SelectOptions'
import { IconButton } from '@/components/SettingsModal/IconButton'
import { NotesSortKey } from '@/utils/enums'
import { backupNotes, downloadNotes } from '@/utils/helpers'
import { Tabs } from '@/components/Tabs/Tabs'
import { TabPanel } from '@/components/Tabs/TabPanel'
import { LabelText } from '@resources/LabelText'
import { TestID } from '@resources/TestID'
import { IconButtonUploader } from '@/components/SettingsModal/IconButtonUploader'
export const SettingsModal: React.FC = () => {
// ===========================================================================
// Selectors
// ===========================================================================
const { codeMirrorOptions, isOpen, previewMarkdown, darkTheme, notesSortKey } = useSelector(
getSettings
)
const { currentUser } = useSelector(getAuth)
const { notes, activeFolder, activeCategoryId } = useSelector(getNotes)
const { categories } = useSelector(getCategories)
// ===========================================================================
// Dispatch
// ===========================================================================
const dispatch = useDispatch()
const _logout = () => dispatch(logout())
const _toggleSettingsModal = () => dispatch(toggleSettingsModal())
const _togglePreviewMarkdown = () => dispatch(togglePreviewMarkdown())
const _toggleDarkTheme = () => dispatch(toggleDarkTheme())
const _updateNotesSortStrategy = (sortBy: NotesSortKey) =>
dispatch(updateNotesSortStrategy(sortBy))
const _updateCodeMirrorOption = (key: string, value: any) =>
dispatch(updateCodeMirrorOption({ key, value }))
const _updateNotes = (sortOrderKey: NotesSortKey) =>
dispatch(updateNotes({ notes, activeFolder, activeCategoryId, sortOrderKey }))
const _importBackup = (notes: NoteItem[], categories: CategoryItem[]) => {
dispatch(importNotes(notes))
dispatch(importCategories(categories))
}
// ===========================================================================
// Refs
// ===========================================================================
const node = useRef<HTMLDivElement>(null)
// ===========================================================================
// Handlers
// ===========================================================================
const handleDomClick = (event: ReactMouseEvent) => {
event.stopPropagation()
if (node.current && node.current.contains(event.target as HTMLDivElement)) return
if (isOpen) {
_toggleSettingsModal()
}
}
const togglePreviewMarkdownHandler = () => _togglePreviewMarkdown()
const toggleDarkThemeHandler = () => {
_toggleDarkTheme()
_updateCodeMirrorOption('theme', darkTheme ? 'base16-light' : 'new-moon')
}
const toggleLineHighlight = () =>
_updateCodeMirrorOption('styleActiveLine', !codeMirrorOptions.styleActiveLine)
const toggleScrollPastEnd = () =>
_updateCodeMirrorOption('scrollPastEnd', !codeMirrorOptions.scrollPastEnd)
const toggleLineNumbersHandler = () =>
_updateCodeMirrorOption('lineNumbers', !codeMirrorOptions.lineNumbers)
const handleEscPress = (event: KeyboardEvent) => {
event.stopPropagation()
if (event.key === 'Escape' && isOpen) {
_toggleSettingsModal()
}
}
const updateNotesSortStrategyHandler = (selectedOption: any) => {
_updateNotesSortStrategy(selectedOption.value)
_updateNotes(selectedOption.value)
}
const updateNotesDirectionHandler = (selectedOption: any) => {
_updateCodeMirrorOption('direction', selectedOption.value)
}
const downloadNotesHandler = () => downloadNotes(notes, categories)
const backupHandler = () => backupNotes(notes, categories)
const importBackupHandler = async (json: File) => {
const content = await json.text()
const { notes, categories } = JSON.parse(content) as {
notes: NoteItem[]
categories: CategoryItem[]
}
if (!notes || !categories) return
_importBackup(notes, categories)
}
// ===========================================================================
// Hooks
// ===========================================================================
useEffect(() => {
document.addEventListener('mousedown', handleDomClick)
document.addEventListener('keydown', handleEscPress)
return () => {
document.removeEventListener('mousedown', handleDomClick)
document.removeEventListener('keydown', handleEscPress)
}
})
return isOpen ? (
<div className="dimmer">
<aside ref={node} className="settings-modal">
<header className="settings-modal-header">
<div
className="close-button"
onClick={() => {
if (isOpen) _toggleSettingsModal()
}}
>
<X size={20} />
</div>
<section className="profile flex">
<div>
{currentUser.avatar_url && (
<img src={currentUser.avatar_url} alt="Profile" className="profile-picture" />
)}
</div>
<div className="profile-details">
<h3>{currentUser.name}</h3>
<div className="subtitle">{currentUser.bio}</div>
</div>
<button
onClick={() => {
_logout()
}}
>
Log out
</button>
</section>
</header>
<section className="settings-content">
<Tabs>
<TabPanel label="Preferences" icon={Settings}>
<Option
title="Active line hig
gitextract_n64ay1gg/ ├── .all-contributorsrc ├── .dockerignore ├── .editorconfig ├── .eslintrc.js ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── config/ │ ├── cypress.config.json │ ├── jest.config.js │ ├── nodemon.config.json │ ├── webpack.common.js │ ├── webpack.dev.js │ └── webpack.prod.js ├── deploy.sh ├── kubernetes.yml ├── package.json ├── public/ │ ├── _redirects │ ├── manifest.json │ ├── robots.txt │ └── template.html ├── seed.js ├── src/ │ ├── client/ │ │ ├── api/ │ │ │ └── index.ts │ │ ├── components/ │ │ │ ├── AppSidebar/ │ │ │ │ ├── ActionButton.tsx │ │ │ │ ├── AddCategoryButton.tsx │ │ │ │ ├── AddCategoryForm.tsx │ │ │ │ ├── CollapseCategoryButton.tsx │ │ │ │ ├── FolderOption.tsx │ │ │ │ └── ScratchpadOption.tsx │ │ │ ├── Editor/ │ │ │ │ ├── EmptyEditor.tsx │ │ │ │ ├── NoteLink.tsx │ │ │ │ └── PreviewEditor.tsx │ │ │ ├── LandingPage.tsx │ │ │ ├── LastSyncedNotification.tsx │ │ │ ├── NoteList/ │ │ │ │ ├── ContextMenuOption.tsx │ │ │ │ ├── NoteListButton.tsx │ │ │ │ ├── SearchBar.tsx │ │ │ │ └── SelectCategory.tsx │ │ │ ├── Select.tsx │ │ │ ├── SettingsModal/ │ │ │ │ ├── IconButton.tsx │ │ │ │ ├── IconButtonUploader.tsx │ │ │ │ ├── Option.tsx │ │ │ │ ├── SelectOptions.tsx │ │ │ │ └── Shortcut.tsx │ │ │ ├── Switch.tsx │ │ │ └── Tabs/ │ │ │ ├── Tab.tsx │ │ │ ├── TabPanel.tsx │ │ │ └── Tabs.tsx │ │ ├── containers/ │ │ │ ├── App.tsx │ │ │ ├── AppSidebar.tsx │ │ │ ├── CategoryList.tsx │ │ │ ├── CategoryOption.tsx │ │ │ ├── ContextMenu.tsx │ │ │ ├── ContextMenuOptions.tsx │ │ │ ├── KeyboardShortcuts.tsx │ │ │ ├── NoteEditor.tsx │ │ │ ├── NoteList.tsx │ │ │ ├── NoteMenuBar.tsx │ │ │ ├── SettingsModal.tsx │ │ │ └── TakeNoteApp.tsx │ │ ├── contexts/ │ │ │ └── TempStateContext.tsx │ │ ├── global.d.ts │ │ ├── index.tsx │ │ ├── router/ │ │ │ ├── PrivateRoute.tsx │ │ │ └── PublicRoute.tsx │ │ ├── sagas/ │ │ │ └── index.ts │ │ ├── selectors/ │ │ │ └── index.ts │ │ ├── serviceWorker.ts │ │ ├── setupTests.ts │ │ ├── slices/ │ │ │ ├── auth.ts │ │ │ ├── category.ts │ │ │ ├── index.ts │ │ │ ├── note.ts │ │ │ ├── settings.ts │ │ │ └── sync.ts │ │ ├── styles/ │ │ │ ├── _app-sidebar.scss │ │ │ ├── _buttons.scss │ │ │ ├── _dark.scss │ │ │ ├── _editor.scss │ │ │ ├── _forms.scss │ │ │ ├── _helpers.scss │ │ │ ├── _landing-page.scss │ │ │ ├── _layout.scss │ │ │ ├── _light-theme.scss │ │ │ ├── _mixins.scss │ │ │ ├── _modal.scss │ │ │ ├── _new-moon.scss │ │ │ ├── _note-menu-bar.scss │ │ │ ├── _note-sidebar.scss │ │ │ ├── _previewer.scss │ │ │ ├── _scaffolding.scss │ │ │ ├── _tabs.scss │ │ │ ├── _variables.scss │ │ │ └── index.scss │ │ ├── types/ │ │ │ └── index.ts │ │ └── utils/ │ │ ├── constants.ts │ │ ├── enums.ts │ │ ├── helpers.ts │ │ ├── history.ts │ │ ├── hooks.ts │ │ ├── notesSortStrategies.ts │ │ └── reactMarkdownPlugins.ts │ ├── resources/ │ │ ├── LabelText.ts │ │ └── TestID.ts │ └── server/ │ ├── handlers/ │ │ ├── auth.ts │ │ └── sync.ts │ ├── index.ts │ ├── initializeServer.ts │ ├── middleware/ │ │ ├── checkAuth.ts │ │ └── getUser.ts │ ├── router/ │ │ ├── auth.ts │ │ ├── index.ts │ │ └── sync.ts │ └── utils/ │ ├── constants.ts │ ├── data/ │ │ ├── scratchpadNote.ts │ │ └── welcomeNote.ts │ ├── enums.ts │ └── helpers.ts ├── tests/ │ ├── __mocks__/ │ │ └── styleMock.ts │ ├── e2e/ │ │ ├── integration/ │ │ │ ├── category.test.ts │ │ │ ├── note.test.ts │ │ │ └── settings.test.ts │ │ ├── plugins/ │ │ │ ├── cy-ts-preprocessor.js │ │ │ └── index.js │ │ ├── support/ │ │ │ ├── commands.js │ │ │ └── index.js │ │ ├── tsconfig.json │ │ └── utils/ │ │ ├── testCategoryHelperUtils.ts │ │ ├── testHelperEnums.ts │ │ ├── testHelperUtils.ts │ │ ├── testNotesHelperUtils.ts │ │ └── testSettingsUtils.ts │ └── unit/ │ ├── client/ │ │ ├── components/ │ │ │ ├── AppSidebar/ │ │ │ │ ├── ActionButton.test.tsx │ │ │ │ ├── AddCategoryButton.test.tsx │ │ │ │ ├── AddCategoryForm.test.tsx │ │ │ │ ├── CollapseCategoryButton.test.tsx │ │ │ │ ├── FolderOption.test.tsx │ │ │ │ └── ScratchpadOption.test.tsx │ │ │ ├── LastSyncedNotification.test.tsx │ │ │ ├── NoteList/ │ │ │ │ ├── NoteListButton.test.tsx │ │ │ │ └── SearchBar.test.tsx │ │ │ ├── SettingsModal/ │ │ │ │ ├── IconButton.test.tsx │ │ │ │ └── IconButtonUploader.test.tsx │ │ │ ├── Switch.test.tsx │ │ │ └── editor/ │ │ │ ├── EditorEmpty.test.tsx │ │ │ └── PreviewEditor.test.tsx │ │ ├── containers/ │ │ │ ├── ContextMenuOptions.test.tsx │ │ │ └── TakeNoteApp.test.tsx │ │ ├── slices/ │ │ │ ├── auth.test.ts │ │ │ ├── category.test.ts │ │ │ ├── note.test.ts │ │ │ ├── settings.test.ts │ │ │ └── sync.test.ts │ │ ├── testHelpers.tsx │ │ └── utils/ │ │ └── index.test.ts │ └── server/ │ └── middleware/ │ └── checkAuth.test.ts └── tsconfig.json
SYMBOL INDEX (79 symbols across 45 files)
FILE: src/client/api/index.ts
type PromiseCallback (line 50) | type PromiseCallback = (value?: any) => void
type GetLocalStorage (line 51) | type GetLocalStorage = (
FILE: src/client/components/AppSidebar/ActionButton.tsx
type ActionButtonProps (line 6) | interface ActionButtonProps {
FILE: src/client/components/AppSidebar/AddCategoryButton.tsx
type AddCategoryButtonProps (line 6) | interface AddCategoryButtonProps {
FILE: src/client/components/AppSidebar/AddCategoryForm.tsx
type AddCategoryFormProps (line 6) | interface AddCategoryFormProps {
FILE: src/client/components/AppSidebar/CollapseCategoryButton.tsx
type CollapseCategoryListButton (line 4) | interface CollapseCategoryListButton {
FILE: src/client/components/AppSidebar/FolderOption.tsx
type FolderOptionProps (line 8) | interface FolderOptionProps {
FILE: src/client/components/AppSidebar/ScratchpadOption.tsx
type ScratchpadOptionProps (line 9) | interface ScratchpadOptionProps {
FILE: src/client/components/Editor/NoteLink.tsx
type NoteLinkProps (line 9) | interface NoteLinkProps {
FILE: src/client/components/Editor/PreviewEditor.tsx
type PreviewEditorProps (line 13) | interface PreviewEditorProps {
FILE: src/client/components/LastSyncedNotification.tsx
type LastSyncedNotificationProps (line 6) | interface LastSyncedNotificationProps {
FILE: src/client/components/NoteList/ContextMenuOption.tsx
type ContextMenuOptionProps (line 6) | interface ContextMenuOptionProps {
FILE: src/client/components/NoteList/NoteListButton.tsx
type NoteListButtonProps (line 3) | interface NoteListButtonProps {
FILE: src/client/components/NoteList/SearchBar.tsx
type SearchBarProps (line 5) | interface SearchBarProps {
FILE: src/client/components/NoteList/SelectCategory.tsx
type SelectCategoryProps (line 7) | interface SelectCategoryProps {
FILE: src/client/components/Select.tsx
type SelectOption (line 3) | interface SelectOption {
type SelectProps (line 8) | interface SelectProps {
FILE: src/client/components/SettingsModal/IconButton.tsx
type IconButtonProps (line 6) | interface IconButtonProps {
FILE: src/client/components/SettingsModal/IconButtonUploader.tsx
type IconButtonUploaderProps (line 6) | interface IconButtonUploaderProps {
FILE: src/client/components/SettingsModal/Option.tsx
type OptionProps (line 5) | interface OptionProps {
FILE: src/client/components/SettingsModal/SelectOptions.tsx
type OptionProps (line 5) | interface OptionProps {
FILE: src/client/components/SettingsModal/Shortcut.tsx
type ShortcutProps (line 3) | interface ShortcutProps {
FILE: src/client/components/Switch.tsx
type SwitchProps (line 3) | interface SwitchProps {
FILE: src/client/components/Tabs/Tab.tsx
type TabProps (line 4) | interface TabProps {
FILE: src/client/components/Tabs/TabPanel.tsx
type TabPanelProps (line 4) | interface TabPanelProps {
FILE: src/client/components/Tabs/Tabs.tsx
type TabsProps (line 5) | interface TabsProps {
FILE: src/client/containers/CategoryOption.tsx
type CategoryOptionProps (line 22) | interface CategoryOptionProps {
FILE: src/client/containers/ContextMenu.tsx
type Position (line 16) | interface Position {
type ContextMenuProps (line 21) | interface ContextMenuProps {
type CategoryMenuProps (line 113) | interface CategoryMenuProps {
type NotesMenuProps (line 121) | interface NotesMenuProps {
FILE: src/client/containers/ContextMenuOptions.tsx
type ContextMenuOptionsProps (line 24) | interface ContextMenuOptionsProps {
type CategoryOptionsProps (line 37) | interface CategoryOptionsProps {
type NotesOptionsProps (line 94) | interface NotesOptionsProps {
FILE: src/client/contexts/TempStateContext.tsx
type TempStateContextInterface (line 3) | interface TempStateContextInterface {
FILE: src/client/router/PrivateRoute.tsx
type PrivateRouteProps (line 7) | interface PrivateRouteProps extends RouteProps {
FILE: src/client/router/PublicRoute.tsx
type PublicRouteProps (line 7) | interface PublicRouteProps extends RouteProps {
FILE: src/client/serviceWorker.ts
type Config (line 9) | type Config = {
function register (line 14) | function register(config?: Config) {
function registerValidSW (line 35) | function registerValidSW(swUrl: string, config?: Config) {
function checkValidServiceWorker (line 58) | function checkValidServiceWorker(swUrl: string, config?: Config) {
function unregister (line 86) | function unregister() {
FILE: src/client/types/index.ts
type NoteItem (line 10) | interface NoteItem {
type CategoryItem (line 24) | interface CategoryItem {
type GithubUser (line 30) | interface GithubUser {
type AuthState (line 38) | interface AuthState {
type CategoryState (line 45) | interface CategoryState {
type NoteState (line 55) | interface NoteState {
type SettingsState (line 66) | interface SettingsState {
type SyncState (line 76) | interface SyncState {
type RootState (line 83) | interface RootState {
type SyncPayload (line 95) | interface SyncPayload {
type SyncAction (line 100) | interface SyncAction {
type ReactDragEvent (line 109) | type ReactDragEvent = React.DragEvent<HTMLDivElement>
type ReactMouseEvent (line 111) | type ReactMouseEvent =
type ReactSubmitEvent (line 116) | type ReactSubmitEvent = React.FormEvent<HTMLFormElement> | React.FocusEv...
type WithPayload (line 123) | type WithPayload<P, T> = T & {
FILE: src/client/utils/enums.ts
type Folder (line 1) | enum Folder {
type Shortcuts (line 9) | enum Shortcuts {
type ContextMenuEnum (line 21) | enum ContextMenuEnum {
type NotesSortKey (line 26) | enum NotesSortKey {
type DirectionText (line 32) | enum DirectionText {
type Errors (line 37) | enum Errors {
FILE: src/client/utils/hooks.ts
function useInterval (line 8) | function useInterval(callback: () => void, delay: number | null) {
function useKey (line 27) | function useKey(key: string, action: () => void) {
function useBeforeUnload (line 43) | function useBeforeUnload(handler: Function = () => {}) {
FILE: src/client/utils/notesSortStrategies.ts
type NotesSortStrategy (line 6) | interface NotesSortStrategy {
FILE: src/client/utils/reactMarkdownPlugins.ts
function transformer (line 28) | function transformer(tree: any) {
FILE: src/resources/LabelText.ts
type LabelText (line 2) | enum LabelText {
FILE: src/resources/TestID.ts
type TestID (line 2) | enum TestID {
FILE: src/server/handlers/auth.ts
function firstTimeLoginCheck (line 109) | async function firstTimeLoginCheck(username: string, accessToken: string...
function createTakeNoteDataRepo (line 121) | async function createTakeNoteDataRepo(username: string, accessToken: str...
function createInitialCommit (line 142) | async function createInitialCommit(username: string, accessToken: string...
FILE: src/server/initializeServer.ts
function initializeServer (line 9) | function initializeServer(router: Router) {
FILE: src/server/utils/enums.ts
type Method (line 1) | enum Method {
FILE: src/server/utils/helpers.ts
function SDK (line 5) | function SDK(method: Method, path: string, accessToken: string, data?: O...
FILE: tests/e2e/plugins/index.js
method getClipboard (line 9) | getClipboard() {
FILE: tests/unit/client/slices/note.test.ts
function createNote (line 29) | function createNote({
FILE: tests/unit/client/testHelpers.tsx
type RenderWithRouterOptions (line 12) | interface RenderWithRouterOptions {
Condensed preview — 164 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (442K chars).
[
{
"path": ".all-contributorsrc",
"chars": 16967,
"preview": "{\n \"projectName\": \"takenote\",\n \"projectOwner\": \"taniarascia\",\n \"repoType\": \"github\",\n \"repoHost\": \"https://github.co"
},
{
"path": ".dockerignore",
"chars": 92,
"preview": "node_modules\ndist\n.git\n.vscode\n!src\n!public\n!docs\n!config/*\n!package.json\n!package-lock.json"
},
{
"path": ".editorconfig",
"chars": 147,
"preview": "root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\ntrim_"
},
{
"path": ".eslintrc.js",
"chars": 1314,
"preview": "/**\n * ESLint Configuration\n */\nmodule.exports = {\n parser: '@typescript-eslint/parser',\n parserOptions: {\n ecmaVer"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 310,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: '[Bug]'\nlabels: 'Type: Bug'\nassignees: ''\n---\n\n**T"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 318,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: '[Feature]'\nlabels: 'Type: Feature'\nassignees: "
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 402,
"preview": "## Description\n\nPlease read the [Contribution Guidelines](../CONTRIBUTING.md) before opening a pull request. Include a s"
},
{
"path": ".gitignore",
"chars": 127,
"preview": "/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"
},
{
"path": ".prettierrc",
"chars": 133,
"preview": "{\n \"bracketSpacing\": true,\n \"printWidth\": 100,\n \"semi\": false,\n \"singleQuote\": true,\n \"tabWidth\": 2,\n \"trailingCom"
},
{
"path": ".travis.yml",
"chars": 1399,
"preview": "language: node_js\n\nnode_js:\n - '12'\n\naddons:\n apt:\n packages:\n # Ubuntu 16+ does not install this dependency b"
},
{
"path": "CHANGELOG.md",
"chars": 2152,
"preview": "# 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-"
},
{
"path": "CONTRIBUTING.md",
"chars": 6755,
"preview": "# Contribution Guidelines\n\nTakeNote is an open source project, and contributions of any kind are welcome and appreciated"
},
{
"path": "Dockerfile",
"chars": 513,
"preview": "# Use small Alpine Linux image\nFROM node:12-alpine\n\n# Set environment variables\nENV PORT=5000\nARG CLIENT_ID\n\nCOPY . app/"
},
{
"path": "LICENSE",
"chars": 1079,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2020 Tania Rascia\n\nPermission is hereby granted, free of charge, to any person obta"
},
{
"path": "README.md",
"chars": 30543,
"preview": "<p align=\"center\">\n <img src=\"./assets/logo.png\">\n</p>\n\n<p align=\"center\">\n <img src=\"https://img.shields.io/badge/Lice"
},
{
"path": "config/cypress.config.json",
"chars": 281,
"preview": "{\n \"baseUrl\": \"http://localhost:3000\",\n \"integrationFolder\": \"tests/e2e/integration\",\n \"pluginsFile\": \"tests/e2e/plug"
},
{
"path": "config/jest.config.js",
"chars": 719,
"preview": "module.exports = {\n // Setting the root to the actual root, since this file is in root/config\n preset: 'ts-jest',\n ro"
},
{
"path": "config/nodemon.config.json",
"chars": 307,
"preview": "{\n \"ignore\": [\n \".git\",\n \".eslintrc\",\n \"node_modules/**\",\n \"src/client/**\",\n \"test/**\",\n \"public/**\","
},
{
"path": "config/webpack.common.js",
"chars": 2658,
"preview": "const path = require('path')\n\nconst dotenv = require('dotenv')\nconst webpack = require('webpack')\nconst { CleanWebpackPl"
},
{
"path": "config/webpack.dev.js",
"chars": 968,
"preview": "const webpack = require('webpack')\nconst { merge } = require('webpack-merge')\nconst HtmlWebpackPlugin = require('html-we"
},
{
"path": "config/webpack.prod.js",
"chars": 1887,
"preview": "const webpack = require('webpack')\nconst { merge } = require('webpack-merge')\nconst MiniCssExtractPlugin = require('mini"
},
{
"path": "deploy.sh",
"chars": 1501,
"preview": "#!/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"
},
{
"path": "kubernetes.yml",
"chars": 1282,
"preview": "apiVersion: v1\ndata:\n client_id: // base64 encoded string\n client_secret: // base64 encoded string\nkind: Secret\nmetada"
},
{
"path": "package.json",
"chars": 5068,
"preview": "{\n \"name\": \"takenote\",\n \"version\": \"0.7.2\",\n \"description\": \"A web-based notes app for developers.\",\n \"author\": \"Tan"
},
{
"path": "public/_redirects",
"chars": 23,
"preview": "/* /index.html 200"
},
{
"path": "public/manifest.json",
"chars": 505,
"preview": "{\n \"short_name\": \"TakeNote\",\n \"name\": \"A web-based notes app for developers.\",\n \"icons\": [\n {\n \"src\": \"favico"
},
{
"path": "public/robots.txt",
"chars": 14,
"preview": "User-agent: *\n"
},
{
"path": "public/template.html",
"chars": 393,
"preview": "<!DOCTYPE html>\n\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-"
},
{
"path": "seed.js",
"chars": 15801,
"preview": "const categories = [\n {\n id: 'goals',\n name: 'goals',\n },\n {\n id: 'health',\n name: 'health',\n },\n {\n "
},
{
"path": "src/client/api/index.ts",
"chars": 4001,
"preview": "import { v4 as uuid } from 'uuid'\nimport dayjs from 'dayjs'\n\nimport { NoteItem, SyncPayload, SettingsState } from '@/typ"
},
{
"path": "src/client/components/AppSidebar/ActionButton.tsx",
"chars": 829,
"preview": "import React, { MouseEventHandler } from 'react'\nimport { Icon } from 'react-feather'\n\nimport { iconColor } from '@/util"
},
{
"path": "src/client/components/AppSidebar/AddCategoryButton.tsx",
"chars": 560,
"preview": "import React from 'react'\nimport { Plus } from 'react-feather'\n\nimport { iconColor } from '@/utils/constants'\n\nexport in"
},
{
"path": "src/client/components/AppSidebar/AddCategoryForm.tsx",
"chars": 1183,
"preview": "import React from 'react'\n\nimport { TestID } from '@resources/TestID'\nimport { ReactSubmitEvent } from '@/types'\n\nexport"
},
{
"path": "src/client/components/AppSidebar/CollapseCategoryButton.tsx",
"chars": 797,
"preview": "import React from 'react'\nimport { ChevronDown, ChevronRight, Layers } from 'react-feather'\n\nexport interface CollapseCa"
},
{
"path": "src/client/components/AppSidebar/FolderOption.tsx",
"chars": 2236,
"preview": "import React, { useState } from 'react'\nimport { Book, Star, Trash2 } from 'react-feather'\n\nimport { Folder } from '@/ut"
},
{
"path": "src/client/components/AppSidebar/ScratchpadOption.tsx",
"chars": 812,
"preview": "import React from 'react'\nimport { Edit } from 'react-feather'\n\nimport { TestID } from '@resources/TestID'\nimport { Labe"
},
{
"path": "src/client/components/Editor/EmptyEditor.tsx",
"chars": 369,
"preview": "import React from 'react'\n\nexport const EmptyEditor: React.FC = () => {\n return (\n <div className=\"empty-editor v-ce"
},
{
"path": "src/client/components/Editor/NoteLink.tsx",
"chars": 900,
"preview": "import React from 'react'\n\nimport { NoteItem } from '@/types'\nimport { Errors } from '@/utils/enums'\nimport { TestID } f"
},
{
"path": "src/client/components/Editor/PreviewEditor.tsx",
"chars": 2228,
"preview": "import React from 'react'\nimport ReactMarkdown from 'react-markdown'\nimport { useDispatch } from 'react-redux'\n\nimport {"
},
{
"path": "src/client/components/LandingPage.tsx",
"chars": 5972,
"preview": "import React from 'react'\nimport { isMobile } from 'react-device-detect'\n\nimport lightScreen from '@resources/assets/scr"
},
{
"path": "src/client/components/LastSyncedNotification.tsx",
"chars": 837,
"preview": "import React from 'react'\nimport dayjs from 'dayjs'\n\nimport { TestID } from '@resources/TestID'\n\nexport interface LastSy"
},
{
"path": "src/client/components/NoteList/ContextMenuOption.tsx",
"chars": 1463,
"preview": "import React, { KeyboardEventHandler, MouseEventHandler, useContext } from 'react'\nimport { Icon } from 'react-feather'\n"
},
{
"path": "src/client/components/NoteList/NoteListButton.tsx",
"chars": 524,
"preview": "import React, { MouseEventHandler } from 'react'\n\nexport interface NoteListButtonProps {\n dataTestID: string\n disabled"
},
{
"path": "src/client/components/NoteList/SearchBar.tsx",
"chars": 617,
"preview": "import React from 'react'\n\nimport { TestID } from '@resources/TestID'\n\nexport interface SearchBarProps {\n searchRef: Re"
},
{
"path": "src/client/components/NoteList/SelectCategory.tsx",
"chars": 1269,
"preview": "import React from 'react'\n\nimport { TestID } from '@resources/TestID'\nimport { NoteItem, CategoryItem } from '@/types'\ni"
},
{
"path": "src/client/components/Select.tsx",
"chars": 892,
"preview": "import React from 'react'\n\nexport interface SelectOption {\n label: string\n value: string\n}\n\nexport interface SelectPro"
},
{
"path": "src/client/components/SettingsModal/IconButton.tsx",
"chars": 775,
"preview": "import React, { MouseEventHandler } from 'react'\nimport { Icon } from 'react-feather'\n\nimport { iconColor } from '@/util"
},
{
"path": "src/client/components/SettingsModal/IconButtonUploader.tsx",
"chars": 1377,
"preview": "import React, { ChangeEvent, useRef } from 'react'\nimport { Icon } from 'react-feather'\n\nimport { iconColor } from '@/ut"
},
{
"path": "src/client/components/SettingsModal/Option.tsx",
"chars": 538,
"preview": "import React from 'react'\n\nimport { Switch } from '@/components/Switch'\n\nexport interface OptionProps {\n title: string\n"
},
{
"path": "src/client/components/SettingsModal/SelectOptions.tsx",
"chars": 676,
"preview": "import React from 'react'\n\nimport { Select } from '../Select'\n\nexport interface OptionProps {\n title: string\n descript"
},
{
"path": "src/client/components/SettingsModal/Shortcut.tsx",
"chars": 371,
"preview": "import React from 'react'\n\nexport interface ShortcutProps {\n action: string\n letter: string\n}\n\nexport const Shortcut: "
},
{
"path": "src/client/components/Switch.tsx",
"chars": 382,
"preview": "import React from 'react'\n\nexport interface SwitchProps {\n toggle: () => void\n checked: boolean\n testId: string\n}\n\nex"
},
{
"path": "src/client/components/Tabs/Tab.tsx",
"chars": 541,
"preview": "import React from 'react'\nimport { Icon } from 'react-feather'\n\nexport interface TabProps {\n label: string\n activeTab:"
},
{
"path": "src/client/components/Tabs/TabPanel.tsx",
"chars": 279,
"preview": "import React from 'react'\nimport { Icon } from 'react-feather'\n\nexport interface TabPanelProps {\n label: string\n icon:"
},
{
"path": "src/client/components/Tabs/Tabs.tsx",
"chars": 1015,
"preview": "import React, { Fragment, useState } from 'react'\n\nimport { Tab } from './Tab'\n\nexport interface TabsProps {\n children:"
},
{
"path": "src/client/containers/App.tsx",
"chars": 2091,
"preview": "import React, { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { Route, Switch, "
},
{
"path": "src/client/containers/AppSidebar.tsx",
"chars": 4214,
"preview": "import React from 'react'\nimport { Plus } from 'react-feather'\nimport { useDispatch, useSelector } from 'react-redux'\n\ni"
},
{
"path": "src/client/containers/CategoryList.tsx",
"chars": 7166,
"preview": "import React, { useRef, useState, useEffect } from 'react'\nimport { useSelector, useDispatch } from 'react-redux'\nimport"
},
{
"path": "src/client/containers/CategoryOption.tsx",
"chars": 6435,
"preview": "import React from 'react'\nimport { useSelector, useDispatch } from 'react-redux'\nimport { Draggable } from 'react-beauti"
},
{
"path": "src/client/containers/ContextMenu.tsx",
"chars": 5529,
"preview": "import ReactDOM from 'react-dom'\nimport React, { useEffect, useState, createContext } from 'react'\nimport { useDispatch,"
},
{
"path": "src/client/containers/ContextMenuOptions.tsx",
"chars": 7627,
"preview": "import React, { useContext } from 'react'\nimport { ArrowUp, Download, Star, Trash, X, Edit2, Clipboard } from 'react-fea"
},
{
"path": "src/client/containers/KeyboardShortcuts.tsx",
"chars": 4850,
"preview": "import React from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport prettier from 'prettier/standalo"
},
{
"path": "src/client/containers/NoteEditor.tsx",
"chars": 3998,
"preview": "import dayjs from 'dayjs'\nimport React from 'react'\nimport { Controlled as CodeMirror } from 'react-codemirror2'\nimport "
},
{
"path": "src/client/containers/NoteList.tsx",
"chars": 10414,
"preview": "import React, { useEffect, useRef, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport"
},
{
"path": "src/client/containers/NoteMenuBar.tsx",
"chars": 6842,
"preview": "import React, { useEffect, useState } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n Eye"
},
{
"path": "src/client/containers/SettingsModal.tsx",
"chars": 11494,
"preview": "import React, { useEffect, useRef } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport {\n X,\n "
},
{
"path": "src/client/containers/TakeNoteApp.tsx",
"chars": 4220,
"preview": "import React, { useEffect } from 'react'\nimport { useDispatch, useSelector } from 'react-redux'\nimport { Helmet, HelmetP"
},
{
"path": "src/client/contexts/TempStateContext.tsx",
"chars": 979,
"preview": "import React, { createContext, FunctionComponent, useContext, useState } from 'react'\n\ninterface TempStateContextInterfa"
},
{
"path": "src/client/global.d.ts",
"chars": 237,
"preview": "declare module '*.png' {\n const value: any\n export default value\n}\n\ndeclare module '*.svg' {\n const value: any\n expo"
},
{
"path": "src/client/index.tsx",
"chars": 841,
"preview": "import React from 'react'\nimport { render } from 'react-dom'\nimport { Provider } from 'react-redux'\nimport { Router } fr"
},
{
"path": "src/client/router/PrivateRoute.tsx",
"chars": 556,
"preview": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { Redirect, Route, RouteProps } from 'react-r"
},
{
"path": "src/client/router/PublicRoute.tsx",
"chars": 557,
"preview": "import React from 'react'\nimport { useSelector } from 'react-redux'\nimport { Redirect, Route, RouteProps } from 'react-r"
},
{
"path": "src/client/sagas/index.ts",
"chars": 3492,
"preview": "import { all, put, takeLatest, select } from 'redux-saga/effects'\nimport dayjs from 'dayjs'\nimport axios from 'axios'\n\ni"
},
{
"path": "src/client/selectors/index.ts",
"chars": 361,
"preview": "import { RootState } from '@/types'\n\nexport const getSettings = (state: RootState) => state.settingsState\nexport const g"
},
{
"path": "src/client/serviceWorker.ts",
"chars": 3015,
"preview": "const isLocalhost = Boolean(\n window.location.hostname === 'localhost' ||\n // [::1] is the IPv6 localhost address.\n "
},
{
"path": "src/client/setupTests.ts",
"chars": 49,
"preview": "import '@testing-library/jest-dom/extend-expect'\n"
},
{
"path": "src/client/slices/auth.ts",
"chars": 1040,
"preview": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\nimport { AuthState } from '@/types'\n\nexport const initial"
},
{
"path": "src/client/slices/category.ts",
"chars": 3074,
"preview": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\nimport { CategoryItem, CategoryState } from '@/types'\n\nco"
},
{
"path": "src/client/slices/index.ts",
"chars": 549,
"preview": "import { combineReducers, Reducer } from 'redux'\n\nimport authReducer from '@/slices/auth'\nimport categoryReducer from '@"
},
{
"path": "src/client/slices/note.ts",
"chars": 10674,
"preview": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\nimport { v4 as uuid } from 'uuid'\n\nimport { Folder, NotesS"
},
{
"path": "src/client/slices/settings.ts",
"chars": 1849,
"preview": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\nimport { SettingsState } from '@/types'\nimport { NotesSor"
},
{
"path": "src/client/slices/sync.ts",
"chars": 896,
"preview": "import { createSlice, PayloadAction } from '@reduxjs/toolkit'\n\nimport { SyncState, SyncPayload } from '@/types'\n\nexport "
},
{
"path": "src/client/styles/_app-sidebar.scss",
"chars": 6708,
"preview": ".app-sidebar {\n background: $app-sidebar-color;\n color: $light-font-color;\n display: flex;\n flex-direction: column;\n"
},
{
"path": "src/client/styles/_buttons.scss",
"chars": 1800,
"preview": "%buttons {\n -webkit-appearance: none;\n display: inline-block;\n border: 2px solid $primary;\n border-radius: 0.3rem;\n "
},
{
"path": "src/client/styles/_dark.scss",
"chars": 6328,
"preview": "$dark-sidebar: #333;\n$dark-editor: #3f3f3f;\n\n.dark {\n a {\n color: lighten($primary, 8%);\n &:hover {\n color: "
},
{
"path": "src/client/styles/_editor.scss",
"chars": 605,
"preview": ".empty-editor {\n background: $light-theme-background;\n width: 100%;\n}\n\n.editor {\n overflow-y: auto;\n}\n\n.CodeMirror {\n"
},
{
"path": "src/client/styles/_forms.scss",
"chars": 419,
"preview": "%forms {\n display: block;\n border-radius: 0.3rem;\n border: 1px solid $accent-gray;\n padding: 0.75rem;\n outline: non"
},
{
"path": "src/client/styles/_helpers.scss",
"chars": 3835,
"preview": ".sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: "
},
{
"path": "src/client/styles/_landing-page.scss",
"chars": 2320,
"preview": ".landing-page {\n a {\n color: $primary;\n text-decoration: none;\n font-weight: 600;\n &.button {\n font-si"
},
{
"path": "src/client/styles/_layout.scss",
"chars": 3013,
"preview": ".loading {\n height: 100vh;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n backgrou"
},
{
"path": "src/client/styles/_light-theme.scss",
"chars": 66,
"preview": ".cm-s-base16-light {\n span.cm-string {\n color: #90a959;\n }\n}\n"
},
{
"path": "src/client/styles/_mixins.scss",
"chars": 164,
"preview": "@mixin small-breakpoint {\n @media (min-width: #{$tablet}) {\n @content;\n }\n}\n\n@mixin large-breakpoint {\n @media (mi"
},
{
"path": "src/client/styles/_modal.scss",
"chars": 2671,
"preview": ".dimmer {\n position: fixed;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center"
},
{
"path": "src/client/styles/_new-moon.scss",
"chars": 2148,
"preview": ".cm-s-new-moon .CodeMirror-gutters {\n background: #333333 !important;\n // Remove white border when line numbers are di"
},
{
"path": "src/client/styles/_note-menu-bar.scss",
"chars": 1150,
"preview": ".note-menu-bar {\n height: 39px;\n border-top: 1px solid darken($note-sidebar-color, 5%);\n background: $note-sidebar-co"
},
{
"path": "src/client/styles/_note-sidebar.scss",
"chars": 2801,
"preview": ".note-sidebar {\n background: $note-sidebar-color;\n border-right: 1px solid darken($note-sidebar-color, 10%);\n height:"
},
{
"path": "src/client/styles/_previewer.scss",
"chars": 3357,
"preview": ".previewer {\n position: relative;\n max-height: calc(100vh);\n overflow-y: auto;\n background: #fafafa;\n color: #40404"
},
{
"path": "src/client/styles/_scaffolding.scss",
"chars": 498,
"preview": "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 margi"
},
{
"path": "src/client/styles/_tabs.scss",
"chars": 978,
"preview": ".tabs {\n margin-top: 1.5rem;\n display: flex;\n flex-direction: row;\n align-items: flex-start;\n justify-content: flex"
},
{
"path": "src/client/styles/_variables.scss",
"chars": 1382,
"preview": "// Sizes\n$app-sidebar-width: 240px;\n$note-sidebar-width: 340px;\n\n$mobile: 575px;\n$tablet: 768px;\n$desktop: 991px;\n\n$note"
},
{
"path": "src/client/styles/index.scss",
"chars": 363,
"preview": "@import 'variables';\n@import 'mixins';\n@import 'scaffolding';\n@import 'layout';\n@import 'app-sidebar';\n@import 'note-sid"
},
{
"path": "src/client/types/index.ts",
"chars": 2934,
"preview": "import React from 'react'\n\nimport { Folder, NotesSortKey } from '@/utils/enums'\nimport { sync } from '@/slices/sync'\n\n//"
},
{
"path": "src/client/utils/constants.ts",
"chars": 1137,
"preview": "import { Folder, NotesSortKey, DirectionText } from '@/utils/enums'\n\nexport const folderMap: Record<Folder, string> = {\n"
},
{
"path": "src/client/utils/enums.ts",
"chars": 775,
"preview": "export enum Folder {\n ALL = 'ALL',\n CATEGORY = 'CATEGORY',\n FAVORITES = 'FAVORITES',\n SCRATCHPAD = 'SCRATCHPAD',\n T"
},
{
"path": "src/client/utils/helpers.ts",
"chars": 8196,
"preview": "import dayjs from 'dayjs'\nimport { v4 as uuid } from 'uuid'\nimport JSZip from 'jszip'\nimport { Action } from 'redux'\nimp"
},
{
"path": "src/client/utils/history.ts",
"chars": 86,
"preview": "import { createBrowserHistory } from 'history'\n\nexport default createBrowserHistory()\n"
},
{
"path": "src/client/utils/hooks.ts",
"chars": 1919,
"preview": "import mousetrap from 'mousetrap'\nimport { useEffect, useRef } from 'react'\n\nimport 'mousetrap-global-bind'\n\nconst noop "
},
{
"path": "src/client/utils/notesSortStrategies.ts",
"chars": 1501,
"preview": "import { NoteItem } from '@/types'\n\nimport { getNoteTitle } from './helpers'\nimport { NotesSortKey } from './enums'\n\nexp"
},
{
"path": "src/client/utils/reactMarkdownPlugins.ts",
"chars": 1755,
"preview": "import visit from 'unist-util-visit'\n\n// This regexp will match any string starting with a # followed by 6 alphanumeric "
},
{
"path": "src/resources/LabelText.ts",
"chars": 1053,
"preview": "// Default Labels\nexport enum LabelText {\n ADD_CATEGORY = 'Add category',\n COLLAPSE_CATEGORY = 'Collapse Category List"
},
{
"path": "src/resources/TestID.ts",
"chars": 2686,
"preview": "// data-testid\nexport enum TestID {\n ACTION_BUTTON = 'action-button',\n ADD_CATEGORY_BUTTON = 'add-category-button',\n "
},
{
"path": "src/server/handlers/auth.ts",
"chars": 4930,
"preview": "import { Request, Response } from 'express'\nimport axios from 'axios'\nimport * as dotenv from 'dotenv'\n\nimport { welcome"
},
{
"path": "src/server/handlers/sync.ts",
"chars": 4235,
"preview": "import { Request, Response } from 'express'\nimport dayjs from 'dayjs'\n\nimport { SDK } from '../utils/helpers'\nimport { M"
},
{
"path": "src/server/index.ts",
"chars": 207,
"preview": "import initializeServer from './initializeServer'\nimport router from './router'\n\nconst app = initializeServer(router)\n\na"
},
{
"path": "src/server/initializeServer.ts",
"chars": 924,
"preview": "import path from 'path'\n\nimport express, { Router } from 'express'\nimport cookieParser from 'cookie-parser'\nimport cors "
},
{
"path": "src/server/middleware/checkAuth.ts",
"chars": 405,
"preview": "import { Request, Response, NextFunction } from 'express'\n\nconst checkAuth = async (request: Request, response: Response"
},
{
"path": "src/server/middleware/getUser.ts",
"chars": 517,
"preview": "import { Request, Response, NextFunction } from 'express'\n\nimport { SDK } from '../utils/helpers'\nimport { Method } from"
},
{
"path": "src/server/router/auth.ts",
"chars": 367,
"preview": "import express from 'express'\nimport * as dotenv from 'dotenv'\n\nimport authHandler from '../handlers/auth'\nimport checkA"
},
{
"path": "src/server/router/index.ts",
"chars": 216,
"preview": "import express from 'express'\n\nimport authRoutes from './auth'\nimport syncRoutes from './sync'\n\nconst router = express.R"
},
{
"path": "src/server/router/sync.ts",
"chars": 414,
"preview": "import express from 'express'\n\nimport syncHandler from '../handlers/sync'\nimport checkAuth from '../middleware/checkAuth"
},
{
"path": "src/server/utils/constants.ts",
"chars": 191,
"preview": "const isProduction = process.env.NODE_ENV === 'production'\n\nexport const thirtyDayCookie = {\n maxAge: 60 * 60 * 1000 * "
},
{
"path": "src/server/utils/data/scratchpadNote.ts",
"chars": 277,
"preview": "import { v4 as uuid } from 'uuid'\nimport dayjs from 'dayjs'\n\nexport const scratchpadNote = {\n id: uuid(),\n text: `# Sc"
},
{
"path": "src/server/utils/data/welcomeNote.ts",
"chars": 1785,
"preview": "import { v4 as uuid } from 'uuid'\nimport dayjs from 'dayjs'\n\nconst markdown = `# Welcome to Takenote!\n\nTakeNote is a fre"
},
{
"path": "src/server/utils/enums.ts",
"chars": 110,
"preview": "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",
"chars": 336,
"preview": "import axios from 'axios'\n\nimport { Method } from './enums'\n\nexport function SDK(method: Method, path: string, accessTok"
},
{
"path": "tests/__mocks__/styleMock.ts",
"chars": 61,
"preview": "// __mocks__/styleMock.js\n\n// @ts-ignore\nmodule.exports = {}\n"
},
{
"path": "tests/e2e/integration/category.test.ts",
"chars": 3829,
"preview": "// category.spec.ts\n// Tests for manipulating note categories\n\nimport {\n addCategory,\n assertCategoryDoesNotExist,\n a"
},
{
"path": "tests/e2e/integration/note.test.ts",
"chars": 20066,
"preview": "// note.test.ts\n// Tests for managing notes (create, trash, favorite, etc)\n\nimport { LabelText } from '@resources/LabelT"
},
{
"path": "tests/e2e/integration/settings.test.ts",
"chars": 6482,
"preview": "// settings.test.ts\n// Tests for functionality available in the settings menu\n\nimport { TestID } from '@resources/TestID"
},
{
"path": "tests/e2e/plugins/cy-ts-preprocessor.js",
"chars": 876,
"preview": "const path = require('path')\n\nconst wp = require('@cypress/webpack-preprocessor')\n\nconst webpackOptions = {\n resolve: {"
},
{
"path": "tests/e2e/plugins/index.js",
"chars": 283,
"preview": "const clipboardy = require('clipboardy')\n\nconst cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor')\n\nmodule."
},
{
"path": "tests/e2e/support/commands.js",
"chars": 1333,
"preview": "import '@testing-library/cypress/add-commands'\nimport 'cypress-file-upload'\n\nCypress.Commands.add('dragAndDrop', { prevS"
},
{
"path": "tests/e2e/support/index.js",
"chars": 564,
"preview": "import './commands'\n\n// Since before unload alert hangs console tests, the alert has to be disabled\n// Check https://git"
},
{
"path": "tests/e2e/tsconfig.json",
"chars": 254,
"preview": "{\n \"extends\": \"../../tsconfig.json\",\n \"include\": [\"../node_modules/cypress\", \"**/*.ts\"],\n \"compilerOptions\": {\n \"t"
},
{
"path": "tests/e2e/utils/testCategoryHelperUtils.ts",
"chars": 2914,
"preview": "// testCategoryHelperUtils.ts\n// Utility functions for use in category tests\n\nimport { TestID } from '@resources/TestID'"
},
{
"path": "tests/e2e/utils/testHelperEnums.ts",
"chars": 202,
"preview": "// testHelperEnums.ts\n// Default enumerated values that can be used in tests\n\nconst entryPoint = '/app'\nconst dynamicTim"
},
{
"path": "tests/e2e/utils/testHelperUtils.ts",
"chars": 2825,
"preview": "// testHelperUtils.ts\n// Utility functions used by all test specs\n\nimport { LabelText } from '@resources/LabelText'\nimpo"
},
{
"path": "tests/e2e/utils/testNotesHelperUtils.ts",
"chars": 4761,
"preview": "// testNotesHelperUtils.ts\n// Utility functions for use in note tests\n\nimport { LabelText } from '@resources/LabelText'\n"
},
{
"path": "tests/e2e/utils/testSettingsUtils.ts",
"chars": 3569,
"preview": "// testHelperUtils.ts\n// Utility functions for use in settings tests\n\nimport { TestID } from '@resources/TestID'\n\nimport"
},
{
"path": "tests/unit/client/components/AppSidebar/ActionButton.test.tsx",
"chars": 738,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jes"
},
{
"path": "tests/unit/client/components/AppSidebar/AddCategoryButton.test.tsx",
"chars": 645,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jes"
},
{
"path": "tests/unit/client/components/AppSidebar/AddCategoryForm.test.tsx",
"chars": 748,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jes"
},
{
"path": "tests/unit/client/components/AppSidebar/CollapseCategoryButton.test.tsx",
"chars": 712,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jes"
},
{
"path": "tests/unit/client/components/AppSidebar/FolderOption.test.tsx",
"chars": 718,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jes"
},
{
"path": "tests/unit/client/components/AppSidebar/ScratchpadOption.test.tsx",
"chars": 546,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jes"
},
{
"path": "tests/unit/client/components/LastSyncedNotification.test.tsx",
"chars": 1732,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport dayjs from 'dayjs'\nimport '@testing-lib"
},
{
"path": "tests/unit/client/components/NoteList/NoteListButton.test.tsx",
"chars": 1028,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jes"
},
{
"path": "tests/unit/client/components/NoteList/SearchBar.test.tsx",
"chars": 1073,
"preview": "import React, { createRef } from 'react'\nimport { render, fireEvent } from '@testing-library/react'\nimport '@testing-lib"
},
{
"path": "tests/unit/client/components/SettingsModal/IconButton.test.tsx",
"chars": 1057,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jes"
},
{
"path": "tests/unit/client/components/SettingsModal/IconButtonUploader.test.tsx",
"chars": 744,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimport 'jes"
},
{
"path": "tests/unit/client/components/Switch.test.tsx",
"chars": 449,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\n\nimport { Switch, SwitchProps } from '@/compon"
},
{
"path": "tests/unit/client/components/editor/EditorEmpty.test.tsx",
"chars": 901,
"preview": "import React from 'react'\nimport { render, screen } from '@testing-library/react'\nimport '@testing-library/jest-dom'\nimp"
},
{
"path": "tests/unit/client/components/editor/PreviewEditor.test.tsx",
"chars": 2115,
"preview": "import React from 'react'\nimport { render } from '@testing-library/react'\n\nimport '@testing-library/jest-dom'\nimport 'je"
},
{
"path": "tests/unit/client/containers/ContextMenuOptions.test.tsx",
"chars": 3651,
"preview": "import React from 'react'\n\nimport { TestID } from '@resources/TestID'\nimport { ContextMenuOptions, ContextMenuOptionsPro"
},
{
"path": "tests/unit/client/containers/TakeNoteApp.test.tsx",
"chars": 2498,
"preview": "import React from 'react'\nimport { mocked } from 'ts-jest/utils'\nimport { waitFor } from '@testing-library/react'\nimport"
},
{
"path": "tests/unit/client/slices/auth.test.ts",
"chars": 2090,
"preview": "import { PayloadAction } from '@reduxjs/toolkit'\n\nimport reducer, {\n initialState,\n login,\n loginError,\n loginSucces"
},
{
"path": "tests/unit/client/slices/category.test.ts",
"chars": 5774,
"preview": "import { PayloadAction } from '@reduxjs/toolkit'\n\nimport reducer, {\n initialState,\n addCategory,\n updateCategory,\n d"
},
{
"path": "tests/unit/client/slices/note.test.ts",
"chars": 23402,
"preview": "import { PayloadAction } from '@reduxjs/toolkit'\nimport dayjs from 'dayjs'\n\nimport reducer, {\n addNote,\n initialState,"
},
{
"path": "tests/unit/client/slices/settings.test.ts",
"chars": 1543,
"preview": "import { PayloadAction } from '@reduxjs/toolkit'\n\nimport reducer, {\n initialState,\n toggleSettingsModal,\n togglePrevi"
},
{
"path": "tests/unit/client/slices/sync.test.ts",
"chars": 1602,
"preview": "import { PayloadAction } from '@reduxjs/toolkit'\n\nimport reducer, { initialState, setPendingSync, sync, syncError, syncS"
},
{
"path": "tests/unit/client/testHelpers.tsx",
"chars": 1073,
"preview": "import { render } from '@testing-library/react'\nimport { createMemoryHistory, MemoryHistory } from 'history'\nimport Reac"
},
{
"path": "tests/unit/client/utils/index.test.ts",
"chars": 3592,
"preview": "import dayjs from 'dayjs'\n\nimport { getNoteTitle, getWebsiteTitle, getActiveNoteFromShortUuid } from '@/utils/helpers'\ni"
},
{
"path": "tests/unit/server/middleware/checkAuth.test.ts",
"chars": 1009,
"preview": "import checkAuth from '../../../../src/server/middleware/checkAuth'\n\ndescribe(`checkAuth middleware`, () => {\n let requ"
},
{
"path": "tsconfig.json",
"chars": 734,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"outDir\": \"./dist\",\n \"strict\": true,\n \"forceConsistentCasingInFil"
}
]
About this extraction
This page contains the full source code of the taniarascia/takenote GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 164 files (404.2 KB), approximately 109.9k tokens, and a symbol index with 79 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.