initial commit

This commit is contained in:
Harivansh Rathi 2024-11-24 20:56:03 -05:00
commit ef9ccf22d3
133 changed files with 20802 additions and 0 deletions

View 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

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View 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'

View file

@ -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>
)
}

View 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>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View 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>
)
}

View 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>
)
}

View 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
)}
/>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View file

@ -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>
)
}

View 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

View 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>
)
}

View file

@ -0,0 +1 @@
export { GET, POST } from '@/auth'

64
app/api/checkout/route.ts Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

88
app/globals.css Normal file
View 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
View 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
View 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
View 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>
</>
)
}