Skip to content

Commit

Permalink
feat: todos page
Browse files Browse the repository at this point in the history
  • Loading branch information
vishalkondle-dev committed Jun 22, 2024
1 parent 6cbdc22 commit 8e764c5
Show file tree
Hide file tree
Showing 11 changed files with 407 additions and 2 deletions.
14 changes: 14 additions & 0 deletions app/(private)/todos/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Container px={0} size="sm">
{children}
</Container>
);
}
18 changes: 18 additions & 0 deletions app/(private)/todos/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<TodoPageActions refetch={refetch} />
{loading ? <TodoSkelton /> : <Todos todos={data} />}
</>
);
};

export default TodosPage;
45 changes: 45 additions & 0 deletions app/api/todos/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
34 changes: 34 additions & 0 deletions app/api/todos/todo-list/route.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
}
1 change: 1 addition & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: '' };
Expand Down
16 changes: 14 additions & 2 deletions components/Layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useDisclosure } from '@mantine/hooks';
import { IconGridDots, IconList } from '@tabler/icons-react';
import { signOut, useSession } from 'next-auth/react';
import { usePathname, useRouter } from 'next/navigation';
import { nprogress } from '@mantine/nprogress';
import React, { useCallback, useEffect, useState } from 'react';
import axios from 'axios';
import { App } from '../App';
Expand All @@ -25,6 +26,7 @@ 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();
Expand All @@ -34,8 +36,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {

const navigateTo = useCallback(
(path?: string) => {
if (path !== pathname) {
router.push(path || '');
if (path && path !== pathname) {
router.push(path);
}
toggleMobile();
},
Expand All @@ -48,10 +50,20 @@ export default function Layout({ children }: { children: React.ReactNode }) {
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 (
<AppShell
header={{ height: 60 }}
Expand Down
55 changes: 55 additions & 0 deletions components/Todo/Todo.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<Stack>
{todos?.map((i: TodoType) => (
<Paper bg={`${i.color}.3`} p="md" key={String(i._id)}>
<Stack gap="xs">
<Group gap="xs" wrap="nowrap" justify="space-between">
<Group gap="xs" wrap="nowrap">
<ActionIcon color="gray.0" variant="transparent">
<IconCircleCheckFilled />
</ActionIcon>
<Text fw={700} c="gray.0" lineClamp={2}>
{i.todo}
</Text>
</Group>
<ActionIcon color="gray.0" variant="transparent">
<IconStarFilled />
</ActionIcon>
</Group>
<Group display={i?.list || i?.date ? 'flex' : 'none'}>
<Badge
c={i?.list?.color || 'dark'}
leftSection={<IconList size={14} />}
radius="xs"
variant="white"
display={i?.list ? 'block' : 'none'}
>
{i?.list?.title}
</Badge>
<Badge
c={i?.list?.color || 'dark'}
leftSection={<IconCalendar size={14} />}
radius="xs"
variant="white"
display={i?.date ? 'block' : 'none'}
>
{dayjs(i?.date).format('DD MMM YYYY')}
</Badge>
</Group>
</Stack>
</Paper>
))}
</Stack>
);

export default Todos;
155 changes: 155 additions & 0 deletions components/Todo/TodoPageActions.tsx
Original file line number Diff line number Diff line change
@@ -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<any>();
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 (
<>
<Group mt="sm" mb="xl" justify="right">
<ActionIcon variant="subtle" color="gray" onClick={() => window.print()}>
<IconPrinter />
</ActionIcon>
<ActionIcon variant="subtle" color="gray">
<IconShare />
</ActionIcon>
<ActionIcon onClick={open} variant="subtle" color="gray">
<IconPlus />
</ActionIcon>
</Group>
<Modal opened={opened} onClose={onClose} title="New todo" centered>
<form onSubmit={form.onSubmit(() => onSubmit())} onReset={() => form.reset()}>
<Stack>
<TextInput
{...form.getInputProps('todo')}
placeholder="Enter todo"
styles={STYLES}
rightSection={<IconCircleCheck />}
ref={ref}
/>
<Select
{...form.getInputProps('list')}
data={todoList.map(({ _id, title }) => ({ label: title, value: _id }))}
placeholder="Select a list (optional)"
rightSection={<IconCaretUpDown />}
clearable
styles={STYLES}
/>
<DatePickerInput
{...form.getInputProps('date')}
placeholder="Pick date"
styles={STYLES}
rightSection={<IconCalendar />}
/>
<Group wrap="nowrap" gap={rem(4)} justify="space-between">
{COLORS?.map((color) => (
<ActionIcon
color={`${color}.3`}
size="sm"
radius="xl"
key={color}
style={{ border: '1px solid gray' }}
onClick={() =>
form.setFieldValue('color', form.values.color === color ? '' : color)
}
>
{color === form.values.color && (
<IconCheck stroke={4} style={{ width: rem(14) }} />
)}
</ActionIcon>
))}
</Group>
<FormButtons />
</Stack>
</form>
</Modal>
</>
);
};

export default TodoPageActions;
7 changes: 7 additions & 0 deletions components/Todo/TodoSkelton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Skeleton } from '@mantine/core';
import React from 'react';

const TodoSkelton = () =>
[...Array(4)].map((_, i) => <Skeleton key={String(i)} height={100} mt="md" animate />);

export default TodoSkelton;
1 change: 1 addition & 0 deletions components/Todo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as TodoPageActions } from './TodoPageActions';
Loading

0 comments on commit 8e764c5

Please sign in to comment.