├── lib ├── constants.ts └── utils.ts ├── .eslintrc.json ├── .env.local.example ├── app ├── favicon.ico ├── login │ └── page.tsx ├── layout.tsx ├── page.tsx └── globals.css ├── next.config.mjs ├── postcss.config.mjs ├── components ├── features │ ├── LogoutButton.tsx │ ├── HomeBody.tsx │ ├── OneTapComp.tsx │ ├── UserButton │ │ ├── LogoutDropdownMenuItem.tsx │ │ └── UserButton.tsx │ └── AuthenticationDialog.tsx └── ui │ ├── avatar.tsx │ ├── button.tsx │ ├── dialog.tsx │ └── dropdown-menu.tsx ├── components.json ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── archive └── middleware.ts ├── package.json ├── middleware.ts ├── README.md └── tailwind.config.ts /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const flowId = "sign-up-or-in"; -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_DESCOPE_PROJECT_ID= -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/descope-sample-apps/onetap-modal/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /components/features/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useDescope } from "@descope/nextjs-sdk/client"; 4 | import { Button } from "../ui/button"; 5 | 6 | export default function LogoutButton() { 7 | const { logout } = useDescope(); 8 | 9 | return ; 13 | } -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /app/login/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { flowId } from '@/lib/constants'; 4 | import { Descope } from '@descope/nextjs-sdk'; 5 | import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; 6 | import { useRouter, useSearchParams } from 'next/navigation'; 7 | import { Suspense } from 'react'; 8 | 9 | export default function Login() { 10 | const router = useRouter(); 11 | return ; 12 | 13 | } 14 | 15 | function LoginContent({ router } : { router: AppRouterInstance }) { 16 | const searchParams = useSearchParams(); 17 | const chatMode = searchParams.get('chatMode'); 18 | return ( 19 | router.push(`/${chatMode ? `?chatMode=${chatMode}` : ''}`)} 22 | /> 23 | ); 24 | } -------------------------------------------------------------------------------- /components/features/HomeBody.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useSession } from "@descope/nextjs-sdk/client"; 4 | import OneTapComp from "./OneTapComp"; 5 | import LogoutButton from "./LogoutButton"; 6 | import AuthenticationDialog from "./AuthenticationDialog"; 7 | 8 | export default function HomeBody() { 9 | const { isAuthenticated, isSessionLoading } = useSession(); 10 | 11 | if (isSessionLoading) { 12 | return

Loading...

; 13 | } 14 | 15 | return ( 16 |
17 | 18 | {isAuthenticated ?
19 |

Authenticated

20 | 21 |
:
22 |

Not Authenticated

23 | 24 |
} 25 |
26 | ); 27 | 28 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter as FontSans } from "next/font/google" 3 | import "./globals.css"; 4 | import { cn } from "@/lib/utils"; 5 | import { AuthProvider } from "@descope/nextjs-sdk"; 6 | 7 | const fontSans = FontSans({ 8 | subsets: ["latin"], 9 | variable: "--font-sans", 10 | }) 11 | 12 | export const metadata: Metadata = { 13 | title: "Create Next App", 14 | description: "Generated by create next app", 15 | }; 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: Readonly<{ 20 | children: React.ReactNode; 21 | }>) { 22 | return ( 23 | 24 | 25 | {children} 29 | 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /archive/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import type { NextRequest } from 'next/server'; 3 | 4 | export function middleware(request: NextRequest) { 5 | const url = request.nextUrl.clone(); 6 | 7 | // Check if the query parameters exist 8 | const code = url.searchParams.get('code'); 9 | const descopeLoginFlow = url.searchParams.get('descope-login-flow'); 10 | 11 | if (code && descopeLoginFlow) { 12 | // Retain the original query parameters in the redirect URL 13 | const newUrl = new URL('/login', request.url); 14 | newUrl.searchParams.set('code', code); 15 | newUrl.searchParams.set('descope-login-flow', descopeLoginFlow); 16 | return NextResponse.redirect(newUrl); 17 | } 18 | 19 | // Continue with the request if the parameters are not present 20 | return NextResponse.next(); 21 | } 22 | 23 | // Specify which paths should use the middleware 24 | export const config = { 25 | matcher: '/', // Apply middleware to the root path, adjust as needed 26 | }; 27 | -------------------------------------------------------------------------------- /components/features/OneTapComp.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useDescope, useSession } from "@descope/nextjs-sdk/client"; 4 | import { useEffect } from "react"; 5 | 6 | let oneTapInitialized = false; 7 | 8 | const OneTapComp = () => { 9 | const oneTap = true; 10 | const sdk = useDescope(); 11 | const { isAuthenticated, isSessionLoading } = useSession(); 12 | 13 | const startOneTap = async () => { 14 | // eslint-disable-next-line 15 | if (oneTapInitialized) return; 16 | 17 | const res: any = await sdk.fedcm.oneTap('google-implicit'); 18 | console.log("Response: ", res.data); 19 | oneTapInitialized = true; 20 | }; 21 | useEffect(() => { 22 | if (oneTap && !isAuthenticated && !isSessionLoading) { 23 | startOneTap(); 24 | } 25 | }, [isAuthenticated, isSessionLoading]); 26 | 27 | // Return some JSX here. For example, return null if there's nothing to render: 28 | return null; 29 | }; 30 | 31 | export default OneTapComp -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@descope/nextjs-sdk": "^0.0.16", 13 | "@radix-ui/react-avatar": "^1.1.0", 14 | "@radix-ui/react-dialog": "^1.0.5", 15 | "@radix-ui/react-dropdown-menu": "^2.1.1", 16 | "@radix-ui/react-slot": "^1.0.2", 17 | "class-variance-authority": "^0.7.0", 18 | "clsx": "^2.1.1", 19 | "lucide-react": "^0.395.0", 20 | "next": "14.2.3", 21 | "react": "^18", 22 | "react-dom": "^18", 23 | "tailwind-merge": "^2.3.0", 24 | "tailwindcss-animate": "^1.0.7" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "^20", 28 | "@types/react": "^18", 29 | "@types/react-dom": "^18", 30 | "eslint": "^8", 31 | "eslint-config-next": "14.2.3", 32 | "postcss": "^8", 33 | "tailwindcss": "^3.4.1", 34 | "typescript": "^5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /components/features/UserButton/LogoutDropdownMenuItem.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useDescope } from "@descope/nextjs-sdk/client"; 4 | import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; 5 | import { LoaderIcon, LogOut } from "lucide-react"; 6 | import { useState } from "react"; 7 | 8 | export default function LogoutDropdownMenuItem() { 9 | const { logout } = useDescope(); 10 | const [loading, setLoading] = useState(false); 11 | 12 | return { 13 | // setLoading(true); 14 | await logout(); 15 | window.location.reload(); 16 | // setTimeout(() => setLoading(false), 1000); 17 | }}> 18 | {loading ? <> 19 | 20 | Logging out... 21 | : <> 22 | 23 | Log out 24 | } 25 | {/* ⇧⌘Q */} 26 | 27 | } -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | import AuthenticationDialog from "@/components/features/AuthenticationDialog"; 3 | import LogoutButton from "@/components/features/LogoutButton"; 4 | import OneTapComp from "@/components/features/OneTapComp"; 5 | import UserButton from "@/components/features/UserButton/UserButton"; 6 | import { AvatarFallback } from "@/components/ui/avatar"; 7 | import { useSession, useUser } from "@descope/nextjs-sdk/client"; 8 | import { session } from "@descope/nextjs-sdk/server"; 9 | import { Avatar, AvatarImage } from "@radix-ui/react-avatar"; 10 | 11 | export default function Home() { 12 | const sessionRes = session(); 13 | 14 | return ( 15 |
16 | 17 | {sessionRes ?
18 |

Authenticated

19 | 22 | {/* */} 23 |
:
24 |

Not Authenticated

25 | 26 |
} 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | // src/middleware.ts 2 | import { authMiddleware } from '@descope/nextjs-sdk/server' 3 | 4 | export default authMiddleware({ 5 | // The Descope project ID to use for authentication 6 | // Defaults to process.env.DESCOPE_PROJECT_ID 7 | projectId: process.env.NEXT_PUBLIC_DESCOPE_PROJECT_ID!, 8 | 9 | // The URL to redirect to if the user is not authenticated 10 | // Defaults to process.env.SIGN_IN_ROUTE or '/sign-in' if not provided 11 | // NOTE: In case it contains query parameters that exist in the original URL, they will override the original query parameters. e.g. if the original URL is /page?param1=1¶m2=2 and the redirect URL is /sign-in?param1=3, the final redirect URL will be /sign-in?param1=3¶m2=2 12 | // redirectUrl: '/login', 13 | 14 | // An array of public routes that do not require authentication 15 | // In addition to the default public routes: 16 | // - process.env.SIGN_IN_ROUTE or /sign-in if not provided 17 | // - process.env.SIGN_UP_ROUTE or /sign-up if not provided 18 | publicRoutes: ['/', '/login'] 19 | }) 20 | 21 | export const config = { 22 | matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'] 23 | } -------------------------------------------------------------------------------- /components/features/AuthenticationDialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react'; 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogTrigger, 8 | } from "@/components/ui/dialog"; 9 | import { Descope } from "@descope/nextjs-sdk"; 10 | import { Button } from "@/components/ui/button"; 11 | import { flowId } from "@/lib/constants"; 12 | import { useSearchParams } from 'next/navigation'; 13 | 14 | export default function AuthenticationDialog() { 15 | const [isOpen, setIsOpen] = useState(false); 16 | 17 | const searchParams = useSearchParams(); 18 | const chatMode = searchParams.get('chatMode'); 19 | 20 | const getRedirectUrl = () => { 21 | return `${window.location.origin}/login?${chatMode ? `chatMode=${chatMode}` : ''}`; 22 | } 23 | 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, add your Descope project ID (`NEXT_PUBLIC_DESCOPE_PROJECT_ID`) to the `.env.local`. You will also need to configure a Custom Social Provider with Google named "google-implicit". 6 | 7 | Then, install dependencies and run the development server: 8 | 9 | ```bash 10 | npm i 11 | npm run dev 12 | ``` 13 | 14 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 15 | 16 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 17 | 18 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 19 | 20 | ## Learn More 21 | 22 | To learn more about Next.js, take a look at the following resources: 23 | 24 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 25 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 26 | 27 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 28 | 29 | ## Deploy on Vercel 30 | 31 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 32 | 33 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 34 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /components/features/UserButton/UserButton.tsx: -------------------------------------------------------------------------------- 1 | import { LogOut } from "lucide-react"; 2 | import { Avatar, AvatarFallback } from "../../ui/avatar"; 3 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuLabel, DropdownMenuShortcut } from "../../ui/dropdown-menu"; 4 | import LogoutDropdownMenuItem from "./LogoutDropdownMenuItem"; 5 | 6 | function UserButton({ session }: { session: any }) { 7 | 8 | const getInitials = (name: string) => { 9 | return name 10 | .split(" ") 11 | .map((word) => word.charAt(0).toUpperCase()) 12 | .join(""); 13 | }; 14 | 15 | if (!session) { 16 | return null; 17 | } 18 | const name = session.token.name 19 | 20 | return 21 |
22 | 23 | {/* */} 24 | {getInitials(name)} 25 | 26 |

{name}

27 |
28 |
29 | } 30 | 31 | export default UserButton 32 | 33 | 34 | function UserDropdown({ children }: { children: React.ReactNode }) { 35 | return 36 | 37 | {children} 38 | 39 | 40 | My Account 41 | 42 | {/* Profile 43 | Billing 44 | Team 45 | Subscription */} 46 | 47 | 48 | 49 | } -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium 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", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | const { fontFamily } = require("tailwindcss/defaultTheme") 3 | 4 | const config = { 5 | darkMode: ["class"], 6 | content: [ 7 | './pages/**/*.{ts,tsx}', 8 | './components/**/*.{ts,tsx}', 9 | './app/**/*.{ts,tsx}', 10 | './src/**/*.{ts,tsx}', 11 | ], 12 | prefix: "", 13 | theme: { 14 | container: { 15 | center: true, 16 | padding: "2rem", 17 | screens: { 18 | "2xl": "1400px", 19 | }, 20 | }, 21 | extend: { 22 | fontFamily: { 23 | sans: ["var(--font-sans)", ...fontFamily.sans], 24 | }, 25 | colors: { 26 | border: "hsl(var(--border))", 27 | input: "hsl(var(--input))", 28 | ring: "hsl(var(--ring))", 29 | background: "hsl(var(--background))", 30 | foreground: "hsl(var(--foreground))", 31 | primary: { 32 | DEFAULT: "hsl(var(--primary))", 33 | foreground: "hsl(var(--primary-foreground))", 34 | }, 35 | secondary: { 36 | DEFAULT: "hsl(var(--secondary))", 37 | foreground: "hsl(var(--secondary-foreground))", 38 | }, 39 | destructive: { 40 | DEFAULT: "hsl(var(--destructive))", 41 | foreground: "hsl(var(--destructive-foreground))", 42 | }, 43 | muted: { 44 | DEFAULT: "hsl(var(--muted))", 45 | foreground: "hsl(var(--muted-foreground))", 46 | }, 47 | accent: { 48 | DEFAULT: "hsl(var(--accent))", 49 | foreground: "hsl(var(--accent-foreground))", 50 | }, 51 | popover: { 52 | DEFAULT: "hsl(var(--popover))", 53 | foreground: "hsl(var(--popover-foreground))", 54 | }, 55 | card: { 56 | DEFAULT: "hsl(var(--card))", 57 | foreground: "hsl(var(--card-foreground))", 58 | }, 59 | }, 60 | borderRadius: { 61 | lg: "var(--radius)", 62 | md: "calc(var(--radius) - 2px)", 63 | sm: "calc(var(--radius) - 4px)", 64 | }, 65 | keyframes: { 66 | "accordion-down": { 67 | from: { height: "0" }, 68 | to: { height: "var(--radix-accordion-content-height)" }, 69 | }, 70 | "accordion-up": { 71 | from: { height: "var(--radix-accordion-content-height)" }, 72 | to: { height: "0" }, 73 | }, 74 | }, 75 | animation: { 76 | "accordion-down": "accordion-down 0.2s ease-out", 77 | "accordion-up": "accordion-up 0.2s ease-out", 78 | }, 79 | }, 80 | }, 81 | plugins: [require("tailwindcss-animate")], 82 | } satisfies Config 83 | 84 | export default config -------------------------------------------------------------------------------- /components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | --------------------------------------------------------------------------------