use shadcn avatar component
This commit is contained in:
17
env.example
Normal file
17
env.example
Normal file
@@ -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"
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^2.0.9",
|
"@ai-sdk/openai": "^2.0.9",
|
||||||
"@ai-sdk/react": "^2.0.9",
|
"@ai-sdk/react": "^2.0.9",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.10",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"ai": "^5.0.9",
|
"ai": "^5.0.9",
|
||||||
"better-auth": "^1.3.4",
|
"better-auth": "^1.3.4",
|
||||||
|
|||||||
112
pnpm-lock.yaml
generated
112
pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
|||||||
'@ai-sdk/react':
|
'@ai-sdk/react':
|
||||||
specifier: ^2.0.9
|
specifier: ^2.0.9
|
||||||
version: 2.0.9(react@19.1.0)(zod@4.0.17)
|
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':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.2.3
|
specifier: ^1.2.3
|
||||||
version: 1.2.3(@types/react@19.1.9)(react@19.1.0)
|
version: 1.2.3(@types/react@19.1.9)(react@19.1.0)
|
||||||
@@ -752,6 +755,19 @@ packages:
|
|||||||
'@peculiar/asn1-x509@2.4.0':
|
'@peculiar/asn1-x509@2.4.0':
|
||||||
resolution: {integrity: sha512-F7mIZY2Eao2TaoVqigGMLv+NDdpwuBKU1fucHPONfzaBS4JXXCNCmfO0Z3dsy7JzKGqtDcYC1mr9JjaZQZNiuw==}
|
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':
|
'@radix-ui/react-compose-refs@1.1.2':
|
||||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -761,6 +777,28 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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':
|
'@radix-ui/react-slot@1.2.3':
|
||||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -770,6 +808,33 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
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':
|
'@rtsao/scc@1.1.0':
|
||||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||||
|
|
||||||
@@ -3010,12 +3075,40 @@ snapshots:
|
|||||||
pvtsutils: 1.3.6
|
pvtsutils: 1.3.6
|
||||||
tslib: 2.8.1
|
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)':
|
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.9)(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.9
|
'@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)':
|
'@radix-ui/react-slot@1.2.3(@types/react@19.1.9)(react@19.1.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.0)
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.9)(react@19.1.0)
|
||||||
@@ -3023,6 +3116,25 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.1.9
|
'@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': {}
|
'@rtsao/scc@1.1.0': {}
|
||||||
|
|
||||||
'@rushstack/eslint-patch@1.12.0': {}
|
'@rushstack/eslint-patch@1.12.0': {}
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import { useChat } from "@ai-sdk/react"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { UserProfile } from "@/components/auth/user-profile"
|
import { UserProfile } from "@/components/auth/user-profile"
|
||||||
import { useSession } from "@/lib/auth-client"
|
import { useSession } from "@/lib/auth-client"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
export default function ChatPage() {
|
export default function ChatPage() {
|
||||||
const { data: session, isPending } = useSession()
|
const { data: session, isPending } = useSession()
|
||||||
const { messages, input, handleInputChange, handleSubmit } = useChat()
|
const { messages, sendMessage, status } = useChat()
|
||||||
|
const [input, setInput] = useState("")
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return <div className="flex justify-center items-center h-screen">Loading...</div>
|
return <div className="flex justify-center items-center h-screen">Loading...</div>
|
||||||
@@ -56,14 +58,23 @@ export default function ChatPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="flex gap-2">
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const text = input.trim()
|
||||||
|
if (!text) return
|
||||||
|
sendMessage({ text })
|
||||||
|
setInput("")
|
||||||
|
}}
|
||||||
|
className="flex gap-2"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
value={input}
|
value={input}
|
||||||
onChange={handleInputChange}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder="Type your message..."
|
placeholder="Type your message..."
|
||||||
className="flex-1 p-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-ring"
|
className="flex-1 p-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
/>
|
/>
|
||||||
<Button type="submit" disabled={!input.trim()}>
|
<Button type="submit" disabled={!input.trim() || status === "streaming"}>
|
||||||
Send
|
Send
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,40 +1,45 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useSession } from "@/lib/auth-client"
|
import { useSession } from "@/lib/auth-client";
|
||||||
import { SignInButton } from "./sign-in-button"
|
import { SignInButton } from "./sign-in-button";
|
||||||
import { SignOutButton } from "./sign-out-button"
|
import { SignOutButton } from "./sign-out-button";
|
||||||
|
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
|
||||||
export function UserProfile() {
|
export function UserProfile() {
|
||||||
const { data: session, isPending } = useSession()
|
const { data: session, isPending } = useSession();
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-4 p-6">
|
<div className="flex flex-col items-center gap-4 p-6">
|
||||||
<h2 className="text-xl font-semibold">Welcome</h2>
|
|
||||||
<p className="text-muted-foreground">Please sign in to continue</p>
|
|
||||||
<SignInButton />
|
<SignInButton />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-4 p-6">
|
<div className="flex flex-col items-center gap-4 p-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{session.user?.image && (
|
<Avatar className="size-16 mx-auto mb-4">
|
||||||
<img
|
<AvatarImage
|
||||||
src={session.user.image}
|
src={session.user?.image || ""}
|
||||||
alt={session.user.name || "User"}
|
alt={session.user?.name || "User"}
|
||||||
className="w-16 h-16 rounded-full mx-auto mb-4"
|
|
||||||
/>
|
/>
|
||||||
)}
|
<AvatarFallback>
|
||||||
|
{(
|
||||||
|
session.user?.name?.[0] ||
|
||||||
|
session.user?.email?.[0] ||
|
||||||
|
"U"
|
||||||
|
).toUpperCase()}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
<h2 className="text-xl font-semibold">{session.user?.name}</h2>
|
<h2 className="text-xl font-semibold">{session.user?.name}</h2>
|
||||||
<p className="text-muted-foreground">{session.user?.email}</p>
|
<p className="text-muted-foreground">{session.user?.email}</p>
|
||||||
</div>
|
</div>
|
||||||
<SignOutButton />
|
<SignOutButton />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
53
src/components/ui/avatar.tsx
Normal file
53
src/components/ui/avatar.tsx
Normal file
@@ -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<typeof AvatarPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
data-slot="avatar"
|
||||||
|
className={cn(
|
||||||
|
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarImage({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
data-slot="avatar-image"
|
||||||
|
className={cn("aspect-square size-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AvatarFallback({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||||
|
return (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Avatar, AvatarImage, AvatarFallback }
|
||||||
Reference in New Issue
Block a user