diff --git a/app/(private)/todos/layout.tsx b/app/(private)/todos/layout.tsx
new file mode 100644
index 0000000..d56e4d6
--- /dev/null
+++ b/app/(private)/todos/layout.tsx
@@ -0,0 +1,14 @@
+import { Container } from '@mantine/core';
+import { ReactNode } from 'react';
+
+interface Props {
+ children: ReactNode;
+}
+
+export default async function RootLayout({ children }: Props) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/app/(private)/todos/page.tsx b/app/(private)/todos/page.tsx
new file mode 100644
index 0000000..7bfe723
--- /dev/null
+++ b/app/(private)/todos/page.tsx
@@ -0,0 +1,18 @@
+'use client';
+
+import { TodoPageActions } from '@/components/Todo';
+import Todos from '@/components/Todo/Todo';
+import TodoSkelton from '@/components/Todo/TodoSkelton';
+import useFetchData from '@/hooks/useFetchData';
+
+const TodosPage = () => {
+ const { data, refetch, loading } = useFetchData('/api/todos');
+ return (
+ <>
+
+ {loading ? : }
+ >
+ );
+};
+
+export default TodosPage;
diff --git a/app/api/list/route.ts b/app/api/list/route.ts
new file mode 100644
index 0000000..c4d5d30
--- /dev/null
+++ b/app/api/list/route.ts
@@ -0,0 +1,31 @@
+import { getServerSession } from 'next-auth';
+import { NextRequest, NextResponse } from 'next/server';
+import startDb from '@/lib/db';
+import TodoList from '@/models/TodoList';
+import { authOptions } from '../auth/[...nextauth]/authOptions';
+import { UserDataTypes } from '../auth/[...nextauth]/next-auth.interfaces';
+
+export async function GET(req: NextRequest) {
+ try {
+ const session: UserDataTypes | null = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+ await startDb();
+ let list: any[] = [];
+ const schema = req.nextUrl.searchParams.get('schema')?.toString();
+ switch (schema) {
+ case 'todos':
+ list = await TodoList.find({ user: session?.user._id }).sort('-updatedAt');
+ break;
+ default:
+ break;
+ }
+ return NextResponse.json(
+ list.map((i) => ({ path: `/${schema}/${i?._id}`, label: i?.title, color: i?.color })),
+ { status: 200 }
+ );
+ } catch (error: any) {
+ return NextResponse.json({ error: error?.message }, { status: 500 });
+ }
+}
diff --git a/app/api/todos/route.ts b/app/api/todos/route.ts
new file mode 100644
index 0000000..97e2bbd
--- /dev/null
+++ b/app/api/todos/route.ts
@@ -0,0 +1,45 @@
+import { getServerSession } from 'next-auth';
+import { NextRequest, NextResponse } from 'next/server';
+import mongoose from 'mongoose';
+import startDb from '@/lib/db';
+import Todo from '@/models/Todo';
+import { authOptions } from '../auth/[...nextauth]/authOptions';
+import { UserDataTypes } from '../auth/[...nextauth]/next-auth.interfaces';
+
+export async function GET() {
+ try {
+ const session: UserDataTypes | null = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+ await startDb();
+ const todoList = await Todo.find({ user: session?.user._id })
+ .populate({
+ path: 'list',
+ select: 'title color -_id',
+ })
+ .sort('-updatedAt');
+ return NextResponse.json(todoList, { status: 200 });
+ } catch (error: any) {
+ return NextResponse.json({ error: error?.message }, { status: 500 });
+ }
+}
+export async function POST(req: NextRequest) {
+ try {
+ const session: UserDataTypes | null = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+ const body = await req.json();
+ await startDb();
+ if (body.list) {
+ body.list = new mongoose.Types.ObjectId(String(body.list));
+ } else {
+ body.list = null;
+ }
+ const todoList = await Todo.create({ ...body, user: session?.user._id });
+ return NextResponse.json(todoList, { status: 200 });
+ } catch (error: any) {
+ return NextResponse.json({ error: error?.message }, { status: 500 });
+ }
+}
diff --git a/app/api/todos/todo-list/route.ts b/app/api/todos/todo-list/route.ts
new file mode 100644
index 0000000..4afc7c5
--- /dev/null
+++ b/app/api/todos/todo-list/route.ts
@@ -0,0 +1,34 @@
+import { getServerSession } from 'next-auth';
+import { NextRequest, NextResponse } from 'next/server';
+import startDb from '@/lib/db';
+import TodoList from '@/models/TodoList';
+import { authOptions } from '../../auth/[...nextauth]/authOptions';
+import { UserDataTypes } from '../../auth/[...nextauth]/next-auth.interfaces';
+
+export async function GET() {
+ try {
+ const session: UserDataTypes | null = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+ await startDb();
+ const todoList = await TodoList.find({ user: session?.user._id }).sort('-updatedAt');
+ return NextResponse.json(todoList, { status: 200 });
+ } catch (error: any) {
+ return NextResponse.json({ error: error?.message }, { status: 500 });
+ }
+}
+export async function POST(req: NextRequest) {
+ try {
+ const session: UserDataTypes | null = await getServerSession(authOptions);
+ if (!session?.user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+ const body = await req.json();
+ await startDb();
+ const todoList = await TodoList.create({ ...body, user: session?.user._id });
+ return NextResponse.json(todoList, { status: 200 });
+ } catch (error: any) {
+ return NextResponse.json({ error: error?.message }, { status: 500 });
+ }
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index eeb2a70..2ee876e 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -7,6 +7,7 @@ import AuthProvider from '@/Providers/AuthProvider';
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import '@mantine/nprogress/styles.css';
+import '@mantine/dates/styles.css';
import { theme } from '../theme';
export const metadata = { title: 'Dream', description: '' };
diff --git a/components/App/App.tsx b/components/App/App.tsx
index ce4ba61..60b15db 100644
--- a/components/App/App.tsx
+++ b/components/App/App.tsx
@@ -17,7 +17,7 @@ const App = ({ app, isCurrent }: Props) => {
ref={ref}
bg={hovered || isCurrent ? `${app.color}.3` : 'transparent'}
key={app.path}
- onClick={() => router.push(app.path)}
+ onClick={() => router.push(app?.path || '')}
style={{ cursor: 'pointer' }}
c={hovered || isCurrent ? 'white' : 'dark'}
>
diff --git a/components/Layout/Layout.tsx b/components/Layout/Layout.tsx
index a4a621c..90982dc 100644
--- a/components/Layout/Layout.tsx
+++ b/components/Layout/Layout.tsx
@@ -13,10 +13,12 @@ import {
Stack,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
-import { IconGridDots } from '@tabler/icons-react';
+import { IconGridDots, IconList } from '@tabler/icons-react';
import { signOut, useSession } from 'next-auth/react';
import { usePathname, useRouter } from 'next/navigation';
-import React from 'react';
+import { nprogress } from '@mantine/nprogress';
+import React, { useCallback, useEffect, useState } from 'react';
+import axios from 'axios';
import { App } from '../App';
import { APPS } from '@/lib/constants';
@@ -24,17 +26,44 @@ export default function Layout({ children }: { children: React.ReactNode }) {
const [mobileOpened, { toggle: toggleMobile }] = useDisclosure();
const [desktopOpened, { toggle: toggleDesktop }] = useDisclosure(true);
const session = useSession();
+ const loading = session?.status === 'loading';
const isLoggedIn = session?.status === 'authenticated';
const isLoggedOff = session?.status === 'unauthenticated';
const router = useRouter();
const pathname = usePathname();
- const APP = APPS.find((app) => `/${pathname.split('/')[1]}` === app.path);
+ const rootpath = pathname.split('/')[1];
+ const [APP, setAPP] = useState(APPS.find((app) => `/${rootpath}` === app?.path));
- const navigateTo = (path: string) => {
- router.push(path);
- toggleMobile();
+ const navigateTo = useCallback(
+ (path?: string) => {
+ if (path && path !== pathname) {
+ router.push(path);
+ }
+ toggleMobile();
+ },
+ [pathname]
+ );
+
+ const getList = async () => {
+ setAPP(APPS.find((app) => `/${rootpath}` === app?.path));
+ const { data } = await axios.get(`/api/list?schema=${rootpath}`);
+ setAPP((old = { sidebar: [] }) => ({ ...old, sidebar: [...old.sidebar, ...data] }));
};
+ if (isLoggedOff) {
+ router.push('/auth/login');
+ }
+
+ useEffect(() => {
+ nprogress.start();
+ getList();
+ nprogress.complete();
+ }, [rootpath]);
+
+ if (loading) {
+ return <>>;
+ }
+
return (
{APPS.map((app) => (
-
+
))}
@@ -126,19 +155,19 @@ export default function Layout({ children }: { children: React.ReactNode }) {
{isLoggedIn ? (
<>
-
- {APP?.sidebar.map((item) => (
+
+ {APP?.sidebar?.map((item) => (
))}
diff --git a/components/Todo/Todo.tsx b/components/Todo/Todo.tsx
new file mode 100644
index 0000000..a28fd94
--- /dev/null
+++ b/components/Todo/Todo.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import { ActionIcon, Badge, Group, Paper, Stack, Text } from '@mantine/core';
+import { IconCalendar, IconCircleCheckFilled, IconList, IconStarFilled } from '@tabler/icons-react';
+import dayjs from 'dayjs';
+import { TodoType } from '@/models/Todo';
+
+interface Props {
+ todos: TodoType[];
+}
+
+const Todos = ({ todos }: Props) => (
+
+ {todos?.map((i: TodoType) => (
+
+
+
+
+
+
+
+
+ {i.todo}
+
+
+
+
+
+
+
+ }
+ radius="xs"
+ variant="white"
+ display={i?.list ? 'block' : 'none'}
+ >
+ {i?.list?.title}
+
+ }
+ radius="xs"
+ variant="white"
+ display={i?.date ? 'block' : 'none'}
+ >
+ {dayjs(i?.date).format('DD MMM YYYY')}
+
+
+
+
+ ))}
+
+);
+
+export default Todos;
diff --git a/components/Todo/TodoPageActions.tsx b/components/Todo/TodoPageActions.tsx
new file mode 100644
index 0000000..274fd82
--- /dev/null
+++ b/components/Todo/TodoPageActions.tsx
@@ -0,0 +1,155 @@
+'use client';
+
+import { ActionIcon, Group, Modal, rem, Select, Stack, TextInput } from '@mantine/core';
+import { useForm } from '@mantine/form';
+import { useDisclosure } from '@mantine/hooks';
+import {
+ IconCalendar,
+ IconCaretUpDown,
+ IconCheck,
+ IconCircleCheck,
+ IconPlus,
+ IconPrinter,
+ IconShare,
+} from '@tabler/icons-react';
+import axios from 'axios';
+import React, { useEffect, useRef, useState } from 'react';
+import { nprogress } from '@mantine/nprogress';
+import { DatePickerInput } from '@mantine/dates';
+import { failure } from '@/lib/client_functions';
+import { COLORS } from '@/lib/constants';
+import FormButtons from '../FormButtons';
+
+const STYLES = {
+ input: {
+ backgroundColor: 'transparent',
+ border: 'none',
+ fontSize: 16,
+ paddingInline: 0,
+ fontWeight: 'bold',
+ },
+};
+
+interface Props {
+ refetch: () => void;
+}
+
+const TodoPageActions = ({ refetch }: Props) => {
+ const ref = useRef();
+ const [opened, { open, close }] = useDisclosure(false);
+ const [todoList, setTodoList] = useState([]);
+
+ const form = useForm({
+ initialValues: {
+ todo: '',
+ list: '',
+ date: null,
+ color: 'blue',
+ },
+ validate: {
+ todo: (value) => {
+ if (value.length === 0) {
+ ref.current.focus();
+ return 'Please enter a todo';
+ }
+ return null;
+ },
+ },
+ });
+
+ const getTodoLists = async () => {
+ try {
+ const res = await axios.get('/api/todos/todo-list');
+ setTodoList(res?.data);
+ } catch (error) {
+ failure('Something went wrong');
+ }
+ };
+
+ const onSubmit = async () => {
+ if (!form.values.todo) {
+ ref.current.focus();
+ failure('Please enter a todo');
+ return;
+ }
+ nprogress.start();
+ await axios.post('/api/todos', form.values);
+ refetch();
+ nprogress.complete();
+ onClose();
+ getTodoLists();
+ };
+
+ const onClose = () => {
+ form.reset();
+ close();
+ };
+
+ useEffect(() => {
+ getTodoLists();
+ }, []);
+
+ return (
+ <>
+
+ window.print()}>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default TodoPageActions;
diff --git a/components/Todo/TodoSkelton.tsx b/components/Todo/TodoSkelton.tsx
new file mode 100644
index 0000000..7da4f27
--- /dev/null
+++ b/components/Todo/TodoSkelton.tsx
@@ -0,0 +1,7 @@
+import { Skeleton } from '@mantine/core';
+import React from 'react';
+
+const TodoSkelton = () =>
+ [...Array(4)].map((_, i) => );
+
+export default TodoSkelton;
diff --git a/components/Todo/index.ts b/components/Todo/index.ts
new file mode 100644
index 0000000..85e91fd
--- /dev/null
+++ b/components/Todo/index.ts
@@ -0,0 +1 @@
+export { default as TodoPageActions } from './TodoPageActions';
diff --git a/components/Welcome/Welcome.test.tsx b/components/Welcome/Welcome.test.tsx
index 29709ac..eaac88c 100644
--- a/components/Welcome/Welcome.test.tsx
+++ b/components/Welcome/Welcome.test.tsx
@@ -4,9 +4,6 @@ import Welcome from './Welcome';
describe('Welcome component', () => {
it('has correct Next.js theming section link', () => {
render();
- expect(screen.getByText('this guide')).toHaveAttribute(
- 'href',
- 'https://mantine.dev/guides/next/'
- );
+ expect(screen.getByText('My Next App Template')).toBeDefined();
});
});
diff --git a/lib/constants.tsx b/lib/constants.tsx
index 6a66e79..c6ec2ff 100644
--- a/lib/constants.tsx
+++ b/lib/constants.tsx
@@ -1,6 +1,7 @@
import {
IconArchive,
IconCalendar,
+ IconCalendarEvent,
IconCheckbox,
IconClipboardText,
IconCloud,
@@ -9,11 +10,25 @@ import {
IconMessageQuestion,
IconMessages,
IconNote,
+ IconStar,
IconTrash,
IconWallet,
} from '@tabler/icons-react';
-export const APPS = [
+interface AppType {
+ label?: string;
+ path?: string;
+ icon?: JSX.Element;
+ color?: string;
+ sidebar: {
+ label?: string;
+ path?: string;
+ icon?: JSX.Element;
+ color?: string;
+ }[];
+}
+
+export const APPS: AppType[] = [
{
label: 'Notes',
path: '/notes',
@@ -30,7 +45,12 @@ export const APPS = [
path: '/todos',
icon: ,
color: 'red',
- sidebar: [],
+ sidebar: [
+ { label: 'All', path: '/todos', icon: },
+ { label: 'Today', path: '/todos/today', icon: },
+ { label: 'Scheduled', path: '/todos/scheduled', icon: },
+ { label: 'Important', path: '/todos/important', icon: },
+ ],
},
{ label: 'Calendar', path: '/calendar', icon: , color: 'green', sidebar: [] },
{ label: 'Forum', path: '/forum', icon: , color: 'indigo', sidebar: [] },
diff --git a/models/Todo.ts b/models/Todo.ts
new file mode 100644
index 0000000..dff4fe0
--- /dev/null
+++ b/models/Todo.ts
@@ -0,0 +1,63 @@
+import mongoose, { Model, model, models, Schema, Types } from 'mongoose';
+import { TodoListDocument } from './TodoList';
+
+export interface TodoDocument extends Document {
+ _id?: Types.ObjectId;
+ todo: string;
+ list: mongoose.Types.ObjectId;
+ isCompleted: boolean;
+ date: Date;
+ user: mongoose.Types.ObjectId;
+ color: string;
+}
+
+const todoSchema = new Schema(
+ {
+ todo: {
+ type: String,
+ index: true,
+ },
+ list: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'TodoList',
+ default: null,
+ },
+ date: {
+ type: Date,
+ default: null,
+ },
+ isCompleted: {
+ type: Boolean,
+ default: false,
+ },
+ user: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ default: null,
+ },
+ color: {
+ type: String,
+ enum: [
+ 'gray.0',
+ 'blue',
+ 'red',
+ 'green',
+ 'indigo',
+ 'teal',
+ 'violet',
+ 'pink',
+ 'cyan',
+ 'grape',
+ 'lime',
+ ],
+ default: 'gray.0',
+ },
+ },
+ { timestamps: true }
+);
+
+const Todo = models.Todo || model('Todo', todoSchema);
+
+export default Todo as Model;
+
+export type TodoType = TodoDocument & { list: TodoListDocument };
diff --git a/models/TodoList.ts b/models/TodoList.ts
new file mode 100644
index 0000000..fe7fb48
--- /dev/null
+++ b/models/TodoList.ts
@@ -0,0 +1,45 @@
+import mongoose, { Model, model, models, Schema, Types } from 'mongoose';
+
+export interface TodoListDocument extends Document {
+ _id?: Types.ObjectId;
+ title: string;
+ color: string;
+ user: mongoose.Types.ObjectId;
+}
+
+const todoListSchema = new Schema(
+ {
+ title: {
+ type: String,
+ collation: { locale: 'en', strength: 2 },
+ index: true,
+ },
+ user: {
+ type: Schema.Types.ObjectId,
+ ref: 'User',
+ required: true,
+ },
+ color: {
+ type: String,
+ enum: [
+ 'gray.0',
+ 'blue',
+ 'red',
+ 'green',
+ 'indigo',
+ 'teal',
+ 'violet',
+ 'pink',
+ 'cyan',
+ 'grape',
+ 'lime',
+ ],
+ default: 'gray.0',
+ },
+ },
+ { timestamps: true }
+);
+
+const TodoList = models.TodoList || model('TodoList', todoListSchema);
+
+export default TodoList as Model;