feat: complete profile page
This commit is contained in:
@ -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="/"
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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],
|
||||
],
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
Reference in New Issue
Block a user