├── .eslintrc.json ├── src ├── app │ ├── favicon.ico │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ └── edgestore │ │ │ └── [...edgestore] │ │ │ └── route.ts │ ├── auth │ │ ├── sign-in │ │ │ └── page.tsx │ │ ├── sign-up │ │ │ └── page.tsx │ │ ├── reset-password │ │ │ └── page.tsx │ │ ├── forgot-password │ │ │ └── page.tsx │ │ ├── email-verification │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (protected) │ │ ├── dashboard │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── tasks │ │ │ ├── [taskId] │ │ │ │ └── edit │ │ │ │ │ └── page.tsx │ │ │ ├── create │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── users │ │ │ └── page.tsx │ │ ├── projects │ │ │ ├── page.tsx │ │ │ └── [projectId] │ │ │ │ └── edit │ │ │ │ └── page.tsx │ │ └── account │ │ │ └── page.tsx │ └── layout.tsx ├── hooks │ ├── use-current-user.ts │ ├── use-current-role.ts │ ├── use-store.ts │ ├── use-debounce.ts │ └── use-sidebar-toggle.ts ├── lib │ ├── db.ts │ ├── authentication.ts │ ├── edgestore.ts │ ├── utils.ts │ ├── handle-error.ts │ ├── mail.ts │ ├── menu-list.ts │ └── tokens.ts ├── data │ ├── account.ts │ ├── two-factor-confirmation.ts │ ├── two-factor-token.ts │ ├── verification-token.ts │ ├── password-reset-token.ts │ ├── user.ts │ └── projects.ts ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── collapsible.tsx │ │ ├── label.tsx │ │ ├── textarea.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── sonner.tsx │ │ ├── checkbox.tsx │ │ ├── tooltip.tsx │ │ ├── switch.tsx │ │ ├── badge.tsx │ │ ├── popover.tsx │ │ ├── avatar.tsx │ │ ├── scroll-area.tsx │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── password-input.tsx │ │ ├── card.tsx │ │ ├── input-otp.tsx │ │ └── table.tsx │ ├── admin-panel │ │ ├── content-layout.tsx │ │ ├── footer.tsx │ │ ├── navbar.tsx │ │ ├── sidebar-toggle.tsx │ │ ├── admin-panel-layout.tsx │ │ ├── sheet-menu.tsx │ │ ├── sidebar.tsx │ │ └── user-nav.tsx │ ├── protected │ │ ├── tasks │ │ │ └── table │ │ │ │ ├── table.tsx │ │ │ │ ├── tasks-table-toolbar-actions.tsx │ │ │ │ ├── tasks-table-cell-actions.tsx │ │ │ │ ├── delete-tasks-dialog.tsx │ │ │ │ ├── tasks-table.tsx │ │ │ │ └── tasks-table-columns.tsx │ │ ├── users │ │ │ └── table │ │ │ │ ├── table.tsx │ │ │ │ ├── users-table-toolbar-actions.tsx │ │ │ │ ├── users-table.tsx │ │ │ │ ├── users-table-cell-actions.tsx │ │ │ │ └── delete-users-dialog.tsx │ │ ├── projects │ │ │ ├── table │ │ │ │ ├── table.tsx │ │ │ │ ├── projects-table.tsx │ │ │ │ ├── projects-table-toolbar-actions.tsx │ │ │ │ ├── projects-table-cell-actions.tsx │ │ │ │ ├── projects-table-columns.tsx │ │ │ │ └── delete-projects-dialog.tsx │ │ │ ├── edit-project-form.tsx │ │ │ ├── assigned-user-action.tsx │ │ │ └── project-users.tsx │ │ └── data-table-card.tsx │ ├── back-button.tsx │ ├── form-error.tsx │ ├── form-success.tsx │ ├── footer.tsx │ ├── mode-toggle.tsx │ ├── header.tsx │ ├── mdx-editor │ │ └── toolbar.tsx │ ├── data-table │ │ ├── data-table-view-options.tsx │ │ ├── data-table.tsx │ │ ├── data-table-column-header.tsx │ │ ├── data-table-pagination.tsx │ │ └── data-table-toolbar.tsx │ ├── auth │ │ ├── email-verification-form.tsx │ │ ├── social.tsx │ │ └── forgot-password-form.tsx │ └── emails │ │ ├── two-factor-authentication.tsx │ │ ├── email-verification.tsx │ │ └── password-reset.tsx ├── providers │ └── theme-provider.tsx ├── next-auth.d.ts ├── types │ └── index.ts ├── actions │ ├── projects │ │ ├── create-project.ts │ │ ├── edit-project.ts │ │ ├── delete-projects.ts │ │ ├── unassign-user.ts │ │ └── assign-user.ts │ ├── users │ │ ├── delete-users.ts │ │ ├── create-user.ts │ │ └── edit-user.ts │ ├── auth │ │ ├── forgot-password.ts │ │ ├── email-verification.ts │ │ ├── register.ts │ │ └── reset-password.ts │ ├── account │ │ ├── cancel-new-email.ts │ │ ├── edit-password.ts │ │ └── edit-profile.ts │ └── tasks │ │ └── create-task.ts ├── routes.ts ├── auth.config.ts ├── middleware.ts └── auth.ts ├── public ├── auth-image.png ├── taskify-logo.png ├── taskify-logo-light.png ├── vercel.svg └── next.svg ├── next.config.mjs ├── postcss.config.js ├── .vscode └── settings.json ├── components.json ├── .env.example ├── .gitignore ├── tsconfig.json ├── README.md └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/taskify/master/src/app/favicon.ico -------------------------------------------------------------------------------- /public/auth-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/taskify/master/public/auth-image.png -------------------------------------------------------------------------------- /public/taskify-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/taskify/master/public/taskify-logo.png -------------------------------------------------------------------------------- /public/taskify-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salimi-my/taskify/master/public/taskify-logo-light.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from '@/auth'; 2 | export const { GET, POST } = handlers; 3 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.jsxSingleQuote": true, 3 | "prettier.singleQuote": true, 4 | "prettier.trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /src/app/auth/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignInForm } from '@/components/auth/sign-in-form'; 2 | 3 | export default function SignInPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/auth/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUpForm } from '@/components/auth/sign-up-form'; 2 | 3 | export default function SignUpPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/hooks/use-current-user.ts: -------------------------------------------------------------------------------- 1 | import { useSession } from 'next-auth/react'; 2 | 3 | export function useCurrentUser() { 4 | const session = useSession(); 5 | 6 | return session.data?.user; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/auth/reset-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ResetPasswordForm } from '@/components/auth/reset-password-form'; 2 | 3 | export default function ResetPasswordPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/hooks/use-current-role.ts: -------------------------------------------------------------------------------- 1 | import { useSession } from 'next-auth/react'; 2 | 3 | export function useCurrentRole() { 4 | const session = useSession(); 5 | 6 | return session.data?.user?.role; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/auth/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ForgotPasswordForm } from '@/components/auth/forgot-password-form'; 2 | 3 | export default function ForgotPasswordPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/auth/email-verification/page.tsx: -------------------------------------------------------------------------------- 1 | import { EmailVerificationForm } from '@/components/auth/email-verification-form'; 2 | 3 | export default function EmailVerification() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | declare global { 4 | var prisma: PrismaClient | undefined; 5 | } 6 | 7 | export const db = globalThis.prisma || new PrismaClient(); 8 | 9 | if (process.env.NODE_ENV !== 'production') globalThis.prisma = db; 10 | -------------------------------------------------------------------------------- /src/app/(protected)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { ContentLayout } from '@/components/admin-panel/content-layout'; 2 | 3 | export default function DashboardPage() { 4 | return ( 5 | 6 |
DashboardPage
7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/authentication.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/auth'; 2 | 3 | export async function currentUser() { 4 | const session = await auth(); 5 | 6 | return session?.user; 7 | } 8 | 9 | export async function currentRole() { 10 | const session = await auth(); 11 | 12 | return session?.user?.role; 13 | } 14 | -------------------------------------------------------------------------------- /src/data/account.ts: -------------------------------------------------------------------------------- 1 | import { db } from '@/lib/db'; 2 | 3 | export async function getAccountByUserId(userId: string) { 4 | try { 5 | const account = await db.account.findUnique({ 6 | where: { 7 | userId 8 | } 9 | }); 10 | 11 | return account; 12 | } catch { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/edgestore.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { type EdgeStoreRouter } from '../app/api/edgestore/[...edgestore]/route'; 4 | import { createEdgeStoreProvider } from '@edgestore/react'; 5 | 6 | const { EdgeStoreProvider, useEdgeStore } = 7 | createEdgeStoreProvider(); 8 | 9 | export { EdgeStoreProvider, useEdgeStore }; 10 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /src/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 5 | import { type ThemeProviderProps } from 'next-themes/dist/types'; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/data/two-factor-confirmation.ts: -------------------------------------------------------------------------------- 1 | import { db } from '@/lib/db'; 2 | 3 | export async function getTwoFactorConfirmationByUserId(userId: string) { 4 | try { 5 | const twoFactorConfirmation = await db.twoFactorConfirmation.findUnique({ 6 | where: { 7 | userId 8 | } 9 | }); 10 | 11 | return twoFactorConfirmation; 12 | } catch { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/hooks/use-store.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export const useStore = ( 4 | store: (callback: (state: T) => unknown) => unknown, 5 | callback: (state: T) => F 6 | ) => { 7 | const result = store(callback) as F; 8 | const [data, setData] = useState(); 9 | 10 | useEffect(() => { 11 | setData(result); 12 | }, [result]); 13 | 14 | return data; 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; 4 | 5 | const Collapsible = CollapsiblePrimitive.Root; 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }; 12 | -------------------------------------------------------------------------------- /src/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { UserRole } from '@prisma/client'; 2 | import NextAuth, { type DefaultSession } from 'next-auth'; 3 | 4 | export type ExtendedUser = DefaultSession['user'] & { 5 | tempEmail: string | null; 6 | role: UserRole; 7 | isTwoFactorEnabled: boolean; 8 | isOAuth: boolean; 9 | }; 10 | 11 | declare module 'next-auth' { 12 | interface Session { 13 | user: ExtendedUser; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /src/app/(protected)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { EdgeStoreProvider } from '@/lib/edgestore'; 2 | import { AdminPanelLayout } from '@/components/admin-panel/admin-panel-layout'; 3 | 4 | export default function ProtectedLayout({ 5 | children 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface SearchParams { 2 | [key: string]: string | string[] | undefined; 3 | } 4 | 5 | export interface Option { 6 | label: string; 7 | value: string; 8 | icon?: React.ComponentType<{ className?: string }>; 9 | withCount?: boolean; 10 | } 11 | 12 | export interface DataTableFilterField { 13 | label: string; 14 | value: keyof TData; 15 | placeholder?: string; 16 | options?: Option[]; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/admin-panel/content-layout.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from '@/components/admin-panel/navbar'; 2 | 3 | interface ContentLayoutProps { 4 | title: string; 5 | children: React.ReactNode; 6 | } 7 | 8 | export function ContentLayout({ title, children }: ContentLayoutProps) { 9 | return ( 10 |
11 | 12 |
{children}
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/hooks/use-debounce.ts: -------------------------------------------------------------------------------- 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 | } 16 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { twMerge } from 'tailwind-merge'; 2 | import { type ClassValue, clsx } from 'clsx'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export function formatDate( 9 | date: Date | string | number, 10 | opts: Intl.DateTimeFormatOptions = {} 11 | ) { 12 | return new Intl.DateTimeFormat('en-GB', { 13 | day: opts.day ?? 'numeric', 14 | month: opts.month ?? 'long', 15 | year: opts.year ?? 'numeric', 16 | ...opts 17 | }).format(new Date(date)); 18 | } 19 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # App configuration 2 | APP_URL=http://localhost:3000 3 | 4 | # Prisma configurations 5 | DATABASE_URL=AddMySQLURL 6 | 7 | # Auth configurations 8 | AUTH_SECRET=AddAuthSecret 9 | AUTH_GOOGLE_ID=AddGoogleClientID 10 | AUTH_GOOGLE_SECRET=AddGoogleClientSecret 11 | AUTH_GITHUB_ID=AddGitHubClientID 12 | AUTH_GITHUB_SECRET=AddGitHubClientSecret 13 | 14 | # Resend configuration 15 | RESEND_API_KEY=AddResendAPIKey 16 | 17 | # EdgeStore configuration 18 | EDGE_STORE_ACCESS_KEY=AddEdgeStoreAccessKey 19 | EDGE_STORE_SECRET_KEY=AddEdgeStoreSecretKey 20 | -------------------------------------------------------------------------------- /src/app/(protected)/tasks/[taskId]/edit/page.tsx: -------------------------------------------------------------------------------- 1 | import { BackButton } from '@/components/back-button'; 2 | import { ContentLayout } from '@/components/admin-panel/content-layout'; 3 | 4 | export default async function EditTaskPage({ 5 | params 6 | }: { 7 | params: { taskId: string }; 8 | }) { 9 | return ( 10 | 11 | 15 | {/* TODO: Edit Task Form */} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /src/components/protected/tasks/table/table.tsx: -------------------------------------------------------------------------------- 1 | import { SearchParams } from '@/types'; 2 | import { getTasks } from '@/data/tasks'; 3 | import { TaskFilterSchema } from '@/schemas'; 4 | import { TasksTable } from '@/components/protected/tasks/table/tasks-table'; 5 | 6 | export interface TableProps { 7 | searchParams: SearchParams; 8 | } 9 | 10 | export function Table({ searchParams }: TableProps) { 11 | const search = TaskFilterSchema.parse(searchParams); 12 | 13 | const tasksPromise = getTasks(search); 14 | 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /src/components/protected/users/table/table.tsx: -------------------------------------------------------------------------------- 1 | import { SearchParams } from '@/types'; 2 | import { getUsers } from '@/data/users'; 3 | import { UserFilterSchema } from '@/schemas'; 4 | import { UsersTable } from '@/components/protected/users/table/users-table'; 5 | 6 | export interface TableProps { 7 | searchParams: SearchParams; 8 | } 9 | 10 | export function Table({ searchParams }: TableProps) { 11 | const search = UserFilterSchema.parse(searchParams); 12 | 13 | const usersPromise = getUsers(search); 14 | 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/use-sidebar-toggle.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { persist, createJSONStorage } from 'zustand/middleware'; 3 | 4 | interface useSidebarToggleStore { 5 | isOpen: boolean; 6 | setIsOpen: () => void; 7 | } 8 | 9 | export const useSidebarToggle = create( 10 | persist( 11 | (set, get) => ({ 12 | isOpen: true, 13 | setIsOpen: () => { 14 | set({ isOpen: !get().isOpen }); 15 | } 16 | }), 17 | { 18 | name: 'sidebarOpen', 19 | storage: createJSONStorage(() => localStorage) 20 | } 21 | ) 22 | ); 23 | -------------------------------------------------------------------------------- /src/components/protected/projects/table/table.tsx: -------------------------------------------------------------------------------- 1 | import { SearchParams } from '@/types'; 2 | import { getProjects } from '@/data/projects'; 3 | import { ProjectFilterSchema } from '@/schemas'; 4 | import { ProjectsTable } from '@/components/protected/projects/table/projects-table'; 5 | 6 | export interface TableProps { 7 | searchParams: SearchParams; 8 | } 9 | 10 | export function Table({ searchParams }: TableProps) { 11 | const search = ProjectFilterSchema.parse(searchParams); 12 | 13 | const projectsPromise = getProjects(search); 14 | 15 | return ; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/handle-error.ts: -------------------------------------------------------------------------------- 1 | import * as z from 'zod'; 2 | import { isRedirectError } from 'next/dist/client/components/redirect'; 3 | 4 | export function getErrorMessage(error: unknown) { 5 | const unknownError = 'Uh oh! Something went wrong.'; 6 | 7 | if (error instanceof z.ZodError) { 8 | const errors = error.issues.map((issue) => { 9 | return issue.message; 10 | }); 11 | return errors.join('\n'); 12 | } else if (error instanceof Error) { 13 | return error.message; 14 | } else if (isRedirectError(error)) { 15 | throw error; 16 | } else { 17 | return unknownError; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/data/two-factor-token.ts: -------------------------------------------------------------------------------- 1 | import { db } from '@/lib/db'; 2 | 3 | export async function getTwoFactorTokenByToken(token: string) { 4 | try { 5 | const twoFactorToken = await db.twoFactorToken.findUnique({ 6 | where: { 7 | token 8 | } 9 | }); 10 | 11 | return twoFactorToken; 12 | } catch { 13 | return null; 14 | } 15 | } 16 | 17 | export async function getTwoFactorTokenByEmail(email: string) { 18 | try { 19 | const twoFactorToken = await db.twoFactorToken.findFirst({ 20 | where: { 21 | email 22 | } 23 | }); 24 | 25 | return twoFactorToken; 26 | } catch { 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/back-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ArrowLeft } from 'lucide-react'; 4 | import { useRouter } from 'next/navigation'; 5 | 6 | import { Button } from '@/components/ui/button'; 7 | 8 | interface BackButtonProps { 9 | slug?: string; 10 | label?: string; 11 | } 12 | 13 | export function BackButton({ slug, label }: BackButtonProps) { 14 | const router = useRouter(); 15 | 16 | return ( 17 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/form-error.tsx: -------------------------------------------------------------------------------- 1 | import { AlertTriangle } from 'lucide-react'; 2 | 3 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; 4 | 5 | interface FormErrorProps { 6 | className?: string; 7 | message?: string; 8 | } 9 | 10 | export function FormError({ className, message }: FormErrorProps) { 11 | if (!message) return null; 12 | 13 | return ( 14 | 15 | 16 | 17 | Error 18 | 19 | {message} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/form-success.tsx: -------------------------------------------------------------------------------- 1 | import { CircleCheck } from 'lucide-react'; 2 | 3 | import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; 4 | 5 | interface FormSuccessProps { 6 | className?: string; 7 | message?: string; 8 | } 9 | 10 | export function FormSuccess({ className, message }: FormSuccessProps) { 11 | if (!message) return null; 12 | 13 | return ( 14 | 15 | 16 | 17 | Success 18 | 19 | {message} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/data/verification-token.ts: -------------------------------------------------------------------------------- 1 | import { db } from '@/lib/db'; 2 | 3 | export async function getVerificationTokenByToken(token: string) { 4 | try { 5 | const verificationToken = await db.verificationToken.findUnique({ 6 | where: { 7 | token 8 | } 9 | }); 10 | 11 | return verificationToken; 12 | } catch { 13 | return null; 14 | } 15 | } 16 | 17 | export async function getVerificationTokenByUserId(userId: string) { 18 | try { 19 | const verificationToken = await db.verificationToken.findFirst({ 20 | where: { 21 | userId 22 | } 23 | }); 24 | 25 | return verificationToken; 26 | } catch { 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/data/password-reset-token.ts: -------------------------------------------------------------------------------- 1 | import { db } from '@/lib/db'; 2 | 3 | export async function getPasswordResetTokenByToken(token: string) { 4 | try { 5 | const passwordResetToken = await db.passwordResetToken.findUnique({ 6 | where: { 7 | token 8 | } 9 | }); 10 | 11 | return passwordResetToken; 12 | } catch { 13 | return null; 14 | } 15 | } 16 | 17 | export async function getPasswordResetTokenByEmail(email: string) { 18 | try { 19 | const passwordResetToken = await db.passwordResetToken.findFirst({ 20 | where: { 21 | email 22 | } 23 | }); 24 | 25 | return passwordResetToken; 26 | } catch { 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "target": "es2015", 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 | "@/*": ["./src/*"], 23 | "@public/*": ["./public/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /src/components/admin-panel/footer.tsx: -------------------------------------------------------------------------------- 1 | export function Footer() { 2 | return ( 3 |
4 |
5 | 6 | © {new Date().getFullYear()} Taskify. Created by{' '} 7 | 14 | Salimi 15 | 16 | . All right reserved. 17 | 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/(protected)/tasks/create/page.tsx: -------------------------------------------------------------------------------- 1 | import { getAllUsers } from '@/data/users'; 2 | import { getAllProjects } from '@/data/projects'; 3 | import { BackButton } from '@/components/back-button'; 4 | import { ContentLayout } from '@/components/admin-panel/content-layout'; 5 | import { CreateTaskForm } from '@/components/protected/tasks/create-task-form'; 6 | 7 | export default async function CreateTaskPage() { 8 | const users = await getAllUsers(); 9 | const projects = await getAllProjects(); 10 | 11 | return ( 12 | 13 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/api/edgestore/[...edgestore]/route.ts: -------------------------------------------------------------------------------- 1 | import { initEdgeStore } from '@edgestore/server'; 2 | import { createEdgeStoreNextHandler } from '@edgestore/server/adapters/next/app'; 3 | 4 | const es = initEdgeStore.create(); 5 | 6 | /** 7 | * This is the main router for the Edge Store buckets. 8 | */ 9 | const edgeStoreRouter = es.router({ 10 | publicImages: es 11 | .imageBucket({ 12 | maxSize: 1024 * 1024 * 2 // 2MB 13 | }) 14 | .beforeDelete(() => { 15 | return true; 16 | }) 17 | }); 18 | 19 | const handler = createEdgeStoreNextHandler({ 20 | router: edgeStoreRouter 21 | }); 22 | 23 | export { handler as GET, handler as POST }; 24 | 25 | /** 26 | * This type is used to create the type-safe client for the frontend. 27 | */ 28 | export type EdgeStoreRouter = typeof edgeStoreRouter; 29 | -------------------------------------------------------------------------------- /src/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/utils'; 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 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |