diff --git a/env.example b/env.example new file mode 100644 index 0000000..7195d3d --- /dev/null +++ b/env.example @@ -0,0 +1,17 @@ +# Rename this file to .env + +# Database +DATABASE_URL="postgresql://username:password@localhost:5432/your_database_name" + +# Authentication - Better Auth +BETTER_AUTH_SECRET="your-random-32-character-secret-key-here" + +# Google OAuth (Get from Google Cloud Console) +GOOGLE_CLIENT_ID="your-google-client-id" +GOOGLE_CLIENT_SECRET="your-google-client-secret" + +# AI Integration (Optional - for chat functionality) +OPENAI_API_KEY="sk-your-openai-api-key-here" + +# App URL (for production deployments) +NEXT_PUBLIC_APP_URL="http://localhost:3000" \ No newline at end of file diff --git a/package.json b/package.json index f60cc4f..19dbc47 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "dependencies": { "@ai-sdk/openai": "^2.0.9", "@ai-sdk/react": "^2.0.9", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-slot": "^1.2.3", "ai": "^5.0.9", "better-auth": "^1.3.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d37f8b9..bb57718 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@ai-sdk/react': specifier: ^2.0.9 version: 2.0.9(react@19.1.0)(zod@4.0.17) + '@radix-ui/react-avatar': + specifier: ^1.1.10 + version: 1.1.10(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@radix-ui/react-slot': specifier: ^1.2.3 version: 1.2.3(@types/react@19.1.9)(react@19.1.0) @@ -752,6 +755,19 @@ packages: '@peculiar/asn1-x509@2.4.0': resolution: {integrity: sha512-F7mIZY2Eao2TaoVqigGMLv+NDdpwuBKU1fucHPONfzaBS4JXXCNCmfO0Z3dsy7JzKGqtDcYC1mr9JjaZQZNiuw==} + '@radix-ui/react-avatar@1.1.10': + resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -761,6 +777,28 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: @@ -770,6 +808,33 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-is-hydrated@0.1.0': + resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -3010,12 +3075,40 @@ snapshots: pvtsutils: 1.3.6 tslib: 2.8.1 + '@radix-ui/react-avatar@1.1.10(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-context': 1.1.2(@types/react@19.1.9)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.9)(react@19.1.0) + '@radix-ui/react-use-is-hydrated': 0.1.0(@types/react@19.1.9)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.9)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.9 + '@types/react-dom': 19.1.7(@types/react@19.1.9) + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.9)(react@19.1.0)': dependencies: react: 19.1.0 optionalDependencies: '@types/react': 19.1.9 + '@radix-ui/react-context@1.1.2(@types/react@19.1.9)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.9)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.9 + '@types/react-dom': 19.1.7(@types/react@19.1.9) + '@radix-ui/react-slot@1.2.3(@types/react@19.1.9)(react@19.1.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.0) @@ -3023,6 +3116,25 @@ snapshots: optionalDependencies: '@types/react': 19.1.9 + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.9)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-use-is-hydrated@0.1.0(@types/react@19.1.9)(react@19.1.0)': + dependencies: + react: 19.1.0 + use-sync-external-store: 1.5.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.9 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.9)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.9 + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.12.0': {} diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index 446cd24..89f9ee2 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -4,10 +4,12 @@ import { useChat } from "@ai-sdk/react" import { Button } from "@/components/ui/button" import { UserProfile } from "@/components/auth/user-profile" import { useSession } from "@/lib/auth-client" +import { useState } from "react" export default function ChatPage() { const { data: session, isPending } = useSession() - const { messages, input, handleInputChange, handleSubmit } = useChat() + const { messages, sendMessage, status } = useChat() + const [input, setInput] = useState("") if (isPending) { return
Loading...
@@ -56,14 +58,23 @@ export default function ChatPage() { ))} -
+ { + e.preventDefault() + const text = input.trim() + if (!text) return + sendMessage({ text }) + setInput("") + }} + className="flex gap-2" + > setInput(e.target.value)} placeholder="Type your message..." className="flex-1 p-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-ring" /> -
diff --git a/src/components/auth/user-profile.tsx b/src/components/auth/user-profile.tsx index e0133b0..17c4884 100644 --- a/src/components/auth/user-profile.tsx +++ b/src/components/auth/user-profile.tsx @@ -1,40 +1,45 @@ -"use client" +"use client"; -import { useSession } from "@/lib/auth-client" -import { SignInButton } from "./sign-in-button" -import { SignOutButton } from "./sign-out-button" +import { useSession } from "@/lib/auth-client"; +import { SignInButton } from "./sign-in-button"; +import { SignOutButton } from "./sign-out-button"; +import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; export function UserProfile() { - const { data: session, isPending } = useSession() + const { data: session, isPending } = useSession(); if (isPending) { - return
Loading...
+ return
Loading...
; } if (!session) { return (
-

Welcome

-

Please sign in to continue

- ) + ); } return (
- {session.user?.image && ( - {session.user.name + - )} + + {( + session.user?.name?.[0] || + session.user?.email?.[0] || + "U" + ).toUpperCase()} + +

{session.user?.name}

{session.user?.email}

- ) -} \ No newline at end of file + ); +} diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..71e428b --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback }