feat: complete profile page
This commit is contained in:
@ -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>
|
||||
)
|
||||
}
|
||||
|
26
frontend/components/profile/navbar.tsx
Normal file
26
frontend/components/profile/navbar.tsx
Normal 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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user