use shadcn avatar component

This commit is contained in:
Leon van Zyl
2025-08-13 09:41:10 +02:00
parent 4eae69b8ce
commit 616ebb8fcc
6 changed files with 220 additions and 21 deletions

17
env.example Normal file
View 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"

View File

@@ -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",

112
pnpm-lock.yaml generated
View File

@@ -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': {}

View File

@@ -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 <div className="flex justify-center items-center h-screen">Loading...</div>
@@ -56,14 +58,23 @@ export default function ChatPage() {
))}
</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
value={input}
onChange={handleInputChange}
onChange={(e) => 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"
/>
<Button type="submit" disabled={!input.trim()}>
<Button type="submit" disabled={!input.trim() || status === "streaming"}>
Send
</Button>
</form>

View File

@@ -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 <div>Loading...</div>
return <div>Loading...</div>;
}
if (!session) {
return (
<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 />
</div>
)
);
}
return (
<div className="flex flex-col items-center gap-4 p-6">
<div className="text-center">
{session.user?.image && (
<img
src={session.user.image}
alt={session.user.name || "User"}
className="w-16 h-16 rounded-full mx-auto mb-4"
<Avatar className="size-16 mx-auto mb-4">
<AvatarImage
src={session.user?.image || ""}
alt={session.user?.name || "User"}
/>
)}
<AvatarFallback>
{(
session.user?.name?.[0] ||
session.user?.email?.[0] ||
"U"
).toUpperCase()}
</AvatarFallback>
</Avatar>
<h2 className="text-xl font-semibold">{session.user?.name}</h2>
<p className="text-muted-foreground">{session.user?.email}</p>
</div>
<SignOutButton />
</div>
)
}
);
}

View 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 }