├── .env ├── .eslintrc ├── .gitignore ├── .prettierrc ├── README.md ├── components ├── editor │ └── index.js ├── project │ ├── add.js │ └── index.js ├── sidebar │ └── index.js ├── task │ ├── add.js │ ├── index.js │ └── view.js └── tasklist │ └── index.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── api │ ├── auth │ │ └── [...nextauth].js │ ├── project │ │ └── new.js │ ├── register.js │ └── task │ │ ├── [taskid].js │ │ └── new.js ├── index.js ├── login.js ├── project │ ├── [projectid].js │ └── [projectid] │ │ └── task │ │ └── [taskid].js └── register.js ├── postcss.config.js ├── public ├── favicon.ico └── vercel.svg ├── src ├── db.js ├── project.js ├── task.js ├── user.js └── utils.js ├── styles └── index.css └── tailwind.config.js /.env: -------------------------------------------------------------------------------- 1 | NEXTAUTH_URL=http://localhost:3000 -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["standard", "standard-jsx", "standard-react", "prettier"], 4 | "plugins": ["prettier"], 5 | "env": { 6 | "node": true, 7 | "browser": true 8 | }, 9 | "rules": { 10 | "max-len": ["error", 120, 4], 11 | "camelcase": "off", 12 | "promise/param-names": "off", 13 | "prefer-promise-reject-errors": "off", 14 | "no-control-regex": "off", 15 | "react/react-in-jsx-scope": "off", 16 | "react/prop-types": "warn", 17 | "prettier/prettier": [ 18 | "error", 19 | { 20 | "endOfLine": "lf", 21 | "trailingComma": "es5", 22 | "tabWidth": 2, 23 | "singleQuote": true, 24 | "semi": true 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /components/editor/index.js: -------------------------------------------------------------------------------- 1 | import EditorJS from '@editorjs/editorjs'; 2 | import { useEffect, useRef } from 'react'; 3 | 4 | // TODO: figure out why Editor.js overlays every modal / absolute div 5 | // in existence and prevents from using them 6 | export default function Editor({ data = {}, onChange }) { 7 | const editorRef = useRef(); 8 | 9 | const handleUpdate = async () => { 10 | const output = await editorRef.current.save(); 11 | onChange(output); 12 | }; 13 | 14 | useEffect(() => { 15 | if (editorRef.current) { 16 | return; 17 | } 18 | 19 | editorRef.current = new EditorJS({ 20 | holder: 'editorjs', 21 | autofocus: true, 22 | placeholder: 'Task body', 23 | logLevel: 'ERROR', 24 | data, 25 | onChange: handleUpdate, 26 | }); 27 | }, []); 28 | 29 | return
; 30 | } 31 | -------------------------------------------------------------------------------- /components/project/add.js: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import { FaPlus } from 'react-icons/fa'; 3 | import Modal from 'react-modal'; 4 | import axios from 'redaxios'; 5 | 6 | Modal.setAppElement('#__next'); 7 | 8 | export function AddProject({ onNewProject }) { 9 | const projectNameRef = useRef(); 10 | const [modalOpen, setModalOpen] = useState(false); 11 | 12 | const createNewProject = async () => { 13 | const data = { name: projectNameRef.current.value }; 14 | const { 15 | data: { project }, 16 | } = await axios.post('/api/project/new', data); 17 | projectNameRef.current.value = ''; 18 | setModalOpen(false); 19 | onNewProject(project); 20 | }; 21 | 22 | return ( 23 | <> 24 | 30 | 31 | 37 |
38 | 44 | 51 |
52 | 53 |
54 | 61 | setModalOpen(false)} 65 | > 66 | Cancel 67 | 68 |
69 |
70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /components/project/index.js: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | const toFirstLetters = (name) => 4 | name 5 | .split(' ') 6 | .map((word) => word[0].toUpperCase()) 7 | .join(''); 8 | 9 | export function Project({ project, isCurrent }) { 10 | return ( 11 | 12 | 20 | {toFirstLetters(project.name)} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/sidebar/index.js: -------------------------------------------------------------------------------- 1 | import { signOut } from 'next-auth/client'; 2 | import { useState } from 'react'; 3 | import { FaSignOutAlt } from 'react-icons/fa'; 4 | import { Project } from '../project'; 5 | import { AddProject } from '../project/add'; 6 | 7 | export function Sidebar({ initialProjects = [], currentProject }) { 8 | const [projects, setProjects] = useState(initialProjects); 9 | 10 | const handleNewProject = (newProject) => { 11 | const newProjects = projects.concat(newProject); 12 | setProjects(newProjects); 13 | }; 14 | 15 | return ( 16 |
17 | {projects.map((project) => ( 18 | 23 | ))} 24 | 25 | 26 | 27 |
28 | 29 | 36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /components/task/add.js: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import Modal from 'react-modal'; 3 | import axios from 'redaxios'; 4 | 5 | Modal.setAppElement('#__next'); 6 | 7 | export function AddTask({ onNewTask, currentProject }) { 8 | const taskNameRef = useRef(); 9 | const [isEditing, setEditing] = useState(false); 10 | 11 | const createNewTask = async () => { 12 | const data = { 13 | name: taskNameRef.current.value, 14 | project: currentProject._id, 15 | }; 16 | const { 17 | data: { task }, 18 | } = await axios.post(`/api/task/new`, data); 19 | taskNameRef.current.value = ''; 20 | onNewTask(task); 21 | setEditing(false); 22 | }; 23 | 24 | return ( 25 | <> 26 | {!isEditing && ( 27 | 33 | )} 34 | 35 | {isEditing && ( 36 | <> 37 |
38 | 44 | 51 |
52 | 53 |
54 | 61 | setEditing(false)} 65 | > 66 | Cancel 67 | 68 |
69 | 70 | )} 71 | 72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /components/task/index.js: -------------------------------------------------------------------------------- 1 | import { Emoji } from 'emoji-mart'; 2 | import Link from 'next/link'; 3 | 4 | export function Task({ project, task, isCurrent }) { 5 | return ( 6 | 7 | 12 |
13 |
14 | 15 |
16 | 17 |
18 |
{task.name}
19 |
{task.description ?? ''}
20 |
21 |
22 |
23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/task/view.js: -------------------------------------------------------------------------------- 1 | import { Emoji, Picker } from 'emoji-mart'; 2 | import 'emoji-mart/css/emoji-mart.css'; 3 | import dynamic from 'next/dynamic'; 4 | import { useRef, useState } from 'react'; 5 | import axios from 'redaxios'; 6 | 7 | const DynamicEditorJs = dynamic(() => import('../editor'), { ssr: false }); 8 | 9 | export function TaskView({ project, task }) { 10 | const saveTimeout = useRef(); 11 | const [taskData, setTaskData] = useState(task); 12 | const [showEmojiPicker, setShowEmojiPicker] = useState(false); 13 | 14 | const saveTask = async (data) => { 15 | console.log('update', data); 16 | await axios.post(`/api/task/${data._id}`, data); 17 | }; 18 | 19 | const throttledSave = (data) => { 20 | if (saveTimeout.current) { 21 | clearTimeout(saveTimeout.current); 22 | } 23 | saveTimeout.current = setTimeout(() => saveTask(data), 500); 24 | }; 25 | 26 | const selectEmoji = (emoji) => { 27 | const newEmojiId = emoji.id; 28 | const newTask = { ...taskData, icon: newEmojiId }; 29 | setTaskData(newTask); 30 | saveTask(newTask); 31 | setShowEmojiPicker(false); 32 | }; 33 | 34 | const handleNameChange = (event) => { 35 | const newName = event.target.value; 36 | const newTask = { ...taskData, name: newName }; 37 | setTaskData(newTask); 38 | throttledSave(newTask); 39 | }; 40 | 41 | const handleDescriptionChange = (event) => { 42 | const newDescription = event.target.value; 43 | const newTask = { ...taskData, description: newDescription }; 44 | setTaskData(newTask); 45 | throttledSave(newTask); 46 | }; 47 | 48 | const handleBodyChange = (newBody) => { 49 | const newTask = { ...taskData, body: newBody }; 50 | setTaskData(newTask); 51 | saveTask(newTask); 52 | }; 53 | 54 | return ( 55 |
56 |
57 |
58 | setShowEmojiPicker((s) => !s)} 62 | /> 63 | {showEmojiPicker && ( 64 |
65 | 70 |
71 | )} 72 |
73 |
74 | 81 |
82 |
83 |
84 |