feat: complete profile page

This commit is contained in:
Hamzat Victor
2024-11-11 22:02:34 +01:00
parent 00e51205cf
commit 105eab9bad
16 changed files with 1172 additions and 485 deletions

View File

@ -8,7 +8,7 @@ import DashboardNavbarSearch from "./search"
export default function DashboardNavbar({ userData }: { userData: User }) {
return (
<div className="h-16 px-4 w-full flex items-center justify-between border-b border-border">
<div className=" py-2 px-4 w-full flex items-center justify-between border-b border-border">
<div className="flex items-center space-x-4">
<Link
href="/"

View File

@ -11,13 +11,13 @@ import {
} from "@/components/ui/dropdown-menu"
export default function ProjectCardDropdown({
sandbox,
visibility,
onVisibilityChange,
onDelete,
}: {
sandbox: Sandbox
onVisibilityChange: (sandbox: Sandbox) => void
onDelete: (sandbox: Sandbox) => void
visibility: Sandbox["visibility"]
onVisibilityChange: () => void
onDelete: () => void
}) {
return (
<DropdownMenu modal={false}>
@ -34,11 +34,11 @@ export default function ProjectCardDropdown({
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
onVisibilityChange(sandbox)
onVisibilityChange()
}}
className="cursor-pointer"
>
{sandbox.visibility === "public" ? (
{visibility === "public" ? (
<>
<Lock className="mr-2 h-4 w-4" />
<span>Make Private</span>
@ -53,7 +53,7 @@ export default function ProjectCardDropdown({
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation()
onDelete(sandbox)
onDelete()
}}
className="!text-destructive cursor-pointer"
>

View File

@ -4,56 +4,154 @@ import { Card } from "@/components/ui/card"
import { projectTemplates } from "@/lib/data"
import { Sandbox } from "@/lib/types"
import { AnimatePresence, motion } from "framer-motion"
import { Clock, Globe, Lock } from "lucide-react"
import { Clock, Eye, Globe, Heart, Lock } from "lucide-react"
import Image from "next/image"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
import { memo, useEffect, useMemo, useState } from "react"
import ProjectCardDropdown from "./dropdown"
import { CanvasRevealEffect } from "./revealEffect"
export default function ProjectCard({
children,
sandbox,
onVisibilityChange,
onDelete,
deletingId,
}: {
children?: React.ReactNode
sandbox: Sandbox
onVisibilityChange: (sandbox: Sandbox) => void
onDelete: (sandbox: Sandbox) => void
type BaseProjectCardProps = {
id: string
name: string
type: string
visibility: "public" | "private"
createdAt: Date
likeCount: number
viewCount: number
}
type AuthenticatedProjectCardProps = BaseProjectCardProps & {
isAuthenticated: true
onVisibilityChange: (
sandbox: Pick<Sandbox, "id" | "name" | "visibility">
) => void
onDelete: (sandbox: Pick<Sandbox, "id" | "name">) => void
deletingId: string
}) {
}
type UnauthenticatedProjectCardProps = BaseProjectCardProps & {
isAuthenticated: false
}
type ProjectCardProps =
| AuthenticatedProjectCardProps
| UnauthenticatedProjectCardProps
const StatItem = memo(({ icon: Icon, value }: { icon: any; value: number }) => (
<div className="flex items-center space-x-1">
<Icon className="size-4" />
<span className="text-xs">{value}</span>
</div>
))
StatItem.displayName = "StatItem"
const formatDate = (date: Date): string => {
const now = new Date()
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / 60000)
if (diffInMinutes < 1) return "Now"
if (diffInMinutes < 60) return `${diffInMinutes}m ago`
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`
return `${Math.floor(diffInMinutes / 1440)}d ago`
}
const ProjectMetadata = memo(
({
visibility,
createdAt,
likeCount,
viewCount,
}: Pick<
BaseProjectCardProps,
"visibility" | "createdAt" | "likeCount" | "viewCount"
>) => {
const [date, setDate] = useState<string>()
useEffect(() => {
setDate(formatDate(new Date(createdAt)))
}, [createdAt])
return (
<div className="flex flex-col text-muted-foreground space-y-2 text-sm z-10">
<div className="flex items-center justify-between">
<div className="flex items-center">
{visibility === "private" ? (
<>
<Lock className="size-4 mr-2" /> Private
</>
) : (
<>
<Globe className="size-4 mr-2" /> Public
</>
)}
</div>
</div>
<div className="flex gap-4">
<div className="flex items-center">
<Clock className="size-4 mr-2" /> {date}
</div>
<StatItem icon={Heart} value={likeCount} />
<StatItem icon={Eye} value={viewCount} />
</div>
</div>
)
}
)
ProjectMetadata.displayName = "ProjectMetadata"
function ProjectCardComponent({
id,
name,
type,
visibility,
createdAt,
likeCount,
viewCount,
...props
}: ProjectCardProps) {
const [hovered, setHovered] = useState(false)
const [date, setDate] = useState<string>()
const router = useRouter()
useEffect(() => {
const createdAt = new Date(sandbox.createdAt)
const now = new Date()
const diffInMinutes = Math.floor(
(now.getTime() - createdAt.getTime()) / 60000
)
const projectIcon = useMemo(
() =>
projectTemplates.find((p) => p.id === type)?.icon ??
"/project-icons/node.svg",
[type]
)
if (diffInMinutes < 1) {
setDate("Now")
} else if (diffInMinutes < 60) {
setDate(`${diffInMinutes}m ago`)
} else if (diffInMinutes < 1440) {
setDate(`${Math.floor(diffInMinutes / 60)}h ago`)
} else {
setDate(`${Math.floor(diffInMinutes / 1440)}d ago`)
const handleVisibilityChange = () => {
if (props.isAuthenticated) {
props.onVisibilityChange({
id,
name,
visibility,
})
}
}, [sandbox])
const projectIcon =
projectTemplates.find((p) => p.id === sandbox.type)?.icon ??
"/project-icons/node.svg"
}
const handleDelete = () => {
if (props.isAuthenticated) {
props.onDelete({
id,
name,
})
}
}
return (
<Card
tabIndex={0}
onClick={() => router.push(`/code/${sandbox.id}`)}
onClick={() => router.push(`/code/${id}`)}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
className={`group/canvas-card p-4 h-48 flex flex-col justify-between items-start hover:border-muted-foreground/50 relative overflow-hidden transition-all`}
className={`
group/canvas-card p-4 h-48 flex flex-col justify-between items-start
hover:border-muted-foreground/50 relative overflow-hidden transition-all
${props.isAuthenticated && props.deletingId === id ? "opacity-50" : ""}
`}
>
<AnimatePresence>
{hovered && (
@ -62,38 +160,59 @@ export default function ProjectCard({
animate={{ opacity: 1 }}
className="h-full w-full absolute inset-0"
>
{children}
<CanvasRevealEffect
animationSpeed={3}
containerClassName="bg-black"
colors={colors[type]}
dotSize={2}
/>
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
</motion.div>
)}
</AnimatePresence>
<div className="space-x-2 flex items-center justify-start w-full z-10">
<Image alt="" src={projectIcon} width={20} height={20} />
<div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
{sandbox.name}
</div>
<ProjectCardDropdown
sandbox={sandbox}
onVisibilityChange={onVisibilityChange}
onDelete={onDelete}
<Image
alt={`${type} project icon`}
src={projectIcon}
width={20}
height={20}
/>
</div>
<div className="flex flex-col text-muted-foreground space-y-0.5 text-sm z-10">
<div className="flex items-center">
{sandbox.visibility === "private" ? (
<>
<Lock className="w-3 h-3 mr-2" /> Private
</>
) : (
<>
<Globe className="w-3 h-3 mr-2" /> Public
</>
)}
</div>
<div className="flex items-center">
<Clock className="w-3 h-3 mr-2" /> {date}
<div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
{name}
</div>
{props.isAuthenticated && (
<ProjectCardDropdown
onVisibilityChange={handleVisibilityChange}
onDelete={handleDelete}
visibility={visibility}
/>
)}
</div>
<ProjectMetadata
visibility={visibility}
createdAt={createdAt}
likeCount={likeCount}
viewCount={viewCount}
/>
</Card>
)
}
ProjectCardComponent.displayName = "ProjectCard"
const ProjectCard = memo(ProjectCardComponent)
export default ProjectCard
const colors: { [key: string]: number[][] } = {
react: [
[71, 207, 237],
[30, 126, 148],
],
node: [
[86, 184, 72],
[59, 112, 52],
],
}

View File

@ -2,11 +2,11 @@
import { deleteSandbox, updateSandbox } from "@/lib/actions"
import { Sandbox } from "@/lib/types"
import { cn } from "@/lib/utils"
import Link from "next/link"
import { useEffect, useState } from "react"
import { useEffect, useMemo, useState } from "react"
import { toast } from "sonner"
import ProjectCard from "./projectCard"
import { CanvasRevealEffect } from "./projectCard/revealEffect"
const colors: { [key: string]: number[][] } = {
react: [
@ -28,11 +28,27 @@ export default function DashboardProjects({
}) {
const [deletingId, setDeletingId] = useState<string>("")
const onDelete = async (sandbox: Sandbox) => {
setDeletingId(sandbox.id)
toast(`Project ${sandbox.name} deleted.`)
await deleteSandbox(sandbox.id)
}
const onVisibilityChange = useMemo(
() => async (sandbox: Pick<Sandbox, "id" | "name" | "visibility">) => {
const newVisibility =
sandbox.visibility === "public" ? "private" : "public"
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
await updateSandbox({
id: sandbox.id,
visibility: newVisibility,
})
},
[]
)
const onDelete = useMemo(
() => async (sandbox: Pick<Sandbox, "id" | "name">) => {
setDeletingId(sandbox.id)
toast(`Project ${sandbox.name} deleted.`)
await deleteSandbox(sandbox.id)
},
[]
)
useEffect(() => {
if (deletingId) {
@ -40,15 +56,6 @@ export default function DashboardProjects({
}
}, [sandboxes])
const onVisibilityChange = async (sandbox: Sandbox) => {
const newVisibility = sandbox.visibility === "public" ? "private" : "public"
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
await updateSandbox({
id: sandbox.id,
visibility: newVisibility,
})
}
return (
<div className="grow p-4 flex flex-col">
<div className="text-xl font-medium mb-8">
@ -67,26 +74,20 @@ export default function DashboardProjects({
<Link
key={sandbox.id}
href={`/code/${sandbox.id}`}
className={`${
className={cn(
"transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg",
deletingId === sandbox.id
? "pointer-events-none opacity-50 cursor-events-none"
: "cursor-pointer"
} transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg`}
)}
>
<ProjectCard
sandbox={sandbox}
onVisibilityChange={onVisibilityChange}
onDelete={onDelete}
deletingId={deletingId}
>
<CanvasRevealEffect
animationSpeed={3}
containerClassName="bg-black"
colors={colors[sandbox.type]}
dotSize={2}
/>
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
</ProjectCard>
isAuthenticated
{...sandbox}
/>
</Link>
)
})}

View File

@ -1,7 +1,6 @@
"use client"
import ProjectCard from "@/components/dashboard/projectCard/"
import { CanvasRevealEffect } from "@/components/dashboard/projectCard/revealEffect"
import { Button } from "@/components/ui/button"
import {
Card,
@ -9,22 +8,23 @@ import {
CardDescription,
CardTitle,
} from "@/components/ui/card"
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { updateSandbox } from "@/lib/actions"
import { deleteSandbox, updateSandbox } from "@/lib/actions"
import { MAX_FREE_GENERATION } from "@/lib/constant"
import { Sandbox, User } from "@/lib/types"
import { PlusCircle } from "lucide-react"
import { cn } from "@/lib/utils"
import { Heart, LucideIcon, Package2, PlusCircle, Sparkles } from "lucide-react"
import Link from "next/link"
import { useMemo, useState } from "react"
import { toast } from "sonner"
const colors: { [key: string]: number[][] } = {
react: [
[71, 207, 237],
[30, 126, 148],
],
node: [
[86, 184, 72],
[59, 112, 52],
],
}
import Avatar from "../ui/avatar"
import { Badge } from "../ui/badge"
import { Progress } from "../ui/progress"
export default function ProfilePage({
publicSandboxes,
@ -35,54 +35,89 @@ export default function ProfilePage({
publicSandboxes: Sandbox[]
privateSandboxes: Sandbox[]
user: User
currentUser: {
id: string
firstName: string | null
lastName: string | null
} | null
currentUser: User | null
}) {
const onVisibilityChange = async (sandbox: Sandbox) => {
const newVisibility = sandbox.visibility === "public" ? "private" : "public"
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
await updateSandbox({
id: sandbox.id,
visibility: newVisibility,
})
}
const [deletingId, setDeletingId] = useState<string>("")
const isLoggedIn = Boolean(currentUser)
const hasPublicSandboxes = publicSandboxes.length > 0
const hasPrivateSandboxes = privateSandboxes.length > 0
const onVisibilityChange = useMemo(
() => async (sandbox: Pick<Sandbox, "id" | "name" | "visibility">) => {
const newVisibility =
sandbox.visibility === "public" ? "private" : "public"
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
await updateSandbox({
id: sandbox.id,
visibility: newVisibility,
})
},
[]
)
const onDelete = useMemo(
() => async (sandbox: Pick<Sandbox, "id" | "name">) => {
setDeletingId(sandbox.id)
toast(`Project ${sandbox.name} deleted.`)
await deleteSandbox(sandbox.id)
setDeletingId("")
},
[]
)
const stats = useMemo(() => {
const allSandboxes = isLoggedIn
? [...publicSandboxes, ...privateSandboxes]
: publicSandboxes
const totalSandboxes = allSandboxes.length
const totalLikes = allSandboxes.reduce(
(sum, sandbox) => sum + sandbox.likeCount,
0
)
return {
sandboxes:
totalSandboxes === 1 ? "1 sandbox" : `${totalSandboxes} sandboxes`,
likes: totalLikes === 1 ? "1 like" : `${totalLikes} likes`,
}
}, [isLoggedIn, publicSandboxes, privateSandboxes])
const joinDate = useMemo(
() =>
new Date(user.createdAt).toLocaleDateString("en-US", {
month: "long",
year: "numeric",
}),
[user.createdAt]
)
return (
<>
<div className="container mx-auto p-6 grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="md:col-span-1">
<Card className="mb-6 md:mb-0 sticky top-6">
<CardContent className="flex flex-col gap-3 items-center pt-6">
<div className="w-16 h-16 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium">
<span className="text-2xl text-background">
{user.name &&
user.name
.split(" ")
.slice(0, 2)
.map((name) => name[0].toUpperCase())}
</span>
</div>
<Avatar
name={user.name}
avatarUrl={user.avatarUrl}
className="size-36"
/>
<CardTitle className="text-2xl">{user.name}</CardTitle>
<CardDescription>@janedoe</CardDescription>
<p className="text-sm text-muted-foreground">
Full-stack developer | Open source enthusiast
</p>
<p className="text-xs text-muted-foreground">
Joined January 2023
</p>
<CardDescription>{`@${user.username}`}</CardDescription>
<div className="flex gap-6">
<StatsItem icon={Package2} label={stats.sandboxes} />
<StatsItem icon={Heart} label={stats.likes} />
</div>
<div className="flex flex-col items-center gap-2">
<p className="text-xs text-muted-foreground">
{`Joined ${joinDate}`}
</p>
{isLoggedIn && <SubscriptionBadge user={currentUser!} />}
</div>
</CardContent>
</Card>
</div>
<div className="md:col-span-2">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold">Sandboxes</h2>
</div>
<Tabs defaultValue="public">
<TabsList className="mb-4">
<TabsTrigger value="public">Public</TabsTrigger>
@ -96,22 +131,24 @@ export default function ProfilePage({
<Link
key={sandbox.id}
href={`/code/${sandbox.id}`}
className={`cursor-pointer transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg`}
className={cn(
"transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg",
deletingId === sandbox.id
? "pointer-events-none opacity-50 cursor-events-none"
: "cursor-pointer"
)}
>
<ProjectCard
sandbox={sandbox}
onVisibilityChange={onVisibilityChange}
onDelete={() => {}}
deletingId={"deletingId"}
>
<CanvasRevealEffect
animationSpeed={3}
containerClassName="bg-black"
colors={colors[sandbox.type]}
dotSize={2}
{isLoggedIn ? (
<ProjectCard
onVisibilityChange={onVisibilityChange}
onDelete={onDelete}
deletingId={deletingId}
isAuthenticated
{...sandbox}
/>
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
</ProjectCard>
) : (
<ProjectCard isAuthenticated={false} {...sandbox} />
)}
</Link>
)
})}
@ -119,7 +156,12 @@ export default function ProfilePage({
) : (
<EmptyState
title="No public sandboxes yet"
description="Create your first public sandbox to share your work with the world!"
description={
isLoggedIn
? "Create your first public sandbox to share your work with the world!"
: "Login to create public sandboxes"
}
isLoggedIn={isLoggedIn}
/>
)}
</TabsContent>
@ -131,29 +173,32 @@ export default function ProfilePage({
<Link
key={sandbox.id}
href={`/code/${sandbox.id}`}
className={`cursor-pointer transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg`}
className={cn(
"transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg",
deletingId === sandbox.id
? "pointer-events-none opacity-50 cursor-events-none"
: "cursor-pointer"
)}
>
<ProjectCard
sandbox={sandbox}
onVisibilityChange={onVisibilityChange}
onDelete={() => {}}
deletingId={"deletingId"}
>
<CanvasRevealEffect
animationSpeed={3}
containerClassName="bg-black"
colors={colors[sandbox.type]}
dotSize={2}
/>
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
</ProjectCard>
onDelete={onDelete}
deletingId={deletingId}
isAuthenticated
{...sandbox}
/>
</Link>
))}
</div>
) : (
<EmptyState
title="No private sandboxes yet"
description="Create your first private sandbox to start working on your personal projects!"
description={
isLoggedIn
? "Create your first private sandbox to start working on your personal projects!"
: "Login to create private sandboxes"
}
isLoggedIn={isLoggedIn}
/>
)}
</TabsContent>
@ -168,19 +213,63 @@ export default function ProfilePage({
function EmptyState({
title,
description,
isLoggedIn,
}: {
title: string
description: string
isLoggedIn: boolean
}) {
return (
<Card className="flex flex-col items-center justify-center p-6 text-center h-[300px]">
<PlusCircle className="h-12 w-12 text-muted-foreground mb-4" />
<CardTitle className="text-xl mb-2">{title}</CardTitle>
<CardDescription className="mb-4">{description}</CardDescription>
<Button>
<PlusCircle className="h-4 w-4 mr-2" />
Create Sandbox
</Button>
{isLoggedIn && (
<Button>
<PlusCircle className="h-4 w-4 mr-2" />
Create Sandbox
</Button>
)}
</Card>
)
}
interface StatsItemProps {
icon: LucideIcon
label: string
}
const StatsItem = ({ icon: Icon, label }: StatsItemProps) => (
<div className="flex items-center gap-2">
<Icon size={18} />
<span className="text-sm text-muted-foreground">{label}</span>
</div>
)
const SubscriptionBadge = ({ user }: { user: User }) => {
return (
<HoverCard>
<HoverCardTrigger>
<Badge variant="secondary" className="text-xs cursor-pointer">
Free
</Badge>
</HoverCardTrigger>
<HoverCardContent>
<div className="w-full space-y-2">
<div className="flex justify-between text-sm">
<span className="font-medium">AI Generations</span>
<span>{`${user.generations} / ${MAX_FREE_GENERATION}`}</span>
</div>
<Progress
value={user?.generations!}
max={MAX_FREE_GENERATION}
className="w-full"
/>
</div>
<Button size="sm" className="w-full mt-4">
<Sparkles className="mr-2 h-4 w-4" /> Upgrade to Pro
</Button>
</HoverCardContent>
</HoverCard>
)
}

View File

@ -0,0 +1,26 @@
import Logo from "@/assets/logo.svg"
import { ThemeSwitcher } from "@/components/ui/theme-switcher"
import UserButton from "@/components/ui/userButton"
import { User } from "@/lib/types"
import Image from "next/image"
import Link from "next/link"
export default function ProfileNavbar({ userData }: { userData: User }) {
return (
<nav className=" py-2 px-4 w-full flex items-center justify-between border-b border-border">
<div className="flex items-center space-x-4">
<Link
href="/"
className="ring-offset-2 ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none rounded-sm"
>
<Image src={Logo} alt="Logo" width={36} height={36} />
</Link>
<div className="text-sm font-medium flex items-center">Sandbox</div>
</div>
<div className="flex items-center space-x-4">
<ThemeSwitcher />
{Boolean(userData?.id) ? <UserButton userData={userData} /> : null}
</div>
</nav>
)
}

View File

@ -1,5 +1,4 @@
import { cn } from "@/lib/utils"
import Image from "next/image"
export default function Avatar({
name,
@ -22,12 +21,12 @@ export default function Avatar({
return (
<div
className={cn(
className,
"w-9 h-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium"
"size-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium",
className
)}
>
{avatarUrl ? (
<Image
<img
src={avatarUrl}
alt={name || "User"}
width={20}

View File

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
import { cn } from "@/lib/utils"
const HoverCard = HoverCardPrimitive.Root
const HoverCardTrigger = HoverCardPrimitive.Trigger
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
export { HoverCard, HoverCardTrigger, HoverCardContent }

View File

@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import * as React from "react"
import { cn } from "@/lib/utils"
@ -18,7 +18,7 @@ const Progress = React.forwardRef<
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
className="h-full w-full flex-1 bg-primary transition-all rounded-full"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>

View File

@ -17,7 +17,7 @@ export function ThemeSwitcher() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="text-muted-foreground">
<Button variant="outline" size="icon" className="text-foreground">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>

View File

@ -7,9 +7,16 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { MAX_FREE_GENERATION } from "@/lib/constant"
import { User } from "@/lib/types"
import { useClerk } from "@clerk/nextjs"
import { LogOut, Sparkles } from "lucide-react"
import {
LayoutDashboard,
LogOut,
Sparkles,
User as UserIcon,
} from "lucide-react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import Avatar from "./avatar"
@ -33,31 +40,43 @@ export default function UserButton({ userData }: { userData: User }) {
</div>
<DropdownMenuSeparator />
<div className="py-1.5 px-2 w-full flex flex-col items-start text-sm">
<div className="flex items-center">
<Sparkles className={`h-4 w-4 mr-2 text-indigo-500`} />
AI Usage: {userData.generations}/1000
<DropdownMenuItem>
<Sparkles className="size-4 mr-2 text-indigo-500" />
<div className="w-full flex flex-col items-start text-sm">
<span className="text-sm">{`AI Usage: ${userData.generations}/${MAX_FREE_GENERATION}`}</span>
<div className="rounded-full w-full mt-1 h-1.5 overflow-hidden bg-secondary border border-muted-foreground">
<div
className="h-full bg-indigo-500 rounded-full"
style={{
width: `${(userData.generations * 100) / 1000}%`,
}}
/>
</div>
</div>
<div className="rounded-full w-full mt-2 h-2 overflow-hidden bg-secondary">
<div
className="h-full bg-indigo-500 rounded-full"
style={{
width: `${(userData.generations * 100) / 1000}%`,
}}
/>
</div>
</div>
<DropdownMenuSeparator />
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer" asChild>
<Link href={"/dashboard"}>
<LayoutDashboard className="mr-2 size-4" />
<span>Dashboard</span>
<DropdownMenuSeparator />
</Link>
</DropdownMenuItem>
<DropdownMenuItem className="cursor-pointer" asChild>
<Link href={`/@${userData.username}`}>
<UserIcon className="mr-2 size-4" />
<span>Profile</span>
<DropdownMenuSeparator />
</Link>
</DropdownMenuItem>
{/* <DropdownMenuItem className="cursor-pointer">
<Pencil className="mr-2 h-4 w-4" />
<Pencil className="mr-2 size-4" />
<span>Edit Profile</span>
</DropdownMenuItem> */}
<DropdownMenuItem
onClick={() => signOut(() => router.push("/"))}
className="!text-destructive cursor-pointer"
>
<LogOut className="mr-2 h-4 w-4" />
<LogOut className="mr-2 size-4" />
<span>Log Out</span>
</DropdownMenuItem>
</DropdownMenuContent>