From 0c214fb66e8688d6233627977b54edd1cd968859 Mon Sep 17 00:00:00 2001 From: Lucas Dupias Date: Wed, 2 Jul 2025 10:38:34 +0200 Subject: [PATCH 1/7] imported nextjs base with auth server side actions --- .eslintignore | 1 + .eslintrc.json | 6 + .gitignore | 104 +--- README.md | 11 +- __tests__/login.test.tsx | 100 ++++ actions/auth.ts | 15 + actions/user.ts | 18 + app/(admin)/layout.tsx | 31 ++ app/(admin)/page.tsx | 10 + app/api/auth/[...nextauth]/route.ts | 1 + app/api/user/route.ts | 4 + app/auth.config.ts | 39 ++ app/auth.ts | 40 ++ app/db.ts | 21 + app/favicon.ico | Bin 0 -> 25931 bytes app/globals.css | 83 +++ app/layout.tsx | 32 ++ app/login/page.tsx | 9 + app/profile/page.tsx | 32 ++ app/register/page.tsx | 9 + app/submit-button.tsx | 42 ++ components.json | 21 + components/app-sidebar.tsx | 168 ++++++ components/custom-link.tsx | 43 ++ components/header.tsx | 22 + components/login-form.tsx | 118 ++++ components/nav-main.tsx | 73 +++ components/profile-form.tsx | 92 ++++ components/providers/session-provider.tsx | 14 + components/register-form.tsx | 124 +++++ components/search.tsx | 13 + components/team-switcher.tsx | 89 +++ components/ui/avatar.tsx | 50 ++ components/ui/button.tsx | 57 ++ components/ui/card.tsx | 76 +++ components/ui/collapsible.tsx | 9 + components/ui/dropdown-menu.tsx | 199 +++++++ components/ui/form.tsx | 178 ++++++ components/ui/input.tsx | 25 + components/ui/label.tsx | 24 + components/ui/navigation-menu.tsx | 128 +++++ components/ui/separator.tsx | 31 ++ components/ui/sheet.tsx | 138 +++++ components/ui/sidebar.tsx | 640 ++++++++++++++++++++++ components/ui/skeleton.tsx | 15 + components/ui/toast.tsx | 127 +++++ components/ui/toaster.tsx | 35 ++ components/ui/tooltip.tsx | 32 ++ components/user-nav.tsx | 73 +++ db/schema.ts | 94 ++++ drizzle.config.ts | 12 + drizzle/0000_new_christian_walker.sql | 67 +++ drizzle/0001_bright_spirit.sql | 1 + drizzle/0002_free_malcolm_colcord.sql | 62 +++ drizzle/meta/0000_snapshot.json | 346 ++++++++++++ drizzle/meta/0001_snapshot.json | 352 ++++++++++++ drizzle/meta/0002_snapshot.json | 352 ++++++++++++ drizzle/meta/_journal.json | 27 + drizzle/relations.ts | 3 + drizzle/schema.ts | 6 + hooks/use-mobile.tsx | 19 + hooks/use-toast.ts | 194 +++++++ jest.config.ts | 209 +++++++ jest.setup.ts | 1 + lib/utils.ts | 6 + middleware.ts | 9 + next-env.d.ts | 5 + package.json | 99 ++-- postcss.config.js | 6 + public/images/user.png | Bin 0 -> 19456 bytes tailwind.config.ts | 71 +++ tsconfig.json | 35 ++ 72 files changed, 5076 insertions(+), 122 deletions(-) create mode 100644 .eslintignore create mode 100644 .eslintrc.json create mode 100644 __tests__/login.test.tsx create mode 100644 actions/auth.ts create mode 100644 actions/user.ts create mode 100644 app/(admin)/layout.tsx create mode 100644 app/(admin)/page.tsx create mode 100644 app/api/auth/[...nextauth]/route.ts create mode 100644 app/api/user/route.ts create mode 100644 app/auth.config.ts create mode 100644 app/auth.ts create mode 100644 app/db.ts create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/login/page.tsx create mode 100644 app/profile/page.tsx create mode 100644 app/register/page.tsx create mode 100644 app/submit-button.tsx create mode 100644 components.json create mode 100644 components/app-sidebar.tsx create mode 100644 components/custom-link.tsx create mode 100644 components/header.tsx create mode 100644 components/login-form.tsx create mode 100644 components/nav-main.tsx create mode 100644 components/profile-form.tsx create mode 100644 components/providers/session-provider.tsx create mode 100644 components/register-form.tsx create mode 100644 components/search.tsx create mode 100644 components/team-switcher.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 components/user-nav.tsx create mode 100644 db/schema.ts create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_new_christian_walker.sql create mode 100644 drizzle/0001_bright_spirit.sql create mode 100644 drizzle/0002_free_malcolm_colcord.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/meta/0002_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 drizzle/relations.ts create mode 100644 drizzle/schema.ts create mode 100644 hooks/use-mobile.tsx create mode 100644 hooks/use-toast.ts create mode 100644 jest.config.ts create mode 100644 jest.setup.ts create mode 100644 lib/utils.ts create mode 100644 middleware.ts create mode 100644 next-env.d.ts create mode 100644 postcss.config.js create mode 100644 public/images/user.png create mode 100644 tailwind.config.ts create mode 100644 tsconfig.json diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..63ca953 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/components/ui/**.tsx diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..8d0f89c --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"], + "rules": { + "@typescript-eslint/no-explicit-any": ["off"] + } +} diff --git a/.gitignore b/.gitignore index 98a70e4..958dd5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,86 +1,40 @@ -# Dependencies -node_modules/ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug npm-debug.log* yarn-debug.log* yarn-error.log* -pnpm-debug.log* +.pnpm-debug.log* -# Environment variables +# local env files .env .env.local .env.development.local .env.test.local .env.production.local -# Next.js -.next/ -out/ -build/ -dist/ - -# Production -/build - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Coverage directory used by tools like istanbul -coverage/ -*.lcov - -# nyc test coverage -.nyc_output - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript -*.tsbuildinfo -next-env.d.ts - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Storybook build outputs -.out -.storybook-out -storybook-static - -# Temporary folders -tmp/ -temp/ - -# Logs -logs -*.log - -# Database -*.db -*.sqlite - -# macOS -.DS_Store - -# Windows -Thumbs.db -ehthumbs.db -Desktop.ini - -# VS Code -.vscode/ - -# IDEs -.idea/ -*.swp -*.swo -*~ +# vercel +.vercel +.env*.local -# Vercel -.vercel \ No newline at end of file +.codegpt +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 76f9d58..9f277dc 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,7 @@ codac empowers Code Academy Berlin students and alumni to learn, collaborate, an ### 📚 Learning Management System -- **Rich Content Editor** - for creating engaging educational content with advanced formatting, media embedding, and collaborative editing capabilities -- **Document Management** - Comprehensive document creation, editing, and sharing system with version control and real-time collaboration -- **Course Structure** - Organized learning paths with projects, lessons, and assignments +- **Course Structure** - Organized learning paths with projects, lessons, and quizzes - **Progress Tracking** - Detailed analytics on learning progress, completion rates, and time spent on various activities - **Assignment System** - Create, submit, and grade assignments with integrated feedback mechanisms - **Resource Library** - Centralized repository of learning materials, code examples, and references @@ -47,14 +45,15 @@ codac empowers Code Academy Berlin students and alumni to learn, collaborate, an - **[Next.js 15](https://nextjs.org/)** for performance and scalability. - **[Auth.js](https://auth.js.org/)** for authentication. - **[Drizzle ORM](https://orm.drizzle.team/)** for type-safe database management. -- **[SQLite](https://www.sqlite.org/)** for lightweight, serverless database management. +- **[PostgreSQL](https://www.postgresql.org/)** for reliable and scalable database solutions. - **[Shadcn/UI](https://ui.shadcn.com/)** for beautiful, customizable components. - **[Biome](https://biomejs.dev/)** for **Formatting and Linting** - **[Jest](https://jestjs.io/)** A testing framework for ensuring your app works as expected. - **React Hook Form**: A library for managing form state and validation in React applications - **Zod**: A TypeScript-first schema declaration and validation library -- **Zustand**: Simple state management - **Nuqs**: Type-safe URL state management +- **[Platejs] (https://platejs.org)Rich Content Editor** - for creating engaging educational content with advanced formatting, media embedding, and collaborative editing capabilities +- **Document Management** - Comprehensive document creation, editing, and sharing system with version control and real-time collaboration ## 🚀 Quick Start @@ -63,7 +62,7 @@ codac empowers Code Academy Berlin students and alumni to learn, collaborate, an Make sure you have the following installed on your system: - **Node.js** (v20 or higher) - [Download here](https://nodejs.org/) -- **pnpm** (recommended) or npm - Install with `npm install -g pnpm` +- **pnpm** - Install with `npm install -g pnpm` - **Git** - [Download here](https://git-scm.com/) ### Installation diff --git a/__tests__/login.test.tsx b/__tests__/login.test.tsx new file mode 100644 index 0000000..878fcf7 --- /dev/null +++ b/__tests__/login.test.tsx @@ -0,0 +1,100 @@ +import React from 'react'; // Ensures React is in scope when JSX is used +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import LoginForm from '../components/login-form'; +import { signIn } from 'next-auth/react'; +import { toast } from '@/hooks/use-toast'; + +// Mock the signIn function from next-auth +jest.mock('next-auth/react', () => ({ + signIn: jest.fn(), +})); + +// Mock the toast function +jest.mock('@/hooks/use-toast', () => ({ + toast: jest.fn(), +})); + +// Test case +test('renders login form and handles input', async () => { + render(); + + // Simulate user entering email and password + const emailInput = screen.getByLabelText(/email/i) as HTMLInputElement; + const passwordInput = screen.getByLabelText(/password/i) as HTMLInputElement; + + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + fireEvent.change(passwordInput, { target: { value: 'password123' } }); + + // Verify that the values are correctly entered + expect(emailInput.value).toBe('test@example.com'); + expect(passwordInput.value).toBe('password123'); + + // Simulate form submission by clicking the submit button + const buttons = screen.getAllByRole('button'); + const loginButton = buttons.find((button) => button.textContent === 'Login'); + + if (!loginButton) { + throw new Error('Login button not found'); + } + + fireEvent.click(loginButton); + + // Check if signIn function is called with the correct parameters + await waitFor(() => { + expect(signIn).toHaveBeenCalledWith('credentials', { + redirect: false, + email: 'test@example.com', + password: 'password123', + }); + }); +}); + +test('displays error message on failed login', async () => { + // Mock signIn to simulate a failure + (signIn as jest.Mock).mockResolvedValueOnce({ error: 'Invalid credentials' }); + + render(); + + // Simulate user entering email and password + const emailInput = screen.getByLabelText(/email/i) as HTMLInputElement; + const passwordInput = screen.getByLabelText(/password/i) as HTMLInputElement; + + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + fireEvent.change(passwordInput, { target: { value: 'password123' } }); + + // Simulate form submission by clicking the submit button + const buttons = screen.getAllByRole('button'); + const loginButton = buttons.find((button) => button.textContent === 'Login'); + + if (!loginButton) { + throw new Error('Login button not found'); + } + + fireEvent.click(loginButton); + + // Wait for the toast to be called + await waitFor(() => { + expect(toast).toHaveBeenCalledWith({ + variant: 'destructive', + title: 'Uh oh! Something went wrong.', + // description: "Invalid credentials", + }); + }); +}); + +test('triggers Google login on button click', async () => { + render(); + + // Find the "Login with Google" button + const googleLoginButton = screen.getByRole('button', { name: /login with google/i }); + + // Simulate a click on the Google login button + fireEvent.click(googleLoginButton); + + // Verify that the signIn function is called with "google" as the provider + await waitFor(() => { + expect(signIn).toHaveBeenCalledWith('google', { + redirectTo: '/', + }); + }); +}); diff --git a/actions/auth.ts b/actions/auth.ts new file mode 100644 index 0000000..587cc5b --- /dev/null +++ b/actions/auth.ts @@ -0,0 +1,15 @@ +'use server'; + +import { getUser, createUser } from '@/app/db'; +import { redirect } from 'next/navigation'; + +export async function register(data: any) { + let user = await getUser(data.email); + + if (user.length > 0) { + return { message: 'A user with this identifier already exists' } + } else { + await createUser(data.email, data.password); + redirect('/login'); + } +} diff --git a/actions/user.ts b/actions/user.ts new file mode 100644 index 0000000..a1a169b --- /dev/null +++ b/actions/user.ts @@ -0,0 +1,18 @@ +'use server'; + +import { db } from '@/app/db'; +import { users } from '@/db/schema'; +import { eq } from 'drizzle-orm'; +import { revalidatePath } from 'next/cache'; + +export async function updateUserProfile(userId: string, data: { + name?: string; + email?: string; + image?: string; +}) { + await db.update(users) + .set(data) + .where(eq(users.id, userId)); + + revalidatePath('/profile'); +} \ No newline at end of file diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx new file mode 100644 index 0000000..40e3df4 --- /dev/null +++ b/app/(admin)/layout.tsx @@ -0,0 +1,31 @@ +import { SidebarProvider } from '@/components/ui/sidebar'; +import '@/app/globals.css'; + +import { AppSidebar } from '@/components/app-sidebar'; +import Header from '@/components/header'; + +const title = 'Admin Page'; +const description = ''; + +export const metadata = { + title, + description, + twitter: { + card: 'summary_large_image', + title, + description, + }, + metadataBase: new URL('https://nextjs-postgres-auth.vercel.app'), +}; + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + return ( + + +
+
+ {children} +
+
+ ); +} diff --git a/app/(admin)/page.tsx b/app/(admin)/page.tsx new file mode 100644 index 0000000..80890a1 --- /dev/null +++ b/app/(admin)/page.tsx @@ -0,0 +1,10 @@ + +export default function Page() { + return ( +
+
+ +
+
+ ); +} diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..311d931 --- /dev/null +++ b/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1 @@ +export { GET, POST } from 'app/auth'; diff --git a/app/api/user/route.ts b/app/api/user/route.ts new file mode 100644 index 0000000..97b53b7 --- /dev/null +++ b/app/api/user/route.ts @@ -0,0 +1,4 @@ +export async function POST(request: Request) { + const res = await request.json() + return Response.json({ res }) +} \ No newline at end of file diff --git a/app/auth.config.ts b/app/auth.config.ts new file mode 100644 index 0000000..9c75f0d --- /dev/null +++ b/app/auth.config.ts @@ -0,0 +1,39 @@ +import { NextAuthConfig } from 'next-auth'; +import { DrizzleAdapter } from "@auth/drizzle-adapter" +import { db } from './db'; + +export const authConfig = { + pages: { + signIn: '/login', + }, + adapter: DrizzleAdapter(db), + providers: [ + // added later in auth.ts since it requires bcrypt which is only compatible with Node.js + // while this file is also used in non-Node.js environments + ], + session: { strategy: "jwt" }, + callbacks: { + authorized({ auth, request: { nextUrl } }) { + const isLoggedIn = !!auth?.user; + const isLoginPage = nextUrl.pathname.startsWith('/login'); + const isRegisterPage = nextUrl.pathname.startsWith('/register'); + + if (!isLoggedIn && !(isLoginPage || isRegisterPage)) { + return false; + } + return true; + }, + async session({ session, token }) { + session.user.image = token.picture + session.user.id = token.sub ?? '' + return session + }, + }, +} satisfies NextAuthConfig; + + +declare module "next-auth" { + interface Session { + accessToken?: string + } +} \ No newline at end of file diff --git a/app/auth.ts b/app/auth.ts new file mode 100644 index 0000000..cac7a6f --- /dev/null +++ b/app/auth.ts @@ -0,0 +1,40 @@ +import NextAuth, { CredentialsSignin, User } from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; + +import { compare } from 'bcrypt-ts'; +import Google from "next-auth/providers/google" + +import { getUser } from 'app/db'; +import { authConfig } from 'app/auth.config'; + +class InvalidLoginError extends CredentialsSignin { + code = 'Invalid identifier or password' +} + +export const { + handlers: { GET, POST }, + auth, + signIn, + signOut, +} = NextAuth({ + ...authConfig, + providers: [ + Google({ + allowDangerousEmailAccountLinking: true, + }), + Credentials({ + credentials: { + email: {}, + password: {}, + }, + + async authorize({ email, password }: any) { + const user = await getUser(email); + if (user.length === 0 || !user[0].password) throw new InvalidLoginError(); + const passwordsMatch = await compare(password, user[0].password!); + if (passwordsMatch) return user[0] as User; + throw new InvalidLoginError(); + }, + }), + ], +}); diff --git a/app/db.ts b/app/db.ts new file mode 100644 index 0000000..691a79e --- /dev/null +++ b/app/db.ts @@ -0,0 +1,21 @@ +import { drizzle } from "drizzle-orm/vercel-postgres"; +import { sql } from "@vercel/postgres"; +import { eq } from 'drizzle-orm'; + +import { users } from "../db/schema"; +import * as schema from "../db/schema"; +import { genSaltSync, hashSync } from "bcrypt-ts"; + + +export const db = drizzle(sql, { schema }); + +export async function getUser(email: string) { + return await db.select().from(users).where(eq(users.email, email)); +} + +export async function createUser(email: string, password: string) { + const salt = genSaltSync(10); + const hash = hashSync(password, salt); + + return await db.insert(users).values({ email: email, password: hash }); +} \ No newline at end of file diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 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 LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..56b2df2 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,83 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +@layer base { + :root { + --background: 0 0% 100%; + --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%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --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 10% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8% + } + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --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%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%} +} +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..f619087 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,32 @@ +import './globals.css'; + +import { GeistSans } from 'geist/font/sans'; + +import { Toaster } from '@/components/ui/toaster'; +import { SessionProvider } from 'next-auth/react'; + +const title = 'Next.js + Postgres Auth Starter'; +const description = + 'This is a Next.js starter kit that uses NextAuth.js for simple email + password login and a Postgres database to persist the data.'; + +export const metadata = { + title, + description, + twitter: { + card: 'summary_large_image', + title, + description, + }, + metadataBase: new URL('https://nextjs-postgres-auth.vercel.app'), +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + + ); +} diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..183ab06 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,9 @@ +import LoginForm from "@/components/login-form"; + +export default function Login() { + return ( +
+ +
+ ); +} diff --git a/app/profile/page.tsx b/app/profile/page.tsx new file mode 100644 index 0000000..0af19a7 --- /dev/null +++ b/app/profile/page.tsx @@ -0,0 +1,32 @@ +import { auth } from '@/app/auth'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; +import ProfileForm from '@/components/profile-form'; + +export default async function ProfilePage() { + const session = await auth(); + + if (!session?.user) { + return null; + } + + return ( +
+ + + My Profile + + +
+ + + {session.user.name?.[0] ?? session.user.email?.[0]} + +
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/register/page.tsx b/app/register/page.tsx new file mode 100644 index 0000000..09b98e2 --- /dev/null +++ b/app/register/page.tsx @@ -0,0 +1,9 @@ +import RegisterForm from '@/components/register-form'; + +export default function register() { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/app/submit-button.tsx b/app/submit-button.tsx new file mode 100644 index 0000000..15477f5 --- /dev/null +++ b/app/submit-button.tsx @@ -0,0 +1,42 @@ +'use client'; + +import { useFormStatus } from 'react-dom'; + +export function SubmitButton({ children }: { children: React.ReactNode }) { + const { pending } = useFormStatus(); + + return ( + + ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..971e2c2 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx new file mode 100644 index 0000000..39865a7 --- /dev/null +++ b/components/app-sidebar.tsx @@ -0,0 +1,168 @@ +"use client" + +import * as React from "react" +import { + AudioWaveform, + BookOpen, + Bot, + Command, + Frame, + GalleryVerticalEnd, + Map, + PieChart, + Settings2, + SquareTerminal, +} from "lucide-react" + +import { NavMain } from "./nav-main" +import { TeamSwitcher } from "./team-switcher" +import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from "./ui/sidebar" + + +// This is sample data. +const data = { + user: { + name: "shadcn", + email: "m@example.com", + avatar: "/avatars/shadcn.jpg", + }, + teams: [ + { + name: "Acme Inc", + logo: GalleryVerticalEnd, + plan: "Enterprise", + }, + { + name: "Acme Corp.", + logo: AudioWaveform, + plan: "Startup", + }, + { + name: "Evil Corp.", + logo: Command, + plan: "Free", + }, + ], + navMain: [ + { + title: "Playground", + url: "#", + icon: SquareTerminal, + isActive: true, + items: [ + { + title: "History", + url: "#", + }, + { + title: "Starred", + url: "#", + }, + { + title: "Settings", + url: "#", + }, + ], + }, + { + title: "Models", + url: "#", + icon: Bot, + items: [ + { + title: "Genesis", + url: "#", + }, + { + title: "Explorer", + url: "#", + }, + { + title: "Quantum", + url: "#", + }, + ], + }, + { + title: "Documentation", + url: "#", + icon: BookOpen, + items: [ + { + title: "Introduction", + url: "#", + }, + { + title: "Get Started", + url: "#", + }, + { + title: "Tutorials", + url: "#", + }, + { + title: "Changelog", + url: "#", + }, + ], + }, + { + title: "Settings", + url: "#", + icon: Settings2, + items: [ + { + title: "General", + url: "#", + }, + { + title: "Team", + url: "#", + }, + { + title: "Billing", + url: "#", + }, + { + title: "Limits", + url: "#", + }, + ], + }, + ], + projects: [ + { + name: "Design Engineering", + url: "#", + icon: Frame, + }, + { + name: "Sales & Marketing", + url: "#", + icon: PieChart, + }, + { + name: "Travel", + url: "#", + icon: Map, + }, + ], +} + +export function AppSidebar({ ...props }: React.ComponentProps) { + return ( + + + + + + + {/* */} + + + {/* */} + + + + ) +} \ No newline at end of file diff --git a/components/custom-link.tsx b/components/custom-link.tsx new file mode 100644 index 0000000..9e1f1cf --- /dev/null +++ b/components/custom-link.tsx @@ -0,0 +1,43 @@ +import { cn } from "@/lib/utils" +import { ExternalLink } from "lucide-react" +import Link from "next/link" + +interface CustomLinkProps extends React.LinkHTMLAttributes { + href: string +} + +const CustomLink = ({ + href, + children, + className, + ...rest +}: CustomLinkProps) => { + const isInternalLink = href.startsWith("/") + const isAnchorLink = href.startsWith("#") + + if (isInternalLink || isAnchorLink) { + return ( + + {children} + + ) + } + + return ( + + {children} + + + ) +} + +export default CustomLink \ No newline at end of file diff --git a/components/header.tsx b/components/header.tsx new file mode 100644 index 0000000..2cfd1d2 --- /dev/null +++ b/components/header.tsx @@ -0,0 +1,22 @@ + +// import { auth } from '@/app/auth'; +import { Search } from './search'; +import { SidebarTrigger } from './ui/sidebar'; +import { UserNav } from './user-nav'; +import { SessionProvider } from 'next-auth/react'; + +export default function Header() { + return ( +
+
+ +
+ + + + +
+
+
+ ); +} diff --git a/components/login-form.tsx b/components/login-form.tsx new file mode 100644 index 0000000..6543123 --- /dev/null +++ b/components/login-form.tsx @@ -0,0 +1,118 @@ +'use client'; + +import Link from 'next/link'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { signIn } from 'next-auth/react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { toast } from '@/hooks/use-toast'; +import { Button } from '@/components/ui/button'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; + +const FormSchema = z.object({ + email: z.string().email(), + password: z.string().min(6), +}); + +export default function LoginForm() { + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + email: '', + password: '', + }, + }); + + async function onSubmit(data: z.infer) { + const res = await signIn('credentials', { + redirect: false, + email: data.email, + password: data.password, + }); + if (res?.error) { + toast({ + variant: 'destructive', + title: 'Uh oh! Something went wrong.', + description: (res as any).code, + }); + form.setError('password', { type: 'manual', message: (res as any).code }); + } else { + window.location.href = '/'; + } + } + + return ( + + + Login + Enter your email below to login to your account + + +
+ +
+ ( + + Email + + + + + + )} + /> + ( + +
+ Password + {/* + Forgot your password? + */} +
+ + + + + +
+ )} + /> + + + +
+
+ + +
+ Don't have an account?{' '} + + Sign up + +
+
+
+ ); +} diff --git a/components/nav-main.tsx b/components/nav-main.tsx new file mode 100644 index 0000000..df4d8d6 --- /dev/null +++ b/components/nav-main.tsx @@ -0,0 +1,73 @@ +"use client" + +import { ChevronRight, type LucideIcon } from "lucide-react" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "./ui/collapsible" +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "./ui/sidebar" + +export function NavMain({ + items, +}: { + items: { + title: string + url: string + icon?: LucideIcon + isActive?: boolean + items?: { + title: string + url: string + }[] + }[] +}) { + return ( + + Platform + + {items.map((item) => ( + + + + + {item.icon && } + {item.title} + + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + + ))} + + + ) +} \ No newline at end of file diff --git a/components/profile-form.tsx b/components/profile-form.tsx new file mode 100644 index 0000000..6b337db --- /dev/null +++ b/components/profile-form.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import * as z from 'zod'; +import { User } from 'next-auth'; +import { useState } from 'react'; +import { useToast } from '@/hooks/use-toast'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { updateUserProfile } from '@/actions/user'; + +const profileFormSchema = z.object({ + name: z.string().min(2, 'Name must be at least 2 characters'), + email: z.string().email('Invalid email address'), + image: z.string().optional(), +}); + +type ProfileFormValues = z.infer; + +export default function ProfileForm({ user }: { user: User }) { + const { toast } = useToast(); + const [isLoading, setIsLoading] = useState(false); + + const form = useForm({ + resolver: zodResolver(profileFormSchema), + defaultValues: { + name: user.name || '', + email: user.email || '', + }, + }); + + async function onSubmit(data: ProfileFormValues) { + setIsLoading(true); + try { + if (!user.id) throw new Error('User ID is required'); + await updateUserProfile(user.id, data); + + toast({ + title: 'Success', + description: 'Profile updated successfully', + }); + } catch (error) { + console.log(error); + toast({ + title: 'Error', + description: 'Failed to update profile', + variant: 'destructive', + }); + } + setIsLoading(false); + } + + return ( +
+ + ( + + Name + + + + + + )} + /> + + ( + + Email + + + + + + )} + /> + + + + + ); +} diff --git a/components/providers/session-provider.tsx b/components/providers/session-provider.tsx new file mode 100644 index 0000000..e1eb6e0 --- /dev/null +++ b/components/providers/session-provider.tsx @@ -0,0 +1,14 @@ +'use client' + +import { SessionProvider } from 'next-auth/react' +import { Session } from 'next-auth' + +export default function AuthProvider({ + children, + session +}: { + children: React.ReactNode + session: Session | null +}) { + return {children} +} \ No newline at end of file diff --git a/components/register-form.tsx b/components/register-form.tsx new file mode 100644 index 0000000..c042296 --- /dev/null +++ b/components/register-form.tsx @@ -0,0 +1,124 @@ +'use client'; + +import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'; +// import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import Link from 'next/link'; + +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { FormField, FormItem, FormLabel, FormControl, FormMessage, Form } from './ui/form'; +import { register } from '@/actions/auth'; +import { useRouter } from 'next/navigation'; + +const formSchema = z.object({ + name: z.string().min(2, 'Name must be at least 2 characters'), + email: z.string().email('Invalid email address'), + password: z.string().min(6, 'Password must be at least 6 characters'), + confirmPassword: z.string().min(6, 'Password must be at least 6 characters'), +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords must match", + path: ['confirmPassword'], +}); + +export default function RegisterForm() { + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + email: '', + password: '', + confirmPassword: '', + }, + }); + + async function onSubmit(values: z.infer) { + const res = await register(values); + if (res.message !== 'success') { + form.setError('email', { type: 'manual', message: res.message }); + } else { + router.push('/login'); + } + } + + return ( + + + Create Account + Create your account to get started + + +
+ + ( + + Name + + + + + + )} + /> + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + ( + + Confirm Password + + + + + + )} + /> + + + +
+ +

+ Already have an account?{' '} + + Sign in + +

+
+
+ ); +} diff --git a/components/search.tsx b/components/search.tsx new file mode 100644 index 0000000..598dc30 --- /dev/null +++ b/components/search.tsx @@ -0,0 +1,13 @@ +import { Input } from "./ui/input" + +export function Search() { + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/components/team-switcher.tsx b/components/team-switcher.tsx new file mode 100644 index 0000000..3941d59 --- /dev/null +++ b/components/team-switcher.tsx @@ -0,0 +1,89 @@ +"use client" + +import * as React from "react" +import { ChevronsUpDown, Plus } from "lucide-react" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "./ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "./ui/sidebar" + +export function TeamSwitcher({ + teams, +}: { + teams: { + name: string + logo: React.ElementType + plan: string + }[] +}) { + const { isMobile } = useSidebar() + const [activeTeam, setActiveTeam] = React.useState(teams[0]) + + return ( + + + + + +
+ +
+
+ + {activeTeam.name} + + {activeTeam.plan} +
+ +
+
+ + + Teams + + {teams.map((team, index) => ( + setActiveTeam(team)} + className="gap-2 p-2" + > +
+ +
+ {team.name} + ⌘{index + 1} +
+ ))} + + +
+ +
+
Add team
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/components/ui/avatar.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..65d4fcd --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,57 @@ +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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..cabfbfc --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,76 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx new file mode 100644 index 0000000..a23e7a2 --- /dev/null +++ b/components/ui/collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..9ff6568 --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,199 @@ +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, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + svg]:size-4 [&>svg]:shrink-0", + inset && "pl-8", + className + )} + {...props} + /> +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/components/ui/form.tsx b/components/ui/form.tsx new file mode 100644 index 0000000..b6daa65 --- /dev/null +++ b/components/ui/form.tsx @@ -0,0 +1,178 @@ +"use client" + +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 = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +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 ") + } + + 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( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +
- ); -} \ No newline at end of file + ) +} diff --git a/app/register/page.tsx b/app/register/page.tsx index 09b98e2..9015733 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -1,9 +1,9 @@ -import RegisterForm from '@/components/register-form'; +import RegisterForm from '@/components/register-form' export default function register() { return (
- ); -} \ No newline at end of file + ) +} diff --git a/app/submit-button.tsx b/app/submit-button.tsx index 15477f5..b668eec 100644 --- a/app/submit-button.tsx +++ b/app/submit-button.tsx @@ -1,9 +1,9 @@ -'use client'; +'use client' -import { useFormStatus } from 'react-dom'; +import { useFormStatus } from 'react-dom' export function SubmitButton({ children }: { children: React.ReactNode }) { - const { pending } = useFormStatus(); + const { pending } = useFormStatus() return ( - ); + ) } diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..c0245fb --- /dev/null +++ b/biome.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": ["node_modules/**", ".next/**", "dist/**", "build/**", "coverage/**", "drizzle/**"] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "useImportType": "error" + }, + "correctness": { + "useExhaustiveDependencies": "warn" + }, + "suspicious": { + "noExplicitAny": "warn" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingCommas": "es5", + "semicolons": "asNeeded" + } + }, + "json": { + "formatter": { + "enabled": true + } + } +} diff --git a/components.json b/components.json index 971e2c2..8c3acf5 100644 --- a/components.json +++ b/components.json @@ -18,4 +18,4 @@ "hooks": "@/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/components/app-sidebar.tsx b/components/app-sidebar.tsx index 39865a7..2173c63 100644 --- a/components/app-sidebar.tsx +++ b/components/app-sidebar.tsx @@ -1,150 +1,178 @@ -"use client" +'use client' -import * as React from "react" -import { - AudioWaveform, - BookOpen, - Bot, - Command, - Frame, - GalleryVerticalEnd, - Map, - PieChart, - Settings2, - SquareTerminal, -} from "lucide-react" +import { BookOpen, GraduationCap, Home, Settings, TrendingUp, UserCheck, Users } from 'lucide-react' +import type * as React from 'react' -import { NavMain } from "./nav-main" -import { TeamSwitcher } from "./team-switcher" -import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from "./ui/sidebar" +import { NavMain } from './nav-main' +import { TeamSwitcher } from './team-switcher' +import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from './ui/sidebar' - -// This is sample data. +// Codac-specific navigation data const data = { user: { - name: "shadcn", - email: "m@example.com", - avatar: "/avatars/shadcn.jpg", + name: 'Student', + email: 'student@codeacademy.berlin', + avatar: '/images/user.png', }, teams: [ { - name: "Acme Inc", - logo: GalleryVerticalEnd, - plan: "Enterprise", + name: 'Code Academy Berlin', + logo: GraduationCap, + plan: 'Premium', }, + ], + navMain: [ { - name: "Acme Corp.", - logo: AudioWaveform, - plan: "Startup", + title: 'Dashboard', + url: '/dashboard', + icon: Home, + isActive: true, + items: [ + { + title: 'Overview', + url: '/dashboard', + }, + { + title: 'Progress', + url: '/dashboard/progress', + }, + { + title: 'Achievements', + url: '/dashboard/achievements', + }, + ], }, { - name: "Evil Corp.", - logo: Command, - plan: "Free", + title: 'Learning', + url: '/learning', + icon: BookOpen, + items: [ + { + title: 'Courses', + url: '/learning/courses', + }, + { + title: 'Assignments', + url: '/learning/assignments', + }, + { + title: 'Resources', + url: '/learning/resources', + }, + { + title: 'Schedule', + url: '/learning/schedule', + }, + ], }, - ], - navMain: [ { - title: "Playground", - url: "#", - icon: SquareTerminal, - isActive: true, + title: 'Community', + url: '/community', + icon: Users, items: [ { - title: "History", - url: "#", + title: 'Posts', + url: '/community/posts', + }, + { + title: 'Discussions', + url: '/community/discussions', }, { - title: "Starred", - url: "#", + title: 'Student Directory', + url: '/community/directory', }, { - title: "Settings", - url: "#", + title: 'Alumni Network', + url: '/community/alumni', }, ], }, { - title: "Models", - url: "#", - icon: Bot, + title: 'Mentorship', + url: '/mentorship', + icon: UserCheck, items: [ { - title: "Genesis", - url: "#", + title: 'Find a Mentor', + url: '/mentorship/find', + }, + { + title: 'My Mentors', + url: '/mentorship/mentors', }, { - title: "Explorer", - url: "#", + title: 'Mentoring', + url: '/mentorship/mentoring', }, { - title: "Quantum", - url: "#", + title: 'Sessions', + url: '/mentorship/sessions', }, ], }, { - title: "Documentation", - url: "#", - icon: BookOpen, + title: 'Career', + url: '/career', + icon: TrendingUp, items: [ { - title: "Introduction", - url: "#", + title: 'Job Board', + url: '/career/jobs', }, { - title: "Get Started", - url: "#", + title: 'Portfolio', + url: '/career/portfolio', }, { - title: "Tutorials", - url: "#", + title: 'Interview Prep', + url: '/career/interview-prep', }, { - title: "Changelog", - url: "#", + title: 'Resources', + url: '/career/resources', }, ], }, { - title: "Settings", - url: "#", - icon: Settings2, + title: 'Settings', + url: '/settings', + icon: Settings, items: [ { - title: "General", - url: "#", + title: 'Profile', + url: '/settings/profile', }, { - title: "Team", - url: "#", + title: 'Notifications', + url: '/settings/notifications', }, { - title: "Billing", - url: "#", + title: 'Privacy', + url: '/settings/privacy', }, { - title: "Limits", - url: "#", + title: 'Account', + url: '/settings/account', }, ], }, ], projects: [ { - name: "Design Engineering", - url: "#", - icon: Frame, + name: 'Full Stack Development', + url: '/projects/full-stack', + icon: BookOpen, }, { - name: "Sales & Marketing", - url: "#", - icon: PieChart, + name: 'Data Science', + url: '/projects/data-science', + icon: TrendingUp, }, { - name: "Travel", - url: "#", - icon: Map, + name: 'Web Development', + url: '/projects/web-dev', + icon: GraduationCap, }, ], } @@ -159,10 +187,8 @@ export function AppSidebar({ ...props }: React.ComponentProps) { {/* */} - - {/* */} - + {/* */} ) -} \ No newline at end of file +} diff --git a/components/custom-link.tsx b/components/custom-link.tsx index 9e1f1cf..524de6a 100644 --- a/components/custom-link.tsx +++ b/components/custom-link.tsx @@ -1,19 +1,14 @@ -import { cn } from "@/lib/utils" -import { ExternalLink } from "lucide-react" -import Link from "next/link" +import { cn } from '@/lib/utils' +import { ExternalLink } from 'lucide-react' +import Link from 'next/link' interface CustomLinkProps extends React.LinkHTMLAttributes { href: string } -const CustomLink = ({ - href, - children, - className, - ...rest -}: CustomLinkProps) => { - const isInternalLink = href.startsWith("/") - const isAnchorLink = href.startsWith("#") +const CustomLink = ({ href, children, className, ...rest }: CustomLinkProps) => { + const isInternalLink = href.startsWith('/') + const isAnchorLink = href.startsWith('#') if (isInternalLink || isAnchorLink) { return ( @@ -29,7 +24,7 @@ const CustomLink = ({ target="_blank" rel="noopener noreferrer" className={cn( - "inline-flex align-baseline gap-1 items-center underline underline-offset-4", + 'inline-flex align-baseline gap-1 items-center underline underline-offset-4', className )} {...rest} @@ -40,4 +35,4 @@ const CustomLink = ({ ) } -export default CustomLink \ No newline at end of file +export default CustomLink diff --git a/components/editor/plate-editor.tsx b/components/editor/plate-editor.tsx new file mode 100644 index 0000000..c059da5 --- /dev/null +++ b/components/editor/plate-editor.tsx @@ -0,0 +1,146 @@ +'use client' + +import { + BlockquotePlugin, + BoldPlugin, + H1Plugin, + H2Plugin, + H3Plugin, + ItalicPlugin, + UnderlinePlugin, +} from '@platejs/basic-nodes/react' +import type { Value } from 'platejs' +import { Plate, PlateContent, usePlateEditor } from 'platejs/react' +import React from 'react' + +// Initial value for the editor +const initialValue: Value = [ + { + type: 'p', + children: [{ text: 'Start typing your content here...' }], + }, +] + +interface PlateEditorProps { + value?: Value + onChange?: (value: Value) => void + placeholder?: string + readOnly?: boolean + className?: string +} + +export function PlateEditor({ + value = initialValue, + onChange, + placeholder = 'Start typing...', + readOnly = false, + className = '', +}: PlateEditorProps) { + const editor = usePlateEditor({ + plugins: [ + BoldPlugin, + ItalicPlugin, + UnderlinePlugin, + H1Plugin, + H2Plugin, + H3Plugin, + BlockquotePlugin, + ], + value, + }) + + return ( +
+ onChange(value) : undefined} + readOnly={readOnly} + > + + +
+ ) +} + +// Example usage for different codac contexts +export function CourseContentEditor({ + value, + onChange, +}: { + value?: Value + onChange?: (value: Value) => void +}) { + return ( + + ) +} + +export function AssignmentEditor({ + value, + onChange, +}: { + value?: Value + onChange?: (value: Value) => void +}) { + return ( + + ) +} + +export function PostEditor({ + value, + onChange, +}: { + value?: Value + onChange?: (value: Value) => void +}) { + return ( + + ) +} + +export function CommentEditor({ + value, + onChange, +}: { + value?: Value + onChange?: (value: Value) => void +}) { + const simpleInitialValue: Value = [ + { + type: 'p', + children: [{ text: '' }], + }, + ] + + const editor = usePlateEditor({ + plugins: [BoldPlugin, ItalicPlugin], // Simplified for comments + value: value || simpleInitialValue, + }) + + return ( +
+ onChange(value) : undefined}> + + +
+ ) +} diff --git a/components/header.tsx b/components/header.tsx index 2cfd1d2..cfed9db 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -1,22 +1,21 @@ - +import { SessionProvider } from 'next-auth/react' // import { auth } from '@/app/auth'; -import { Search } from './search'; -import { SidebarTrigger } from './ui/sidebar'; -import { UserNav } from './user-nav'; -import { SessionProvider } from 'next-auth/react'; +import { Search } from './search' +import { SidebarTrigger } from './ui/sidebar' +import { UserNav } from './user-nav' export default function Header() { - return ( + return (
- - - + + +
- ); + ) } diff --git a/components/login-form.tsx b/components/login-form.tsx index 6543123..ea7a678 100644 --- a/components/login-form.tsx +++ b/components/login-form.tsx @@ -1,21 +1,33 @@ -'use client'; +'use client' -import Link from 'next/link'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { signIn } from 'next-auth/react'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { zodResolver } from '@hookform/resolvers/zod' +import { signIn } from 'next-auth/react' +import Link from 'next/link' +import { useForm } from 'react-hook-form' +import { z } from 'zod' -import { toast } from '@/hooks/use-toast'; -import { Button } from '@/components/ui/button'; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { toast } from '@/hooks/use-toast' const FormSchema = z.object({ email: z.string().email(), password: z.string().min(6), -}); +}) + +interface AuthError { + code: string + message?: string +} export default function LoginForm() { const form = useForm>({ @@ -24,23 +36,23 @@ export default function LoginForm() { email: '', password: '', }, - }); + }) async function onSubmit(data: z.infer) { const res = await signIn('credentials', { redirect: false, email: data.email, password: data.password, - }); + }) if (res?.error) { toast({ variant: 'destructive', title: 'Uh oh! Something went wrong.', - description: (res as any).code, - }); - form.setError('password', { type: 'manual', message: (res as any).code }); + description: (res as AuthError).code, + }) + form.setError('password', { type: 'manual', message: (res as AuthError).code }) } else { - window.location.href = '/'; + window.location.href = '/' } } @@ -97,7 +109,7 @@ export default function LoginForm() { onClick={async () => { await signIn('google', { redirectTo: '/', - }); + }) }} > Login with Google @@ -114,5 +126,5 @@ export default function LoginForm() {
- ); + ) } diff --git a/components/nav-main.tsx b/components/nav-main.tsx index df4d8d6..cf6453a 100644 --- a/components/nav-main.tsx +++ b/components/nav-main.tsx @@ -1,12 +1,8 @@ -"use client" +'use client' -import { ChevronRight, type LucideIcon } from "lucide-react" +import { ChevronRight, type LucideIcon } from 'lucide-react' -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "./ui/collapsible" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible' import { SidebarGroup, SidebarGroupLabel, @@ -16,7 +12,7 @@ import { SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, -} from "./ui/sidebar" +} from './ui/sidebar' export function NavMain({ items, @@ -70,4 +66,4 @@ export function NavMain({ ) -} \ No newline at end of file +} diff --git a/components/profile-form.tsx b/components/profile-form.tsx index 6b337db..a011702 100644 --- a/components/profile-form.tsx +++ b/components/profile-form.tsx @@ -1,27 +1,34 @@ -'use client'; +'use client' -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import * as z from 'zod'; -import { User } from 'next-auth'; -import { useState } from 'react'; -import { useToast } from '@/hooks/use-toast'; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; -import { updateUserProfile } from '@/actions/user'; +import { updateUserProfile } from '@/actions/user' +import { Button } from '@/components/ui/button' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { useToast } from '@/hooks/use-toast' +import { zodResolver } from '@hookform/resolvers/zod' +import type { User } from 'next-auth' +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import * as z from 'zod' const profileFormSchema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters'), email: z.string().email('Invalid email address'), image: z.string().optional(), -}); +}) -type ProfileFormValues = z.infer; +type ProfileFormValues = z.infer export default function ProfileForm({ user }: { user: User }) { - const { toast } = useToast(); - const [isLoading, setIsLoading] = useState(false); + const { toast } = useToast() + const [isLoading, setIsLoading] = useState(false) const form = useForm({ resolver: zodResolver(profileFormSchema), @@ -29,27 +36,27 @@ export default function ProfileForm({ user }: { user: User }) { name: user.name || '', email: user.email || '', }, - }); + }) async function onSubmit(data: ProfileFormValues) { - setIsLoading(true); + setIsLoading(true) try { - if (!user.id) throw new Error('User ID is required'); - await updateUserProfile(user.id, data); + if (!user.id) throw new Error('User ID is required') + await updateUserProfile(user.id, data) toast({ title: 'Success', description: 'Profile updated successfully', - }); + }) } catch (error) { - console.log(error); + console.log(error) toast({ title: 'Error', description: 'Failed to update profile', variant: 'destructive', - }); + }) } - setIsLoading(false); + setIsLoading(false) } return ( @@ -88,5 +95,5 @@ export default function ProfileForm({ user }: { user: User }) { - ); + ) } diff --git a/components/providers/session-provider.tsx b/components/providers/session-provider.tsx index e1eb6e0..5f01d21 100644 --- a/components/providers/session-provider.tsx +++ b/components/providers/session-provider.tsx @@ -1,14 +1,14 @@ 'use client' +import type { Session } from 'next-auth' import { SessionProvider } from 'next-auth/react' -import { Session } from 'next-auth' -export default function AuthProvider({ +export default function AuthProvider({ children, - session -}: { + session, +}: { children: React.ReactNode session: Session | null }) { return {children} -} \ No newline at end of file +} diff --git a/components/register-form.tsx b/components/register-form.tsx index c042296..4de446f 100644 --- a/components/register-form.tsx +++ b/components/register-form.tsx @@ -1,30 +1,39 @@ -'use client'; +'use client' -import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'; +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card' // import { Label } from '@/components/ui/label'; -import { Input } from '@/components/ui/input'; -import { Button } from '@/components/ui/button'; -import Link from 'next/link'; +import { Input } from '@/components/ui/input' +import Link from 'next/link' -import { z } from 'zod'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { FormField, FormItem, FormLabel, FormControl, FormMessage, Form } from './ui/form'; -import { register } from '@/actions/auth'; -import { useRouter } from 'next/navigation'; +import { register } from '@/actions/auth' +import { zodResolver } from '@hookform/resolvers/zod' +import { useRouter } from 'next/navigation' +import { useForm } from 'react-hook-form' +import { z } from 'zod' +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form' -const formSchema = z.object({ - name: z.string().min(2, 'Name must be at least 2 characters'), - email: z.string().email('Invalid email address'), - password: z.string().min(6, 'Password must be at least 6 characters'), - confirmPassword: z.string().min(6, 'Password must be at least 6 characters'), -}).refine((data) => data.password === data.confirmPassword, { - message: "Passwords must match", - path: ['confirmPassword'], -}); +const formSchema = z + .object({ + name: z.string().min(2, 'Name must be at least 2 characters'), + email: z.string().email('Invalid email address'), + password: z.string().min(6, 'Password must be at least 6 characters'), + confirmPassword: z.string().min(6, 'Password must be at least 6 characters'), + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Passwords must match', + path: ['confirmPassword'], + }) export default function RegisterForm() { - const router = useRouter(); + const router = useRouter() const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -33,14 +42,14 @@ export default function RegisterForm() { password: '', confirmPassword: '', }, - }); + }) - async function onSubmit(values: z.infer) { - const res = await register(values); + async function onSubmit(values: z.infer) { + const res = await register(values) if (res.message !== 'success') { - form.setError('email', { type: 'manual', message: res.message }); + form.setError('email', { type: 'manual', message: res.message }) } else { - router.push('/login'); + router.push('/login') } } @@ -120,5 +129,5 @@ export default function RegisterForm() {

- ); + ) } diff --git a/components/search.tsx b/components/search.tsx index 598dc30..9c15d42 100644 --- a/components/search.tsx +++ b/components/search.tsx @@ -1,13 +1,9 @@ -import { Input } from "./ui/input" +import { Input } from './ui/input' export function Search() { return (
- +
) -} \ No newline at end of file +} diff --git a/components/team-switcher.tsx b/components/team-switcher.tsx index 3941d59..485191a 100644 --- a/components/team-switcher.tsx +++ b/components/team-switcher.tsx @@ -1,7 +1,7 @@ -"use client" +'use client' -import * as React from "react" -import { ChevronsUpDown, Plus } from "lucide-react" +import { ChevronsUpDown, Plus } from 'lucide-react' +import * as React from 'react' import { DropdownMenu, @@ -11,13 +11,8 @@ import { DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, -} from "./ui/dropdown-menu" -import { - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "./ui/sidebar" +} from './ui/dropdown-menu' +import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from './ui/sidebar' export function TeamSwitcher({ teams, @@ -44,9 +39,7 @@ export function TeamSwitcher({
- - {activeTeam.name} - + {activeTeam.name} {activeTeam.plan}
@@ -55,12 +48,10 @@ export function TeamSwitcher({ - - Teams - + Teams {teams.map((team, index) => ( ) -} \ No newline at end of file +} diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx index 51e507b..f009937 100644 --- a/components/ui/avatar.tsx +++ b/components/ui/avatar.tsx @@ -1,9 +1,9 @@ -"use client" +'use client' -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as AvatarPrimitive from '@radix-ui/react-avatar' +import * as React from 'react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const Avatar = React.forwardRef< React.ElementRef, @@ -11,10 +11,7 @@ const Avatar = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -26,7 +23,7 @@ const AvatarImage = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -39,7 +36,7 @@ const AvatarFallback = React.forwardRef< ( ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button' return ( - + ) } ) -Button.displayName = "Button" +Button.displayName = 'Button' export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx index cabfbfc..5581f36 100644 --- a/components/ui/card.tsx +++ b/components/ui/card.tsx @@ -1,76 +1,55 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -Card.displayName = "Card" - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardHeader.displayName = "CardHeader" - -const CardTitle = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardTitle.displayName = "CardTitle" - -const CardDescription = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardDescription.displayName = "CardDescription" - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardContent.displayName = "CardContent" - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardFooter.displayName = "CardFooter" +import * as React from 'react' + +import { cn } from '@/lib/utils' + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +Card.displayName = 'Card' + +const CardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardHeader.displayName = 'CardHeader' + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardTitle.displayName = 'CardTitle' + +const CardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardDescription.displayName = 'CardDescription' + +const CardContent = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardContent.displayName = 'CardContent' + +const CardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ) +) +CardFooter.displayName = 'CardFooter' export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx index a23e7a2..7cee61e 100644 --- a/components/ui/collapsible.tsx +++ b/components/ui/collapsible.tsx @@ -1,4 +1,4 @@ -import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible' const Collapsible = CollapsiblePrimitive.Root diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx index 9ff6568..0dd189a 100644 --- a/components/ui/dropdown-menu.tsx +++ b/components/ui/dropdown-menu.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" -import { Check, ChevronRight, Circle } from "lucide-react" +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' +import { Check, ChevronRight, Circle } from 'lucide-react' +import * as React from 'react' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const DropdownMenu = DropdownMenuPrimitive.Root @@ -25,8 +25,8 @@ const DropdownMenuSubTrigger = React.forwardRef< )) -DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName +DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName const DropdownMenuSubContent = React.forwardRef< React.ElementRef, @@ -45,14 +44,13 @@ const DropdownMenuSubContent = React.forwardRef< )) -DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName +DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName const DropdownMenuContent = React.forwardRef< React.ElementRef, @@ -63,8 +61,8 @@ const DropdownMenuContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover 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", + 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover 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} @@ -82,8 +80,8 @@ const DropdownMenuItem = React.forwardRef< svg]:size-4 [&>svg]:shrink-0", - inset && "pl-8", + 'relative flex cursor-default select-none items-center gap-2 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 [&>svg]:size-4 [&>svg]:shrink-0', + inset && 'pl-8', className )} {...props} @@ -98,7 +96,7 @@ const DropdownMenuCheckboxItem = React.forwardRef< )) -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName +DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName const DropdownMenuRadioItem = React.forwardRef< React.ElementRef, @@ -122,7 +119,7 @@ const DropdownMenuRadioItem = React.forwardRef< (({ className, inset, ...props }, ref) => ( )) @@ -161,24 +154,16 @@ const DropdownMenuSeparator = React.forwardRef< >(({ className, ...props }, ref) => ( )) DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName -const DropdownMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes) => { - return ( - - ) +const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return } -DropdownMenuShortcut.displayName = "DropdownMenuShortcut" +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut' export { DropdownMenu, diff --git a/components/ui/form.tsx b/components/ui/form.tsx index b6daa65..46a4608 100644 --- a/components/ui/form.tsx +++ b/components/ui/form.tsx @@ -1,36 +1,34 @@ -"use client" +'use client' -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" +import type * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' +import * as React from 'react' import { Controller, - ControllerProps, - FieldPath, - FieldValues, + type ControllerProps, + type FieldPath, + type FieldValues, FormProvider, useFormContext, -} from "react-hook-form" +} from 'react-hook-form' -import { cn } from "@/lib/utils" -import { Label } from "@/components/ui/label" +import { Label } from '@/components/ui/label' +import { cn } from '@/lib/utils' const Form = FormProvider type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, > = { name: TName } -const FormFieldContext = React.createContext( - {} as FormFieldContextValue -) +const FormFieldContext = React.createContext({} as FormFieldContextValue) const FormField = < TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, >({ ...props }: ControllerProps) => { @@ -49,7 +47,7 @@ const useFormField = () => { const fieldState = getFieldState(fieldContext.name, formState) if (!fieldContext) { - throw new Error("useFormField should be used within ") + throw new Error('useFormField should be used within ') } const { id } = itemContext @@ -68,23 +66,20 @@ type FormItemContextValue = { id: string } -const FormItemContext = React.createContext( - {} as FormItemContextValue -) +const FormItemContext = React.createContext({} as FormItemContextValue) -const FormItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const id = React.useId() +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId() - return ( - -
- - ) -}) -FormItem.displayName = "FormItem" + return ( + +
+ + ) + } +) +FormItem.displayName = 'FormItem' const FormLabel = React.forwardRef< React.ElementRef, @@ -95,13 +90,13 @@ const FormLabel = React.forwardRef< return (