├── .vs ├── ProjectSettings.json ├── VSWorkspaceState.json ├── slnx.sqlite └── speech-scheduler │ ├── FileContentIndex │ ├── 44eaf78b-c070-4b12-92bd-b94d6c46f211.vsidx │ ├── b348d438-ae52-41fd-8ade-aae6d166eb7b.vsidx │ ├── b52c5fa4-9244-4240-a9c7-c3809eed7eba.vsidx │ ├── f5a6262b-0a14-4656-9963-433d5975588d.vsidx │ └── read.lock │ └── v17 │ ├── .futdcache.v2 │ └── .wsuo ├── README.md ├── client ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── actions │ ├── get-labels.tsx │ ├── get-lists.tsx │ ├── get-tasks.tsx │ └── get-user.ts ├── app │ ├── (platform) │ │ ├── _components │ │ │ ├── left-sidebar.tsx │ │ │ ├── mobile-sidebar.tsx │ │ │ ├── navbar.tsx │ │ │ ├── prompt-menu.tsx │ │ │ ├── right-sidebar.tsx │ │ │ ├── side-nav.tsx │ │ │ ├── toggle-theme.tsx │ │ │ └── user-nav.tsx │ │ ├── inbox │ │ │ ├── _components │ │ │ │ └── board-column.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── labels │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── lists │ │ │ └── [listId] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ ├── today │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ └── upcoming │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── (website) │ │ ├── _components │ │ │ ├── auth-nav.tsx │ │ │ ├── fade-on-view.tsx │ │ │ ├── footer.tsx │ │ │ ├── mobile-nav.tsx │ │ │ ├── navbar-link.tsx │ │ │ ├── navbar.tsx │ │ │ ├── prompt-form-preview.tsx │ │ │ ├── task-form-preview.tsx │ │ │ ├── theme-switcher.tsx │ │ │ └── typewriter-effect.tsx │ │ ├── _sections │ │ │ ├── additional-features.tsx │ │ │ ├── call-to-action.tsx │ │ │ ├── features.tsx │ │ │ ├── hero.tsx │ │ │ └── pricing.tsx │ │ ├── docs │ │ │ ├── [...slug] │ │ │ │ └── page.tsx │ │ │ ├── _components │ │ │ │ ├── breadcrumbs.tsx │ │ │ │ ├── sidebar-link.tsx │ │ │ │ ├── sidebar.tsx │ │ │ │ └── table-of-contents.tsx │ │ │ ├── _content │ │ │ │ └── test.mdx │ │ │ └── layout.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ ├── [...nextAuth] │ │ │ │ └── route.ts │ │ │ ├── login │ │ │ │ └── route.ts │ │ │ └── register │ │ │ │ └── route.ts │ │ ├── labels │ │ │ ├── [labelId] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── lists │ │ │ ├── [listId] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ └── tasks │ │ │ ├── [taskId] │ │ │ ├── labels │ │ │ │ └── [labelId] │ │ │ │ │ └── route.ts │ │ │ ├── route.ts │ │ │ └── subtasks │ │ │ │ ├── [subtaskId] │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ └── route.ts │ ├── auth │ │ ├── auth-error.tsx │ │ ├── auth-form.tsx │ │ ├── error │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── login │ │ │ └── page.tsx │ │ ├── register-user.ts │ │ ├── register │ │ │ └── page.tsx │ │ └── socials-actions.tsx │ ├── favicon.ico │ └── layout.tsx ├── auth.config.ts ├── components.json ├── components │ ├── date-picker.tsx │ ├── filter-panel.tsx │ ├── filter-view.tsx │ ├── filter-week.tsx │ ├── label-actions.tsx │ ├── label-item.tsx │ ├── list-form.tsx │ ├── list-item.tsx │ ├── list-picker.tsx │ ├── mention-input.tsx │ ├── modals │ │ ├── alert-modal.tsx │ │ ├── filter-overlay.tsx │ │ ├── list-modal.tsx │ │ ├── settings-overlay.tsx │ │ └── task-overlay.tsx │ ├── page-with-views.tsx │ ├── priority-picker.tsx │ ├── prompt-form.tsx │ ├── providers │ │ ├── overlay-provider.tsx │ │ ├── theme-provider.tsx │ │ └── toast-provider.tsx │ ├── resizable-layout.tsx │ ├── retain-query-link.tsx │ ├── settings │ │ ├── account-form.tsx │ │ ├── label-form.tsx │ │ ├── layout-form.tsx │ │ ├── preferences-form.tsx │ │ └── settings-panel.tsx │ ├── status-checkbox.tsx │ ├── subtask-actions.tsx │ ├── subtask-form.tsx │ ├── subtask-item.tsx │ ├── subtask-list.tsx │ ├── task-actions.tsx │ ├── task-form.tsx │ ├── task-item.tsx │ ├── task-list.tsx │ ├── task-suggestion.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── container.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── experimental-calendar.tsx │ │ ├── form.tsx │ │ ├── icons.tsx │ │ ├── input.tsx │ │ ├── label-badge.tsx │ │ ├── label.tsx │ │ ├── loading.tsx │ │ ├── page.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── select.tsx │ │ ├── seperator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx ├── hooks │ ├── use-click-outside.tsx │ ├── use-current-user.tsx │ ├── use-debounce.tsx │ ├── use-filter.tsx │ ├── use-media-query.tsx │ └── use-mounted.tsx ├── lib │ ├── api.ts │ ├── auth.ts │ ├── config.ts │ ├── constants.ts │ ├── db.ts │ ├── util │ │ ├── error.ts │ │ ├── filter.ts │ │ ├── mention-processor.ts │ │ ├── open-ai.ts │ │ └── tw-merge.ts │ └── validations │ │ ├── auth-schema.ts │ │ ├── label-schema.ts │ │ ├── list-schema.ts │ │ ├── subtask-schema.ts │ │ └── task-schema.ts ├── mdx-components.tsx ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma │ └── schema.prisma ├── public │ ├── hero-1.png │ ├── keyboard-shortcuts.gif │ ├── next.svg │ ├── noise.webp │ ├── static │ │ ├── bg-blur-1.webp │ │ ├── bg-blur-2.webp │ │ ├── bg-blur-3.webp │ │ ├── bg-blur-4.webp │ │ └── bg-blur-5.webp │ └── vercel.svg ├── routes.ts ├── services │ ├── label-service.ts │ ├── list-service.ts │ ├── subtask-service.ts │ └── task-service.ts ├── store │ ├── layout-store.tsx │ ├── modal-store.tsx │ └── settings-store.ts ├── styles │ ├── fonts.ts │ ├── globals.css │ └── styles.ts ├── tailwind.config.ts ├── tsconfig.json └── types.ts └── server ├── .gitignore ├── Context └── ApplicationContext.cs ├── Controllers ├── AuthController.cs ├── LabelsController.cs ├── ListsController.cs ├── ProjectsController.cs ├── RecurringTasksController.cs ├── SubtasksController.cs ├── TasksController.cs └── UsersController.cs ├── Migrations ├── 20231125112555_V2Init.cs ├── 20231125122843_CascadeRelation.Designer.cs ├── 20231125122843_CascadeRelation.cs ├── 20231202101035_UsernameToEmail.Designer.cs ├── 20231202101035_UsernameToEmail.cs ├── 20231202184105_AddProject.Designer.cs ├── 20231202184105_AddProject.cs ├── 20231202184734_ProjectToProjects.Designer.cs ├── 20231202184734_ProjectToProjects.cs ├── 20231203185027_AddTaskProps.Designer.cs ├── 20231203185027_AddTaskProps.cs ├── 20231217235849_RenameIsCompleted.Designer.cs ├── 20231217235849_RenameIsCompleted.cs └── ApplicationContextModelSnapshot.cs ├── Models ├── Enums.cs ├── Label.cs ├── List.cs ├── Project.cs ├── RecurringTask.cs ├── SeedData.cs ├── Subtask.cs ├── Task.cs ├── User.cs └── UserDto.cs ├── Program.cs ├── Properties └── launchSettings.json ├── Services ├── IUserService.cs └── UserService.cs ├── Utility └── AuthUtils.cs ├── appsettings.Development.json ├── appsettings.json ├── server.csproj └── server.sln /.vs/ProjectSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "CurrentProjectSetting": null 3 | } -------------------------------------------------------------------------------- /.vs/VSWorkspaceState.json: -------------------------------------------------------------------------------- 1 | { 2 | "ExpandedNodes": [ 3 | "", 4 | "\\server" 5 | ], 6 | "SelectedNode": "\\server\\WeatherForecast.cs", 7 | "PreviewInSolutionExplorer": false 8 | } -------------------------------------------------------------------------------- /.vs/slnx.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/.vs/slnx.sqlite -------------------------------------------------------------------------------- /.vs/speech-scheduler/FileContentIndex/44eaf78b-c070-4b12-92bd-b94d6c46f211.vsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/.vs/speech-scheduler/FileContentIndex/44eaf78b-c070-4b12-92bd-b94d6c46f211.vsidx -------------------------------------------------------------------------------- /.vs/speech-scheduler/FileContentIndex/b348d438-ae52-41fd-8ade-aae6d166eb7b.vsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/.vs/speech-scheduler/FileContentIndex/b348d438-ae52-41fd-8ade-aae6d166eb7b.vsidx -------------------------------------------------------------------------------- /.vs/speech-scheduler/FileContentIndex/b52c5fa4-9244-4240-a9c7-c3809eed7eba.vsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/.vs/speech-scheduler/FileContentIndex/b52c5fa4-9244-4240-a9c7-c3809eed7eba.vsidx -------------------------------------------------------------------------------- /.vs/speech-scheduler/FileContentIndex/f5a6262b-0a14-4656-9963-433d5975588d.vsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/.vs/speech-scheduler/FileContentIndex/f5a6262b-0a14-4656-9963-433d5975588d.vsidx -------------------------------------------------------------------------------- /.vs/speech-scheduler/FileContentIndex/read.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/.vs/speech-scheduler/FileContentIndex/read.lock -------------------------------------------------------------------------------- /.vs/speech-scheduler/v17/.futdcache.v2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/.vs/speech-scheduler/v17/.futdcache.v2 -------------------------------------------------------------------------------- /.vs/speech-scheduler/v17/.wsuo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/.vs/speech-scheduler/v17/.wsuo -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://github.com/mariangle/.taskify/assets/124585244/d4130585-5ea5-4f34-bfaf-ea8daf5cc7a0) 2 | 3 |

4 | 5 |

Taskify

6 | 7 |

8 | 9 |

10 | Task Manager app with modern features. 11 |

12 | 13 |

14 | 15 | LinkedIn 16 | 17 |

18 |
19 | 20 | ### APP Features 21 | - AI Generated tasks 22 | - Natural Language Processing (NLP) 23 | - Mention-driven task assignments via @ or # 24 | - Keyboard Shortcuts 25 | - Lists and labels 26 | - Speech Recognition 27 | - Subtasks 28 | - Drag and drop 29 | - Reccuring tasks 30 | - Filtering tasks 31 | - Multiple views 32 | - Dark and light mode 33 | - Layout customization 34 | - Native drawers for smaller devices 35 | - Loading skeleton states for everything 36 | 37 | ### Landing page 38 | - Docs page 39 | - Pure CSS animations and fade in animations 40 | - Interactive preview components 41 | - Just overall sleek and modern designed 42 | 43 | ### Libraries and technologies 44 | - .NET Web API 45 | - C# and Typescript 46 | - Next.js14 47 | - TailwindCSS 48 | - Vercel 49 | - PostgreSQL 50 | - Prisma 51 | - Zustand (state management) 52 | - SWR (stale-while-revalidate strategy) 53 | - zod 54 | - react-speech-recognition 55 | - reusable components 56 | - dnd-kit (drag and drop) 57 | - openai API (AI) 58 | - date-fns 59 | -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | PUBLIC_URL= 2 | DEV_URL= 3 | 4 | DATABASE_URL= 5 | DIRECT_URL= 6 | 7 | AUTH_SECRET= 8 | 9 | GOOGLE_CLIENT_ID= 10 | GOOGLE_CLIENT_SECRET= 11 | 12 | GITHUB_CLIENT_ID= 13 | GITHUB_CLIENT_SECRET= 14 | 15 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["next/core-web-vitals", "airbnb", "prettier"], 4 | "rules": { 5 | "no-shadow": "off", 6 | "no-unused-vars": "warn", 7 | "no-unused-expressions": "off", 8 | "jsx-a11y/click-events-have-key-events": "off", 9 | "react/require-default-props": "off", 10 | "react/jsx-props-no-spreading": "off", 11 | "react/react-in-jsx-scope": "off", 12 | "react/prop-types": "off", 13 | "import/prefer-default-export": "off", 14 | "import/no-unresolved": "off", 15 | "react/jsx-filename-extension": [1, { "extensions": [".tsx"] }], 16 | "linebreak-style": "off", 17 | "@typescript-eslint/consistent-type-imports": [ 18 | "off", 19 | { "prefer": "type-imports" } 20 | ], 21 | "import/extensions": [ 22 | "off", 23 | "ignorePackages", 24 | { 25 | "js": "never", 26 | "jsx": "never", 27 | "ts": "never", 28 | "tsx": "never" 29 | } 30 | ] 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /node_modules/ 6 | /.pnp 7 | .pnp.js 8 | .yarn/install-state.gz 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | .env 32 | notes.txt 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "tabWidth": 2, 6 | "endOfLine": "auto", 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /client/actions/get-labels.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | import { db } from '@/lib/db'; 3 | import { auth } from '@/lib/auth'; 4 | import { LOGIN_PATH } from '@/routes'; 5 | 6 | export const getLabels = async () => { 7 | const session = await auth(); 8 | 9 | if (!session || !session.user) { 10 | redirect(LOGIN_PATH); 11 | } 12 | 13 | const labels = await db.label.findMany({ 14 | where: { userId: session.user?.id }, 15 | }); 16 | 17 | return labels; 18 | }; 19 | -------------------------------------------------------------------------------- /client/actions/get-lists.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | import { db } from '@/lib/db'; 3 | import { auth } from '@/lib/auth'; 4 | import { LOGIN_PATH } from '@/routes'; 5 | 6 | export const getLists = async () => { 7 | const session = await auth(); 8 | 9 | if (!session || !session.user) { 10 | redirect(LOGIN_PATH); 11 | } 12 | 13 | const lists = await db.list.findMany({ 14 | where: { userId: session.user?.id }, 15 | }); 16 | 17 | return lists; 18 | }; 19 | -------------------------------------------------------------------------------- /client/actions/get-tasks.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | import { startOfDay, addDays, parseISO } from 'date-fns'; 3 | import { db } from '@/lib/db'; 4 | import { auth } from '@/lib/auth'; 5 | import { LOGIN_PATH } from '@/routes'; 6 | import { SearchParamsOptions } from '@/lib/util/filter'; 7 | 8 | export const getTasks = async (options?: SearchParamsOptions) => { 9 | const session = await auth(); 10 | 11 | if (!session || !session.user) { 12 | redirect(LOGIN_PATH); 13 | } 14 | 15 | const today = startOfDay(new Date()); 16 | const tomorrow = startOfDay(addDays(new Date(), 1)); 17 | 18 | let dueDateRange; 19 | 20 | if (options?.today) { 21 | dueDateRange = { 22 | gte: today.toISOString(), 23 | lt: tomorrow.toISOString(), 24 | }; 25 | } else if (options?.dueDate) { 26 | const customDate = startOfDay(parseISO(options.dueDate)); 27 | dueDateRange = { 28 | gte: customDate.toISOString(), 29 | lt: startOfDay(addDays(customDate, 1)).toISOString(), 30 | }; 31 | } 32 | 33 | const tasks = await db.task.findMany({ 34 | where: { 35 | userId: session.user?.id, 36 | listId: options?.listId ?? null, 37 | dueDate: dueDateRange, 38 | }, 39 | include: { 40 | subtasks: true, 41 | labels: true, 42 | }, 43 | }); 44 | 45 | return tasks; 46 | }; 47 | -------------------------------------------------------------------------------- /client/actions/get-user.ts: -------------------------------------------------------------------------------- 1 | import { db } from '@/lib/db'; 2 | 3 | export const getUserByEmail = async (email: string) => { 4 | try { 5 | const user = await db.user.findUnique({ where: { email } }); 6 | 7 | return user; 8 | } catch { 9 | return null; 10 | } 11 | }; 12 | 13 | export const getUserById = async (id: string) => { 14 | try { 15 | const user = await db.user.findUnique({ where: { id } }); 16 | 17 | return user; 18 | } catch { 19 | return null; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /client/app/(platform)/_components/left-sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import SideNav from './side-nav'; 5 | 6 | import { cn } from '@/lib/util/tw-merge'; 7 | import { useLayoutStore } from '@/store/layout-store'; 8 | 9 | export default function LeftSidebar() { 10 | const { showLeftSidebar, toggleLeftSidebar } = useLayoutStore(); 11 | 12 | React.useEffect(() => { 13 | const down = (e: KeyboardEvent) => { 14 | if (e.key === 's' && (e.metaKey || e.ctrlKey)) { 15 | e.preventDefault(); 16 | toggleLeftSidebar(); 17 | } 18 | }; 19 | 20 | document.addEventListener('keydown', down); 21 | return () => document.removeEventListener('keydown', down); 22 | }, [toggleLeftSidebar]); 23 | 24 | return ( 25 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /client/app/(platform)/_components/mobile-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Icons } from '@/components/ui/icons'; 2 | import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; 3 | import SideNav from './side-nav'; 4 | import { Button } from '@/components/ui/button'; 5 | import { useMounted } from '@/hooks/use-mounted'; 6 | 7 | export function MobileSidebar() { 8 | const isMounted = useMounted(); 9 | 10 | if (!isMounted) return null; 11 | 12 | return ( 13 | 14 | 15 | 19 | 20 | 24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /client/app/(platform)/_components/navbar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { Icons } from '@/components/ui/icons'; 6 | import { Button } from '@/components/ui/button'; 7 | import { MobileSidebar } from './mobile-sidebar'; 8 | 9 | import FilterOverlay from '@/components/modals/filter-overlay'; 10 | import FilterView from '@/components/filter-view'; 11 | 12 | import { useLayoutStore } from '@/store/layout-store'; 13 | import { cn } from '@/lib/util/tw-merge'; 14 | 15 | export default function Navbar() { 16 | const { showLeftSidebar, toggleLeftSidebar } = useLayoutStore(); 17 | 18 | return ( 19 |
20 |
21 |
22 |
23 | 24 |
25 |
26 | 38 |
39 |
40 |
41 | 42 | 43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /client/app/(platform)/_components/prompt-menu.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import 'regenerator-runtime/runtime'; 4 | import * as React from 'react'; 5 | 6 | import { cn } from '@/lib/util/tw-merge'; 7 | import { Button } from '@/components/ui/button'; 8 | import { useLayoutStore } from '@/store/layout-store'; 9 | import { Dialog, DialogContent } from '@/components/ui/dialog'; 10 | import { Icons } from '@/components/ui/icons'; 11 | import PromptForm from '@/components/prompt-form'; 12 | 13 | export default function PromptMenu({ ...props }) { 14 | const [open, setOpen] = React.useState(false); 15 | const { showLeftSidebar } = useLayoutStore(); 16 | 17 | return ( 18 | <> 19 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /client/app/(platform)/_components/right-sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { Button } from '@/components/ui/button'; 5 | 6 | import { cn } from '@/lib/util/tw-merge'; 7 | import { useTaskStore } from '@/store/modal-store'; 8 | import LabelBadge from '@/components/ui/label-badge'; 9 | import { Separator } from '@/components/ui/seperator'; 10 | import SubtaskList from '@/components/subtask-list'; 11 | import StatusCheckbox from '@/components/status-checkbox'; 12 | 13 | export default function RightSidebar() { 14 | const { task } = useTaskStore(); 15 | 16 | return ( 17 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /client/app/(platform)/_components/toggle-theme.tsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from 'next-themes'; 2 | import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; 3 | import { Button } from '@/components/ui/button'; 4 | import { Icons } from '@/components/ui/icons'; 5 | import { useLayoutStore } from '@/store/layout-store'; 6 | 7 | export function ToggleTheme() { 8 | const { showLeftSidebar } = useLayoutStore(); 9 | const { setTheme, resolvedTheme } = useTheme(); 10 | 11 | const toggleTheme = () => { 12 | const newTheme = resolvedTheme === 'light' ? 'dark' : 'light'; 13 | setTheme(newTheme); 14 | }; 15 | 16 | const renderButton = () => ( 17 | 25 | ); 26 | 27 | const renderTabs = () => ( 28 | 29 | 30 | setTheme('light')} 33 | className="w-full" 34 | > 35 | Light 36 | 37 | setTheme('dark')} 40 | className="w-full" 41 | > 42 | Dark 43 | 44 | 45 | 46 | ); 47 | 48 | return showLeftSidebar ? renderTabs() : renderButton(); 49 | } 50 | -------------------------------------------------------------------------------- /client/app/(platform)/inbox/_components/board-column.tsx: -------------------------------------------------------------------------------- 1 | import type { Label, List, Task } from '@/types'; 2 | import { cn } from '@/lib/util/tw-merge'; 3 | import TaskItem from '@/components/task-item'; 4 | 5 | interface ColumnProps { 6 | tasks: Task[]; 7 | lists: List[]; 8 | labels: Label[]; 9 | status: 'Incomplete' | 'Pending' | 'Completed'; 10 | color: string; 11 | } 12 | 13 | export default function BoardColumn({ 14 | tasks, 15 | status, 16 | color, 17 | lists, 18 | labels, 19 | }: ColumnProps) { 20 | return ( 21 |
22 |
23 |
24 | 30 |
31 |

{status}

32 |

33 | ({tasks.length}) 34 |

35 |
36 |
37 |
38 |
39 | {tasks.map((task) => ( 40 | 47 | ))} 48 | {status === 'Incomplete' && ( 49 | 50 | )} 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /client/app/(platform)/inbox/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingListPage } from '@/components/ui/loading' 2 | 3 | export default function Loading() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /client/app/(platform)/inbox/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { ExtendedSearchParamsOptions } from '@/lib/util/filter'; 4 | 5 | import PageWithViews from '@/components/page-with-views'; 6 | 7 | interface PageProps { 8 | searchParams: Partial; 9 | } 10 | 11 | export default function Inbox({ searchParams }: PageProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /client/app/(platform)/labels/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingListPage } from '@/components/ui/loading' 2 | 3 | export default function Loading() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /client/app/(platform)/labels/page.tsx: -------------------------------------------------------------------------------- 1 | import { PageList, PageHeading, LabelList } from '@/components/ui/page'; 2 | import { getLabels } from '@/actions/get-labels'; 3 | 4 | export default async function Labels() { 5 | const labels = await getLabels(); 6 | return ( 7 | 8 | Labels 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /client/app/(platform)/layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { SessionProvider } from 'next-auth/react'; 4 | import { auth } from '@/lib/auth'; 5 | 6 | import LeftSidebar from './_components/left-sidebar'; 7 | import Navbar from './_components/navbar'; 8 | 9 | import LoadingScreen from '@/components/ui/loading'; 10 | import OverlayProvider from '@/components/providers/overlay-provider'; 11 | 12 | interface PageProps { 13 | children: React.ReactNode; 14 | } 15 | 16 | export default async function Layout(props: PageProps) { 17 | const session = await auth(); 18 | 19 | return ( 20 | }> 21 | 22 | 23 |
24 | 25 |
26 | 27 |
28 | {props.children} 29 |
30 |
31 |
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /client/app/(platform)/lists/[listId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingListPage } from '@/components/ui/loading' 2 | 3 | export default function Loading() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /client/app/(platform)/lists/[listId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExtendedSearchParamsOptions } from '@/lib/util/filter'; 2 | 3 | import PageWithViews from '@/components/page-with-views'; 4 | import { db } from '@/lib/db'; 5 | 6 | interface PageProps { 7 | params: { listId: string }; 8 | searchParams: Partial; 9 | } 10 | 11 | export default async function List({ params, searchParams }: PageProps) { 12 | const list = await db.list.findUnique({ 13 | where: { id: params.listId }, 14 | }); 15 | 16 | if (!list) { 17 | return null; 18 | } 19 | 20 | return ( 21 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /client/app/(platform)/today/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingListPage } from '@/components/ui/loading' 2 | 3 | export default function Loading() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /client/app/(platform)/today/page.tsx: -------------------------------------------------------------------------------- 1 | import { ExtendedSearchParamsOptions } from '@/lib/util/filter'; 2 | 3 | import PageWithViews from '@/components/page-with-views'; 4 | 5 | interface PageProps { 6 | searchParams: Partial; 7 | } 8 | 9 | export default async function Today({ searchParams }: PageProps) { 10 | return ( 11 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /client/app/(platform)/upcoming/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingBoardPage } from '@/components/ui/loading' 2 | 3 | export default function Loading() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /client/app/(website)/_components/auth-nav.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { Icons } from '@/components/ui/icons'; 4 | import { buttonVariants } from '@/components/ui/button'; 5 | import { cn } from '@/lib/util/tw-merge'; 6 | 7 | export default function AuthNavigation({ 8 | isAuthenticated, 9 | }: { 10 | isAuthenticated: boolean; 11 | }) { 12 | return ( 13 |
14 | {isAuthenticated ? ( 15 | 22 | App 23 | 24 | 25 | ) : ( 26 | <> 27 | 34 | Login 35 | 36 | 43 | Sign Up 44 | 45 | 46 | )} 47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /client/app/(website)/_components/fade-on-view.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { useEffect, useState } from 'react'; 5 | import { useInView } from 'react-intersection-observer'; 6 | import { cn } from '@/lib/util/tw-merge'; 7 | 8 | interface FadeOnViewProps extends React.HTMLAttributes { 9 | children: React.ReactNode; 10 | delay?: number; 11 | offset?: number; 12 | className?: string; 13 | } 14 | 15 | export function FadeOnView({ 16 | children, 17 | delay, 18 | offset, 19 | className, 20 | ...props 21 | }: FadeOnViewProps) { 22 | const [viewed, setViewed] = useState(false); 23 | 24 | const { ref, inView } = useInView({ 25 | rootMargin: `0% 0% ${-15 + (offset ?? 0)}% 0%`, 26 | initialInView: false, 27 | }); 28 | 29 | useEffect(() => { 30 | if (inView) setViewed(true); 31 | }, [inView]); 32 | 33 | return ( 34 |
52 | {children} 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /client/app/(website)/_components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { Icons } from '@/components/ui/icons'; 2 | 3 | const footer = () => ( 4 |
5 |
6 |
taskify
7 |
8 | 9 |
10 |
11 |
12 |
13 |
14 |
Privacy Policy
15 |
·
16 |
Terms of Conditions
17 |
18 |
19 | © 2024 Taskify. All rights reserved. 20 |
21 |
22 |
23 | ); 24 | 25 | export default footer; 26 | -------------------------------------------------------------------------------- /client/app/(website)/_components/mobile-nav.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import Link from 'next/link'; 5 | import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; 6 | 7 | import { Icons } from '@/components/ui/icons'; 8 | import { Button } from '@/components/ui/button'; 9 | import AuthNavigation from './auth-nav'; 10 | import NavbarLink from './navbar-link'; 11 | 12 | import { useMounted } from '@/hooks/use-mounted'; 13 | import { config } from '@/lib/config'; 14 | import { cn } from '@/lib/util/tw-merge'; 15 | import { Separator } from '@/components/ui/seperator'; 16 | 17 | export default function MobileNav({ 18 | isAuthenticated, 19 | }: { 20 | isAuthenticated: boolean; 21 | }) { 22 | const [isOpen, setIsOpen] = React.useState(false); 23 | const isMounted = useMounted(); 24 | 25 | if (!isMounted) return null; 26 | 27 | return ( 28 | 29 | 30 | 38 | 39 | 46 |
47 | {config.marketing.links.map((link) => ( 48 | setIsOpen(false)} 51 | aria-current="page" 52 | className={cn( 53 | 'text-muted-foreground hover:text-foreground font-medium', 54 | )} 55 | > 56 | {link.title} 57 | 58 | ))} 59 |
60 | 61 | 62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /client/app/(website)/_components/navbar-link.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | 5 | import { usePathname } from 'next/navigation'; 6 | import { cn } from '@/lib/util/tw-merge'; 7 | 8 | interface NavbarLinkProps { 9 | link: { 10 | title: string; 11 | href: string; 12 | }; 13 | onClose?: () => void; 14 | } 15 | 16 | export default function NavbarLink({ link, onClose }: NavbarLinkProps) { 17 | const path = usePathname(); 18 | const isActive = 19 | path === link.href || 20 | (link.href === '/docs/getting-started' && path.includes('docs')); 21 | 22 | return ( 23 | // eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role 24 |
  • 25 | 33 | {link.title} 34 | 35 |
  • 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /client/app/(website)/_components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import NavbarLink from './navbar-link'; 4 | import MobileNav from './mobile-nav'; 5 | import AuthNavigation from './auth-nav'; 6 | import ThemeSwitcher from './theme-switcher'; 7 | 8 | import { config } from '@/lib/config'; 9 | import { auth } from '@/lib/auth'; 10 | 11 | export default async function Navbar() { 12 | const session = await auth(); 13 | 14 | return ( 15 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /client/app/(website)/_components/prompt-form-preview.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import PromptForm from '@/components/prompt-form'; 4 | import { useMounted } from '@/hooks/use-mounted'; 5 | 6 | export default function PromptFormPreview() { 7 | const isMounted = useMounted(); 8 | if (isMounted) return ; 9 | } 10 | -------------------------------------------------------------------------------- /client/app/(website)/_components/task-form-preview.tsx: -------------------------------------------------------------------------------- 1 | import TaskForm from '@/components/task-form'; 2 | 3 | const lists = [ 4 | { 5 | id: '1', 6 | userId: 'user1', 7 | name: 'Personal', 8 | order: 1, 9 | tasks: [ 10 | { id: 'task1', description: 'Buy groceries', completed: false }, 11 | { id: 'task2', description: 'Exercise', completed: true }, 12 | ], 13 | }, 14 | { 15 | id: '2', 16 | userId: 'user1', 17 | name: 'Work', 18 | order: 2, 19 | tasks: [ 20 | { id: 'task3', description: 'Prepare presentation', completed: false }, 21 | { id: 'task4', description: 'Reply to emails', completed: true }, 22 | ], 23 | }, 24 | ]; 25 | 26 | const labels = [ 27 | { 28 | id: 'label1', 29 | userId: 'user1', 30 | name: 'Urgent', 31 | color: 'red', 32 | tasks: [ 33 | { id: 'task1', description: 'Buy groceries', completed: false }, 34 | { id: 'task3', description: 'Prepare presentation', completed: false }, 35 | ], 36 | }, 37 | { 38 | id: 'label2', 39 | userId: 'user1', 40 | name: 'Personal', 41 | color: 'blue', 42 | tasks: [ 43 | { id: 'task2', description: 'Exercise', completed: true }, 44 | { id: 'task4', description: 'Reply to emails', completed: true }, 45 | ], 46 | }, 47 | ]; 48 | 49 | export default function TaskFormPreview() { 50 | return ( 51 |
    52 | 53 |
    54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /client/app/(website)/_components/theme-switcher.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { Moon, Sun } from 'lucide-react'; 5 | import { useTheme } from 'next-themes'; 6 | 7 | import { Button } from '@/components/ui/button'; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuTrigger, 13 | } from '@/components/ui/dropdown-menu'; 14 | 15 | export default function ThemeSwitcher() { 16 | const { setTheme } = useTheme(); 17 | 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | setTheme('light')}> 29 | Light 30 | 31 | setTheme('dark')}> 32 | Dark 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /client/app/(website)/_components/typewriter-effect.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Typewriter from 'typewriter-effect'; 4 | 5 | export default function TypewriterEffect() { 6 | const words = ['Natural Language Processing']; 7 | 8 | return ( 9 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /client/app/(website)/_sections/call-to-action.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@/components/ui/button'; 2 | 3 | export default function CallToAction() { 4 | return ( 5 |
    6 |
    7 |
    8 |
    9 |

    Convinced yet?

    10 |

    11 | Lorem ipsum dolor sit amet consectetur, adipisicing elit. Itaque, 12 | dolore omnis. Sit aliquam nobis excepturi! 13 |

    14 |
    15 |
    16 | 19 | 25 | 31 | 38 |
    39 |
    40 |
    41 |
    42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /client/app/(website)/_sections/hero.tsx: -------------------------------------------------------------------------------- 1 | import { SparklesIcon } from 'lucide-react'; 2 | import Image from 'next/image'; 3 | 4 | import { Button } from '@/components/ui/button'; 5 | import { Badge } from '@/components/ui/badge'; 6 | import { cn } from '@/lib/util/tw-merge'; 7 | import { FadeOnView } from '../_components/fade-on-view'; 8 | import Blur1 from '@/public/static/bg-blur-1.webp'; 9 | 10 | export default function Hero() { 11 | return ( 12 |
    13 |
    14 | 15 | 16 | 17 | Under construction 18 | 19 | 20 | 21 |

    22 | Increase your productivity 23 |

    24 |
    25 | 26 |

    27 | Lorem ipsum dolor sit amet consectetur adipisicing elit. 28 |

    29 |
    30 |
    31 | 32 | 33 | 34 | 35 | 38 | 39 |
    40 |
    41 | 48 |
    54 |
    55 | App image 62 | background blur 67 |
    68 |
    69 |
    70 |
    71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /client/app/(website)/docs/[...slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import Content from '../_content/test.mdx'; 2 | import TableOfContents from '../_components/table-of-contents'; 3 | import Breadcrumbs from '../_components/breadcrumbs'; 4 | 5 | export default function Page() { 6 | return ( 7 |
    8 |
    9 | 10 |
    11 | 12 |
    13 |
    14 | 15 |
    16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /client/app/(website)/docs/_components/breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { usePathname } from 'next/navigation'; 4 | import { Icons } from '@/components/ui/icons'; 5 | 6 | export default function Breadcrumbs() { 7 | const pathname = usePathname(); 8 | 9 | const segments = pathname.split('/').filter((segment) => segment !== ''); 10 | 11 | const formatSegment = (segment: string): string => segment.replace(/-/g, ' '); 12 | 13 | return ( 14 |
      15 | {segments.map((segment, index) => ( 16 |
    • 17 | {formatSegment(segment)} 18 | {index !== segments.length - 1 && ( 19 | 20 | )} 21 |
    • 22 | ))} 23 |
    24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /client/app/(website)/docs/_components/sidebar-link.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | 5 | import { usePathname } from 'next/navigation'; 6 | import { cn } from '@/lib/util/tw-merge'; 7 | 8 | interface SidebarLinkProps { 9 | link: { 10 | text: string; 11 | href: string; 12 | }; 13 | } 14 | 15 | export default function SidebarLink({ link }: SidebarLinkProps) { 16 | const path = usePathname(); 17 | const isActive = path === link.href; 18 | 19 | return ( 20 | 27 | {link.text} 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /client/app/(website)/docs/_components/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import _uniqueId from 'lodash/uniqueId'; 2 | import SidebarLink from './sidebar-link'; 3 | 4 | import { config } from '@/lib/config'; 5 | 6 | import { 7 | Accordion, 8 | AccordionContent, 9 | AccordionItem, 10 | AccordionTrigger, 11 | } from '@/components/ui/accordion'; 12 | 13 | const overviewLinks = [ 14 | { text: 'Getting Started', href: '/docs/getting-started' }, 15 | { text: 'Account Setup', href: '#' }, 16 | { text: 'Features', href: '/docs/features' }, 17 | ]; 18 | export default function Sidebar() { 19 | return ( 20 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /client/app/(website)/docs/_components/table-of-contents.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import Link from 'next/link'; 5 | import { kebabCase } from 'lodash'; 6 | import { cn } from '@/lib/util/tw-merge'; 7 | 8 | export default function TableOfContents() { 9 | const [headings, setHeadings] = React.useState([]); 10 | const [activeHeading, setActiveHeading] = React.useState(); 11 | 12 | React.useEffect(() => { 13 | const h3Elements = document.querySelectorAll('h3'); 14 | const h3TextArray = Array.from(h3Elements).map( 15 | (element) => element.textContent || '', 16 | ); 17 | 18 | setHeadings(h3TextArray); 19 | 20 | const options = { 21 | root: null, 22 | rootMargin: '0px', 23 | threshold: 0.2, 24 | }; 25 | 26 | const observer = new IntersectionObserver((entries) => { 27 | entries.forEach((entry) => { 28 | if (entry.isIntersecting) { 29 | const headingTarget = entry.target.textContent; 30 | setActiveHeading(headingTarget); 31 | } 32 | }); 33 | }, options); 34 | 35 | h3Elements.forEach((h3) => { 36 | observer.observe(h3); 37 | }); 38 | 39 | return () => { 40 | h3Elements.forEach((h3) => { 41 | observer.unobserve(h3); 42 | }); 43 | }; 44 | }, []); 45 | 46 | return ( 47 |
    48 |
    On this page
    49 |
      50 | {headings.map((heading) => ( 51 |
    • 52 | 59 | {heading} 60 | 61 |
    • 62 | ))} 63 |
    64 |
    65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /client/app/(website)/docs/_content/test.mdx: -------------------------------------------------------------------------------- 1 | # Using MDX 2 | 3 | **Author:** ChatGPT 4 | 5 | This documentation site is primarily designed for showcasing purposes and will be updated once all features are fully completed. All features seen in the navigation will be implemented. 6 | 7 | Here's what I used to build it: Next.js's integrated [MDX](https://nextjs.org/docs/pages/building-your-application/configuring/mdx), styled with [Tailwind Typography](https://tailwindcss.com/docs/typography-plugin#basic-usage). 8 | 9 | ### What is MDX? 10 | 11 | > MDX seamlessly bridges the elegance of Markdown simplicity with the dynamic expressiveness of React components, empowering content creators to craft rich, interactive narratives effortlessly. 12 | 13 | ### MDX Advantages 14 | 15 | - **Rich Content Integration:** 16 | Combine Markdown simplicity with React components. 17 | 18 | - **Developer-Friendly Syntax:** 19 | Markdown-like syntax with embedded JSX for a smooth developer experience. 20 | 21 | - **Flexible and Extensible:** 22 | Accommodate simple Markdown and complex React components. 23 | 24 | ### How to Use MDX in Next.js? 25 | 26 | To use MDX in Next.js, you need to set up the necessary packages and configurations. Follow the documentation to integrate MDX seamlessly into your project. 27 | 28 | ```jsx 29 | // Example MDX component 30 | export const MyMDXComponent = () => { 31 | return
    This is a custom MDX component!
    ; 32 | }; 33 | ``` 34 | -------------------------------------------------------------------------------- /client/app/(website)/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Sidebar from './_components/sidebar'; 4 | 5 | interface DocsLayoutProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | export default function DocsLayout({ children }: DocsLayoutProps) { 10 | return ( 11 |
    12 | 13 | {children} 14 |
    15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /client/app/(website)/layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Navbar from './_components/navbar'; 4 | import Footer from './_components/footer'; 5 | 6 | interface PageProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | export default function Layout({ children }: PageProps) { 11 | return ( 12 |
    13 | 14 |
    15 |
    16 |
    17 |
    18 | {children} 19 |
    20 |
    21 |
    22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /client/app/(website)/page.tsx: -------------------------------------------------------------------------------- 1 | import FeaturesSection from './_sections/features'; 2 | import AdditionalFeaturesSection from './_sections/additional-features'; 3 | import PricingSection from './_sections/pricing'; 4 | import HeroSection from './_sections/hero'; 5 | import CallToActionSection from './_sections/call-to-action'; 6 | 7 | export default async function Home() { 8 | return ( 9 |
    10 | 11 | 12 |
    13 | 14 |
    15 |
    16 |
    17 |
    18 |
    19 | 20 | 21 |
    22 |
    23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /client/app/api/auth/[...nextAuth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from '@/lib/auth'; 2 | -------------------------------------------------------------------------------- /client/app/api/auth/login/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { AuthError } from 'next-auth'; 3 | import { signIn } from '@/lib/auth'; 4 | import { DEFAULT_LOGIN_REDIRECT } from '@/routes'; 5 | 6 | export const POST = async (req: Request) => { 7 | const { email, password } = await req.json(); 8 | 9 | if (!email || !password) { 10 | return new NextResponse('Missing Fields', { status: 400 }); 11 | } 12 | 13 | try { 14 | await signIn('credentials', { 15 | email, 16 | password, 17 | }); 18 | 19 | return new Response('Redirect', { 20 | headers: { Location: DEFAULT_LOGIN_REDIRECT }, 21 | }); 22 | } catch (error) { 23 | if (error instanceof AuthError) { 24 | switch (error.type) { 25 | case 'CredentialsSignin': 26 | return new NextResponse('Invalid Credentials', { status: 401 }); 27 | default: 28 | return new NextResponse('An unknown error occured.', { status: 500 }); 29 | } 30 | } 31 | throw error; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /client/app/api/auth/register/route.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | 3 | import { NextResponse } from 'next/server'; 4 | import { db } from '@/lib/db'; 5 | import { getUserByEmail } from '@/actions/get-user'; 6 | 7 | export const POST = async (req: Request) => { 8 | const { name, email, password } = await req.json(); 9 | 10 | if (!name || !email || !password) { 11 | return new NextResponse('Missing Fields', { status: 400 }); 12 | } 13 | 14 | const existingEmail = await getUserByEmail(email); 15 | 16 | if (existingEmail) { 17 | return new NextResponse('Email exists', { status: 400 }); 18 | } 19 | 20 | const hashedPassword = await bcrypt.hash(password, 10); 21 | 22 | const user = await db.user.create({ 23 | data: { 24 | name, 25 | email, 26 | password: hashedPassword, 27 | }, 28 | }); 29 | 30 | return NextResponse.json(user); 31 | }; 32 | -------------------------------------------------------------------------------- /client/app/api/labels/[labelId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { db } from '@/lib/db'; 3 | import { auth } from '@/lib/auth'; 4 | 5 | export async function PATCH( 6 | req: Request, 7 | { params }: { params: { labelId: string } }, 8 | ) { 9 | try { 10 | if (!params.labelId) { 11 | return new NextResponse('Label Id is required', { status: 400 }); 12 | } 13 | 14 | const session = await auth(); 15 | 16 | if (!session || !session.user) { 17 | return new NextResponse('Unauthenticated', { status: 403 }); 18 | } 19 | 20 | // TODO: Authorize user 21 | 22 | const { name, color } = await req.json(); 23 | 24 | const label = await db.label.update({ 25 | where: { 26 | id: params.labelId, 27 | }, 28 | data: { 29 | name, 30 | color, 31 | }, 32 | }); 33 | 34 | return NextResponse.json(label, { status: 200 }); 35 | } catch (error) { 36 | return new NextResponse('Internal error', { status: 500 }); 37 | } 38 | } 39 | 40 | export async function DELETE( 41 | req: Request, 42 | { params }: { params: { labelId: string } }, 43 | ) { 44 | try { 45 | if (!params.labelId) { 46 | return new NextResponse('Label Id is required', { status: 400 }); 47 | } 48 | 49 | const session = await auth(); 50 | 51 | if (!session || !session.user) { 52 | return new NextResponse('Unauthenticated', { status: 403 }); 53 | } 54 | 55 | const label = await db.label.delete({ 56 | where: { 57 | id: params.labelId, 58 | }, 59 | }); 60 | 61 | return NextResponse.json(label, { status: 200 }); 62 | } catch (error) { 63 | return new NextResponse('Internal error', { status: 500 }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /client/app/api/labels/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { db } from '@/lib/db'; 3 | import { auth } from '@/lib/auth'; 4 | 5 | export async function GET() { 6 | const session = await auth(); 7 | 8 | if (!session || !session.user) { 9 | return new NextResponse('Unauthorized', { status: 401 }); 10 | } 11 | 12 | const labels = await db.label.findMany({ 13 | where: { userId: session.user.id }, 14 | }); 15 | 16 | return NextResponse.json(labels, { status: 200 }); 17 | } 18 | 19 | export async function POST(req: Request) { 20 | try { 21 | const session = await auth(); 22 | 23 | if (!session || !session.user) { 24 | return new NextResponse('Unauthorized', { status: 401 }); 25 | } 26 | 27 | const { name, color } = await req.json(); 28 | 29 | if (!name) { 30 | return new NextResponse('Bad Request', { status: 401 }); 31 | } 32 | 33 | const label = await db.label.create({ 34 | data: { 35 | userId: session.user.id, 36 | name, 37 | color, 38 | }, 39 | }); 40 | 41 | return NextResponse.json(label, { status: 200 }); 42 | } catch (error) { 43 | return new NextResponse('Internal server error', { status: 500 }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /client/app/api/lists/[listId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { db } from '@/lib/db'; 3 | import { auth } from '@/lib/auth'; 4 | 5 | export async function PATCH( 6 | req: Request, 7 | { params }: { params: { listId: string } }, 8 | ) { 9 | try { 10 | if (!params.listId) { 11 | return new NextResponse('List Id is required', { status: 400 }); 12 | } 13 | 14 | const session = await auth(); 15 | 16 | if (!session || !session.user) { 17 | return new NextResponse('Unauthenticated', { status: 403 }); 18 | } 19 | 20 | // TODO: Authorize user 21 | 22 | const { name } = await req.json(); 23 | 24 | const list = await db.list.update({ 25 | where: { 26 | id: params.listId, 27 | }, 28 | data: { 29 | name, 30 | }, 31 | }); 32 | 33 | return NextResponse.json(list, { status: 200 }); 34 | } catch (error) { 35 | return new NextResponse('Internal error', { status: 500 }); 36 | } 37 | } 38 | 39 | export async function DELETE( 40 | req: Request, 41 | { params }: { params: { listId: string } }, 42 | ) { 43 | try { 44 | if (!params.listId) { 45 | return new NextResponse('List Id is required', { status: 400 }); 46 | } 47 | 48 | const session = await auth(); 49 | 50 | if (!session || !session.user) { 51 | return new NextResponse('Unauthenticated', { status: 403 }); 52 | } 53 | 54 | const list = await db.list.delete({ 55 | where: { 56 | id: params.listId, 57 | }, 58 | }); 59 | 60 | return NextResponse.json(list, { status: 200 }); 61 | } catch (error) { 62 | return new NextResponse('Internal error', { status: 500 }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client/app/api/lists/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { db } from '@/lib/db'; 3 | import { auth } from '@/lib/auth'; 4 | 5 | export async function GET() { 6 | const session = await auth(); 7 | 8 | if (!session || !session.user) { 9 | return new NextResponse('Unauthenticated', { status: 403 }); 10 | } 11 | 12 | try { 13 | const lists = await db.list.findMany({ 14 | where: { userId: session.user.id }, 15 | }); 16 | 17 | return NextResponse.json(lists); 18 | } catch (error) { 19 | return new NextResponse('Internal server error', { status: 500 }); 20 | } 21 | } 22 | 23 | export async function POST(req: Request) { 24 | try { 25 | const session = await auth(); 26 | 27 | if (!session || !session.user) { 28 | return new NextResponse('Unauthorized', { status: 401 }); 29 | } 30 | 31 | const { name } = await req.json(); 32 | 33 | if (!name) { 34 | return new NextResponse('Bad Request', { status: 401 }); 35 | } 36 | 37 | const lastList = await db.list.findFirst({ 38 | where: { userId: session.user.id }, 39 | orderBy: { order: 'desc' }, 40 | select: { order: true }, 41 | }); 42 | 43 | const order = lastList ? lastList.order : 1; 44 | const list = await db.list.create({ 45 | data: { 46 | userId: session.user.id, 47 | name, 48 | order, 49 | }, 50 | }); 51 | 52 | return NextResponse.json(list, { status: 200 }); 53 | } catch (error) { 54 | return new NextResponse('Internal server error', { status: 500 }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/app/api/tasks/[taskId]/labels/[labelId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { db } from '@/lib/db'; 3 | import { auth } from '@/lib/auth'; 4 | 5 | export async function POST( 6 | req: Request, 7 | { params }: { params: { taskId: string; labelId: string } }, 8 | ) { 9 | if (!params.taskId || !params.labelId) { 10 | return new NextResponse('Task Id and Label Id is required', { 11 | status: 400, 12 | }); 13 | } 14 | 15 | const session = await auth(); 16 | 17 | if (!session || !session.user) { 18 | return new NextResponse('Unauthenticated', { status: 403 }); 19 | } 20 | 21 | try { 22 | // Check if the task and label exist 23 | const existingTask = await db.task.findUnique({ 24 | where: { id: params.taskId }, 25 | }); 26 | 27 | const existingLabel = await db.label.findUnique({ 28 | where: { id: params.labelId }, 29 | }); 30 | 31 | if (!existingTask || !existingLabel) { 32 | return new NextResponse('Task or Label not found', { status: 404 }); 33 | } 34 | 35 | // Create a new label and associate it with the task 36 | const updatedTask = await db.task.update({ 37 | where: { id: params.taskId }, 38 | data: { 39 | labels: { 40 | connect: { id: params.labelId }, 41 | }, 42 | }, 43 | }); 44 | 45 | return NextResponse.json(updatedTask, { status: 200 }); 46 | } catch (error) { 47 | return new NextResponse('Internal Server Error', { status: 500 }); 48 | } 49 | } 50 | 51 | export async function DELETE( 52 | req: Request, 53 | { params }: { params: { taskId: string; labelId: string } }, 54 | ) { 55 | try { 56 | if (!params.taskId || !params.labelId) { 57 | return new NextResponse('Task Id and Label Id is required', { 58 | status: 400, 59 | }); 60 | } 61 | 62 | const session = await auth(); 63 | 64 | if (!session || !session.user) { 65 | return new NextResponse('Unauthenticated', { status: 403 }); 66 | } 67 | 68 | const existingLabel = await db.label.findUnique({ 69 | where: { id: params.labelId }, 70 | }); 71 | 72 | if (!existingLabel) { 73 | return new NextResponse('Label not found', { status: 404 }); 74 | } 75 | 76 | // Disconnect the label from the task 77 | const updatedTask = await db.task.update({ 78 | where: { id: params.taskId }, 79 | data: { 80 | labels: { 81 | disconnect: { id: params.labelId }, 82 | }, 83 | }, 84 | }); 85 | 86 | return NextResponse.json(updatedTask, { status: 200 }); 87 | } catch (error) { 88 | return new NextResponse('Internal error', { status: 500 }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /client/app/api/tasks/[taskId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { db } from '@/lib/db'; 3 | import { auth } from '@/lib/auth'; 4 | 5 | export async function PATCH( 6 | req: Request, 7 | { params }: { params: { taskId: string } }, 8 | ) { 9 | try { 10 | if (!params.taskId) { 11 | return new NextResponse('Task Id is required', { status: 400 }); 12 | } 13 | 14 | const session = await auth(); 15 | 16 | if (!session || !session.user) { 17 | return new NextResponse('Unauthenticated', { status: 403 }); 18 | } 19 | 20 | // TODO: Authorize user 21 | 22 | const { name, listId, description, dueDate, priority, isComplete } = 23 | await req.json(); 24 | 25 | const task = await db.task.update({ 26 | where: { 27 | id: params.taskId, 28 | }, 29 | data: { 30 | userId: session.user.id, 31 | name, 32 | listId, 33 | description, 34 | isComplete, 35 | dueDate, 36 | priority, 37 | }, 38 | }); 39 | 40 | return NextResponse.json(task, { status: 200 }); 41 | } catch (error) { 42 | return new NextResponse('Internal error', { status: 500 }); 43 | } 44 | } 45 | 46 | export async function DELETE( 47 | req: Request, 48 | { params }: { params: { taskId: string } }, 49 | ) { 50 | try { 51 | if (!params.taskId) { 52 | return new NextResponse('Task Id is required', { status: 400 }); 53 | } 54 | 55 | const session = await auth(); 56 | 57 | if (!session || !session.user) { 58 | return new NextResponse('Unauthenticated', { status: 403 }); 59 | } 60 | 61 | const subtasks = await db.subtask.findMany({ 62 | where: { 63 | taskId: params.taskId, 64 | }, 65 | }); 66 | 67 | await Promise.all( 68 | subtasks.map((subtask) => 69 | db.subtask.delete({ 70 | where: { id: subtask.id }, 71 | }), 72 | ), 73 | ); 74 | 75 | const task = await db.task.delete({ 76 | where: { 77 | id: params.taskId, 78 | }, 79 | }); 80 | 81 | return NextResponse.json(task, { status: 200 }); 82 | } catch (error) { 83 | return new NextResponse('Internal error', { status: 500 }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /client/app/api/tasks/[taskId]/subtasks/[subtaskId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { db } from '@/lib/db'; 3 | import { auth } from '@/lib/auth'; 4 | 5 | export async function PATCH( 6 | req: Request, 7 | { params }: { params: { taskId: string; subtaskId: string } }, 8 | ) { 9 | try { 10 | if (!params.taskId || !params.subtaskId) { 11 | return new NextResponse('Task Id and Subtask Id is required', { 12 | status: 400, 13 | }); 14 | } 15 | 16 | const session = await auth(); 17 | 18 | if (!session || !session.user) { 19 | return new NextResponse('Unauthenticated', { status: 403 }); 20 | } 21 | 22 | // TODO: Authorize user 23 | 24 | const { name, isComplete } = await req.json(); 25 | 26 | const subtask = await db.subtask.update({ 27 | where: { 28 | id: params.subtaskId, 29 | taskId: params.taskId, 30 | }, 31 | data: { 32 | name, 33 | isComplete, 34 | }, 35 | }); 36 | 37 | return NextResponse.json(subtask, { status: 200 }); 38 | } catch (error) { 39 | return new NextResponse('Internal error', { status: 500 }); 40 | } 41 | } 42 | 43 | export async function DELETE( 44 | req: Request, 45 | { params }: { params: { taskId: string; subtaskId: string } }, 46 | ) { 47 | try { 48 | if (!params.taskId || !params.subtaskId) { 49 | return new NextResponse('Task Id and Subtask Id is required', { 50 | status: 400, 51 | }); 52 | } 53 | 54 | const session = await auth(); 55 | 56 | if (!session || !session.user) { 57 | return new NextResponse('Unauthenticated', { status: 403 }); 58 | } 59 | 60 | const subtask = await db.subtask.delete({ 61 | where: { 62 | id: params.subtaskId, 63 | taskId: params.taskId, 64 | }, 65 | }); 66 | 67 | return NextResponse.json(subtask, { status: 200 }); 68 | } catch (error) { 69 | return new NextResponse('Internal error', { status: 500 }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /client/app/api/tasks/[taskId]/subtasks/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { db } from '@/lib/db'; 3 | import { auth } from '@/lib/auth'; 4 | 5 | export async function POST( 6 | req: Request, 7 | { params }: { params: { taskId: string } }, 8 | ) { 9 | try { 10 | const session = await auth(); 11 | 12 | if (!session || !session.user) { 13 | return new NextResponse('Unauthenticated', { status: 403 }); 14 | } 15 | 16 | const { name } = await req.json(); 17 | 18 | if (!name) { 19 | return new NextResponse('Bad Request', { status: 401 }); 20 | } 21 | 22 | const lastSubtask = await db.subtask.findFirst({ 23 | where: { userId: session.user.id, taskId: params.taskId }, 24 | orderBy: { order: 'desc' }, 25 | select: { order: true }, 26 | }); 27 | 28 | const order = lastSubtask ? lastSubtask.order : 1; 29 | const list = await db.subtask.create({ 30 | data: { 31 | userId: session.user.id, 32 | taskId: params.taskId, 33 | name, 34 | order, 35 | }, 36 | }); 37 | 38 | return NextResponse.json(list, { status: 200 }); 39 | } catch (error) { 40 | return new NextResponse('Internal server error', { status: 500 }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /client/app/api/tasks/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { db } from '@/lib/db'; 3 | import { auth } from '@/lib/auth'; 4 | 5 | export async function GET() { 6 | try { 7 | const session = await auth(); 8 | 9 | if (!session || !session.user) { 10 | return new NextResponse('Unauthorized', { status: 401 }); 11 | } 12 | 13 | const tasks = await db.task.findMany({ 14 | where: { userId: session.user.id }, 15 | include: { 16 | subtasks: true, 17 | labels: true, 18 | }, 19 | }); 20 | 21 | return NextResponse.json(tasks, { status: 200 }); 22 | } catch (error) { 23 | return new NextResponse('Internal server error', { status: 500 }); 24 | } 25 | } 26 | 27 | export async function POST(req: Request) { 28 | try { 29 | const session = await auth(); 30 | 31 | if (!session || !session.user) { 32 | return new NextResponse('Unauthorized', { status: 401 }); 33 | } 34 | 35 | const { name, listId, description, dueDate, priority } = await req.json(); 36 | 37 | if (!name) { 38 | return new NextResponse('Bad Request', { status: 401 }); 39 | } 40 | 41 | let lastTask; 42 | 43 | if (listId) { 44 | lastTask = await db.task.findFirst({ 45 | where: { listId }, 46 | orderBy: { order: 'desc' }, 47 | select: { order: true }, 48 | }); 49 | } else { 50 | lastTask = await db.task.findFirst({ 51 | where: { listId: null }, 52 | orderBy: { order: 'desc' }, 53 | select: { order: true }, 54 | }); 55 | } 56 | 57 | const newOrder = lastTask ? lastTask.order + 1 : 1; 58 | 59 | const list = await db.task.create({ 60 | data: { 61 | userId: session.user.id, 62 | name, 63 | listId, 64 | description, 65 | dueDate, 66 | priority, 67 | order: newOrder, 68 | }, 69 | }); 70 | 71 | return NextResponse.json(list, { status: 200 }); 72 | } catch (error) { 73 | return new NextResponse('Internal server error', { status: 500 }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /client/app/auth/auth-error.tsx: -------------------------------------------------------------------------------- 1 | import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; 2 | 3 | interface FormErrorProps { 4 | message?: string; 5 | } 6 | 7 | export function AuthError({ message }: FormErrorProps) { 8 | if (!message) return null; 9 | 10 | return ( 11 |
    12 | 13 |

    {message}

    14 |
    15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /client/app/auth/error/page.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const page = () =>
    ERROR!
    ; 4 | 5 | export default page; 6 | -------------------------------------------------------------------------------- /client/app/auth/layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface AuthLayoutProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | export default function AuthLayout({ children }: AuthLayoutProps) { 8 | return
    {children}
    ; 9 | } 10 | -------------------------------------------------------------------------------- /client/app/auth/login/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Link from 'next/link'; 4 | 5 | import { Metadata } from 'next'; 6 | import { cn } from '@/lib/util/tw-merge'; 7 | import { buttonVariants } from '@/components/ui/button'; 8 | import { Icons } from '@/components/ui/icons'; 9 | 10 | import AuthForm from '../auth-form'; 11 | 12 | export const metadata: Metadata = { 13 | title: 'Login', 14 | description: 'Login to your account', 15 | }; 16 | 17 | export default function LoginPage() { 18 | return ( 19 |
    25 |
    26 | 33 | <> 34 | 35 | Back 36 | 37 | 38 |
    39 |
    40 | 41 |

    42 | Login to Taskify 43 |

    44 |

    45 | Ready to tackle your tasks? Just sign in below. 46 |

    47 |
    48 | 49 |

    50 | 54 | Don't have an account? Sign Up 55 | 56 |

    57 |
    {' '} 58 |
    59 |
    60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /client/app/auth/register-user.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import bcrypt from 'bcryptjs'; 4 | 5 | import { redirect } from 'next/navigation'; 6 | import { db } from '@/lib/db'; 7 | import { getUserByEmail } from '@/actions/get-user'; 8 | 9 | interface UserDetails { 10 | name: string; 11 | email: string; 12 | password: string; 13 | } 14 | 15 | export const registerUser = async ({ name, email, password }: UserDetails) => { 16 | const hashedPassword = await bcrypt.hash(password, 10); 17 | 18 | if (!name || !email || !password) { 19 | throw new Error('Missing fields.'); 20 | } 21 | 22 | const existingEmail = await getUserByEmail(email); 23 | 24 | if (existingEmail) { 25 | throw new Error('Email already exists.'); 26 | } 27 | 28 | await db.user.create({ 29 | data: { 30 | name, 31 | email, 32 | password: hashedPassword, 33 | }, 34 | }); 35 | 36 | return redirect('/login'); 37 | }; 38 | -------------------------------------------------------------------------------- /client/app/auth/register/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Link from 'next/link'; 4 | 5 | import { cn } from '@/lib/util/tw-merge'; 6 | import { buttonVariants } from '@/components/ui/button'; 7 | import { Icons } from '@/components/ui/icons'; 8 | 9 | import AuthForm from '../auth-form'; 10 | 11 | export const metadata = { 12 | title: 'Register', 13 | description: 'Create an account to get started.', 14 | }; 15 | 16 | export default function RegisterPage() { 17 | return ( 18 |
    19 | 26 | Login 27 | 28 |
    29 |
    30 |
    31 |
    32 | 33 |

    34 | Create an account 35 |

    36 |

    37 | Enter your email below to create your account 38 |

    39 |
    40 | 41 |

    42 | By clicking continue, you agree to our{' '} 43 | 47 | Terms of Service 48 | {' '} 49 | and 50 | 54 | Privacy Policy 55 | 56 | . 57 |

    58 |
    59 |
    60 |
    61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /client/app/auth/socials-actions.tsx: -------------------------------------------------------------------------------- 1 | import { signIn } from 'next-auth/react'; 2 | import * as React from 'react'; 3 | import { DEFAULT_LOGIN_REDIRECT } from '@/routes'; 4 | 5 | import { Icons } from '@/components/ui/icons'; 6 | import { Button } from '@/components/ui/button'; 7 | import { handleError } from '@/lib/util/error'; 8 | 9 | export default function SocialsActions() { 10 | const [isLoading, setIsLoading] = React.useState(false); 11 | 12 | const loginSocial = (provider: 'google' | 'github') => { 13 | setIsLoading(true); 14 | try { 15 | signIn(provider, { 16 | callbackUrl: DEFAULT_LOGIN_REDIRECT, 17 | }); 18 | } catch (error) { 19 | handleError(error); 20 | } finally { 21 | setIsLoading(false); 22 | } 23 | }; 24 | 25 | return ( 26 |
    27 |
    28 |
    29 |
    Or continue with
    30 |
    31 |
    32 |
    33 | 44 | 55 |
    56 |
    57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /client/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/client/app/favicon.ico -------------------------------------------------------------------------------- /client/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import type { Metadata } from 'next'; 4 | import { inter } from '@/styles/fonts'; 5 | import { config } from '@/lib/config'; 6 | import '@/styles/globals.css'; 7 | 8 | import { ToastProvider } from '@/components/providers/toast-provider'; 9 | import { ThemeProvider } from '@/components/providers/theme-provider'; 10 | 11 | // eslint-disable-next-line prefer-destructuring 12 | export const metadata: Metadata = config.metadata; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | return ( 20 | 21 | 22 | 28 |
    29 | {children} 30 | 31 |
    32 |
    33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /client/auth.config.ts: -------------------------------------------------------------------------------- 1 | import Credentials from 'next-auth/providers/credentials'; 2 | import GitHub from 'next-auth/providers/github'; 3 | import Google from 'next-auth/providers/google'; 4 | import bcrypt from 'bcryptjs'; 5 | import type { NextAuthConfig } from 'next-auth'; 6 | import { getUserByEmail } from './actions/get-user'; 7 | 8 | export default { 9 | providers: [ 10 | Google({ 11 | clientId: process.env.GOOGLE_CLIENT_ID, 12 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 13 | }), 14 | GitHub({ 15 | clientId: process.env.GITHUB_CLIENT_ID, 16 | clientSecret: process.env.GITHUB_CLIENT_SECRET, 17 | }), 18 | Credentials({ 19 | async authorize(credentials) { 20 | const { email, password } = credentials; 21 | 22 | const user = await getUserByEmail(email as string); 23 | if (!user || !user.password) return null; 24 | 25 | const passwordsMatch = await bcrypt.compare( 26 | password as string, 27 | user.password, 28 | ); 29 | 30 | if (passwordsMatch) return user; 31 | return null; 32 | }, 33 | }), 34 | ], 35 | } satisfies NextAuthConfig; 36 | -------------------------------------------------------------------------------- /client/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /client/components/filter-view.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'; 6 | import { Icons } from '@/components/ui/icons'; 7 | import { useFilter } from '@/hooks/use-filter'; 8 | 9 | export default function FilterView() { 10 | const { view, pathname, createQueryString } = useFilter(); 11 | 12 | const hasViewOptions = /inbox|lists|today/.test(pathname); 13 | 14 | if (hasViewOptions) { 15 | return ( 16 | 17 | 18 | createQueryString('view', 'list')} 22 | > 23 | 24 | 25 | createQueryString('view', 'board')} 29 | > 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | return null; 38 | } 39 | -------------------------------------------------------------------------------- /client/components/filter-week.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { Button } from '@/components/ui/button'; 6 | import { Icons } from '@/components/ui/icons'; 7 | 8 | import { useFilter } from '@/hooks/use-filter'; 9 | 10 | export default function FilterWeek() { 11 | const { createQueryString, removeQueryString } = useFilter(); 12 | 13 | return ( 14 |
    15 | 23 | 31 | 38 |
    39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /client/components/label-actions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import toast from 'react-hot-toast'; 3 | import { mutate } from 'swr'; 4 | import { useRouter } from 'next/navigation'; 5 | 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuSeparator, 11 | DropdownMenuTrigger, 12 | } from '@/components/ui/dropdown-menu'; 13 | import { Button } from '@/components/ui/button'; 14 | import { Icons } from '@/components/ui/icons'; 15 | import AlertModal from '@/components/modals/alert-modal'; 16 | 17 | import type { Label } from '@/types'; 18 | import { useMounted } from '@/hooks/use-mounted'; 19 | import { LabelService } from '@/services/label-service'; 20 | import { handleError } from '@/lib/util/error'; 21 | import { LABELS_KEY } from '@/lib/api'; 22 | 23 | interface LabelActionsProps { 24 | label: Label; 25 | setOpen: React.Dispatch>; 26 | } 27 | 28 | export default function LabelActions({ label, setOpen }: LabelActionsProps) { 29 | const [isOpen, setIsOpen] = React.useState(false); 30 | const [isLoading, setIsLoading] = React.useState(false); 31 | 32 | const isMounted = useMounted(); 33 | const router = useRouter(); 34 | 35 | const onEdit = () => setOpen(true); 36 | 37 | const onDelete = async (labelId: string) => { 38 | setIsLoading(true); 39 | try { 40 | await LabelService.deleteLabel(labelId); 41 | 42 | mutate(LABELS_KEY); 43 | router.refresh(); 44 | toast.success('Label removed.'); 45 | } catch (err) { 46 | handleError(err); 47 | } finally { 48 | setIsLoading(false); 49 | } 50 | }; 51 | 52 | if (!isMounted) return null; 53 | 54 | return ( 55 | <> 56 | setIsOpen(false)} 59 | onConfirm={async () => onDelete(label.id)} 60 | loading={isLoading} 61 | description="Deleting the label will result in the removal of all labels associated with a task." 62 | /> 63 | 64 | 65 | 68 | 69 | e.preventDefault()}> 70 | 71 | 72 | Edit 73 | 74 | 75 | onDelete(label.id)} 77 | className="text-destructive" 78 | onSelect={(e) => e.preventDefault()} 79 | > 80 | 81 | Delete 82 | 83 | 84 | 85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /client/components/label-item.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { Button } from '@/components/ui/button'; 6 | import { Icons } from '@/components/ui/icons'; 7 | import { ListContainer, BoardContainer } from '@/components/ui/container'; 8 | 9 | import LabelForm from '@/components/settings/label-form'; 10 | import LabelBadge from '@/components/ui/label-badge'; 11 | import LabelActions from '@/components/label-actions'; 12 | 13 | import type { Label } from '@/types'; 14 | 15 | interface LabelItemProps { 16 | label?: Label; 17 | } 18 | 19 | export default function LabelItem({ label }: LabelItemProps) { 20 | const [isOpen, setIsOpen] = React.useState(false); 21 | 22 | const open = () => setIsOpen(true); 23 | const close = () => setIsOpen(false); 24 | 25 | if (isOpen) { 26 | return ( 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | if (!label) { 34 | return ( 35 | 45 | ); 46 | } 47 | 48 | return ( 49 | 50 |
    51 | 52 |
    53 | 54 |
    55 |
    56 |
    57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /client/components/list-item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { usePathname, useRouter } from 'next/navigation'; 4 | import { Button } from '@/components/ui/button'; 5 | import { Icons } from '@/components/ui/icons'; 6 | 7 | import ListModal from '@/components/modals/list-modal'; 8 | 9 | import type { List } from '@/types'; 10 | import { cn } from '@/lib/util/tw-merge'; 11 | import { useFilter } from '@/hooks/use-filter'; 12 | 13 | export default function ListItem({ list }: { list?: List }) { 14 | const { persistQueryString } = useFilter(); 15 | const router = useRouter(); 16 | const path = usePathname(); 17 | 18 | if (!list) { 19 | return ( 20 | 21 | 24 | 25 | ); 26 | } 27 | 28 | const persistRouterPush = (href: string, e: React.MouseEvent) => { 29 | e.preventDefault(); 30 | router.push(`${href}?${persistQueryString()}`); 31 | }; 32 | 33 | return ( 34 | 59 | 60 |
    61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /client/components/list-picker.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { FieldValues, PathValue, Path, UseFormReturn } from 'react-hook-form'; 6 | import { Icons } from '@/components/ui/icons'; 7 | import { Command, CommandGroup, CommandItem } from '@/components/ui/command'; 8 | import { 9 | Popover, 10 | PopoverContent, 11 | PopoverTrigger, 12 | } from '@/components/ui/popover'; 13 | import { Button } from '@/components/ui/button'; 14 | 15 | import { cn } from '@/lib/util/tw-merge'; 16 | import type { List } from '@/types'; 17 | 18 | interface ListPickerProps { 19 | form: UseFormReturn; 20 | register: Path; 21 | lists: List[]; 22 | defaultValue?: string; 23 | } 24 | 25 | export function ListPicker({ 26 | form, 27 | defaultValue, 28 | register, 29 | lists, 30 | }: ListPickerProps) { 31 | const defaultListValue = lists.find((l) => l.id === defaultValue)?.name; 32 | 33 | const [open, setOpen] = React.useState(false); 34 | const [value, setValue] = React.useState(defaultListValue); 35 | 36 | const onSelect = (list?: List) => { 37 | if (list) { 38 | setValue(list.name === value ? '' : list.name); 39 | setOpen(false); 40 | form.setValue(register, list.id as PathValue>); 41 | } else { 42 | setValue('Inbox'); 43 | setOpen(false); 44 | form.unregister(register); 45 | } 46 | }; 47 | 48 | return ( 49 | 50 | 51 | 61 | 62 | 63 | 64 | 65 | onSelect()}> 66 | 72 | Inbox 73 | 74 | {lists.map((item) => ( 75 | onSelect(item)} 79 | > 80 | 86 | {item.name} 87 | 88 | ))} 89 | 90 | 91 | 92 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /client/components/modals/alert-modal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { 6 | Dialog, 7 | DialogContent, 8 | DialogHeader, 9 | DialogTitle, 10 | DialogDescription, 11 | } from '@/components/ui/dialog'; 12 | import { Button } from '@/components/ui/button'; 13 | import { useMounted } from '@/hooks/use-mounted'; 14 | 15 | interface AlertModalProps { 16 | title?: string; 17 | description?: string; 18 | isOpen: boolean; 19 | onClose: () => void; 20 | onConfirm: () => void; 21 | loading: boolean; 22 | } 23 | 24 | export default function AlertModal({ 25 | title, 26 | description, 27 | isOpen, 28 | onClose, 29 | onConfirm, 30 | loading, 31 | }: AlertModalProps) { 32 | const isMounted = useMounted(); 33 | 34 | if (!isMounted) { 35 | return null; 36 | } 37 | 38 | const onChange = (open: boolean) => { 39 | if (!open) { 40 | onClose(); 41 | } 42 | }; 43 | 44 | const modalTitle = title || 'Are you sure?'; 45 | const modalDescription = description || 'This action cannot be undone.'; 46 | 47 | return ( 48 | 49 | 50 | 51 | {modalTitle} 52 | {modalDescription} 53 | 54 |
    55 | 58 | 61 |
    62 |
    63 |
    64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /client/components/modals/filter-overlay.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import useSWR from 'swr'; 3 | 4 | import { Icons } from '@/components/ui/icons'; 5 | import { Button } from '@/components/ui/button'; 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuTrigger, 10 | } from '@/components/ui/dropdown-menu'; 11 | import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'; 12 | 13 | import FilterPanel from '@/components/filter-panel'; 14 | 15 | import type { Label } from '@/types'; 16 | import { useMediaQuery } from '@/hooks/use-media-query'; 17 | import { useMounted } from '@/hooks/use-mounted'; 18 | import { fetcher, LABELS_KEY } from '@/lib/api'; 19 | 20 | export default function FilterOverlay() { 21 | const [isOpen, setOpen] = React.useState(false); 22 | const { data: labels } = useSWR(LABELS_KEY, fetcher); 23 | 24 | const isDesktop = useMediaQuery('(min-width: 768px)'); 25 | const isMounted = useMounted(); 26 | 27 | const open = () => setOpen(true); 28 | const close = () => setOpen(false); 29 | 30 | if (!isMounted) 31 | return ( 32 | 36 | ); 37 | 38 | if (isDesktop) { 39 | return ( 40 | 41 | 42 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | } 53 | 54 | return ( 55 | 56 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /client/components/modals/list-modal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogHeader, 8 | DialogDescription, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from '@/components/ui/dialog'; 12 | 13 | import type { List } from '@/types'; 14 | import ListForm from '@/components/list-form'; 15 | 16 | interface ModalProps { 17 | list?: List; 18 | children: React.ReactNode; 19 | } 20 | 21 | export default function ListModal({ list, children }: ModalProps) { 22 | const [isOpen, setIsOpen] = React.useState(false); 23 | const dialogRef = React.useRef(null); 24 | 25 | const close = () => setIsOpen(false); 26 | 27 | return ( 28 | 29 | {children} 30 | 31 | 32 | 33 | {list ? `Edit ${list.name}` : 'Create list'} 34 | 35 | {!list && ( 36 | 37 | Use lists to categorize and manage your tasks. 38 | 39 | )} 40 | 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /client/components/modals/settings-overlay.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { Dialog, DialogContent } from '@/components/ui/dialog'; 6 | import { Drawer, DrawerContent } from '@/components/ui/drawer'; 7 | 8 | import { useLayoutStore } from '@/store/layout-store'; 9 | import { useMediaQuery } from '@/hooks/use-media-query'; 10 | 11 | import SettingsPanel from '@/components/settings/settings-panel'; 12 | 13 | export default function SettingsOverlay() { 14 | const [isOpen, setOpen] = React.useState(false); 15 | const { showSettingsOverlay, toggleSettingsOverlay, setSettingsOverlay } = 16 | useLayoutStore(); 17 | const isDesktop = useMediaQuery('(min-width: 768px)'); 18 | 19 | React.useEffect(() => { 20 | showSettingsOverlay ? setOpen(true) : setOpen(false); 21 | }, [showSettingsOverlay]); 22 | 23 | if (isDesktop) { 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | // A workaround to manage drawer state since it has different behavior than the Dialog 34 | const onOpenChange = () => { 35 | setOpen(!isOpen); 36 | if (!isOpen) { 37 | setSettingsOverlay(false); 38 | } 39 | }; 40 | 41 | return ( 42 | 43 | 44 |
    45 | 46 |
    47 |
    48 |
    49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /client/components/modals/task-overlay.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import useSWR from 'swr'; 3 | 4 | import type { Label, List } from '@/types'; 5 | import { Dialog, DialogContent } from '@/components/ui/dialog'; 6 | import { Drawer, DrawerContent } from '@/components/ui/drawer'; 7 | 8 | import TaskForm from '@/components/task-form'; 9 | 10 | import { useLayoutStore } from '@/store/layout-store'; 11 | import { useMediaQuery } from '@/hooks/use-media-query'; 12 | import { fetcher, LABELS_KEY, LISTS_KEY } from '@/lib/api'; 13 | 14 | export default function TaskOverlay() { 15 | const [isOpen, setOpen] = React.useState(false); 16 | const { data: lists } = useSWR(LISTS_KEY, fetcher); 17 | const { data: labels } = useSWR(LABELS_KEY, fetcher); 18 | const { showTaskOverlay, toggleTaskOverlay, setTaskOverlay } = 19 | useLayoutStore(); 20 | const isDesktop = useMediaQuery('(min-width: 768px)'); 21 | 22 | React.useEffect(() => { 23 | showTaskOverlay ? setOpen(true) : setOpen(false); 24 | }, [showTaskOverlay]); 25 | 26 | React.useEffect(() => { 27 | const down = (e: KeyboardEvent) => { 28 | if (e.key === 'q' && (e.metaKey || e.ctrlKey)) { 29 | e.preventDefault(); 30 | toggleTaskOverlay(); 31 | } 32 | }; 33 | 34 | document.addEventListener('keydown', down); 35 | return () => document.removeEventListener('keydown', down); 36 | }, [toggleTaskOverlay]); 37 | 38 | if (isDesktop) { 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | } 47 | 48 | // A workaround to manage drawer state since it has different behavior than the Dialog 49 | const onOpenChange = () => { 50 | setOpen(!isOpen); 51 | if (!isOpen) { 52 | setTaskOverlay(false); 53 | } 54 | }; 55 | 56 | return ( 57 | 58 | 59 |
    60 | 61 |
    62 |
    63 |
    64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /client/components/providers/overlay-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import SettingsOverlay from '@/components/modals/settings-overlay'; 4 | import TaskOverlay from '@/components/modals/task-overlay'; 5 | 6 | import { useMounted } from '@/hooks/use-mounted'; 7 | 8 | export default function OverlayProvider() { 9 | const isMounted = useMounted(); 10 | 11 | if (!isMounted) return null; 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /client/components/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ThemeProvider as NextThemesProvider } from 'next-themes' 4 | import { type ThemeProviderProps } from 'next-themes/dist/types' 5 | 6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 7 | return {children} 8 | } 9 | -------------------------------------------------------------------------------- /client/components/providers/toast-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { Toaster } from 'react-hot-toast'; 6 | import { toastStyles } from '@/styles/styles'; 7 | 8 | export function ToastProvider() { 9 | return ( 10 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /client/components/resizable-layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { 6 | ResizableHandle, 7 | ResizablePanel, 8 | ResizablePanelGroup, 9 | } from '@/components/ui/resizable'; 10 | 11 | export default function ResizableLayout({ 12 | left, 13 | right, 14 | }: { 15 | left: React.ReactNode; 16 | right: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | 21 | {left} 22 | 23 | 24 | 25 | {right} 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /client/components/retain-query-link.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Link, { LinkProps } from 'next/link'; 3 | import { useFilter } from '@/hooks/use-filter'; 4 | 5 | interface RetainQueryLinkProps extends LinkProps { 6 | children: React.ReactNode; 7 | className?: string; 8 | } 9 | 10 | export default function RetainQueryLink({ 11 | href, 12 | children, 13 | className, 14 | ...props 15 | }: RetainQueryLinkProps) { 16 | const { persistQueryString } = useFilter(); 17 | 18 | return ( 19 | 24 | {children} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /client/components/settings/account-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as z from 'zod'; 4 | 5 | import { zodResolver } from '@hookform/resolvers/zod'; 6 | import { useForm } from 'react-hook-form'; 7 | import { Button } from '@/components/ui/button'; 8 | import { 9 | Form, 10 | FormControl, 11 | FormDescription, 12 | FormField, 13 | FormItem, 14 | FormLabel, 15 | FormMessage, 16 | } from '@/components/ui/form'; 17 | import { Input } from '@/components/ui/input'; 18 | 19 | const accountFormSchema = z.object({ 20 | name: z 21 | .string() 22 | .min(2, { 23 | message: 'Name must be at least 2 characters.', 24 | }) 25 | .max(15, { 26 | message: 'Name must not be longer than 15 characters.', 27 | }), 28 | }); 29 | 30 | type AccountFormValues = z.infer; 31 | 32 | // This can come from your database or API. 33 | const defaultValues: Partial = { 34 | // name: "Your name", 35 | // dob: new Date("2023-01-23"), 36 | }; 37 | 38 | export function AccountForm() { 39 | const form = useForm({ 40 | resolver: zodResolver(accountFormSchema), 41 | defaultValues, 42 | }); 43 | 44 | function onSubmit(data: AccountFormValues) { 45 | // eslint-disable-next-line no-console 46 | console.log(data); 47 | } 48 | 49 | return ( 50 |
    51 | 52 | ( 56 | 57 | Name 58 | 59 | 60 | 61 | 62 | This is the name that will be displayed. 63 | 64 | 65 | 66 | )} 67 | /> 68 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /client/components/settings/settings-panel.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | 5 | import { 6 | Cog, 7 | UserCircle, 8 | Bell, 9 | Heart, 10 | LucideIcon, 11 | PanelLeft, 12 | Palette, 13 | Sparkles, 14 | } from 'lucide-react'; 15 | import { Button } from '@/components/ui/button'; 16 | import { AccountForm } from '@/components/settings/account-form'; 17 | import { PreferencesForm } from '@/components/settings/preferences-form'; 18 | import { LayoutForm } from './layout-form'; 19 | 20 | interface Tab { 21 | id: string; 22 | label: string; 23 | icon: LucideIcon; 24 | } 25 | 26 | const settingsTabs: Tab[] = [ 27 | { 28 | id: 'account', 29 | label: 'Account', 30 | icon: UserCircle, 31 | }, 32 | { 33 | id: 'general', 34 | label: 'General', 35 | icon: Cog, 36 | }, 37 | { 38 | id: 'reminders', 39 | label: 'Reminders', 40 | icon: Bell, 41 | }, 42 | { 43 | id: 'preferences', 44 | label: 'Preferences', 45 | icon: Heart, 46 | }, 47 | { 48 | id: 'layout', 49 | label: 'Layout', 50 | icon: PanelLeft, 51 | }, 52 | { 53 | id: 'productivity', 54 | label: 'Productivity', 55 | icon: Sparkles, 56 | }, 57 | { 58 | id: 'appearance', 59 | label: 'Appearance', 60 | icon: Palette, 61 | }, 62 | ]; 63 | 64 | type FormComponents = { 65 | // eslint-disable-next-line no-unused-vars 66 | [key in Tab['id']]: React.ComponentType; 67 | }; 68 | 69 | const formComponents: FormComponents = { 70 | account: AccountForm, 71 | preferences: PreferencesForm, 72 | layout: LayoutForm, 73 | }; 74 | 75 | export default function SettingsPanel() { 76 | const [tab, setTab] = React.useState(settingsTabs[0]); 77 | 78 | const FormComponent = formComponents[tab.id]; 79 | 80 | return ( 81 |
    82 |
    83 |
    84 | Settings 85 |
    86 |
      87 | {settingsTabs.map((item) => ( 88 |
    • 89 | 97 |
    • 98 | ))} 99 |
    100 |
    101 |
    102 |
    103 | {tab.label} 104 |
    105 |
    {FormComponent && }
    106 |
    107 |
    108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /client/components/subtask-actions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import toast from 'react-hot-toast'; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuSeparator, 10 | DropdownMenuTrigger, 11 | } from '@/components/ui/dropdown-menu'; 12 | import { Button } from '@/components/ui/button'; 13 | import { Icons } from '@/components/ui/icons'; 14 | import AlertModal from '@/components/modals/alert-modal'; 15 | 16 | import type { Subtask, Task } from '@/types'; 17 | import { useMounted } from '@/hooks/use-mounted'; 18 | import { SubtaskService } from '@/services/subtask-service'; 19 | import { handleError } from '@/lib/util/error'; 20 | 21 | interface SubtaskActionsProps { 22 | task: Task; 23 | subtask: Subtask; 24 | setOpen: React.Dispatch>; 25 | } 26 | 27 | export default function SubtaskActions({ 28 | task, 29 | subtask, 30 | setOpen, 31 | }: SubtaskActionsProps) { 32 | const [isOpen, setIsOpen] = React.useState(false); 33 | const [isLoading, setIsLoading] = React.useState(false); 34 | 35 | const isMounted = useMounted(); 36 | const router = useRouter(); 37 | 38 | const onEdit = () => setOpen(true); 39 | 40 | const onDelete = async (subtaskId: string) => { 41 | setIsLoading(true); 42 | try { 43 | await SubtaskService.deleteSubtask(task.id, subtaskId); 44 | toast.success('Deleted!'); 45 | router.refresh(); 46 | } catch (err) { 47 | handleError(err); 48 | } finally { 49 | setIsLoading(false); 50 | } 51 | }; 52 | 53 | if (!isMounted) return null; 54 | 55 | return ( 56 | <> 57 | setIsOpen(false)} 60 | onConfirm={async () => onDelete(subtask.id)} 61 | loading={isLoading} 62 | description="Deleting the Subtask will result in the removal of all labels associated with a task." 63 | /> 64 | 65 | 66 | 69 | 70 | e.preventDefault()}> 71 | 72 | 73 | Edit 74 | 75 | 76 | onDelete(subtask.id)} 78 | className="text-destructive" 79 | onSelect={(e) => e.preventDefault()} 80 | > 81 | 82 | Delete 83 | 84 | 85 | 86 | 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /client/components/subtask-item.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { SubtaskContainer } from '@/components/ui/container'; 4 | import { Button } from '@/components/ui/button'; 5 | 6 | import SubtaskActions from '@/components/subtask-actions'; 7 | import StatusCheckbox from '@/components/status-checkbox'; 8 | import SubtaskForm from '@/components/subtask-form'; 9 | 10 | import type { Subtask, Task } from '@/types'; 11 | import { cn } from '@/lib/util/tw-merge'; 12 | 13 | interface SubtaskItemProps { 14 | subtask?: Subtask; 15 | task: Task; 16 | } 17 | 18 | export default function SubtaskItem({ subtask, task }: SubtaskItemProps) { 19 | const [isOpen, setIsOpen] = React.useState(false); 20 | 21 | const close = () => setIsOpen(false); 22 | const open = () => setIsOpen(true); 23 | 24 | if (isOpen) { 25 | return ( 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | if (!subtask) { 33 | return ( 34 | 35 | 43 | 44 | ); 45 | } 46 | 47 | return ( 48 | 49 |
    50 | 51 | 52 | {subtask.name} 53 | 54 |
    55 |
    56 | 57 |
    58 |
    59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /client/components/subtask-list.tsx: -------------------------------------------------------------------------------- 1 | import SubtaskItem from '@/components/subtask-item'; 2 | import { Progress } from '@/components/ui/progress'; 3 | 4 | import { Subtask, Task } from '@/types'; 5 | import { cn } from '@/lib/util/tw-merge'; 6 | 7 | interface SubtaskListProps { 8 | task: Task; 9 | subtasks?: Subtask[]; 10 | showSubtaskList?: boolean; 11 | alwaysOpen?: boolean; 12 | } 13 | 14 | export default function SubtaskList({ 15 | subtasks, 16 | task, 17 | showSubtaskList, 18 | alwaysOpen, 19 | }: SubtaskListProps) { 20 | const calculateProgress = (): number => { 21 | if (!subtasks || subtasks.length === 0) { 22 | return 0; 23 | } 24 | 25 | const completedSubtasks = subtasks.filter( 26 | (subtask) => subtask.isComplete, 27 | ).length; 28 | const totalSubtasks = subtasks.length; 29 | const progress = (completedSubtasks / totalSubtasks) * 100; 30 | 31 | return progress; 32 | }; 33 | 34 | return ( 35 |
    38 | {subtasks?.length && subtasks.length > 0 ? ( 39 | 40 | ) : null} 41 | {subtasks && 42 | subtasks.map((subtask) => ( 43 | 44 | ))} 45 | {showSubtaskList && } 46 | {alwaysOpen && } 47 |
    48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /client/components/task-list.tsx: -------------------------------------------------------------------------------- 1 | import TaskItem from '@/components/task-item'; 2 | import { Skeleton } from '@/components/ui/skeleton'; 3 | import { Button } from '@/components/ui/button'; 4 | 5 | import type { Label, List, Task } from '@/types'; 6 | 7 | export function TaskList({ 8 | tasks, 9 | lists, 10 | labels, 11 | type = 'list', 12 | expandable = true, 13 | }: { 14 | tasks: Task[]; 15 | lists: List[]; 16 | labels: Label[]; 17 | type?: 'board' | 'list'; 18 | expandable?: boolean; 19 | }) { 20 | return ( 21 |
    22 | {tasks.map((task) => ( 23 | 30 | ))} 31 | {expandable && } 32 |
    33 | ); 34 | } 35 | 36 | TaskList.Empty = function EmptyList() { 37 | return ( 38 |
    39 |
    40 |

    No tasks

    41 |

    42 | Seems like you're totally on top of things. 43 |

    44 | 47 |
    48 |
    49 | ); 50 | }; 51 | 52 | TaskList.Skeleton = function LoadingList() { 53 | return ; 54 | }; 55 | -------------------------------------------------------------------------------- /client/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as AccordionPrimitive from '@radix-ui/react-accordion'; 5 | import { ChevronDown } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/util/tw-merge'; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 16 | )); 17 | AccordionItem.displayName = 'AccordionItem'; 18 | 19 | const AccordionTrigger = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, children, ...props }, ref) => ( 23 | 24 | svg]:rotate-180', 28 | className, 29 | )} 30 | {...props} 31 | > 32 | {children} 33 | 34 | 35 | 36 | )); 37 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 38 | 39 | const AccordionContent = React.forwardRef< 40 | React.ElementRef, 41 | React.ComponentPropsWithoutRef 42 | >(({ className, children, ...props }, ref) => ( 43 | 48 |
    {children}
    49 |
    50 | )); 51 | 52 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 53 | 54 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 55 | -------------------------------------------------------------------------------- /client/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | 4 | import { cn } from '@/lib/util/tw-merge'; 5 | 6 | const badgeVariants = cva( 7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', 13 | secondary: 14 | 'border bg-secondary text-secondary-foreground hover:bg-secondary/80', 15 | destructive: 16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', 17 | outline: 'text-foreground', 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: 'default', 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
    33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /client/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Slot } from '@radix-ui/react-slot'; 3 | import { cva, type VariantProps } from 'class-variance-authority'; 4 | 5 | import { cn } from '@/lib/util/tw-merge'; 6 | import { Icons } from '@/components/ui/icons'; 7 | 8 | const buttonVariants = cva( 9 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', 10 | { 11 | variants: { 12 | variant: { 13 | default: 14 | 'bg-primary text-primary-foreground hover:bg-primary/80 hover:shadow-lg transition-all duration-100', 15 | destructive: 16 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 17 | outline: 18 | 'border border-border bg-background hover:bg-accent hover:text-accent-foreground', 19 | secondary: 20 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80 border', 21 | ghost: 'hover:bg-accent hover:text-accent-foreground', 22 | link: 'text-primary underline-offset-4 hover:underline', 23 | picker: 24 | 'bg-transparent hover:bg-transparent text-muted-foreground hover:text-foreground', 25 | }, 26 | size: { 27 | default: 'h-10 px-4 py-2', 28 | sm: 'h-9 rounded-md px-3', 29 | lg: 'h-11 rounded-md px-8', 30 | icon: 'h-5 w-5', 31 | }, 32 | }, 33 | defaultVariants: { 34 | variant: 'default', 35 | size: 'default', 36 | }, 37 | }, 38 | ); 39 | 40 | export interface ButtonProps 41 | extends React.ButtonHTMLAttributes, 42 | VariantProps { 43 | asChild?: boolean; 44 | rounded?: boolean; 45 | loading?: boolean; 46 | children: React.ReactNode; 47 | } 48 | 49 | const Button = React.forwardRef( 50 | ( 51 | { className, variant, size, asChild = false, loading, children, ...props }, 52 | ref, 53 | ) => { 54 | const Comp = asChild ? Slot : 'button'; 55 | return ( 56 | 61 | {loading ? ( 62 | 63 | ) : ( 64 | children 65 | )} 66 | 67 | ); 68 | }, 69 | ); 70 | Button.displayName = 'Button'; 71 | 72 | export { Button, buttonVariants }; 73 | -------------------------------------------------------------------------------- /client/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/util/tw-merge'; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
    14 | )); 15 | Card.displayName = 'Card'; 16 | 17 | const CardHeader = React.forwardRef< 18 | HTMLDivElement, 19 | React.HTMLAttributes 20 | >(({ className, ...props }, ref) => ( 21 |
    26 | )); 27 | CardHeader.displayName = 'CardHeader'; 28 | 29 | const CardTitle = React.forwardRef< 30 | HTMLParagraphElement, 31 | React.HTMLAttributes 32 | >(({ className, ...props }, ref) => ( 33 | // eslint-disable-next-line jsx-a11y/heading-has-content 34 |

    42 | )); 43 | CardTitle.displayName = 'CardTitle'; 44 | 45 | const CardDescription = React.forwardRef< 46 | HTMLParagraphElement, 47 | React.HTMLAttributes 48 | >(({ className, ...props }, ref) => ( 49 |

    54 | )); 55 | CardDescription.displayName = 'CardDescription'; 56 | 57 | const CardContent = React.forwardRef< 58 | HTMLDivElement, 59 | React.HTMLAttributes 60 | >(({ className, ...props }, ref) => ( 61 |

    62 | )); 63 | CardContent.displayName = 'CardContent'; 64 | 65 | const CardFooter = React.forwardRef< 66 | HTMLDivElement, 67 | React.HTMLAttributes 68 | >(({ className, ...props }, ref) => ( 69 |
    74 | )); 75 | CardFooter.displayName = 'CardFooter'; 76 | 77 | export { 78 | Card, 79 | CardHeader, 80 | CardFooter, 81 | CardTitle, 82 | CardDescription, 83 | CardContent, 84 | }; 85 | -------------------------------------------------------------------------------- /client/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; 5 | import { Check } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/util/tw-merge'; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /client/components/ui/container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/util/tw-merge'; 4 | 5 | export function BoardContainer({ 6 | children, 7 | className, 8 | ...props 9 | }: { 10 | children: React.ReactNode; 11 | className?: string; 12 | }) { 13 | return ( 14 |
    21 | {children} 22 |
    23 | ); 24 | } 25 | 26 | export function ListContainer({ 27 | children, 28 | className, 29 | }: { 30 | children: React.ReactNode; 31 | className?: string; 32 | }) { 33 | return
    {children}
    ; 34 | } 35 | 36 | export function SubtaskContainer({ 37 | children, 38 | className, 39 | }: { 40 | children: React.ReactNode; 41 | className?: string; 42 | }) { 43 | return ( 44 |
    {children}
    45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /client/components/ui/experimental-calendar.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unstable-nested-components */ 2 | 3 | 'use client'; 4 | 5 | import * as React from 'react'; 6 | import { DayPicker } from 'react-day-picker'; 7 | import { Icons } from '@/components/ui/icons'; 8 | import { cn } from '@/lib/util/tw-merge'; 9 | import { buttonVariants } from '@/components/ui/button'; 10 | 11 | export type CalendarProps = React.ComponentProps; 12 | 13 | function Calendar({ 14 | className, 15 | classNames, 16 | showOutsideDays = true, 17 | ...props 18 | }: CalendarProps) { 19 | return ( 20 | , 55 | IconRight: () => , 56 | // eslint-disable-next-line react/no-unstable-nested-components 57 | }} 58 | {...props} 59 | /> 60 | ); 61 | } 62 | Calendar.displayName = 'Calendar'; 63 | 64 | export { Calendar }; 65 | -------------------------------------------------------------------------------- /client/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/util/tw-merge'; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes { 7 | transparent?: boolean; 8 | outline?: boolean; 9 | } 10 | 11 | const Input = React.forwardRef( 12 | ({ className, transparent, outline, type, ...props }, ref) => ( 13 | 26 | ), 27 | ); 28 | Input.displayName = 'Input'; 29 | 30 | export { Input }; 31 | -------------------------------------------------------------------------------- /client/components/ui/label-badge.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | import { useRouter } from 'next/navigation'; 6 | import { Badge } from '@/components/ui/badge'; 7 | import { Icons } from '@/components/ui/icons'; 8 | 9 | import { TaskService } from '@/services/task-service'; 10 | import { handleError } from '@/lib/util/error'; 11 | import { cn } from '@/lib/util/tw-merge'; 12 | import type { Label } from '@/types'; 13 | 14 | interface LabelBadgeProps { 15 | label: Label; 16 | noBorder?: boolean; 17 | taskId?: string; 18 | } 19 | 20 | export function LabelColor({ 21 | color, 22 | className, 23 | }: { 24 | color?: string | null; 25 | className?: string; 26 | }) { 27 | return ( 28 |
    32 | ); 33 | } 34 | 35 | export default function LabelBadge({ 36 | label, 37 | noBorder, 38 | taskId, 39 | }: LabelBadgeProps) { 40 | const [isLoading, setIsLoading] = React.useState(false); 41 | const router = useRouter(); 42 | 43 | const onRemove = async (taskId: string) => { 44 | setIsLoading(true); 45 | try { 46 | await TaskService.removeLabel({ taskId, labelId: label.id }); 47 | router.refresh(); 48 | } catch (e) { 49 | handleError(e); 50 | } finally { 51 | setIsLoading(false); 52 | } 53 | }; 54 | 55 | if (noBorder) 56 | return ( 57 |
    58 | 59 | {label.name} 60 |
    61 | ); 62 | 63 | return ( 64 | 65 | 66 | {label.name} 67 | {taskId && !isLoading && ( 68 | onRemove(taskId)} 71 | /> 72 | )} 73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /client/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as LabelPrimitive from '@radix-ui/react-label'; 5 | import { cva, type VariantProps } from 'class-variance-authority'; 6 | 7 | import { cn } from '@/lib/util/tw-merge'; 8 | 9 | const labelVariants = cva( 10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /client/components/ui/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import TaskItem from '../task-item'; 4 | import LabelItem from '../label-item'; 5 | 6 | import { cn } from '@/lib/util/tw-merge'; 7 | 8 | import type { Label, List, Task } from '@/types'; 9 | 10 | export function PageList({ children }: { children: React.ReactNode }) { 11 | return
    {children}
    ; 12 | } 13 | 14 | export function PageBoard({ children }: { children: React.ReactNode }) { 15 | return ( 16 |
    17 | {children} 18 |
    19 | ); 20 | } 21 | 22 | export function PageHeading({ 23 | children, 24 | items, 25 | color, 26 | level = 'h1', 27 | className, 28 | }: { 29 | children: React.ReactNode; 30 | items?: any[]; 31 | color?: string; 32 | level?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; 33 | className?: string; 34 | }) { 35 | const HeadingTag = level; 36 | 37 | return ( 38 |
    39 | {color && ( 40 | 46 | )} 47 | 48 | {children} 49 | 50 | {items && ( 51 | ({items.length}) 52 | )} 53 |
    54 | ); 55 | } 56 | 57 | export function PageDescription({ children }: { children: React.ReactNode }) { 58 | return

    {children}

    ; 59 | } 60 | 61 | export function LabelList({ labels }: { labels: Label[] }) { 62 | return ( 63 |
    64 | {labels.map((label) => ( 65 | 66 | ))} 67 | 68 |
    69 | ); 70 | } 71 | 72 | export function TaskList({ 73 | tasks, 74 | lists, 75 | labels, 76 | type = 'list', 77 | expandable = true, 78 | }: { 79 | tasks: Task[]; 80 | lists: List[]; 81 | labels: Label[]; 82 | type?: 'board' | 'list'; 83 | expandable?: boolean; 84 | }) { 85 | return ( 86 |
    87 | {tasks.map((task) => ( 88 | 95 | ))} 96 | {expandable && } 97 |
    98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /client/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as PopoverPrimitive from '@radix-ui/react-popover'; 5 | 6 | import { cn } from '@/lib/util/tw-merge'; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )); 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 30 | 31 | export { Popover, PopoverTrigger, PopoverContent }; 32 | -------------------------------------------------------------------------------- /client/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as ProgressPrimitive from '@radix-ui/react-progress'; 5 | 6 | import { cn } from '@/lib/util/tw-merge'; 7 | 8 | const Progress = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef & { 11 | indicatorColor?: string; 12 | } 13 | >(({ className, value, indicatorColor, ...props }, ref) => ( 14 | 22 | 31 | 32 | )); 33 | Progress.displayName = ProgressPrimitive.Root.displayName; 34 | 35 | export { Progress }; 36 | -------------------------------------------------------------------------------- /client/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; 5 | import { Circle } from 'lucide-react'; 6 | 7 | import { cn } from '@/lib/util/tw-merge'; 8 | 9 | const RadioGroup = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 18 | )); 19 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; 20 | 21 | const RadioGroupItem = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 33 | 34 | 35 | 36 | 37 | )); 38 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; 39 | 40 | export { RadioGroup, RadioGroupItem }; 41 | -------------------------------------------------------------------------------- /client/components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { GripVertical } from 'lucide-react'; 4 | import * as ResizablePrimitive from 'react-resizable-panels'; 5 | 6 | import { cn } from '@/lib/util/tw-merge'; 7 | 8 | function ResizablePanelGroup({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 20 | ); 21 | } 22 | 23 | const ResizablePanel = ResizablePrimitive.Panel; 24 | 25 | function ResizableHandle({ 26 | withHandle, 27 | className, 28 | ...props 29 | }: React.ComponentProps & { 30 | withHandle?: boolean; 31 | }) { 32 | return ( 33 | div]:rotate-90', 36 | className, 37 | )} 38 | {...props} 39 | > 40 | {withHandle && ( 41 |
    42 | 43 |
    44 | )} 45 |
    46 | ); 47 | } 48 | 49 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; 50 | -------------------------------------------------------------------------------- /client/components/ui/seperator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SeparatorPrimitive from '@radix-ui/react-separator'; 5 | 6 | import { cn } from '@/lib/util/tw-merge'; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = 'horizontal', decorative = true, ...props }, 14 | ref, 15 | ) => ( 16 | 27 | ), 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /client/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/util/tw-merge'; 4 | 5 | function Skeleton({ 6 | className, 7 | ...props 8 | }: React.HTMLAttributes) { 9 | return ( 10 |
    14 | ); 15 | } 16 | 17 | export { Skeleton }; 18 | -------------------------------------------------------------------------------- /client/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as SwitchPrimitives from '@radix-ui/react-switch'; 5 | 6 | import { cn } from '@/lib/util/tw-merge'; 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )); 27 | Switch.displayName = SwitchPrimitives.Root.displayName; 28 | 29 | export { Switch }; 30 | -------------------------------------------------------------------------------- /client/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import * as TabsPrimitive from '@radix-ui/react-tabs'; 5 | 6 | import { cn } from '@/lib/util/tw-merge'; 7 | 8 | const Tabs = TabsPrimitive.Root; 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | TabsList.displayName = TabsPrimitive.List.displayName; 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )); 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )); 53 | TabsContent.displayName = TabsPrimitive.Content.displayName; 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 56 | -------------------------------------------------------------------------------- /client/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/util/tw-merge'; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => ( 10 |