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