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

41
.env.example Normal file
View file

@ -0,0 +1,41 @@
# Connect to your database (I use Neon. For setup you can checkout: https://youtu.be/ZAjOep8-M-Y?si=L94KHhlbV27y-V_O).
DATABASE_URL=""
DATABASE_URL_UNPOOLED=""
# Change to your domain when deploying.
APP_URL="http://localhost:3000"
# Go to: https://generate-secret.vercel.app/32 and copy the generated secret key.
AUTH_SECRET=""
# Go to https://github.com/settings/developers and click on OAuth Apps then New OAuth App. Fill in the details as follows:
# 1. Homepage URL: http://localhost:3000 (change to your domain when deploying).
# 2. Authorization callback URL: http://localhost:3000/api/auth/callback/github (change to your domain when deploying).
# 3. Click on Register Application.
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
# Go to https://console.cloud.google.com and click on 'Create Project' at the top and do as follows:
# 1. Name your project and click create.
# 2. Go to the search and look for 'APIs and Services', go to it.
# 3. Click on OAuth consent screen on the sidebar, select external and fill out the App Name, Support Email, and Developer Contact Information. You may 'Add Domain' if you have one, else leave it blank.
# 4. Click on Save and Continue until you reach the summary page.
# 5. Click on Credentials on the sidebar, click on 'Create Credentials' and select OAuth client ID.
# 6. Click on 'Web Application' and click on add URI for both Authorized JavaScript Origins and Authorized Redirect URIs. Fill in the details as follows:
# Authorized JavaScript Origins: http://localhost:3000 (change to your domain when deploying).
# Authorized Redirect URIs: http://localhost:3000/api/auth/callback/google (change to your domain when deploying).
# 7. Click on Create and copy the Client ID and Client Secret.
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
# API can be found at https://resend.com/home in the API section.
RESEND_API_KEY=""
# API Keys can be found at https://stripe.com for more setup information you can checkout: https://youtu.be/5hSEEod_BuA?si=377LS1z0ThLcs46T.
STRIPE_API_KEY=""
STRIPE_WEBHOOK_SECRET=""

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

36
.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.env
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true
}

309
README.md Normal file
View file

@ -0,0 +1,309 @@
# Nizzy Starter
An SaaS (Service as a Software) template starter kit already set up, that requires bare minimum configuration. For more details, watch [Free & Open Source SaaS Template Starter Kit](https://youtu.be/Q6jDdtbkMIU) on YouTube.
## Tech Stack
- NextJS 14
- Prisma
- TailwindCSS
## Saas Features
- Simple elegant UI
- Light and dark mode
- Manual authentication and OAuth (Google and GitHub)
- Database integration using Prisma
- Admin analytics page
- Payment system using Stripe
- Email integration with Resend and React Email
## Getting started
Here will be a step by step guide of how to setup the SaaS starter kit on your machine.
### Cloning the repository
Firstly, you need to clone the repository. You can do it like this:
```bash
# Using Git
git clone https://github.com/NizarAbiZaher/nizzy-starter.git
# Using GitHub CLI
gh repo clone NizarAbiZaher/nizzy-starter
```
Alternatively, you can also clone it using [GitHub Desktop](https://desktop.github.com/) application.
### Setup using NPM
This starter kit currently uses NPM as the package manager, if you use any other package manager follow [this section](#setup-using-different-package-managers).
Follow the steps below to correctly setup the project on your machine, and ensure everything works as expected.
First, install the modules and dependencies:
```bash
npm install
```
When that's done, you need to setup your `.env` file. On the root folder of your project you should see a file called `.env.example`. That file already contains a written guide to get the variables you need, but bellow will be the same steps in case you can't find it.
### Setup using different package managers
Follow the steps below to correctly setup the project on your machine, and ensure everything works as expected.
Since you wont use NPM, you need to remove the `package-lock.json` to prevent any possible conflicts.
Install the modules and dependencies:
```bash
# Using pnpm
pnpm install
# Using yarn
yarn install
# Using bun
bun install
```
If you dont use any of these, find the equivalent command for your package manager.
#### Create the environment variables file
On the root folder of your project, create a file called `.env` or `.env.local` and paste the following:
```env
DATABASE_URL=""
DATABASE_URL_UNPOOLED=""
APP_URL="http://localhost:3000"
AUTH_SECRET=""
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
RESEND_API_KEY=""
STRIPE_API_KEY=""
STRIPE_WEBHOOK_SECRET=""
```
You are not required to use quotes your variables, although its recommended because in some cases, due to some special characters, it can mess up the variable.
#### Connect your database
You can use any database of your choice, i like to use [Neon](https://neon.tech/). You can watch [this video](https://youtu.be/ZAjOep8-M-Y?si=L94KHhlbV27y-V_O) if you want help related to database configuration.
If you already have a database prepared and know how to use it, you can just get the Database URL and the Database URL Unpooled.
Example:
```env
DATABASE_URL="postgresql://username:password@hostname:port/database_name"
DATABASE_URL_UNPOOLED="postgresql://username:password@hostname:port/database_name"
```
#### App URL
Change this value when deploying your application.
Example:
```env
APP_URL="https://example.com/" or "http://localhost:3000/"
```
#### Auth Secret
The auth system was made using NextAuth, which required an auth secret for it to work. You can use any Secret generator, such as [https://generate-secret.vercel.app/32](https://generate-secret.vercel.app/32).
Example:
```env
AUTH_SECRET="bae23fc1c21a41743058c56a366459a2"
```
#### GitHub Client ID and Client Secret
Go to [GitHub Developers](https://github.com/settings/developers) and click on **OAuth Apps** then **New OAuth App**. Fill in the details as follows:
1. Homepage URL: http://localhost:3000 (change to your domain when deploying)
2. Authorization callback URL: http://localhost:3000/api/auth/callback/github (replace `localhost:3000` with your domain when deploying)
3. Click on Register Application
After creating a new OAuth App, copy the required Client ID and Client Secret and populate the respective variables.
Example:
```env
GITHUB_CLIENT_ID="abc123def456ghi789jkl"
GITHUB_CLIENT_SECRET="s3cr3tK3yV@lu3!"
```
Disclaimer: This was generated as an example from ChatGPT and will not work.
#### Google Client ID and Client Secret
Go to [Google Cloud Console](https://console.cloud.google.com) and click on **Create project** at the top and do as follows:
1. Name your project and click create
2. Go to the search, find and go to **APIs and Services**.
3. Click on **OAuth Consent Screen** on the sidebar, select **External** and fill out the **App Name**, **Support Email**, and **Developer Contact Information**. You may **Add Domain** if you have one, else leave it blank.
4. Click on **Save and Continue** until you reach the summary page.
5. Click on **Credentials** on the sidebar, click on **Create Credentials** and select **OAuth client ID**
6. Click on **Web Application** then on **Add URI** for both **Authorized JavaScript Origins** and **Authorized Redirect URIs**. Fill in the details as follows:
- Authorized JavaScript Origins: http://localhost:3000 (change to your domain when deploying)
- Authorized Redirect URIs: http://localhost:3000/api/auth/callback/google (change to your domain when deploying)
After creating a new OAuth App, copy the required Client ID and Client Secret and populate the respective variables.
Example:
```env
GOOGLE_CLIENT_ID="1234567890-abcde12345fghij67890klmno.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="XyZ_1234567890AbCdEfGhIj"
```
#### Resend API Key
Go to [Resend Website](https://resend.com/home) and on the **API Keys** section create a new API key. Fill the inputs and configure the settings as you wish, then copy the key and populate the respective variable.
Example:
```env
RESEND_API_KEY="re_2514c2801e44bed9a3bdf9e094e2ce1c"
```
#### Stripe API Key and Webhook Secret
You can find both at [Stripe](https://stripe.com). For more setup information you can checkout [this video](https://youtu.be/5hSEEod_BuA?si=377LS1z0ThLcs46T).
Example:
```env
STRIPE_API_KEY="sk_test_4eC39HqLyjWDarjtT1zdp7dc"
STRIPE_WEBHOOK_SECRET="whsec_1234567890abcdef1234567890abcdef"
```
#### Full `.env` file example
```env
DATABASE_URL="postgresql://username:password@hostname:port/database_name"
DATABASE_URL_UNPOOLED="postgresql://username:password@hostname:port/database_name"
APP_URL="https://example.com/"
AUTH_SECRET="bae23fc1c21a41743058c56a366459a2"
GITHUB_CLIENT_ID="abc123def456ghi789jkl"
GITHUB_CLIENT_SECRET="s3cr3tK3yV@lu3!"
GOOGLE_CLIENT_ID="1234567890-abcde12345fghij67890klmno.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="XyZ_1234567890AbCdEfGhIj"
RESEND_API_KEY="re_2514c2801e44bed9a3bdf9e094e2ce1c"
STRIPE_API_KEY="sk_test_4eC39HqLyjWDarjtT1zdp7dc"
STRIPE_WEBHOOK_SECRET="whsec_1234567890abcdef1234567890abcdef"
```
### Generating the Database tables
Now that the env variables are setup, we need to create the tables in your database in order for the authentication and analytics to work. To do so, run the following command:
```bash
npm run database
```
If you have any trouble in this process, please open an issue or join my discord for help.
### Running the project
In order to verify if everything is working properly and to further customize the project to your own SaaS, you need to start a local development server. You can do so using any of the following commands:
```bash
# Using npm
npm run dev
# Using pnpm
pnpm run dev
# Using yarn
yarn run dev
# Using bun
bun run dev
```
It should output something like this:
```bash
▲ Next.js 14.2.3
- Local: http://localhost:3000
- Environments: .env.local
✓ Starting...
✓ Ready in 2.4s
```
If this is the case, you can just **CTRL** + **Click** on http://localhost:3000, or manually opening it on your browser.
If this is not the case, make sure you didn't miss any of the steps before. If the error persists and you don't know what to do, open an issue [here](https://github.com/NizarAbiZaher/nizzy-starter/issues).
### That's it!
You should now be able to edit and change whatever you'd like, if you are having trouble with specific libraries, make sure to check out the respective documentation and properly research about the issue.
Make sure you remove the Alert at the top if you intend to use this starter kit. To do so go to `app/layout.tsx` and remove the `<AlertDemo />` component (you may also delete it from `components/alert.tsx`).
### Theme configuration
The project is theme based, which means it is based on one color for the entire thing. The theme color is [Vivid blue](https://www.colorhexa.com/1f8eef) and it's defined in HSL.
You can find the theme colors in `app/globals.css`. To change the main theme color replace the `--primary` with HSL or RBG, without comas seperating them (I'm not sure if it is possible to use other color models, but feel free to try it out).
Here is how you can do it using CSS variables:
```css
/* Using HSL */
--primary: 208 87% 53%; /* Background colors, borders,... */
--primary-foreground: 0 0% 0%; /* Foreground in buttons */
/* Using RBG Decimal */
--primary: 31 142 239;
--primary-foreground: 0 0 0;
/* Using RBG Percent */
--primary: 12.2% 55.7% 93.7%;
--primary-foreground: 0% 0% 0%;
```
Here's how you can change the `tailwind.config.ts` file to the color model you want to use:
```ts
// Using HSL
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
// Using RGB Decimal/Percent
primary: {
DEFAULT: 'rgb(var(--primary))',
foreground: 'rgb(var(--primary-foreground))'
},
```
The `tailwind.config.ts` file doesn't only have the content above, so find the matching code and update accordingly.
The reason for the colors not being defined like `hsl(208, 87%, 53%)` is because the project uses tailwind for styling, and with the current definition we can control the alpha within the class, for example `bg-primary/20` (this will make the primary color have an alpha value of 0.2).
If you do change the main color please consider changing the `--primary-foreground` as well, since it is currently a shade of white and it might not look good on the color you chose.
### Aditional information
SaaS Starter Kit Author: NizarAbiZaher
#### Special thanks to all the contributors below:
- [dpaulos6](https://github.com/dpaulos6) - *README, Theme Config and UI*
### Socials
- [YouTube](https://www.youtube.com/@NizzyABI)
- [GitHub](https://github.com/NizarAbiZaher)
- [Discord Community](https://discord.com/invite/nizar)

62
actions/login.ts Normal file
View file

@ -0,0 +1,62 @@
'use server'
import * as z from 'zod'
import { LoginSchema } from '@/schemas'
import { signIn } from '@/auth'
import { AuthError } from 'next-auth'
import { generateVerificationToken } from '@/lib/tokens'
import { getUserByEmail } from '@/data/user'
import { sendVerificationEmail } from '@/lib/mail'
export const login = async (values: z.infer<typeof LoginSchema>) => {
// Validate fields
const validatedFields = LoginSchema.safeParse(values)
// If fields are not valid
if (!validatedFields.success) {
return { error: 'Invalid fields' }
}
// If fields are valid
const { email, password } = validatedFields.data
const exisitingUser = await getUserByEmail(email)
if (!exisitingUser || !exisitingUser.email || !exisitingUser.password) {
return { error: 'Email does not exisit' }
}
if (!exisitingUser.emailVerified) {
const verificationToken = await generateVerificationToken(
exisitingUser.email
)
await sendVerificationEmail(
verificationToken.email,
verificationToken.token
)
return { success: 'Confirmation email sent!' }
}
try {
const result = await signIn('credentials', {
redirect: false,
email,
password
})
if (result?.error) {
return { error: result.error }
}
return { success: 'Logged In!' }
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return { error: 'Invalid credentials' }
default:
return { error: 'Something went wrong' }
}
}
throw error
}
}

7
actions/logout.ts Normal file
View file

@ -0,0 +1,7 @@
'use server'
import { signOut } from '@/auth'
export const logout = async () => {
await signOut()
}

70
actions/new-password.ts Normal file
View file

@ -0,0 +1,70 @@
'use server'
import * as z from 'zod'
import { NewPasswordSchema } from '@/schemas'
import { getPasswordResetTokenByToken } from '@/data/password-reset-token'
import { getUserByEmail } from '@/data/user'
import bcrypt from 'bcrypt'
import { db } from '@/lib/db'
export const newPassword = async (
values: z.infer<typeof NewPasswordSchema>,
token?: string | null
) => {
// if error, return error
if (!token) {
return { error: 'Token is required' }
}
// validate fields
const validatedFields = NewPasswordSchema.safeParse(values)
// if not fields are not valid, return error
if (!validatedFields.success) {
return { error: 'Invalid Fields' }
}
// extract password
const { password } = validatedFields.data
// token validation
const existingToken = await getPasswordResetTokenByToken(token)
// if token not found, return error
if (!existingToken) {
return { error: 'Invalid Token' }
}
// check if token is expired (if it is less than the date we set, which is an hour)
const hasExpired = new Date(existingToken.expires) < new Date()
// if expired, return error
if (hasExpired) {
return { error: 'Token has expired' }
}
// check exisiting user
const existingUser = await getUserByEmail(existingToken.email)
// if user not found, return error
if (!existingUser) {
return { error: 'Email not found' }
}
// hash password
const hashedPassword = await bcrypt.hash(password, 10)
// update db
await db.user.update({
where: { id: existingUser.id },
data: {
password: hashedPassword
}
})
// delete token
await db.passwordResetToken.delete({
where: { id: existingToken.id }
})
// return success message
return { success: 'Password updated successfully' }
}

View file

@ -0,0 +1,41 @@
'use server'
import { db } from '@/lib/db'
import { getUserByEmail } from '@/data/user'
import { getVerificationTokenByToken } from '@/data/verification-token'
export const newVerification = async (token: string) => {
// if no token, display message
const exisitingToken = await getVerificationTokenByToken(token)
if (!exisitingToken) {
return { error: 'Token does not exisit!' }
}
// if token has expired, display message
const hasExpired = new Date(exisitingToken.expires) < new Date()
if (hasExpired) {
return { error: 'Token has expired!' }
}
// if user does not exist, display message
const existingUser = await getUserByEmail(exisitingToken.email)
if (!existingUser) {
return { error: 'User does not exisit!' }
}
// update email value when they verify
await db.user.update({
where: { id: existingUser.id },
data: {
emailVerified: new Date(),
email: exisitingToken.email
}
})
// delete token
await db.verificationToken.delete({
where: { id: exisitingToken.id }
})
return { success: 'Email verified! Login to continue' }
}

39
actions/register.ts Normal file
View file

@ -0,0 +1,39 @@
'use server'
import * as z from 'zod'
import { RegisterSchema } from '@/schemas'
import bcrypt from 'bcrypt'
import { db } from '@/lib/db'
import { getUserByEmail } from '@/data/user'
import { generateVerificationToken } from '@/lib/tokens'
import { sendVerificationEmail } from '@/lib/mail'
export const register = async (values: z.infer<typeof RegisterSchema>) => {
const validatedFields = RegisterSchema.safeParse(values)
if (!validatedFields.success) {
return { error: 'Invalid fields' }
}
const { name, email, password } = validatedFields.data
const hashedPassword = await bcrypt.hash(password, 10)
const exisitingUser = await getUserByEmail(email)
if (exisitingUser) {
return { error: 'Email already exists!' }
}
await db.user.create({
data: {
name,
email,
password: hashedPassword
}
})
const verificationToken = await generateVerificationToken(email)
await sendVerificationEmail(verificationToken.email, verificationToken.token)
return { success: 'Email sent!' }
}

32
actions/reset.ts Normal file
View file

@ -0,0 +1,32 @@
'use server'
import * as z from 'zod'
import { getUserByEmail } from '@/data/user'
import { ResetSchema } from '@/schemas'
import { sendPasswordResetEmail } from '@/lib/mail'
import { generatePasswordResetToken } from '@/lib/tokens'
export const reset = async (values: z.infer<typeof ResetSchema>) => {
// validate fields
const validatedFields = ResetSchema.safeParse(values)
// check if fields are valid
if (!validatedFields.success) {
return { error: 'Invalid email!' }
}
// extract email
const { email } = validatedFields.data
// check exisiting user
const existingUser = await getUserByEmail(email)
// if user does not exist
if (!existingUser) {
return { error: 'Email does not exist!' }
}
//send reset email
const passwordResetToken = await generatePasswordResetToken(email)
await sendPasswordResetEmail(
passwordResetToken.email,
passwordResetToken.token
)
// success message
return { success: 'Reset email sent 📫' }
}

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

42
auth.config.ts Normal file
View file

@ -0,0 +1,42 @@
import bcrypt from 'bcryptjs'
import type { NextAuthConfig } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import Github from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'
import { LoginSchema } from '@/schemas'
import { getUserByEmail } from '@/data/user'
export default {
providers: [
// OAuth authentication providers... the raw data is found at /api/auth/providers
Github({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET
}),
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET
}),
Credentials({
async authorize(credentials) {
const validatedFields = await LoginSchema.safeParse(credentials)
if (validatedFields.success) {
const { email, password } = validatedFields.data
const user = await getUserByEmail(email)
// by no password I mean that the user is using a social login (Google, Github, etc.)
if (!user || !user.password) return null
// check if passwords match
const passwordsMatch = await bcrypt.compare(password, user.password)
// if the passwords match, return the user
if (passwordsMatch) return user
}
return null
}
})
]
} satisfies NextAuthConfig

76
auth.ts Normal file
View file

@ -0,0 +1,76 @@
import NextAuth from 'next-auth'
import { PrismaAdapter } from '@auth/prisma-adapter'
import { UserRole } from '@prisma/client'
import { getUserById } from '@/data/user'
import { db } from '@/lib/db'
import authConfig from '@/auth.config'
// auth
export const {
handlers: { GET, POST },
auth, // This auth thing helps us get user info such as for display certain content for them and specific data
signIn,
signOut
} = NextAuth({
// if there is an error, redirect to this page
pages: {
signIn: '/login',
error: '/error'
},
// events to get emailverfiied if the user used Oauth
events: {
async linkAccount({ user }) {
await db.user.update({
where: { id: user.id },
data: { emailVerified: new Date() }
})
}
},
// Callbacks allow us to customuzie the auth process such as who has access to what, get ID, and block users.
callbacks: {
// sign in
async signIn({ user, account }) {
// Allow OAuth without verification
if (account?.provider !== 'credentials') return true
// get exisiting user & restrict signin if they have not verified their email
const exisitingUser = await getUserById(user.id ?? '')
if (!exisitingUser?.emailVerified) return false
return true
},
// token & session
async session({ session, token }) {
// if they have an id (sub) and user has been created, return it
if (token.sub && session.user) {
session.user.id = token.sub
}
// if they have a role and user has been created, return it
if (token.role && session.user) {
session.user.role = token.role as UserRole
}
return session
},
// jwt
async jwt({ token }) {
// fetch user
if (!token.sub) return token
const exisitingUser = await getUserById(token.sub)
if (!exisitingUser) return token
token.role = exisitingUser.role
return token
}
// session userId
},
adapter: PrismaAdapter(db),
session: { strategy: 'jwt' },
...authConfig
})

16
components.json Normal file
View file

@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

104
components/alert.tsx Normal file
View file

@ -0,0 +1,104 @@
'use client'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Palette, RocketIcon } from 'lucide-react'
import { Button } from './ui/button'
import Link from 'next/link'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { toast } from 'react-hot-toast'
import { useState, useTransition } from 'react'
import { Form } from '@/components/ui/form'
export function AlertDemo() {
const [email, setEmail] = useState('')
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 (
<div className="w-full max-w-6xl px-6 py-4">
<Alert className="flex flex-col sm:flex-row gap-4 justify-between px-6 rounded-xl border-0 ring ring-primary/20 ring-inset text-secondary bg-primary/15 text-black dark:text-white cursor-default">
<div>
<AlertTitle className="flex gap-1">
<RocketIcon className="h-4 w-4" />
Heads up!
</AlertTitle>
<AlertDescription className="flex">
<p>This is a demo. You can get the github repository</p>
<Dialog>
<DialogTrigger asChild>
<p className="text-primary ml-1.5 underline cursor-pointer">
here
</p>
</DialogTrigger>
<DialogContent className="rounded-lg sm:max-w-[425px] ">
<DialogHeader>
<DialogTitle>Starter Kit</DialogTitle>
<DialogDescription>
Enter your email to get the starterkit link in your inbox
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit}>
<Label htmlFor="name" className="text-right mb-2">
Email
</Label>
<Input
onChange={(e) => setEmail(e.target.value)}
name="email"
id="email"
type="email"
required
placeholder="tylerdurder@gmail.com"
className="mt-3"
value={email}
/>
<DialogFooter>
<Button type="submit" className="mt-4">
Get The Starter Kit
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</AlertDescription>
</div>
<Link href="/customize" className="mx-auto sm:mx-0">
<Button className="gap-2 text-foreground" variant="link">
<Palette className="w-5 h-5" />
Customize
</Button>
</Link>
</Alert>
</div>
)
}

View file

@ -0,0 +1,22 @@
'use client'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
interface BackButtonProps {
label: string
href: string
}
export const BackButton = ({ label, href }: BackButtonProps) => {
return (
<Button
variant="link"
className="w-full mt-2 text-blue-500"
size="sm"
asChild
>
<Link href={href}>{label}</Link>
</Button>
)
}

View file

@ -0,0 +1,42 @@
'use client'
import { Header } from '@/components/auth/header'
import { Social } from '@/components/auth/social'
import { BackButton } from '@/components/auth/back-button'
import { Card, CardFooter, CardHeader } from '@/components/ui/card'
interface CardWrapperProps {
children: React.ReactNode
headerTitle: string
backButtonLabel: string
backButtonHref: string
showSocial?: boolean
}
export const CardWrapper = ({
children,
headerTitle,
backButtonLabel,
backButtonHref,
showSocial
}: CardWrapperProps) => {
return (
<Card className="mx-auto w-full max-w-sm bg-secondary/90 border border-foreground/5 rounded-lg px-7">
<CardHeader>
<Header title={headerTitle} />
</CardHeader>
<div>{children}</div>
{showSocial && (
<div>
<Social />
</div>
)}
<CardFooter>
<BackButton label={backButtonLabel} href={backButtonHref} />
</CardFooter>
</Card>
)
}

View file

@ -0,0 +1,11 @@
interface HeaderProps {
title: string
}
export const Header = ({ title }: HeaderProps) => {
return (
<div className="w-full flex flex-col items-center justify-center">
<h1 className="text-3xl font-bold ">{title}</h1>
</div>
)
}

View file

@ -0,0 +1,29 @@
'use client'
import { Button } from '@/components/ui/button'
import { signIn } from 'next-auth/react'
import { FaGithub, FaGoogle } from 'react-icons/fa'
export const Social = () => {
const onClick = (provider: 'google' | 'github') => {
signIn(provider, {
callbackUrl: '/'
})
}
return (
<div className="flex gap-2 mt-3">
<Button
className="rounded-[5px] w-full border border-primary/20 bg-secondary text-primary hover:bg-primary/10 text-md"
onClick={() => onClick('google')}
>
<FaGoogle className="mr-2" /> Google
</Button>
<Button
className="rounded-[5px] w-full border border-primary/20 bg-secondary text-primary hover:bg-primary/10 text-md"
onClick={() => onClick('github')}
>
<FaGithub className="mr-2" /> Github
</Button>
</div>
)
}

109
components/color-picker.tsx Normal file
View file

@ -0,0 +1,109 @@
'use client'
import { useState, useEffect, useRef } from 'react'
import { HslColorPicker, HslColor } from 'react-colorful'
import { CodeBlock } from 'react-code-block'
import { themes } from 'prism-react-renderer'
import { useCopyToClipboard } from 'react-use'
interface HslColorType {
h: number
s: number
l: number
}
interface ColorPickerProps {
variable: string
}
export function ColorPicker({ variable }: ColorPickerProps) {
const [color, setColor] = useState<HslColorType>({ h: 0, s: 100, l: 50 })
const mainDivRef = useRef<HTMLDivElement>(null)
const [isDragging, setIsDragging] = useState(false)
useEffect(() => {
const rootStyles = getComputedStyle(document.documentElement)
const primaryColor = rootStyles.getPropertyValue(variable).trim()
const [h, s, l] = primaryColor.split(' ').map((value) => {
if (value.endsWith('%')) {
return parseFloat(value)
}
return parseFloat(value)
})
setColor({ h, s, l })
}, [variable])
const handleColorChange = (newColor: HslColor) => {
const fixedColor = `${newColor.h} ${newColor.s}% ${newColor.l}%`
setColor(newColor)
document.documentElement.style.setProperty(variable, fixedColor)
}
const handleMouseDown = (event: MouseEvent) => {
if (
mainDivRef.current &&
mainDivRef.current.contains(event.target as Node)
) {
setIsDragging(true)
document.documentElement.classList.add('disable-transitions')
}
}
const handleMouseUp = () => {
if (isDragging) {
setIsDragging(false)
document.documentElement.classList.remove('disable-transitions')
}
}
useEffect(() => {
window.addEventListener('mousedown', handleMouseDown)
window.addEventListener('mouseup', handleMouseUp)
return () => {
window.removeEventListener('mousedown', handleMouseDown)
window.removeEventListener('mouseup', handleMouseUp)
}
}, [isDragging])
const [state, copyToClipboard] = useCopyToClipboard()
const copyCode = () => {
copyToClipboard(`${variable}: ${color.h} ${color.s}% ${color.l}%;`)
setTimeout(() => {
copyToClipboard('')
}, 2500)
}
return (
<div
ref={mainDivRef}
className="flex flex-col items-center sm:items-start w-min space-y-4"
>
<HslColorPicker color={color} onChange={handleColorChange} />
<CodeBlock
code={`${variable}: ${color.h} ${color.s}% ${color.l}%;`}
language="css"
theme={themes.oneDark}
>
<div className="relative">
<CodeBlock.Code className="bg-foreground/[0.025] border p-4 rounded-lg shadow-lg break-words whitespace-pre-wrap">
<span className="flex items-center text-sm text-foreground/70 h-6 mb-1">
globals.css
</span>
<CodeBlock.LineContent className="text-base">
<CodeBlock.Token />
</CodeBlock.LineContent>
</CodeBlock.Code>
<button
className="bg-foreground/5 border rounded px-3.5 py-1.5 absolute top-2 right-2 text-sm font-semibold"
onClick={copyCode}
>
{state.value ? 'Copied!' : 'Copy'}
</button>
</div>
</CodeBlock>
</div>
)
}

106
components/email/email.tsx Normal file
View file

@ -0,0 +1,106 @@
import {
Body,
Button,
Container,
Head,
Hr,
Html,
Preview,
Section,
Text,
} from "@react-email/components";
import * as React from "react";
const baseUrl = process.env.VERCEL_URL
? `https://${process.env.VERCEL_URL}`
: "";
export const RepoEmail = () => (
<Html>
<Head />
<Preview>
Your Free SaaS Starter Kit
</Preview>
<Body style={main}>
<Container style={container}>
<Text style={paragraph}>Hey!!</Text>
<Text style={paragraph}>
Welcome to the Nizzy Starter Kit!! The free (and better) SaaS Starter Kit.
</Text>
<Text style={paragraph}>
Below is the link to the github repository where you can find the starter kit.
</Text>
<Text style={paragraph}>
Enjoy building your next project with it!
</Text>
<Section style={btnContainer}>
<Button style={button} href="https://github.com/NizarAbiZaher/nizzy-starter">
Go To The Starter Kit
</Button>
</Section>
<Text style={paragraph}>
Best,
<br />
Nizzy
</Text>
<Hr style={hr} />
<Text style={footer}>
470 Noor Ave STE B #1148, Ottawa, Canada 94080
</Text>
</Container>
</Body>
</Html>
);
export default RepoEmail;
const main = {
backgroundColor: "#ffffff",
fontFamily:
'Poppins, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif',
};
const container = {
margin: "0 auto",
padding: "20px 0 48px",
};
const logo = {
margin: "0 auto",
};
const paragraph = {
fontSize: "16px",
lineHeight: "26px",
color: 'black'
};
const btnContainer = {
textAlign: "center" as const,
};
const button = {
backgroundColor: "#1F8EEF",
borderRadius: "6px",
color: "#fff",
fontSize: "16px",
textDecoration: "none",
textAlign: "center" as const,
display: "block",
padding: "12px",
};
const hr = {
borderColor: "#cccccc",
margin: "20px 0",
};
const footer = {
color: "#8898aa",
fontSize: "12px",
};

19
components/footer.tsx Normal file
View file

@ -0,0 +1,19 @@
import Link from 'next/link'
import { Logo } from '@/components/logo'
export const Footer = () => {
return (
<footer className="max-w-6xl w-full p-6">
<div className="flex justify-center bg-secondary dark:bg-secondary/50 py-6 w-full rounded-xl">
<div className="w-full max-w-6xl flex flex-col md:flex-row items-center justify-between gap-6 px-6">
<Link href="/">
<Logo />
</Link>
<span className="text-sm">
&copy; 2024 YourCompany. All rights reserved.
</span>
</div>
</div>
</footer>
)
}

67
components/header.tsx Normal file
View file

@ -0,0 +1,67 @@
'use client'
import { useState, useEffect } from 'react'
import { Sparkles } from 'lucide-react'
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
const IframeWithSkeleton = () => {
const [iframeLoaded, setIframeLoaded] = useState(false);
useEffect(() => {
const iframe = document.getElementById('youtube-iframe') as HTMLIFrameElement;
if (iframe) {
const handleIframeLoad = () => {
setIframeLoaded(true);
};
iframe.addEventListener('load', handleIframeLoad);
return () => {
iframe.removeEventListener('load', handleIframeLoad);
};
}
}, []);
return (
<>
{!iframeLoaded && <Skeleton className="w-full max-w-2xl h-auto aspect-video" />}
<iframe
id="youtube-iframe"
src="https://www.youtube.com/embed/Q6jDdtbkMIU?si=YtgU89RhYiwt5-U5"
title="YouTube Video Player"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
className={`w-full max-w-2xl h-auto aspect-video rounded-[6px] ${iframeLoaded ? '' : 'hidden'}`}
></iframe>
</>
);
};
export const Header = () => {
return (
<div className="space-y-20 mt-32">
<div className="mx-auto grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="flex flex-col justify-center text-center lg:text-left ">
<h2 className="text-4xl font-extrabold sm:text-5xl">
Clone. Build. Ship.
</h2>
<p className="mt-4 text-lg text-foreground">
Build your SaaS faster with our fully customizable template.
</p>
<div className="flex justify-center lg:justify-start items-center mt-4">
<Link href="/overview">
<Button className="gap-2">
<Sparkles className="h-5 w-5" />
<span>Get Started</span>
</Button>
</Link>
</div>
</div>
<div className="flex items-center justify-center rounded-lg overflow-hidden">
<IframeWithSkeleton />
</div>
</div>
</div>
)
}

104
components/icons/index.tsx Normal file
View file

@ -0,0 +1,104 @@
import React from 'react'
import type { SVGProps } from 'react'
export function StripeIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<g fill="none" fillRule="evenodd">
<path d="M24 0v24H0V0zM12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035c-.01-.004-.019-.001-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427c-.002-.01-.009-.017-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093c.012.004.023 0 .029-.008l.004-.014l-.034-.614c-.003-.012-.01-.02-.02-.022m-.715.002a.023.023 0 0 0-.027.006l-.006.014l-.034.614c0 .012.007.02.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"></path>
<path
fill="currentColor"
d="M10.729 8.36c0-.771.63-1.068 1.675-1.068c1.112 0 2.442.251 3.666.712c.567.214 1.222-.182 1.222-.789V4.588a.952.952 0 0 0-.636-.912C15.236 3.195 13.828 3 12.404 3C8.403 3 5.742 5.097 5.742 8.598c0 5.46 7.49 4.589 7.49 6.943c0 .91-.788 1.206-1.892 1.206c-1.255 0-2.777-.396-4.165-.99c-.56-.24-1.216.152-1.216.76v2.698c0 .4.236.763.61.904c1.632.615 3.263.881 4.77.881c4.1 0 6.919-2.037 6.919-5.578c-.02-5.895-7.53-4.846-7.53-7.062Z"
></path>
</g>
</svg>
)
}
export function ResendIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M2.023 0v24h5.553v-8.434h2.998L15.326 24h6.65l-5.372-9.258a7.65 7.65 0 0 0 3.316-3.016q1.063-1.815 1.062-4.08q0-2.194-1.062-3.91q-1.063-1.747-2.95-2.742Q15.12 0 12.823 0Zm5.553 4.87h4.219q1.097 0 1.851.376q.788.378 1.2 1.098q.412.685.412 1.611c0 .926-.126 1.165-.378 1.645q-.343.72-1.03 1.13q-.651.379-1.542.38H7.576Z"
></path>
</svg>
)
}
export function TailwindcssIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="currentColor"
d="M12.001 4.8q-4.8 0-6 4.8q1.8-2.4 4.2-1.8c.913.228 1.565.89 2.288 1.624C13.666 10.618 15.027 12 18.001 12q4.8 0 6-4.8q-1.8 2.4-4.2 1.8c-.913-.228-1.565-.89-2.288-1.624C16.337 6.182 14.976 4.8 12.001 4.8m-6 7.2q-4.8 0-6 4.8q1.8-2.4 4.2-1.8c.913.228 1.565.89 2.288 1.624c1.177 1.194 2.538 2.576 5.512 2.576q4.8 0 6-4.8q-1.8 2.4-4.2 1.8c-.913-.228-1.565-.89-2.288-1.624C10.337 13.382 8.976 12 6.001 12"
></path>
</svg>
)
}
export function NextjsIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<g fill="none">
<g clipPath="url(#akarIconsNextjsFill0)">
<path
fill="currentColor"
d="M11.214.006c-.052.005-.216.022-.364.033c-3.408.308-6.6 2.147-8.624 4.974a11.88 11.88 0 0 0-2.118 5.243c-.096.66-.108.854-.108 1.748s.012 1.089.108 1.748c.652 4.507 3.86 8.293 8.209 9.696c.779.251 1.6.422 2.533.526c.364.04 1.936.04 2.3 0c1.611-.179 2.977-.578 4.323-1.265c.207-.105.247-.134.219-.157a211.64 211.64 0 0 1-1.955-2.62l-1.919-2.593l-2.404-3.559a342.499 342.499 0 0 0-2.422-3.556c-.009-.003-.018 1.578-.023 3.51c-.007 3.38-.01 3.516-.052 3.596a.426.426 0 0 1-.206.213c-.075.038-.14.045-.495.045H7.81l-.108-.068a.44.44 0 0 1-.157-.172l-.05-.105l.005-4.704l.007-4.706l.073-.092a.644.644 0 0 1 .174-.143c.096-.047.133-.051.54-.051c.478 0 .558.018.682.154c.035.038 1.337 2 2.895 4.362l4.734 7.172l1.9 2.878l.097-.063a12.318 12.318 0 0 0 2.465-2.163a11.947 11.947 0 0 0 2.825-6.135c.096-.66.108-.854.108-1.748s-.012-1.088-.108-1.748C23.24 5.75 20.032 1.963 15.683.56a12.6 12.6 0 0 0-2.498-.523c-.226-.024-1.776-.05-1.97-.03m4.913 7.26a.473.473 0 0 1 .237.276c.018.06.023 1.365.018 4.305l-.007 4.218l-.743-1.14l-.746-1.14V10.72c0-1.983.009-3.097.023-3.151a.478.478 0 0 1 .232-.296c.097-.05.132-.054.5-.054c.347 0 .408.005.486.047"
></path>
</g>
<defs>
<clipPath id="akarIconsNextjsFill0">
<path d="M0 0h24v24H0z"></path>
</clipPath>
</defs>
</g>
</svg>
)
}
export function Error404(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<path
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="m2 2.207l20 19.794M6.562 2.515c2.006-.602 7.754-.723 11.162-.118c.825.147 1.62.679 2.054 1.385c.726 1.183.687 2.56.687 3.942l-.12 8.455M4 4.172c-.632 2.267-.547 6.428-.506 12.57c.006.79.043 1.59.281 2.345c.369 1.17.983 1.88 2.332 2.45c.575.244 1.206.324 1.833.324h4.043c3.796-.092 5.328-.488 7.006-2.678m-8.502 2.678c2.381-1.207 3.608-1.375 3.296-4.411c-.06-.786.39-1.725 1.194-1.977m5.428-3.428c-.243 1.436-.406 1.97-1.375 2.801"
color="currentColor"
></path>
</svg>
)
}

46
components/languages.tsx Normal file
View file

@ -0,0 +1,46 @@
'use client'
import { NextjsIcon, ResendIcon, StripeIcon, TailwindcssIcon } from './icons'
import Marquee from 'react-fast-marquee'
const languages = [
{
label: 'Stripe',
icon: StripeIcon,
className: 'w-36 h-auto aspect-square'
},
{
label: 'Resend',
icon: ResendIcon,
className: 'w-36 p-5 h-auto aspect-square'
},
{
label: 'TailwindCSS',
icon: TailwindcssIcon,
className: 'w-48 px-5 h-auto aspect-square'
},
{
label: 'NextJS',
icon: NextjsIcon,
className: 'w-36 p-5 h-auto aspect-square'
}
]
export function Language() {
return (
<section className="relative">
<div className="absolute top-0 left-0 w-1/4 h-full bg-gradient-to-r from-background pointer-events-none z-10"></div>
<div className="absolute top-0 right-0 w-1/4 h-full bg-gradient-to-r from-transparent to-background pointer-events-none z-10"></div>
<Marquee speed={30} autoFill>
{languages.map((language, i) => (
<language.icon
key={i}
className={
language.className +
'text-foreground opacity-50 hover:opacity-100 transition duration-300 ml-32 cursor-pointer'
}
/>
))}
</Marquee>
</section>
)
}

12
components/logo.tsx Normal file
View file

@ -0,0 +1,12 @@
import { Squircle } from 'lucide-react'
export const Logo = () => {
return (
<div className="flex items-center gap-2 group">
<Squircle className="w-12 h-12 text-primary group-hover:-rotate-12 transition-all duration-300" />
<span className="text-xl group-hover:translate-x-0.5 transition-all duration-300">
YourCompany
</span>
</div>
)
}

View file

@ -0,0 +1,23 @@
'use client'
import { PanelRightClose } from 'lucide-react'
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import { Sidebar } from '@/components/sidebar'
import { useState } from 'react'
export const MobileSidebar = () => {
const [open, setOpen] = useState(false)
const openSidebar = () => setOpen(true)
const closeSidebar = () => setOpen(false)
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger className="sm:hidden pr-4 text-primary" onClick={openSidebar}>
<PanelRightClose className="h-6 w-6 hover:text-primary/50 duration-300" />
</SheetTrigger>
<SheetContent side="left" className="p-6 border-none w-80 bg-secondary">
<Sidebar closeSidebar={closeSidebar} />
</SheetContent>
</Sheet>
)
}

View file

@ -0,0 +1,39 @@
'use client'
import * as React from 'react'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
export function ModeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="rounded-md" variant="outline" size="icon">
<Sun className="block dark:hidden h-5 w-5" />
<Moon className="hidden dark:block h-5 w-5" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="">
<DropdownMenuItem onClick={() => setTheme('light')}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

55
components/navbar.tsx Normal file
View file

@ -0,0 +1,55 @@
import Link from 'next/link'
import { ModeToggle } from '@/components/mode-toggle'
import Image from 'next/image'
import { UserButton } from '@/components/user-button'
import { MobileSidebar } from '@/components/mobile-sidebar'
import { Logo } from '@/components/logo'
export const navPages = [
{
title: 'Dashboard',
link: '/dashboard'
},
{
title: 'Pricing',
link: '/#pricing'
},
{
title: 'Items',
link: '/#items'
}
]
export const Navbar = () => {
return (
<nav className="top-0 w-full z-50 transition">
<div className="max-w-6xl mx-auto px-6 py-4">
<div className="flex justify-between items-center">
<MobileSidebar />
{/* Logo */}
<Link href="/">
<Logo />
</Link>
{/* Links, Theme, & User */}
<div className="hidden sm:flex h-[40px] items-center text-lg md:text-lg font-medium gap-4 transition-all">
<div className="flex items-center h-full text-base font-medium">
{navPages.map((page, index) => (
<Link
key={index}
href={page.link}
className="flex items-center hover:text-primary hover:bg-primary/10 h-full transition duration-300 px-4 rounded-md"
>
{page.title}
</Link>
))}
</div>
<div className="flex h-full gap-4">
<ModeToggle />
<UserButton />
</div>
</div>
</div>
</div>
</nav>
)
}

154
components/pricing-card.tsx Normal file
View file

@ -0,0 +1,154 @@
'use client'
import { Check, Sparkle } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useState } from 'react'
import { toast } from 'react-hot-toast'
import axios from 'axios'
import { useCurrentUser } from '@/hooks/use-current-user'
import { useRouter } from 'next/navigation'
// Update Tiers Here
export const tiers = [
{
name: 'Your Competitor Name',
price: '18.99',
features: [
'Feature 1',
'Feature 2',
'Feature 3',
'Feature 4',
'Feature 5',
'Feature 6',
'Feature 7',
'Feature 8',
'Feature 9',
'Feature 10'
],
cta: 'Get Started',
yourProduct: false
},
{
name: 'Your Product Name',
priceBefore: '$19.99',
price: '8.99',
features: [
'Feature 1',
'Feature 2',
'Feature 3',
'Feature 4',
'Feature 5',
'Feature 6',
'Feature 7',
'Feature 8',
'Feature 9',
'Feature 10'
],
cta: 'Get Started',
yourProduct: true
}
]
export const PricingCard = () => {
const [isLoading, setIsLoading] = useState(false)
const session = useCurrentUser()
const router = useRouter();
const onClick = async () => {
if (!session) {
toast('👇 Sign in to purchase!')
router.push('/login')
return
}
try {
setIsLoading(true)
const response = await axios.post('/api/checkout')
window.location.href = response.data.url
} catch (error) {
toast.error('An error occured! Please try again.')
} finally {
setIsLoading(false)
}
}
return (
<section id="pricing" className="scroll-mt-4">
{/* Title */}
<div className="mx-auto flex flex-col items-center pb-8">
<h2 className="pb-4 text-4xl font-extrabold text-foreground">
Pricing
</h2>
<p className="text-md opacity-50 max-w-lg text-center">
Describe your product / service here that will impress the user & want
them to buy the product
</p>
</div>
{/* Pricing Card Display */}
<div className="flex flex-col sm:place-items-center md:flex-row items-center justify-center gap-6">
{tiers.map((tier) => (
<div
key={tier.name}
className={`relative flex flex-col p-6 shadow-lg rounded-lg justify-between ring-2 ring-inset w-full max-w-[550px] ${
tier.yourProduct
? 'bg-primary/10 ring-primary/50'
: 'bg-secondary ring-foreground/10'
}`}
>
{tier.yourProduct && (
<div className="px-3 py-1 text-primary-foreground text-sm bg-primary rounded-full inline-block absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
Popular
</div>
)}
{/* Pricing */}
<div>
<h3
className={`text-lg font-semibold ${
tier.yourProduct ? 'text-primary' : 'text-foreground/70'
}`}
>
{tier.name}
</h3>
<div
className={`mt-4 ${
tier.yourProduct ? 'text-foreground/90' : 'text-foreground/70'
}`}
>
{tier.priceBefore ? (
<span className="font-semibold mr-2 line-through text-lg opacity-75">
{tier.priceBefore}
</span>
) : null}
<span className="text-4xl font-bold">${tier.price}</span> /month
</div>
<ul className="mt-4 space-y-2.5">
{tier.features.map((feature, index) => (
<li
key={index}
className="flex items-center text-foreground/90 gap-2"
>
<Check
className={`h-6 w-6 rounded-full ${
tier.yourProduct ? 'text-primary' : 'text-foreground/70'
}`}
/>
{feature}
</li>
))}
</ul>
</div>
{/* Button */}
<div className="mt-6">
<Button
disabled={!tier.yourProduct}
onClick={onClick}
className={`w-full ${tier.yourProduct && 'hover:-translate-y-1'}`}
>
{tier.cta}
<Sparkle className="ml-1 h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</section>
)
}

View file

@ -0,0 +1,6 @@
'use client'
import { Toaster } from 'react-hot-toast'
export const ToastProvider = () => {
return <Toaster />
}

View file

@ -0,0 +1,25 @@
'use client'
import axios from 'axios'
import { useState } from 'react'
import toast from 'react-hot-toast'
import { Button } from '@/components/ui/button'
export const PurchaseButton = () => {
const [isLoading, setIsLoading] = useState(false)
const onClick = async () => {
try {
setIsLoading(true)
const response = await axios.post('/api/checkout')
window.location.href = response.data.url
} catch (error) {
toast.error('An error occurred. Please try again.')
} finally {
setIsLoading(false)
}
}
return (
<Button onClick={onClick} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Purchase'}
</Button>
)
}

130
components/sidebar.tsx Normal file
View file

@ -0,0 +1,130 @@
import { cn } from '@/lib/utils'
import Link from 'next/link'
import { useCurrentUser } from '@/hooks/use-current-user'
import { signOut } from 'next-auth/react'
import Image from 'next/image'
import { ModeToggle } from '@/components/mode-toggle'
import { Logo } from '@/components/logo'
const sidebarPages = [
{
link: '/',
title: 'Home'
},
{
link: '#profile',
title: 'Profile'
},
{
link: '#purchases',
title: 'Purchases'
}
]
const socials = [
{
link: 'https://github.com/NizarAbiZaher',
title: 'Github'
},
{
link: 'https://www.youtube.com/@NizzyABI',
title: 'Youtube'
},
{
link: 'https://twitter.com/NizarAbiZaher',
title: 'Twitter'
},
{
link: 'https://www.linkedin.com/in/nizarabizaher/',
title: 'Tiktok'
},
{
link: 'https://www.instagram.com/nizarabizaher/',
title: 'Instagram'
},
{
link: 'https://discord.gg/nizar',
title: 'Discord'
}
]
interface SidebarProps {
closeSidebar?: () => void
}
export const Sidebar = ({ closeSidebar }: SidebarProps) => {
const session = useCurrentUser()
const Logout = () => {
signOut()
}
return (
<div className="flex flex-col justify-between pl-2">
<Link href="/" onClick={closeSidebar}>
<Logo />
</Link>
<div className="flex pt-4">
<ModeToggle />
</div>
<div className=" pt-3">
<div className="space-y-4">
<div className="ml-2">
<h1 className="text-xl font-semibold">Main</h1>
{sidebarPages.map((page) => (
<Link
key={page.link}
href={page.link}
className={cn(
'group flex py-1.5 w-full justify-start font-light cursor-pointer'
)}
onClick={closeSidebar}
>
<div className="flex w-full">
<p className="font-normal text-foreground/75">{page.title}</p>
</div>
</Link>
))}
</div>
<div className="ml-2">
<h1 className="text-xl font-semibold">Socials</h1>
{socials.map((page) => (
<Link
key={page.link}
href={page.link}
className={cn(
'group flex w-full justify-start font-light cursor-pointer py-1.5'
)}
onClick={closeSidebar}
>
<div className="flex w-full">
<p className="font-normal text-foreground/75">{page.title}</p>
</div>
</Link>
))}
</div>
{session ? (
<Link
href="/login"
className="group flex py-2 w-full justify-start cursor-pointer rounded ml-2 font-bold text-xl"
onClick={() => {
Logout()
closeSidebar && closeSidebar()
}}
>
Logout
</Link>
) : (
<Link
href="/login"
className="group flex pt-2 w-full justify-start font-light cursor-pointer"
onClick={closeSidebar}
>
<div className="flex w-full ml-2 pb-3">
<p className="font-semibold ">Sign Up</p>
</div>
</Link>
)}
</div>
</div>
</div>
)
}

110
components/testimonials.tsx Normal file
View file

@ -0,0 +1,110 @@
'use client'
import { Card, CardContent, CardTitle } from '@/components/ui/card'
import { Avatar, AvatarImage } from '@/components/ui/avatar'
import AvatarCircles from '@/components/ui/user-avatar-card'
export const Testimonials = () => {
// Add or remove testimonials here following this format
const testimonials = [
{
name: 'John Doe',
avatar: '/testimonials/john-doe.jpg',
message:
'Write customer / user testimonials here. Please make sure it is a real one & not a fake one. You can add as many as you want. In fact, the more the better since people like to see what others are saying about your product or service.'
},
{
name: 'John Doe',
avatar: '/testimonials/john-doe.jpg',
message:
'Write customer / user testimonials here. Please make sure it is a real one & not a fake one. You can add as many as you want. In fact, the more the better since people like to see what others are saying about your product or service.'
},
{
name: 'John Doe',
avatar: '/testimonials/john-doe.jpg',
message:
'Write customer / user testimonials here. Please make sure it is a real one & not a fake one. You can add as many as you want. In fact, the more the better since people like to see what others are saying about your product or service.'
},
{
name: 'John Doe',
avatar: '/testimonials/john-doe.jpg',
message:
'Write customer / user testimonials here. Please make sure it is a real one & not a fake one. You can add as many as you want. In fact, the more the better since people like to see what others are saying about your product or service.'
},
{
name: 'John Doe',
avatar: '/testimonials/john-doe.jpg',
message:
'Write customer / user testimonials here. Please make sure it is a real one & not a fake one. You can add as many as you want. In fact, the more the better since people like to see what others are saying about your product or service.'
},
{
name: 'John Doe',
avatar: '/testimonials/john-doe.jpg',
message:
'Write customer / user testimonials here. Please make sure it is a real one & not a fake one. You can add as many as you want. In fact, the more the better since people like to see what others are saying about your product or service.'
},
{
name: 'John Doe',
avatar: '/testimonials/john-doe.jpg',
message:
'Write customer / user testimonials here. Please make sure it is a real one & not a fake one. You can add as many as you want. In fact, the more the better since people like to see what others are saying about your product or service.'
},
{
name: 'John Doe',
avatar: '/testimonials/john-doe.jpg',
message:
'Write customer / user testimonials here. Please make sure it is a real one & not a fake one. You can add as many as you want. In fact, the more the better since people like to see what others are saying about your product or service.'
},
{
name: 'John Doe',
avatar: '/testimonials/john-doe.jpg',
message:
'Write customer / user testimonials here. Please make sure it is a real one & not a fake one. You can add as many as you want. In fact, the more the better since people like to see what others are saying about your product or service.'
}
]
return (
<div>
{/* Section Title */}
<div className="max-w-3xl mx-auto flex flex-col items-center">
<h2 className="pb-4 text-4xl font-extrabold text-foreground">
Testimonials
</h2>
<p className="text-md opacity-50 max-w-lg text-center">
Describe your product / service here that will impress the user & want
them to buy the product
</p>
<AvatarCircles />
</div>
{/* Testimonials Card*/}
<div className="flex items-center justify-center my-6">
<div className="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl">
{testimonials.map((testimonial, i) => (
<Card
key={i}
className="py-4 px-0 bg-secondary border-0 ring-[2px] ring-foreground/10 ring-inset rounded-lg hover:bg-primary/10 hover:ring-primary/25 transition duration-300 cursor-default"
>
<CardContent className="py-0">
<div className="flex">
<Avatar className="h-7 w-7">
<AvatarImage
src={testimonial.avatar}
alt={testimonial.name}
/>
</Avatar>
<CardTitle className="text-lg pl-2 text-foreground pt-1">
{testimonial.name}
</CardTitle>
</div>
<p className="pt-3 text-foreground/70">
"{testimonial.message}"
</p>
</CardContent>
</Card>
))}
</div>
</div>
</div>
)
}

59
components/ui/alert.tsx Normal file
View file

@ -0,0 +1,59 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const alertVariants = cva(
'relative w-full border-b p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive'
}
},
defaultVariants: {
variant: 'default'
}
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = 'Alert'
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
{...props}
/>
))
AlertTitle.displayName = 'AlertTitle'
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm [&_p]:leading-relaxed', className)}
{...props}
/>
))
AlertDescription.displayName = 'AlertDescription'
export { Alert, AlertTitle, AlertDescription }

50
components/ui/avatar.tsx Normal file
View file

@ -0,0 +1,50 @@
'use client'
import * as React from 'react'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import { cn } from '@/lib/utils'
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted',
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

56
components/ui/button.tsx Normal file
View file

@ -0,0 +1,56 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const buttonVariants = cva(
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-all duration-300 focus-visible:outline-none disabled:text-muted-foreground disabled:bg-foreground/15 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:none',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline:
'border border-input bg-background hover:border-primary/25 hover:bg-primary/10 hover:text-primary',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-primary/10 hover:text-primary',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = 'Button'
export { Button, buttonVariants }

View file

@ -0,0 +1,67 @@
'use client'
import * as React from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { DayPicker } from 'react-day-picker'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn('p-3', className)}
classNames={{
months: 'flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0',
month: 'space-y-4',
caption: 'flex justify-center pt-1 relative items-center',
caption_label: 'text-sm font-medium',
nav: 'space-x-1 flex items-center',
nav_button: cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100'
),
nav_button_previous: 'absolute left-1',
nav_button_next: 'absolute right-1',
table: 'w-full border-collapse space-y-1',
head_row: 'flex',
head_cell:
'text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]',
row: 'flex w-full mt-2',
cell: 'h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-start)]:rounded-l-md [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20',
day: cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal aria-selected:opacity-100'
),
day_range_start: 'day-range-start',
day_range_end: 'day-range-end',
day_selected:
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground',
day_today: 'bg-accent text-accent-foreground',
day_outside:
'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30',
day_disabled: 'text-muted-foreground opacity-50',
day_range_middle:
'aria-selected:bg-accent aria-selected:text-accent-foreground',
day_hidden: 'invisible',
...classNames
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />
}}
{...props}
/>
)
}
Calendar.displayName = 'Calendar'
export { Calendar }

79
components/ui/card.tsx Normal file
View file

@ -0,0 +1,79 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'rounded-lg border bg-card text-card-foreground shadow-sm',
className
)}
{...props}
/>
))
Card.displayName = 'Card'
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 p-6', className)}
{...props}
/>
))
CardHeader.displayName = 'CardHeader'
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
'text-2xl font-semibold leading-none tracking-tight',
className
)}
{...props}
/>
))
CardTitle.displayName = 'CardTitle'
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
CardDescription.displayName = 'CardDescription'
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
))
CardContent.displayName = 'CardContent'
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('flex items-center p-6 pt-0', className)}
{...props}
/>
))
CardFooter.displayName = 'CardFooter'
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

365
components/ui/chart.tsx Normal file
View file

@ -0,0 +1,365 @@
"use client"
import * as React from "react"
import * as RechartsPrimitive from "recharts"
import { cn } from "@/lib/utils"
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: "", dark: ".dark" } as const
export type ChartConfig = {
[k in string]: {
label?: React.ReactNode
icon?: React.ComponentType
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
)
}
type ChartContextProps = {
config: ChartConfig
}
const ChartContext = React.createContext<ChartContextProps | null>(null)
function useChart() {
const context = React.useContext(ChartContext)
if (!context) {
throw new Error("useChart must be used within a <ChartContainer />")
}
return context
}
const ChartContainer = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<
typeof RechartsPrimitive.ResponsiveContainer
>["children"]
}
>(({ id, className, children, config, ...props }, ref) => {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
return (
<ChartContext.Provider value={{ config }}>
<div
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line-line]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
className
)}
{...props}
>
<ChartStyle id={chartId} config={config} />
<RechartsPrimitive.ResponsiveContainer>
{children}
</RechartsPrimitive.ResponsiveContainer>
</div>
</ChartContext.Provider>
)
})
ChartContainer.displayName = "Chart"
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
const colorConfig = Object.entries(config).filter(
([_, config]) => config.theme || config.color
)
if (!colorConfig.length) {
return null
}
return (
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(
([theme, prefix]) => `
${prefix} [data-chart=${id}] {
${colorConfig
.map(([key, itemConfig]) => {
const color =
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
itemConfig.color
return color ? ` --color-${key}: ${color};` : null
})
.join("\n")}
}
`
)
.join("\n"),
}}
/>
)
}
const ChartTooltip = RechartsPrimitive.Tooltip
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & {
hideLabel?: boolean
hideIndicator?: boolean
indicator?: "line" | "dot" | "dashed"
nameKey?: string
labelKey?: string
}
>(
(
{
active,
payload,
className,
indicator = "dot",
hideLabel = false,
hideIndicator = false,
label,
labelFormatter,
labelClassName,
formatter,
color,
nameKey,
labelKey,
},
ref
) => {
const { config } = useChart()
const tooltipLabel = React.useMemo(() => {
if (hideLabel || !payload?.length) {
return null
}
const [item] = payload
const key = `${labelKey || item.dataKey || item.name || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const value =
!labelKey && typeof label === "string"
? config[label as keyof typeof config]?.label || label
: itemConfig?.label
if (labelFormatter) {
return (
<div className={cn("font-medium", labelClassName)}>
{labelFormatter(value, payload)}
</div>
)
}
if (!value) {
return null
}
return <div className={cn("font-medium", labelClassName)}>{value}</div>
}, [
label,
labelFormatter,
payload,
hideLabel,
labelClassName,
config,
labelKey,
])
if (!active || !payload?.length) {
return null
}
const nestLabel = payload.length === 1 && indicator !== "dot"
return (
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</div>
</div>
)
}
)
ChartTooltipContent.displayName = "ChartTooltip"
const ChartLegend = RechartsPrimitive.Legend
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean
nameKey?: string
}
>(
(
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
ref
) => {
const { config } = useChart()
if (!payload?.length) {
return null
}
return (
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
verticalAlign === "top" ? "pb-3" : "pt-3",
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}
)
ChartLegendContent.displayName = "ChartLegend"
// Helper to extract item config from a payload.
function getPayloadConfigFromPayload(
config: ChartConfig,
payload: unknown,
key: string
) {
if (typeof payload !== "object" || payload === null) {
return undefined
}
const payloadPayload =
"payload" in payload &&
typeof payload.payload === "object" &&
payload.payload !== null
? payload.payload
: undefined
let configLabelKey: string = key
if (
key in payload &&
typeof payload[key as keyof typeof payload] === "string"
) {
configLabelKey = payload[key as keyof typeof payload] as string
} else if (
payloadPayload &&
key in payloadPayload &&
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
) {
configLabelKey = payloadPayload[
key as keyof typeof payloadPayload
] as string
}
return configLabelKey in config
? config[configLabelKey]
: config[key as keyof typeof config]
}
export {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
ChartLegend,
ChartLegendContent,
ChartStyle,
}

View file

@ -0,0 +1,30 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

122
components/ui/dialog.tsx Normal file
View file

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View file

@ -0,0 +1,200 @@
'use client'
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { Check, ChevronRight, Circle } from 'lucide-react'
import { cn } from '@/lib/utils'
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-background p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
'px-2 py-1.5 text-sm font-semibold',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup
}

176
components/ui/form.tsx Normal file
View file

@ -0,0 +1,176 @@
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from '@radix-ui/react-slot'
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext
} from 'react-hook-form'
import { cn } from '@/lib/utils'
import { Label } from '@/components/ui/label'
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>')
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn('space-y-2', className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = 'FormItem'
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = 'FormLabel'
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = 'FormControl'
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
)
})
FormDescription.displayName = 'FormDescription'
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn('text-sm font-medium', className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = 'FormMessage'
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField
}

25
components/ui/input.tsx Normal file
View file

@ -0,0 +1,25 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md ring-1 ring-border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50 transition duration-300',
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export { Input }

26
components/ui/label.tsx Normal file
View file

@ -0,0 +1,26 @@
'use client'
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View file

@ -0,0 +1,28 @@
'use client'
import * as React from 'react'
import * as ProgressPrimitive from '@radix-ui/react-progress'
import { cn } from '@/lib/utils'
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
'relative h-4 w-full overflow-hidden rounded-full bg-secondary',
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

160
components/ui/select.tsx Normal file
View file

@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

140
components/ui/sheet.tsx Normal file
View file

@ -0,0 +1,140 @@
'use client'
import * as React from 'react'
import * as SheetPrimitive from '@radix-ui/react-dialog'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
{
variants: {
side: {
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
bottom:
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
right:
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm'
}
},
defaultVariants: {
side: 'right'
}
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = 'right', className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col space-y-2 text-center sm:text-left',
className
)}
{...props}
/>
)
SheetHeader.displayName = 'SheetHeader'
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
className
)}
{...props}
/>
)
SheetFooter.displayName = 'SheetFooter'
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold text-foreground', className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription
}

View file

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

28
components/ui/slider.tsx Normal file
View file

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

29
components/ui/switch.tsx Normal file
View file

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

52
components/ui/tabs.tsx Normal file
View file

@ -0,0 +1,52 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View file

@ -0,0 +1,9 @@
'use client'
import * as React from 'react'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { type ThemeProviderProps } from 'next-themes/dist/types'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

127
components/ui/toast.tsx Normal file
View file

@ -0,0 +1,127 @@
import * as React from 'react'
import * as ToastPrimitives from '@radix-ui/react-toast'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
import { cn } from '@/lib/utils'
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
default: 'border bg-background text-foreground',
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground'
}
},
defaultVariants: {
variant: 'default'
}
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
'absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn('text-sm font-semibold', className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn('text-sm opacity-90', className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction
}

35
components/ui/toaster.tsx Normal file
View file

@ -0,0 +1,35 @@
'use client'
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport
} from '@/components/ui/toast'
import { useToast } from '@/components/ui/use-toast'
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

30
components/ui/tooltip.tsx Normal file
View file

@ -0,0 +1,30 @@
'use client'
import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { cn } from '@/lib/utils'
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 overflow-hidden rounded-md border border-foreground/10 bg-background px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

189
components/ui/use-toast.ts Normal file
View file

@ -0,0 +1,189 @@
// Inspired by react-hot-toast library
import * as React from 'react'
import type { ToastActionElement, ToastProps } from '@/components/ui/toast'
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: 'ADD_TOAST',
UPDATE_TOAST: 'UPDATE_TOAST',
DISMISS_TOAST: 'DISMISS_TOAST',
REMOVE_TOAST: 'REMOVE_TOAST'
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType['ADD_TOAST']
toast: ToasterToast
}
| {
type: ActionType['UPDATE_TOAST']
toast: Partial<ToasterToast>
}
| {
type: ActionType['DISMISS_TOAST']
toastId?: ToasterToast['id']
}
| {
type: ActionType['REMOVE_TOAST']
toastId?: ToasterToast['id']
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: 'REMOVE_TOAST',
toastId: toastId
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case 'ADD_TOAST':
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT)
}
case 'UPDATE_TOAST':
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
)
}
case 'DISMISS_TOAST': {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false
}
: t
)
}
}
case 'REMOVE_TOAST':
if (action.toastId === undefined) {
return {
...state,
toasts: []
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId)
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, 'id'>
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: 'UPDATE_TOAST',
toast: { ...props, id }
})
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
dispatch({
type: 'ADD_TOAST',
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
}
}
})
return {
id: id,
dismiss,
update
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId })
}
}
export { useToast, toast }

View file

@ -0,0 +1,42 @@
'use client'
import { cn } from '@/lib/utils'
interface AvatarCirclesProps {
className?: string
}
export default function AvatarCircles({ className }: AvatarCirclesProps) {
return (
<div
className={cn(
'z-10 flex items-center justify-center -space-x-4 rtl:space-x-reverse pt-3',
className
)}
>
<img
className="h-9 w-9 rounded-full border-2 border-secondary"
src="/testimonials/john-doe.jpg"
alt=""
/>
<img
className="h-9 w-9 rounded-full border-2 border-secondary"
src="/testimonials/john-doe.jpg"
alt=""
/>
<img
className="h-9 w-9 rounded-full border-2 border-secondary"
src="/testimonials/john-doe.jpg"
alt=""
/>
<img
className="h-9 w-9 rounded-full border-2 border-secondary"
src="/testimonials/john-doe.jpg"
alt=""
/>
<img
className="h-9 w-9 rounded-full border-2 border-secondary"
src="/testimonials/john-doe.jpg"
alt=""
/>
</div>
)
}

123
components/user-button.tsx Normal file
View file

@ -0,0 +1,123 @@
'use client'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useCurrentUser } from '@/hooks/use-current-user'
import { signOut } from 'next-auth/react'
import {
Book,
CreditCard,
LayoutDashboard,
LogOut,
Settings
} from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/button'
import { useEffect, useState } from 'react'
export const UserButton = () => {
const userButtonItems = [
{
label: 'Dashboard',
href: '/dashboard',
icon: LayoutDashboard
},
{
label: 'Docs',
href: '/docs',
icon: Book
},
{
label: 'Billing',
href: '/payments',
icon: CreditCard
},
{
label: 'Settings',
href: '/settings',
icon: Settings
}
]
// Random gradient colors for Avatar
const router = useRouter()
const session = useCurrentUser()
const onClick = () => {
router.push('/register')
}
const Logout = () => {
signOut()
router.push('/login')
}
return (
<>
{!session ? (
<div>
<Link
href="/register"
className="flex md:hidden items-center justify-center rounded-lg cursor-pointer transition duration-300 hover:bg-primary/10 px-2 py-2"
>
<LogOut className="h-5.5 w-5" />
</Link>
<Button
type="submit"
onClick={onClick}
className="px-5 rounded-md hidden md:flex"
>
Get Started
</Button>
</div>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
{/* User Avatar / Logo */}
<Avatar className="cursor-pointer">
<AvatarImage src={session.image ? session.image : ''} alt="pfp" />
<AvatarFallback className="bg-gradient-to-r from-blue-400 via-blue-500 to-blue-700"></AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
{/* Content */}
<DropdownMenuContent className="w-48">
<DropdownMenuLabel className="py-0 pt-1">
{session?.name}
</DropdownMenuLabel>
<DropdownMenuLabel className="opacity-70 text-sm font-normal">
{session?.email}
</DropdownMenuLabel>
<DropdownMenuSeparator />
{/* Main Icons */}
<DropdownMenuGroup>
{userButtonItems.map((item, index) => (
<DropdownMenuItem key={index}>
<Link href={item.href} className="flex">
<item.icon className="mr-2 mt-0.5 h-4 w-4" />
<span>{item.label}</span>
</Link>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
{/* Logout Button */}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={Logout} className="cursor-pointer">
<LogOut className="mr-2 mt-0.5 h-4 w-4" />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</>
)
}
export default UserButton

View file

@ -0,0 +1,25 @@
import { db } from '@/lib/db'
// token functionality
export const getPasswordResetTokenByToken = async (token: string) => {
try {
const passwordResetToken = await db.passwordResetToken.findUnique({
where: { token }
})
return passwordResetToken
} catch {
return null
}
}
// Token Email functionality (match emails)
export const getPasswordResetTokenByEmail = async (email: string) => {
try {
const passwordResetToken = await db.passwordResetToken.findFirst({
where: { email }
})
return passwordResetToken
} catch {
return null
}
}

19
data/user.ts Normal file
View file

@ -0,0 +1,19 @@
import { db } from '@/lib/db'
export const getUserByEmail = async (email: string) => {
try {
const user = await db.user.findUnique({ where: { email } })
return user
} catch {
return null
}
}
export const getUserById = async (id: string) => {
try {
const user = await db.user.findUnique({ where: { id } })
return user
} catch {
return null
}
}

View file

@ -0,0 +1,27 @@
import { db } from '@/lib/db'
export const getVerificationTokenByToken = async (token: string) => {
// Get Verification Token
try {
const verificationToken = await db.verificationToken.findUnique({
where: { token }
})
return verificationToken
} catch {
return null
}
}
export const getVerificationTokenByEmail = async (email: string) => {
// Get Email Verification
try {
const verificationToken = await db.verificationToken.findFirst({
where: { email }
})
return verificationToken
} catch {
return null
}
}

143
emails/reset-email.tsx Normal file
View file

@ -0,0 +1,143 @@
import {
Body,
Container,
Head,
Heading,
Html,
Img,
Link,
Preview,
Text
} from '@react-email/components'
import * as React from 'react'
interface ResetPasswordProps {
token?: string
}
const gradientStyle = {
background: 'linear-gradient(to right, #6366F1, #A855F7)', // from-indigo-500 to-purple-500
WebkitBackgroundClip: 'text', // Clips the background to the text (for Safari)
WebkitTextFillColor: 'transparent', // Makes the text color transparent (for Safari)
backgroundClip: 'text', // Clips the background to the text (standard)
color: 'transparent' // Makes the text color transparent
}
export const ResetPassword = ({ token }: ResetPasswordProps) => (
<Html>
<Head />
<Preview>Reset password with this link</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Reset Password</Heading>
<Link
href={`${process.env.APP_URL}/new-password?token=${token}`}
target="_blank"
style={{
...link,
display: 'block',
marginBottom: '16px'
}}
>
Click here to reset password
</Link>
<Text
style={{
...text,
color: '#ababab',
marginTop: '14px',
marginBottom: '16px'
}}
>
If you didn&apos;t try to reset your password, you can safely ignore
this email.
</Text>
<Text
style={{
...text,
marginTop: '14px',
marginBottom: '16px',
fontWeight: 'bold'
}}
>
<span>nizzy</span>
<span style={gradientStyle}>abi</span>
</Text>
<Text style={footer}>
<Link
href="https://nizzyabi.com"
target="_blank"
style={{ ...link, color: '#898989' }}
>
nizzyabi.com
</Link>
, learn to code
<br />
&& have fun doing it.
</Text>
</Container>
</Body>
</Html>
)
ResetPassword.PreviewProps = {
loginCode: 'sparo-ndigo-amurt-secan'
} as ResetPasswordProps
export default ResetPassword
const main = {
backgroundColor: '#ffffff'
}
const container = {
paddingLeft: '12px',
paddingRight: '12px',
margin: '0 auto'
}
const h1 = {
color: '#333',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: '24px',
fontWeight: 'bold',
padding: '0'
}
const link = {
color: '#2754C5',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: '14px',
textDecoration: 'underline'
}
const text = {
color: '#333',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: '14px',
margin: '24px 0'
}
const footer = {
color: '#898989',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: '12px',
lineHeight: '22px',
marginTop: '12px',
marginBottom: '24px'
}
const code = {
display: 'inline-block',
padding: '16px 4.5%',
width: '90.5%',
backgroundColor: '#f4f4f4',
borderRadius: '5px',
border: '1px solid #eee',
color: '#333'
}

134
emails/verify-email.tsx Normal file
View file

@ -0,0 +1,134 @@
import {
Body,
Container,
Head,
Heading,
Html,
Img,
Link,
Preview,
Text
} from '@react-email/components'
import * as React from 'react'
interface LinkEmailProps {
token?: string
}
export const LinkEmail = ({ token }: LinkEmailProps) => (
<Html>
<Head />
<Preview>Log in with this link</Preview>
<Body style={main}>
<Container style={container}>
<Heading style={h1}>Login</Heading>
<Link
href={`${process.env.APP_URL}/verify-email?token=${token}`}
target="_blank"
style={{
...link,
display: 'block',
marginBottom: '16px'
}}
>
Click here to verify your email
</Link>
<Text
style={{
...text,
color: '#ababab',
marginTop: '14px',
marginBottom: '16px'
}}
>
If you didn&apos;t try to login, you can safely ignore this email.
</Text>
<Text
style={{
...text,
marginTop: '14px',
marginBottom: '16px'
}}
>
your logo here
</Text>
<Text style={footer}>
<Link
href="https://nizzyabi.com"
target="_blank"
style={{ ...link, color: '#898989' }}
>
yoururl.com
</Link>
, learn to code
<br />
&& have fun doing it.
</Text>
</Container>
</Body>
</Html>
)
LinkEmail.PreviewProps = {
loginCode: 'sparo-ndigo-amurt-secan'
} as LinkEmailProps
export default LinkEmail
const main = {
backgroundColor: '#ffffff'
}
const container = {
paddingLeft: '12px',
paddingRight: '12px',
margin: '0 auto'
}
const h1 = {
color: '#333',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: '24px',
fontWeight: 'bold',
margin: '40px 0',
padding: '0'
}
const link = {
color: '#2754C5',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: '14px',
textDecoration: 'underline'
}
const text = {
color: '#333',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: '14px',
margin: '24px 0'
}
const footer = {
color: '#898989',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: '12px',
lineHeight: '22px',
marginTop: '12px',
marginBottom: '24px'
}
const code = {
display: 'inline-block',
padding: '16px 4.5%',
width: '90.5%',
backgroundColor: '#f4f4f4',
borderRadius: '5px',
border: '1px solid #eee',
color: '#333'
}

View file

@ -0,0 +1,7 @@
import { useSession } from 'next-auth/react'
export const useCurrentRole = () => {
const session = useSession()
return session.data?.user?.role
}

View file

@ -0,0 +1,7 @@
// Hook to get user data in a more convenient way
import { useSession } from 'next-auth/react'
export const useCurrentUser = () => {
const session = useSession()
return session.data?.user
}

14
hooks/use-pro-modal.ts Normal file
View file

@ -0,0 +1,14 @@
import { create } from 'zustand'
// create interface for proModal Display
interface userProModalStore {
isOpen: boolean
onOpen: () => void
onClose: () => void
}
// if the button is clicked, isOpen will become true and the modal will be displayed
export const useProModal = create<userProModalStore>((set) => ({
isOpen: false,
onOpen: () => set({ isOpen: true }),
onClose: () => set({ isOpen: false })
}))

Some files were not shown because too many files have changed in this diff Show more