├── .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 | 
2 |
3 |
4 |
5 |
Taskify
6 |
7 |
8 |
9 |
10 | Task Manager app with modern features.
11 |
12 |
13 |
14 |
15 |
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 |
16 |
17 | Open menu
18 |
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 |
31 |
37 |
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 | setOpen(true)}
23 | {...props}
24 | className={cn('', !showLeftSidebar && 'md:hidden')}
25 | >
26 |
27 |
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 |
22 |
23 |
24 |
25 |
26 |
{task?.name}
27 |
28 | {task?.description}
29 |
30 |
31 |
32 |
40 | Test
41 |
42 |
43 |
44 |
Due Date
45 |
{task?.dueDate?.toString()}
46 |
47 |
48 |
Priority
49 |
{task?.priority}
50 |
51 |
52 |
Labels
53 |
54 | {task?.labels?.map((label) => (
55 |
56 | ))}
57 |
58 |
59 |
60 |
61 |
Subtasks
62 | {task && (
63 |
68 | )}
69 |
70 |
71 |
72 |
73 |
74 |
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 |
18 | {resolvedTheme === 'light' ? (
19 |
20 | ) : (
21 |
22 | )}
23 | Toggle theme
24 |
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 |
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 | setIsOpen(!isOpen)}>
31 | {isOpen ? (
32 |
33 | ) : (
34 |
35 | )}
36 | Open menu
37 |
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 |
16 |
17 |
18 |
19 | Taskify
20 |
21 |
22 | {config.marketing.links.map((link) => (
23 |
24 | ))}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
35 |
36 |
37 |
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 |
22 |
23 |
24 | Toggle theme
25 |
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 |
17 | Sign up for free
18 |
19 |
23 | Explore more
24 |
25 |
29 | Sign up for free
30 |
31 |
36 | Explore more
37 |
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 | Get Started
33 |
34 |
35 |
36 | GitHub
37 |
38 |
39 |
40 |
41 |
48 |
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 |
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 |
21 | Overview
22 |
23 | {overviewLinks.map((link) => (
24 |
25 | ))}
26 |
27 | {config.marketing.docsLinks.map((section) => (
28 |
29 |
30 |
31 | {section.heading}
32 |
33 | {section.links.map((link) => (
34 |
35 | ))}
36 |
37 |
38 |
39 |
40 | ))}
41 |
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 |
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 |
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 |
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 | loginSocial('google')}
40 | >
41 |
42 | Google
43 |
44 | loginSocial('github')}
51 | >
52 |
53 | GitHub
54 |
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 | createQueryString('offset', 'prev')}
19 | className="w-8 h-8 rounded-sm"
20 | >
21 |
22 |
23 | createQueryString('offset', 'next')}
27 | className="w-8 h-8 rounded-sm"
28 | >
29 |
30 |
31 | removeQueryString('offset')}
34 | className="h-8 rounded-sm"
35 | >
36 | Today
37 |
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 |
66 |
67 |
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 |
40 |
41 |
42 |
43 | Add label
44 |
45 | );
46 | }
47 |
48 | return (
49 |
50 |
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 |
22 |
23 |
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 | persistRouterPush(`/lists/${list.id}`, e)}
41 | >
42 |
43 |
44 | {list.name[0]}
45 |
46 |
47 | {list.name}
48 | {
52 | e.stopPropagation();
53 | }}
54 | >
55 |
56 |
57 |
58 |
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 |
58 | {value || 'Inbox'}
59 |
60 |
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 |
56 | Cancel
57 |
58 |
59 | Continue
60 |
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 |
33 |
34 | Display
35 |
36 | );
37 |
38 | if (isDesktop) {
39 | return (
40 |
41 |
42 |
43 |
44 | Display
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
54 | return (
55 |
56 |
57 |
58 |
59 |
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 |
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 | setTab(item)}
93 | >
94 |
95 | {item.label}
96 |
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 |
67 |
68 |
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 |
40 |
41 | Add subtask
42 |
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 |
45 | Create task
46 |
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 |
18 | ),
19 | );
20 | Textarea.displayName = 'Textarea';
21 |
22 | export { Textarea };
23 |
--------------------------------------------------------------------------------
/client/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 |
3 | 'use client';
4 |
5 | import * as React from 'react';
6 | import * as TooltipPrimitive from '@radix-ui/react-tooltip';
7 |
8 | import { cn } from '@/lib/util/tw-merge';
9 |
10 | const TooltipProvider = TooltipPrimitive.Provider;
11 |
12 | const Tooltip = TooltipPrimitive.Root;
13 |
14 | const TooltipTrigger = TooltipPrimitive.Trigger;
15 |
16 | const TooltipContent = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, sideOffset = 4, ...props }, ref) => (
20 |
29 | ));
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName;
31 |
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
33 |
--------------------------------------------------------------------------------
/client/hooks/use-click-outside.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | // eslint-disable-next-line no-unused-vars
4 | type ClickOutsideCallback = (event: MouseEvent) => void;
5 |
6 | export function useClickOutside(
7 | ref: React.RefObject,
8 | callback: ClickOutsideCallback,
9 | ) {
10 | React.useEffect(() => {
11 | const handleClickOutside = (event: MouseEvent) => {
12 | if (ref.current && !ref.current.contains(event.target as Node)) {
13 | callback(event);
14 | }
15 | };
16 | document.addEventListener('mousedown', handleClickOutside);
17 |
18 | return () => {
19 | document.removeEventListener('mousedown', handleClickOutside);
20 | };
21 | }, [ref, callback]);
22 | }
23 |
--------------------------------------------------------------------------------
/client/hooks/use-current-user.tsx:
--------------------------------------------------------------------------------
1 | import { useSession } from 'next-auth/react';
2 |
3 | export const useCurrentUser = () => {
4 | const session = useSession();
5 |
6 | return session.data?.user;
7 | };
8 |
--------------------------------------------------------------------------------
/client/hooks/use-debounce.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export function useDebounce(value: T, delay?: number): T {
4 | const [debouncedValue, setDebouncedValue] = React.useState(value)
5 |
6 | React.useEffect(() => {
7 | const timer = setTimeout(() => setDebouncedValue(value), delay || 500)
8 |
9 | return () => {
10 | clearTimeout(timer)
11 | }
12 | }, [value, delay])
13 |
14 | return debouncedValue
15 | }
--------------------------------------------------------------------------------
/client/hooks/use-filter.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { useRouter, usePathname, useSearchParams } from 'next/navigation';
4 | import {
5 | queryParamsMapping,
6 | ExtendedSearchParamsOptions,
7 | FilterOption,
8 | } from '@/lib/util/filter';
9 |
10 | export const useFilter = () => {
11 | const router = useRouter();
12 | const pathname = usePathname();
13 | const searchParams = useSearchParams();
14 |
15 | const offset = parseInt(searchParams.get('offset') ?? '0', 10);
16 | const view =
17 | (searchParams.get('view') as ExtendedSearchParamsOptions['view']) ?? 'list';
18 | const completed = searchParams.get('completed') ?? false;
19 | const labelId = searchParams.get('labelId');
20 |
21 | const createQueryString = (
22 | name: FilterOption,
23 | value: string | 'prev' | 'next',
24 | ) => {
25 | const params = new URLSearchParams(searchParams);
26 |
27 | if (value === 'prev' || value === 'next') {
28 | const newOffset = value === 'prev' ? offset - 1 : offset + 1;
29 | params.set(name, (newOffset ?? 0).toString());
30 | } else {
31 | params.set(name, value);
32 | }
33 |
34 | const newQueryString = params.toString();
35 | router.push(`${pathname}?${newQueryString}`);
36 | };
37 |
38 | const removeQueryString = (name?: FilterOption) => {
39 | const params = new URLSearchParams(searchParams.toString());
40 |
41 | if (!name) {
42 | Object.keys(queryParamsMapping).forEach((param) => params.delete(param));
43 | } else {
44 | params.delete(name);
45 | }
46 | const newQueryString = params.toString();
47 | router.push(`${pathname}?${newQueryString}`);
48 | };
49 |
50 | const persistQueryString = React.useCallback(() => {
51 | const params = new URLSearchParams(searchParams);
52 |
53 | return params.toString();
54 | }, [searchParams]);
55 |
56 | return {
57 | createQueryString,
58 | removeQueryString,
59 | persistQueryString,
60 | searchParams,
61 | pathname,
62 | view,
63 | offset,
64 | completed,
65 | labelId,
66 | };
67 | };
68 |
--------------------------------------------------------------------------------
/client/hooks/use-media-query.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export function useMediaQuery(query: string) {
4 | const isClient = typeof window === 'object'; // Check if running in a browser environment
5 | const [matches, setMatches] = React.useState(() =>
6 | isClient ? window.matchMedia(query).matches : false,
7 | );
8 |
9 | React.useEffect(() => {
10 | if (typeof window === 'undefined') {
11 | return; // Avoid running the effect on the server or in a non-browser environment
12 | }
13 |
14 | const matchMediaList = window.matchMedia(query);
15 |
16 | function handleChange(e: MediaQueryListEvent) {
17 | setMatches(e.matches);
18 | }
19 |
20 | matchMediaList.addEventListener('change', handleChange);
21 |
22 | // Cleanup function to remove the listener when the component is unmounted
23 | // eslint-disable-next-line consistent-return
24 | return () => {
25 | matchMediaList.removeEventListener('change', handleChange);
26 | return undefined; // Explicitly return undefined
27 | };
28 | }, [query]);
29 |
30 | return matches;
31 | }
32 |
--------------------------------------------------------------------------------
/client/hooks/use-mounted.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | export function useMounted() {
4 | const [mounted, setMounted] = React.useState(false)
5 |
6 | React.useEffect(() => {
7 | setMounted(true)
8 | }, [])
9 |
10 | return mounted
11 | }
--------------------------------------------------------------------------------
/client/lib/api.ts:
--------------------------------------------------------------------------------
1 | import axios, { AxiosInstance } from 'axios';
2 |
3 | export const fetcher = (url: string) =>
4 | fetch(url).then((res) => {
5 | if (!res.ok) {
6 | throw new Error(`Failed to fetch data from ${url}`);
7 | }
8 | return res.json();
9 | });
10 |
11 | export const LISTS_KEY = '/api/lists';
12 | export const LABELS_KEY = '/api/labels';
13 |
14 | export const api: AxiosInstance = axios.create({
15 | baseURL:
16 | process.env.NODE_ENV === 'production'
17 | ? process.env.PUBLIC_URL
18 | : process.env.DEV_URL,
19 | });
20 |
--------------------------------------------------------------------------------
/client/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth';
2 |
3 | import { PrismaAdapter } from '@auth/prisma-adapter';
4 |
5 | import authConfig from '@/auth.config';
6 |
7 | import { db } from '@/lib/db';
8 |
9 | export const {
10 | handlers: { GET, POST },
11 | auth,
12 | signIn,
13 | signOut,
14 | } = NextAuth({
15 | pages: {
16 | signIn: '/auth/login',
17 | error: '/auth/error',
18 | },
19 | events: {
20 | async linkAccount({ user }) {
21 | await db.user.update({
22 | where: { id: user.id },
23 | data: { emailVerified: new Date() },
24 | });
25 | },
26 | },
27 | callbacks: {
28 | async session({ token, session }) {
29 | if (token.sub && session.user) {
30 | // eslint-disable-next-line no-param-reassign
31 | session.user.id = token.sub;
32 | }
33 | return session;
34 | },
35 | async jwt({ token }) {
36 | return token;
37 | },
38 | },
39 | secret: process.env.AUTH_SECRET,
40 | adapter: PrismaAdapter(db),
41 | session: { strategy: 'jwt' },
42 | ...authConfig,
43 | });
44 |
--------------------------------------------------------------------------------
/client/lib/config.ts:
--------------------------------------------------------------------------------
1 | import { Inbox, CalendarClock, CalendarDays, Tag } from 'lucide-react';
2 |
3 | export const config = {
4 | metadata: {
5 | title: {
6 | default: 'Taskify',
7 | template: `%s | Taskify`,
8 | },
9 | description:
10 | 'Collaborate, manage projects, and reach new productivity peaks',
11 | icons: [
12 | {
13 | url: '/vercel.svg',
14 | href: '/vercel.svg',
15 | },
16 | ],
17 | },
18 | marketing: {
19 | links: [
20 | {
21 | title: 'Features',
22 | href: '/#features',
23 | },
24 | {
25 | title: 'Pricing',
26 | href: '/#pricing',
27 | },
28 | {
29 | title: 'Docs',
30 | href: '/docs/features',
31 | },
32 | ],
33 | docsLinks: [
34 | {
35 | heading: 'Create tasks',
36 | links: [
37 | { text: 'Creating Tasks', href: '#' },
38 | { text: 'Markdown Editor', href: '#' },
39 | { text: 'Mention to Action', href: '#' },
40 | { text: 'Natural Language Processing', href: '#' },
41 | { text: 'AI Integration', href: '#' },
42 | { text: 'Speech Recognition', href: '#' },
43 | ],
44 | },
45 | {
46 | heading: 'Task Properties',
47 | links: [
48 | { text: 'Labels', href: '#' },
49 | { text: 'Lists', href: '#' },
50 | { text: 'Status', href: '#' },
51 | { text: 'Priority', href: '#' },
52 | { text: 'Subtasks', href: '#' },
53 | { text: 'Reminders', href: '#' },
54 | { text: 'Attachments', href: '#' },
55 | { text: 'Recurring Tasks', href: '#' },
56 | ],
57 | },
58 | {
59 | heading: 'Additional Features',
60 | links: [
61 | { text: 'Time Tracking', href: '#' },
62 | { text: 'Publishing', href: '#' },
63 | { text: 'Keyboard shortcuts', href: '#' },
64 | { text: 'Drag and Drop', href: '#' },
65 | ],
66 | },
67 | {
68 | heading: 'Filtering and Views',
69 | links: [
70 | { text: 'Filters', href: '#' },
71 | { text: 'Views', href: '#' },
72 | { text: 'Display Options', href: '#' },
73 | ],
74 | },
75 | ],
76 | },
77 | platform: {
78 | links: [
79 | {
80 | id: 'inbox',
81 | label: 'Inbox',
82 | icon: Inbox,
83 | href: '/inbox',
84 | },
85 | {
86 | id: 'today',
87 | label: 'Today',
88 | icon: CalendarDays,
89 | href: '/today',
90 | },
91 | {
92 | id: 'upcoming',
93 | label: 'Upcoming',
94 | icon: CalendarClock,
95 | href: '/upcoming',
96 | },
97 | {
98 | id: 'labels',
99 | label: 'Labels',
100 | icon: Tag,
101 | href: '/labels',
102 | },
103 | ],
104 | },
105 | };
106 |
--------------------------------------------------------------------------------
/client/lib/constants.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Calendar,
3 | CalendarClock,
4 | CalendarDays,
5 | Clipboard,
6 | Inbox,
7 | Menu,
8 | Tags,
9 | } from 'lucide-react';
10 |
11 | export type Mode = {
12 | label: string;
13 | value: string;
14 | };
15 |
16 | export const modes: Mode[] = [
17 | {
18 | label: 'Automatic',
19 | value: 'auto',
20 | },
21 | {
22 | label: 'Manual',
23 | value: 'manual',
24 | },
25 | {
26 | label: 'Hold-to-talk',
27 | value: 'hold',
28 | },
29 | ];
30 |
31 | export const statuses = [
32 | {
33 | id: 1,
34 | value: 'Incomplete',
35 | label: 'Incomplete',
36 | },
37 | {
38 | id: 2,
39 | value: 'Completed',
40 | label: 'Complete',
41 | },
42 | ];
43 |
44 | export const sidebarItems = [
45 | {
46 | id: 'inbox',
47 | label: 'Inbox',
48 | icon: Inbox,
49 | },
50 | {
51 | id: 'today',
52 | label: 'Today',
53 | icon: CalendarDays,
54 | },
55 | {
56 | id: 'upcoming',
57 | label: 'Upcoming',
58 | icon: CalendarClock,
59 | },
60 | {
61 | id: 'labels',
62 | label: 'Labels',
63 | icon: Tags,
64 | },
65 | {
66 | id: 'lists',
67 | label: 'Lists',
68 | icon: Menu,
69 | },
70 | ] as const;
71 |
72 | export type SidebarItem = (typeof sidebarItems)[number]['id'];
73 |
74 | export const widgetItems = [
75 | {
76 | id: 'calendar',
77 | label: 'Calendar',
78 | icon: Calendar,
79 | },
80 | {
81 | id: 'notes',
82 | label: 'Notes',
83 | icon: Clipboard,
84 | },
85 | ] as const;
86 |
87 | export type WidgetItem = (typeof widgetItems)[number]['id'];
88 |
89 | export const statusColumns = [
90 | {
91 | status: 'New',
92 | color: 'bg-yellow-500',
93 | },
94 | {
95 | status: 'In Progress',
96 | color: 'bg-yellow-500',
97 | },
98 | {
99 | status: 'Completed',
100 | color: 'bg-yellow-500',
101 | },
102 | ];
103 |
--------------------------------------------------------------------------------
/client/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | declare global {
4 | // eslint-disable-next-line vars-on-top, no-var, no-unused-vars
5 | var prisma: PrismaClient | undefined;
6 | }
7 |
8 | export const db = global.prisma || new PrismaClient();
9 |
10 | if (process.env.NODE_ENV !== 'production') global.prisma = db;
11 |
--------------------------------------------------------------------------------
/client/lib/util/error.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import { AxiosError, AxiosResponse } from 'axios';
3 | import toast from 'react-hot-toast';
4 |
5 | const handleResponseError = (response: AxiosResponse) => {
6 | console.error('Unhandled response error:', response);
7 | toast.error('Oops. Something went wrong.');
8 | };
9 |
10 | const handleRequestError = (error: any) => {
11 | console.error('Request error:', error);
12 | toast.error('Something went wrong.');
13 | };
14 |
15 | const handleAxiosError = (error: AxiosError) => {
16 | const { response, request } = error;
17 |
18 | if (response) {
19 | handleResponseError(response);
20 | } else if (request) {
21 | handleRequestError(request);
22 | } else {
23 | console.error('Unhandled AxiosError:', error);
24 | toast.error('An unhandled error occured.');
25 | }
26 | };
27 |
28 | export const handleError = (error: unknown) => {
29 | if (error instanceof AxiosError) {
30 | handleAxiosError(error);
31 | } else if (error instanceof Error) {
32 | toast.error(error.message);
33 | } else {
34 | console.log('Unhandled error:', error);
35 | toast.error('An unexpected error occurred.');
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/client/lib/util/filter.ts:
--------------------------------------------------------------------------------
1 | export interface SearchParamsOptions {
2 | listId?: string;
3 | labelId?: string;
4 | unsorted?: boolean;
5 | upcoming?: boolean;
6 | overdue?: boolean;
7 | incomplete?: boolean;
8 | today?: boolean;
9 | pending?: boolean;
10 | completed?: boolean;
11 | view?: string;
12 | offset?: number;
13 | dueDate?: string;
14 | }
15 |
16 | export const queryParamsMapping: Record<
17 | keyof SearchParamsOptions,
18 | keyof SearchParamsOptions
19 | > = {
20 | listId: 'listId',
21 | labelId: 'labelId',
22 | unsorted: 'unsorted',
23 | upcoming: 'upcoming',
24 | overdue: 'overdue',
25 | today: 'today',
26 | pending: 'pending',
27 | incomplete: 'incomplete',
28 | completed: 'completed',
29 | view: 'view',
30 | offset: 'offset',
31 | dueDate: 'dueDate',
32 | };
33 |
34 | export interface ExtendedSearchParamsOptions extends SearchParamsOptions {
35 | view?: 'board' | 'list';
36 | status: 'incomplete' | 'pending' | 'completed';
37 | }
38 |
39 | export type FilterOption = keyof ExtendedSearchParamsOptions;
40 |
--------------------------------------------------------------------------------
/client/lib/util/open-ai.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { Task } from '@/types';
4 |
5 | const OpenAI = require('openai');
6 |
7 | export type PromptTask = Pick<
8 | Task,
9 | 'name' | 'description' | 'dueDate' | 'priority'
10 | > & { dueDate: string | null };
11 |
12 | export async function sendPrompt(userInput: string) {
13 | if (!userInput) throw new Error('prompt cannot be empty');
14 | const prompt = `
15 | Create a task object based on given input "${userInput}". Rephrase the user's to enhance the task's name and description. Given a date expression, if it represents a relative time, calculate the distance from the current date using ${new Date().toISOString()}. Otherwise, set the due date to null. ONLY reply with the JSON Object
16 | {
17 | "name": string,
18 | "description": string,
19 | "dueDate": isostring, (nullable)
20 | "priority": "LOW" | "MEDIUM" | "HIGH" (nullable)
21 | }
22 | `;
23 |
24 | const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
25 | const completion = await openai.chat.completions.create({
26 | messages: [{ role: 'system', content: prompt }],
27 | model: 'gpt-3.5-turbo',
28 | });
29 |
30 | const jsonString = completion.choices[0].message.content;
31 |
32 | let responseObject;
33 |
34 | try {
35 | responseObject = JSON.parse(jsonString);
36 | } catch (error) {
37 | responseObject = jsonString;
38 | }
39 |
40 | return responseObject as PromptTask;
41 | }
42 |
--------------------------------------------------------------------------------
/client/lib/util/tw-merge.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/client/lib/validations/auth-schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from 'zod';
2 |
3 | export const loginFormSchema = z.object({
4 | email: z.string().email('Invalid email'),
5 | password: z
6 | .string()
7 | .min(6, 'Password should be at least 6 characters long')
8 | .max(20, 'Password is too long (maximum 20 characters)'),
9 | });
10 |
11 | export type LoginFormValues = z.infer;
12 |
13 | export const registerFormSchema = z
14 | .object({
15 | email: z.string().email('Invalid email'),
16 | name: z
17 | .string()
18 | .min(2, 'Name should be at least 2 characters long')
19 | .max(15, 'Name is too long (maximum 15 characters)'),
20 | password: z
21 | .string()
22 | .min(6, 'Password should be at least 6 characters long')
23 | .max(20, 'Password is too long (maximum 20 characters)'),
24 | confirmPassword: z
25 | .string()
26 | .min(6, 'Confirm Password should be at least 6 characters long')
27 | .max(20, 'Confirm Password is too long (maximum 20 characters)'),
28 | })
29 | .refine((data) => data.password === data.confirmPassword, {
30 | path: ['confirmPassword'],
31 | message: 'Passwords do not match',
32 | });
33 |
34 | export type RegisterFormValues = z.infer;
35 |
--------------------------------------------------------------------------------
/client/lib/validations/label-schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from 'zod';
2 |
3 | export const labelFormSchema = z.object({
4 | name: z.string().min(2),
5 | color: z.string().min(2),
6 | });
7 |
8 | export type LabelFormValues = z.infer;
9 |
--------------------------------------------------------------------------------
/client/lib/validations/list-schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from 'zod';
2 |
3 | export const listFormSchema = z.object({
4 | name: z.string().min(2).max(15),
5 | });
6 |
7 | export type ListFormValues = z.infer;
8 |
--------------------------------------------------------------------------------
/client/lib/validations/subtask-schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from 'zod';
2 |
3 | export const subtaskFormSchema = z.object({
4 | name: z
5 | .string()
6 | .min(1, {
7 | message: 'Name must be at least 2 characters.',
8 | })
9 | .max(30, {
10 | message: 'Name must not be longer than 30 characters.',
11 | }),
12 | });
13 |
14 | export type SubtaskFormValues = z.infer;
15 |
--------------------------------------------------------------------------------
/client/lib/validations/task-schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from 'zod';
2 |
3 | export const taskFormSchema = z.object({
4 | name: z.string().min(1),
5 | description: z.string().optional(),
6 | dueDate: z.union([z.date(), z.string()]).optional(),
7 | priority: z.enum(['LOW', 'MEDIUM', 'HIGH']).optional(),
8 | listId: z.string().optional(),
9 | labelIds: z.array(z.string()).optional(),
10 | });
11 |
12 | export type TaskFormValues = z.infer;
13 |
--------------------------------------------------------------------------------
/client/mdx-components.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import type { MDXComponents } from 'mdx/types';
4 | import { kebabCase } from 'lodash';
5 |
6 | function h3({ children }: { children: React.ReactNode }) {
7 | return {children} ;
8 | }
9 |
10 | export function useMDXComponents(components: MDXComponents): MDXComponents {
11 | return {
12 | // @ts-expect-error
13 | h3,
14 | ...components,
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/client/middleware.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth';
2 | import authConfig from '@/auth.config';
3 | import {
4 | DEFAULT_LOGIN_REDIRECT,
5 | apiAuthPrefix,
6 | publicRoutes,
7 | authRoutes,
8 | } from './routes';
9 |
10 | const { auth } = NextAuth(authConfig);
11 |
12 | export default auth((req) => {
13 | const { nextUrl } = req;
14 | const isLoggedIn = !!req.auth;
15 | const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix);
16 | const isPublicRoute = publicRoutes.includes(nextUrl.pathname);
17 | const isAuthRoute = authRoutes.includes(nextUrl.pathname);
18 |
19 | if (isApiAuthRoute) {
20 | return null;
21 | }
22 |
23 | if (isAuthRoute) {
24 | if (isLoggedIn) {
25 | return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl));
26 | }
27 | return null;
28 | }
29 |
30 | if (!isLoggedIn && !isPublicRoute) {
31 | return Response.redirect(new URL('/auth/login', nextUrl));
32 | }
33 |
34 | return null;
35 | });
36 |
37 | // Optionally, don't invoke Middleware on some paths
38 | export const config = {
39 | matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
40 | };
41 |
--------------------------------------------------------------------------------
/client/next.config.mjs:
--------------------------------------------------------------------------------
1 | import nextMDX from '@next/mdx';
2 |
3 | const withMDX = nextMDX({
4 | extension: /\.mdx?$/,
5 | options: {
6 | remarkPlugins: [],
7 | rehypePlugins: [],
8 | },
9 | });
10 |
11 | /** @type {import('next').NextConfig} */
12 | const nextConfig = {
13 | pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
14 | experimental: {
15 | mdxRs: true,
16 | },
17 | images: {
18 | remotePatterns: [
19 | {
20 | protocol: 'https',
21 | hostname: 'github.com',
22 | pathname: '**',
23 | },
24 | {
25 | protocol: 'https',
26 | hostname: 'lh3.googleusercontent.com',
27 | pathname: '**',
28 | },
29 | {
30 | protocol: 'https',
31 | hostname: 'avatars.githubusercontent.com',
32 | pathname: '**',
33 | },
34 | ],
35 | },
36 | };
37 |
38 | export default withMDX(nextConfig);
39 |
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
--------------------------------------------------------------------------------
/client/public/hero-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/client/public/hero-1.png
--------------------------------------------------------------------------------
/client/public/keyboard-shortcuts.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/client/public/keyboard-shortcuts.gif
--------------------------------------------------------------------------------
/client/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/noise.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/client/public/noise.webp
--------------------------------------------------------------------------------
/client/public/static/bg-blur-1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/client/public/static/bg-blur-1.webp
--------------------------------------------------------------------------------
/client/public/static/bg-blur-2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/client/public/static/bg-blur-2.webp
--------------------------------------------------------------------------------
/client/public/static/bg-blur-3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/client/public/static/bg-blur-3.webp
--------------------------------------------------------------------------------
/client/public/static/bg-blur-4.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/client/public/static/bg-blur-4.webp
--------------------------------------------------------------------------------
/client/public/static/bg-blur-5.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mariangle/taskify/53ad8b9c236f5f0425a2572ed6c5ee18047f5e1a/client/public/static/bg-blur-5.webp
--------------------------------------------------------------------------------
/client/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/routes.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * An array of routes that are accessible to the public
3 | * These routes do not require authentication
4 | * @type {string[]}
5 | */
6 | export const publicRoutes = ['/', '/auth/new-verification', '/docs/features'];
7 |
8 | /**
9 | * An array of routes that are used for authentication
10 | * These routes will redirect logged in users to /settings
11 | * @type {string[]}
12 | */
13 | export const authRoutes = [
14 | '/auth/login',
15 | '/auth/register',
16 | '/auth/error',
17 | '/auth/reset',
18 | '/auth/new-password',
19 | ];
20 |
21 | /**
22 | * The prefix for API authentication routes
23 | * Routes that start with this prefix are used for API authentication purposes
24 | * @type {string}
25 | */
26 | export const apiAuthPrefix = '/api';
27 |
28 | /**
29 | * The default redirect path after logging in
30 | * @type {string}
31 | */
32 | export const DEFAULT_LOGIN_REDIRECT = '/inbox';
33 | export const LOGIN_PATH = '/auth/login';
34 |
--------------------------------------------------------------------------------
/client/services/label-service.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-useless-catch */
2 | import { AxiosResponse } from 'axios';
3 | import { api } from '@/lib/api';
4 | import { Label, LabelEntry } from '@/types';
5 |
6 | export const LabelService = {
7 | createLabel: async (label: LabelEntry): Promise => {
8 | try {
9 | const response: AxiosResponse = await api.post('/labels', label);
10 | return response.data;
11 | } catch (error) {
12 | throw error;
13 | }
14 | },
15 | getLabels: async (): Promise => {
16 | try {
17 | const response: AxiosResponse = await api.get(`/labels`);
18 | return response.data;
19 | } catch (error) {
20 | throw error;
21 | }
22 | },
23 | getLabel: async (labelId: string): Promise => {
24 | try {
25 | const response: AxiosResponse = await api.get(`/labels/${labelId}`);
26 | return response.data;
27 | } catch (error) {
28 | throw error;
29 | }
30 | },
31 | updateLabel: async (
32 | labelId: string,
33 | updatedLabel: LabelEntry,
34 | ): Promise => {
35 | try {
36 | const response: AxiosResponse = await api.patch(
37 | `/labels/${labelId}`,
38 | updatedLabel,
39 | );
40 | return response.data;
41 | } catch (error) {
42 | throw error;
43 | }
44 | },
45 |
46 | deleteLabel: async (labelId: string): Promise => {
47 | try {
48 | const response: AxiosResponse = await api.delete(`/labels/${labelId}`);
49 | return response.data;
50 | } catch (error: any) {
51 | throw error;
52 | }
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/client/services/list-service.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-useless-catch */
2 | import { AxiosResponse } from 'axios';
3 | import { List } from '@/types';
4 | import { api } from '@/lib/api';
5 |
6 | export const ListService = {
7 | createList: async (list: any): Promise => {
8 | try {
9 | const response: AxiosResponse = await api.post('/lists', list);
10 | return response.data;
11 | } catch (error) {
12 | throw error;
13 | }
14 | },
15 | getLists: async (): Promise => {
16 | try {
17 | const response = await api.get('/lists');
18 | return response.data;
19 | } catch (error) {
20 | throw error;
21 | }
22 | },
23 |
24 | getList: async (listId: string): Promise => {
25 | try {
26 | const response: AxiosResponse = await api.get(`/lists/${listId}`);
27 | return response.data;
28 | } catch (error) {
29 | throw error;
30 | }
31 | },
32 |
33 | updateList: async (listId: string, updatedList: any): Promise => {
34 | try {
35 | const response: AxiosResponse = await api.patch(
36 | `/lists/${listId}`,
37 | updatedList,
38 | );
39 | return response.data;
40 | } catch (error) {
41 | throw error;
42 | }
43 | },
44 |
45 | deleteList: async (listId: string): Promise => {
46 | try {
47 | const response: AxiosResponse = await api.delete(`/lists/${listId}`);
48 | return response.data;
49 | } catch (error: any) {
50 | throw error;
51 | }
52 | },
53 | };
54 |
--------------------------------------------------------------------------------
/client/services/subtask-service.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-useless-catch */
2 | import { AxiosResponse } from 'axios';
3 | import { api } from '@/lib/api';
4 | import { Subtask, SubtaskEntry } from '@/types';
5 |
6 | export const SubtaskService = {
7 | createSubtask: async (
8 | taskId: string,
9 | subtask: SubtaskEntry,
10 | ): Promise => {
11 | try {
12 | const response: AxiosResponse = await api.post(
13 | `/tasks/${taskId}/subtasks`,
14 | subtask,
15 | );
16 | return response.data;
17 | } catch (err) {
18 | throw err;
19 | }
20 | },
21 | updateSubtask: async (
22 | taskId: string,
23 | subtaskId: string,
24 | subtask: Subtask,
25 | ): Promise => {
26 | try {
27 | const response: AxiosResponse = await api.patch(
28 | `/tasks/${taskId}/subtasks/${subtaskId}`,
29 | subtask,
30 | );
31 | return response.data;
32 | } catch (err) {
33 | throw err;
34 | }
35 | },
36 | deleteSubtask: async (taskId: string, subtaskId: string): Promise => {
37 | try {
38 | const response: AxiosResponse = await api.delete(
39 | `/tasks/${taskId}/subtasks/${subtaskId}`,
40 | );
41 | return response.data;
42 | } catch (err) {
43 | throw err;
44 | }
45 | },
46 | };
47 |
--------------------------------------------------------------------------------
/client/store/layout-store.tsx:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 |
3 | interface InterfaceStore {
4 | showLeftSidebar: boolean;
5 | showSettingsOverlay: boolean;
6 | showTaskOverlay: boolean;
7 | toggleSettingsOverlay: () => void;
8 | toggleLeftSidebar: () => void;
9 | toggleTaskOverlay: () => void;
10 | closeTaskOverlay: () => void;
11 | setTaskOverlay: (open: boolean) => void;
12 | setSettingsOverlay: (open: boolean) => void;
13 | }
14 |
15 | export const useLayoutStore = create((set) => ({
16 | showLeftSidebar: true,
17 | showSettingsOverlay: false,
18 | showTaskOverlay: false,
19 | toggleLeftSidebar: () =>
20 | set((state) => ({ showLeftSidebar: !state.showLeftSidebar })),
21 | toggleSettingsOverlay: () =>
22 | set((state) => ({ showSettingsOverlay: !state.showSettingsOverlay })),
23 | toggleTaskOverlay: () =>
24 | set((state) => ({ showTaskOverlay: !state.showTaskOverlay })),
25 | closeTaskOverlay: () => set(() => ({ showTaskOverlay: false })),
26 | setTaskOverlay: (open: boolean) => set(() => ({ showTaskOverlay: open })),
27 | setSettingsOverlay: (open: boolean) =>
28 | set(() => ({ showSettingsOverlay: open })),
29 | }));
30 |
--------------------------------------------------------------------------------
/client/store/modal-store.tsx:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { Task } from '@/types';
3 |
4 | interface TaskStore {
5 | task: Task | null;
6 | setTask: (task: Task) => void;
7 | }
8 |
9 | export const useTaskStore = create((set) => ({
10 | task: null,
11 | setTask: (newTask: Task) => set({ task: newTask }),
12 | }));
13 |
--------------------------------------------------------------------------------
/client/store/settings-store.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { persist, createJSONStorage } from 'zustand/middleware';
3 |
4 | import type { SidebarItem, WidgetItem } from '@/lib/constants';
5 |
6 | type Mode = 'light' | 'dark';
7 |
8 | interface Settings {
9 | theme: Mode;
10 | sidebar: SidebarItem[];
11 | widgets: WidgetItem[];
12 | }
13 |
14 | interface SettingsStore {
15 | settings: Settings;
16 | setSettings: (settings: Settings) => void;
17 | }
18 |
19 | export const useSettingsStore = create(
20 | persist(
21 | (set, get) => ({
22 | settings: { theme: 'dark' as Mode, sidebar: [], widgets: [] },
23 | setSettings: (settings: Settings) => {
24 | set({ settings });
25 | },
26 | }),
27 | {
28 | name: 'settings-storage',
29 | storage: createJSONStorage(() => localStorage),
30 | },
31 | ),
32 | );
33 |
--------------------------------------------------------------------------------
/client/styles/fonts.ts:
--------------------------------------------------------------------------------
1 | import { Inter } from 'next/font/google'
2 |
3 | export const inter = Inter({
4 | variable: '--font-inter',
5 | subsets: ['latin'],
6 | })
7 |
--------------------------------------------------------------------------------
/client/styles/styles.ts:
--------------------------------------------------------------------------------
1 | export const inputStyle = {
2 | control: {
3 | padding: '9px 9px 0px 9px',
4 | },
5 | highlighter: {
6 | border: '1px solid transparent',
7 | padding: '0px',
8 | margin: '0px',
9 | },
10 | input: {
11 | padding: '9px',
12 | outline: 'none',
13 | margin: '0px 0px 0px 2px',
14 | },
15 | suggestions: {
16 | backgroundColor: 'transparent',
17 | width: '100%',
18 | maxWidth: '150px',
19 | list: {
20 | backgroundColor: 'hsl(var(--background))',
21 | border: '1px solid hsl(var(--border))',
22 | fontSize: 14,
23 | borderRadius: '10px',
24 | overflow: 'hidden',
25 | boxShadow: '0px 3px 10px rgba(0, 0, 0, 0.2)',
26 | },
27 | item: {
28 | padding: '5px 15px',
29 | '&focused': {
30 | backgroundColor: 'hsl(var(--border))',
31 | },
32 | },
33 | },
34 | }
35 |
36 | export const toastStyles = {
37 | blank: {
38 | style: {
39 | background: 'hsl(var(--background))',
40 | color: 'hsl(var(--foreground))',
41 | border: '1px solid hsl(var(--border))',
42 | },
43 | },
44 | success: {
45 | iconTheme: {
46 | primary: '#00a859',
47 | secondary: 'white',
48 | },
49 | style: {
50 | background: 'hsl(var(--background))',
51 | color: 'hsl(var(--foreground))',
52 | border: '1px solid hsl(var(--border))',
53 | },
54 | },
55 | error: {
56 | iconTheme: {
57 | primary: 'white',
58 | secondary: '#ff6161',
59 | },
60 | style: {
61 | background: 'hsl(var(--destructive))',
62 | color: 'white',
63 | },
64 | },
65 | }
66 |
--------------------------------------------------------------------------------
/client/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 |
3 | module.exports = {
4 | darkMode: ['class'],
5 | content: ['./pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}'],
6 | theme: {
7 | container: {
8 | center: true,
9 | padding: '2rem',
10 | screens: {
11 | '2xl': '1400px',
12 | },
13 | },
14 | extend: {
15 | fontFamily: {
16 | inter: ['var(--font-inter)'],
17 | },
18 | colors: {
19 | border: 'hsl(var(--border))',
20 | input: 'hsl(var(--input))',
21 | ring: 'hsl(var(--ring))',
22 | background: {
23 | DEFAULT: 'hsl(var(--background))',
24 | secondary: 'hsl(var(--background-secondary))',
25 | },
26 | foreground: 'hsl(var(--foreground))',
27 | primary: {
28 | DEFAULT: 'hsl(var(--primary))',
29 | foreground: 'hsl(var(--primary-foreground))',
30 | },
31 | secondary: {
32 | DEFAULT: 'hsl(var(--secondary))',
33 | foreground: 'hsl(var(--secondary-foreground))',
34 | },
35 | destructive: {
36 | DEFAULT: 'hsl(var(--destructive))',
37 | foreground: 'hsl(var(--destructive-foreground))',
38 | },
39 | muted: {
40 | DEFAULT: 'hsl(var(--muted))',
41 | foreground: 'hsl(var(--muted-foreground))',
42 | },
43 | accent: {
44 | DEFAULT: 'hsl(var(--accent))',
45 | foreground: 'hsl(var(--accent-foreground))',
46 | },
47 | popover: {
48 | DEFAULT: 'hsl(var(--popover))',
49 | foreground: 'hsl(var(--popover-foreground))',
50 | },
51 | card: {
52 | DEFAULT: 'hsl(var(--card))',
53 | foreground: 'hsl(var(--card-foreground))',
54 | },
55 | theme: {
56 | DEFAULT: '#06b6d4',
57 |
58 | 200: '#a5f3fc',
59 | 300: '#67e8f9',
60 | 400: '#22d3ee',
61 | 500: '#06b6d4',
62 | 600: '#0891b2',
63 | 700: '#0e7490',
64 | 800: '#155e75',
65 | 900: '#164e63',
66 | },
67 | },
68 | borderRadius: {
69 | lg: 'var(--radius)',
70 | md: 'calc(var(--radius) - 2px)',
71 | sm: 'calc(var(--radius) - 4px)',
72 | },
73 | keyframes: {
74 | 'accordion-down': {
75 | from: { height: 0 },
76 | to: { height: 'var(--radix-accordion-content-height)' },
77 | },
78 | 'accordion-up': {
79 | from: { height: 'var(--radix-accordion-content-height)' },
80 | to: { height: 0 },
81 | },
82 | },
83 | animation: {
84 | 'accordion-down': 'accordion-down 0.2s ease-out',
85 | 'accordion-up': 'accordion-up 0.2s ease-out',
86 | },
87 | },
88 | },
89 | plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
90 | }
91 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": [
26 | "next-env.d.ts",
27 | "**/*.ts",
28 | "**/*.tsx",
29 | ".next/types/**/*.ts",
30 | "components/fetch.jsx"
31 | ], // "lib/validations/auth-ts"
32 | "exclude": ["node_modules"]
33 | }
34 |
--------------------------------------------------------------------------------
/client/types.ts:
--------------------------------------------------------------------------------
1 | import type { Task as PrismaTask, Label, Subtask } from '@prisma/client';
2 |
3 | export type Task = PrismaTask & {
4 | labels?: Label[];
5 | subtasks?: Subtask[];
6 | };
7 |
8 | export type TaskEntry = Partial & { name: string };
9 | export type SubtaskEntry = Partial & { name: string };
10 | export type LabelEntry = Partial & { name: string };
11 | export type { User, List, Label, Subtask, TaskPriority } from '@prisma/client';
12 |
--------------------------------------------------------------------------------
/server/Context/ApplicationContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using server.Models;
3 |
4 | namespace server.Context
5 | {
6 | public class ApplicationContext : DbContext
7 | {
8 | public ApplicationContext(DbContextOptions options)
9 | : base(options)
10 | {
11 | }
12 | public DbSet Users { get; set; }
13 |
14 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
15 | {
16 | var configuration = new ConfigurationBuilder()
17 | .SetBasePath(Directory.GetCurrentDirectory())
18 | .AddJsonFile("appsettings.json")
19 | .Build();
20 |
21 | var connectionString = configuration.GetConnectionString("DefaultConnection");
22 | optionsBuilder.UseSqlServer(connectionString);
23 | }
24 |
25 | public DbSet? Tasks { get; set; }
26 |
27 | public DbSet? Subtasks { get; set; }
28 |
29 | public DbSet? RecurringTasks { get; set; }
30 |
31 | public DbSet? Lists { get; set; }
32 |
33 | public DbSet? Labels { get; set; }
34 |
35 | public DbSet? Projects { get; set; }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/server/Migrations/20231125122843_CascadeRelation.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | #nullable disable
4 |
5 | namespace server.Migrations
6 | {
7 | ///
8 | public partial class CascadeRelation : Migration
9 | {
10 | ///
11 | protected override void Up(MigrationBuilder migrationBuilder)
12 | {
13 |
14 | }
15 |
16 | ///
17 | protected override void Down(MigrationBuilder migrationBuilder)
18 | {
19 |
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/server/Migrations/20231202101035_UsernameToEmail.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | #nullable disable
4 |
5 | namespace server.Migrations
6 | {
7 | ///
8 | public partial class UsernameToEmail : Migration
9 | {
10 | ///
11 | protected override void Up(MigrationBuilder migrationBuilder)
12 | {
13 | migrationBuilder.RenameColumn(
14 | name: "Username",
15 | table: "Users",
16 | newName: "Email");
17 | }
18 |
19 | ///
20 | protected override void Down(MigrationBuilder migrationBuilder)
21 | {
22 | migrationBuilder.RenameColumn(
23 | name: "Email",
24 | table: "Users",
25 | newName: "Username");
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/server/Migrations/20231203185027_AddTaskProps.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 |
4 | #nullable disable
5 |
6 | namespace server.Migrations
7 | {
8 | ///
9 | public partial class AddTaskProps : Migration
10 | {
11 | ///
12 | protected override void Up(MigrationBuilder migrationBuilder)
13 | {
14 | migrationBuilder.AddColumn(
15 | name: "CompletedAt",
16 | table: "Tasks",
17 | type: "datetime2",
18 | nullable: false,
19 | defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
20 |
21 | migrationBuilder.AddColumn(
22 | name: "CreatedAt",
23 | table: "Tasks",
24 | type: "datetime2",
25 | nullable: false,
26 | defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
27 | }
28 |
29 | ///
30 | protected override void Down(MigrationBuilder migrationBuilder)
31 | {
32 | migrationBuilder.DropColumn(
33 | name: "CompletedAt",
34 | table: "Tasks");
35 |
36 | migrationBuilder.DropColumn(
37 | name: "CreatedAt",
38 | table: "Tasks");
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/server/Migrations/20231217235849_RenameIsCompleted.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Migrations;
2 |
3 | #nullable disable
4 |
5 | namespace server.Migrations
6 | {
7 | ///
8 | public partial class RenameIsCompleted : Migration
9 | {
10 | ///
11 | protected override void Up(MigrationBuilder migrationBuilder)
12 | {
13 | migrationBuilder.RenameColumn(
14 | name: "isCompleted",
15 | table: "Subtasks",
16 | newName: "IsCompleted");
17 | }
18 |
19 | ///
20 | protected override void Down(MigrationBuilder migrationBuilder)
21 | {
22 | migrationBuilder.RenameColumn(
23 | name: "IsCompleted",
24 | table: "Subtasks",
25 | newName: "isCompleted");
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/server/Models/Enums.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace server.Models
4 | {
5 | [JsonConverter(typeof(JsonStringEnumConverter))]
6 | public enum Status
7 | {
8 | Incomplete,
9 | InProgress,
10 | Completed
11 | }
12 |
13 | [JsonConverter(typeof(JsonStringEnumConverter))]
14 | public enum Priority
15 | {
16 | Low,
17 | Medium,
18 | High
19 | }
20 |
21 | public enum NoteType
22 | {
23 |
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/server/Models/Label.cs:
--------------------------------------------------------------------------------
1 | namespace server.Models
2 | {
3 | public class Label
4 | {
5 | public Guid Id { get; set; }
6 | public Guid UserId { get; set; }
7 | public string Name { get; set; } = string.Empty;
8 | public string? Color { get; set; }
9 | public List? Tasks { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/server/Models/List.cs:
--------------------------------------------------------------------------------
1 | namespace server.Models
2 | {
3 | public class List
4 | {
5 | public Guid Id { get; set; }
6 | public Guid UserId { get; set; }
7 | public string Name { get; set; } = string.Empty;
8 | public string? Emoji { get; set; } = string.Empty;
9 | public List? Tasks { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/server/Models/Project.cs:
--------------------------------------------------------------------------------
1 | namespace server.Models
2 | {
3 | public class Project
4 | {
5 | public Guid Id { get; set; }
6 | public Guid UserId { get; set; }
7 | public string Name { get; set; } = string.Empty;
8 | public List? Tasks { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/server/Models/RecurringTask.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace server.Models
4 | {
5 | public class RecurringTask
6 | {
7 | [Key]
8 | public Guid Id { get; set; }
9 | public Guid TaskId { get; set; }
10 | public string Frequency { get; set; } = string.Empty;
11 | public int? Interval { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/server/Models/SeedData.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using server.Context;
3 |
4 | namespace server.Models
5 | {
6 | public static class SeedData
7 | {
8 | public static void Initialize(IServiceProvider serviceProvider)
9 | {
10 | using (var context = new ApplicationContext(
11 | serviceProvider.GetRequiredService<
12 | DbContextOptions>()))
13 | {
14 | // Look for any surfboards
15 | if (context.Users.Any())
16 | {
17 | return; // DB has been seeded
18 | }
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/server/Models/Subtask.cs:
--------------------------------------------------------------------------------
1 | namespace server.Models
2 | {
3 | public class Subtask
4 | {
5 | public Guid Id { get; set; }
6 | public Guid TaskId { get; set; }
7 | public Guid UserId { get; set; }
8 | public string Name { get; set; } = string.Empty;
9 | public bool IsCompleted { get; set; } = false;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/server/Models/Task.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.ComponentModel.DataAnnotations.Schema;
3 |
4 | namespace server.Models
5 | {
6 |
7 | public class Task
8 | {
9 | public Guid Id { get; set; }
10 | public Guid UserId { get; set; }
11 | public Guid? ListId { get; set; }
12 | public DateTime CreatedAt { get; set; } = DateTime.Now;
13 | public DateTime CompletedAt { get; set; }
14 | public Guid? ProjectId { get; set; }
15 | public string Name { get; set; } = string.Empty;
16 | public string? Note { get; set; } = string.Empty;
17 | public DateTime? DueDate { get; set; }
18 | public TimeSpan? Duration { get; set; }
19 | public Priority? Priority { get; set; }
20 | public Status Status { get; set; } = Status.Incomplete;
21 | public List? List { get; set; }
22 | public User? User { get; set; }
23 | public List? Subtasks { get; set; }
24 | public List? Labels { get; set; }
25 | public RecurringTask? Recurring { get; set; }
26 | public Project? Project { get; set; }
27 |
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/server/Models/User.cs:
--------------------------------------------------------------------------------
1 | namespace server.Models
2 | {
3 | public class User
4 | {
5 | public Guid Id { get; set; }
6 | public string Email { get; set; } = string.Empty;
7 | public string? Image { get; set; }
8 | public string Name { get; set; } = string.Empty;
9 | public byte[] PasswordHash { get; set; }
10 | public byte[] PasswordSalt { get; set; }
11 | public List? Tasks { get; set; }
12 | public List? Labels { get; set; }
13 | public List? Projects { get; set; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/Models/UserDto.cs:
--------------------------------------------------------------------------------
1 | namespace server.Models
2 | {
3 | public class UserDto
4 | {
5 | public string Email { get; set; }
6 | public string? Name { get; set; }
7 | public string Password { get; set; }
8 |
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/server/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:36186",
8 | "sslPort": 44347
9 | }
10 | },
11 | "profiles": {
12 | "server": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "https://localhost:7232;http://localhost:5152",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | },
22 | "IIS Express": {
23 | "commandName": "IISExpress",
24 | "launchBrowser": true,
25 | "launchUrl": "swagger",
26 | "environmentVariables": {
27 | "ASPNETCORE_ENVIRONMENT": "Development"
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/server/Services/IUserService.cs:
--------------------------------------------------------------------------------
1 | using server.Models;
2 |
3 | namespace server.Services
4 | {
5 | public interface IUserService
6 | {
7 | UserDto GetCurrentUser();
8 |
9 | Guid GetUserId();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/server/Services/UserService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using server.Models;
3 | using System.Security.Claims;
4 |
5 | namespace server.Services
6 | {
7 | public class UserService : IUserService
8 | {
9 | private readonly IHttpContextAccessor _httpContextAccessor;
10 |
11 | public UserService(IHttpContextAccessor httpContextAccessor, IConfiguration configuration)
12 | {
13 | _httpContextAccessor = httpContextAccessor;
14 | }
15 |
16 | public UserDto GetCurrentUser()
17 | {
18 | var userId = _httpContextAccessor.HttpContext.User.FindFirst("Id")?.Value;
19 | var parsedUserId = userId != null ? Guid.Parse(userId) : Guid.Empty;
20 |
21 | var email = _httpContextAccessor.HttpContext.User.FindFirst("Email")?.Value;
22 | var name = _httpContextAccessor.HttpContext.User.FindFirst("Name")?.Value;
23 |
24 | return new UserDto
25 | {
26 | Email = email,
27 | Name = name
28 | };
29 | }
30 |
31 | public Guid GetUserId()
32 | {
33 | var userId = _httpContextAccessor.HttpContext.User.FindFirst("Id")?.Value;
34 | var parsedUserId = userId != null ? Guid.Parse(userId) : Guid.Empty;
35 |
36 | return parsedUserId;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/server/Utility/AuthUtils.cs:
--------------------------------------------------------------------------------
1 | using System.IdentityModel.Tokens.Jwt;
2 | using System.Security.Claims;
3 | using System.Security.Cryptography;
4 | using Microsoft.IdentityModel.Tokens;
5 | using server.Models;
6 |
7 | namespace server.Utility
8 | {
9 | public class AuthUtils
10 | {
11 | private readonly IConfiguration _configuration;
12 |
13 | public AuthUtils(IConfiguration configuration)
14 | {
15 | _configuration = configuration;
16 | }
17 |
18 | public string CreateToken(User user)
19 | {
20 | List claims = new List
21 | {
22 | new Claim("Id", user.Id.ToString()),
23 | new Claim("Name", user.Name),
24 | new Claim("Email", user.Email),
25 | // new Claim(ClaimTypes.Role, "User")
26 | };
27 |
28 | var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(
29 | _configuration.GetSection("AppSettings:Token").Value));
30 |
31 | var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha512Signature);
32 |
33 | var token = new JwtSecurityToken(
34 | claims: claims,
35 | expires: DateTime.Now.AddDays(1),
36 | signingCredentials: creds);
37 |
38 | var jwt = new JwtSecurityTokenHandler().WriteToken(token);
39 |
40 | return jwt;
41 | }
42 |
43 | public void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
44 | {
45 | using (var hmac = new HMACSHA512())
46 | {
47 | passwordSalt = hmac.Key;
48 | passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
49 | }
50 | }
51 |
52 | public bool VerifyPasswordHash(string password, byte[] passwordHash, byte[] passwordSalt)
53 | {
54 | using (var hmac = new HMACSHA512(passwordSalt))
55 | {
56 | var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
57 | return computedHash.SequenceEqual(passwordHash);
58 | }
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/server/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/server/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "AppSettings": {
3 | "Token": "mySuperSecretKey123!#"
4 | },
5 | "Logging": {
6 | "LogLevel": {
7 | "Default": "Information",
8 | "Microsoft.AspNetCore": "Warning"
9 | }
10 | },
11 | "AllowedHosts": "*",
12 | "ConnectionStrings": {
13 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=TaskifyDB;Trusted_Connection=True;MultipleActiveResultSets=true"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/server.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 |
18 | all
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/server/server.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.5.33530.505
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "server", "server.csproj", "{50511427-76A4-48C6-8276-166A293EC312}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {50511427-76A4-48C6-8276-166A293EC312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {50511427-76A4-48C6-8276-166A293EC312}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {50511427-76A4-48C6-8276-166A293EC312}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {50511427-76A4-48C6-8276-166A293EC312}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {2D31F198-DFFB-4060-90A7-438315CC65C1}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------