mirror of
https://github.com/harivansh-afk/Saas-Teamspace.git
synced 2026-04-20 20:01:13 +00:00
initial commit
This commit is contained in:
commit
ef9ccf22d3
133 changed files with 20802 additions and 0 deletions
7
app/(root)/(routes)/(auth)/layout.tsx
Normal file
7
app/(root)/(routes)/(auth)/layout.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
const AuthLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<main className="w-full flex items-center justify-center">{children}</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthLayout
|
||||
124
app/(root)/(routes)/(auth)/login/page.tsx
Normal file
124
app/(root)/(routes)/(auth)/login/page.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
'use client'
|
||||
import * as z from 'zod'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/form'
|
||||
import { LoginSchema } from '@/schemas'
|
||||
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 { useEffect, useRef, useTransition } from 'react'
|
||||
import { login } from '@/actions/login'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams()
|
||||
const urlError =
|
||||
searchParams.get('error') === 'OAuthAccountNotLinked'
|
||||
? 'Email already in use with different provider!'
|
||||
: ''
|
||||
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const hasDisplayedError = useRef(false)
|
||||
useEffect(() => {
|
||||
if (urlError && !hasDisplayedError.current) {
|
||||
toast.error(urlError)
|
||||
hasDisplayedError.current = true
|
||||
}
|
||||
}, [urlError])
|
||||
|
||||
const form = useForm<z.infer<typeof LoginSchema>>({
|
||||
resolver: zodResolver(LoginSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: ''
|
||||
}
|
||||
})
|
||||
|
||||
const onSubmit = (values: z.infer<typeof LoginSchema>) => {
|
||||
startTransition(() => {
|
||||
login(values).then((data) => {
|
||||
if (data?.error) {
|
||||
toast.error(data.error)
|
||||
}
|
||||
if (data?.success) {
|
||||
toast.success(data.success)
|
||||
form.reset({ email: '', password: '' })
|
||||
window.location.href = '/'
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
return (
|
||||
<CardWrapper
|
||||
headerTitle="Login"
|
||||
backButtonLabel="Don't have an account?"
|
||||
backButtonHref="/register"
|
||||
showSocial
|
||||
>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-1">
|
||||
<div className="space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="tylerdurden@gmail.com"
|
||||
disabled={isPending}
|
||||
type="email"
|
||||
className="bg-background/50 dark:bg-background/30 ring-foreground/5"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-red-500" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="••••••••"
|
||||
disabled={isPending}
|
||||
type="password"
|
||||
className="bg-background/50 dark:bg-background/30 ring-foreground/5"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-red-500" />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="link"
|
||||
className="px-0 text-blue-500"
|
||||
>
|
||||
<Link href="/reset">Forgot Password?</Link>
|
||||
</Button>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button className="w-full" disabled={isPending} type="submit">
|
||||
Login
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardWrapper>
|
||||
)
|
||||
}
|
||||
67
app/(root)/(routes)/(auth)/new-password/page.tsx
Normal file
67
app/(root)/(routes)/(auth)/new-password/page.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
'use client'
|
||||
|
||||
import { CardWrapper } from '@/components/auth/card-wrapper'
|
||||
import { newVerification } from '@/actions/new-verification'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
export default function NewVerificationForm() {
|
||||
const [error, setError] = useState<string | undefined>()
|
||||
const [success, setSuccess] = useState<string | undefined>()
|
||||
const [hasErrorToastShown, setHasErrorToastShown] = useState<boolean>(false)
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const token = searchParams.get('token')
|
||||
const router = useRouter()
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (!token) {
|
||||
toast.error('No token provided')
|
||||
return
|
||||
}
|
||||
newVerification(token)
|
||||
.then((data) => {
|
||||
if (data?.error) {
|
||||
setTimeout(() => {
|
||||
setError(data.error)
|
||||
}, 500)
|
||||
} else if (data?.success) {
|
||||
toast.success(data.success)
|
||||
setTimeout(() => {
|
||||
router.push('/login')
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
const errorMessage = 'Something went wrong'
|
||||
setError(errorMessage)
|
||||
})
|
||||
}, [token, router])
|
||||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<CardWrapper
|
||||
headerTitle="Verify your email"
|
||||
backButtonLabel="Back to Login"
|
||||
backButtonHref="/login"
|
||||
>
|
||||
<div className="flex items-center w-full justify-center">
|
||||
{!success && !error && <p>Verifying...</p>}
|
||||
</div>
|
||||
</CardWrapper>
|
||||
)
|
||||
}
|
||||
67
app/(root)/(routes)/(auth)/new-verification/page.tsx
Normal file
67
app/(root)/(routes)/(auth)/new-verification/page.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
'use client'
|
||||
|
||||
import { CardWrapper } from '@/components/auth/card-wrapper'
|
||||
import { newVerification } from '@/actions/new-verification'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
export default function NewVerificationForm() {
|
||||
const [error, setError] = useState<string | undefined>()
|
||||
const [success, setSuccess] = useState<string | undefined>()
|
||||
const [hasErrorToastShown, setHasErrorToastShown] = useState<boolean>(false)
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const token = searchParams.get('token')
|
||||
const router = useRouter()
|
||||
|
||||
const onSubmit = useCallback(() => {
|
||||
if (!token) {
|
||||
toast.error('No token provided')
|
||||
return
|
||||
}
|
||||
newVerification(token)
|
||||
.then((data) => {
|
||||
if (data?.error) {
|
||||
setTimeout(() => {
|
||||
setError(data.error)
|
||||
}, 500)
|
||||
} else if (data?.success) {
|
||||
toast.success(data.success)
|
||||
setTimeout(() => {
|
||||
router.push('/login')
|
||||
}, 100)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
const errorMessage = 'Something went wrong'
|
||||
setError(errorMessage)
|
||||
})
|
||||
}, [token, router])
|
||||
|
||||
useEffect(() => {
|
||||
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 (
|
||||
<CardWrapper
|
||||
headerTitle="Verify your email"
|
||||
backButtonLabel="Back to Login"
|
||||
backButtonHref="/login"
|
||||
>
|
||||
<div className="flex items-center w-full justify-center">
|
||||
{!success && !error && <p>Verifying...</p>}
|
||||
</div>
|
||||
</CardWrapper>
|
||||
)
|
||||
}
|
||||
126
app/(root)/(routes)/(auth)/register/page.tsx
Normal file
126
app/(root)/(routes)/(auth)/register/page.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
'use client'
|
||||
import * as z from 'zod'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/form'
|
||||
import { RegisterSchema } from '@/schemas'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { CardWrapper } from '@/components/auth/card-wrapper'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTransition } from 'react'
|
||||
import { register } from '@/actions/register'
|
||||
import toast from 'react-hot-toast'
|
||||
|
||||
export default function Page() {
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const form = useForm<z.infer<typeof RegisterSchema>>({
|
||||
resolver: zodResolver(RegisterSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
name: ''
|
||||
}
|
||||
})
|
||||
|
||||
const onSubmit = (values: z.infer<typeof RegisterSchema>) => {
|
||||
startTransition(() => {
|
||||
register(values).then((data) => {
|
||||
if (data?.error) {
|
||||
toast.error(data.error)
|
||||
}
|
||||
if (data?.success) {
|
||||
toast.success(data.success)
|
||||
form.reset({ email: '', password: '', name: '' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<CardWrapper
|
||||
headerTitle="Register"
|
||||
backButtonLabel="Already have an account?"
|
||||
backButtonHref="/login"
|
||||
showSocial
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-1 w-full"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Tyler Durden"
|
||||
disabled={isPending}
|
||||
type="name"
|
||||
className="bg-background/50 dark:bg-background/30 ring-foreground/5"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-red-500" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="tylerdurden@gmail.com"
|
||||
disabled={isPending}
|
||||
type="email"
|
||||
className="bg-background/50 dark:bg-background/30 ring-foreground/5"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-red-500" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="••••••••"
|
||||
disabled={isPending}
|
||||
type="password"
|
||||
className="bg-background/50 dark:bg-background/30 ring-foreground/5"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-red-500 " />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div></div>
|
||||
</div>
|
||||
<Button className="w-full" disabled={isPending} type="submit">
|
||||
Register
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardWrapper>
|
||||
)
|
||||
}
|
||||
86
app/(root)/(routes)/(auth)/reset/page.tsx
Normal file
86
app/(root)/(routes)/(auth)/reset/page.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
'use client'
|
||||
import * as z from 'zod'
|
||||
import { CardWrapper } from '@/components/auth/card-wrapper'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/form'
|
||||
import { ResetSchema } from '@/schemas'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { useState, useTransition } from 'react'
|
||||
import { reset } from '@/actions/reset'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
export default function ResetForm() {
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const form = useForm<z.infer<typeof ResetSchema>>({
|
||||
resolver: zodResolver(ResetSchema),
|
||||
defaultValues: {
|
||||
email: ''
|
||||
}
|
||||
})
|
||||
|
||||
const onSubmit = (values: z.infer<typeof ResetSchema>) => {
|
||||
startTransition(() => {
|
||||
reset(values).then((data) => {
|
||||
if (data?.error) {
|
||||
toast.error(data.error)
|
||||
}
|
||||
if (data?.success) {
|
||||
toast.success(data.success)
|
||||
form.reset({ email: '' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<CardWrapper
|
||||
headerTitle="Password Reset"
|
||||
backButtonLabel="Back to login"
|
||||
backButtonHref="/login"
|
||||
>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4 w-full"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="tylerdurden@gmail.com"
|
||||
disabled={isPending}
|
||||
type="email"
|
||||
className="bg-background/50 dark:bg-background/30 ring-foreground/5"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-red-500" />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button className="w-full mt-4" disabled={isPending} type="submit">
|
||||
Send Reset Email
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</CardWrapper>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Bar, BarChart, XAxis, YAxis } from 'recharts'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@/components/ui/card'
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent
|
||||
} from '@/components/ui/chart'
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' }
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors'
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))'
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))'
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))'
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))'
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))'
|
||||
}
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function BarChartMixed() {
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="w-full h-[200px]">
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
layout="vertical"
|
||||
margin={{
|
||||
left: 0
|
||||
}}
|
||||
>
|
||||
<YAxis
|
||||
dataKey="browser"
|
||||
type="category"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) =>
|
||||
chartConfig[value as keyof typeof chartConfig]?.label
|
||||
}
|
||||
/>
|
||||
<XAxis dataKey="visitors" type="number" hide />
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Bar dataKey="visitors" layout="vertical" radius={5} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from 'recharts'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@/components/ui/card'
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent
|
||||
} from '@/components/ui/chart'
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 }
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))'
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))'
|
||||
}
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function BarChartMultiple() {
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="w-full h-[200px]">
|
||||
<BarChart accessibilityLayer data={chartData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dashed" />}
|
||||
/>
|
||||
<Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
|
||||
<Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
6
app/(root)/(routes)/customize/_components/index.ts
Normal file
6
app/(root)/(routes)/customize/_components/index.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export { BarChartMixed } from './bar-chart-mixed'
|
||||
export { BarChartMultiple } from './bar-chart-multiple'
|
||||
export { LineChartMultiple } from './line-chart-multiple'
|
||||
export { PieChartDonut } from './pie-chart-donut'
|
||||
export { RadarChartLines } from './radar-chart-lines'
|
||||
export { RadialChartGrid } from './radial-chart-grid'
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { CartesianGrid, Line, LineChart, XAxis } from 'recharts'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@/components/ui/card'
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent
|
||||
} from '@/components/ui/chart'
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186, mobile: 80 },
|
||||
{ month: 'February', desktop: 305, mobile: 200 },
|
||||
{ month: 'March', desktop: 237, mobile: 120 },
|
||||
{ month: 'April', desktop: 73, mobile: 190 },
|
||||
{ month: 'May', desktop: 209, mobile: 130 },
|
||||
{ month: 'June', desktop: 214, mobile: 140 }
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))'
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--chart-2))'
|
||||
}
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function LineChartMultiple() {
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="w-full h-[200px]">
|
||||
<LineChart
|
||||
accessibilityLayer
|
||||
data={chartData}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="month"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<Line
|
||||
dataKey="desktop"
|
||||
type="monotone"
|
||||
stroke="var(--color-desktop)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
<Line
|
||||
dataKey="mobile"
|
||||
type="monotone"
|
||||
stroke="var(--color-mobile)"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
</LineChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
110
app/(root)/(routes)/customize/_components/pie-chart-donut.tsx
Normal file
110
app/(root)/(routes)/customize/_components/pie-chart-donut.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { Label, Pie, PieChart } from 'recharts'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@/components/ui/card'
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent
|
||||
} from '@/components/ui/chart'
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 287, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 190, fill: 'var(--color-other)' }
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors'
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))'
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))'
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))'
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))'
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))'
|
||||
}
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function PieChartDonut() {
|
||||
const totalVisitors = React.useMemo(() => {
|
||||
return chartData.reduce((acc, curr) => acc + curr.visitors, 0)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-square w-full h-[200px]"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel />}
|
||||
/>
|
||||
<Pie
|
||||
data={chartData}
|
||||
dataKey="visitors"
|
||||
nameKey="browser"
|
||||
innerRadius={60}
|
||||
strokeWidth={5}
|
||||
>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="fill-foreground text-3xl font-bold"
|
||||
>
|
||||
{totalVisitors.toLocaleString()}
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 24}
|
||||
className="fill-muted-foreground"
|
||||
>
|
||||
Visitors
|
||||
</tspan>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
'use client'
|
||||
|
||||
import { PolarAngleAxis, PolarGrid, Radar, RadarChart } from 'recharts'
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent
|
||||
} from '@/components/ui/chart'
|
||||
const chartData = [
|
||||
{ month: 'January', desktop: 186 },
|
||||
{ month: 'February', desktop: 285 },
|
||||
{ month: 'March', desktop: 237 },
|
||||
{ month: 'April', desktop: 203 },
|
||||
{ month: 'May', desktop: 209 },
|
||||
{ month: 'June', desktop: 264 }
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--chart-1))'
|
||||
}
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function RadarChartLines() {
|
||||
return (
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="w-full aspect-square h-[200px]"
|
||||
>
|
||||
<RadarChart data={chartData}>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<PolarGrid
|
||||
className="fill-[--color-desktop] opacity-20"
|
||||
gridType="circle"
|
||||
/>
|
||||
<PolarAngleAxis dataKey="month" />
|
||||
<Radar
|
||||
dataKey="desktop"
|
||||
fill="var(--color-desktop)"
|
||||
fillOpacity={0.5}
|
||||
/>
|
||||
</RadarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import { PolarGrid, RadialBar, RadialBarChart } from 'recharts'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@/components/ui/card'
|
||||
import {
|
||||
ChartConfig,
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent
|
||||
} from '@/components/ui/chart'
|
||||
const chartData = [
|
||||
{ browser: 'chrome', visitors: 275, fill: 'var(--color-chrome)' },
|
||||
{ browser: 'safari', visitors: 200, fill: 'var(--color-safari)' },
|
||||
{ browser: 'firefox', visitors: 187, fill: 'var(--color-firefox)' },
|
||||
{ browser: 'edge', visitors: 173, fill: 'var(--color-edge)' },
|
||||
{ browser: 'other', visitors: 90, fill: 'var(--color-other)' }
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors'
|
||||
},
|
||||
chrome: {
|
||||
label: 'Chrome',
|
||||
color: 'hsl(var(--chart-1))'
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))'
|
||||
},
|
||||
firefox: {
|
||||
label: 'Firefox',
|
||||
color: 'hsl(var(--chart-3))'
|
||||
},
|
||||
edge: {
|
||||
label: 'Edge',
|
||||
color: 'hsl(var(--chart-4))'
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
color: 'hsl(var(--chart-5))'
|
||||
}
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function RadialChartGrid() {
|
||||
return (
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-square w-full h-[200px]"
|
||||
>
|
||||
<RadialBarChart data={chartData} innerRadius={30} outerRadius={100}>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent hideLabel nameKey="browser" />}
|
||||
/>
|
||||
<PolarGrid gridType="circle" />
|
||||
<RadialBar dataKey="visitors" />
|
||||
</RadialBarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
'use client'
|
||||
|
||||
import { TrendingUp } from 'lucide-react'
|
||||
import {
|
||||
Label,
|
||||
PolarGrid,
|
||||
PolarRadiusAxis,
|
||||
RadialBar,
|
||||
RadialBarChart
|
||||
} from 'recharts'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@/components/ui/card'
|
||||
import { ChartConfig, ChartContainer } from '@/components/ui/chart'
|
||||
const chartData = [
|
||||
{ browser: 'safari', visitors: 1260, fill: 'var(--color-safari)' }
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
visitors: {
|
||||
label: 'Visitors'
|
||||
},
|
||||
safari: {
|
||||
label: 'Safari',
|
||||
color: 'hsl(var(--chart-2))'
|
||||
}
|
||||
} satisfies ChartConfig
|
||||
|
||||
export function RadialChartShape() {
|
||||
return (
|
||||
<ChartContainer
|
||||
config={chartConfig}
|
||||
className="aspect-square w-full h-[200px]"
|
||||
>
|
||||
<RadialBarChart
|
||||
data={chartData}
|
||||
endAngle={100}
|
||||
innerRadius={80}
|
||||
outerRadius={140}
|
||||
>
|
||||
<PolarGrid
|
||||
gridType="circle"
|
||||
radialLines={false}
|
||||
stroke="none"
|
||||
className="first:fill-muted last:fill-background"
|
||||
polarRadius={[86, 74]}
|
||||
/>
|
||||
<RadialBar dataKey="visitors" background />
|
||||
<PolarRadiusAxis tick={false} tickLine={false} axisLine={false}>
|
||||
<Label
|
||||
content={({ viewBox }) => {
|
||||
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
|
||||
return (
|
||||
<text
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={viewBox.cy}
|
||||
className="fill-foreground text-4xl font-bold"
|
||||
>
|
||||
{chartData[0].visitors.toLocaleString()}
|
||||
</tspan>
|
||||
<tspan
|
||||
x={viewBox.cx}
|
||||
y={(viewBox.cy || 0) + 24}
|
||||
className="fill-muted-foreground"
|
||||
>
|
||||
Visitors
|
||||
</tspan>
|
||||
</text>
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PolarRadiusAxis>
|
||||
</RadialBarChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
188
app/(root)/(routes)/customize/page.tsx
Normal file
188
app/(root)/(routes)/customize/page.tsx
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
'use client'
|
||||
import { ColorPicker } from '@/components/color-picker'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { TriangleAlert } from 'lucide-react'
|
||||
import React, { useEffect, useState, useTransition } from 'react'
|
||||
import {
|
||||
BarChartMixed,
|
||||
BarChartMultiple,
|
||||
PieChartDonut,
|
||||
LineChartMultiple,
|
||||
RadialChartGrid,
|
||||
RadarChartLines
|
||||
} from './_components'
|
||||
import { Calendar } from '@/components/ui/calendar'
|
||||
import { DateRange } from 'react-day-picker'
|
||||
import { RadialChartShape } from './_components/radial-chart-shape'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { toast } from 'react-hot-toast'
|
||||
|
||||
export default function () {
|
||||
const [progress, setProgress] = useState(0)
|
||||
const [email, setEmail] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setProgress((prevProgress) => {
|
||||
const newProgress = (prevProgress + 10) % 101
|
||||
return newProgress
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault()
|
||||
const email = e.target.email.value
|
||||
try {
|
||||
const response = await fetch('/api/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ email })
|
||||
})
|
||||
const data = await response.json()
|
||||
|
||||
if (data.message) {
|
||||
toast.success(data.message)
|
||||
setEmail('')
|
||||
} else {
|
||||
console.error(data, 'ha')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="w-full max-w-6xl flex flex-col gap-16 p-6">
|
||||
<div className="flex flex-col sm:flex-row w-fit mx-auto md:max-w-md gap-2 rounded-lg bg-yellow-300/50 border-2 border-yellow-500/75 p-4 text-foreground">
|
||||
<TriangleAlert className="w-8 h-8 sm:w-6 sm:h-6 mx-auto sm:mx-0" />
|
||||
<span className="w-fit">
|
||||
If you refresh the page, it will reset the colors. Update{' '}
|
||||
<code className="rounded-lg p-1 bg-foreground/10">globals.css</code>{' '}
|
||||
with your new colors.
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col w-fit mx-auto max-w-xs xs:max-w-xl lg:max-w-full lg:mx-0 lg:flex-row lg:w-full justify-between gap-8">
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 w-fit xs:w-full mx-auto sm:mx-0 gap-8">
|
||||
<div className="w-min sm:w-max flex flex-col items-center mx-auto gap-2 text-lg md:text-xl sm:whitespace-nowrap">
|
||||
<span>Primary color</span>
|
||||
<ColorPicker variable="--primary" />
|
||||
</div>
|
||||
<div className="w-min sm:w-max flex flex-col items-center mx-auto gap-2 text-lg md:text-xl sm:whitespace-nowrap">
|
||||
<span>Primary text color</span>
|
||||
<ColorPicker variable="--primary-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-fit flex flex-col gap-6 text-lg md:text-xl sm:whitespace-nowrap">
|
||||
<span>Components</span>
|
||||
<div className="flex flex-wrap justify-center items-center sm:justify-start w-full h-fit gap-6">
|
||||
<Button variant="default">Default</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Checkbox defaultChecked={true} className="w-5 h-5" />
|
||||
<Slider
|
||||
defaultValue={[50]}
|
||||
max={100}
|
||||
step={1}
|
||||
className="max-w-36"
|
||||
/>
|
||||
<Switch defaultChecked />
|
||||
<Progress value={progress} />
|
||||
<Card className="w-full bg-transparent relative">
|
||||
<CardHeader>
|
||||
<CardTitle>Get the Starter Kit</CardTitle>
|
||||
<CardDescription>
|
||||
Receive the Starter Kit for free in your mailbox.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent>
|
||||
<div className="grid w-full items-center gap-4">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<Label htmlFor="name">Email</Label>
|
||||
<Input
|
||||
name="email"
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="example@gmail.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setEmail('')}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit">Continue</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-fit mx-auto max-w-xs xs:max-w-xl lg:max-w-full lg:mx-0 lg:flex-row lg:w-full justify-between gap-8">
|
||||
<div className="grid grid-cols-1 xs:grid-cols-2 w-fit xs:w-full mx-auto sm:mx-0 gap-8">
|
||||
<div className="w-min flex flex-col items-center mx-auto gap-2 text-lg md:text-xl sm:whitespace-nowrap">
|
||||
<span>Chart Color 1</span>
|
||||
<ColorPicker variable="--chart-1" />
|
||||
</div>
|
||||
<div className="w-min flex flex-col items-center mx-auto gap-2 text-lg md:text-xl sm:whitespace-nowrap">
|
||||
<span>Chart Color 2</span>
|
||||
<ColorPicker variable="--chart-2" />
|
||||
</div>
|
||||
<div className="w-min flex flex-col items-center mx-auto gap-2 text-lg md:text-xl sm:whitespace-nowrap">
|
||||
<span>Chart Color 3</span>
|
||||
<ColorPicker variable="--chart-3" />
|
||||
</div>
|
||||
<div className="w-min flex flex-col items-center mx-auto gap-2 text-lg md:text-xl sm:whitespace-nowrap">
|
||||
<span>Chart Color 4</span>
|
||||
<ColorPicker variable="--chart-4" />
|
||||
</div>
|
||||
<div className="w-min col-span-2 flex flex-col items-center mx-auto gap-2 text-lg md:text-xl sm:whitespace-nowrap">
|
||||
<span>Chart Color 5</span>
|
||||
<ColorPicker variable="--chart-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-fit flex flex-col gap-6 text-lg md:text-xl sm:whitespace-nowrap">
|
||||
<span>Charts</span>
|
||||
<div className="flex flex-wrap max-w-[250px] xs:max-w-full mx-auto justify-center items-center sm:justify-start w-full h-fit gap-6">
|
||||
<BarChartMultiple />
|
||||
<BarChartMixed />
|
||||
<LineChartMultiple />
|
||||
<div className="flex flex-col sm:flex-row w-full">
|
||||
<PieChartDonut />
|
||||
<RadialChartGrid />
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row w-full">
|
||||
<RadarChartLines />
|
||||
<RadialChartShape />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
81
app/(root)/(routes)/dashboard/_components/barchart.tsx
Normal file
81
app/(root)/(routes)/dashboard/_components/barchart.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
'use client'
|
||||
import { useTheme } from 'next-themes'
|
||||
import {
|
||||
BarChart as BarGraph,
|
||||
ResponsiveContainer,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Bar,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'recharts'
|
||||
import { ChartConfig, ChartContainer } from "@/components/ui/chart"
|
||||
import { light_theme } from '@/lib/theme-constant'
|
||||
import { CandlestickChart } from 'lucide-react'
|
||||
import { ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"
|
||||
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: "Desktop",
|
||||
color: "hsl(var(--primary))",
|
||||
},
|
||||
mobile: {
|
||||
label: "Mobile",
|
||||
color: "hsl(var(--primary))",
|
||||
},
|
||||
} satisfies ChartConfig
|
||||
|
||||
export type BarChartProps = {
|
||||
data: { month: string; total: number }[]
|
||||
}
|
||||
|
||||
export default function BarChart({ data }: BarChartProps) {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<div className="bg-secondary dark:bg-secondary/50 shadow flex w-full flex-col gap-3 rounded-lg p-5">
|
||||
<section className="flex justify-between gap-2 pb-2">
|
||||
<p>Sales Data</p>
|
||||
<CandlestickChart className="h-4 w-4" />
|
||||
</section>
|
||||
<ChartContainer config={chartConfig} >
|
||||
<ResponsiveContainer width="100%" height={500}>
|
||||
<BarGraph
|
||||
data={data}
|
||||
margin={{ top: 20, left: -10, right: 10, bottom: 0 }}
|
||||
>
|
||||
<CartesianGrid
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey={'month'}
|
||||
tickLine={false}
|
||||
axisLine={true}
|
||||
stroke={`${theme === light_theme ? '#000' : '#fff'}`}
|
||||
fontSize={13}
|
||||
padding={{ left: 0, right: 0 }}
|
||||
/>
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<YAxis
|
||||
tickLine={false}
|
||||
axisLine={true}
|
||||
stroke={`${theme === light_theme ? '#000' : '#fff'}`}
|
||||
fontSize={13}
|
||||
padding={{ top: 0, bottom: 0 }}
|
||||
allowDecimals={false}
|
||||
tickFormatter={(value) => `$${value}`}
|
||||
/>
|
||||
|
||||
<Bar
|
||||
dataKey={'total'}
|
||||
radius={[5, 5, 0, 0]}
|
||||
stroke="hsl(var(--primary))"
|
||||
fill="hsl(var(--primary))"
|
||||
/>
|
||||
</BarGraph>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
44
app/(root)/(routes)/dashboard/_components/dashboard-card.tsx
Normal file
44
app/(root)/(routes)/dashboard/_components/dashboard-card.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { cn } from '@/lib/utils'
|
||||
import { LucideIcon } from 'lucide-react'
|
||||
interface DashboardCardProps {
|
||||
label: string
|
||||
Icon: LucideIcon
|
||||
amount: any
|
||||
description: string
|
||||
}
|
||||
|
||||
export const DashboardCard = ({
|
||||
label,
|
||||
Icon,
|
||||
amount,
|
||||
description
|
||||
}: DashboardCardProps) => {
|
||||
return (
|
||||
<div className="bg-secondary dark:bg-secondary/50 shadow flex w-full flex-col gap-3 rounded-lg p-5">
|
||||
{/* Label & Icon */}
|
||||
<section className="flex justify-between gap-2 text-black dark:text-white">
|
||||
<p className="text-sm">{label}</p>
|
||||
<Icon className="h-4 w-4" />
|
||||
</section>
|
||||
{/* Amount & Description */}
|
||||
<section className="flex flex-col gap-1">
|
||||
<h2 className="text-2lg font-semibold">{amount}</h2>
|
||||
<p className="text-xs">{description}</p>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DashboardCardContent(
|
||||
props: React.HTMLAttributes<HTMLDivElement>
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex w-full flex-col gap-3 rounded-lg p-5 shadow bg-secondary dark:bg-secondary/50',
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
32
app/(root)/(routes)/dashboard/_components/goal.tsx
Normal file
32
app/(root)/(routes)/dashboard/_components/goal.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { Progress } from '@/components/ui/progress'
|
||||
import { Target } from 'lucide-react'
|
||||
|
||||
export type GoalDataProps = {
|
||||
value: number
|
||||
goal: number
|
||||
}
|
||||
|
||||
export default function GoalDataCard(props: GoalDataProps) {
|
||||
return (
|
||||
<div className="rounded-lg p-5 bg-secondary dark:bg-secondary/50">
|
||||
<section className="flex justify-between gap-2 text-black dark:text-white pb-2">
|
||||
<p>Goal Progress</p>
|
||||
<Target className="h-4 w-4" />
|
||||
</section>
|
||||
<div className="gap-3 pt-2">
|
||||
<section className="flex justify-between gap-3 ">
|
||||
<div className=" w-full rounded-full">
|
||||
<Progress
|
||||
value={props.value}
|
||||
className="border border-primary/20 bg-primary/20 h-2"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<div className="flex justify-between text-sm opacity-75 pt-3">
|
||||
<p>Goal: ${props.goal}</p>
|
||||
<p className="">${Math.round(props.value)} made</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
93
app/(root)/(routes)/dashboard/_components/line-graph.tsx
Normal file
93
app/(root)/(routes)/dashboard/_components/line-graph.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
'use client'
|
||||
import { useTheme } from 'next-themes'
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
ResponsiveContainer,
|
||||
LabelList
|
||||
} from 'recharts'
|
||||
import { light_theme } from '@/lib/theme-constant'
|
||||
import { User } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
CardFooter
|
||||
} from '@/components/ui/card'
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent
|
||||
} from '@/components/ui/chart'
|
||||
|
||||
export type LineGraphProps = {
|
||||
data: { month: string; users: number }[]
|
||||
}
|
||||
|
||||
export default function LineGraph({ data }: LineGraphProps) {
|
||||
const { theme } = useTheme()
|
||||
const chartConfig = {
|
||||
desktop: {
|
||||
label: 'Desktop',
|
||||
color: 'hsl(var(--primary))'
|
||||
},
|
||||
mobile: {
|
||||
label: 'Mobile',
|
||||
color: 'hsl(var(--primary))'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-none bg-secondary dark:bg-secondary/50 shadow">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-md font-normal">Number Of Users</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={chartConfig}>
|
||||
<ResponsiveContainer width="100%" height={500}>
|
||||
<LineChart
|
||||
data={data}
|
||||
margin={{ top: 20, left: 12, right: 12, bottom: 0 }}
|
||||
accessibilityLayer
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey={'month'}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
stroke={`${theme === light_theme ? '#000' : '#fff'}`}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => value.slice(0, 3)}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="line" />}
|
||||
/>
|
||||
<Line
|
||||
dataKey="users"
|
||||
type="natural"
|
||||
stroke="hsl(var(--primary))"
|
||||
strokeWidth={2}
|
||||
dot={{ fill: 'hsl(var(--primary))' }}
|
||||
activeDot={{ r: 6 }}
|
||||
>
|
||||
<LabelList
|
||||
position="top"
|
||||
offset={12}
|
||||
className="fill-foreground"
|
||||
fontSize={12}
|
||||
/>
|
||||
</Line>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
30
app/(root)/(routes)/dashboard/_components/user-data-card.tsx
Normal file
30
app/(root)/(routes)/dashboard/_components/user-data-card.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
export type UserDataProps = {
|
||||
name: string
|
||||
email: string
|
||||
image: any
|
||||
time: string
|
||||
}
|
||||
|
||||
export default function UserDataCard(props: UserDataProps) {
|
||||
const defaultImage = '/mesh.avif'
|
||||
return (
|
||||
<section className="flex justify-between gap-2 text-foreground">
|
||||
<div className="flex gap-3 h-12 w-12 rounded-full bg-secondary/30">
|
||||
<img
|
||||
width={300}
|
||||
height={300}
|
||||
src={props.image || defaultImage}
|
||||
alt="avatar"
|
||||
className="rounded-full h-12 w-12"
|
||||
/>
|
||||
<div className="text-sm">
|
||||
<p>{props.name}</p>
|
||||
<div className="text-ellipsis overflow-hidden whitespace-nowrap max-w-1/2 sm:w-auto opacity-50">
|
||||
{props.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm">{props.time}</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { CreditCard } from 'lucide-react'
|
||||
|
||||
export type UserPurchaseDataProps = {
|
||||
name: string
|
||||
email: string
|
||||
image: string
|
||||
saleAmount: string
|
||||
}
|
||||
|
||||
export default function UserPurchaseDataCard(props: UserPurchaseDataProps) {
|
||||
const defaultImage = '/mesh.avif'
|
||||
return (
|
||||
<section className="flex justify-between gap-2 text-foreground">
|
||||
<div className="flex gap-3 h-12 w-12 rounded-full bg-secondary/30">
|
||||
<img
|
||||
width={300}
|
||||
height={300}
|
||||
src={props.image || defaultImage}
|
||||
alt="avatar"
|
||||
className="rounded-full h-12 w-12"
|
||||
/>
|
||||
<div className="text-sm">
|
||||
<p>{props.name}</p>
|
||||
<div className="text-ellipsis overflow-hidden whitespace-nowrap max-w-1/2 sm:w-auto opacity-50">
|
||||
{props.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm">{props.saleAmount}</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
9
app/(root)/(routes)/dashboard/layout.tsx
Normal file
9
app/(root)/(routes)/dashboard/layout.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const DashboardLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<main className="max-w-6xl w-full flex items-center justify-center px-6">
|
||||
{children}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default DashboardLayout
|
||||
300
app/(root)/(routes)/dashboard/page.tsx
Normal file
300
app/(root)/(routes)/dashboard/page.tsx
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
import { Metadata } from 'next'
|
||||
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'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Dashboard',
|
||||
description: 'Task management and team collaboration dashboard'
|
||||
}
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const session = await auth()
|
||||
|
||||
if (!session) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
// 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 }
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
|
||||
<div className="flex items-center justify-between space-y-2">
|
||||
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add New Task
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="overview" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<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">
|
||||
<Card className="p-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<ListTodo className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Total Tasks</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">12</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">In Progress</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">4</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Completed</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">8</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-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" />
|
||||
<div>
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="tasks" className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-medium">All Tasks</h3>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Task
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Todo Column */}
|
||||
<Card className="p-4">
|
||||
<h4 className="font-medium mb-4 flex items-center">
|
||||
<ListTodo className="h-4 w-4 mr-2" />
|
||||
Todo
|
||||
</h4>
|
||||
<div className="space-y-4">
|
||||
{tasks.filter(t => t.status === 'Todo').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>
|
||||
|
||||
{/* In Progress Column */}
|
||||
<Card className="p-4">
|
||||
<h4 className="font-medium mb-4 flex items-center">
|
||||
<Clock className="h-4 w-4 mr-2" />
|
||||
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 className="grid gap-4">
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6">
|
||||
{/* Today's Schedule */}
|
||||
<div>
|
||||
<h4 className="font-medium mb-4">Today's Schedule</h4>
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
</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 */}
|
||||
<div>
|
||||
<h4 className="font-medium mb-4">Upcoming Events</h4>
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
1
app/api/auth/[...nextauth]/route.ts
Normal file
1
app/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { GET, POST } from '@/auth'
|
||||
64
app/api/checkout/route.ts
Normal file
64
app/api/checkout/route.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { auth } from '@/auth'
|
||||
import { db } from '@/lib/db'
|
||||
import { stripe } from '@/lib/stripe'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const user = await auth()
|
||||
|
||||
if (!user || !user.user.id) {
|
||||
return new NextResponse('Unauthorized', { status: 401 })
|
||||
}
|
||||
|
||||
const userSubscription = await db.userSubscription.findUnique({
|
||||
where: {
|
||||
userId: user.user.id
|
||||
}
|
||||
})
|
||||
|
||||
if (userSubscription && userSubscription.stripeCustomerId) {
|
||||
const stripeSession = await stripe.billingPortal.sessions.create({
|
||||
customer: userSubscription.stripeCustomerId,
|
||||
return_url: process.env.APP_URL
|
||||
})
|
||||
|
||||
return new NextResponse(JSON.stringify({ url: stripeSession.url }))
|
||||
}
|
||||
|
||||
const stripeSession = await stripe.checkout.sessions.create({
|
||||
success_url: process.env.APP_URL,
|
||||
cancel_url: process.env.APP_URL,
|
||||
payment_method_types: ['card'],
|
||||
|
||||
mode: 'subscription',
|
||||
billing_address_collection: 'auto',
|
||||
customer_email: user?.user.email!,
|
||||
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: 'USD',
|
||||
product_data: {
|
||||
name: 'Your SaaS Subscription Name',
|
||||
description: 'Saas Subscription Description'
|
||||
},
|
||||
// cost (change this to the price of your product)
|
||||
unit_amount: 899,
|
||||
recurring: {
|
||||
interval: 'month'
|
||||
}
|
||||
},
|
||||
quantity: 1
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
userId: user.user.id
|
||||
}
|
||||
})
|
||||
return new NextResponse(JSON.stringify({ url: stripeSession.url }))
|
||||
} catch (error) {
|
||||
console.log('[STRIPE_GET]', error)
|
||||
return new NextResponse('Internal Error', { status: 500 })
|
||||
}
|
||||
}
|
||||
47
app/api/emails/route.ts
Normal file
47
app/api/emails/route.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { Resend } from "resend"
|
||||
import RepoEmail from "@/components/email/email";
|
||||
import { render } from "@react-email/render"
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { email } = await request.json();
|
||||
|
||||
const { data, error } = await resend.emails.send({
|
||||
from: "Nizzy <noreply@nizzystarter.com>",
|
||||
to: [email],
|
||||
subject: "Nizzy Starter Kit",
|
||||
html: render(RepoEmail()),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return new NextResponse(JSON.stringify(error), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await resend.contacts.create({
|
||||
email: email,
|
||||
audienceId: process.env.RESEND_AUDIENCE as string
|
||||
});
|
||||
|
||||
return new NextResponse(JSON.stringify({ message: "Email sent successfully" }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (error:any) {
|
||||
return new NextResponse(JSON.stringify({ error: error.message }), {
|
||||
status: 500,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
96
app/api/webhook/route.ts
Normal file
96
app/api/webhook/route.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import Stripe from 'stripe'
|
||||
import { headers } from 'next/headers'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { db } from '@/lib/db'
|
||||
import { stripe } from '@/lib/stripe'
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const body = await req.text()
|
||||
const signature = headers().get('Stripe-Signature') as string
|
||||
|
||||
let event: Stripe.Event
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(
|
||||
body,
|
||||
signature,
|
||||
process.env.STRIPE_WEBHOOK_SECRET!
|
||||
)
|
||||
} catch (error: any) {
|
||||
return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 })
|
||||
}
|
||||
|
||||
const session = event.data.object as Stripe.Checkout.Session
|
||||
|
||||
// checkout.session.completed is sent after the initial purchase of the subscription from the checkout page
|
||||
if (event.type === 'checkout.session.completed') {
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
session.subscription as string
|
||||
)
|
||||
if (!session?.metadata?.userId) {
|
||||
return new NextResponse('No user id found', { status: 400 })
|
||||
}
|
||||
|
||||
await db.userSubscription.create({
|
||||
data: {
|
||||
userId: session.metadata.userId,
|
||||
stripeSubscriptionId: subscription.id,
|
||||
stripeCustomerId: subscription.customer as string,
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// invoice.payment_succeeded is sent on subscription renewals
|
||||
if (event.type === "invoice.payment_succeeded") {
|
||||
// note: sometimes the subscription we get back doesn't have the up to date current_period_end
|
||||
// which is why we also need to listen for customer.subscription.updated
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
session.subscription as string
|
||||
);
|
||||
|
||||
await db.userSubscription.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id
|
||||
},
|
||||
data: {
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// customer.subscription.updated is fired when their subscription end date changes
|
||||
if (event.type === 'customer.subscription.updated') {
|
||||
const subscriptionId = event.data.object.id as string;
|
||||
|
||||
await db.userSubscription.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscriptionId
|
||||
},
|
||||
data: {
|
||||
stripeCurrentPeriodEnd: new Date(event.data.object.current_period_end * 1000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// invoice.payment_failed if the renewal fails
|
||||
if (event.type === 'invoice.payment_failed') {
|
||||
const subscription = await stripe.subscriptions.retrieve(
|
||||
session.subscription as string
|
||||
)
|
||||
|
||||
await db.userSubscription.update({
|
||||
where: {
|
||||
stripeSubscriptionId: subscription.id
|
||||
},
|
||||
data: {
|
||||
stripePriceId: subscription.items.data[0].price.id,
|
||||
stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return new NextResponse(null, { status: 200 })
|
||||
}
|
||||
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
88
app/globals.css
Normal file
88
app/globals.css
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@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%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 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%;
|
||||
--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;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 249 11% 12%;
|
||||
--background-hover: 0 0% 13%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.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;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.disable-transitions * {
|
||||
transition-property: background-color, color !important;
|
||||
transition-duration: 0s !important;
|
||||
}
|
||||
43
app/layout.tsx
Normal file
43
app/layout.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import type { Metadata } from 'next'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
import { ThemeProvider } from '@/components/ui/theme-provider'
|
||||
import { Navbar } from '@/components/navbar'
|
||||
import { ToastProvider } from '@/components/providers/toaster-provider'
|
||||
import { SessionProvider } from 'next-auth/react'
|
||||
import { auth } from '@/auth'
|
||||
import { AlertDemo } from '@/components/alert'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Nizzy-Starter',
|
||||
description: 'The best SaaS starter kit on the web 🌎 🚀 HAHA'
|
||||
}
|
||||
|
||||
export default async function RootLayout({
|
||||
children
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
const session = await auth()
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ToastProvider />
|
||||
<AlertDemo />
|
||||
<Navbar />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
</SessionProvider>
|
||||
)
|
||||
}
|
||||
15
app/not-found.tsx
Normal file
15
app/not-found.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Error404 } from '@/components/icons'
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="flex items-center justify-center">
|
||||
<div className="flex flex-col items-center justify-center gap-2 relative">
|
||||
<Error404 className="w-[500px] h-auto absolute top-1/2 -translate-y-1/2 left-1/2 -translate-x-1/2 text-primary/5 -z-10" />
|
||||
<div className="text-6xl">
|
||||
<span>Error</span> <span className="text-primary font-bold">404</span>
|
||||
</div>
|
||||
<span className="text-4xl">Page not found!!!</span>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
77
app/page.tsx
Normal file
77
app/page.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { Footer } from '@/components/footer'
|
||||
import { Header } from '@/components/header'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<main className="w-full min-h-screen bg-gray-50">
|
||||
<div className="max-w-6xl mx-auto px-6">
|
||||
<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>
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue