mirror of
https://github.com/harivansh-afk/Saas-Teamspace.git
synced 2026-04-15 07:04:48 +00:00
changed a lot
This commit is contained in:
parent
ef9ccf22d3
commit
28901128ff
20 changed files with 1794 additions and 526 deletions
|
|
@ -6,6 +6,7 @@ import { AuthError } from 'next-auth'
|
||||||
import { generateVerificationToken } from '@/lib/tokens'
|
import { generateVerificationToken } from '@/lib/tokens'
|
||||||
import { getUserByEmail } from '@/data/user'
|
import { getUserByEmail } from '@/data/user'
|
||||||
import { sendVerificationEmail } from '@/lib/mail'
|
import { sendVerificationEmail } from '@/lib/mail'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
|
||||||
export const login = async (values: z.infer<typeof LoginSchema>) => {
|
export const login = async (values: z.infer<typeof LoginSchema>) => {
|
||||||
// Validate fields
|
// Validate fields
|
||||||
|
|
@ -15,48 +16,42 @@ export const login = async (values: z.infer<typeof LoginSchema>) => {
|
||||||
if (!validatedFields.success) {
|
if (!validatedFields.success) {
|
||||||
return { error: 'Invalid fields' }
|
return { error: 'Invalid fields' }
|
||||||
}
|
}
|
||||||
// If fields are valid
|
|
||||||
const { email, password } = validatedFields.data
|
|
||||||
const exisitingUser = await getUserByEmail(email)
|
|
||||||
|
|
||||||
if (!exisitingUser || !exisitingUser.email || !exisitingUser.password) {
|
const { email, password } = validatedFields.data
|
||||||
return { error: 'Email does not exisit' }
|
const existingUser = await getUserByEmail(email)
|
||||||
|
|
||||||
|
if (!existingUser || !existingUser.email || !existingUser.password) {
|
||||||
|
return { error: 'Email does not exist' }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!exisitingUser.emailVerified) {
|
// Check if email is verified first
|
||||||
const verificationToken = await generateVerificationToken(
|
if (!existingUser.emailVerified) {
|
||||||
exisitingUser.email
|
const verificationToken = await generateVerificationToken(email)
|
||||||
)
|
|
||||||
|
|
||||||
await sendVerificationEmail(
|
await sendVerificationEmail(
|
||||||
verificationToken.email,
|
verificationToken.email,
|
||||||
verificationToken.token
|
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 {
|
try {
|
||||||
const result = await signIn('credentials', {
|
await signIn('credentials', {
|
||||||
redirect: false,
|
|
||||||
email,
|
email,
|
||||||
password
|
password,
|
||||||
|
redirect: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result?.error) {
|
return { success: 'Logged in successfully!' }
|
||||||
return { error: result.error }
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: 'Logged In!' }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof AuthError) {
|
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
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,40 +2,47 @@
|
||||||
|
|
||||||
import { db } from '@/lib/db'
|
import { db } from '@/lib/db'
|
||||||
import { getUserByEmail } from '@/data/user'
|
import { getUserByEmail } from '@/data/user'
|
||||||
|
|
||||||
import { getVerificationTokenByToken } from '@/data/verification-token'
|
import { getVerificationTokenByToken } from '@/data/verification-token'
|
||||||
|
|
||||||
export const newVerification = async (token: string) => {
|
export const newVerification = async (token: string) => {
|
||||||
// if no token, display message
|
try {
|
||||||
const exisitingToken = await getVerificationTokenByToken(token)
|
const existingToken = await getVerificationTokenByToken(token)
|
||||||
|
|
||||||
if (!exisitingToken) {
|
if (!existingToken) {
|
||||||
return { error: 'Token does not exisit!' }
|
return { error: 'Verification link is invalid!' }
|
||||||
}
|
}
|
||||||
// if token has expired, display message
|
|
||||||
const hasExpired = new Date(exisitingToken.expires) < new Date()
|
|
||||||
|
|
||||||
|
const hasExpired = new Date(existingToken.expires) < new Date()
|
||||||
if (hasExpired) {
|
if (hasExpired) {
|
||||||
return { error: 'Token has expired!' }
|
return { error: 'Verification link has expired! Please request a new one.' }
|
||||||
}
|
}
|
||||||
// if user does not exist, display message
|
|
||||||
const existingUser = await getUserByEmail(exisitingToken.email)
|
|
||||||
|
|
||||||
|
const existingUser = await getUserByEmail(existingToken.email)
|
||||||
if (!existingUser) {
|
if (!existingUser) {
|
||||||
return { error: 'User does not exisit!' }
|
return { error: 'Email not found! Please sign up first.' }
|
||||||
}
|
}
|
||||||
// update email value when they verify
|
|
||||||
|
// If already verified, just return success
|
||||||
|
if (existingUser.emailVerified) {
|
||||||
|
return { success: 'Email already verified! Please login.' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user verification status
|
||||||
await db.user.update({
|
await db.user.update({
|
||||||
where: { id: existingUser.id },
|
where: { id: existingUser.id },
|
||||||
data: {
|
data: {
|
||||||
emailVerified: new Date(),
|
emailVerified: new Date(),
|
||||||
email: exisitingToken.email
|
email: existingToken.email
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// delete token
|
|
||||||
|
// Delete the verification token
|
||||||
await db.verificationToken.delete({
|
await db.verificationToken.delete({
|
||||||
where: { id: exisitingToken.id }
|
where: { id: existingToken.id }
|
||||||
})
|
})
|
||||||
|
|
||||||
return { success: 'Email verified! Login to continue' }
|
return { success: 'Email verified successfully! You can now login.' }
|
||||||
|
} catch (error) {
|
||||||
|
return { error: 'Something went wrong! Please try again.' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,15 @@ import { Input } from '@/components/ui/input'
|
||||||
import { CardWrapper } from '@/components/auth/card-wrapper'
|
import { CardWrapper } from '@/components/auth/card-wrapper'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams, useRouter } from 'next/navigation'
|
||||||
import { useEffect, useRef, useTransition } from 'react'
|
import { useEffect, useRef, useTransition } from 'react'
|
||||||
import { login } from '@/actions/login'
|
import { login } from '@/actions/login'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
const callbackUrl = searchParams.get('callbackUrl')
|
||||||
const urlError =
|
const urlError =
|
||||||
searchParams.get('error') === 'OAuthAccountNotLinked'
|
searchParams.get('error') === 'OAuthAccountNotLinked'
|
||||||
? 'Email already in use with different provider!'
|
? 'Email already in use with different provider!'
|
||||||
|
|
@ -52,8 +54,12 @@ export default function Page() {
|
||||||
}
|
}
|
||||||
if (data?.success) {
|
if (data?.success) {
|
||||||
toast.success(data.success)
|
toast.success(data.success)
|
||||||
form.reset({ email: '', password: '' })
|
if (data.success === 'Confirmation email sent!') {
|
||||||
window.location.href = '/'
|
// Don't redirect if we're just sending a confirmation email
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push('/dashboard');
|
||||||
|
router.refresh();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ import { newVerification } from '@/actions/new-verification'
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
|
import { Loader2 } from 'lucide-react'
|
||||||
|
|
||||||
export default function NewVerificationForm() {
|
export default function NewVerificationForm() {
|
||||||
const [error, setError] = useState<string | undefined>()
|
const [error, setError] = useState<string | undefined>()
|
||||||
const [success, setSuccess] = useState<string | undefined>()
|
const [success, setSuccess] = useState<string | undefined>()
|
||||||
const [hasErrorToastShown, setHasErrorToastShown] = useState<boolean>(false)
|
|
||||||
|
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const token = searchParams.get('token')
|
const token = searchParams.get('token')
|
||||||
|
|
@ -17,25 +17,25 @@ export default function NewVerificationForm() {
|
||||||
|
|
||||||
const onSubmit = useCallback(() => {
|
const onSubmit = useCallback(() => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
toast.error('No token provided')
|
setError('Missing token!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newVerification(token)
|
newVerification(token)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data?.error) {
|
if (data?.error) {
|
||||||
setTimeout(() => {
|
|
||||||
setError(data.error)
|
setError(data.error)
|
||||||
}, 500)
|
}
|
||||||
} else if (data?.success) {
|
if (data?.success) {
|
||||||
|
setSuccess(data.success)
|
||||||
toast.success(data.success)
|
toast.success(data.success)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}, 100)
|
}, 2000)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
const errorMessage = 'Something went wrong'
|
setError('Something went wrong!')
|
||||||
setError(errorMessage)
|
|
||||||
})
|
})
|
||||||
}, [token, router])
|
}, [token, router])
|
||||||
|
|
||||||
|
|
@ -43,24 +43,37 @@ export default function NewVerificationForm() {
|
||||||
onSubmit()
|
onSubmit()
|
||||||
}, [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 (
|
return (
|
||||||
<CardWrapper
|
<CardWrapper
|
||||||
headerTitle="Verify your email"
|
headerTitle="Confirm your email"
|
||||||
backButtonLabel="Back to Login"
|
backButtonLabel="Back to login"
|
||||||
backButtonHref="/login"
|
backButtonHref="/login"
|
||||||
>
|
>
|
||||||
<div className="flex items-center w-full justify-center">
|
<div className="flex items-center w-full justify-center">
|
||||||
{!success && !error && <p>Verifying...</p>}
|
{!success && !error && (
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Verifying your email...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{success && (
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<p className="text-success text-sm">{success}</p>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Redirecting to login...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<p className="text-destructive text-sm">{error}</p>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Please try again or contact support.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardWrapper>
|
</CardWrapper>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
import { Metadata } from "next"
|
||||||
return (
|
|
||||||
<main className="max-w-6xl w-full flex items-center justify-center px-6">
|
export const metadata: Metadata = {
|
||||||
{children}
|
title: "Dashboard",
|
||||||
</main>
|
description: "Task management and team collaboration dashboard",
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DashboardLayout
|
export default function DashboardLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,151 @@
|
||||||
import { Metadata } from 'next'
|
"use client"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock,
|
Clock,
|
||||||
ListTodo,
|
ListTodo,
|
||||||
Plus,
|
Plus,
|
||||||
UserRoundCheck
|
Search,
|
||||||
} from 'lucide-react'
|
Bell,
|
||||||
import { Button } from '@/components/ui/button'
|
Mail,
|
||||||
import { Card } from '@/components/ui/card'
|
Settings,
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
} from "lucide-react"
|
||||||
import { auth } from '@/auth'
|
import { Button } from "@/components/ui/button"
|
||||||
import { redirect } from 'next/navigation'
|
import { Card } from "@/components/ui/card"
|
||||||
import { db } from '@/lib/db'
|
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 = {
|
interface Task {
|
||||||
title: 'Dashboard',
|
id: string
|
||||||
description: 'Task management and team collaboration dashboard'
|
title: string
|
||||||
|
status: string
|
||||||
|
dueDate: string
|
||||||
|
progress: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const session = await auth()
|
const [tasks, setTasks] = useState<Task[]>([
|
||||||
|
{ 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) {
|
const [activeTask, setActiveTask] = useState<Task | null>(null)
|
||||||
redirect('/login')
|
|
||||||
|
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 handleDragEnd = (event: DragEndEvent) => {
|
||||||
const tasks = [
|
const { active, over } = event
|
||||||
{ 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 },
|
if (over && active.id !== over.id) {
|
||||||
{ id: 3, title: 'Write documentation', status: 'Done', dueDate: '2023-11-30', progress: 100 }
|
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 (
|
return (
|
||||||
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
|
<div className="flex min-h-screen">
|
||||||
<div className="flex items-center justify-between space-y-2">
|
{/* Sidebar */}
|
||||||
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
|
<aside className="w-64 border-r bg-card">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="p-6">
|
||||||
<Button>
|
<h2 className="text-lg font-semibold">Shadcn UI Kit</h2>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
</div>
|
||||||
Add New Task
|
<nav className="px-4 space-y-2">
|
||||||
|
<Button variant="ghost" className="w-full justify-start">
|
||||||
|
<ListTodo className="mr-2 h-4 w-4" />
|
||||||
|
Dashboard
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" className="w-full justify-start">
|
||||||
|
<Mail className="mr-2 h-4 w-4" />
|
||||||
|
Messages
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" className="w-full justify-start">
|
||||||
|
<Settings className="mr-2 h-4 w-4" />
|
||||||
|
Settings
|
||||||
|
</Button>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<main className="flex-1">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="border-b">
|
||||||
|
<div className="flex h-16 items-center px-6 gap-4">
|
||||||
|
<ProjectSelect />
|
||||||
|
<div className="flex-1">
|
||||||
|
<form>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="search"
|
||||||
|
placeholder="Search..."
|
||||||
|
className="w-full bg-background pl-8 md:w-[300px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<Bell className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<Tabs defaultValue="overview" className="space-y-4">
|
{/* Dashboard Content */}
|
||||||
<TabsList>
|
<div className="p-6 space-y-6">
|
||||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
{/* Stats */}
|
||||||
<TabsTrigger value="tasks">Tasks</TabsTrigger>
|
|
||||||
<TabsTrigger value="calendar">Calendar</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="overview" className="space-y-4">
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-2">
|
||||||
<ListTodo className="h-4 w-4 text-muted-foreground" />
|
<ListTodo className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-medium">Total Tasks</span>
|
<span className="text-sm font-medium">Total Tasks</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -65,7 +154,7 @@ export default async function DashboardPage() {
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-2">
|
||||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-medium">In Progress</span>
|
<span className="text-sm font-medium">In Progress</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -74,7 +163,7 @@ export default async function DashboardPage() {
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="text-sm font-medium">Completed</span>
|
<span className="text-sm font-medium">Completed</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,218 +172,77 @@ export default async function DashboardPage() {
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center gap-2">
|
||||||
<UserRoundCheck className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span className="text-sm font-medium">Team Members</span>
|
|
||||||
</div>
|
|
||||||
<div className="text-2xl font-bold">6</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
|
|
||||||
<Card className="col-span-4">
|
|
||||||
<div className="p-6">
|
|
||||||
<h3 className="text-lg font-medium">Recent Tasks</h3>
|
|
||||||
<div className="mt-4 space-y-4">
|
|
||||||
{tasks.map(task => (
|
|
||||||
<div key={task.id} className="flex items-center justify-between border-b pb-4">
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium">{task.title}</h4>
|
|
||||||
<p className="text-sm text-muted-foreground">Due: {task.dueDate}</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="text-sm">{task.status}</span>
|
|
||||||
<div className="h-2 w-24 bg-gray-200 rounded-full">
|
|
||||||
<div
|
|
||||||
className="h-full bg-blue-500 rounded-full"
|
|
||||||
style={{ width: `${task.progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="col-span-3">
|
|
||||||
<div className="p-6">
|
|
||||||
<h3 className="text-lg font-medium">Upcoming Deadlines</h3>
|
|
||||||
<div className="mt-4 space-y-4">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||||
<div>
|
<span className="text-sm font-medium">Upcoming</span>
|
||||||
<p className="font-medium">Design Review</p>
|
|
||||||
<p className="text-sm text-muted-foreground">Tomorrow at 2:00 PM</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">Team Meeting</p>
|
|
||||||
<p className="text-sm text-muted-foreground">Friday at 10:00 AM</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-2xl font-bold">3</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="tasks" className="space-y-4">
|
{/* Task Board */}
|
||||||
<div className="flex justify-between items-center">
|
<div className="space-y-4">
|
||||||
<h3 className="text-lg font-medium">All Tasks</h3>
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">Tasks</h2>
|
||||||
<Button>
|
<Button>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
New Task
|
Add Task
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
{/* Todo Column */}
|
<TaskColumn
|
||||||
<Card className="p-4">
|
id="Todo"
|
||||||
<h4 className="font-medium mb-4 flex items-center">
|
title="Todo"
|
||||||
<ListTodo className="h-4 w-4 mr-2" />
|
icon={ListTodo}
|
||||||
Todo
|
tasks={tasks.filter(t => t.status === "Todo")}
|
||||||
</h4>
|
/>
|
||||||
<div className="space-y-4">
|
<TaskColumn
|
||||||
{tasks.filter(t => t.status === 'Todo').map(task => (
|
id="In Progress"
|
||||||
<Card key={task.id} className="p-3">
|
title="In Progress"
|
||||||
<h5 className="font-medium">{task.title}</h5>
|
icon={Clock}
|
||||||
<p className="text-sm text-muted-foreground mt-1">Due: {task.dueDate}</p>
|
tasks={tasks.filter(t => t.status === "In Progress")}
|
||||||
<div className="mt-3 h-1.5 w-full bg-gray-100 rounded-full">
|
/>
|
||||||
<div
|
<TaskColumn
|
||||||
className="h-full bg-blue-500 rounded-full"
|
id="Done"
|
||||||
style={{ width: `${task.progress}%` }}
|
title="Done"
|
||||||
|
icon={CheckCircle2}
|
||||||
|
tasks={tasks.filter(t => t.status === "Done")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* In Progress Column */}
|
<DragOverlay>
|
||||||
<Card className="p-4">
|
{activeTask ? <TaskCard {...activeTask} /> : null}
|
||||||
<h4 className="font-medium mb-4 flex items-center">
|
</DragOverlay>
|
||||||
<Clock className="h-4 w-4 mr-2" />
|
</DndContext>
|
||||||
In Progress
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{tasks.filter(t => t.status === 'In Progress').map(task => (
|
|
||||||
<Card key={task.id} className="p-3">
|
|
||||||
<h5 className="font-medium">{task.title}</h5>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">Due: {task.dueDate}</p>
|
|
||||||
<div className="mt-3 h-1.5 w-full bg-gray-100 rounded-full">
|
|
||||||
<div
|
|
||||||
className="h-full bg-blue-500 rounded-full"
|
|
||||||
style={{ width: `${task.progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Done Column */}
|
|
||||||
<Card className="p-4">
|
|
||||||
<h4 className="font-medium mb-4 flex items-center">
|
|
||||||
<CheckCircle2 className="h-4 w-4 mr-2" />
|
|
||||||
Done
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{tasks.filter(t => t.status === 'Done').map(task => (
|
|
||||||
<Card key={task.id} className="p-3">
|
|
||||||
<h5 className="font-medium">{task.title}</h5>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">Due: {task.dueDate}</p>
|
|
||||||
<div className="mt-3 h-1.5 w-full bg-gray-100 rounded-full">
|
|
||||||
<div
|
|
||||||
className="h-full bg-blue-500 rounded-full"
|
|
||||||
style={{ width: `${task.progress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="calendar" className="space-y-4">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<h3 className="text-lg font-medium">Calendar</h3>
|
|
||||||
<Button variant="outline">
|
|
||||||
<Calendar className="mr-2 h-4 w-4" />
|
|
||||||
Select Date
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4">
|
{/* Task Details */}
|
||||||
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
<div className="space-y-6">
|
<h3 className="text-lg font-medium mb-4">Task Description</h3>
|
||||||
{/* Today's Schedule */}
|
<RichTextEditor
|
||||||
<div>
|
className="min-h-[200px]"
|
||||||
<h4 className="font-medium mb-4">Today's Schedule</h4>
|
content="<p>Add your task description here...</p>"
|
||||||
<div className="space-y-4">
|
onChange={(content) => console.log(content)}
|
||||||
<div className="flex items-center space-x-4">
|
/>
|
||||||
<div className="w-14 text-sm text-muted-foreground">09:00 AM</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Card className="p-3">
|
|
||||||
<h5 className="font-medium">Team Standup</h5>
|
|
||||||
<p className="text-sm text-muted-foreground">Daily team sync meeting</p>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="w-14 text-sm text-muted-foreground">02:00 PM</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Card className="p-3">
|
|
||||||
<h5 className="font-medium">Design Review</h5>
|
|
||||||
<p className="text-sm text-muted-foreground">Review new landing page design</p>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="w-14 text-sm text-muted-foreground">04:30 PM</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Card className="p-3">
|
|
||||||
<h5 className="font-medium">Sprint Planning</h5>
|
|
||||||
<p className="text-sm text-muted-foreground">Plan next sprint tasks</p>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Upcoming Events */}
|
<Card className="p-6">
|
||||||
<div>
|
<h3 className="text-lg font-medium mb-4">Attachments</h3>
|
||||||
<h4 className="font-medium mb-4">Upcoming Events</h4>
|
<FileUpload
|
||||||
<div className="space-y-4">
|
onUpload={(files) => console.log("Uploaded files:", files)}
|
||||||
<div className="flex items-center space-x-4">
|
/>
|
||||||
<div className="w-20 text-sm text-muted-foreground">Tomorrow</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Card className="p-3">
|
|
||||||
<h5 className="font-medium">Client Meeting</h5>
|
|
||||||
<p className="text-sm text-muted-foreground">10:00 AM - Project update discussion</p>
|
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
</main>
|
||||||
<div className="w-20 text-sm text-muted-foreground">Friday</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<Card className="p-3">
|
|
||||||
<h5 className="font-medium">Team Building</h5>
|
|
||||||
<p className="text-sm text-muted-foreground">02:00 PM - Virtual team activity</p>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
118
app/globals.css
118
app/globals.css
|
|
@ -3,70 +3,96 @@
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html,
|
|
||||||
body {
|
|
||||||
@apply bg-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
: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: 0 0% 100%;
|
||||||
--background-hover: 0 0% 95%;
|
--foreground: 0 0% 3.9%;
|
||||||
--foreground: 240 10% 3.9%;
|
|
||||||
--card: 0 0% 100%;
|
--card: 0 0% 100%;
|
||||||
--card-foreground: 240 10% 3.9%;
|
--card-foreground: 0 0% 3.9%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 240 10% 3.9%;
|
--popover-foreground: 0 0% 3.9%;
|
||||||
--secondary: 240 4.8% 95.9%;
|
|
||||||
--secondary-foreground: 240 5.9% 10%;
|
--primary: 0 0% 9%;
|
||||||
--muted: 240 4.8% 95.9%;
|
--primary-foreground: 0 0% 98%;
|
||||||
--muted-foreground: 240 3.8% 46.1%;
|
|
||||||
--accent: 240 4.8% 95.9%;
|
--secondary: 0 0% 96.1%;
|
||||||
--accent-foreground: 240 5.9% 10%;
|
--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: 0 84.2% 60.2%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 240 5.9% 90%;
|
|
||||||
--input: 240 5.9% 90%;
|
--border: 0 0% 89.8%;
|
||||||
--ring: 240 5.9% 10%;
|
--input: 0 0% 89.8%;
|
||||||
--radius: 0.5rem;
|
--ring: 0 0% 3.9%;
|
||||||
|
|
||||||
|
--radius: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 249 11% 12%;
|
--background: 0 0% 0%;
|
||||||
--background-hover: 0 0% 13%;
|
|
||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 98%;
|
||||||
--card: 240 10% 3.9%;
|
|
||||||
|
--card: 0 0% 3.9%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 98%;
|
||||||
--popover: 240 10% 3.9%;
|
|
||||||
|
--popover: 0 0% 3.9%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--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%;
|
--secondary-foreground: 0 0% 98%;
|
||||||
--muted: 240 3.7% 15.9%;
|
|
||||||
--muted-foreground: 240 5% 64.9%;
|
--muted: 0 0% 14.9%;
|
||||||
--accent: 240 3.7% 15.9%;
|
--muted-foreground: 0 0% 63.9%;
|
||||||
|
|
||||||
|
--accent: 0 0% 14.9%;
|
||||||
--accent-foreground: 0 0% 98%;
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 0 0% 98%;
|
--destructive-foreground: 0 0% 98%;
|
||||||
--border: 240 3.7% 15.9%;
|
|
||||||
--input: 240 3.7% 15.9%;
|
|
||||||
--ring: 240 4.9% 83.9%;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
--border: 0 0% 14.9%;
|
||||||
@apply border-border;
|
--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 {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
@ -80,9 +106,9 @@ body {
|
||||||
|
|
||||||
main {
|
main {
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disable-transitions * {
|
.disable-transitions * {
|
||||||
transition-property: background-color, color !important;
|
transition-property: none !important;
|
||||||
transition-duration: 0s !important;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
62
app/page.tsx
62
app/page.tsx
|
|
@ -1,6 +1,5 @@
|
||||||
import { Footer } from '@/components/footer'
|
import { Footer } from '@/components/footer'
|
||||||
import { Header } from '@/components/header'
|
import { Header } from '@/components/header'
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -8,67 +7,6 @@ export default function Home() {
|
||||||
<main className="w-full min-h-screen bg-gray-50">
|
<main className="w-full min-h-screen bg-gray-50">
|
||||||
<div className="max-w-6xl mx-auto px-6">
|
<div className="max-w-6xl mx-auto px-6">
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<div className="relative mt-20 mb-40">
|
|
||||||
{/* Hero Section */}
|
|
||||||
<div className="text-center space-y-8 relative z-10">
|
|
||||||
<h1 className="text-6xl font-bold tracking-tight">
|
|
||||||
Think, plan, and track
|
|
||||||
<span className="block text-gray-400 mt-2">all in one place</span>
|
|
||||||
</h1>
|
|
||||||
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
|
||||||
Efficiently manage your tasks and boost productivity.
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<Button size="lg" className="bg-blue-500 hover:bg-blue-600">
|
|
||||||
Get free demo
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Decorative Elements */}
|
|
||||||
<div className="absolute top-0 left-0 w-full h-full overflow-hidden">
|
|
||||||
{/* Yellow Note */}
|
|
||||||
<div className="absolute left-20 top-20 transform -rotate-6">
|
|
||||||
<div className="bg-yellow-100 p-6 rounded shadow-lg w-48">
|
|
||||||
<p className="text-sm text-gray-700">Take notes to keep track of crucial details, and accomplish more tasks with ease.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Task List */}
|
|
||||||
<div className="absolute right-20 bottom-20">
|
|
||||||
<div className="bg-white p-4 rounded-lg shadow-lg w-64">
|
|
||||||
<h3 className="font-semibold mb-3">Today's tasks</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-2 w-full bg-blue-100 rounded">
|
|
||||||
<div className="h-full w-3/5 bg-blue-500 rounded"></div>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-500">60%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="h-2 w-full bg-blue-100 rounded">
|
|
||||||
<div className="h-full w-1/3 bg-blue-500 rounded"></div>
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-gray-500">33%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Integrations */}
|
|
||||||
<div className="absolute right-40 top-20">
|
|
||||||
<div className="bg-white p-4 rounded-lg shadow-lg">
|
|
||||||
<p className="text-sm font-medium mb-2">100+ Integrations</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="w-8 h-8 bg-red-100 rounded"></div>
|
|
||||||
<div className="w-8 h-8 bg-green-100 rounded"></div>
|
|
||||||
<div className="w-8 h-8 bg-blue-100 rounded"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
84
components/file-upload.tsx
Normal file
84
components/file-upload.tsx
Normal file
|
|
@ -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<File[]>(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 (
|
||||||
|
<div className={className}>
|
||||||
|
<div
|
||||||
|
{...getRootProps()}
|
||||||
|
className={cn(
|
||||||
|
"border-2 border-dashed rounded-lg p-8 text-center hover:border-primary/50 transition-colors",
|
||||||
|
isDragActive && "border-primary",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<Cloud className="h-10 w-10 text-muted-foreground" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Drag & drop files here, or click to select files
|
||||||
|
</p>
|
||||||
|
<Button variant="secondary" size="sm">
|
||||||
|
Select Files
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{files.length > 0 && (
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
{files.map((file, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center gap-2 rounded-md border p-2"
|
||||||
|
>
|
||||||
|
<File className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm flex-1 truncate">{file.name}</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
onClick={() => removeFile(file)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,55 +1,21 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect } from 'react'
|
|
||||||
import { Sparkles } from 'lucide-react'
|
import { Sparkles } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { Button } from '@/components/ui/button'
|
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 && <Skeleton className="w-full max-w-2xl h-auto aspect-video" />}
|
|
||||||
<iframe
|
|
||||||
id="youtube-iframe"
|
|
||||||
src="https://www.youtube.com/embed/Q6jDdtbkMIU?si=YtgU89RhYiwt5-U5"
|
|
||||||
title="YouTube Video Player"
|
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
||||||
className={`w-full max-w-2xl h-auto aspect-video rounded-[6px] ${iframeLoaded ? '' : 'hidden'}`}
|
|
||||||
></iframe>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-20 mt-32">
|
<div className="space-y-20 mt-32">
|
||||||
<div className="mx-auto grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div className="mx-auto grid grid-cols-1 gap-8">
|
||||||
<div className="flex flex-col justify-center text-center lg:text-left ">
|
<div className="flex flex-col justify-center text-center">
|
||||||
<h2 className="text-4xl font-extrabold sm:text-5xl">
|
<h2 className="text-4xl font-extrabold sm:text-5xl">
|
||||||
Clone. Build. Ship.
|
Clone. Build. Ship.
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-lg text-foreground">
|
<p className="mt-4 text-lg text-foreground">
|
||||||
Build your SaaS faster with our fully customizable template.
|
Build your SaaS faster with our fully customizable template.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center lg:justify-start items-center mt-4">
|
<div className="flex justify-center items-center mt-4">
|
||||||
<Link href="/overview">
|
<Link href="/overview">
|
||||||
<Button className="gap-2">
|
<Button className="gap-2">
|
||||||
<Sparkles className="h-5 w-5" />
|
<Sparkles className="h-5 w-5" />
|
||||||
|
|
@ -58,9 +24,6 @@ export const Header = () => {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center rounded-lg overflow-hidden">
|
|
||||||
<IframeWithSkeleton />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { ModeToggle } from '@/components/mode-toggle'
|
import { ModeToggle } from '@/components/mode-toggle'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { UserButton } from '@/components/user-button'
|
import { UserButton } from '@/components/user-button'
|
||||||
import { MobileSidebar } from '@/components/mobile-sidebar'
|
import { MobileSidebar } from '@/components/mobile-sidebar'
|
||||||
import { Logo } from '@/components/logo'
|
import { Logo } from '@/components/logo'
|
||||||
|
import { usePathname } from 'next/navigation'
|
||||||
|
|
||||||
export const navPages = [
|
export const navPages = [
|
||||||
{
|
{
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
link: '/dashboard'
|
link: '/dashboard'
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Pricing',
|
|
||||||
link: '/#pricing'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Items',
|
|
||||||
link: '/#items'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const Navbar = () => {
|
export const Navbar = () => {
|
||||||
|
const pathname = usePathname()
|
||||||
|
|
||||||
|
// Don't show navbar on home page
|
||||||
|
if (pathname === '/') return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="top-0 w-full z-50 transition">
|
<nav className="top-0 w-full z-50 transition">
|
||||||
<div className="max-w-6xl mx-auto px-6 py-4">
|
<div className="max-w-6xl mx-auto px-6 py-4">
|
||||||
|
|
|
||||||
91
components/project-select.tsx
Normal file
91
components/project-select.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { PlusCircle } from "lucide-react"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Label } from "@/components/ui/label"
|
||||||
|
|
||||||
|
export function ProjectSelect() {
|
||||||
|
const [open, setOpen] = React.useState(false)
|
||||||
|
const [projectName, setProjectName] = React.useState("")
|
||||||
|
|
||||||
|
// This would be fetched from your backend
|
||||||
|
const projects = [
|
||||||
|
{ id: "1", name: "Marketing Website" },
|
||||||
|
{ id: "2", name: "Mobile App" },
|
||||||
|
{ id: "3", name: "Dashboard Redesign" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleCreateProject = () => {
|
||||||
|
// Handle project creation here
|
||||||
|
setOpen(false)
|
||||||
|
setProjectName("")
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Select>
|
||||||
|
<SelectTrigger className="w-[200px]">
|
||||||
|
<SelectValue placeholder="Select project" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Projects</SelectLabel>
|
||||||
|
{projects.map((project) => (
|
||||||
|
<SelectItem key={project.id} value={project.id}>
|
||||||
|
{project.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" size="icon">
|
||||||
|
<PlusCircle className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create New Project</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Add a new project to organize your tasks.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="name">Project name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={projectName}
|
||||||
|
onChange={(e) => setProjectName(e.target.value)}
|
||||||
|
placeholder="Enter project name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onClick={handleCreateProject}>Create Project</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
101
components/rich-text-editor.tsx
Normal file
101
components/rich-text-editor.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { useEditor, EditorContent } from '@tiptap/react'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { Toggle } from "@/components/ui/toggle"
|
||||||
|
import {
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
List,
|
||||||
|
ListOrdered,
|
||||||
|
Quote,
|
||||||
|
Heading2,
|
||||||
|
Code,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const MenuBar = ({ editor }: { editor: any }) => {
|
||||||
|
if (!editor) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-b flex flex-wrap gap-1 p-1">
|
||||||
|
<Toggle
|
||||||
|
size="sm"
|
||||||
|
pressed={editor.isActive('bold')}
|
||||||
|
onPressedChange={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
>
|
||||||
|
<Bold className="h-4 w-4" />
|
||||||
|
</Toggle>
|
||||||
|
<Toggle
|
||||||
|
size="sm"
|
||||||
|
pressed={editor.isActive('italic')}
|
||||||
|
onPressedChange={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
>
|
||||||
|
<Italic className="h-4 w-4" />
|
||||||
|
</Toggle>
|
||||||
|
<Toggle
|
||||||
|
size="sm"
|
||||||
|
pressed={editor.isActive('heading')}
|
||||||
|
onPressedChange={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||||
|
>
|
||||||
|
<Heading2 className="h-4 w-4" />
|
||||||
|
</Toggle>
|
||||||
|
<Toggle
|
||||||
|
size="sm"
|
||||||
|
pressed={editor.isActive('bulletList')}
|
||||||
|
onPressedChange={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
>
|
||||||
|
<List className="h-4 w-4" />
|
||||||
|
</Toggle>
|
||||||
|
<Toggle
|
||||||
|
size="sm"
|
||||||
|
pressed={editor.isActive('orderedList')}
|
||||||
|
onPressedChange={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
>
|
||||||
|
<ListOrdered className="h-4 w-4" />
|
||||||
|
</Toggle>
|
||||||
|
<Toggle
|
||||||
|
size="sm"
|
||||||
|
pressed={editor.isActive('blockquote')}
|
||||||
|
onPressedChange={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
>
|
||||||
|
<Quote className="h-4 w-4" />
|
||||||
|
</Toggle>
|
||||||
|
<Toggle
|
||||||
|
size="sm"
|
||||||
|
pressed={editor.isActive('code')}
|
||||||
|
onPressedChange={() => editor.chain().focus().toggleCode().run()}
|
||||||
|
>
|
||||||
|
<Code className="h-4 w-4" />
|
||||||
|
</Toggle>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RichTextEditorProps {
|
||||||
|
content?: string
|
||||||
|
onChange?: (content: string) => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RichTextEditor({ content, onChange, className }: RichTextEditorProps) {
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [
|
||||||
|
StarterKit,
|
||||||
|
],
|
||||||
|
content,
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
onChange?.(editor.getHTML())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("border rounded-md", className)}>
|
||||||
|
<MenuBar editor={editor} />
|
||||||
|
<EditorContent
|
||||||
|
editor={editor}
|
||||||
|
className="prose prose-sm dark:prose-invert max-w-none p-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
54
components/task-card.tsx
Normal file
54
components/task-card.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { useSortable } from "@dnd-kit/sortable"
|
||||||
|
import { CSS } from "@dnd-kit/utilities"
|
||||||
|
import { Card } from "@/components/ui/card"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
interface TaskCardProps {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
dueDate: string
|
||||||
|
progress: number
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskCard({ id, title, dueDate, progress, status }: TaskCardProps) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({
|
||||||
|
id: id,
|
||||||
|
data: {
|
||||||
|
type: "Task",
|
||||||
|
task: { id, title, dueDate, progress, status },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
"p-3 cursor-move hover:border-primary/50 transition-colors",
|
||||||
|
isDragging && "opacity-50 border-dashed"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<h5 className="font-medium">{title}</h5>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">Due: {dueDate}</p>
|
||||||
|
<div className="mt-3 h-1.5 w-full bg-secondary rounded-full">
|
||||||
|
<div
|
||||||
|
className="h-full bg-primary rounded-full transition-all"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
52
components/task-column.tsx
Normal file
52
components/task-column.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { useDroppable } from "@dnd-kit/core"
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from "@dnd-kit/sortable"
|
||||||
|
import { Card } from "@/components/ui/card"
|
||||||
|
import { TaskCard } from "./task-card"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { LucideIcon } from "lucide-react"
|
||||||
|
|
||||||
|
interface Task {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
dueDate: string
|
||||||
|
progress: number
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskColumnProps {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
icon: LucideIcon
|
||||||
|
tasks: Task[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TaskColumn({ id, title, icon: Icon, tasks }: TaskColumnProps) {
|
||||||
|
const { setNodeRef, isOver } = useDroppable({
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const taskIds = tasks.map(task => task.id)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={cn("p-4", isOver && "ring-2 ring-primary")}>
|
||||||
|
<h4 className="font-medium mb-4 flex items-center">
|
||||||
|
<Icon className="h-4 w-4 mr-2" />
|
||||||
|
{title}
|
||||||
|
</h4>
|
||||||
|
<div ref={setNodeRef} className="space-y-4">
|
||||||
|
<SortableContext
|
||||||
|
id={id}
|
||||||
|
items={taskIds}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
{tasks.map((task) => (
|
||||||
|
<TaskCard key={task.id} {...task} />
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
42
components/ui/toggle.tsx
Normal file
42
components/ui/toggle.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const toggleVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-transparent",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-3",
|
||||||
|
sm: "h-9 px-2.5",
|
||||||
|
lg: "h-11 px-5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const Toggle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TogglePrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
|
||||||
|
VariantProps<typeof toggleVariants>
|
||||||
|
>(({ className, variant, size, ...props }, ref) => (
|
||||||
|
<TogglePrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toggleVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
Toggle.displayName = TogglePrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Toggle, toggleVariants }
|
||||||
|
|
@ -18,12 +18,12 @@ import {
|
||||||
export const LinkEmail = ({ token }: LinkEmailProps) => (
|
export const LinkEmail = ({ token }: LinkEmailProps) => (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
<Preview>Log in with this link</Preview>
|
<Preview>Verify your email</Preview>
|
||||||
<Body style={main}>
|
<Body style={main}>
|
||||||
<Container style={container}>
|
<Container style={container}>
|
||||||
<Heading style={h1}>Login</Heading>
|
<Heading style={h1}>Email Verification</Heading>
|
||||||
<Link
|
<Link
|
||||||
href={`${process.env.APP_URL}/verify-email?token=${token}`}
|
href={`${process.env.APP_URL}/new-verification?token=${token}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
style={{
|
style={{
|
||||||
...link,
|
...link,
|
||||||
|
|
@ -41,7 +41,7 @@ import {
|
||||||
marginBottom: '16px'
|
marginBottom: '16px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
If you didn't try to login, you can safely ignore this email.
|
If you didn't request this verification, you can safely ignore this email.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
|
|
@ -71,10 +71,6 @@ import {
|
||||||
</Html>
|
</Html>
|
||||||
)
|
)
|
||||||
|
|
||||||
LinkEmail.PreviewProps = {
|
|
||||||
loginCode: 'sparo-ndigo-amurt-secan'
|
|
||||||
} as LinkEmailProps
|
|
||||||
|
|
||||||
export default LinkEmail
|
export default LinkEmail
|
||||||
|
|
||||||
const main = {
|
const main = {
|
||||||
|
|
@ -122,13 +118,3 @@ import {
|
||||||
marginTop: '12px',
|
marginTop: '12px',
|
||||||
marginBottom: '24px'
|
marginBottom: '24px'
|
||||||
}
|
}
|
||||||
|
|
||||||
const code = {
|
|
||||||
display: 'inline-block',
|
|
||||||
padding: '16px 4.5%',
|
|
||||||
width: '90.5%',
|
|
||||||
backgroundColor: '#f4f4f4',
|
|
||||||
borderRadius: '5px',
|
|
||||||
border: '1px solid #eee',
|
|
||||||
color: '#333'
|
|
||||||
}
|
|
||||||
943
package-lock.json
generated
943
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.1.0",
|
"@auth/prisma-adapter": "^2.1.0",
|
||||||
|
"@dnd-kit/core": "^6.2.0",
|
||||||
|
"@dnd-kit/sortable": "^9.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@mui/material": "^5.15.18",
|
"@mui/material": "^5.15.18",
|
||||||
"@neondatabase/serverless": "^0.9.1",
|
"@neondatabase/serverless": "^0.9.1",
|
||||||
|
|
@ -33,9 +36,13 @@
|
||||||
"@radix-ui/react-switch": "^1.1.0",
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
"@radix-ui/react-tabs": "^1.1.1",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@react-email/components": "^0.0.19",
|
"@react-email/components": "^0.0.19",
|
||||||
"@react-email/render": "^0.0.16",
|
"@react-email/render": "^0.0.16",
|
||||||
|
"@tiptap/pm": "^2.10.2",
|
||||||
|
"@tiptap/react": "^2.10.2",
|
||||||
|
"@tiptap/starter-kit": "^2.10.2",
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
|
@ -57,6 +64,7 @@
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-dropzone": "^14.3.5",
|
||||||
"react-email": "^2.1.4",
|
"react-email": "^2.1.4",
|
||||||
"react-fast-marquee": "^1.6.4",
|
"react-fast-marquee": "^1.6.4",
|
||||||
"react-hook-form": "^7.51.4",
|
"react-hook-form": "^7.51.4",
|
||||||
|
|
|
||||||
36
routes.ts
Normal file
36
routes.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* An array of routes that are accessible to the public
|
||||||
|
* These routes do not require authentication
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
export const publicRoutes = [
|
||||||
|
'/',
|
||||||
|
'/new-verification',
|
||||||
|
'/verify-email'
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of routes that are used for authentication
|
||||||
|
* These routes will redirect logged in users to /dashboard
|
||||||
|
* @type {string[]}
|
||||||
|
*/
|
||||||
|
export const authRoutes = [
|
||||||
|
'/login',
|
||||||
|
'/register',
|
||||||
|
'/error',
|
||||||
|
'/reset',
|
||||||
|
'/new-verification'
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The prefix for API authentication routes
|
||||||
|
* Routes that start with this prefix are used for API authentication purposes
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export const apiAuthPrefix = '/api/auth'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default redirect path after logging in
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export const DEFAULT_LOGIN_REDIRECT = '/dashboard'
|
||||||
Loading…
Add table
Add a link
Reference in a new issue