├── .env.example ├── src ├── app │ ├── favicon.ico │ ├── (auth) │ │ ├── sign-in │ │ │ └── [[...sign-in]] │ │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── [[...sign-up]] │ │ │ └── page.tsx │ ├── robots.ts │ ├── api │ │ ├── test-db │ │ │ └── route.ts │ │ └── webhook │ │ │ └── clerk │ │ │ └── route.ts │ ├── sitemap.ts │ ├── layout.tsx │ ├── page.tsx │ ├── (dashboard) │ │ └── dashboard │ │ │ └── page.tsx │ └── globals.css ├── lib │ ├── utils.ts │ ├── mongodb.ts │ └── csrf.ts ├── middleware.ts ├── components │ ├── ui │ │ ├── sonner.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── alert.tsx │ │ ├── theme-toggle.tsx │ │ ├── card.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── form.tsx │ │ └── dropdown-menu.tsx │ └── layout │ │ ├── Footer.tsx │ │ ├── AccessibilityControls.tsx │ │ └── Navbar.tsx ├── models │ └── User.ts ├── utils │ ├── test-db-connection.ts │ ├── metadata.ts │ └── validation.ts ├── scripts │ └── test-mongodb.js └── context │ └── AccessibilityContext.tsx ├── postcss.config.mjs ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── next.config.ts ├── eslint.config.mjs ├── components.json ├── next.config.js ├── tsconfig.json ├── .gitignore ├── README.md ├── package.json ├── tailwind.config.ts └── PLAN.md /.env.example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoodini/yuv-ai-nextjs-boilerplate/HEAD/.env.example -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hoodini/yuv-ai-nextjs-boilerplate/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware } from '@clerk/nextjs/server' 2 | 3 | // Pass the clerkMiddleware to Next.js as middleware 4 | export default clerkMiddleware(); 5 | 6 | export const config = { 7 | matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], 8 | }; -------------------------------------------------------------------------------- /src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from 'next'; 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: '*', 7 | allow: '/', 8 | disallow: ['/private/', '/api/'], 9 | }, 10 | sitemap: 'https://your-website.com/sitemap.xml', 11 | }; 12 | } -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /src/app/api/test-db/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import { testMongoDbConnection } from '@/utils/test-db-connection'; 3 | 4 | export async function GET() { 5 | try { 6 | const result = await testMongoDbConnection(); 7 | 8 | if (result.success) { 9 | return NextResponse.json(result, { status: 200 }); 10 | } else { 11 | return NextResponse.json(result, { status: 500 }); 12 | } 13 | } catch (error: any) { 14 | return NextResponse.json( 15 | { success: false, message: 'Failed to test connection', error: error.message }, 16 | { status: 500 } 17 | ); 18 | } 19 | } -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner, ToasterProps } from "sonner" 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } 26 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const withBundleAnalyzer = process.env.ANALYZE === 'true' 4 | ? require('@next/bundle-analyzer')({ 5 | enabled: true, 6 | }) 7 | : (config) => config; 8 | 9 | const nextConfig = { 10 | reactStrictMode: true, 11 | images: { 12 | domains: [ 13 | 'img.clerk.com', // For Clerk user images 14 | ], 15 | }, 16 | // Enable SWC minification 17 | swcMinify: true, 18 | compiler: { 19 | // Enables emotion for styled components 20 | emotion: false, 21 | // Remove console.log in production 22 | removeConsole: process.env.NODE_ENV === 'production', 23 | }, 24 | }; 25 | 26 | module.exports = withBundleAnalyzer(nextConfig); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/models/User.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema, models } from 'mongoose'; 2 | 3 | const userSchema = new Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: [true, 'Name is required'], 8 | trim: true, 9 | }, 10 | email: { 11 | type: String, 12 | required: [true, 'Email is required'], 13 | unique: true, 14 | trim: true, 15 | lowercase: true, 16 | }, 17 | clerkId: { 18 | type: String, 19 | required: true, 20 | unique: true, 21 | }, 22 | profileImage: { 23 | type: String, 24 | default: '', 25 | }, 26 | }, 27 | { 28 | timestamps: true, 29 | } 30 | ); 31 | 32 | const User = models.User || mongoose.model('User', userSchema); 33 | export default User; -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from 'next'; 2 | 3 | export default function sitemap(): MetadataRoute.Sitemap { 4 | const baseUrl = 'https://your-website.com'; 5 | 6 | return [ 7 | { 8 | url: baseUrl, 9 | lastModified: new Date(), 10 | changeFrequency: 'daily', 11 | priority: 1, 12 | }, 13 | { 14 | url: `${baseUrl}/dashboard`, 15 | lastModified: new Date(), 16 | changeFrequency: 'weekly', 17 | priority: 0.8, 18 | }, 19 | { 20 | url: `${baseUrl}/sign-in`, 21 | lastModified: new Date(), 22 | changeFrequency: 'monthly', 23 | priority: 0.5, 24 | }, 25 | { 26 | url: `${baseUrl}/sign-up`, 27 | lastModified: new Date(), 28 | changeFrequency: 'monthly', 29 | priority: 0.5, 30 | }, 31 | ]; 32 | } -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | # clerk 42 | .clerk 43 | 44 | # clerk configuration (can include secrets) 45 | /.clerk/ 46 | 47 | # IDE files 48 | .idea/ 49 | .vscode/ 50 | *.swp 51 | *.swo 52 | 53 | # OS generated files 54 | .DS_Store 55 | .DS_Store? 56 | ._* 57 | .Spotlight-V100 58 | .Trashes 59 | ehthumbs.db 60 | Thumbs.db 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YUV.AI Next.js Boilerplate 2 | 3 | A modern Next.js boilerplate with Clerk authentication, Tailwind CSS, and shadcn/ui components. 4 | 5 | ![Boilerplate Homepage - Light](https://i.imgur.com/CnwTENH.png) 6 | 7 | ![Boilerplate Homepage - Dark](https://i.imgur.com/KoLCKZQ.png) 8 | 9 | ![Boilerplate Dashboard - Light](https://i.imgur.com/W3p472n.png) 10 | 11 | ## Features 12 | 13 | - Next.js 14+ with App Router 14 | - Authentication with Clerk 15 | - UI Components with shadcn/ui 16 | - Styling with Tailwind CSS 17 | - Dark/Light mode toggle 18 | - Mobile-first responsive design 19 | - Accessible UI components 20 | 21 | ## Getting Started 22 | 23 | 1. Clone this repository 24 | 2. Copy `.env.example` to `.env.local` and fill in your own values 25 | 3. Install dependencies with `npm install` 26 | 4. Run the development server with `npm run dev` 27 | 28 | ## Created By 29 | 30 | Yuval Avidani, AI Builder & Speaker 31 | 32 | "Fly High With YUV.AI" 33 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /src/lib/mongodb.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const MONGODB_URI = process.env.MONGODB_URI; 4 | 5 | if (!MONGODB_URI) { 6 | throw new Error('Please define the MONGODB_URI environment variable inside .env.local'); 7 | } 8 | 9 | // Define the cached type 10 | interface Cached { 11 | conn: typeof mongoose | null; 12 | promise: Promise | null; 13 | } 14 | 15 | // Add the mongoose property to global 16 | declare global { 17 | var mongoose: Cached | undefined; 18 | } 19 | 20 | let cached: Cached = global.mongoose || { conn: null, promise: null }; 21 | 22 | if (!global.mongoose) { 23 | global.mongoose = cached; 24 | } 25 | 26 | export async function connectToDatabase() { 27 | if (cached.conn) { 28 | return cached.conn; 29 | } 30 | 31 | if (!cached.promise) { 32 | const opts = { 33 | bufferCommands: false, 34 | }; 35 | 36 | cached.promise = mongoose.connect(MONGODB_URI!, opts).then((mongoose) => { 37 | return mongoose; 38 | }); 39 | } 40 | 41 | try { 42 | cached.conn = await cached.promise; 43 | } catch (e) { 44 | cached.promise = null; 45 | throw e; 46 | } 47 | 48 | return cached.conn; 49 | } -------------------------------------------------------------------------------- /src/utils/test-db-connection.ts: -------------------------------------------------------------------------------- 1 | import { connectToDatabase } from '@/lib/mongodb'; 2 | 3 | /** 4 | * Tests the MongoDB connection 5 | * @returns {Promise<{ success: boolean, message: string }>} The result of the connection test 6 | */ 7 | export async function testMongoDbConnection() { 8 | try { 9 | const mongoose = await connectToDatabase(); 10 | 11 | // Check connection state 12 | if (mongoose.connection.readyState === 1) { 13 | return { 14 | success: true, 15 | message: 'Successfully connected to MongoDB', 16 | details: { 17 | host: mongoose.connection.host, 18 | name: mongoose.connection.name, 19 | models: Object.keys(mongoose.models), 20 | readyState: 'Connected' 21 | } 22 | }; 23 | } else { 24 | const states = ['Disconnected', 'Connected', 'Connecting', 'Disconnecting']; 25 | return { 26 | success: false, 27 | message: 'Not connected to MongoDB', 28 | details: { 29 | readyState: states[mongoose.connection.readyState] || 'Unknown' 30 | } 31 | }; 32 | } 33 | } catch (error: any) { 34 | return { 35 | success: false, 36 | message: 'Failed to connect to MongoDB', 37 | error: error.message 38 | }; 39 | } 40 | } -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scripts/test-mongodb.js: -------------------------------------------------------------------------------- 1 | // This is a standalone script to test MongoDB connection 2 | // Run with: node -r dotenv/config src/scripts/test-mongodb.js 3 | 4 | const mongoose = require('mongoose'); 5 | 6 | async function testConnection() { 7 | const uri = process.env.MONGODB_URI; 8 | 9 | if (!uri) { 10 | console.error('❌ MONGODB_URI environment variable is not set'); 11 | process.exit(1); 12 | } 13 | 14 | console.log('🔄 Attempting to connect to MongoDB...'); 15 | 16 | try { 17 | await mongoose.connect(uri, { bufferCommands: false }); 18 | 19 | console.log('✅ Successfully connected to MongoDB'); 20 | console.log(`📊 Connection Details:`); 21 | console.log(` - Host: ${mongoose.connection.host}`); 22 | console.log(` - Database: ${mongoose.connection.name}`); 23 | console.log(` - Ready State: Connected (${mongoose.connection.readyState})`); 24 | 25 | // List collections if any 26 | const collections = await mongoose.connection.db.listCollections().toArray(); 27 | console.log(`📁 Collections: ${collections.length > 0 ? collections.map(c => c.name).join(', ') : 'No collections found'}`); 28 | 29 | } catch (error) { 30 | console.error('❌ Failed to connect to MongoDB'); 31 | console.error(` Error: ${error.message}`); 32 | process.exit(1); 33 | } finally { 34 | // Close the connection 35 | await mongoose.disconnect(); 36 | console.log('🔌 Connection closed'); 37 | process.exit(0); 38 | } 39 | } 40 | 41 | testConnection(); -------------------------------------------------------------------------------- /src/lib/csrf.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from 'crypto'; 2 | import { cookies } from 'next/headers'; 3 | 4 | export function generateCSRFToken(): string { 5 | const token = randomBytes(32).toString('hex'); 6 | cookies().set('csrf-token', token, { 7 | httpOnly: true, 8 | secure: process.env.NODE_ENV === 'production', 9 | sameSite: 'strict', 10 | path: '/', 11 | maxAge: 3600, // 1 hour 12 | }); 13 | return token; 14 | } 15 | 16 | export function validateCSRFToken(token: string): boolean { 17 | const storedToken = cookies().get('csrf-token')?.value; 18 | if (!storedToken || storedToken !== token) { 19 | return false; 20 | } 21 | return true; 22 | } 23 | 24 | // Client-side functions to work with CSRF tokens 25 | export const clientCSRF = { 26 | getCSRFFromMeta: (): string | null => { 27 | if (typeof document === 'undefined') return null; 28 | const csrfMeta = document.querySelector('meta[name="csrf-token"]'); 29 | return csrfMeta ? csrfMeta.getAttribute('content') : null; 30 | }, 31 | 32 | attachCSRFToFormData: (formData: FormData): FormData => { 33 | const token = clientCSRF.getCSRFFromMeta(); 34 | if (token) { 35 | formData.append('csrf-token', token); 36 | } 37 | return formData; 38 | }, 39 | 40 | attachCSRFToHeaders: (headers: HeadersInit = {}): HeadersInit => { 41 | const token = clientCSRF.getCSRFFromMeta(); 42 | if (token) { 43 | const newHeaders = new Headers(headers); 44 | newHeaders.append('X-CSRF-Token', token); 45 | return newHeaders; 46 | } 47 | return headers; 48 | } 49 | }; -------------------------------------------------------------------------------- /src/utils/metadata.ts: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | type MetadataProps = { 4 | title?: string; 5 | description?: string; 6 | keywords?: string[]; 7 | image?: string; 8 | author?: string; 9 | twitterHandle?: string; 10 | }; 11 | 12 | export function constructMetadata({ 13 | title = 'Next.js Boilerplate by YUV.AI', 14 | description = 'A modern Next.js boilerplate with MongoDB, Clerk, Tailwind CSS, and shadcn/ui', 15 | keywords = ['Next.js', 'React', 'Tailwind CSS', 'MongoDB', 'Clerk', 'YUV.AI'], 16 | image = '/og-image.png', 17 | author = 'Yuval Avidani', 18 | twitterHandle = '@yuvlav', 19 | }: MetadataProps = {}): Metadata { 20 | return { 21 | title, 22 | description, 23 | keywords: keywords.join(', '), 24 | authors: [{ name: author, url: 'https://linktr.ee/yuvai' }], 25 | creator: 'YUV.AI', 26 | openGraph: { 27 | type: 'website', 28 | locale: 'en_US', 29 | url: 'https://your-website.com', 30 | title, 31 | description, 32 | siteName: 'Next.js Boilerplate by YUV.AI', 33 | images: [ 34 | { 35 | url: image, 36 | width: 1200, 37 | height: 630, 38 | alt: title, 39 | }, 40 | ], 41 | }, 42 | twitter: { 43 | card: 'summary_large_image', 44 | title, 45 | description, 46 | images: [image], 47 | creator: twitterHandle, 48 | }, 49 | robots: { 50 | index: true, 51 | follow: true, 52 | googleBot: { 53 | index: true, 54 | follow: true, 55 | 'max-video-preview': -1, 56 | 'max-image-preview': 'large', 57 | 'max-snippet': -1, 58 | }, 59 | }, 60 | metadataBase: new URL('https://your-website.com'), 61 | }; 62 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boiler-plate", 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 | "format": "prettier --write .", 11 | "analyze": "cross-env ANALYZE=true next build", 12 | "type-check": "tsc --noEmit", 13 | "postinstall": "echo 'Installed by Yuval Avidani - Fly High With YUV.AI'" 14 | }, 15 | "dependencies": { 16 | "@clerk/nextjs": "^6.18.5", 17 | "@hookform/resolvers": "^5.0.1", 18 | "@radix-ui/react-dialog": "^1.1.11", 19 | "@radix-ui/react-dropdown-menu": "^2.1.12", 20 | "@radix-ui/react-label": "^2.1.4", 21 | "@radix-ui/react-slot": "^1.2.0", 22 | "class-variance-authority": "^0.7.1", 23 | "clsx": "^2.1.1", 24 | "framer-motion": "^12.9.4", 25 | "lucide-react": "^0.507.0", 26 | "mongodb": "^6.16.0", 27 | "mongoose": "^8.14.1", 28 | "next": "15.3.1", 29 | "next-themes": "^0.4.6", 30 | "react": "^19.0.0", 31 | "react-dom": "^19.0.0", 32 | "react-hook-form": "^7.56.2", 33 | "sonner": "^2.0.3", 34 | "svix": "^1.64.1", 35 | "tailwind-merge": "^3.2.0", 36 | "zod": "^3.24.3" 37 | }, 38 | "devDependencies": { 39 | "@eslint/eslintrc": "^3", 40 | "@next/bundle-analyzer": "^15.3.1", 41 | "@shadcn/ui": "^0.0.4", 42 | "@tailwindcss/postcss": "^4", 43 | "@types/node": "^20", 44 | "@types/react": "^19", 45 | "@types/react-dom": "^19", 46 | "cross-env": "^7.0.3", 47 | "eslint": "^9", 48 | "eslint-config-next": "15.3.1", 49 | "prettier": "^3.5.3", 50 | "tailwindcss": "^4", 51 | "tailwindcss-animate": "^1.0.7", 52 | "tw-animate-css": "^1.2.9", 53 | "typescript": "^5" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-card text-card-foreground", 12 | destructive: 13 | "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<"div"> & VariantProps) { 27 | return ( 28 |
34 | ) 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { 38 | return ( 39 |
47 | ) 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<"div">) { 54 | return ( 55 |
63 | ) 64 | } 65 | 66 | export { Alert, AlertTitle, AlertDescription } 67 | -------------------------------------------------------------------------------- /src/components/ui/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { useAccessibility } from "@/context/AccessibilityContext" 5 | import { Button } from "@/components/ui/button" 6 | import { Sun, Moon } from "lucide-react" 7 | import { AnimatePresence, motion } from "framer-motion" 8 | 9 | export function ThemeToggle() { 10 | const { darkMode, toggleDarkMode } = useAccessibility() 11 | 12 | const handleToggle = () => { 13 | console.log("Theme toggle button clicked, current darkMode:", darkMode); 14 | toggleDarkMode(); 15 | } 16 | 17 | React.useEffect(() => { 18 | console.log("ThemeToggle rendered with darkMode:", darkMode); 19 | }, [darkMode]); 20 | 21 | return ( 22 | 55 | ) 56 | } -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /src/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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90 dark:text-white dark:shadow-sm dark:shadow-primary/30 dark:text-shadow-sm", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /src/utils/validation.ts: -------------------------------------------------------------------------------- 1 | export const sanitizeInput = (input: string): string => { 2 | return input 3 | .trim() 4 | .replace(/&/g, '&') 5 | .replace(//g, '>') 7 | .replace(/"/g, '"') 8 | .replace(/'/g, ''') 9 | .replace(/\//g, '/'); 10 | }; 11 | 12 | export const validateEmail = (email: string): boolean => { 13 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 14 | return emailRegex.test(email); 15 | }; 16 | 17 | export const validatePassword = (password: string): boolean => { 18 | // At least 8 characters, must contain at least 1 uppercase letter, 1 lowercase letter, and 1 number 19 | const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$/; 20 | return passwordRegex.test(password); 21 | }; 22 | 23 | export const validateUsername = (username: string): boolean => { 24 | // Username should be 3-20 characters and can only contain alphanumeric characters and underscores 25 | const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/; 26 | return usernameRegex.test(username); 27 | }; 28 | 29 | export const validateUrl = (url: string): boolean => { 30 | try { 31 | new URL(url); 32 | return true; 33 | } catch (error) { 34 | return false; 35 | } 36 | }; 37 | 38 | export const validatePhoneNumber = (phoneNumber: string): boolean => { 39 | // Basic phone number validation - can be enhanced for specific country formats 40 | const phoneRegex = /^\+?[\d\s()-]{7,20}$/; 41 | return phoneRegex.test(phoneNumber); 42 | }; 43 | 44 | export const isStrongPassword = (password: string): { 45 | isValid: boolean; 46 | feedback: string[]; 47 | } => { 48 | const feedback: string[] = []; 49 | let isValid = true; 50 | 51 | if (password.length < 8) { 52 | feedback.push('Password should be at least 8 characters long'); 53 | isValid = false; 54 | } 55 | 56 | if (!/[A-Z]/.test(password)) { 57 | feedback.push('Password should contain at least one uppercase letter'); 58 | isValid = false; 59 | } 60 | 61 | if (!/[a-z]/.test(password)) { 62 | feedback.push('Password should contain at least one lowercase letter'); 63 | isValid = false; 64 | } 65 | 66 | if (!/[0-9]/.test(password)) { 67 | feedback.push('Password should contain at least one number'); 68 | isValid = false; 69 | } 70 | 71 | if (!/[^A-Za-z0-9]/.test(password)) { 72 | feedback.push('Password should contain at least one special character'); 73 | isValid = false; 74 | } 75 | 76 | return { isValid, feedback }; 77 | }; -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: "class", 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./src/**/*.{js,ts,jsx,tsx,mdx}", 10 | ], 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | fontFamily: { 21 | outfit: ["var(--font-outfit)"], 22 | }, 23 | colors: { 24 | yuvai: { 25 | purple: "#7c3aed", // YUV.AI purple 26 | yellow: "#fbbf24", // YUV.AI yellow 27 | }, 28 | border: "hsl(var(--border))", 29 | input: "hsl(var(--input))", 30 | ring: "hsl(var(--ring))", 31 | background: "hsl(var(--background))", 32 | foreground: "hsl(var(--foreground))", 33 | primary: { 34 | DEFAULT: "hsl(var(--primary))", 35 | foreground: "hsl(var(--primary-foreground))", 36 | }, 37 | secondary: { 38 | DEFAULT: "hsl(var(--secondary))", 39 | foreground: "hsl(var(--secondary-foreground))", 40 | }, 41 | destructive: { 42 | DEFAULT: "hsl(var(--destructive))", 43 | foreground: "hsl(var(--destructive-foreground))", 44 | }, 45 | muted: { 46 | DEFAULT: "hsl(var(--muted))", 47 | foreground: "hsl(var(--muted-foreground))", 48 | }, 49 | accent: { 50 | DEFAULT: "hsl(var(--accent))", 51 | foreground: "hsl(var(--accent-foreground))", 52 | }, 53 | popover: { 54 | DEFAULT: "hsl(var(--popover))", 55 | foreground: "hsl(var(--popover-foreground))", 56 | }, 57 | card: { 58 | DEFAULT: "hsl(var(--card))", 59 | foreground: "hsl(var(--card-foreground))", 60 | }, 61 | }, 62 | borderRadius: { 63 | lg: "var(--radius)", 64 | md: "calc(var(--radius) - 2px)", 65 | sm: "calc(var(--radius) - 4px)", 66 | }, 67 | keyframes: { 68 | "accordion-down": { 69 | from: { height: "0" }, 70 | to: { height: "var(--radix-accordion-content-height)" }, 71 | }, 72 | "accordion-up": { 73 | from: { height: "var(--radix-accordion-content-height)" }, 74 | to: { height: "0" }, 75 | }, 76 | "phoenix-fly": { 77 | "0%": { transform: "translateY(0) rotate(0deg)" }, 78 | "25%": { transform: "translateY(-15px) rotate(2deg)" }, 79 | "50%": { transform: "translateY(0) rotate(0deg)" }, 80 | "75%": { transform: "translateY(-10px) rotate(-2deg)" }, 81 | "100%": { transform: "translateY(0) rotate(0deg)" }, 82 | }, 83 | }, 84 | animation: { 85 | "accordion-down": "accordion-down 0.2s ease-out", 86 | "accordion-up": "accordion-up 0.2s ease-out", 87 | "phoenix-fly": "phoenix-fly 6s ease-in-out infinite", 88 | }, 89 | }, 90 | }, 91 | plugins: [require("tailwindcss-animate")], 92 | }; 93 | 94 | export default config; -------------------------------------------------------------------------------- /src/components/layout/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from 'next/link'; 3 | import Image from 'next/image'; 4 | 5 | export function Footer() { 6 | return ( 7 |
8 |
9 |
10 |
11 |
12 | Yuval Avidani 19 |
20 |
21 |

22 | Built by{' '} 23 | 29 | Yuval Avidani 30 | 31 | {' '}- AI Builder & Speaker 32 |

33 |

34 | Fly High With YUV.AI 35 |

36 |
37 | 38 | 39 | 40 | 41 |
42 |
43 |
44 | 50 | YUV.AI Phoenix Logo 57 | 58 |
59 |
60 |
61 | ); 62 | } 63 | 64 | function SocialLink({ href, label }: { href: string; label: string }) { 65 | return ( 66 | 73 | 79 | 80 | 81 | 82 | 83 | 84 | ); 85 | } -------------------------------------------------------------------------------- /src/components/layout/AccessibilityControls.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | import { useAccessibility } from '@/context/AccessibilityContext'; 5 | import { Sun, Moon, ZoomIn, ZoomOut, RotateCcw, Eye } from 'lucide-react'; 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuLabel, 11 | DropdownMenuSeparator, 12 | DropdownMenuTrigger, 13 | } from '@/components/ui/dropdown-menu'; 14 | 15 | export function AccessibilityControls() { 16 | const { 17 | highContrast, 18 | toggleHighContrast, 19 | increaseFontSize, 20 | decreaseFontSize, 21 | resetFontSize, 22 | darkMode, 23 | toggleDarkMode, 24 | } = useAccessibility(); 25 | 26 | return ( 27 |
28 | 29 | 30 | 38 | 39 | 40 | Accessibility Options 41 | 42 | 43 | 44 |
45 | {darkMode ? 'Light Mode' : 'Dark Mode'} 46 | {darkMode ? : } 47 |
48 |
49 |
50 | Theme toggle also available in navbar 51 |
52 | 53 | 54 |
55 | {highContrast ? 'Standard Contrast' : 'High Contrast'} 56 | 57 |
58 |
59 | 60 | 61 | 62 | 63 |
64 | Increase Font Size 65 | 66 |
67 |
68 | 69 | 70 |
71 | Decrease Font Size 72 | 73 |
74 |
75 | 76 | 77 |
78 | Reset Font Size 79 | 80 |
81 |
82 |
83 |
84 |
85 | ); 86 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import { ClerkProvider } from "@clerk/nextjs"; 4 | import { Navbar } from "@/components/layout/Navbar"; 5 | import { AccessibilityProvider } from "@/context/AccessibilityContext"; 6 | import { AccessibilityControls } from "@/components/layout/AccessibilityControls"; 7 | import { Toaster } from "sonner"; 8 | 9 | export const metadata: Metadata = { 10 | title: "Next.js Boilerplate by YUV.AI", 11 | description: "A modern Next.js boilerplate with MongoDB, Clerk, Tailwind CSS, and shadcn/ui", 12 | authors: [{ name: "Yuval Avidani", url: "https://linktr.ee/yuvai" }], 13 | keywords: ["Next.js", "React", "Tailwind CSS", "MongoDB", "Clerk", "YUV.AI"], 14 | creator: "YUV.AI", 15 | }; 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: { 20 | children: React.ReactNode; 21 | }) { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | {children} 35 |
36 | 81 | 82 | 83 |
84 | 85 | 86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/app/api/webhook/clerk/route.ts: -------------------------------------------------------------------------------- 1 | import { connectToDatabase } from '@/lib/mongodb'; 2 | import User from '@/models/User'; 3 | import { WebhookEvent } from '@clerk/nextjs/server'; 4 | import { headers } from 'next/headers'; 5 | import { NextResponse } from 'next/server'; 6 | import { Webhook } from 'svix'; 7 | 8 | export async function POST(req: Request) { 9 | const WEBHOOK_SECRET = process.env.CLERK_WEBHOOK_SECRET; 10 | 11 | if (!WEBHOOK_SECRET) { 12 | throw new Error('Please add CLERK_WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local'); 13 | } 14 | 15 | // Get the headers 16 | const headersList = headers(); 17 | const svix_id = headersList.get('svix-id'); 18 | const svix_timestamp = headersList.get('svix-timestamp'); 19 | const svix_signature = headersList.get('svix-signature'); 20 | 21 | // If there are no headers, error out 22 | if (!svix_id || !svix_timestamp || !svix_signature) { 23 | return new NextResponse('Error: Missing svix headers', { status: 400 }); 24 | } 25 | 26 | // Get the body 27 | const payload = await req.json(); 28 | const body = JSON.stringify(payload); 29 | 30 | // Create a new Svix instance with your secret 31 | const wh = new Webhook(WEBHOOK_SECRET); 32 | 33 | let evt: WebhookEvent; 34 | 35 | // Verify the payload with the headers 36 | try { 37 | evt = wh.verify(body, { 38 | 'svix-id': svix_id, 39 | 'svix-timestamp': svix_timestamp, 40 | 'svix-signature': svix_signature, 41 | }) as WebhookEvent; 42 | } catch (err) { 43 | console.error('Error verifying webhook:', err); 44 | return new NextResponse('Error verifying webhook', { status: 400 }); 45 | } 46 | 47 | // Get the event type 48 | const eventType = evt.type; 49 | 50 | // Connect to MongoDB 51 | await connectToDatabase(); 52 | 53 | // Handle user creation 54 | if (eventType === 'user.created') { 55 | const { id, email_addresses, first_name, last_name, image_url } = evt.data; 56 | 57 | try { 58 | await User.create({ 59 | clerkId: id, 60 | email: email_addresses[0].email_address, 61 | name: `${first_name || ''} ${last_name || ''}`.trim(), 62 | profileImage: image_url, 63 | }); 64 | 65 | return NextResponse.json({ message: 'User created' }, { status: 201 }); 66 | } catch (error) { 67 | console.error('Error creating user:', error); 68 | return NextResponse.json({ error: 'Error creating user' }, { status: 500 }); 69 | } 70 | } 71 | 72 | // Handle user update 73 | if (eventType === 'user.updated') { 74 | const { id, email_addresses, first_name, last_name, image_url } = evt.data; 75 | 76 | try { 77 | await User.findOneAndUpdate( 78 | { clerkId: id }, 79 | { 80 | email: email_addresses[0].email_address, 81 | name: `${first_name || ''} ${last_name || ''}`.trim(), 82 | profileImage: image_url, 83 | } 84 | ); 85 | 86 | return NextResponse.json({ message: 'User updated' }, { status: 200 }); 87 | } catch (error) { 88 | console.error('Error updating user:', error); 89 | return NextResponse.json({ error: 'Error updating user' }, { status: 500 }); 90 | } 91 | } 92 | 93 | // Handle user deletion 94 | if (eventType === 'user.deleted') { 95 | const { id } = evt.data; 96 | 97 | try { 98 | await User.findOneAndDelete({ clerkId: id }); 99 | 100 | return NextResponse.json({ message: 'User deleted' }, { status: 200 }); 101 | } catch (error) { 102 | console.error('Error deleting user:', error); 103 | return NextResponse.json({ error: 'Error deleting user' }, { status: 500 }); 104 | } 105 | } 106 | 107 | return NextResponse.json({ message: 'Webhook received' }, { status: 200 }); 108 | } -------------------------------------------------------------------------------- /PLAN.md: -------------------------------------------------------------------------------- 1 | # Next.js Boilerplate Project Plan 2 | 3 | This document outlines the development plan and tracks progress for the Next.js Boilerplate project. 4 | 5 | ## Project Overview 6 | 7 | The goal is to create a comprehensive Next.js boilerplate with MongoDB, Clerk authentication, Tailwind CSS, shadcn/ui, and various other features for modern web application development. 8 | 9 | ## Tasks and Progress 10 | 11 | ### 1. Project Setup and Basic Configuration 12 | - [x] Initialize Next.js project with TypeScript - *Completed: May 26, 2023* 13 | - [x] Configure Tailwind CSS - *Completed: May 26, 2023* 14 | - [x] Set up shadcn/ui components - *Completed: May 26, 2023* 15 | - [x] Install necessary dependencies - *Completed: May 26, 2023* 16 | - [x] Configure project structure - *Completed: May 26, 2023* 17 | 18 | ### 2. Authentication Implementation 19 | - [x] Set up Clerk authentication - *Completed: May 26, 2023* 20 | - [x] Create sign-in and sign-up pages - *Completed: May 26, 2023* 21 | - [x] Implement authentication middleware - *Completed: May 26, 2023* 22 | - [x] Set up protected routes - *Completed: May 26, 2023* 23 | - [x] Create webhook handler for user sync - *Completed: May 26, 2023* 24 | 25 | ### 3. Database Integration 26 | - [x] Set up MongoDB connection - *Completed: May 26, 2023* 27 | - [x] Create user model - *Completed: May 26, 2023* 28 | - [x] Implement data synchronization between Clerk and MongoDB - *Completed: May 26, 2023* 29 | 30 | ### 4. UI Components and Layout 31 | - [x] Create responsive layout components - *Completed: May 26, 2023* 32 | - [x] Implement Navbar component - *Completed: May 26, 2023* 33 | - [x] Set up dark mode and theme support - *Completed: May 26, 2023* 34 | - [x] Implement accessibility features - *Completed: May 26, 2023* 35 | - [x] Create home page and dashboard UI - *Completed: May 26, 2023* 36 | 37 | ### 5. Security Implementation 38 | - [x] Set up secure HTTP headers - *Completed: May 26, 2023* 39 | - [x] Implement CSRF protection - *Completed: May 26, 2023* 40 | - [x] Set up input validation and sanitization - *Completed: May 26, 2023* 41 | - [x] Implement content security policy - *Completed: May 26, 2023* 42 | 43 | ### 6. SEO Optimization 44 | - [x] Set up metadata utilities - *Completed: May 26, 2023* 45 | - [x] Create sitemap and robots.txt - *Completed: May 26, 2023* 46 | - [x] Implement OpenGraph tags - *Completed: May 26, 2023* 47 | 48 | ### 7. Documentation and Final Touches 49 | - [x] Create detailed README.md - *Completed: May 26, 2023* 50 | - [x] Document project structure and features - *Completed: May 26, 2023* 51 | - [x] Set up environment variables template - *Completed: May 26, 2023* 52 | - [x] Create this PLAN.md file - *Completed: May 26, 2023* 53 | 54 | ## Future Enhancements 55 | 56 | 1. **API Rate Limiting** 57 | - Implement rate limiting for API routes 58 | - Add request throttling 59 | 60 | 2. **Advanced Database Features** 61 | - Add more MongoDB models and relationships 62 | - Implement data caching 63 | 64 | 3. **Testing Setup** 65 | - Set up Jest or Vitest for unit testing 66 | - Implement E2E testing with Cypress or Playwright 67 | 68 | 4. **CI/CD Integration** 69 | - Set up GitHub Actions for CI/CD 70 | - Configure automated testing and deployment 71 | 72 | 5. **Progressive Web App Features** 73 | - Add service worker 74 | - Implement offline capabilities 75 | - Add PWA manifest 76 | 77 | ## Tech Stack 78 | 79 | - **Frontend**: Next.js, React, TypeScript, Tailwind CSS, shadcn/ui 80 | - **Backend**: Next.js API routes 81 | - **Database**: MongoDB with Mongoose 82 | - **Authentication**: Clerk 83 | - **Deployment**: Vercel (recommended) 84 | 85 | ## Architecture Decisions 86 | 87 | 1. **App Router**: Using Next.js App Router for better routing, layouts, and server components 88 | 2. **Clerk Authentication**: Chosen for its comprehensive auth features and easy integration 89 | 3. **MongoDB**: Selected for flexibility and scalability 90 | 4. **Tailwind + shadcn/ui**: For rapid UI development with consistent design 91 | 5. **Mobile-First Approach**: Ensuring responsive design across all devices 92 | 93 | ## Contact 94 | 95 | For questions or suggestions, please contact: 96 | 97 | Yuval Avidani 98 | [https://linktr.ee/yuvai](https://linktr.ee/yuvai) 99 | 100 | --- 101 | 102 | *Last updated: May 26, 2023* | Fly High With YUV.AI -------------------------------------------------------------------------------- /src/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 { XIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Dialog({ 10 | ...props 11 | }: React.ComponentProps) { 12 | return 13 | } 14 | 15 | function DialogTrigger({ 16 | ...props 17 | }: React.ComponentProps) { 18 | return 19 | } 20 | 21 | function DialogPortal({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return 25 | } 26 | 27 | function DialogClose({ 28 | ...props 29 | }: React.ComponentProps) { 30 | return 31 | } 32 | 33 | function DialogOverlay({ 34 | className, 35 | ...props 36 | }: React.ComponentProps) { 37 | return ( 38 | 46 | ) 47 | } 48 | 49 | function DialogContent({ 50 | className, 51 | children, 52 | ...props 53 | }: React.ComponentProps) { 54 | return ( 55 | 56 | 57 | 65 | {children} 66 | 67 | 68 | Close 69 | 70 | 71 | 72 | ) 73 | } 74 | 75 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { 76 | return ( 77 |
82 | ) 83 | } 84 | 85 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { 86 | return ( 87 |
95 | ) 96 | } 97 | 98 | function DialogTitle({ 99 | className, 100 | ...props 101 | }: React.ComponentProps) { 102 | return ( 103 | 108 | ) 109 | } 110 | 111 | function DialogDescription({ 112 | className, 113 | ...props 114 | }: React.ComponentProps) { 115 | return ( 116 | 121 | ) 122 | } 123 | 124 | export { 125 | Dialog, 126 | DialogClose, 127 | DialogContent, 128 | DialogDescription, 129 | DialogFooter, 130 | DialogHeader, 131 | DialogOverlay, 132 | DialogPortal, 133 | DialogTitle, 134 | DialogTrigger, 135 | } 136 | -------------------------------------------------------------------------------- /src/components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { Slot } from "@radix-ui/react-slot" 6 | import { 7 | Controller, 8 | FormProvider, 9 | useFormContext, 10 | useFormState, 11 | type ControllerProps, 12 | type FieldPath, 13 | type FieldValues, 14 | } from "react-hook-form" 15 | 16 | import { cn } from "@/lib/utils" 17 | import { Label } from "@/components/ui/label" 18 | 19 | const Form = FormProvider 20 | 21 | type FormFieldContextValue< 22 | TFieldValues extends FieldValues = FieldValues, 23 | TName extends FieldPath = FieldPath, 24 | > = { 25 | name: TName 26 | } 27 | 28 | const FormFieldContext = React.createContext( 29 | {} as FormFieldContextValue 30 | ) 31 | 32 | const FormField = < 33 | TFieldValues extends FieldValues = FieldValues, 34 | TName extends FieldPath = FieldPath, 35 | >({ 36 | ...props 37 | }: ControllerProps) => { 38 | return ( 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | const useFormField = () => { 46 | const fieldContext = React.useContext(FormFieldContext) 47 | const itemContext = React.useContext(FormItemContext) 48 | const { getFieldState } = useFormContext() 49 | const formState = useFormState({ name: fieldContext.name }) 50 | const fieldState = getFieldState(fieldContext.name, formState) 51 | 52 | if (!fieldContext) { 53 | throw new Error("useFormField should be used within ") 54 | } 55 | 56 | const { id } = itemContext 57 | 58 | return { 59 | id, 60 | name: fieldContext.name, 61 | formItemId: `${id}-form-item`, 62 | formDescriptionId: `${id}-form-item-description`, 63 | formMessageId: `${id}-form-item-message`, 64 | ...fieldState, 65 | } 66 | } 67 | 68 | type FormItemContextValue = { 69 | id: string 70 | } 71 | 72 | const FormItemContext = React.createContext( 73 | {} as FormItemContextValue 74 | ) 75 | 76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
86 | 87 | ) 88 | } 89 | 90 | function FormLabel({ 91 | className, 92 | ...props 93 | }: React.ComponentProps) { 94 | const { error, formItemId } = useFormField() 95 | 96 | return ( 97 |