├── .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 |
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 |
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 |
71 | )}
72 |
73 |
74 |
81 |
82 |
83 |
84 |
90 |
91 |
92 |
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/components/tasklist/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { Task } from '../task';
3 | import { AddTask } from '../task/add';
4 |
5 | export function TaskList({ initialTasks = [], currentProject, currentTask }) {
6 | const [tasks, setTask] = useState(initialTasks);
7 |
8 | useEffect(() => {
9 | setTask(initialTasks);
10 | }, [initialTasks]);
11 |
12 | const handleNewTask = (newTask) => {
13 | const newProjects = tasks.concat(newTask);
14 | setTask(newProjects);
15 | };
16 |
17 | return (
18 |
19 | {tasks.map((task) => (
20 |
26 | ))}
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | serverRuntimeConfig: {
3 | databaseUrl: process.env.DATABASE_URL || 'mongodb://localhost/taskmanager',
4 | baseUrl: process.env.BASE_URL || 'http://localhost:3000',
5 | },
6 | publicRuntimeConfig: {},
7 | };
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "task-manager",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "eslint pages/ src/"
10 | },
11 | "dependencies": {
12 | "@editorjs/editorjs": "^2.19.0",
13 | "@editorjs/paragraph": "^2.8.0",
14 | "argon2": "^0.27.0",
15 | "emoji-mart": "^3.0.0",
16 | "mongoose": "^5.10.11",
17 | "next": "10.0.1",
18 | "next-auth": "^3.1.0",
19 | "react": "17.0.1",
20 | "react-dom": "17.0.1",
21 | "react-icons": "^4.1.0",
22 | "react-modal": "^3.11.2",
23 | "redaxios": "^0.3.0"
24 | },
25 | "devDependencies": {
26 | "babel-eslint": "^10.1.0",
27 | "eslint": "^7.13.0",
28 | "eslint-config-prettier": "^6.15.0",
29 | "eslint-config-standard": "^16.0.1",
30 | "eslint-config-standard-jsx": "^10.0.0",
31 | "eslint-config-standard-react": "^11.0.1",
32 | "eslint-plugin-import": "^2.22.1",
33 | "eslint-plugin-node": "^11.1.0",
34 | "eslint-plugin-prettier": "^3.1.4",
35 | "eslint-plugin-promise": "^4.2.1",
36 | "eslint-plugin-react": "^7.21.5",
37 | "eslint-plugin-standard": "^5.0.0",
38 | "postcss-flexbugs-fixes": "^5.0.0",
39 | "postcss-preset-env": "^6.7.0",
40 | "prettier": "^2.1.2",
41 | "tailwindcss": "^2.0.1"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '../styles/index.css';
2 |
3 | function MyApp({ Component, pageProps }) {
4 | return ;
5 | }
6 |
7 | export default MyApp;
8 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].js:
--------------------------------------------------------------------------------
1 | import { verify } from 'argon2';
2 | import NextAuth from 'next-auth';
3 | import Providers from 'next-auth/providers';
4 | import { serverRuntimeConfig } from '../../../next.config';
5 | import { User } from '../../../src/db';
6 |
7 | const options = {
8 | pages: {
9 | signIn: '/login',
10 | signOut: '/register',
11 | },
12 | session: {
13 | jwt: true,
14 | maxAge: 30 * 24 * 60 * 60, // 30 days
15 | updateAge: 24 * 60 * 60, // 24 hours
16 | },
17 | // Configure one or more authentication providers
18 | providers: [
19 | Providers.Credentials({
20 | name: 'Credentials',
21 | credentials: {
22 | email: {
23 | label: 'Email',
24 | type: 'text',
25 | placeholder: 'your@email.com',
26 | },
27 | password: { label: 'Password', type: 'password' },
28 | },
29 | authorize: async (credentials) => {
30 | const existingUser = await User.findOne({ email: credentials.email });
31 | if (await verify(existingUser.password, credentials.password)) {
32 | return existingUser.toObject();
33 | }
34 | return null;
35 | },
36 | }),
37 | ],
38 |
39 | callbacks: {
40 | /**
41 | * We define those two callbacks to have our actual
42 | * user in the session because by default next-auth
43 | * uses some arbitrary user object with arbitrary fields with
44 | * no other way to override it
45 | */
46 | jwt: async (token, user, account, profile, isNewUser) => {
47 | if (!user) {
48 | return token;
49 | }
50 | const { password, __v, ...tokenUser } = user;
51 | return Promise.resolve(tokenUser);
52 | },
53 | session: async (session, user, sessionToken) => {
54 | session.user = user;
55 | return Promise.resolve(session);
56 | },
57 | },
58 |
59 | // A database is optional, but required to persist accounts in a database
60 | database: serverRuntimeConfig.databaseUrl,
61 | };
62 |
63 | export default (req, res) => NextAuth(req, res, options);
64 |
--------------------------------------------------------------------------------
/pages/api/project/new.js:
--------------------------------------------------------------------------------
1 | import { getSession } from 'next-auth/client';
2 | import { Project } from '../../../src/db';
3 |
4 | export default async (req, res) => {
5 | const { name } = req.body;
6 | const { user } = await getSession({ req });
7 | try {
8 | const project = new Project({ name, user: user._id });
9 | await project.save();
10 | res.status(200).json({ project: project.toObject() });
11 | } catch (error) {
12 | res.status(400).json({ error });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/pages/api/register.js:
--------------------------------------------------------------------------------
1 | import { hash } from 'argon2';
2 | import { User } from '../../src/db';
3 |
4 | export default async (req, res) => {
5 | const { email, password } = req.body;
6 | try {
7 | const hashedPassword = await hash(password);
8 | const user = new User({ email, password: hashedPassword });
9 | await user.save();
10 | res.status(200).json({ user: user.toObject() });
11 | } catch (error) {
12 | res.status(400).json({ error });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/pages/api/task/[taskid].js:
--------------------------------------------------------------------------------
1 | import { getSession } from 'next-auth/client';
2 | import { Task } from '../../../src/db';
3 |
4 | export default async (req, res) => {
5 | try {
6 | const { user } = await getSession({ req });
7 | if (!user) {
8 | throw new Error('Not logged in!');
9 | }
10 |
11 | const { _id, project, user: taskUser, ...newData } = req.body;
12 | const tasks = await Task.findByIdAndUpdate(req.query.taskid, newData, {
13 | new: true,
14 | });
15 | res.status(200).json({ tasks });
16 | } catch (error) {
17 | res.status(400).json({ error });
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/pages/api/task/new.js:
--------------------------------------------------------------------------------
1 | import { getSession } from 'next-auth/client';
2 | import { Task } from '../../../src/db';
3 |
4 | export default async (req, res) => {
5 | const { name, project } = req.body;
6 | const { user } = await getSession({ req });
7 | try {
8 | const task = new Task({ name, project, user: user._id });
9 | await task.save();
10 | res.status(200).json({ task: task.toObject() });
11 | } catch (error) {
12 | res.status(400).json({ error });
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import { getSession } from 'next-auth/client';
2 | import Head from 'next/head';
3 | import { Sidebar } from '../components/sidebar';
4 | import { getUserProjects } from '../src/utils';
5 |
6 | export default function Home({ session, projects }) {
7 | return (
8 | <>
9 |
10 | Task Manager
11 |
12 |
13 |
14 |
15 |
16 |
17 | Welcome to Task Manager!
18 | You are signed in as {session.user.email}
19 |
20 |
21 | >
22 | );
23 | }
24 |
25 | export async function getServerSideProps(context) {
26 | // get user session
27 | const session = await getSession(context);
28 | // if there's no session - redirect to login
29 | if (!session?.user) {
30 | return {
31 | redirect: {
32 | destination: '/login',
33 | permanent: false,
34 | },
35 | };
36 | }
37 |
38 | // get user projects
39 | const userProjects = await getUserProjects(session.user);
40 | // Convert mongoose ObjectIDs to strings
41 | // because Next.js doesn't understand you can serialize
42 | // (for some reason)
43 | const projects = userProjects.map((project) => {
44 | const { __v, _id, user, ...obj } = project;
45 | return { ...obj, _id: String(_id), user: String(user) };
46 | });
47 |
48 | return {
49 | props: { projects, session }, // will be passed to the page component as props
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/pages/login.js:
--------------------------------------------------------------------------------
1 | import { csrfToken } from 'next-auth/client';
2 | import Head from 'next/head';
3 | import Link from 'next/link';
4 |
5 | export default function Login({ csrfToken }) {
6 | return (
7 |
8 |
9 |
Task Manager - Login
10 |
11 |
12 |
13 |
14 |
15 | Task Manager: Login
16 |
17 |
18 |
71 |
72 |
73 | );
74 | }
75 |
76 | Login.getInitialProps = async (context) => {
77 | return {
78 | csrfToken: await csrfToken(context),
79 | };
80 | };
81 |
--------------------------------------------------------------------------------
/pages/project/[projectid].js:
--------------------------------------------------------------------------------
1 | import { getSession, signOut } from 'next-auth/client';
2 | import Head from 'next/head';
3 | import { Sidebar } from '../../components/sidebar';
4 | import { TaskList } from '../../components/tasklist';
5 | import { getProjectTasks, getUserProjects } from '../../src/utils';
6 |
7 | export default function ProjectPage({
8 | session,
9 | projects,
10 | tasks,
11 | currentProject,
12 | }) {
13 | return (
14 | <>
15 |
16 | Task Manager - {currentProject.name}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Welcome to Next.js!
26 | Signed in as {session.user.email}
27 |
28 |
29 |
30 | >
31 | );
32 | }
33 |
34 | export async function getServerSideProps(context) {
35 | // get user session
36 | const session = await getSession(context);
37 | // if there's no session - redirect to login
38 | if (!session?.user) {
39 | return {
40 | redirect: {
41 | destination: '/login',
42 | permanent: false,
43 | },
44 | };
45 | }
46 |
47 | const userProjects = await getUserProjects(session.user);
48 | // Convert mongoose ObjectIDs to strings
49 | // because Next.js doesn't understand you can serialize
50 | // (for some reason)
51 | const projects = userProjects.map((project) => {
52 | const { __v, _id, user, ...obj } = project;
53 | return { ...obj, _id: String(_id), user: String(user) };
54 | });
55 | // resolve current project
56 | const currentProjectId = context.params.projectid;
57 | const currentProject = projects.find(
58 | (project) => project._id === currentProjectId
59 | );
60 |
61 | // fetch tasks list for current project
62 | const projectTasks = await getProjectTasks(currentProject._id);
63 | // Convert mongoose ObjectIDs to strings
64 | // because Next.js doesn't understand you can serialize
65 | // (for some reason)
66 | const tasks = projectTasks.map((task) => {
67 | const { __v, _id, user, project, ...obj } = task;
68 | return {
69 | ...obj,
70 | _id: String(_id),
71 | user: String(user),
72 | project: String(project),
73 | };
74 | });
75 |
76 | return {
77 | props: { session, projects, tasks, currentProject }, // will be passed to the page component as props
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/pages/project/[projectid]/task/[taskid].js:
--------------------------------------------------------------------------------
1 | import { getSession } from 'next-auth/client';
2 | import Head from 'next/head';
3 | import { Sidebar } from '../../../../components/sidebar';
4 | import { TaskView } from '../../../../components/task/view';
5 | import { TaskList } from '../../../../components/tasklist';
6 | import { getProjectTasks, getUserProjects } from '../../../../src/utils';
7 |
8 | export default function ProjectPage({
9 | session,
10 | projects,
11 | tasks,
12 | currentProject,
13 | currentTask,
14 | }) {
15 | return (
16 | <>
17 |
18 | Task Manager - {currentProject.name}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | >
33 | );
34 | }
35 |
36 | export async function getServerSideProps(context) {
37 | // get user session
38 | const session = await getSession(context);
39 | // if there's no session - redirect to login
40 | if (!session?.user) {
41 | return {
42 | redirect: {
43 | destination: '/login',
44 | permanent: false,
45 | },
46 | };
47 | }
48 |
49 | const userProjects = await getUserProjects(session.user);
50 | // Convert mongoose ObjectIDs to strings
51 | // because Next.js doesn't understand you can serialize
52 | // (for some reason)
53 | const projects = userProjects.map((project) => {
54 | const { __v, _id, user, ...obj } = project;
55 | return { ...obj, _id: String(_id), user: String(user) };
56 | });
57 | // resolve current project
58 | const currentProjectId = context.params.projectid;
59 | const currentProject = projects.find(
60 | (project) => project._id === currentProjectId
61 | );
62 |
63 | // fetch tasks list for current project
64 | const projectTasks = await getProjectTasks(currentProject._id);
65 | // Convert mongoose ObjectIDs to strings
66 | // because Next.js doesn't understand you can serialize
67 | // (for some reason)
68 | const tasks = projectTasks.map((task) => {
69 | const { __v, _id, user, project, ...obj } = task;
70 | return {
71 | ...obj,
72 | _id: String(_id),
73 | user: String(user),
74 | project: String(project),
75 | };
76 | });
77 |
78 | // resolve current project
79 | const currentTaskId = context.params.taskid;
80 | const currentTask = tasks.find((task) => task._id === currentTaskId);
81 |
82 | return {
83 | props: { session, projects, tasks, currentProject, currentTask }, // will be passed to the page component as props
84 | };
85 | }
86 |
--------------------------------------------------------------------------------
/pages/register.js:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import Link from 'next/link';
3 | import { useRef } from 'react';
4 | import axios from 'redaxios';
5 |
6 | export default function Register() {
7 | const emailRef = useRef();
8 | const passRef = useRef();
9 |
10 | const doRegister = async () => {
11 | const data = {
12 | email: emailRef.current.value,
13 | password: passRef.current.value,
14 | };
15 |
16 | const result = await axios.post('/api/register', data);
17 | console.log(result);
18 | // TODO: login & redirect to home
19 | };
20 |
21 | return (
22 |
23 |
24 |
Task Manager - Register
25 |
26 |
27 |
28 |
29 |
30 | Task Manager: Register
31 |
32 |
33 |
34 |
35 |
41 |
50 |
51 |
52 |
58 |
66 |
67 |
68 |
69 |
70 |
Login
71 |
72 |
73 |
79 |
80 |
81 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | 'tailwindcss',
4 | 'postcss-flexbugs-fixes',
5 | [
6 | 'postcss-preset-env',
7 | {
8 | autoprefixer: {
9 | flexbox: 'no-2009',
10 | },
11 | stage: 3,
12 | features: {
13 | 'custom-properties': false,
14 | },
15 | },
16 | ],
17 | ],
18 | };
19 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BuildingXwithJS/task-manager/f20fc04448f36e013356ad6383c7b8720d65a1ac/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/db.js:
--------------------------------------------------------------------------------
1 | const { createConnection } = require('mongoose');
2 | const {
3 | serverRuntimeConfig: { databaseUrl },
4 | } = require('../next.config');
5 |
6 | // schemas
7 | const userSchema = require('./user');
8 | const projectSchema = require('./project');
9 | const taskSchema = require('./task');
10 |
11 | // connect to given URL
12 | const db = createConnection(databaseUrl, {
13 | useNewUrlParser: true,
14 | useCreateIndex: true,
15 | useUnifiedTopology: true,
16 | });
17 | exports.db = db;
18 |
19 | // handle DB errors
20 | db.on('error', (error) => {
21 | console.error('MongoDB connection error:', error);
22 | // exit immediately on error
23 | process.exit(1);
24 | });
25 |
26 | // connection ready
27 | exports.connected = new Promise((resolve) => db.once('open', resolve));
28 |
29 | // models
30 | exports.User = db.model('User', userSchema);
31 | exports.Project = db.model('Project', projectSchema);
32 | exports.Task = db.model('Task', taskSchema);
33 |
--------------------------------------------------------------------------------
/src/project.js:
--------------------------------------------------------------------------------
1 | const { Schema, Types } = require('mongoose');
2 |
3 | const projectSchema = new Schema({
4 | name: { type: String, required: true },
5 | user: { type: Types.ObjectId, ref: 'User', required: true },
6 | });
7 |
8 | module.exports = projectSchema;
9 |
--------------------------------------------------------------------------------
/src/task.js:
--------------------------------------------------------------------------------
1 | const { Schema, Types } = require('mongoose');
2 |
3 | const taskSchema = new Schema({
4 | name: { type: String, required: true },
5 | description: { type: String },
6 | icon: { type: String },
7 | body: { type: Object },
8 | project: { type: Types.ObjectId, ref: 'Project', required: true },
9 | user: { type: Types.ObjectId, ref: 'User', required: true },
10 | });
11 |
12 | module.exports = taskSchema;
13 |
--------------------------------------------------------------------------------
/src/user.js:
--------------------------------------------------------------------------------
1 | const { Schema } = require('mongoose');
2 |
3 | const userSchema = new Schema({
4 | email: { type: String, unique: true, required: true },
5 | password: { type: String, required: true },
6 | });
7 |
8 | module.exports = userSchema;
9 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import { Project, Task } from './db';
2 |
3 | export const getUserProjects = async (user) => {
4 | const projects = await Project.find({ user: user._id }).lean();
5 | return projects;
6 | };
7 |
8 | export const getProjectTasks = async (project) => {
9 | const tasks = await Task.find({ project }).lean();
10 | return tasks;
11 | };
12 |
--------------------------------------------------------------------------------
/styles/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 |
3 | /* Write your own custom base styles here */
4 |
5 | /* Start purging... */
6 | @tailwind components;
7 | /* Stop purging. */
8 |
9 | /* Write your own custom component styles here */
10 | .btn-blue {
11 | @apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
12 | }
13 |
14 | /* Start purging... */
15 | @tailwind utilities;
16 | /* Stop purging. */
17 |
18 | /* Your own custom utilities */
19 | html,
20 | body,
21 | #__next {
22 | @apply h-screen w-screen;
23 | }
24 |
25 | .codex-editor__redactor {
26 | padding-top: 2em;
27 | padding-bottom: 2em !important;
28 | }
29 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require('tailwindcss/colors');
2 |
3 | module.exports = {
4 | purge: ['./components/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}'],
5 | theme: {
6 | extend: {
7 | colors: {
8 | coolGray: colors.coolGray,
9 | indigo: colors.indigo,
10 | rose: colors.rose,
11 | 'accent-1': '#333',
12 | },
13 | gridTemplateColumns: {
14 | // 1 column project layout
15 | 'projects-layout': '4em 1fr',
16 | // Full 3 columns layout
17 | 'main-layout': '4em 15em 1fr',
18 | },
19 | },
20 | },
21 | variants: {},
22 | plugins: [],
23 | };
24 |
--------------------------------------------------------------------------------