From 6cbdc226ea829a3a3e832424fc90a595569d5cb8 Mon Sep 17 00:00:00 2001 From: vishal Date: Sat, 22 Jun 2024 17:29:10 +0530 Subject: [PATCH 1/3] feat: todos layout | sidebar | api --- app/api/list/route.ts | 31 ++++++++++++++++++++++++ components/Layout/Layout.tsx | 47 ++++++++++++++++++++++++------------ lib/constants.tsx | 24 ++++++++++++++++-- models/TodoList.ts | 45 ++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 17 deletions(-) create mode 100644 app/api/list/route.ts create mode 100644 models/TodoList.ts 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/components/Layout/Layout.tsx b/components/Layout/Layout.tsx index a4a621c..9832e2d 100644 --- a/components/Layout/Layout.tsx +++ b/components/Layout/Layout.tsx @@ -13,10 +13,11 @@ 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 React, { useCallback, useEffect, useState } from 'react'; +import axios from 'axios'; import { App } from '../App'; import { APPS } from '@/lib/constants'; @@ -28,13 +29,29 @@ export default function Layout({ children }: { children: React.ReactNode }) { 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 !== 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] })); }; + useEffect(() => { + getList(); + }, [rootpath]); + return ( {APPS.map((app) => ( - + ))} @@ -126,19 +143,19 @@ export default function Layout({ children }: { children: React.ReactNode }) { {isLoggedIn ? ( <> - - {APP?.sidebar.map((item) => ( + + {APP?.sidebar?.map((item) => ( ))} 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/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; From 8e764c550f4733fc381bb049068b820b23e3927f Mon Sep 17 00:00:00 2001 From: vishal Date: Sat, 22 Jun 2024 21:27:18 +0530 Subject: [PATCH 2/3] feat: todos page --- app/(private)/todos/layout.tsx | 14 +++ app/(private)/todos/page.tsx | 18 ++++ app/api/todos/route.ts | 45 ++++++++ app/api/todos/todo-list/route.ts | 34 ++++++ app/layout.tsx | 1 + components/Layout/Layout.tsx | 16 ++- components/Todo/Todo.tsx | 55 ++++++++++ components/Todo/TodoPageActions.tsx | 155 ++++++++++++++++++++++++++++ components/Todo/TodoSkelton.tsx | 7 ++ components/Todo/index.ts | 1 + models/Todo.ts | 63 +++++++++++ 11 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 app/(private)/todos/layout.tsx create mode 100644 app/(private)/todos/page.tsx create mode 100644 app/api/todos/route.ts create mode 100644 app/api/todos/todo-list/route.ts create mode 100644 components/Todo/Todo.tsx create mode 100644 components/Todo/TodoPageActions.tsx create mode 100644 components/Todo/TodoSkelton.tsx create mode 100644 components/Todo/index.ts create mode 100644 models/Todo.ts 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/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/Layout/Layout.tsx b/components/Layout/Layout.tsx index 9832e2d..90982dc 100644 --- a/components/Layout/Layout.tsx +++ b/components/Layout/Layout.tsx @@ -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'; @@ -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(); @@ -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(); }, @@ -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 ( ( + + {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()}> + + + + + + + + + + +
onSubmit())} onReset={() => form.reset()}> + + } + ref={ref} + /> +