diff --git a/.cspell.jsonc b/.cspell.jsonc new file mode 100644 index 0000000000..ce61f0751a --- /dev/null +++ b/.cspell.jsonc @@ -0,0 +1,101 @@ +{ + "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", + "dictionaries": [ + "software-terms", + "npm", + "fullstack" + ], + "files": [ + "**", + ".vscode/**", + ".github/**" + ], + "ignorePaths": [ + "pnpm-lock.yaml", + "bun.lock", + "bun.lockb", + "lib/db/migrations" + ], + "ignoreRegExpList": [ + "apiKey='[a-zA-Z0-9-]{32}'" + ], + "import": [ + "@cspell/dict-bash/cspell-ext.json" + ], + "useGitignore": true, + "version": "0.2", + "words": [ + "turbopack", + "remark", + "katex", + "lefthook", + "orama", + "catppuccin", + "inkeep", + "devcontainers", + "twoslash", + "vitesse", + "scira", + "subsecond", + "bprogress", + "contentlayer", + "basehub", + "shadcn", + "subcomponents", + "nuqs", + "nocompatible", + "pseudoelements", + "matplotlib", + "pyplot", + "savefig", + "Pyodide", + "unparse", + "papaparse", + "diffview", + "autofocus", + "Vercelians", + "orderedmap", + "prosemirror", + "hitbox", + "usehooks", + "inputrules", + "Delba", + "Oliveira", + "upvoting", + "downvoting", + "downvoted", + "noninteractive", + "generationtime", + "vaul", + "nums", + "groq", + "deepseek", + "sdxl", + "logprobs", + "dall", + "textblock", + "Geordanna", + "Cordero", + "Pawel", + "Czerwinski", + "Olah", + "Filipe", + "aisdk", + "yxxx", + "nosniff", + "sameorigin", + "neondatabase", + "viewports", + "authjs", + "webauthn", + "filechooser", + "emilkowalski", + "Embla", + "tokenlens", + "viewbox", + "noblank", + "prefault", + "shiki", + "opengraph" + ] +} \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..ddf36c81f3 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,55 @@ +{ + "name": "ai-chatbot", + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", + "forwardPorts": [ + 3000 + ], + "portsAttributes": { + "3000": { + "label": "Web", + "onAutoForward": "notify" + } + }, + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "configureZshAsDefaultShell": true + }, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + "containerEnv": {}, + "remoteEnv": {}, + "customizations": { + "vscode": { + "extensions": [ + // TypeScript + "better-ts-errors.better-ts-errors", + // Other + "bradlc.vscode-tailwindcss", + "streetsidesoftware.code-spell-checker", + "biomejs.biome", + "aaron-bond.better-comments", + "formulahendry.auto-rename-tag" + ], + // Set *default* container specific settings.json values on container create. + "settings": { + "terminal.integrated.defaultProfile.linux": "zsh", + "debug.internalConsoleOptions": "neverOpen", + "editor.formatOnPaste": true, + "editor.guides.bracketPairs": "active", + "scm.defaultViewMode": "tree", + "diffEditor.diffAlgorithm": "advanced", + "diffEditor.experimental.showMoves": true, + "diffEditor.renderSideBySide": false, + "files.watcherExclude": { + "**/node_modules/**": true + }, + // Prettifies the response with emojis and such. + "betterTypeScriptErrors.prettify": true + } + } + }, + "postCreateCommand": { + "web": "pnpm install", + "playwright": "pnpm exec playwright install-deps && pnpm exec playwright install" + } + } \ No newline at end of file diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000000..357c47b6d2 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,33 @@ +name: Check setup +description: Set up Node, and pnpm, prime caches, and install dependencies. + +runs: + using: composite + steps: + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - name: Get pnpm store path + id: pnpm-cache + shell: bash + run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Cache pnpm store + uses: actions/cache@v4 + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: '.nvmrc' + cache: 'pnpm' + + - name: Install dependencies + shell: bash + run: pnpm install --frozen-lockfile \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..7b5e5d47c0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + +jobs: + types: + name: TypeScript + runs-on: ubuntu-22.04 + + steps: + - name: Checkout branch + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Generate types + run: pnpm typegen + + - name: Run type check + run: pnpm typecheck + + biome: + name: Biome + runs-on: ubuntu-22.04 + steps: + - name: Checkout branch + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Run Biome + run: pnpm check + + spelling: + name: Spelling + runs-on: ubuntu-22.04 + steps: + - name: Checkout branch + uses: actions/checkout@v4 + + - name: Setup + uses: ./.github/actions/setup + + - name: Run spelling check + run: pnpm check:spelling \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 1d57a670b7..0000000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Lint -on: - push: - -jobs: - build: - runs-on: ubuntu-22.04 - strategy: - matrix: - node-version: [20] - steps: - - uses: actions/checkout@v4 - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 9.12.3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: "pnpm" - - name: Install dependencies - run: pnpm install - - name: Run lint - run: pnpm lint diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 6e18c3ec60..df6ee65783 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -20,36 +20,8 @@ jobs: with: fetch-depth: 1 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: latest - run_install: false - - - name: Get pnpm store directory - id: pnpm-cache - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - - - uses: actions/cache@v3 - with: - path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - uses: actions/setup-node@v4 - with: - node-version: lts/* - cache: "pnpm" - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup + uses: ./.github/actions/setup - name: Cache Playwright browsers uses: actions/cache@v3 @@ -70,4 +42,4 @@ jobs: with: name: playwright-report path: playwright-report/ - retention-days: 7 + retention-days: 7 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 54b31f9fcf..0750257b79 100644 --- a/.gitignore +++ b/.gitignore @@ -24,20 +24,19 @@ yarn-error.log* .pnpm-debug.log* # local env files -.env.local -.env.development.local -.env.test.local -.env.production.local +.env* +!.env.example # turbo .turbo - -.env .vercel -.env*.local +.pnpm-store # Playwright /test-results/ /playwright-report/ /blob-report/ /playwright/* + +# cspell +.cspellcache \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..5d9c217de3 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v22.19.0 \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index fb5a7ca0b5..8031abfa92 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,14 +4,18 @@ "editor.defaultFormatter": "biomejs.biome" }, "[typescript]": { - "editor.defaultFormatter": "biomejs.biome" + "editor.defaultFormatter": "vscode.typescript-language-features" }, "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, "typescript.tsdk": "node_modules/typescript/lib", "eslint.workingDirectories": [ - { "pattern": "app/*" }, - { "pattern": "packages/*" } + { + "pattern": "app/*" + }, + { + "pattern": "packages/*" + } ] -} +} \ No newline at end of file diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts index 84f8ffde43..866f4f0668 100644 --- a/app/(auth)/actions.ts +++ b/app/(auth)/actions.ts @@ -1,84 +1,39 @@ 'use server'; +import 'server-only'; -import { z } from 'zod'; - +import { actionClient } from '@/lib/safe-action'; +import { SignInSchema, SignUpSchema } from '@/lib/validators'; +import { signIn } from '@/app/(auth)/auth'; import { createUser, getUser } from '@/lib/db/queries'; +import { returnValidationErrors } from 'next-safe-action'; -import { signIn } from './auth'; - -const authFormSchema = z.object({ - email: z.string().email(), - password: z.string().min(6), -}); - -export interface LoginActionState { - status: 'idle' | 'in_progress' | 'success' | 'failed' | 'invalid_data'; -} - -export const login = async ( - _: LoginActionState, - formData: FormData, -): Promise => { - try { - const validatedData = authFormSchema.parse({ - email: formData.get('email'), - password: formData.get('password'), - }); - +export const login = actionClient + .inputSchema(SignInSchema) + .action(async ({ parsedInput: { email, password } }) => { await signIn('credentials', { - email: validatedData.email, - password: validatedData.password, + email, + password, redirect: false, }); - return { status: 'success' }; - } catch (error) { - if (error instanceof z.ZodError) { - return { status: 'invalid_data' }; - } - - return { status: 'failed' }; - } -}; - -export interface RegisterActionState { - status: - | 'idle' - | 'in_progress' - | 'success' - | 'failed' - | 'user_exists' - | 'invalid_data'; -} - -export const register = async ( - _: RegisterActionState, - formData: FormData, -): Promise => { - try { - const validatedData = authFormSchema.parse({ - email: formData.get('email'), - password: formData.get('password'), - }); + return { success: true } + }); - const [user] = await getUser(validatedData.email); +export const register = actionClient + .inputSchema(SignUpSchema) + .action(async ({ parsedInput: { email, password } }) => { + const [user] = await getUser(email); if (user) { - return { status: 'user_exists' } as RegisterActionState; + return returnValidationErrors(SignUpSchema, { _errors: ["User already exists"] }); } - await createUser(validatedData.email, validatedData.password); + + await createUser(email, password); await signIn('credentials', { - email: validatedData.email, - password: validatedData.password, + email, + password, redirect: false, }); - return { status: 'success' }; - } catch (error) { - if (error instanceof z.ZodError) { - return { status: 'invalid_data' }; - } - - return { status: 'failed' }; - } -}; + return { success: true } + }); diff --git a/app/(auth)/api/auth/guest/route.ts b/app/(auth)/api/auth/guest/route.ts index 25af1fa7b7..e96d01e55c 100644 --- a/app/(auth)/api/auth/guest/route.ts +++ b/app/(auth)/api/auth/guest/route.ts @@ -1,15 +1,16 @@ import { signIn } from '@/app/(auth)/auth'; import { isDevelopmentEnvironment } from '@/lib/constants'; import { getToken } from 'next-auth/jwt'; -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; +import { env } from '@/env'; -export async function GET(request: Request) { +export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const redirectUrl = searchParams.get('redirectUrl') || '/'; const token = await getToken({ req: request, - secret: process.env.AUTH_SECRET, + secret: env.AUTH_SECRET, secureCookie: !isDevelopmentEnvironment, }); diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 2c9759b08e..8bc4363df6 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,54 +1,18 @@ -'use client'; +import type { Metadata } from 'next'; +import { CardWrapper } from '@/components/auth/card-wrapper'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { useActionState, useEffect, useState } from 'react'; -import { toast } from '@/components/toast'; +import { MessageSquare } from 'lucide-react'; +import { AbstractImage } from '../../../components/auth/abstract-image'; +import { LoginForm } from '@/components/auth/login-form'; -import { AuthForm } from '@/components/auth-form'; -import { SubmitButton } from '@/components/submit-button'; +export const metadata: Metadata = { + title: 'Login', + description: 'Login to AI Chatbot', +}; -import { login, type LoginActionState } from '../actions'; -import { useSession } from 'next-auth/react'; - -export default function Page() { - const router = useRouter(); - - const [email, setEmail] = useState(''); - const [isSuccessful, setIsSuccessful] = useState(false); - - const [state, formAction] = useActionState( - login, - { - status: 'idle', - }, - ); - - const { update: updateSession } = useSession(); - - useEffect(() => { - if (state.status === 'failed') { - toast({ - type: 'error', - description: 'Invalid credentials!', - }); - } else if (state.status === 'invalid_data') { - toast({ - type: 'error', - description: 'Failed validating your submission!', - }); - } else if (state.status === 'success') { - setIsSuccessful(true); - updateSession(); - router.refresh(); - } - }, [state.status, router, updateSession]); - - const handleSubmit = (formData: FormData) => { - setEmail(formData.get('email') as string); - formAction(formData); - }; +export const dynamic = 'force-dynamic'; +export default async function LoginPage() { return (
@@ -66,12 +30,11 @@ export default function Page() { href="/register" className="font-semibold text-gray-800 hover:underline dark:text-zinc-200" > - Sign up - - {' for free.'} -

- + + +
+
- + ); -} +} \ No newline at end of file diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index d14c79ef4e..c5a53402fa 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -1,55 +1,17 @@ -'use client'; +import type { Metadata } from 'next'; +import { CardWrapper } from '@/components/auth/card-wrapper'; +import { MessageSquare } from 'lucide-react'; +import { AbstractImage } from '../../../components/auth/abstract-image'; +import { RegisterForm } from '@/components/auth/register-form'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { useActionState, useEffect, useState } from 'react'; +export const metadata: Metadata = { + title: 'Register', + description: 'Register to AI Chatbot', +}; -import { AuthForm } from '@/components/auth-form'; -import { SubmitButton } from '@/components/submit-button'; - -import { register, type RegisterActionState } from '../actions'; -import { toast } from '@/components/toast'; -import { useSession } from 'next-auth/react'; - -export default function Page() { - const router = useRouter(); - - const [email, setEmail] = useState(''); - const [isSuccessful, setIsSuccessful] = useState(false); - - const [state, formAction] = useActionState( - register, - { - status: 'idle', - }, - ); - - const { update: updateSession } = useSession(); - - useEffect(() => { - if (state.status === 'user_exists') { - toast({ type: 'error', description: 'Account already exists!' }); - } else if (state.status === 'failed') { - toast({ type: 'error', description: 'Failed to create account!' }); - } else if (state.status === 'invalid_data') { - toast({ - type: 'error', - description: 'Failed validating your submission!', - }); - } else if (state.status === 'success') { - toast({ type: 'success', description: 'Account created successfully!' }); - - setIsSuccessful(true); - updateSession(); - router.refresh(); - } - }, [state, router, updateSession]); - - const handleSubmit = (formData: FormData) => { - setEmail(formData.get('email') as string); - formAction(formData); - }; +export const dynamic = 'force-dynamic'; +export default function RegisterPage() { return (
@@ -67,12 +29,11 @@ export default function Page() { href="/login" className="font-semibold text-gray-800 hover:underline dark:text-zinc-200" > - Sign in - - {' instead.'} -

- + + +
+
- + ); -} +} \ No newline at end of file diff --git a/app/(chat)/actions.ts b/app/(chat)/actions.ts index 14ee7dc2a4..d3bfa76188 100644 --- a/app/(chat)/actions.ts +++ b/app/(chat)/actions.ts @@ -1,11 +1,12 @@ 'use server'; +import 'server-only'; import { generateText, type UIMessage } from 'ai'; import { cookies } from 'next/headers'; import { deleteMessagesByChatIdAfterTimestamp, getMessageById, - updateChatVisiblityById, + updateChatVisibilityById, } from '@/lib/db/queries'; import type { VisibilityType } from '@/components/visibility-selector'; import { myProvider } from '@/lib/ai/providers'; @@ -49,5 +50,5 @@ export async function updateChatVisibility({ chatId: string; visibility: VisibilityType; }) { - await updateChatVisiblityById({ chatId, visibility }); + await updateChatVisibilityById({ chatId, visibility }); } diff --git a/app/(chat)/api/chat/[id]/stream/route.ts b/app/(chat)/api/chat/[id]/stream/route.ts index 58b46432a5..c5d9649bfc 100644 --- a/app/(chat)/api/chat/[id]/stream/route.ts +++ b/app/(chat)/api/chat/[id]/stream/route.ts @@ -13,7 +13,7 @@ import { differenceInSeconds } from 'date-fns'; export async function GET( _: Request, - { params }: { params: Promise<{ id: string }> }, + { params }: RouteContext<'/api/chat/[id]/stream'>, ) { const { id: chatId } = await params; diff --git a/app/(chat)/api/chat/schema.ts b/app/(chat)/api/chat/schema.ts index 555ef8b95c..f705cb97f6 100644 --- a/app/(chat)/api/chat/schema.ts +++ b/app/(chat)/api/chat/schema.ts @@ -9,15 +9,15 @@ const filePartSchema = z.object({ type: z.enum(['file']), mediaType: z.enum(['image/jpeg', 'image/png']), name: z.string().min(1).max(100), - url: z.string().url(), + url: z.url(), }); const partSchema = z.union([textPartSchema, filePartSchema]); export const postRequestBodySchema = z.object({ - id: z.string().uuid(), + id: z.uuid(), message: z.object({ - id: z.string().uuid(), + id: z.uuid(), role: z.enum(['user']), parts: z.array(partSchema), }), diff --git a/app/(chat)/api/files/upload/route.ts b/app/(chat)/api/files/upload/route.ts index 699a4cbef8..c0a8006851 100644 --- a/app/(chat)/api/files/upload/route.ts +++ b/app/(chat)/api/files/upload/route.ts @@ -9,11 +9,11 @@ const FileSchema = z.object({ file: z .instanceof(Blob) .refine((file) => file.size <= 5 * 1024 * 1024, { - message: 'File size should be less than 5MB', + error: 'File size should be less than 5MB' }) // Update the file type based on the kind of files you want to accept .refine((file) => ['image/jpeg', 'image/png'].includes(file.type), { - message: 'File type should be JPEG or PNG', + error: 'File type should be JPEG or PNG' }), }); @@ -39,8 +39,8 @@ export async function POST(request: Request) { const validatedFile = FileSchema.safeParse({ file }); if (!validatedFile.success) { - const errorMessage = validatedFile.error.errors - .map((error) => error.message) + const errorMessage = validatedFile.error.issues + .map((issue) => issue.message) .join(', '); return NextResponse.json({ error: errorMessage }, { status: 400 }); diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx index b4eaaacdc8..b1c64f2c7e 100644 --- a/app/(chat)/chat/[id]/page.tsx +++ b/app/(chat)/chat/[id]/page.tsx @@ -7,9 +7,8 @@ import { getChatById, getMessagesByChatId } from '@/lib/db/queries'; import { DataStreamHandler } from '@/components/data-stream-handler'; import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models'; import { convertToUIMessages } from '@/lib/utils'; -import { LanguageModelV2Usage } from '@ai-sdk/provider'; -export default async function Page(props: { params: Promise<{ id: string }> }) { +export default async function Page(props: PageProps<'/chat/[id]'>) { const params = await props.params; const { id } = params; const chat = await getChatById({ id }); diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx index dfd9da2fdf..6fda94a3a3 100644 --- a/app/(chat)/layout.tsx +++ b/app/(chat)/layout.tsx @@ -10,11 +10,9 @@ export const experimental_ppr = true; export default async function Layout({ children, -}: { - children: React.ReactNode; -}) { +}: LayoutProps<'/'>) { const [session, cookieStore] = await Promise.all([auth(), cookies()]); - const isCollapsed = cookieStore.get('sidebar:state')?.value !== 'true'; + const isCollapsed = cookieStore.get('sidebar_state')?.value !== 'true'; return ( <> diff --git a/app/globals.css b/app/globals.css index 4be4d707ec..137aeab6e9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -183,9 +183,73 @@ @media (prefers-color-scheme: dark) { :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; + --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.625rem; + --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%; } } } diff --git a/app/layout.tsx b/app/layout.tsx index 1813e0f71b..87f98d080e 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -50,9 +50,7 @@ const THEME_COLOR_SCRIPT = `\ export default async function RootLayout({ children, -}: Readonly<{ - children: React.ReactNode; -}>) { +}: LayoutProps<'/'>) { return ( void | Promise) | undefined - >; - children: React.ReactNode; - defaultEmail?: string; -}) { - return ( -
-
- - - -
- -
- - - -
- - {children} -
- ); -} diff --git a/components/auth/abstract-image.tsx b/components/auth/abstract-image.tsx new file mode 100644 index 0000000000..c37a82cb9b --- /dev/null +++ b/components/auth/abstract-image.tsx @@ -0,0 +1,44 @@ +import BlurImage from '@/components/blur-image'; +import { abstractImages } from '@/lib/images'; +import { unstable_cache } from 'next/cache'; + +const getImage = unstable_cache( + async () => { + const idx = Math.floor(Math.random() * abstractImages.length); + const img = abstractImages[idx]; + + return { + url: img.url, + author: { + name: img.author.name, + url: img.author.url, + }, + }; + }, + ['auth-bg'], + { revalidate: 60 * 60 * 24, tags: ['auth-bg'] }, +); + +export async function AbstractImage() { + const image = await getImage(); + + return ( + <> + + + + ); +} diff --git a/components/auth/back-button.tsx b/components/auth/back-button.tsx new file mode 100644 index 0000000000..7cdaa3f845 --- /dev/null +++ b/components/auth/back-button.tsx @@ -0,0 +1,19 @@ +import { Route } from 'next'; +import Link from 'next/link'; + +interface BackButtonProps { + label: string; + linkLabel: string; + href: Route; +} + +export const BackButton = ({ label, linkLabel, href }: BackButtonProps) => { + return ( +
+ {label} + + {linkLabel} + +
+ ); +}; diff --git a/components/auth/card-wrapper.tsx b/components/auth/card-wrapper.tsx new file mode 100644 index 0000000000..9fea9445ea --- /dev/null +++ b/components/auth/card-wrapper.tsx @@ -0,0 +1,34 @@ +import { cn } from '@/lib/utils'; + +import { BackButton } from '@/components/auth/back-button'; +import { baseUrl } from '@/lib/constants'; + +interface CardWrapperProps { + children: React.ReactNode; + backButtonLabel: string; + backButtonLinkLabel: string; + backButtonHref: string; + showSocial?: boolean; + showCredentials?: boolean; + className?: string; +} + +export const CardWrapper = async ({ + children, + backButtonLabel, + backButtonLinkLabel, + backButtonHref, + className, +}: CardWrapperProps) => { + return ( +
+ {children} + + +
+ ); +}; diff --git a/components/auth/login-form.tsx b/components/auth/login-form.tsx new file mode 100644 index 0000000000..30f43aa9c6 --- /dev/null +++ b/components/auth/login-form.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useHookFormAction } from '@next-safe-action/adapter-react-hook-form/hooks'; +import { Input } from '@/components/ui/input'; + +import { TriangleAlertIcon as IconWarning, LoaderIcon } from 'lucide-react'; +import { CheckCircleFillIcon as IconCheckCircle } from '@/components/icons'; +import { Alert, AlertTitle } from '../ui/alert'; + +import { SignInSchema } from '@/lib/validators'; +import { login } from '@/app/(auth)/actions'; +import { redirect } from 'next/navigation'; + +export const LoginForm = () => { + const { form, action, handleSubmitWithAction, resetFormAndAction } = + useHookFormAction(login, zodResolver(SignInSchema), { + formProps: { + mode: 'onChange', + }, + actionProps: { + onSuccess: () => { + resetFormAndAction(); + redirect('/'); + }, + }, + }); + + const { status, result } = action; + const { isValid, errors } = form.formState; + + return ( +
+ +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + +
+ + {errors.root && {errors.root.message}} + + {status === 'hasSucceeded' && ( + + + + Logged in successfully! + + + )} + + {result.serverError && ( + + + + {result.serverError} + + + )} + + +
+ + ); +}; diff --git a/components/auth/register-form.tsx b/components/auth/register-form.tsx new file mode 100644 index 0000000000..1fdfba5fe0 --- /dev/null +++ b/components/auth/register-form.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from '@/components/ui/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useHookFormAction } from '@next-safe-action/adapter-react-hook-form/hooks'; +import { Input } from '@/components/ui/input'; + +import { TriangleAlertIcon as IconWarning, LoaderIcon } from 'lucide-react'; +import { CheckCircleFillIcon as IconCheckCircle } from '@/components/icons'; +import { Alert, AlertTitle } from '../ui/alert'; + +import { SignUpSchema } from '@/lib/validators'; +import { register } from '@/app/(auth)/actions'; +import { redirect } from 'next/navigation'; + +export const RegisterForm = () => { + const { form, action, handleSubmitWithAction, resetFormAndAction } = + useHookFormAction(register, zodResolver(SignUpSchema), { + formProps: { + mode: 'onChange', + }, + actionProps: { + onSuccess: () => { + resetFormAndAction(); + redirect('/'); + }, + }, + }); + + const { status, result } = action; + const { isValid, errors } = form.formState; + + return ( +
+ +
+ ( + + + + + + + )} + /> + ( + + + + + + + )} + /> + +
+ + {errors.root && {errors.root.message}} + + {status === 'hasSucceeded' && ( + + + + Account created successfully! + + + )} + + {result.serverError && ( + + + + {result.serverError} + + + )} + + +
+ + ); +}; diff --git a/components/blur-image.tsx b/components/blur-image.tsx new file mode 100644 index 0000000000..ab8520a251 --- /dev/null +++ b/components/blur-image.tsx @@ -0,0 +1,48 @@ +/** + * website + * Copyright (c) Delba de Oliveira + * Source: https://github.com/delbaoliveira/website/blob/59e6f181ad75751342ceaa8931db4cbcef86b018/ui/BlurImage.tsx + */ + +'use client'; + +import { cn } from '@/lib/utils'; +import NextImage from 'next/image'; +import { useState } from 'react'; + +type ImageProps = { + imageClassName?: string; + lazy?: boolean; +} & React.ComponentProps; + +const BlurImage = (props: ImageProps) => { + const { alt, src, className, imageClassName, lazy = true, ...rest } = props; + const [isLoading, setIsLoading] = useState(true); + + return ( +
+ { + setIsLoading(false); + }} + {...rest} + /> +
+ ); +}; + +export default BlurImage; \ No newline at end of file diff --git a/components/document-preview.tsx b/components/document-preview.tsx index 2248e58562..7ff2ec0d27 100644 --- a/components/document-preview.tsx +++ b/components/document-preview.tsx @@ -146,7 +146,7 @@ const PureHitboxLayer = ({ result, setArtifact, }: { - hitboxRef: React.RefObject; + hitboxRef: React.RefObject; result: any; setArtifact: ( updaterFn: UIArtifact | ((currentArtifact: UIArtifact) => UIArtifact), diff --git a/components/elements/branch.tsx b/components/elements/branch.tsx index b730e187c5..39c7ada540 100644 --- a/components/elements/branch.tsx +++ b/components/elements/branch.tsx @@ -12,8 +12,8 @@ type BranchContextType = { totalBranches: number; goToPrevious: () => void; goToNext: () => void; - branches: ReactElement[]; - setBranches: (branches: ReactElement[]) => void; + branches: ReactElement[]; + setBranches: (branches: ReactElement[]) => void; }; const BranchContext = createContext(null); @@ -40,7 +40,7 @@ export const Branch = ({ ...props }: BranchProps) => { const [currentBranch, setCurrentBranch] = useState(defaultBranch); - const [branches, setBranches] = useState([]); + const [branches, setBranches] = useState[]>([]); const handleBranchChange = (newBranch: number) => { setCurrentBranch(newBranch); diff --git a/components/elements/context.tsx b/components/elements/context.tsx index 9ddbeb4fea..72ed480303 100644 --- a/components/elements/context.tsx +++ b/components/elements/context.tsx @@ -275,7 +275,7 @@ export const Context = ({