From 28901128ff54c98b520133f452a8b1bf0e5f0562 Mon Sep 17 00:00:00 2001 From: rathi Date: Mon, 25 Nov 2024 01:24:37 -0500 Subject: [PATCH] changed a lot --- actions/login.ts | 45 +- actions/new-verification.ts | 67 +- app/(root)/(routes)/(auth)/login/page.tsx | 12 +- .../(routes)/(auth)/new-verification/page.tsx | 57 +- app/(root)/(routes)/dashboard/layout.tsx | 19 +- app/(root)/(routes)/dashboard/page.tsx | 426 ++++---- app/globals.css | 118 ++- app/page.tsx | 62 -- components/file-upload.tsx | 84 ++ components/header.tsx | 43 +- components/navbar.tsx | 16 +- components/project-select.tsx | 91 ++ components/rich-text-editor.tsx | 101 ++ components/task-card.tsx | 54 + components/task-column.tsx | 52 + components/ui/toggle.tsx | 42 + emails/verify-email.tsx | 44 +- package-lock.json | 943 +++++++++++++++++- package.json | 8 + routes.ts | 36 + 20 files changed, 1794 insertions(+), 526 deletions(-) create mode 100644 components/file-upload.tsx create mode 100644 components/project-select.tsx create mode 100644 components/rich-text-editor.tsx create mode 100644 components/task-card.tsx create mode 100644 components/task-column.tsx create mode 100644 components/ui/toggle.tsx create mode 100644 routes.ts diff --git a/actions/login.ts b/actions/login.ts index 84f8efa..108dd28 100644 --- a/actions/login.ts +++ b/actions/login.ts @@ -6,6 +6,7 @@ import { AuthError } from 'next-auth' import { generateVerificationToken } from '@/lib/tokens' import { getUserByEmail } from '@/data/user' import { sendVerificationEmail } from '@/lib/mail' +import bcrypt from 'bcryptjs' export const login = async (values: z.infer) => { // Validate fields @@ -15,47 +16,41 @@ export const login = async (values: z.infer) => { if (!validatedFields.success) { return { error: 'Invalid fields' } } - // If fields are valid - const { email, password } = validatedFields.data - const exisitingUser = await getUserByEmail(email) - if (!exisitingUser || !exisitingUser.email || !exisitingUser.password) { - return { error: 'Email does not exisit' } + const { email, password } = validatedFields.data + const existingUser = await getUserByEmail(email) + + if (!existingUser || !existingUser.email || !existingUser.password) { + return { error: 'Email does not exist' } } - if (!exisitingUser.emailVerified) { - const verificationToken = await generateVerificationToken( - exisitingUser.email - ) - + // Check if email is verified first + if (!existingUser.emailVerified) { + const verificationToken = await generateVerificationToken(email) await sendVerificationEmail( verificationToken.email, verificationToken.token ) + return { error: 'Please verify your email to login. Verification email sent!' } + } - return { success: 'Confirmation email sent!' } + // Verify password + const passwordsMatch = await bcrypt.compare(password, existingUser.password) + if (!passwordsMatch) { + return { error: 'Invalid credentials' } } try { - const result = await signIn('credentials', { - redirect: false, + await signIn('credentials', { email, - password + password, + redirect: false, }) - if (result?.error) { - return { error: result.error } - } - - return { success: 'Logged In!' } + return { success: 'Logged in successfully!' } } catch (error) { if (error instanceof AuthError) { - switch (error.type) { - case 'CredentialsSignin': - return { error: 'Invalid credentials' } - default: - return { error: 'Something went wrong' } - } + return { error: 'Something went wrong' } } throw error } diff --git a/actions/new-verification.ts b/actions/new-verification.ts index 1e9c1c8..98c3d79 100644 --- a/actions/new-verification.ts +++ b/actions/new-verification.ts @@ -2,40 +2,47 @@ import { db } from '@/lib/db' import { getUserByEmail } from '@/data/user' - import { getVerificationTokenByToken } from '@/data/verification-token' export const newVerification = async (token: string) => { - // if no token, display message - const exisitingToken = await getVerificationTokenByToken(token) + try { + const existingToken = await getVerificationTokenByToken(token) - if (!exisitingToken) { - return { error: 'Token does not exisit!' } - } - // if token has expired, display message - const hasExpired = new Date(exisitingToken.expires) < new Date() - - if (hasExpired) { - return { error: 'Token has expired!' } - } - // if user does not exist, display message - const existingUser = await getUserByEmail(exisitingToken.email) - - if (!existingUser) { - return { error: 'User does not exisit!' } - } - // update email value when they verify - await db.user.update({ - where: { id: existingUser.id }, - data: { - emailVerified: new Date(), - email: exisitingToken.email + if (!existingToken) { + return { error: 'Verification link is invalid!' } } - }) - // delete token - await db.verificationToken.delete({ - where: { id: exisitingToken.id } - }) - return { success: 'Email verified! Login to continue' } + const hasExpired = new Date(existingToken.expires) < new Date() + if (hasExpired) { + return { error: 'Verification link has expired! Please request a new one.' } + } + + const existingUser = await getUserByEmail(existingToken.email) + if (!existingUser) { + return { error: 'Email not found! Please sign up first.' } + } + + // If already verified, just return success + if (existingUser.emailVerified) { + return { success: 'Email already verified! Please login.' } + } + + // Update user verification status + await db.user.update({ + where: { id: existingUser.id }, + data: { + emailVerified: new Date(), + email: existingToken.email + } + }) + + // Delete the verification token + await db.verificationToken.delete({ + where: { id: existingToken.id } + }) + + return { success: 'Email verified successfully! You can now login.' } + } catch (error) { + return { error: 'Something went wrong! Please try again.' } + } } diff --git a/app/(root)/(routes)/(auth)/login/page.tsx b/app/(root)/(routes)/(auth)/login/page.tsx index daf2fb1..56ff4ae 100644 --- a/app/(root)/(routes)/(auth)/login/page.tsx +++ b/app/(root)/(routes)/(auth)/login/page.tsx @@ -15,13 +15,15 @@ import { Input } from '@/components/ui/input' import { CardWrapper } from '@/components/auth/card-wrapper' import { Button } from '@/components/ui/button' import Link from 'next/link' -import { useSearchParams } from 'next/navigation' +import { useSearchParams, useRouter } from 'next/navigation' import { useEffect, useRef, useTransition } from 'react' import { login } from '@/actions/login' import toast from 'react-hot-toast' export default function Page() { + const router = useRouter(); const searchParams = useSearchParams() + const callbackUrl = searchParams.get('callbackUrl') const urlError = searchParams.get('error') === 'OAuthAccountNotLinked' ? 'Email already in use with different provider!' @@ -52,8 +54,12 @@ export default function Page() { } if (data?.success) { toast.success(data.success) - form.reset({ email: '', password: '' }) - window.location.href = '/' + if (data.success === 'Confirmation email sent!') { + // Don't redirect if we're just sending a confirmation email + return; + } + router.push('/dashboard'); + router.refresh(); } }) }) diff --git a/app/(root)/(routes)/(auth)/new-verification/page.tsx b/app/(root)/(routes)/(auth)/new-verification/page.tsx index 10f75b5..2c19e6c 100644 --- a/app/(root)/(routes)/(auth)/new-verification/page.tsx +++ b/app/(root)/(routes)/(auth)/new-verification/page.tsx @@ -5,11 +5,11 @@ import { newVerification } from '@/actions/new-verification' import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useEffect, useState } from 'react' import { toast } from 'react-hot-toast' +import { Loader2 } from 'lucide-react' export default function NewVerificationForm() { const [error, setError] = useState() const [success, setSuccess] = useState() - const [hasErrorToastShown, setHasErrorToastShown] = useState(false) const searchParams = useSearchParams() const token = searchParams.get('token') @@ -17,25 +17,25 @@ export default function NewVerificationForm() { const onSubmit = useCallback(() => { if (!token) { - toast.error('No token provided') + setError('Missing token!') return } + newVerification(token) .then((data) => { if (data?.error) { - setTimeout(() => { - setError(data.error) - }, 500) - } else if (data?.success) { + setError(data.error) + } + if (data?.success) { + setSuccess(data.success) toast.success(data.success) setTimeout(() => { router.push('/login') - }, 100) + }, 2000) } }) .catch(() => { - const errorMessage = 'Something went wrong' - setError(errorMessage) + setError('Something went wrong!') }) }, [token, router]) @@ -43,24 +43,37 @@ export default function NewVerificationForm() { onSubmit() }, [onSubmit]) - useEffect(() => { - if (error && !hasErrorToastShown) { - const timer = setTimeout(() => { - toast.error(error) - setHasErrorToastShown(true) - }, 100) - return () => clearTimeout(timer) // Cleanup the timeout if component unmounts - } - }, [error, hasErrorToastShown]) - return (
- {!success && !error &&

Verifying...

} + {!success && !error && ( +
+ +

+ Verifying your email... +

+
+ )} + {success && ( +
+

{success}

+

+ Redirecting to login... +

+
+ )} + {error && ( +
+

{error}

+

+ Please try again or contact support. +

+
+ )}
) diff --git a/app/(root)/(routes)/dashboard/layout.tsx b/app/(root)/(routes)/dashboard/layout.tsx index d959041..062261c 100644 --- a/app/(root)/(routes)/dashboard/layout.tsx +++ b/app/(root)/(routes)/dashboard/layout.tsx @@ -1,9 +1,14 @@ -const DashboardLayout = ({ children }: { children: React.ReactNode }) => { - return ( -
- {children} -
- ) +import { Metadata } from "next" + +export const metadata: Metadata = { + title: "Dashboard", + description: "Task management and team collaboration dashboard", } -export default DashboardLayout +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode +}) { + return children +} diff --git a/app/(root)/(routes)/dashboard/page.tsx b/app/(root)/(routes)/dashboard/page.tsx index 031c049..bc697bd 100644 --- a/app/(root)/(routes)/dashboard/page.tsx +++ b/app/(root)/(routes)/dashboard/page.tsx @@ -1,62 +1,151 @@ -import { Metadata } from 'next' +"use client" + import { Calendar, CheckCircle2, Clock, ListTodo, Plus, - UserRoundCheck -} from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Card } from '@/components/ui/card' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { auth } from '@/auth' -import { redirect } from 'next/navigation' -import { db } from '@/lib/db' + Search, + Bell, + Mail, + Settings, +} from "lucide-react" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { ProjectSelect } from "@/components/project-select" +import { TaskColumn } from "@/components/task-column" +import { RichTextEditor } from "@/components/rich-text-editor" +import { FileUpload } from "@/components/file-upload" +import { + DndContext, + DragEndEvent, + DragOverlay, + DragStartEvent, + MouseSensor, + TouchSensor, + useSensor, + useSensors, +} from "@dnd-kit/core" +import { useState } from "react" +import { TaskCard } from "@/components/task-card" -export const metadata: Metadata = { - title: 'Dashboard', - description: 'Task management and team collaboration dashboard' +interface Task { + id: string + title: string + status: string + dueDate: string + progress: number } -export default async function DashboardPage() { - const session = await auth() +export default function DashboardPage() { + const [tasks, setTasks] = useState([ + { id: "1", title: "Design new landing page", status: "In Progress", dueDate: "2023-12-01", progress: 60 }, + { id: "2", title: "Implement authentication", status: "Todo", dueDate: "2023-12-05", progress: 0 }, + { id: "3", title: "Write documentation", status: "Done", dueDate: "2023-11-30", progress: 100 }, + ]) - if (!session) { - redirect('/login') + const [activeTask, setActiveTask] = useState(null) + + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 300, + tolerance: 8, + }, + }) + ) + + const handleDragStart = (event: DragStartEvent) => { + const { active } = event + const task = tasks.find(t => t.id === active.id) + if (task) { + setActiveTask(task) + } } - // Fetch tasks (placeholder - implement actual DB queries) - const tasks = [ - { id: 1, title: 'Design new landing page', status: 'In Progress', dueDate: '2023-12-01', progress: 60 }, - { id: 2, title: 'Implement authentication', status: 'Todo', dueDate: '2023-12-05', progress: 0 }, - { id: 3, title: 'Write documentation', status: 'Done', dueDate: '2023-11-30', progress: 100 } - ] + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event + + if (over && active.id !== over.id) { + setTasks(tasks => { + const oldIndex = tasks.findIndex(t => t.id === active.id) + const task = tasks[oldIndex] + const newStatus = over.id as string + + if (task && (newStatus === "Todo" || newStatus === "In Progress" || newStatus === "Done")) { + const updatedTasks = [...tasks] + updatedTasks[oldIndex] = { ...task, status: newStatus } + return updatedTasks + } + + return tasks + }) + } + + setActiveTask(null) + } return ( -
-
-

Dashboard

-
- +
+ {/* Sidebar */} +
+ + - - - Overview - Tasks - Calendar - + {/* Main Content */} +
+ {/* Header */} +
+
+ +
+
+
+ + +
+
+
+ +
+
- + {/* Dashboard Content */} +
+ {/* Stats */}
-
+
Total Tasks
@@ -65,7 +154,7 @@ export default async function DashboardPage() {
-
+
In Progress
@@ -74,7 +163,7 @@ export default async function DashboardPage() {
-
+
Completed
@@ -83,218 +172,77 @@ export default async function DashboardPage() {
-
- - Team Members +
+ + Upcoming
-
6
+
3
-
- -
-

Recent Tasks

-
- {tasks.map(task => ( -
-
-

{task.title}

-

Due: {task.dueDate}

-
-
- {task.status} -
-
-
-
-
- ))} -
-
- + {/* Task Board */} +
+
+

Tasks

+ +
- -
-

Upcoming Deadlines

-
-
- -
-

Design Review

-

Tomorrow at 2:00 PM

-
-
-
- -
-

Team Meeting

-

Friday at 10:00 AM

-
-
-
+ +
+ t.status === "Todo")} + /> + t.status === "In Progress")} + /> + t.status === "Done")} + />
- -
- - -
-

All Tasks

- + + {activeTask ? : null} + +
-
- {/* Todo Column */} - -

- - Todo -

-
- {tasks.filter(t => t.status === 'Todo').map(task => ( - -
{task.title}
-

Due: {task.dueDate}

-
-
-
- - ))} -
-
- - {/* In Progress Column */} - -

- - In Progress -

-
- {tasks.filter(t => t.status === 'In Progress').map(task => ( - -
{task.title}
-

Due: {task.dueDate}

-
-
-
- - ))} -
-
- - {/* Done Column */} - -

- - Done -

-
- {tasks.filter(t => t.status === 'Done').map(task => ( - -
{task.title}
-

Due: {task.dueDate}

-
-
-
- - ))} -
-
-
- - - -
-

Calendar

- -
- -
+ {/* Task Details */} +
-
- {/* Today's Schedule */} -
-

Today's Schedule

-
-
-
09:00 AM
-
- -
Team Standup
-

Daily team sync meeting

-
-
-
-
-
02:00 PM
-
- -
Design Review
-

Review new landing page design

-
-
-
-
-
04:30 PM
-
- -
Sprint Planning
-

Plan next sprint tasks

-
-
-
-
-
+

Task Description

+ console.log(content)} + /> + - {/* Upcoming Events */} -
-

Upcoming Events

-
-
-
Tomorrow
-
- -
Client Meeting
-

10:00 AM - Project update discussion

-
-
-
-
-
Friday
-
- -
Team Building
-

02:00 PM - Virtual team activity

-
-
-
-
-
-
+ +

Attachments

+ console.log("Uploaded files:", files)} + />
- - +
+
) } diff --git a/app/globals.css b/app/globals.css index f41b71f..13d4334 100644 --- a/app/globals.css +++ b/app/globals.css @@ -3,70 +3,96 @@ @tailwind utilities; @layer base { - html, - body { - @apply bg-background; - } - :root { - /* Adjust these to change the theme's color */ - --primary: 234 88% 74%; - --primary-foreground: 0 0% 0%; - - /* Adjust these to change the chart's colors */ - --chart-1: 208 87% 53%; - --chart-2: 301 56% 56%; - --chart-3: 99 55% 51%; - --chart-4: 56 70% 54%; - --chart-5: 0 60% 57%; - --background: 0 0% 100%; - --background-hover: 0 0% 95%; - --foreground: 240 10% 3.9%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; + --popover-foreground: 0 0% 3.9%; + + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; --destructive-foreground: 0 0% 98%; - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 240 5.9% 10%; - --radius: 0.5rem; + + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + + --radius: 0.3rem; } .dark { - --background: 249 11% 12%; - --background-hover: 0 0% 13%; + --background: 0 0% 0%; --foreground: 0 0% 98%; - --card: 240 10% 3.9%; + + --card: 0 0% 3.9%; --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; + + --popover: 0 0% 3.9%; --popover-foreground: 0 0% 98%; - --secondary: 240 3.7% 15.9%; + + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + + --secondary: 0 0% 14.9%; --secondary-foreground: 0 0% 98%; - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - --accent: 240 3.7% 15.9%; + + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + + --accent: 0 0% 14.9%; --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; --destructive-foreground: 0 0% 98%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; - } - * { - @apply border-border; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; } } +* { + @apply border-border; +} + +body { + @apply bg-background text-foreground; +} + +/* Custom scrollbar for modern browsers */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + @apply bg-muted; +} + +::-webkit-scrollbar-thumb { + @apply bg-muted-foreground/50 rounded-full; +} + +::-webkit-scrollbar-thumb:hover { + @apply bg-muted-foreground; +} + +/* Base styles */ html { scroll-behavior: smooth; } @@ -80,9 +106,9 @@ body { main { flex: 1 0 0; + width: 100%; } .disable-transitions * { - transition-property: background-color, color !important; - transition-duration: 0s !important; + transition-property: none !important; } diff --git a/app/page.tsx b/app/page.tsx index 91772c3..355d1e7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,5 @@ import { Footer } from '@/components/footer' import { Header } from '@/components/header' -import { Button } from '@/components/ui/button' export default function Home() { return ( @@ -8,67 +7,6 @@ export default function Home() {
- -
- {/* Hero Section */} -
-

- Think, plan, and track - all in one place -

-

- Efficiently manage your tasks and boost productivity. -

-
- -
-
- - {/* Decorative Elements */} -
- {/* Yellow Note */} -
-
-

Take notes to keep track of crucial details, and accomplish more tasks with ease.

-
-
- - {/* Task List */} -
-
-

Today's tasks

-
-
-
-
-
- 60% -
-
-
-
-
- 33% -
-
-
-
- - {/* Integrations */} -
-
-

100+ Integrations

-
-
-
-
-
-
-
-
-
diff --git a/components/file-upload.tsx b/components/file-upload.tsx new file mode 100644 index 0000000..7bb606f --- /dev/null +++ b/components/file-upload.tsx @@ -0,0 +1,84 @@ +import * as React from "react" +import { useDropzone } from "react-dropzone" +import { Cloud, File, X } from "lucide-react" +import { Button } from "@/components/ui/button" +import { cn } from "@/lib/utils" + +interface FileUploadProps { + onUpload: (files: File[]) => void + value?: File[] + onChange?: (files: File[]) => void + className?: string +} + +export function FileUpload({ onUpload, value = [], onChange, className }: FileUploadProps) { + const [files, setFiles] = React.useState(value) + + const onDrop = React.useCallback((acceptedFiles: File[]) => { + setFiles(prev => [...prev, ...acceptedFiles]) + onChange?.(acceptedFiles) + onUpload(acceptedFiles) + }, [onChange, onUpload]) + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + accept: { + 'image/*': [], + 'application/pdf': [], + 'application/msword': [], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': [], + } + }) + + const removeFile = (fileToRemove: File) => { + const newFiles = files.filter(file => file !== fileToRemove) + setFiles(newFiles) + onChange?.(newFiles) + } + + return ( +
+
+ +
+ +

+ Drag & drop files here, or click to select files +

+ +
+
+ + {files.length > 0 && ( +
+ {files.map((file, index) => ( +
+ + {file.name} + +
+ ))} +
+ )} +
+ ) +} diff --git a/components/header.tsx b/components/header.tsx index 8e2ddc1..2659eb1 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -1,55 +1,21 @@ 'use client' -import { useState, useEffect } from 'react' import { Sparkles } from 'lucide-react' import Link from 'next/link' import { Button } from '@/components/ui/button' -import { Skeleton } from '@/components/ui/skeleton' - -const IframeWithSkeleton = () => { - const [iframeLoaded, setIframeLoaded] = useState(false); - - useEffect(() => { - const iframe = document.getElementById('youtube-iframe') as HTMLIFrameElement; - if (iframe) { - const handleIframeLoad = () => { - setIframeLoaded(true); - }; - - iframe.addEventListener('load', handleIframeLoad); - - return () => { - iframe.removeEventListener('load', handleIframeLoad); - }; - } - }, []); - - return ( - <> - {!iframeLoaded && } - - - ); -}; export const Header = () => { return (
-
-
+
+

Clone. Build. Ship.

Build your SaaS faster with our fully customizable template.

-
+
-
- -
) diff --git a/components/navbar.tsx b/components/navbar.tsx index 555b0c4..b21c1f9 100644 --- a/components/navbar.tsx +++ b/components/navbar.tsx @@ -1,26 +1,26 @@ +'use client' + import Link from 'next/link' import { ModeToggle } from '@/components/mode-toggle' import Image from 'next/image' import { UserButton } from '@/components/user-button' import { MobileSidebar } from '@/components/mobile-sidebar' import { Logo } from '@/components/logo' +import { usePathname } from 'next/navigation' export const navPages = [ { title: 'Dashboard', link: '/dashboard' - }, - { - title: 'Pricing', - link: '/#pricing' - }, - { - title: 'Items', - link: '/#items' } ] export const Navbar = () => { + const pathname = usePathname() + + // Don't show navbar on home page + if (pathname === '/') return null + return (