diff --git a/.cursor/rules/ultracite.mdc b/.cursor/rules/ultracite.mdc deleted file mode 100644 index f2da237..0000000 --- a/.cursor/rules/ultracite.mdc +++ /dev/null @@ -1,333 +0,0 @@ ---- -description: Ultracite Rules - AI-Ready Formatter and Linter -globs: "**/*.{ts,tsx,js,jsx}" -alwaysApply: true ---- - -# Project Context -Ultracite enforces strict type safety, accessibility standards, and consistent code quality for JavaScript/TypeScript projects using Biome's lightning-fast formatter and linter. - -## Key Principles -- Zero configuration required -- Subsecond performance -- Maximum type safety -- AI-friendly code generation - -## Before Writing Code -1. Analyze existing patterns in the codebase -2. Consider edge cases and error scenarios -3. Follow the rules below strictly -4. Validate accessibility requirements - -## Rules - -### Accessibility (a11y) -- Don't use `accessKey` attribute on any HTML element. -- Don't set `aria-hidden="true"` on focusable elements. -- Don't add ARIA roles, states, and properties to elements that don't support them. -- Don't use distracting elements like `` or ``. -- Only use the `scope` prop on `` elements. -- Don't assign non-interactive ARIA roles to interactive HTML elements. -- Make sure label elements have text content and are associated with an input. -- Don't assign interactive ARIA roles to non-interactive HTML elements. -- Don't assign `tabIndex` to non-interactive HTML elements. -- Don't use positive integers for `tabIndex` property. -- Don't include "image", "picture", or "photo" in img alt prop. -- Don't use explicit role property that's the same as the implicit/default role. -- Make static elements with click handlers use a valid role attribute. -- Always include a `title` element for SVG elements. -- Give all elements requiring alt text meaningful information for screen readers. -- Make sure anchors have content that's accessible to screen readers. -- Assign `tabIndex` to non-interactive HTML elements with `aria-activedescendant`. -- Include all required ARIA attributes for elements with ARIA roles. -- Make sure ARIA properties are valid for the element's supported roles. -- Always include a `type` attribute for button elements. -- Make elements with interactive roles and handlers focusable. -- Give heading elements content that's accessible to screen readers (not hidden with `aria-hidden`). -- Always include a `lang` attribute on the html element. -- Always include a `title` attribute for iframe elements. -- Accompany `onClick` with at least one of: `onKeyUp`, `onKeyDown`, or `onKeyPress`. -- Accompany `onMouseOver`/`onMouseOut` with `onFocus`/`onBlur`. -- Include caption tracks for audio and video elements. -- Use semantic elements instead of role attributes in JSX. -- Make sure all anchors are valid and navigable. -- Ensure all ARIA properties (`aria-*`) are valid. -- Use valid, non-abstract ARIA roles for elements with ARIA roles. -- Use valid ARIA state and property values. -- Use valid values for the `autocomplete` attribute on input elements. -- Use correct ISO language/country codes for the `lang` attribute. - -### Code Complexity and Quality -- Don't use consecutive spaces in regular expression literals. -- Don't use the `arguments` object. -- Don't use primitive type aliases or misleading types. -- Don't use the comma operator. -- Don't use empty type parameters in type aliases and interfaces. -- Don't write functions that exceed a given Cognitive Complexity score. -- Don't nest describe() blocks too deeply in test files. -- Don't use unnecessary boolean casts. -- Don't use unnecessary callbacks with flatMap. -- Use for...of statements instead of Array.forEach. -- Don't create classes that only have static members (like a static namespace). -- Don't use this and super in static contexts. -- Don't use unnecessary catch clauses. -- Don't use unnecessary constructors. -- Don't use unnecessary continue statements. -- Don't export empty modules that don't change anything. -- Don't use unnecessary escape sequences in regular expression literals. -- Don't use unnecessary fragments. -- Don't use unnecessary labels. -- Don't use unnecessary nested block statements. -- Don't rename imports, exports, and destructured assignments to the same name. -- Don't use unnecessary string or template literal concatenation. -- Don't use String.raw in template literals when there are no escape sequences. -- Don't use useless case statements in switch statements. -- Don't use ternary operators when simpler alternatives exist. -- Don't use useless `this` aliasing. -- Don't use any or unknown as type constraints. -- Don't initialize variables to undefined. -- Don't use the void operators (they're not familiar). -- Use arrow functions instead of function expressions. -- Use Date.now() to get milliseconds since the Unix Epoch. -- Use .flatMap() instead of map().flat() when possible. -- Use literal property access instead of computed property access. -- Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work. -- Use concise optional chaining instead of chained logical expressions. -- Use regular expression literals instead of the RegExp constructor when possible. -- Don't use number literal object member names that aren't base 10 or use underscore separators. -- Remove redundant terms from logical expressions. -- Use while loops instead of for loops when you don't need initializer and update expressions. -- Don't pass children as props. -- Don't reassign const variables. -- Don't use constant expressions in conditions. -- Don't use `Math.min` and `Math.max` to clamp values when the result is constant. -- Don't return a value from a constructor. -- Don't use empty character classes in regular expression literals. -- Don't use empty destructuring patterns. -- Don't call global object properties as functions. -- Don't declare functions and vars that are accessible outside their block. -- Make sure builtins are correctly instantiated. -- Don't use super() incorrectly inside classes. Also check that super() is called in classes that extend other constructors. -- Don't use variables and function parameters before they're declared. -- Don't use 8 and 9 escape sequences in string literals. -- Don't use literal numbers that lose precision. - -### React and JSX Best Practices -- Don't use the return value of React.render. -- Make sure all dependencies are correctly specified in React hooks. -- Make sure all React hooks are called from the top level of component functions. -- Don't forget key props in iterators and collection literals. -- Don't destructure props inside JSX components in Solid projects. -- Don't define React components inside other components. -- Don't use event handlers on non-interactive elements. -- Don't assign to React component props. -- Don't use both `children` and `dangerouslySetInnerHTML` props on the same element. -- Don't use dangerous JSX props. -- Don't use Array index in keys. -- Don't insert comments as text nodes. -- Don't assign JSX properties multiple times. -- Don't add extra closing tags for components without children. -- Use `<>...` instead of `...`. -- Watch out for possible "wrong" semicolons inside JSX elements. - -### Correctness and Safety -- Don't assign a value to itself. -- Don't return a value from a setter. -- Don't compare expressions that modify string case with non-compliant values. -- Don't use lexical declarations in switch clauses. -- Don't use variables that haven't been declared in the document. -- Don't write unreachable code. -- Make sure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass. -- Don't use control flow statements in finally blocks. -- Don't use optional chaining where undefined values aren't allowed. -- Don't have unused function parameters. -- Don't have unused imports. -- Don't have unused labels. -- Don't have unused private class members. -- Don't have unused variables. -- Make sure void (self-closing) elements don't have children. -- Don't return a value from a function with the return type 'void' -- Use isNaN() when checking for NaN. -- Make sure "for" loop update clauses move the counter in the right direction. -- Make sure typeof expressions are compared to valid values. -- Make sure generator functions contain yield. -- Don't use await inside loops. -- Don't use bitwise operators. -- Don't use expressions where the operation doesn't change the value. -- Make sure Promise-like statements are handled appropriately. -- Don't use __dirname and __filename in the global scope. -- Prevent import cycles. -- Don't use configured elements. -- Don't hardcode sensitive data like API keys and tokens. -- Don't let variable declarations shadow variables from outer scopes. -- Don't use the TypeScript directive @ts-ignore. -- Prevent duplicate polyfills from Polyfill.io. -- Don't use useless backreferences in regular expressions that always match empty strings. -- Don't use unnecessary escapes in string literals. -- Don't use useless undefined. -- Make sure getters and setters for the same property are next to each other in class and object definitions. -- Make sure object literals are declared consistently (defaults to explicit definitions). -- Use static Response methods instead of new Response() constructor when possible. -- Make sure switch-case statements are exhaustive. -- Make sure the `preconnect` attribute is used when using Google Fonts. -- Use `Array#{indexOf,lastIndexOf}()` instead of `Array#{findIndex,findLastIndex}()` when looking for the index of an item. -- Make sure iterable callbacks return consistent values. -- Use `with { type: "json" }` for JSON module imports. -- Use numeric separators in numeric literals. -- Use object spread instead of `Object.assign()` when constructing new objects. -- Always use the radix argument when using `parseInt()`. -- Make sure JSDoc comment lines start with a single asterisk, except for the first one. -- Include a description parameter for `Symbol()`. -- Don't use spread (`...`) syntax on accumulators. -- Don't use the `delete` operator. -- Don't access namespace imports dynamically. -- Don't use namespace imports. -- Declare regex literals at the top level. -- Don't use `target="_blank"` without `rel="noopener"`. - -### TypeScript Best Practices -- Don't use TypeScript enums. -- Don't export imported variables. -- Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions. -- Don't use TypeScript namespaces. -- Don't use non-null assertions with the `!` postfix operator. -- Don't use parameter properties in class constructors. -- Don't use user-defined types. -- Use `as const` instead of literal types and type annotations. -- Use either `T[]` or `Array` consistently. -- Initialize each enum member value explicitly. -- Use `export type` for types. -- Use `import type` for types. -- Make sure all enum members are literal values. -- Don't use TypeScript const enum. -- Don't declare empty interfaces. -- Don't let variables evolve into any type through reassignments. -- Don't use the any type. -- Don't misuse the non-null assertion operator (!) in TypeScript files. -- Don't use implicit any type on variable declarations. -- Don't merge interfaces and classes unsafely. -- Don't use overload signatures that aren't next to each other. -- Use the namespace keyword instead of the module keyword to declare TypeScript namespaces. - -### Style and Consistency -- Don't use global `eval()`. -- Don't use callbacks in asynchronous tests and hooks. -- Don't use negation in `if` statements that have `else` clauses. -- Don't use nested ternary expressions. -- Don't reassign function parameters. -- This rule lets you specify global variable names you don't want to use in your application. -- Don't use specified modules when loaded by import or require. -- Don't use constants whose value is the upper-case version of their name. -- Use `String.slice()` instead of `String.substr()` and `String.substring()`. -- Don't use template literals if you don't need interpolation or special-character handling. -- Don't use `else` blocks when the `if` block breaks early. -- Don't use yoda expressions. -- Don't use Array constructors. -- Use `at()` instead of integer index access. -- Follow curly brace conventions. -- Use `else if` instead of nested `if` statements in `else` clauses. -- Use single `if` statements instead of nested `if` clauses. -- Use `new` for all builtins except `String`, `Number`, and `Boolean`. -- Use consistent accessibility modifiers on class properties and methods. -- Use `const` declarations for variables that are only assigned once. -- Put default function parameters and optional function parameters last. -- Include a `default` clause in switch statements. -- Use the `**` operator instead of `Math.pow`. -- Use `for-of` loops when you need the index to extract an item from the iterated array. -- Use `node:assert/strict` over `node:assert`. -- Use the `node:` protocol for Node.js builtin modules. -- Use Number properties instead of global ones. -- Use assignment operator shorthand where possible. -- Use function types instead of object types with call signatures. -- Use template literals over string concatenation. -- Use `new` when throwing an error. -- Don't throw non-Error values. -- Use `String.trimStart()` and `String.trimEnd()` over `String.trimLeft()` and `String.trimRight()`. -- Use standard constants instead of approximated literals. -- Don't assign values in expressions. -- Don't use async functions as Promise executors. -- Don't reassign exceptions in catch clauses. -- Don't reassign class members. -- Don't compare against -0. -- Don't use labeled statements that aren't loops. -- Don't use void type outside of generic or return types. -- Don't use console. -- Don't use control characters and escape sequences that match control characters in regular expression literals. -- Don't use debugger. -- Don't assign directly to document.cookie. -- Use `===` and `!==`. -- Don't use duplicate case labels. -- Don't use duplicate class members. -- Don't use duplicate conditions in if-else-if chains. -- Don't use two keys with the same name inside objects. -- Don't use duplicate function parameter names. -- Don't have duplicate hooks in describe blocks. -- Don't use empty block statements and static blocks. -- Don't let switch clauses fall through. -- Don't reassign function declarations. -- Don't allow assignments to native objects and read-only global variables. -- Use Number.isFinite instead of global isFinite. -- Use Number.isNaN instead of global isNaN. -- Don't assign to imported bindings. -- Don't use irregular whitespace characters. -- Don't use labels that share a name with a variable. -- Don't use characters made with multiple code points in character class syntax. -- Make sure to use new and constructor properly. -- Don't use shorthand assign when the variable appears on both sides. -- Don't use octal escape sequences in string literals. -- Don't use Object.prototype builtins directly. -- Don't redeclare variables, functions, classes, and types in the same scope. -- Don't have redundant "use strict". -- Don't compare things where both sides are exactly the same. -- Don't let identifiers shadow restricted names. -- Don't use sparse arrays (arrays with holes). -- Don't use template literal placeholder syntax in regular strings. -- Don't use the then property. -- Don't use unsafe negation. -- Don't use var. -- Don't use with statements in non-strict contexts. -- Make sure async functions actually use await. -- Make sure default clauses in switch statements come last. -- Make sure to pass a message value when creating a built-in error. -- Make sure get methods always return a value. -- Use a recommended display strategy with Google Fonts. -- Make sure for-in loops include an if statement. -- Use Array.isArray() instead of instanceof Array. -- Make sure to use the digits argument with Number#toFixed(). -- Make sure to use the "use strict" directive in script files. - -### Next.js Specific Rules -- Don't use `` elements in Next.js projects. -- Don't use `` elements in Next.js projects. -- Don't import next/document outside of pages/_document.jsx in Next.js projects. -- Don't use the next/head module in pages/_document.js on Next.js projects. - -### Testing Best Practices -- Don't use export or module.exports in test files. -- Don't use focused tests. -- Make sure the assertion function, like expect, is placed inside an it() function call. -- Don't use disabled tests. - -## Common Tasks -- `npx ultracite init` - Initialize Ultracite in your project -- `npx ultracite fix` - Format and fix code automatically -- `npx ultracite check` - Check for issues without fixing - -## Example: Error Handling -```typescript -// ✅ Good: Comprehensive error handling -try { - const result = await fetchData(); - return { success: true, data: result }; -} catch (error) { - console.error('API call failed:', error); - return { success: false, error: error.message }; -} - -// ❌ Bad: Swallowing errors -try { - return await fetchData(); -} catch (e) { - console.log(e); -} -``` \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3e386d5..fb5a7ca 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,35 +1,17 @@ { - "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, "[javascript]": { "editor.defaultFormatter": "biomejs.biome" }, "[typescript]": { "editor.defaultFormatter": "biomejs.biome" }, - "[javascriptreact]": { - "editor.defaultFormatter": "biomejs.biome" - }, "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, - "[json]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[jsonc]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[css]": { - "editor.defaultFormatter": "biomejs.biome" - }, - "[graphql]": { - "editor.defaultFormatter": "biomejs.biome" - }, "typescript.tsdk": "node_modules/typescript/lib", - "editor.formatOnSave": true, - "editor.formatOnPaste": true, - "emmet.showExpandedAbbreviation": "never", - "editor.codeActionsOnSave": { - "source.fixAll.biome": "explicit", - "source.organizeImports.biome": "explicit" - } + "eslint.workingDirectories": [ + { "pattern": "app/*" }, + { "pattern": "packages/*" } + ] } diff --git a/app/(auth)/actions.ts b/app/(auth)/actions.ts index 024ff51..84f8ffd 100644 --- a/app/(auth)/actions.ts +++ b/app/(auth)/actions.ts @@ -1,84 +1,84 @@ -"use server"; +'use server'; -import { z } from "zod"; +import { z } from 'zod'; -import { createUser, getUser } from "@/lib/db/queries"; +import { createUser, getUser } from '@/lib/db/queries'; -import { signIn } from "./auth"; +import { signIn } from './auth'; const authFormSchema = z.object({ email: z.string().email(), password: z.string().min(6), }); -export type LoginActionState = { - status: "idle" | "in_progress" | "success" | "failed" | "invalid_data"; -}; +export interface LoginActionState { + status: 'idle' | 'in_progress' | 'success' | 'failed' | 'invalid_data'; +} export const login = async ( _: LoginActionState, - formData: FormData + formData: FormData, ): Promise => { try { const validatedData = authFormSchema.parse({ - email: formData.get("email"), - password: formData.get("password"), + email: formData.get('email'), + password: formData.get('password'), }); - await signIn("credentials", { + await signIn('credentials', { email: validatedData.email, password: validatedData.password, redirect: false, }); - return { status: "success" }; + return { status: 'success' }; } catch (error) { if (error instanceof z.ZodError) { - return { status: "invalid_data" }; + return { status: 'invalid_data' }; } - return { status: "failed" }; + return { status: 'failed' }; } }; -export type RegisterActionState = { +export interface RegisterActionState { status: - | "idle" - | "in_progress" - | "success" - | "failed" - | "user_exists" - | "invalid_data"; -}; + | 'idle' + | 'in_progress' + | 'success' + | 'failed' + | 'user_exists' + | 'invalid_data'; +} export const register = async ( _: RegisterActionState, - formData: FormData + formData: FormData, ): Promise => { try { const validatedData = authFormSchema.parse({ - email: formData.get("email"), - password: formData.get("password"), + email: formData.get('email'), + password: formData.get('password'), }); const [user] = await getUser(validatedData.email); if (user) { - return { status: "user_exists" } as RegisterActionState; + return { status: 'user_exists' } as RegisterActionState; } await createUser(validatedData.email, validatedData.password); - await signIn("credentials", { + await signIn('credentials', { email: validatedData.email, password: validatedData.password, redirect: false, }); - return { status: "success" }; + return { status: 'success' }; } catch (error) { if (error instanceof z.ZodError) { - return { status: "invalid_data" }; + return { status: 'invalid_data' }; } - return { status: "failed" }; + return { status: 'failed' }; } }; diff --git a/app/(auth)/api/auth/[...nextauth]/route.ts b/app/(auth)/api/auth/[...nextauth]/route.ts index 588ff6a..ba24234 100644 --- a/app/(auth)/api/auth/[...nextauth]/route.ts +++ b/app/(auth)/api/auth/[...nextauth]/route.ts @@ -1,2 +1 @@ -// biome-ignore lint/performance/noBarrelFile: "Required" -export { GET, POST } from "@/app/(auth)/auth"; +export { GET, POST } from '@/app/(auth)/auth'; diff --git a/app/(auth)/api/auth/guest/route.ts b/app/(auth)/api/auth/guest/route.ts index dca565c..25af1fa 100644 --- a/app/(auth)/api/auth/guest/route.ts +++ b/app/(auth)/api/auth/guest/route.ts @@ -1,11 +1,11 @@ -import { NextResponse } from "next/server"; -import { getToken } from "next-auth/jwt"; -import { signIn } from "@/app/(auth)/auth"; -import { isDevelopmentEnvironment } from "@/lib/constants"; +import { signIn } from '@/app/(auth)/auth'; +import { isDevelopmentEnvironment } from '@/lib/constants'; +import { getToken } from 'next-auth/jwt'; +import { NextResponse } from 'next/server'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const redirectUrl = searchParams.get("redirectUrl") || "/"; + const redirectUrl = searchParams.get('redirectUrl') || '/'; const token = await getToken({ req: request, @@ -14,8 +14,8 @@ export async function GET(request: Request) { }); if (token) { - return NextResponse.redirect(new URL("/", request.url)); + return NextResponse.redirect(new URL('/', request.url)); } - return signIn("guest", { redirect: true, redirectTo: redirectUrl }); + return signIn('guest', { redirect: true, redirectTo: redirectUrl }); } diff --git a/app/(auth)/auth.config.ts b/app/(auth)/auth.config.ts index b8bc9e1..b7d7d50 100644 --- a/app/(auth)/auth.config.ts +++ b/app/(auth)/auth.config.ts @@ -1,9 +1,9 @@ -import type { NextAuthConfig } from "next-auth"; +import type { NextAuthConfig } from 'next-auth'; export const authConfig = { pages: { - signIn: "/login", - newUser: "/", + signIn: '/login', + newUser: '/', }, providers: [ // added later in auth.ts since it requires bcrypt which is only compatible with Node.js diff --git a/app/(auth)/auth.ts b/app/(auth)/auth.ts index dbebb1d..9185248 100644 --- a/app/(auth)/auth.ts +++ b/app/(auth)/auth.ts @@ -1,22 +1,21 @@ -import { compare } from "bcrypt-ts"; -import NextAuth, { type DefaultSession } from "next-auth"; -import type { DefaultJWT } from "next-auth/jwt"; -import Credentials from "next-auth/providers/credentials"; -import { DUMMY_PASSWORD } from "@/lib/constants"; -import { createGuestUser, getUser } from "@/lib/db/queries"; -import { authConfig } from "./auth.config"; +import { compare } from 'bcrypt-ts'; +import NextAuth, { type DefaultSession } from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; +import { createGuestUser, getUser } from '@/lib/db/queries'; +import { authConfig } from './auth.config'; +import { DUMMY_PASSWORD } from '@/lib/constants'; +import type { DefaultJWT } from 'next-auth/jwt'; -export type UserType = "guest" | "regular"; +export type UserType = 'guest' | 'regular'; -declare module "next-auth" { +declare module 'next-auth' { interface Session extends DefaultSession { user: { id: string; type: UserType; - } & DefaultSession["user"]; + } & DefaultSession['user']; } - // biome-ignore lint/nursery/useConsistentTypeDefinitions: "Required" interface User { id?: string; email?: string | null; @@ -24,7 +23,7 @@ declare module "next-auth" { } } -declare module "next-auth/jwt" { +declare module 'next-auth/jwt' { interface JWT extends DefaultJWT { id: string; type: UserType; @@ -58,24 +57,22 @@ export const { const passwordsMatch = await compare(password, user.password); - if (!passwordsMatch) { - return null; - } + if (!passwordsMatch) return null; - return { ...user, type: "regular" }; + return { ...user, type: 'regular' }; }, }), Credentials({ - id: "guest", + id: 'guest', credentials: {}, async authorize() { const [guestUser] = await createGuestUser(); - return { ...guestUser, type: "guest" }; + return { ...guestUser, type: 'guest' }; }, }), ], callbacks: { - jwt({ token, user }) { + async jwt({ token, user }) { if (user) { token.id = user.id as string; token.type = user.type; @@ -83,7 +80,7 @@ export const { return token; }, - session({ session, token }) { + async session({ session, token }) { if (session.user) { session.user.id = token.id; session.user.type = token.type; diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 1140cbe..cd670ee 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -1,51 +1,52 @@ -"use client"; +'use client'; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; -import { useActionState, useEffect, useState } from "react"; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useActionState, useEffect, useState } from 'react'; +import { toast } from '@/components/toast'; -import { AuthForm } from "@/components/auth-form"; -import { SubmitButton } from "@/components/submit-button"; -import { toast } from "@/components/toast"; -import { type LoginActionState, login } from "../actions"; +import { AuthForm } from '@/components/auth-form'; +import { SubmitButton } from '@/components/submit-button'; + +import { login, type LoginActionState } from '../actions'; +import { useSession } from 'next-auth/react'; export default function Page() { const router = useRouter(); - const [email, setEmail] = useState(""); + const [email, setEmail] = useState(''); const [isSuccessful, setIsSuccessful] = useState(false); const [state, formAction] = useActionState( login, { - status: "idle", - } + status: 'idle', + }, ); const { update: updateSession } = useSession(); useEffect(() => { - if (state.status === "failed") { + if (state.status === 'failed') { toast({ - type: "error", - description: "Invalid credentials!", + type: 'error', + description: 'Invalid credentials!', }); - } else if (state.status === "invalid_data") { + } else if (state.status === 'invalid_data') { toast({ - type: "error", - description: "Failed validating your submission!", + type: 'error', + description: 'Failed validating your submission!', }); - } else if (state.status === "success") { + } else if (state.status === 'success') { setIsSuccessful(true); updateSession(); router.refresh(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.status, router.refresh, updateSession]); + }, [state.status]); const handleSubmit = (formData: FormData) => { - setEmail(formData.get("email") as string); + setEmail(formData.get('email') as string); formAction(formData); }; @@ -63,12 +64,12 @@ export default function Page() {

{"Don't have an account? "} Sign up - {" for free."} + {' for free.'}

diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index 950f67b..74b4ff6 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -1,51 +1,53 @@ -"use client"; +'use client'; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { useSession } from "next-auth/react"; -import { useActionState, useEffect, useState } from "react"; -import { AuthForm } from "@/components/auth-form"; -import { SubmitButton } from "@/components/submit-button"; -import { toast } from "@/components/toast"; -import { type RegisterActionState, register } from "../actions"; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useActionState, useEffect, useState } from 'react'; + +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 [email, setEmail] = useState(''); const [isSuccessful, setIsSuccessful] = useState(false); const [state, formAction] = useActionState( register, { - status: "idle", - } + 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") { + 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!", + type: 'error', + description: 'Failed validating your submission!', }); - } else if (state.status === "success") { - toast({ type: "success", description: "Account created successfully!" }); + } else if (state.status === 'success') { + toast({ type: 'success', description: 'Account created successfully!' }); setIsSuccessful(true); updateSession(); router.refresh(); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [state.status, router.refresh, updateSession]); + }, [state.status]); const handleSubmit = (formData: FormData) => { - setEmail(formData.get("email") as string); + setEmail(formData.get('email') as string); formAction(formData); }; @@ -61,14 +63,14 @@ export default function Page() { Sign Up

- {"Already have an account? "} + {'Already have an account? '} Sign in - {" instead."} + {' instead.'}

diff --git a/app/(chat)/actions.ts b/app/(chat)/actions.ts index 4e8a3ee..14ee7dc 100644 --- a/app/(chat)/actions.ts +++ b/app/(chat)/actions.ts @@ -1,18 +1,18 @@ -"use server"; +'use server'; -import { generateText, type UIMessage } from "ai"; -import { cookies } from "next/headers"; -import type { VisibilityType } from "@/components/visibility-selector"; -import { myProvider } from "@/lib/ai/providers"; +import { generateText, type UIMessage } from 'ai'; +import { cookies } from 'next/headers'; import { deleteMessagesByChatIdAfterTimestamp, getMessageById, updateChatVisiblityById, -} from "@/lib/db/queries"; +} from '@/lib/db/queries'; +import type { VisibilityType } from '@/components/visibility-selector'; +import { myProvider } from '@/lib/ai/providers'; export async function saveChatModelAsCookie(model: string) { const cookieStore = await cookies(); - cookieStore.set("chat-model", model); + cookieStore.set('chat-model', model); } export async function generateTitleFromUserMessage({ @@ -21,7 +21,7 @@ export async function generateTitleFromUserMessage({ message: UIMessage; }) { const { text: title } = await generateText({ - model: myProvider.languageModel("title-model"), + model: myProvider.languageModel('title-model'), system: `\n - you will generate a short title based on the first message a user begins a conversation with - ensure it is not more than 80 characters long diff --git a/app/(chat)/api/chat/[id]/stream/route.ts b/app/(chat)/api/chat/[id]/stream/route.ts index 48352e9..58b4643 100644 --- a/app/(chat)/api/chat/[id]/stream/route.ts +++ b/app/(chat)/api/chat/[id]/stream/route.ts @@ -1,19 +1,19 @@ -import { createUIMessageStream, JsonToSseTransformStream } from "ai"; -import { differenceInSeconds } from "date-fns"; -import { auth } from "@/app/(auth)/auth"; +import { auth } from '@/app/(auth)/auth'; import { getChatById, getMessagesByChatId, getStreamIdsByChatId, -} from "@/lib/db/queries"; -import type { Chat } from "@/lib/db/schema"; -import { ChatSDKError } from "@/lib/errors"; -import type { ChatMessage } from "@/lib/types"; -import { getStreamContext } from "../../route"; +} from '@/lib/db/queries'; +import type { Chat } from '@/lib/db/schema'; +import { ChatSDKError } from '@/lib/errors'; +import type { ChatMessage } from '@/lib/types'; +import { createUIMessageStream, JsonToSseTransformStream } from 'ai'; +import { getStreamContext } from '../../route'; +import { differenceInSeconds } from 'date-fns'; export async function GET( _: Request, - { params }: { params: Promise<{ id: string }> } + { params }: { params: Promise<{ id: string }> }, ) { const { id: chatId } = await params; @@ -25,13 +25,13 @@ export async function GET( } if (!chatId) { - return new ChatSDKError("bad_request:api").toResponse(); + return new ChatSDKError('bad_request:api').toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError("unauthorized:chat").toResponse(); + return new ChatSDKError('unauthorized:chat').toResponse(); } let chat: Chat | null; @@ -39,36 +39,35 @@ export async function GET( try { chat = await getChatById({ id: chatId }); } catch { - return new ChatSDKError("not_found:chat").toResponse(); + return new ChatSDKError('not_found:chat').toResponse(); } if (!chat) { - return new ChatSDKError("not_found:chat").toResponse(); + return new ChatSDKError('not_found:chat').toResponse(); } - if (chat.visibility === "private" && chat.userId !== session.user.id) { - return new ChatSDKError("forbidden:chat").toResponse(); + if (chat.visibility === 'private' && chat.userId !== session.user.id) { + return new ChatSDKError('forbidden:chat').toResponse(); } const streamIds = await getStreamIdsByChatId({ chatId }); if (!streamIds.length) { - return new ChatSDKError("not_found:stream").toResponse(); + return new ChatSDKError('not_found:stream').toResponse(); } const recentStreamId = streamIds.at(-1); if (!recentStreamId) { - return new ChatSDKError("not_found:stream").toResponse(); + return new ChatSDKError('not_found:stream').toResponse(); } const emptyDataStream = createUIMessageStream({ - // biome-ignore lint/suspicious/noEmptyBlockStatements: "Needs to exist" execute: () => {}, }); const stream = await streamContext.resumableStream(recentStreamId, () => - emptyDataStream.pipeThrough(new JsonToSseTransformStream()) + emptyDataStream.pipeThrough(new JsonToSseTransformStream()), ); /* @@ -83,7 +82,7 @@ export async function GET( return new Response(emptyDataStream, { status: 200 }); } - if (mostRecentMessage.role !== "assistant") { + if (mostRecentMessage.role !== 'assistant') { return new Response(emptyDataStream, { status: 200 }); } @@ -96,7 +95,7 @@ export async function GET( const restoredStream = createUIMessageStream({ execute: ({ writer }) => { writer.write({ - type: "data-appendMessage", + type: 'data-appendMessage', data: JSON.stringify(mostRecentMessage), transient: true, }); @@ -105,7 +104,7 @@ export async function GET( return new Response( restoredStream.pipeThrough(new JsonToSseTransformStream()), - { status: 200 } + { status: 200 }, ); } diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 238fe84..7a62f5a 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -1,4 +1,3 @@ -import { geolocation } from "@vercel/functions"; import { convertToModelMessages, createUIMessageStream, @@ -6,27 +5,9 @@ import { smoothStream, stepCountIs, streamText, -} from "ai"; -import { unstable_cache as cache } from "next/cache"; -import { after } from "next/server"; -import { - createResumableStreamContext, - type ResumableStreamContext, -} from "resumable-stream"; -import type { ModelCatalog } from "tokenlens/core"; -import { fetchModels } from "tokenlens/fetch"; -import { getUsage } from "tokenlens/helpers"; -import { auth, type UserType } from "@/app/(auth)/auth"; -import type { VisibilityType } from "@/components/visibility-selector"; -import { entitlementsByUserType } from "@/lib/ai/entitlements"; -import type { ChatModel } from "@/lib/ai/models"; -import { type RequestHints, systemPrompt } from "@/lib/ai/prompts"; -import { myProvider } from "@/lib/ai/providers"; -import { createDocument } from "@/lib/ai/tools/create-document"; -import { getWeather } from "@/lib/ai/tools/get-weather"; -import { requestSuggestions } from "@/lib/ai/tools/request-suggestions"; -import { updateDocument } from "@/lib/ai/tools/update-document"; -import { isProductionEnvironment } from "@/lib/constants"; +} from 'ai'; +import { auth, type UserType } from '@/app/(auth)/auth'; +import { type RequestHints, systemPrompt } from '@/lib/ai/prompts'; import { createStreamId, deleteChatById, @@ -35,14 +16,33 @@ import { getMessagesByChatId, saveChat, saveMessages, - updateChatLastContextById, -} from "@/lib/db/queries"; -import { ChatSDKError } from "@/lib/errors"; -import type { ChatMessage } from "@/lib/types"; -import type { AppUsage } from "@/lib/usage"; -import { convertToUIMessages, generateUUID } from "@/lib/utils"; -import { generateTitleFromUserMessage } from "../../actions"; -import { type PostRequestBody, postRequestBodySchema } from "./schema"; +} from '@/lib/db/queries'; +import { updateChatLastContextById } from '@/lib/db/queries'; +import { convertToUIMessages, generateUUID } from '@/lib/utils'; +import { generateTitleFromUserMessage } from '../../actions'; +import { createDocument } from '@/lib/ai/tools/create-document'; +import { updateDocument } from '@/lib/ai/tools/update-document'; +import { requestSuggestions } from '@/lib/ai/tools/request-suggestions'; +import { getWeather } from '@/lib/ai/tools/get-weather'; +import { isProductionEnvironment } from '@/lib/constants'; +import { myProvider } from '@/lib/ai/providers'; +import { entitlementsByUserType } from '@/lib/ai/entitlements'; +import { postRequestBodySchema, type PostRequestBody } from './schema'; +import { geolocation } from '@vercel/functions'; +import { + createResumableStreamContext, + type ResumableStreamContext, +} from 'resumable-stream'; +import { after } from 'next/server'; +import { ChatSDKError } from '@/lib/errors'; +import type { ChatMessage } from '@/lib/types'; +import type { ChatModel } from '@/lib/ai/models'; +import type { VisibilityType } from '@/components/visibility-selector'; +import { unstable_cache as cache } from 'next/cache'; +import { fetchModels } from 'tokenlens/fetch'; +import { getUsage } from 'tokenlens/helpers'; +import type { ModelCatalog } from 'tokenlens/core'; +import type { AppUsage } from '@/lib/usage'; export const maxDuration = 60; @@ -54,14 +54,14 @@ const getTokenlensCatalog = cache( return await fetchModels(); } catch (err) { console.warn( - "TokenLens: catalog fetch failed, using default catalog", - err + 'TokenLens: catalog fetch failed, using default catalog', + err, ); - return; // tokenlens helpers will fall back to defaultCatalog + return undefined; // tokenlens helpers will fall back to defaultCatalog } }, - ["tokenlens-catalog"], - { revalidate: 24 * 60 * 60 } // 24 hours + ['tokenlens-catalog'], + { revalidate: 24 * 60 * 60 }, // 24 hours ); export function getStreamContext() { @@ -71,9 +71,9 @@ export function getStreamContext() { waitUntil: after, }); } catch (error: any) { - if (error.message.includes("REDIS_URL")) { + if (error.message.includes('REDIS_URL')) { console.log( - " > Resumable streams are disabled due to missing REDIS_URL" + ' > Resumable streams are disabled due to missing REDIS_URL', ); } else { console.error(error); @@ -91,7 +91,7 @@ export async function POST(request: Request) { const json = await request.json(); requestBody = postRequestBodySchema.parse(json); } catch (_) { - return new ChatSDKError("bad_request:api").toResponse(); + return new ChatSDKError('bad_request:api').toResponse(); } try { @@ -103,14 +103,14 @@ export async function POST(request: Request) { }: { id: string; message: ChatMessage; - selectedChatModel: ChatModel["id"]; + selectedChatModel: ChatModel['id']; selectedVisibilityType: VisibilityType; } = requestBody; const session = await auth(); if (!session?.user) { - return new ChatSDKError("unauthorized:chat").toResponse(); + return new ChatSDKError('unauthorized:chat').toResponse(); } const userType: UserType = session.user.type; @@ -121,16 +121,12 @@ export async function POST(request: Request) { }); if (messageCount > entitlementsByUserType[userType].maxMessagesPerDay) { - return new ChatSDKError("rate_limit:chat").toResponse(); + return new ChatSDKError('rate_limit:chat').toResponse(); } const chat = await getChatById({ id }); - if (chat) { - if (chat.userId !== session.user.id) { - return new ChatSDKError("forbidden:chat").toResponse(); - } - } else { + if (!chat) { const title = await generateTitleFromUserMessage({ message, }); @@ -141,6 +137,10 @@ export async function POST(request: Request) { title, visibility: selectedVisibilityType, }); + } else { + if (chat.userId !== session.user.id) { + return new ChatSDKError('forbidden:chat').toResponse(); + } } const messagesFromDb = await getMessagesByChatId({ id }); @@ -160,7 +160,7 @@ export async function POST(request: Request) { { chatId: id, id: message.id, - role: "user", + role: 'user', parts: message.parts, attachments: [], createdAt: new Date(), @@ -181,15 +181,15 @@ export async function POST(request: Request) { messages: convertToModelMessages(uiMessages), stopWhen: stepCountIs(5), experimental_activeTools: - selectedChatModel === "chat-model-reasoning" + selectedChatModel === 'chat-model-reasoning' ? [] : [ - "getWeather", - "createDocument", - "updateDocument", - "requestSuggestions", + 'getWeather', + 'createDocument', + 'updateDocument', + 'requestSuggestions', ], - experimental_transform: smoothStream({ chunking: "word" }), + experimental_transform: smoothStream({ chunking: 'word' }), tools: { getWeather, createDocument: createDocument({ session, dataStream }), @@ -201,7 +201,7 @@ export async function POST(request: Request) { }, experimental_telemetry: { isEnabled: isProductionEnvironment, - functionId: "stream-text", + functionId: 'stream-text', }, onFinish: async ({ usage }) => { try { @@ -210,29 +210,23 @@ export async function POST(request: Request) { myProvider.languageModel(selectedChatModel).modelId; if (!modelId) { finalMergedUsage = usage; - dataStream.write({ - type: "data-usage", - data: finalMergedUsage, - }); + dataStream.write({ type: 'data-usage', data: finalMergedUsage }); return; } if (!providers) { finalMergedUsage = usage; - dataStream.write({ - type: "data-usage", - data: finalMergedUsage, - }); + dataStream.write({ type: 'data-usage', data: finalMergedUsage }); return; } const summary = getUsage({ modelId, usage, providers }); finalMergedUsage = { ...usage, ...summary, modelId } as AppUsage; - dataStream.write({ type: "data-usage", data: finalMergedUsage }); + dataStream.write({ type: 'data-usage', data: finalMergedUsage }); } catch (err) { - console.warn("TokenLens enrichment failed", err); + console.warn('TokenLens enrichment failed', err); finalMergedUsage = usage; - dataStream.write({ type: "data-usage", data: finalMergedUsage }); + dataStream.write({ type: 'data-usage', data: finalMergedUsage }); } }, }); @@ -242,16 +236,16 @@ export async function POST(request: Request) { dataStream.merge( result.toUIMessageStream({ sendReasoning: true, - }) + }), ); }, generateId: generateUUID, onFinish: async ({ messages }) => { await saveMessages({ - messages: messages.map((currentMessage) => ({ - id: currentMessage.id, - role: currentMessage.role, - parts: currentMessage.parts, + messages: messages.map((message) => ({ + id: message.id, + role: message.role, + parts: message.parts, createdAt: new Date(), attachments: [], chatId: id, @@ -265,12 +259,12 @@ export async function POST(request: Request) { context: finalMergedUsage, }); } catch (err) { - console.warn("Unable to persist last usage for chat", id, err); + console.warn('Unable to persist last usage for chat', id, err); } } }, onError: () => { - return "Oops, an error occurred!"; + return 'Oops, an error occurred!'; }, }); @@ -279,12 +273,12 @@ export async function POST(request: Request) { if (streamContext) { return new Response( await streamContext.resumableStream(streamId, () => - stream.pipeThrough(new JsonToSseTransformStream()) - ) + stream.pipeThrough(new JsonToSseTransformStream()), + ), ); + } else { + return new Response(stream.pipeThrough(new JsonToSseTransformStream())); } - - return new Response(stream.pipeThrough(new JsonToSseTransformStream())); } catch (error) { if (error instanceof ChatSDKError) { return error.toResponse(); @@ -294,35 +288,35 @@ export async function POST(request: Request) { if ( error instanceof Error && error.message?.includes( - "AI Gateway requires a valid credit card on file to service requests" + 'AI Gateway requires a valid credit card on file to service requests', ) ) { - return new ChatSDKError("bad_request:activate_gateway").toResponse(); + return new ChatSDKError('bad_request:activate_gateway').toResponse(); } - console.error("Unhandled error in chat API:", error); - return new ChatSDKError("offline:chat").toResponse(); + console.error('Unhandled error in chat API:', error); + return new ChatSDKError('offline:chat').toResponse(); } } export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); + const id = searchParams.get('id'); if (!id) { - return new ChatSDKError("bad_request:api").toResponse(); + return new ChatSDKError('bad_request:api').toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError("unauthorized:chat").toResponse(); + return new ChatSDKError('unauthorized:chat').toResponse(); } const chat = await getChatById({ id }); if (chat?.userId !== session.user.id) { - return new ChatSDKError("forbidden:chat").toResponse(); + return new ChatSDKError('forbidden:chat').toResponse(); } const deletedChat = await deleteChatById({ id }); diff --git a/app/(chat)/api/chat/schema.ts b/app/(chat)/api/chat/schema.ts index 090aa63..555ef8b 100644 --- a/app/(chat)/api/chat/schema.ts +++ b/app/(chat)/api/chat/schema.ts @@ -1,13 +1,13 @@ -import { z } from "zod"; +import { z } from 'zod'; const textPartSchema = z.object({ - type: z.enum(["text"]), + type: z.enum(['text']), text: z.string().min(1).max(2000), }); const filePartSchema = z.object({ - type: z.enum(["file"]), - mediaType: z.enum(["image/jpeg", "image/png"]), + type: z.enum(['file']), + mediaType: z.enum(['image/jpeg', 'image/png']), name: z.string().min(1).max(100), url: z.string().url(), }); @@ -18,11 +18,11 @@ export const postRequestBodySchema = z.object({ id: z.string().uuid(), message: z.object({ id: z.string().uuid(), - role: z.enum(["user"]), + role: z.enum(['user']), parts: z.array(partSchema), }), - selectedChatModel: z.enum(["chat-model", "chat-model-reasoning"]), - selectedVisibilityType: z.enum(["public", "private"]), + selectedChatModel: z.enum(['chat-model', 'chat-model-reasoning']), + selectedVisibilityType: z.enum(['public', 'private']), }); export type PostRequestBody = z.infer; diff --git a/app/(chat)/api/document/route.ts b/app/(chat)/api/document/route.ts index 0ea78ff..de41836 100644 --- a/app/(chat)/api/document/route.ts +++ b/app/(chat)/api/document/route.ts @@ -1,27 +1,27 @@ -import { auth } from "@/app/(auth)/auth"; -import type { ArtifactKind } from "@/components/artifact"; +import { auth } from '@/app/(auth)/auth'; +import type { ArtifactKind } from '@/components/artifact'; import { deleteDocumentsByIdAfterTimestamp, getDocumentsById, saveDocument, -} from "@/lib/db/queries"; -import { ChatSDKError } from "@/lib/errors"; +} from '@/lib/db/queries'; +import { ChatSDKError } from '@/lib/errors'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); + const id = searchParams.get('id'); if (!id) { return new ChatSDKError( - "bad_request:api", - "Parameter id is missing" + 'bad_request:api', + 'Parameter id is missing', ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError("unauthorized:document").toResponse(); + return new ChatSDKError('unauthorized:document').toResponse(); } const documents = await getDocumentsById({ id }); @@ -29,11 +29,11 @@ export async function GET(request: Request) { const [document] = documents; if (!document) { - return new ChatSDKError("not_found:document").toResponse(); + return new ChatSDKError('not_found:document').toResponse(); } if (document.userId !== session.user.id) { - return new ChatSDKError("forbidden:document").toResponse(); + return new ChatSDKError('forbidden:document').toResponse(); } return Response.json(documents, { status: 200 }); @@ -41,19 +41,19 @@ export async function GET(request: Request) { export async function POST(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); + const id = searchParams.get('id'); if (!id) { return new ChatSDKError( - "bad_request:api", - "Parameter id is required." + 'bad_request:api', + 'Parameter id is required.', ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError("not_found:document").toResponse(); + return new ChatSDKError('not_found:document').toResponse(); } const { @@ -66,10 +66,10 @@ export async function POST(request: Request) { const documents = await getDocumentsById({ id }); if (documents.length > 0) { - const [doc] = documents; + const [document] = documents; - if (doc.userId !== session.user.id) { - return new ChatSDKError("forbidden:document").toResponse(); + if (document.userId !== session.user.id) { + return new ChatSDKError('forbidden:document').toResponse(); } } @@ -86,27 +86,27 @@ export async function POST(request: Request) { export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); - const id = searchParams.get("id"); - const timestamp = searchParams.get("timestamp"); + const id = searchParams.get('id'); + const timestamp = searchParams.get('timestamp'); if (!id) { return new ChatSDKError( - "bad_request:api", - "Parameter id is required." + 'bad_request:api', + 'Parameter id is required.', ).toResponse(); } if (!timestamp) { return new ChatSDKError( - "bad_request:api", - "Parameter timestamp is required." + 'bad_request:api', + 'Parameter timestamp is required.', ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError("unauthorized:document").toResponse(); + return new ChatSDKError('unauthorized:document').toResponse(); } const documents = await getDocumentsById({ id }); @@ -114,7 +114,7 @@ export async function DELETE(request: Request) { const [document] = documents; if (document.userId !== session.user.id) { - return new ChatSDKError("forbidden:document").toResponse(); + return new ChatSDKError('forbidden:document').toResponse(); } const documentsDeleted = await deleteDocumentsByIdAfterTimestamp({ diff --git a/app/(chat)/api/files/upload/route.ts b/app/(chat)/api/files/upload/route.ts index 4e4e4f3..699a4cb 100644 --- a/app/(chat)/api/files/upload/route.ts +++ b/app/(chat)/api/files/upload/route.ts @@ -1,19 +1,19 @@ -import { put } from "@vercel/blob"; -import { NextResponse } from "next/server"; -import { z } from "zod"; +import { put } from '@vercel/blob'; +import { NextResponse } from 'next/server'; +import { z } from 'zod'; -import { auth } from "@/app/(auth)/auth"; +import { auth } from '@/app/(auth)/auth'; // Use Blob instead of File since File is not available in Node.js environment const FileSchema = z.object({ file: z .instanceof(Blob) .refine((file) => file.size <= 5 * 1024 * 1024, { - message: "File size should be less than 5MB", + message: '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", + .refine((file) => ['image/jpeg', 'image/png'].includes(file.type), { + message: 'File type should be JPEG or PNG', }), }); @@ -21,19 +21,19 @@ export async function POST(request: Request) { const session = await auth(); if (!session) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } if (request.body === null) { - return new Response("Request body is empty", { status: 400 }); + return new Response('Request body is empty', { status: 400 }); } try { const formData = await request.formData(); - const file = formData.get("file") as Blob; + const file = formData.get('file') as Blob; if (!file) { - return NextResponse.json({ error: "No file uploaded" }, { status: 400 }); + return NextResponse.json({ error: 'No file uploaded' }, { status: 400 }); } const validatedFile = FileSchema.safeParse({ file }); @@ -41,28 +41,28 @@ export async function POST(request: Request) { if (!validatedFile.success) { const errorMessage = validatedFile.error.errors .map((error) => error.message) - .join(", "); + .join(', '); return NextResponse.json({ error: errorMessage }, { status: 400 }); } // Get filename from formData since Blob doesn't have name property - const filename = (formData.get("file") as File).name; + const filename = (formData.get('file') as File).name; const fileBuffer = await file.arrayBuffer(); try { const data = await put(`${filename}`, fileBuffer, { - access: "public", + access: 'public', }); return NextResponse.json(data); - } catch (_error) { - return NextResponse.json({ error: "Upload failed" }, { status: 500 }); + } catch (error) { + return NextResponse.json({ error: 'Upload failed' }, { status: 500 }); } - } catch (_error) { + } catch (error) { return NextResponse.json( - { error: "Failed to process request" }, - { status: 500 } + { error: 'Failed to process request' }, + { status: 500 }, ); } } diff --git a/app/(chat)/api/history/route.ts b/app/(chat)/api/history/route.ts index 412f10f..e28290c 100644 --- a/app/(chat)/api/history/route.ts +++ b/app/(chat)/api/history/route.ts @@ -1,26 +1,26 @@ -import type { NextRequest } from "next/server"; -import { auth } from "@/app/(auth)/auth"; -import { getChatsByUserId } from "@/lib/db/queries"; -import { ChatSDKError } from "@/lib/errors"; +import { auth } from '@/app/(auth)/auth'; +import type { NextRequest } from 'next/server'; +import { getChatsByUserId } from '@/lib/db/queries'; +import { ChatSDKError } from '@/lib/errors'; export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl; - const limit = Number.parseInt(searchParams.get("limit") || "10", 10); - const startingAfter = searchParams.get("starting_after"); - const endingBefore = searchParams.get("ending_before"); + const limit = Number.parseInt(searchParams.get('limit') || '10'); + const startingAfter = searchParams.get('starting_after'); + const endingBefore = searchParams.get('ending_before'); if (startingAfter && endingBefore) { return new ChatSDKError( - "bad_request:api", - "Only one of starting_after or ending_before can be provided." + 'bad_request:api', + 'Only one of starting_after or ending_before can be provided.', ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError("unauthorized:chat").toResponse(); + return new ChatSDKError('unauthorized:chat').toResponse(); } const chats = await getChatsByUserId({ diff --git a/app/(chat)/api/suggestions/route.ts b/app/(chat)/api/suggestions/route.ts index 8801004..ffbda2f 100644 --- a/app/(chat)/api/suggestions/route.ts +++ b/app/(chat)/api/suggestions/route.ts @@ -1,22 +1,22 @@ -import { auth } from "@/app/(auth)/auth"; -import { getSuggestionsByDocumentId } from "@/lib/db/queries"; -import { ChatSDKError } from "@/lib/errors"; +import { auth } from '@/app/(auth)/auth'; +import { getSuggestionsByDocumentId } from '@/lib/db/queries'; +import { ChatSDKError } from '@/lib/errors'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const documentId = searchParams.get("documentId"); + const documentId = searchParams.get('documentId'); if (!documentId) { return new ChatSDKError( - "bad_request:api", - "Parameter documentId is required." + 'bad_request:api', + 'Parameter documentId is required.', ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError("unauthorized:suggestions").toResponse(); + return new ChatSDKError('unauthorized:suggestions').toResponse(); } const suggestions = await getSuggestionsByDocumentId({ @@ -30,7 +30,7 @@ export async function GET(request: Request) { } if (suggestion.userId !== session.user.id) { - return new ChatSDKError("forbidden:api").toResponse(); + return new ChatSDKError('forbidden:api').toResponse(); } return Response.json(suggestions, { status: 200 }); diff --git a/app/(chat)/api/vote/route.ts b/app/(chat)/api/vote/route.ts index 2c0ce3f..b3d7e45 100644 --- a/app/(chat)/api/vote/route.ts +++ b/app/(chat)/api/vote/route.ts @@ -1,32 +1,32 @@ -import { auth } from "@/app/(auth)/auth"; -import { getChatById, getVotesByChatId, voteMessage } from "@/lib/db/queries"; -import { ChatSDKError } from "@/lib/errors"; +import { auth } from '@/app/(auth)/auth'; +import { getChatById, getVotesByChatId, voteMessage } from '@/lib/db/queries'; +import { ChatSDKError } from '@/lib/errors'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const chatId = searchParams.get("chatId"); + const chatId = searchParams.get('chatId'); if (!chatId) { return new ChatSDKError( - "bad_request:api", - "Parameter chatId is required." + 'bad_request:api', + 'Parameter chatId is required.', ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError("unauthorized:vote").toResponse(); + return new ChatSDKError('unauthorized:vote').toResponse(); } const chat = await getChatById({ id: chatId }); if (!chat) { - return new ChatSDKError("not_found:chat").toResponse(); + return new ChatSDKError('not_found:chat').toResponse(); } if (chat.userId !== session.user.id) { - return new ChatSDKError("forbidden:vote").toResponse(); + return new ChatSDKError('forbidden:vote').toResponse(); } const votes = await getVotesByChatId({ id: chatId }); @@ -39,37 +39,37 @@ export async function PATCH(request: Request) { chatId, messageId, type, - }: { chatId: string; messageId: string; type: "up" | "down" } = + }: { chatId: string; messageId: string; type: 'up' | 'down' } = await request.json(); if (!chatId || !messageId || !type) { return new ChatSDKError( - "bad_request:api", - "Parameters chatId, messageId, and type are required." + 'bad_request:api', + 'Parameters chatId, messageId, and type are required.', ).toResponse(); } const session = await auth(); if (!session?.user) { - return new ChatSDKError("unauthorized:vote").toResponse(); + return new ChatSDKError('unauthorized:vote').toResponse(); } const chat = await getChatById({ id: chatId }); if (!chat) { - return new ChatSDKError("not_found:vote").toResponse(); + return new ChatSDKError('not_found:vote').toResponse(); } if (chat.userId !== session.user.id) { - return new ChatSDKError("forbidden:vote").toResponse(); + return new ChatSDKError('forbidden:vote').toResponse(); } await voteMessage({ chatId, messageId, - type, + type: type, }); - return new Response("Message voted", { status: 200 }); + return new Response('Message voted', { status: 200 }); } diff --git a/app/(chat)/chat/[id]/page.tsx b/app/(chat)/chat/[id]/page.tsx index f208fac..cf3b71e 100644 --- a/app/(chat)/chat/[id]/page.tsx +++ b/app/(chat)/chat/[id]/page.tsx @@ -1,12 +1,12 @@ -import { cookies } from "next/headers"; -import { notFound, redirect } from "next/navigation"; +import { cookies } from 'next/headers'; +import { notFound, redirect } from 'next/navigation'; -import { auth } from "@/app/(auth)/auth"; -import { Chat } from "@/components/chat"; -import { DataStreamHandler } from "@/components/data-stream-handler"; -import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models"; -import { getChatById, getMessagesByChatId } from "@/lib/db/queries"; -import { convertToUIMessages } from "@/lib/utils"; +import { auth } from '@/app/(auth)/auth'; +import { Chat } from '@/components/chat'; +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'; export default async function Page(props: { params: Promise<{ id: string }> }) { const params = await props.params; @@ -20,10 +20,10 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { const session = await auth(); if (!session) { - redirect("/api/auth/guest"); + redirect('/api/auth/guest'); } - if (chat.visibility === "private") { + if (chat.visibility === 'private') { if (!session.user) { return notFound(); } @@ -40,19 +40,20 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { const uiMessages = convertToUIMessages(messagesFromDb); const cookieStore = await cookies(); - const chatModelFromCookie = cookieStore.get("chat-model"); + const chatModelFromCookie = cookieStore.get('chat-model'); if (!chatModelFromCookie) { return ( <> @@ -62,13 +63,14 @@ export default async function Page(props: { params: Promise<{ id: string }> }) { return ( <> diff --git a/app/(chat)/layout.tsx b/app/(chat)/layout.tsx index 6900400..dfd9da2 100644 --- a/app/(chat)/layout.tsx +++ b/app/(chat)/layout.tsx @@ -1,9 +1,10 @@ -import { cookies } from "next/headers"; -import Script from "next/script"; -import { AppSidebar } from "@/components/app-sidebar"; -import { DataStreamProvider } from "@/components/data-stream-provider"; -import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; -import { auth } from "../(auth)/auth"; +import { cookies } from 'next/headers'; + +import { AppSidebar } from '@/components/app-sidebar'; +import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; +import { auth } from '../(auth)/auth'; +import Script from 'next/script'; +import { DataStreamProvider } from '@/components/data-stream-provider'; export const experimental_ppr = true; @@ -13,7 +14,7 @@ export default async function Layout({ children: React.ReactNode; }) { 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/(chat)/page.tsx b/app/(chat)/page.tsx index 225d2ac..41328cb 100644 --- a/app/(chat)/page.tsx +++ b/app/(chat)/page.tsx @@ -1,34 +1,36 @@ -import { cookies } from "next/headers"; -import { redirect } from "next/navigation"; -import { Chat } from "@/components/chat"; -import { DataStreamHandler } from "@/components/data-stream-handler"; -import { DEFAULT_CHAT_MODEL } from "@/lib/ai/models"; -import { generateUUID } from "@/lib/utils"; -import { auth } from "../(auth)/auth"; +import { cookies } from 'next/headers'; + +import { Chat } from '@/components/chat'; +import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models'; +import { generateUUID } from '@/lib/utils'; +import { DataStreamHandler } from '@/components/data-stream-handler'; +import { auth } from '../(auth)/auth'; +import { redirect } from 'next/navigation'; export default async function Page() { const session = await auth(); if (!session) { - redirect("/api/auth/guest"); + redirect('/api/auth/guest'); } const id = generateUUID(); const cookieStore = await cookies(); - const modelIdFromCookie = cookieStore.get("chat-model"); + const modelIdFromCookie = cookieStore.get('chat-model'); if (!modelIdFromCookie) { return ( <> @@ -38,13 +40,14 @@ export default async function Page() { return ( <> diff --git a/app/globals.css b/app/globals.css index d0851ff..4be4d70 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,4 +1,5 @@ -@import "tailwindcss"; +/* set base path to root directory to include utility classes from app, components, pages, etc. */ +@import "tailwindcss" source(".."); /* include utility classes in streamdown */ @source '../node_modules/streamdown/dist/index.js'; @@ -46,7 +47,6 @@ --sidebar-ring: hsl(217.2 91.2% 59.8%); /* border radius unit */ --radius: 0.5rem; - --sidebar: hsl(0 0% 98%); } /* define design tokens (dark mode) */ @@ -83,7 +83,6 @@ --sidebar-accent-foreground: hsl(240 4.8% 95.9%); --sidebar-border: hsl(240 3.7% 15.9%); --sidebar-ring: hsl(217.2 91.2% 59.8%); - --sidebar: hsl(240 5.9% 10%); } /* define theme */ @@ -295,23 +294,3 @@ scrollbar-width: thin; scrollbar-color: var(--border) transparent; } - -@theme inline { - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - body { - @apply bg-background text-foreground; - } -} diff --git a/app/layout.tsx b/app/layout.tsx index 66db5da..1813e0f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,15 +1,15 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import { Toaster } from "sonner"; -import { ThemeProvider } from "@/components/theme-provider"; +import { Toaster } from 'sonner'; +import type { Metadata } from 'next'; +import { Geist, Geist_Mono } from 'next/font/google'; +import { ThemeProvider } from '@/components/theme-provider'; -import "./globals.css"; -import { SessionProvider } from "next-auth/react"; +import './globals.css'; +import { SessionProvider } from 'next-auth/react'; export const metadata: Metadata = { - metadataBase: new URL("https://chat.vercel.ai"), - title: "Next.js Chatbot Template", - description: "Next.js chatbot template using the AI SDK.", + metadataBase: new URL('https://chat.vercel.ai'), + title: 'Next.js Chatbot Template', + description: 'Next.js chatbot template using the AI SDK.', }; export const viewport = { @@ -17,19 +17,19 @@ export const viewport = { }; const geist = Geist({ - subsets: ["latin"], - display: "swap", - variable: "--font-geist", + subsets: ['latin'], + display: 'swap', + variable: '--font-geist', }); const geistMono = Geist_Mono({ - subsets: ["latin"], - display: "swap", - variable: "--font-geist-mono", + subsets: ['latin'], + display: 'swap', + variable: '--font-geist-mono', }); -const LIGHT_THEME_COLOR = "hsl(0 0% 100%)"; -const DARK_THEME_COLOR = "hsl(240deg 10% 3.92%)"; +const LIGHT_THEME_COLOR = 'hsl(0 0% 100%)'; +const DARK_THEME_COLOR = 'hsl(240deg 10% 3.92%)'; const THEME_COLOR_SCRIPT = `\ (function() { var html = document.documentElement; @@ -48,24 +48,23 @@ const THEME_COLOR_SCRIPT = `\ updateThemeColor(); })();`; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return (