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

@ -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>
)
}