├── .gitignore ├── .travis.yml ├── license ├── main ├── index.js ├── notify.js ├── static │ ├── icon.icns │ ├── icon.ico │ └── icon.png ├── updater.js └── utils │ └── config.js ├── media ├── banner.png ├── linux.png ├── macos.png ├── medium.png ├── pricing.png ├── product-hunt.png └── windows.png ├── package.json ├── readme.md ├── renderer ├── components │ ├── content.js │ ├── empty-state.js │ ├── hero.js │ ├── home │ │ ├── backlog.js │ │ ├── done.js │ │ └── today.js │ ├── navigation.js │ ├── settings │ │ ├── account.js │ │ ├── app-info.js │ │ ├── identity.js │ │ ├── item.js │ │ └── social.js │ ├── sortable │ │ ├── drag-handle.js │ │ ├── sortable-component.js │ │ ├── sortable-item.js │ │ └── sortable-list.js │ ├── task │ │ ├── index.js │ │ ├── task-actions.js │ │ ├── task-check.js │ │ └── task-project.js │ └── win-controls.js ├── icons │ ├── arrow-right.js │ ├── award.js │ ├── download.js │ ├── dragger.js │ ├── github.js │ ├── layers.js │ ├── logo.js │ ├── product-hunt.js │ ├── settings.js │ ├── trash.js │ ├── twitter.js │ └── upload.js ├── layouts │ └── page.js ├── next.config.js ├── pages │ ├── add.js │ ├── edit.js │ ├── home.js │ ├── login.js │ ├── onboard.js │ ├── settings.js │ ├── signup.js │ └── start.js ├── services │ ├── api.js │ ├── cookies.js │ ├── local-storage.js │ ├── notify.js │ └── settings.js ├── static │ └── icon.ico ├── theme │ ├── index.js │ └── utils.js └── ui │ ├── button-link.js │ ├── button.js │ ├── input.js │ └── row.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist 3 | renderer/.next 4 | renderer/out 5 | 6 | # dependencies 7 | node_modules 8 | 9 | # logs 10 | npm-debug.log 11 | package-lock.json 12 | 13 | # Certificates 14 | Certificates.p12 15 | 16 | ## .files 17 | .env 18 | .env.production 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | - '7' 5 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Bu Kinoshita (https://bukinoshita.io) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Native 4 | const { format } = require('url') 5 | const { join } = require('path') 6 | const { platform } = require('os') 7 | 8 | // Packages 9 | const { BrowserWindow, app, Menu } = require('electron') 10 | const isDev = require('electron-is-dev') 11 | const prepareNext = require('electron-next') 12 | const { resolve } = require('app-root-path') 13 | 14 | // Utils 15 | const { getConfig } = require('./utils/config') 16 | const autoUpdater = require('./updater') 17 | 18 | // Prepare the renderer once the app is ready 19 | app.on('ready', async () => { 20 | await prepareNext('./renderer') 21 | 22 | app.config = await getConfig() 23 | 24 | const mainWindow = new BrowserWindow({ 25 | width: 320, 26 | height: 580, 27 | minWidth: 320, 28 | minHeight: 580, 29 | maxWidth: 320, 30 | maxHeight: 580, 31 | resizable: true, 32 | backgroundColor: '#000000', 33 | frame: platform() !== 'win32', 34 | titleBarStyle: 'hiddenInset', 35 | icon: 36 | platform() === 'win32' 37 | ? join(__dirname, 'main/static/icon.ico') 38 | : join(__dirname, 'main/static/icon.icns') 39 | }) 40 | 41 | const devPath = 'http://localhost:8000/start' 42 | 43 | const prodPath = format({ 44 | pathname: resolve('renderer/out/start/index.html'), 45 | protocol: 'file:', 46 | slashes: true 47 | }) 48 | 49 | const url = isDev ? devPath : prodPath 50 | 51 | const template = [ 52 | { 53 | label: 'Application', 54 | submenu: [ 55 | { 56 | label: 'About Application', 57 | selector: 'orderFrontStandardAboutPanel:' 58 | }, 59 | { type: 'separator' }, 60 | { 61 | label: 'Quit', 62 | accelerator: 'Command+Q', 63 | click: () => app.quit() 64 | } 65 | ] 66 | }, 67 | { 68 | label: 'Edit', 69 | submenu: [ 70 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' }, 71 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }, 72 | { type: 'separator' }, 73 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' }, 74 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, 75 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, 76 | { 77 | label: 'Select All', 78 | accelerator: 'CmdOrCtrl+A', 79 | selector: 'selectAll:' 80 | } 81 | ] 82 | }, 83 | { 84 | label: 'View', 85 | submenu: [ 86 | { 87 | label: 'Developer Tools', 88 | accelerator: 'CmdOrCtrl+alt+I', 89 | click: (item, focusedWindow) => { 90 | const webContents = focusedWindow.webContents 91 | 92 | if (webContents.isDevToolsOpened()) { 93 | webContents.closeDevTools() 94 | } else { 95 | webContents.openDevTools({ mode: 'detach' }) 96 | } 97 | } 98 | } 99 | ] 100 | } 101 | ] 102 | 103 | if (platform() !== 'win32') { 104 | autoUpdater() 105 | } 106 | 107 | mainWindow.loadURL(url) 108 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)) 109 | }) 110 | 111 | // Quit the app once all windows are closed 112 | app.on('window-all-closed', app.quit) 113 | -------------------------------------------------------------------------------- /main/notify.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | const { shell, Notification } = require('electron') 3 | const { resolve } = require('app-root-path') 4 | 5 | const icon = resolve('./static/icon.ico') 6 | 7 | module.exports = ({ title, body, url, onClick }) => { 8 | const specs = { 9 | title, 10 | body, 11 | icon, 12 | silent: true 13 | } 14 | 15 | const notification = new Notification(specs) 16 | 17 | if (url || onClick) { 18 | notification.on('click', () => { 19 | if (onClick) { 20 | return onClick() 21 | } 22 | 23 | shell.openExternal(url) 24 | }) 25 | } 26 | 27 | notification.show() 28 | console.log(`[Notification] ${title}: ${body}`) 29 | } 30 | -------------------------------------------------------------------------------- /main/static/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukinoshita/taskr/83f0a65c6c1df5e888268511759ed53bccb7b46c/main/static/icon.icns -------------------------------------------------------------------------------- /main/static/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukinoshita/taskr/83f0a65c6c1df5e888268511759ed53bccb7b46c/main/static/icon.ico -------------------------------------------------------------------------------- /main/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukinoshita/taskr/83f0a65c6c1df5e888268511759ed53bccb7b46c/main/static/icon.png -------------------------------------------------------------------------------- /main/updater.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | const { app, autoUpdater } = require('electron') 5 | const isDev = require('electron-is-dev') 6 | 7 | // Utils 8 | const notify = require('./notify') 9 | 10 | module.exports = () => { 11 | if (!isDev) { 12 | const server = 'https://taskr.now.sh' 13 | const feed = `${server}/update/${process.platform}/${app.getVersion()}` 14 | 15 | autoUpdater.setFeedURL(feed) 16 | 17 | autoUpdater.on('update-downloaded', () => { 18 | autoUpdater.quitAndInstall() 19 | app.quit() 20 | }) 21 | 22 | autoUpdater.on('update-available', () => { 23 | notify({ 24 | title: 'New update available', 25 | body: 'Found update for the app! Downloading...' 26 | }) 27 | }) 28 | 29 | autoUpdater.on('error', () => { 30 | notify({ 31 | title: 'Update failed', 32 | body: 'Unable to reach update server' 33 | }) 34 | }) 35 | 36 | return autoUpdater.checkForUpdates() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /main/utils/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { homedir } = require('os') 4 | const { join } = require('path') 5 | 6 | const { readJSON, writeJSON } = require('fs-extra') 7 | const pathExists = require('path-exists') 8 | 9 | const paths = { 10 | config: '.taskr.json', 11 | theming: '.taskr-theme.json' 12 | } 13 | 14 | for (const file in paths) { 15 | if (!{}.hasOwnProperty.call(paths, file)) { 16 | continue 17 | } 18 | 19 | paths[file] = join(homedir(), paths[file]) 20 | } 21 | 22 | const hasConfig = async () => { 23 | const configExists = await pathExists(paths.config) 24 | 25 | return configExists 26 | } 27 | 28 | const hasTheming = async () => { 29 | const themingExists = await pathExists(paths.theming) 30 | 31 | return themingExists 32 | } 33 | 34 | const createConfig = async () => { 35 | const cfg = { 36 | pro: false, 37 | lastUpdate: new Date() 38 | } 39 | 40 | await writeJSON(paths.config, cfg, { 41 | spaces: 2 42 | }) 43 | } 44 | 45 | const createTheming = async () => { 46 | const theming = { 47 | white: '#ffffff', 48 | black: '#000000', 49 | romanSilver: '#868e96', 50 | darkMediumGray: '#aaaaaa', 51 | brightTurquoise: '00e7c0', 52 | fontSizeBase: '16px' 53 | } 54 | 55 | await writeJSON(paths.theming, theming, { 56 | spaces: 2 57 | }) 58 | } 59 | 60 | exports.getConfig = async () => { 61 | if (!await hasConfig() && !await hasTheming()) { 62 | await createConfig() 63 | await createTheming() 64 | 65 | const user = await readJSON(paths.config) 66 | const theme = await readJSON(paths.theming) 67 | 68 | return { user, theme } 69 | } 70 | 71 | const user = await readJSON(paths.config) 72 | const theme = await readJSON(paths.theming) 73 | 74 | return { user, theme } 75 | } 76 | -------------------------------------------------------------------------------- /media/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukinoshita/taskr/83f0a65c6c1df5e888268511759ed53bccb7b46c/media/banner.png -------------------------------------------------------------------------------- /media/linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukinoshita/taskr/83f0a65c6c1df5e888268511759ed53bccb7b46c/media/linux.png -------------------------------------------------------------------------------- /media/macos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukinoshita/taskr/83f0a65c6c1df5e888268511759ed53bccb7b46c/media/macos.png -------------------------------------------------------------------------------- /media/medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukinoshita/taskr/83f0a65c6c1df5e888268511759ed53bccb7b46c/media/medium.png -------------------------------------------------------------------------------- /media/pricing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukinoshita/taskr/83f0a65c6c1df5e888268511759ed53bccb7b46c/media/pricing.png -------------------------------------------------------------------------------- /media/product-hunt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukinoshita/taskr/83f0a65c6c1df5e888268511759ed53bccb7b46c/media/product-hunt.png -------------------------------------------------------------------------------- /media/windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukinoshita/taskr/83f0a65c6c1df5e888268511759ed53bccb7b46c/media/windows.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Taskr", 3 | "version": "1.0.0", 4 | "description": "A simple task manager app", 5 | "main": "main/index.js", 6 | "repository": "https://github.com/bukinoshita/taskr.git", 7 | "author": "Bu Kinoshita ", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "electron .", 11 | "build": "next build renderer && next export renderer", 12 | "dist": "npm run build && build --dir", 13 | "prod": "electron-builder", 14 | "test": "xo", 15 | "postinstall": "electron-builder install-app-deps", 16 | "precommit": "lint-staged" 17 | }, 18 | "build": { 19 | "appId": "taskr", 20 | "files": [ 21 | "**/*", 22 | "!renderer", 23 | "renderer/out" 24 | ], 25 | "win": { 26 | "target": [ 27 | "squirrel" 28 | ], 29 | "icon": "main/static/icon.ico" 30 | }, 31 | "mac": { 32 | "category": "public.app-category.developer-tools", 33 | "icon": "main/static/icon.icns" 34 | }, 35 | "linux": { 36 | "target": [ 37 | "AppImage", 38 | "deb" 39 | ] 40 | } 41 | }, 42 | "dependencies": { 43 | "app-root-path": "^2.1.0", 44 | "axios": "^0.18.1", 45 | "babel-plugin-inline-dotenv": "^1.2.1", 46 | "babel-plugin-transform-inline-environment-variables": "^0.4.3", 47 | "classnames": "^2.2.6", 48 | "electron-is-dev": "^1.0.1", 49 | "electron-next": "^3.1.5", 50 | "fs-extra": "^7.0.1", 51 | "ngprogress": "^1.1.3", 52 | "nprogress": "^0.2.0", 53 | "path-exists": "^3.0.0", 54 | "prop-types": "^15.7.2", 55 | "react-cookies": "^0.1.0", 56 | "react-hash-avatar": "^0.0.2", 57 | "react-render-html": "^0.6.0", 58 | "react-sortable-hoc": "^1.6.1", 59 | "uid-promise": "^1.1.0" 60 | }, 61 | "devDependencies": { 62 | "electron": "^4.0.6", 63 | "electron-builder": "^20.38.5", 64 | "electron-builder-squirrel-windows": "^20.39.0", 65 | "eslint-config-prettier": "^4.1.0", 66 | "eslint-plugin-react": "^7.12.4", 67 | "husky": "^1.3.1", 68 | "lint-staged": "^8.1.5", 69 | "next": "^8.0.3", 70 | "prettier": "^1.16.4", 71 | "react": "^16.8.3", 72 | "react-dom": "^16.8.3", 73 | "xo": "^0.24.0" 74 | }, 75 | "xo": { 76 | "extends": [ 77 | "prettier", 78 | "prettier/react", 79 | "plugin:react/recommended" 80 | ], 81 | "rules": { 82 | "react/no-unescaped-entities": 0, 83 | "react/react-in-jsx-scope": 0, 84 | "react/display-name": 0, 85 | "react/prop-types": 0, 86 | "default-case": 0, 87 | "no-case-declarations": 0, 88 | "no-return-assign": 0, 89 | "new-cap": 0, 90 | "import/prefer-default-export": 0 91 | }, 92 | "ignores": [ 93 | "node_modules" 94 | ], 95 | "globals": [ 96 | "localStorage", 97 | "document" 98 | ] 99 | }, 100 | "lint-staged": { 101 | "*.js": [ 102 | "yarn test", 103 | "prettier --semi false --single-quote --write", 104 | "git add" 105 | ] 106 | }, 107 | "babel": { 108 | "presets": [ 109 | "next/babel" 110 | ], 111 | "env": { 112 | "development": { 113 | "plugins": [ 114 | "inline-dotenv" 115 | ] 116 | }, 117 | "production": { 118 | "plugins": [ 119 | "transform-inline-environment-variables" 120 | ] 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/bukinoshita/taskr/blob/master/media/banner.png) 2 | 3 |

4 | 5 | Build Status 6 | 7 | 8 | 9 | XO code style 10 | 11 | 12 | 13 | styled with prettier 14 | 15 |

16 | 17 | ## Downloads 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ## Contribute 32 | 33 | To run it locally on your own computer: 34 | 35 | * [Fork](https://help.github.com/articles/fork-a-repo/) this repository 36 | * [Clone](https://help.github.com/articles/cloning-a-repository/) it to your 37 | local device 38 | * [Install](https://yarnpkg.com/en/docs/cli/install) the dependencies 39 | * [Run](https://github.com/bukinoshita/taskr/blob/master/package.json#L10) the 40 | project 41 | 42 | To make sure that your code works in the finished application, you can generate 43 | the binaries like this: 44 | 45 | ```bash 46 | $ yarn dist 47 | ``` 48 | 49 | After that, you will see the binary in the `./dist` folder! 50 | 51 | ## About 52 | 53 | 54 | 55 | 56 | 57 |

58 | 59 | 60 | 61 |

62 | 63 | ## Pricing 64 | 65 | 66 | 67 | ## Related 68 | 69 | * [taskr-design](https://github.com/bukinoshita/taskr-design) — Taskr design assets 70 | * [taskr-api](https://github.com/bukinoshita/taskr-api) — Taskr API for PRO users (private) 71 | * [taskr-ios](https://github.com/bukinoshita/taskr-ios) Taskr App for iOS (private) 72 | 73 | ## License 74 | 75 | MIT © [Bu Kinoshita](https://bukinoshita.io) 76 | -------------------------------------------------------------------------------- /renderer/components/content.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Content = ({ children }) => { 4 | return ( 5 |
6 | {children} 7 | 8 | 20 |
21 | ) 22 | } 23 | 24 | export default Content 25 | -------------------------------------------------------------------------------- /renderer/components/empty-state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Theme 4 | import { colors, typography } from './../theme' 5 | 6 | const EmptyState = ({ title }) => { 7 | return ( 8 |
9 |

You do not have any {title}.

10 | 11 | 29 |
30 | ) 31 | } 32 | 33 | export default EmptyState 34 | -------------------------------------------------------------------------------- /renderer/components/hero.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import Link from 'next/link' 5 | 6 | // Components 7 | import SettingsIcon from './../icons/settings' 8 | import LayersIcon from './../icons/layers' 9 | 10 | // Theme 11 | import { colors, typography } from './../theme' 12 | 13 | const Hero = ({ type, isSettings }) => { 14 | return ( 15 |
16 |

{type}

17 | 18 | {isSettings ? ( 19 | 20 |
21 | 22 |
23 | 24 | ) : ( 25 | 26 |
27 | 28 |
29 | 30 | )} 31 | 32 | 46 |
47 | ) 48 | } 49 | 50 | Hero.defaultProps = { 51 | type: 'Today' 52 | } 53 | 54 | export default Hero 55 | -------------------------------------------------------------------------------- /renderer/components/home/backlog.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Components 4 | import EmptyState from './../empty-state' 5 | import SortableComponent from './../sortable/sortable-component' 6 | 7 | const Backlog = ({ tasks, onDelete, onMove, onSortEnd }) => { 8 | const list = 9 | tasks.length === 0 ? ( 10 | 11 | ) : ( 12 | 18 | ) 19 | 20 | return
{list}
21 | } 22 | 23 | Backlog.defaultProps = { 24 | tasks: [] 25 | } 26 | 27 | export default Backlog 28 | -------------------------------------------------------------------------------- /renderer/components/home/done.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Components 4 | import EmptyState from './../empty-state' 5 | import Task from './../task' 6 | 7 | const Done = ({ tasks, onDelete, onMove }) => { 8 | const list = 9 | tasks.length === 0 ? ( 10 | 11 | ) : ( 12 | tasks.map(task => ( 13 | 14 | )) 15 | ) 16 | 17 | return
    {list}
18 | } 19 | 20 | Done.defaultProps = { 21 | tasks: [] 22 | } 23 | 24 | export default Done 25 | -------------------------------------------------------------------------------- /renderer/components/home/today.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Components 4 | import EmptyState from './../empty-state' 5 | import SortableComponent from './../sortable/sortable-component' 6 | 7 | const Today = ({ tasks, onDelete, onMove, onSortEnd }) => { 8 | const list = 9 | tasks.length === 0 ? ( 10 | 11 | ) : ( 12 | 18 | ) 19 | 20 | return
{list}
21 | } 22 | 23 | Today.defaultProps = { 24 | tasks: [] 25 | } 26 | 27 | export default Today 28 | -------------------------------------------------------------------------------- /renderer/components/navigation.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import Link from 'next/link' 5 | import PropTypes from 'prop-types' 6 | 7 | // Theme 8 | import { colors, typography } from './../theme' 9 | 10 | const Navigation = ({ list, tabSelected }) => { 11 | return ( 12 | 60 | ) 61 | } 62 | 63 | Navigation.propTypes = { 64 | list: PropTypes.array.isRequired 65 | } 66 | 67 | export default Navigation 68 | -------------------------------------------------------------------------------- /renderer/components/settings/account.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { shell } from 'electron' 5 | 6 | // Components 7 | import Item from './item' 8 | import ArrowRightIcon from './../../icons/arrow-right' 9 | import AwardIcon from './../../icons/award' 10 | import UploadIcon from './../../icons/upload' 11 | import DownloadIcon from './../../icons/download' 12 | import TrashIcon from './../../icons/trash' 13 | 14 | // Theme 15 | import { colors, typography } from './../../theme' 16 | 17 | const Account = ({ 18 | importUser, 19 | exportUser, 20 | clearHistory, 21 | selectChange, 22 | defaultOption 23 | }) => { 24 | return ( 25 |
    26 | {/* 27 |
  • shell.openExternal('https://gettaskr.now.sh/pro')}> 28 | 29 |
    30 |

    31 | Taskr Pro 14 days free trial 32 |

    33 |

    Cloud-sync, powerful plugins and more

    34 |
    35 | 36 | 37 |
  • 38 | */} 39 | 40 | 44 |
    45 | 49 |
    50 |
    51 | 52 | 57 | 58 | 59 | 60 | 65 | 66 | 67 | 68 | 73 | 74 | 75 | 76 | 132 |
133 | ) 134 | } 135 | 136 | export default Account 137 | -------------------------------------------------------------------------------- /renderer/components/settings/app-info.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { remote } from 'electron' 5 | 6 | // Components 7 | import Item from './item' 8 | import ArrowRightIcon from './../../icons/arrow-right' 9 | 10 | // Theme 11 | import { colors, typography } from './../../theme' 12 | 13 | const AppInfo = () => { 14 | const appVersion = remote && remote.app ? remote.app.getVersion() : '1.0.0' 15 | 16 | return ( 17 |
    18 | 19 | v{appVersion} 20 | 21 | 22 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 | 51 | 52 | 53 | 54 | 61 |
62 | ) 63 | } 64 | 65 | export default AppInfo 66 | -------------------------------------------------------------------------------- /renderer/components/settings/identity.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import reactHashAvatar from 'react-hash-avatar' 5 | import renderHTML from 'react-render-html' 6 | 7 | import ButtonLink from './../../ui/button-link' 8 | 9 | // Theme 10 | import { colors, typography } from './../../theme' 11 | 12 | const Identity = ({ user }) => { 13 | return ( 14 |
15 |
16 | {user && 17 | user.username && 18 | user.email && 19 | !user.subscription.trial.ended ? ( 20 |
21 |
22 | {renderHTML( 23 | reactHashAvatar(user.username, { size: 75, radius: '50px' }) 24 | )} 25 |

{user.username.substring(0, 2)}

26 | 27 |
Pro
28 |
29 | 30 |

31 | Your username is {user.username} 32 |

33 | 34 | {user.email} 35 |
36 | ) : ( 37 |
38 |

39 | You are on Free version. 40 |

41 | 42 | {/* 43 | Pro — 14 days free trial 44 | 45 | Login into your account 46 | 47 | */} 48 |
49 | )} 50 |
51 | 52 | 124 |
125 | ) 126 | } 127 | 128 | export default Identity 129 | -------------------------------------------------------------------------------- /renderer/components/settings/item.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { shell } from 'electron' 5 | 6 | // Theme 7 | import { colors, typography } from './../../theme' 8 | 9 | const Item = ({ children, name, description, url, onClick }) => { 10 | const hasClick = onClick ? onClick : undefined 11 | const hasUrl = url ? () => shell.openExternal(url) : undefined 12 | const clickFn = hasClick || hasUrl 13 | 14 | return ( 15 |
  • 16 |
    17 | {name} 18 |

    {description}

    19 |
    20 | 21 | {children} 22 | 23 | 56 |
  • 57 | ) 58 | } 59 | 60 | export default Item 61 | -------------------------------------------------------------------------------- /renderer/components/settings/social.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { shell } from 'electron' 5 | 6 | // Components 7 | import TwitterIcon from './../../icons/twitter' 8 | import GithubIcon from './../../icons/github' 9 | import ProductHuntIcon from './../../icons/product-hunt' 10 | 11 | const Social = () => { 12 | return ( 13 |
      14 |
    • shell.openExternal('https://twitter.com/gettaskrapp')}> 15 | 16 |
    • 17 | 18 |
    • 20 | shell.openExternal('https://github.com/bukinoshita/taskr') 21 | } 22 | > 23 | 24 |
    • 25 | 26 |
    • 28 | shell.openExternal('https://producthunt.com/posts/taskr') 29 | } 30 | > 31 | 32 |
    • 33 | 34 | 47 |
    48 | ) 49 | } 50 | 51 | export default Social 52 | -------------------------------------------------------------------------------- /renderer/components/sortable/drag-handle.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { SortableHandle } from 'react-sortable-hoc' 5 | 6 | // Components 7 | import DraggerIcon from './../../icons/dragger' 8 | 9 | const DragHandle = SortableHandle(() => ) 10 | 11 | export default DragHandle 12 | -------------------------------------------------------------------------------- /renderer/components/sortable/sortable-component.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Components 4 | import SortableList from './sortable-list' 5 | 6 | const SortableComponent = ({ tasks, onSortEnd, onDelete, onMove }) => { 7 | return ( 8 | 15 | ) 16 | } 17 | 18 | export default SortableComponent 19 | -------------------------------------------------------------------------------- /renderer/components/sortable/sortable-item.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { SortableElement } from 'react-sortable-hoc' 5 | 6 | // Components 7 | import Task from './../task' 8 | 9 | const SortableItem = SortableElement(({ task, onMove, onDelete }) => { 10 | return 11 | }) 12 | 13 | export default SortableItem 14 | -------------------------------------------------------------------------------- /renderer/components/sortable/sortable-list.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { SortableContainer } from 'react-sortable-hoc' 5 | 6 | // Components 7 | import SortableItem from './sortable-item' 8 | 9 | const SortableList = SortableContainer(({ tasks, onDelete, onMove }) => { 10 | return ( 11 |
      12 | {tasks.map((task, index) => ( 13 | 20 | ))} 21 |
    22 | ) 23 | }) 24 | 25 | export default SortableList 26 | -------------------------------------------------------------------------------- /renderer/components/task/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Components 4 | import TaskCheck from './task-check' 5 | import TaskProject from './task-project' 6 | import TaskActions from './task-actions' 7 | import DragHandle from './../sortable/drag-handle' 8 | 9 | // Theme 10 | import { colors, typography } from './../../theme' 11 | 12 | const Task = ({ task, onMove, onDelete }) => { 13 | const { title, description, project, type } = task 14 | const desc = 15 | description.length >= 30 ? `${description.substr(0, 50)}...` : description 16 | 17 | return ( 18 |
  • 19 | {type === 'done' ? : null} 20 | 21 |
    22 |
    23 |

    24 | {title} 25 |

    26 |

    {desc}

    27 |
    28 | 29 |
    30 | 31 |
    32 |
    33 | 34 | {type !== 'done' ? ( // eslint-disable-line no-negated-condition 35 |
    36 | 37 |
    38 | ) : null} 39 | 40 | 90 |
  • 91 | ) 92 | } 93 | 94 | export default Task 95 | -------------------------------------------------------------------------------- /renderer/components/task/task-actions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import Link from 'next/link' 5 | 6 | // Theme 7 | import { colors, typography } from './../../theme' 8 | 9 | const TaskActions = ({ task, onDelete, onMove }) => { 10 | const { id, type } = task 11 | const isToday = type === 'today' ? 'done' : 'today' 12 | const nextType = type === 'today' ? 'today' : 'backlog' 13 | 14 | return ( 15 |
      16 | {type !== 'done' ? ( // eslint-disable-line no-negated-condition 17 |
    • onMove(nextType, task)}>{isToday}
    • 18 | ) : null} 19 | 20 | {type !== 'backlog' ? ( // eslint-disable-line no-negated-condition 21 |
    • onMove('back', task)}>backlog
    • 22 | ) : null} 23 | 24 |
    • 25 | 26 | view 27 | 28 |
    • 29 | 30 |
    • onDelete(task)}>delete
    • 31 | 32 | 52 |
    53 | ) 54 | } 55 | 56 | export default TaskActions 57 | -------------------------------------------------------------------------------- /renderer/components/task/task-check.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Theme 4 | import { colors } from './../../theme' 5 | 6 | const TaskCheck = () => { 7 | return ( 8 |
    9 |
    43 | ) 44 | } 45 | 46 | export default TaskCheck 47 | -------------------------------------------------------------------------------- /renderer/components/task/task-project.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import reactHashAvatar from 'react-hash-avatar' 5 | import renderHTML from 'react-render-html' 6 | 7 | const TaskProject = ({ project }) => { 8 | if (project) { 9 | return ( 10 | 11 | {renderHTML(reactHashAvatar(project, { size: 8, radius: '50px' }))} 12 | 13 | 18 | 19 | ) 20 | } 21 | 22 | return null 23 | } 24 | 25 | export default TaskProject 26 | -------------------------------------------------------------------------------- /renderer/components/win-controls.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | const { remote } = require('electron') 5 | 6 | const WinControls = () => { 7 | return ( 8 |
    9 |
    10 |
    { 13 | remote.BrowserWindow.getFocusedWindow().minimize() 14 | }} 15 | /> 16 |
    { 19 | remote.BrowserWindow.getFocusedWindow().close() 20 | }} 21 | /> 22 |
    23 | 24 | 85 |
    86 | ) 87 | } 88 | 89 | export default WinControls 90 | -------------------------------------------------------------------------------- /renderer/icons/arrow-right.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ArrowRight = () => { 4 | return ( 5 | 6 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export default ArrowRight 33 | -------------------------------------------------------------------------------- /renderer/icons/award.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Award = () => { 4 | return ( 5 | 6 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default Award 31 | -------------------------------------------------------------------------------- /renderer/icons/download.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Download = () => { 4 | return ( 5 | 6 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export default Download 34 | -------------------------------------------------------------------------------- /renderer/icons/dragger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Dragger = () => { 4 | return ( 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | export default Dragger 37 | -------------------------------------------------------------------------------- /renderer/icons/github.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Github = () => { 4 | return ( 5 | 6 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | export default Github 28 | -------------------------------------------------------------------------------- /renderer/icons/layers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Layers = ({ styles }) => { 4 | return ( 5 | 6 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | export default Layers 32 | -------------------------------------------------------------------------------- /renderer/icons/logo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Logo = () => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 17 | 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | 29 | export default Logo 30 | -------------------------------------------------------------------------------- /renderer/icons/product-hunt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const ProductHunt = () => { 4 | return ( 5 | 6 | 13 | 14 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default ProductHunt 31 | -------------------------------------------------------------------------------- /renderer/icons/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Settings = ({ styles }) => { 4 | return ( 5 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default Settings 23 | -------------------------------------------------------------------------------- /renderer/icons/trash.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Trash = () => { 4 | return ( 5 | 6 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | export default Trash 35 | -------------------------------------------------------------------------------- /renderer/icons/twitter.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Twitter = () => { 4 | return ( 5 | 6 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | export default Twitter 28 | -------------------------------------------------------------------------------- /renderer/icons/upload.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Upload = () => { 4 | return ( 5 | 6 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export default Upload 34 | -------------------------------------------------------------------------------- /renderer/layouts/page.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Native 4 | import { platform } from 'os' 5 | 6 | // Packages 7 | import { Component } from 'react' 8 | import Router from 'next/router' 9 | import Progress from 'nprogress' 10 | 11 | // Components 12 | import WinControls from '../components/win-controls' 13 | 14 | // Theme 15 | import { colors, typography } from './../theme' 16 | 17 | let progress 18 | const stopProgress = () => { 19 | clearTimeout(progress) 20 | Progress.done() 21 | } 22 | 23 | Router.onRouteChangeStart = () => { 24 | progress = setTimeout(Progress.start, 200) 25 | } 26 | 27 | Router.onRouteChangeComplete = stopProgress 28 | Router.onRouteChangeError = stopProgress 29 | 30 | class Page extends Component { 31 | constructor() { 32 | super() 33 | 34 | this.handleKeypress = this.handleKeypress.bind(this) 35 | } 36 | 37 | componentDidMount() { 38 | document.addEventListener('keydown', this.handleKeypress, true) 39 | } 40 | 41 | componentWillUnmount() { 42 | document.removeEventListener('keydown', this.handleKeypress, true) 43 | } 44 | 45 | handleKeypress(event) { 46 | if (event.keyCode === 27) { 47 | return Router.push({ 48 | pathname: '/home', 49 | query: { tab: 'Today' } 50 | }) 51 | } 52 | 53 | if ((event.ctrlKey || event.metaKey) && event.keyCode === 78) { 54 | return Router.push('/add') 55 | } 56 | 57 | if ((event.ctrlKey || event.metaKey) && event.keyCode === 49) { 58 | return Router.push({ 59 | pathname: '/home', 60 | query: { tab: 'Today' } 61 | }) 62 | } 63 | 64 | if ((event.ctrlKey || event.metaKey) && event.keyCode === 50) { 65 | return Router.push({ 66 | pathname: '/home', 67 | query: { tab: 'Backlog' } 68 | }) 69 | } 70 | 71 | if ((event.ctrlKey || event.metaKey) && event.keyCode === 51) { 72 | return Router.push({ 73 | pathname: '/home', 74 | query: { tab: 'Done' } 75 | }) 76 | } 77 | } 78 | 79 | render() { 80 | const { children } = this.props 81 | const bgColor = colors ? colors.black : colors.black 82 | const fontSizeBase = typography 83 | ? typography.fontSizeBase 84 | : typography.fontSizeBase 85 | 86 | return ( 87 |
    88 | {platform() === 'win32' ? : null} 89 | 90 | {children} 91 | 92 | 154 |
    155 | ) 156 | } 157 | } 158 | 159 | export default Page 160 | -------------------------------------------------------------------------------- /renderer/next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack(config) { 3 | config.target = 'electron-renderer' 4 | config.plugins = config.plugins.filter( 5 | plugin => plugin.constructor.name !== 'UglifyJsPlugin' 6 | ) 7 | 8 | return config 9 | }, 10 | exportPathMap() { 11 | // Let Next.js know where to find the entry page 12 | // when it's exporting the static bundle for the use 13 | // in the production version of your app 14 | return { 15 | '/start': { page: '/start' } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /renderer/pages/add.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { Component } from 'react' 5 | import Router from 'next/router' 6 | 7 | // Layouts 8 | import Page from './../layouts/page' 9 | 10 | // Components 11 | import Row from './../ui/row' 12 | import Hero from './../components/hero' 13 | import Input from './../ui/input' 14 | import Button from './../ui/button' 15 | import ButtonLink from './../ui/button-link' 16 | 17 | // Services 18 | import { addTask, getUser, updateUser } from './../services/local-storage' 19 | 20 | // Theme 21 | import { colors, typography } from './../theme' 22 | 23 | class Add extends Component { 24 | constructor() { 25 | super() 26 | 27 | this.inputChange = this.inputChange.bind(this) 28 | this.createTask = this.createTask.bind(this) 29 | 30 | this.state = { 31 | title: '', 32 | description: '', 33 | project: '' 34 | } 35 | } 36 | 37 | inputChange(event) { 38 | const { target } = event 39 | const { name, value } = target 40 | 41 | this.setState({ [name]: value }) 42 | } 43 | 44 | createTask(e) { 45 | e.preventDefault() 46 | const { title, description, project } = this.state 47 | const { user } = getUser() 48 | let tab = 'Backlog' 49 | 50 | if (!user.createOn) { 51 | user.createOn = 'Today' 52 | updateUser(user) 53 | } 54 | 55 | tab = user.createOn 56 | 57 | addTask({ title, description, project, tab }) 58 | .then(() => Router.push(`/home?tab=${tab}`)) 59 | .catch(err => console.log(err)) 60 | } 61 | 62 | render() { 63 | const { title, description, project } = this.state 64 | 65 | return ( 66 | 67 | 68 |
    69 | 70 | 71 |
    72 |
    73 | 83 | 84 | 93 | 94 | 103 |
    104 | 105 |
    106 | 107 | Back 108 | 109 | 110 | 111 |
    112 |
    113 |
    114 |
    115 | 116 | 153 |
    154 | ) 155 | } 156 | } 157 | 158 | export default Add 159 | -------------------------------------------------------------------------------- /renderer/pages/edit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { Component } from 'react' 5 | import Router from 'next/router' 6 | 7 | // Layouts 8 | import Page from './../layouts/page' 9 | 10 | // Components 11 | import Row from './../ui/row' 12 | import Hero from './../components/hero' 13 | import Input from './../ui/input' 14 | import Button from './../ui/button' 15 | import ButtonLink from './../ui/button-link' 16 | 17 | // Services 18 | import { getUser, updateTask } from './../services/local-storage' 19 | 20 | // Theme 21 | import { colors, typography } from './../theme' 22 | 23 | class Edit extends Component { 24 | constructor() { 25 | super() 26 | 27 | this.inputChange = this.inputChange.bind(this) 28 | this.editTask = this.editTask.bind(this) 29 | 30 | this.state = { title: '', description: '', project: '' } 31 | } 32 | 33 | componentDidMount() { 34 | const { url: { query: { id } } } = this.props 35 | const { user } = getUser() 36 | const { title, description, project } = user.tasks.filter( 37 | task => task.id === id 38 | )[0] 39 | 40 | if (title) { 41 | return this.setState({ title, description, project }) 42 | } 43 | } 44 | 45 | inputChange(event) { 46 | const { target } = event 47 | const { name, value } = target 48 | 49 | this.setState({ [name]: value }) 50 | } 51 | 52 | editTask(e) { 53 | e.preventDefault() 54 | 55 | const { url: { query: { id } } } = this.props 56 | const { title, description, project } = this.state 57 | const newTask = { title, description, project } 58 | 59 | updateTask({ id, newTask }) 60 | .then(() => Router.push('/home')) 61 | .catch(err => console.log(err)) 62 | } 63 | 64 | render() { 65 | const { title, description, project } = this.state 66 | 67 | return ( 68 | 69 | 70 |
    71 | 72 | 73 |
    74 |
    75 | 85 | 86 | 95 | 96 | 105 |
    106 | 107 |
    108 | 109 | Back 110 | 111 | 112 | 113 |
    114 |
    115 |
    116 |
    117 | 118 | 155 |
    156 | ) 157 | } 158 | } 159 | 160 | export default Edit 161 | -------------------------------------------------------------------------------- /renderer/pages/home.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { Component } from 'react' 5 | import { arrayMove } from 'react-sortable-hoc' 6 | 7 | // Layouts 8 | import Page from './../layouts/page' 9 | 10 | // Components 11 | import Row from './../ui/row' 12 | import Hero from './../components/hero' 13 | import ButtonLink from './../ui/button-link' 14 | import Navigation from './../components/navigation' 15 | import Content from './../components/content' 16 | import Today from './../components/home/today' 17 | import Backlog from './../components/home/backlog' 18 | import Done from './../components/home/done' 19 | 20 | // Services 21 | import { getUser, updateUser } from './../services/local-storage' 22 | 23 | class Home extends Component { 24 | constructor() { 25 | super() 26 | 27 | this.selectTab = this.selectTab.bind(this) 28 | this.onDelete = this.onDelete.bind(this) 29 | this.onSortEnd = this.onSortEnd.bind(this) 30 | 31 | this.state = { 32 | user: {}, 33 | tabSelected: 'Today' 34 | } 35 | } 36 | 37 | componentDidMount() { 38 | const { url: { query: { tab } } } = this.props 39 | const { user } = getUser() 40 | const tabSelected = tab ? tab : 'Today' 41 | 42 | this.setState({ user, tabSelected }) 43 | } 44 | 45 | componentWillReceiveProps(nextProps) { 46 | if (nextProps.url.query.tab !== this.props.url.query) { 47 | this.selectTab(nextProps.url.query.tab) 48 | } 49 | } 50 | 51 | selectTab(tabSelected) { 52 | this.setState({ tabSelected }) 53 | } 54 | 55 | onDelete(task) { 56 | const { user } = getUser() 57 | 58 | user.tasks = user.tasks.filter(({ id }) => id !== task.id) 59 | updateUser(user) 60 | this.setState({ user }) 61 | } 62 | 63 | onMove(type, task) { 64 | const { user } = getUser() 65 | 66 | if (type === 'backlog') { 67 | const taskUpdated = user.tasks.map(t => { 68 | if (t.id === task.id) { 69 | t.type = 'today' 70 | return t 71 | } 72 | 73 | return t 74 | }) 75 | 76 | user.tasks = taskUpdated 77 | updateUser(user) 78 | return this.setState({ user }) 79 | } 80 | 81 | if (type === 'today') { 82 | const taskUpdated = user.tasks.map(t => { 83 | if (t.id === task.id) { 84 | t.type = 'done' 85 | return t 86 | } 87 | 88 | return t 89 | }) 90 | 91 | user.tasks = taskUpdated 92 | updateUser(user) 93 | return this.setState({ user }) 94 | } 95 | 96 | if (type === 'back') { 97 | const taskUpdated = user.tasks.map(t => { 98 | if (t.id === task.id) { 99 | t.type = 'backlog' 100 | return t 101 | } 102 | 103 | return t 104 | }) 105 | 106 | user.tasks = taskUpdated 107 | updateUser(user) 108 | return this.setState({ user }) 109 | } 110 | } 111 | 112 | onSortEnd({ oldIndex, newIndex }) { 113 | const userObj = getUser() 114 | const { tabSelected, user } = this.state 115 | const tasks = user.tasks 116 | const filteredTasks = tasks.filter( 117 | task => task.type === tabSelected.toLowerCase() 118 | ) 119 | const reordered = arrayMove(filteredTasks, oldIndex, newIndex) 120 | const tasksRemoved = tasks.filter( 121 | task => task.type !== tabSelected.toLowerCase() 122 | ) 123 | const taskUpdated = tasksRemoved.concat(reordered) 124 | 125 | userObj.tasks = taskUpdated 126 | updateUser(userObj) 127 | return this.setState({ user: userObj }) 128 | } 129 | 130 | render() { 131 | let content 132 | const { tabSelected, user } = this.state 133 | const tasks = user.tasks 134 | const list = [ 135 | { name: 'Today', href: '/home?tab=Today' }, 136 | { name: 'Backlog', href: '/home?tab=Backlog' }, 137 | { name: 'Done', href: '/home?tab=Done' } 138 | ] 139 | 140 | switch (tabSelected) { 141 | case 'Today': 142 | const todayTasks = tasks 143 | ? tasks.filter(({ type }) => type === 'today') 144 | : [] 145 | content = ( 146 | this.onMove(type, task)} 151 | /> 152 | ) 153 | break 154 | 155 | case 'Backlog': 156 | const backlogTasks = tasks 157 | ? tasks.filter(({ type }) => type === 'backlog') 158 | : [] 159 | content = ( 160 | this.onMove(type, task)} 165 | /> 166 | ) 167 | break 168 | 169 | case 'Done': 170 | const doneTasks = tasks 171 | ? tasks.filter(({ type }) => type === 'done') 172 | : [] 173 | content = ( 174 | this.onMove(type, task)} 179 | /> 180 | ) 181 | break 182 | } 183 | 184 | return ( 185 | 186 | 187 |
    188 | 189 | 190 | 191 | 192 | {content} 193 | 194 | Add new task 195 |
    196 |
    197 | 198 | 207 |
    208 | ) 209 | } 210 | } 211 | 212 | export default Home 213 | -------------------------------------------------------------------------------- /renderer/pages/login.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { Component } from 'react' 5 | import Link from 'next/link' 6 | import Router from 'next/router' 7 | 8 | // Layouts 9 | import Page from './../layouts/page' 10 | 11 | // Components 12 | import Row from './../ui/row' 13 | import Input from './../ui/input' 14 | import Button from './../ui/button' 15 | import ButtonLink from './../ui/button-link' 16 | 17 | // Services 18 | import api from './../services/api' 19 | import { setCookie } from './../services/cookies' 20 | import { getUser, updateUser } from './../services/local-storage' 21 | 22 | // Theme 23 | import { colors, typography } from './../theme' 24 | 25 | class Login extends Component { 26 | constructor() { 27 | super() 28 | 29 | this.inputChange = this.inputChange.bind(this) 30 | this.onLogin = this.onLogin.bind(this) 31 | 32 | this.state = { 33 | email: '', 34 | password: '' 35 | } 36 | } 37 | 38 | inputChange(event) { 39 | const { target } = event 40 | const { name, value } = target 41 | 42 | this.setState({ [name]: value }) 43 | } 44 | 45 | onLogin(e) { 46 | e.preventDefault() 47 | const { email, password } = this.state 48 | 49 | api 50 | .post('/login', { 51 | email, 52 | password 53 | }) 54 | .then(res => { 55 | if (res.token) { 56 | const { email, name, username, subscription } = res.user 57 | const { user } = getUser() 58 | 59 | setCookie(res.token) 60 | 61 | user.token = res.token 62 | user.email = email 63 | user.name = name 64 | user.username = username 65 | user.subscription = subscription 66 | 67 | updateUser(user) 68 | 69 | return Router.push('/home?tab=Today') 70 | } 71 | console.log(res) 72 | }) 73 | .catch(err => { 74 | console.log(err) 75 | }) 76 | } 77 | 78 | render() { 79 | const { email, password } = this.state 80 | 81 | return ( 82 | 83 | 84 | 85 | Back 86 | 87 | 88 |
    89 |

    90 | Welcome to Taskr 91 |

    92 | 93 |
    94 |
    95 | 103 | 104 | 113 |
    114 | 115 | 116 |
    117 | 118 | 119 | Forgot password? 120 | 121 |
    122 |
    123 | 124 | 170 |
    171 | ) 172 | } 173 | } 174 | 175 | export default Login 176 | -------------------------------------------------------------------------------- /renderer/pages/onboard.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { Component } from 'react' 5 | 6 | // Layouts 7 | import Page from './../layouts/page' 8 | 9 | // Components 10 | import Row from './../ui/row' 11 | import ButtonLink from './../ui/button-link' 12 | 13 | // Theme 14 | import { colors, typography } from './../theme' 15 | 16 | class Onboard extends Component { 17 | render() { 18 | return ( 19 | 20 | 21 |
    22 |

    23 | Welcome to Taskr 24 |

    25 | 26 |
    27 | {/* 28 | Pro — 14 days free trial 29 | */} 30 | 31 | 32 | Continue on free version 33 | 34 |
    35 |
    36 |
    37 | 38 | 59 |
    60 | ) 61 | } 62 | } 63 | 64 | export default Onboard 65 | -------------------------------------------------------------------------------- /renderer/pages/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { remote, shell } from 'electron' 5 | import { Component } from 'react' 6 | 7 | // Layouts 8 | import Page from './../layouts/page' 9 | 10 | // Components 11 | import Row from './../ui/row' 12 | import Hero from './../components/hero' 13 | import Navigation from './../components/navigation' 14 | import Identity from './../components/settings/identity' 15 | import Account from './../components/settings/account' 16 | import AppInfo from './../components/settings/app-info' 17 | import Social from './../components/settings/social' 18 | import Content from './../components/content' 19 | 20 | // Sertvices 21 | import { getUser, updateUser } from './../services/local-storage' 22 | import { exportUser, importUser, clearHistory } from './../services/settings' 23 | 24 | class Settings extends Component { 25 | constructor() { 26 | super() 27 | 28 | this.openUrl = this.openUrl.bind(this) 29 | this.onClearHistory = this.onClearHistory.bind(this) 30 | this.onSelectChange = this.onSelectChange.bind(this) 31 | this.selectTab = this.selectTab.bind(this) 32 | 33 | this.state = { 34 | defaultOption: 'Today', 35 | tabSelected: 'Identity', 36 | email: '', 37 | username: '' 38 | } 39 | } 40 | 41 | componentDidMount() { 42 | const { url: { query: { tab } } } = this.props 43 | const { user } = getUser() 44 | const { createOn } = user 45 | const tabSelected = tab ? tab : 'Identity' 46 | 47 | console.log(user) 48 | 49 | this.setState({ 50 | defaultOption: createOn, 51 | tabSelected, 52 | user 53 | }) 54 | } 55 | 56 | componentWillReceiveProps({ url: { query: { tab } } }) { 57 | if (tab !== this.props.url.query) { 58 | this.selectTab(tab) 59 | } 60 | } 61 | 62 | selectTab(tabSelected) { 63 | this.setState({ tabSelected }) 64 | } 65 | 66 | openUrl(url) { 67 | shell.openExternal(url) 68 | } 69 | 70 | onClearHistory() { 71 | const choice = remote.dialog.showMessageBox(remote.getCurrentWindow(), { 72 | type: 'question', 73 | buttons: ['Yes', 'No'], 74 | title: 'Confirm', 75 | message: 'Are you sure you want to clear your history?' 76 | }) 77 | 78 | if (choice === 0) { 79 | return clearHistory() 80 | } 81 | } 82 | 83 | onSelectChange(event) { 84 | const { target } = event 85 | const { value } = target 86 | const { user } = getUser() 87 | 88 | user.createOn = value 89 | 90 | this.setState({ defaultOption: value }) 91 | updateUser(user) 92 | } 93 | 94 | render() { 95 | let content 96 | const { tabSelected, defaultOption, user } = this.state 97 | const list = [ 98 | { name: 'Identity', href: '/settings?tab=Identity' }, 99 | { name: 'Account', href: '/settings?tab=Account' }, 100 | // { name: 'Billing', href: '/settings?tab=Billing' }, 101 | { name: 'App info', href: '/settings?tab=App info' } 102 | ] 103 | 104 | switch (tabSelected) { 105 | case 'Identity': 106 | content = 107 | break 108 | 109 | case 'Account': 110 | content = ( 111 | 118 | ) 119 | break 120 | 121 | case 'Billing': 122 | content =

    Billing

    123 | break 124 | 125 | case 'App info': 126 | content = 127 | break 128 | 129 | default: 130 | content = 131 | } 132 | 133 | return ( 134 | 135 | 136 |
    137 | 138 | 139 | 140 | 141 | {content} 142 | 143 | 144 |
    145 |
    146 | 147 | 156 |
    157 | ) 158 | } 159 | } 160 | 161 | export default Settings 162 | -------------------------------------------------------------------------------- /renderer/pages/signup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { Component } from 'react' 5 | import Link from 'next/link' 6 | import Router from 'next/router' 7 | 8 | // Layouts 9 | import Page from './../layouts/page' 10 | 11 | // Components 12 | import Row from './../ui/row' 13 | import Input from './../ui/input' 14 | import Button from './../ui/button' 15 | 16 | // Services 17 | import api from './../services/api' 18 | import { setCookie } from './../services/cookies' 19 | import { getUser, updateUser } from './../services/local-storage' 20 | 21 | // Theme 22 | import { colors, typography } from './../theme' 23 | 24 | class Signup extends Component { 25 | constructor() { 26 | super() 27 | 28 | this.inputChange = this.inputChange.bind(this) 29 | this.onSignup = this.onSignup.bind(this) 30 | 31 | this.state = { 32 | email: '', 33 | name: '', 34 | password: '' 35 | } 36 | } 37 | 38 | inputChange(event) { 39 | const { target } = event 40 | const { name, value } = target 41 | 42 | this.setState({ [name]: value }) 43 | } 44 | 45 | onSignup(e) { 46 | e.preventDefault() 47 | const { email, name, password } = this.state 48 | 49 | api 50 | .post('/signup', { 51 | email, 52 | name, 53 | password 54 | }) 55 | .then(res => { 56 | if (res.token) { 57 | const { email, name, username, subscription } = res.user 58 | const { user } = getUser() 59 | 60 | setCookie(res.token) 61 | 62 | user.token = res.token 63 | user.email = email 64 | user.name = name 65 | user.username = username 66 | user.subscription = subscription 67 | 68 | updateUser(user) 69 | 70 | return Router.push('/home?tab=Today') 71 | } 72 | }) 73 | .catch(err => { 74 | console.log(err) 75 | }) 76 | } 77 | 78 | render() { 79 | const { email, name, password } = this.state 80 | 81 | return ( 82 | 83 | 84 | 85 | Back 86 | 87 | 88 |
    89 |

    90 | Welcome to Taskr 91 |

    92 | 93 |
    94 |
    95 | 103 | 104 | 112 | 113 | 122 |
    123 | 124 | 125 |
    126 |
    127 |
    128 | 129 | 175 |
    176 | ) 177 | } 178 | } 179 | 180 | export default Signup 181 | -------------------------------------------------------------------------------- /renderer/pages/start.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import { remote } from 'electron' 5 | import { Component } from 'react' 6 | import Router from 'next/router' 7 | 8 | // Layouts 9 | import Page from './../layouts/page' 10 | 11 | // Components 12 | import Row from './../ui/row' 13 | import Logo from './../icons/logo' 14 | 15 | // Services 16 | import { getCookie } from './../services/cookies' 17 | import { getUser, updateUser } from './../services/local-storage' 18 | 19 | // Theme 20 | import { colors, typography } from './../theme' 21 | 22 | class Start extends Component { 23 | componentDidMount() { 24 | const { user } = getUser() 25 | const cfg = remote && remote.app ? remote.app.config : {} 26 | const token = getCookie('taskr') 27 | const { pro } = cfg.user 28 | const skipOnboard = user.onboard ? '/home?tab=Today' : '/onboard' 29 | const redirectUrl = pro && token ? '/home?tab=Today' : skipOnboard 30 | 31 | if (!user.onboard) { 32 | const userUpdated = Object.assign(user, { onboard: true }) 33 | 34 | updateUser(userUpdated) 35 | } 36 | 37 | Router.push(redirectUrl) 38 | } 39 | 40 | render() { 41 | return ( 42 | 43 | 44 |
    45 |
    46 | 47 | 48 |

    Taskr

    49 |

    A simple task manager app

    50 |
    51 |
    52 |
    53 | 54 | 83 |
    84 | ) 85 | } 86 | } 87 | 88 | export default Start 89 | -------------------------------------------------------------------------------- /renderer/services/api.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import axios from 'axios' 4 | 5 | import { getCookie } from './cookies' 6 | 7 | const apiUrl = process.env.API_URL 8 | 9 | const api = axios.create({ 10 | baseURL: apiUrl, 11 | headers: { 12 | Accept: 'application/json' 13 | }, 14 | withCredentials: true 15 | }) 16 | 17 | api.interceptors.request.use(config => { 18 | const accessToken = getCookie('taskr') 19 | 20 | if (accessToken) { 21 | config.headers.authorization = accessToken 22 | } 23 | 24 | return config 25 | }) 26 | 27 | api.interceptors.response.use( 28 | response => { 29 | if (response.data) { 30 | return response.data 31 | } 32 | 33 | return response 34 | }, 35 | error => { 36 | if (error.response && error.response.data) { 37 | return Promise.reject(error.response.data.error) 38 | } 39 | 40 | return Promise.reject(error) 41 | } 42 | ) 43 | 44 | export default api 45 | -------------------------------------------------------------------------------- /renderer/services/cookies.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import cookie from 'react-cookies' 4 | 5 | export const getCookie = key => { 6 | return cookie.load(key) 7 | } 8 | 9 | export const setCookie = value => { 10 | const now = new Date() 11 | now.setDate(now.getDate() + 14) 12 | 13 | return cookie.save('taskr', value, { 14 | expires: now, 15 | path: '/' 16 | }) 17 | } 18 | 19 | export const logout = () => { 20 | return cookie.remove('taskr', { path: '/' }) 21 | } 22 | -------------------------------------------------------------------------------- /renderer/services/local-storage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import uid from 'uid-promise' 5 | 6 | export const getUser = () => { 7 | const storage = JSON.parse(localStorage.getItem('taskr')) 8 | 9 | if (storage) { 10 | return storage 11 | } 12 | 13 | const cfg = { 14 | user: { 15 | tasks: [], 16 | createOn: 'Today', 17 | onboard: false, 18 | pro: false 19 | } 20 | } 21 | 22 | localStorage.setItem('taskr', JSON.stringify(cfg)) 23 | 24 | return cfg 25 | } 26 | 27 | export const updateUser = user => { 28 | return localStorage.setItem('taskr', JSON.stringify({ user })) 29 | } 30 | 31 | export const addTask = ({ title, description, project, tab = 'Today' }) => { 32 | return new Promise(async (resolve, reject) => { 33 | if (!title) { 34 | return reject(new TypeError('title is required')) 35 | } 36 | 37 | const { user } = getUser() 38 | const id = await uid(20) 39 | const task = { 40 | id, 41 | title, 42 | description, 43 | project, 44 | createdAt: new Date(), 45 | updatedAt: new Date(), 46 | type: tab.toLowerCase() 47 | } 48 | const tasks = [...user.tasks, task] 49 | user.tasks = tasks 50 | 51 | resolve(localStorage.setItem('taskr', JSON.stringify({ user }))) 52 | }) 53 | } 54 | 55 | export const updateTask = ({ id, newTask }) => { 56 | return new Promise(async (resolve, reject) => { 57 | if (!newTask && !id) { 58 | return reject(new TypeError('id and task is required')) 59 | } 60 | 61 | const { user } = getUser() 62 | const task = user.tasks.filter(task => task.id === id)[0] 63 | 64 | task.title = newTask.title || task.title 65 | task.description = newTask.description || task.description 66 | task.project = newTask.project || task.project 67 | task.updatedAt = new Date() 68 | 69 | const tasks = user.tasks.filter(t => { 70 | if (t.id === id) { 71 | return (t = task) 72 | } 73 | 74 | return t 75 | }) 76 | 77 | user.tasks = tasks 78 | 79 | resolve(localStorage.setItem('taskr', JSON.stringify({ user }))) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /renderer/services/notify.js: -------------------------------------------------------------------------------- 1 | // Packages 2 | const { remote } = require('electron') 3 | 4 | module.exports = ({ title, body, url, onClick }) => { 5 | const icon = 'static/icon.ico' 6 | const specs = { 7 | title, 8 | body, 9 | icon, 10 | silent: true 11 | } 12 | 13 | const notification = new remote.Notification(specs) 14 | 15 | if (url || onClick) { 16 | notification.on('click', () => { 17 | if (onClick) { 18 | return onClick() 19 | } 20 | 21 | remote.shell.openExternal(url) 22 | }) 23 | } 24 | 25 | notification.show() 26 | console.log(`[Notification] ${title}: ${body}`) 27 | } 28 | -------------------------------------------------------------------------------- /renderer/services/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Native 4 | const { homedir } = require('os') 5 | 6 | // Packages 7 | const { remote } = require('electron') 8 | const { writeJSON, readJson } = require('fs-extra') 9 | 10 | // Services 11 | const { getUser, updateUser } = require('./local-storage') 12 | const notify = require('./notify') 13 | 14 | export const exportUser = () => { 15 | remote.dialog.showSaveDialog( 16 | undefined, 17 | { defaultPath: `${homedir()}/taskr.json` }, 18 | fileName => { 19 | if (fileName) { 20 | const user = getUser() 21 | 22 | writeJSON(fileName, user) 23 | .then(() => 24 | notify({ 25 | title: 'User config exported!', 26 | body: 'Your user config was exported successfully' 27 | }) 28 | ) 29 | .catch(err => { 30 | console.log(err) 31 | return notify({ 32 | title: 'Error!', 33 | body: 'Oops, something happened! Please, try again.' 34 | }) 35 | }) 36 | } 37 | } 38 | ) 39 | } 40 | 41 | export const importUser = () => { 42 | remote.dialog.showOpenDialog( 43 | undefined, 44 | { properties: ['openFile'] }, 45 | filePath => { 46 | readJson(filePath[0]) 47 | .then(({ user }) => { 48 | if (user) { 49 | return updateUser(user) 50 | } 51 | }) 52 | .then(() => 53 | notify({ 54 | title: 'User config imported!', 55 | body: 'Your user config was imported successfully' 56 | }) 57 | ) 58 | .catch(err => { 59 | console.log(err) 60 | return notify({ 61 | title: 'Error!', 62 | body: 'Oops, something happened! Please, try again.' 63 | }) 64 | }) 65 | } 66 | ) 67 | } 68 | 69 | export const clearHistory = () => { 70 | const user = { 71 | tasks: [] 72 | } 73 | 74 | updateUser(user) 75 | } 76 | -------------------------------------------------------------------------------- /renderer/static/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bukinoshita/taskr/83f0a65c6c1df5e888268511759ed53bccb7b46c/renderer/static/icon.ico -------------------------------------------------------------------------------- /renderer/theme/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Utils 4 | import { applyTheme, applyFontSize } from './utils' 5 | 6 | export const colors = { 7 | white: applyTheme('white'), 8 | black: applyTheme('black'), 9 | romanSilver: applyTheme('romanSilver'), 10 | darkMediumGray: applyTheme('darkMediumGray'), 11 | brightTurquoise: applyTheme('brightTurquoise') 12 | } 13 | 14 | export const typography = { 15 | fontSizeBase: applyFontSize(), 16 | f8: '.5rem', 17 | f10: '.6rem', 18 | f12: '.75rem', 19 | f14: '.85rem', 20 | f16: '1rem', 21 | f18: '1.15rem', 22 | f20: '1.25rem', 23 | f22: '1.35rem', 24 | f24: '1.5rem', 25 | f26: '1.75rem', 26 | f30: '1.9rem', 27 | f32: '2rem', 28 | f35: '2.2rem', 29 | f45: '2.8rem', 30 | f56: '3.5rem', 31 | ultralight: 100, 32 | thin: 200, 33 | light: 300, 34 | regular: 400, 35 | medium: 500, 36 | semibold: 600, 37 | bold: 700, 38 | heavy: 800, 39 | black: 900 40 | } 41 | -------------------------------------------------------------------------------- /renderer/theme/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import electron from 'electron' 5 | 6 | const app = process.type === 'renderer' ? electron.remote.app : electron.app 7 | 8 | const baseColor = { 9 | white: '#ffffff', 10 | black: '#000000', 11 | romanSilver: '#868e96', 12 | darkMediumGray: '#aaaaaa', 13 | brightTurquoise: '#00e7c0' 14 | } 15 | 16 | const baseFont = { 17 | fontSizeBase: '16px' 18 | } 19 | 20 | export const applyTheme = color => { 21 | const themeColor = 22 | app && app.config ? app.config.theme[color] : baseColor[color] 23 | 24 | return themeColor 25 | } 26 | 27 | export const applyFontSize = () => { 28 | const baseFontSize = 29 | app && app.config ? app.config.theme.fontSizeBase : baseFont.fontSizeBase 30 | 31 | return baseFontSize 32 | } 33 | -------------------------------------------------------------------------------- /renderer/ui/button-link.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import PropTypes from 'prop-types' 5 | import classNames from 'classnames' 6 | import Link from 'next/link' 7 | 8 | // Theme 9 | import { colors, typography } from './../theme' 10 | 11 | const ButtonLink = ({ children, onClick, color, href }) => { 12 | const classnames = classNames(color) 13 | 14 | return ( 15 | 53 | ) 54 | } 55 | 56 | ButtonLink.defaultProps = { 57 | onClick: null, 58 | href: '/home' 59 | } 60 | 61 | ButtonLink.propTypes = { 62 | children: PropTypes.node, 63 | onClick: PropTypes.func, 64 | href: PropTypes.string.isRequired 65 | } 66 | 67 | export default ButtonLink 68 | -------------------------------------------------------------------------------- /renderer/ui/button.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import PropTypes from 'prop-types' 5 | import classNames from 'classnames' 6 | 7 | // Theme 8 | import { colors, typography } from './../theme' 9 | 10 | const Button = ({ children, type, onClick, color }) => { 11 | const classnames = classNames(color) 12 | 13 | return ( 14 | 38 | ) 39 | } 40 | 41 | Button.defaultProps = { 42 | onClick: null, 43 | type: 'button' 44 | } 45 | 46 | Button.propTypes = { 47 | children: PropTypes.node, 48 | type: PropTypes.string, 49 | onClick: PropTypes.func 50 | } 51 | 52 | export default Button 53 | -------------------------------------------------------------------------------- /renderer/ui/input.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Packages 4 | import classNames from 'classnames' 5 | import PropTypes from 'prop-types' 6 | import reactHashAvatar from 'react-hash-avatar' 7 | import renderHTML from 'react-render-html' 8 | 9 | // Theme 10 | import { colors, typography } from './../theme' 11 | 12 | const Input = ({ 13 | name, 14 | label, 15 | multiline, 16 | type, 17 | placeholder, 18 | success, 19 | error, 20 | hint, 21 | size, 22 | value, 23 | autoFocus, 24 | onChange, 25 | readOnly, 26 | hasProject 27 | }) => { 28 | const classnames = classNames(size) 29 | 30 | const project = 31 | hasProject && value ? ( 32 | 33 | {renderHTML(reactHashAvatar(value, { size: 8, radius: '50px' }))} 34 | 39 | 40 | ) : null 41 | 42 | return ( 43 |
    44 | 47 | 48 | {multiline ? ( 49 |