-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16 from ARYPROGRAMMER/develop/home
feat: snippet page collection setup
- Loading branch information
Showing
7 changed files
with
682 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
"use client"; | ||
import { Snippet } from "@/types"; | ||
import { useUser } from "@clerk/nextjs"; | ||
import { useMutation } from "convex/react"; | ||
import { api } from "../../../../convex/_generated/api"; | ||
import { useState } from "react"; | ||
|
||
import { motion } from "framer-motion"; | ||
import Link from "next/link"; | ||
import { Clock, Trash2, User } from "lucide-react"; | ||
import Image from "next/image"; | ||
import toast from "react-hot-toast"; | ||
import StarButton from "@/components/ui/StarButton"; | ||
|
||
|
||
function SnippetCard({ snippet }: { snippet: Snippet }) { | ||
const { user } = useUser(); | ||
const deleteSnippet = useMutation(api.snippets.deleteSnippet); | ||
const [isDeleting, setIsDeleting] = useState(false); | ||
|
||
const handleDelete = async () => { | ||
setIsDeleting(true); | ||
|
||
try { | ||
await deleteSnippet({ snippetId: snippet._id }); | ||
} catch (error) { | ||
console.log("Error deleting snippet:", error); | ||
toast.error("Error deleting snippet"); | ||
} finally { | ||
setIsDeleting(false); | ||
} | ||
}; | ||
|
||
return ( | ||
<motion.div | ||
layout | ||
className="group relative" | ||
whileHover={{ y: -2 }} | ||
transition={{ duration: 0.2 }} | ||
> | ||
<Link href={`/snippets/${snippet._id}`} className="h-full block"> | ||
<div | ||
className="relative h-full bg-[#1e1e2e]/80 backdrop-blur-sm rounded-xl | ||
border border-[#313244]/50 hover:border-[#313244] | ||
transition-all duration-300 overflow-hidden" | ||
> | ||
<div className="p-6"> | ||
{/* Header */} | ||
<div className="flex items-start justify-between mb-4"> | ||
<div className="flex items-center gap-3"> | ||
<div className="relative"> | ||
<div | ||
className="absolute inset-0 bg-gradient-to-r from-blue-500 to-purple-500 rounded-lg blur opacity-20 | ||
group-hover:opacity-30 transition-all duration-500" | ||
area-hidden="true" | ||
/> | ||
<div | ||
className="relative p-2 rounded-lg bg-gradient-to-br from-blue-500/10 to-purple-500/10 group-hover:from-blue-500/20 | ||
group-hover:to-purple-500/20 transition-all duration-500" | ||
> | ||
<Image | ||
src={`/${snippet.language}.png`} | ||
alt={`${snippet.language} logo`} | ||
className="w-6 h-6 object-contain relative z-10" | ||
width={24} | ||
height={24} | ||
/> | ||
</div> | ||
</div> | ||
<div className="space-y-1"> | ||
<span className="px-3 py-1 bg-blue-500/10 text-blue-400 rounded-lg text-xs font-medium"> | ||
{snippet.language} | ||
</span> | ||
<div className="flex items-center gap-2 text-xs text-gray-500"> | ||
<Clock className="size-3" /> | ||
{new Date(snippet._creationTime).toLocaleDateString()} | ||
</div> | ||
</div> | ||
</div> | ||
<div | ||
className="absolute top-5 right-5 z-10 flex gap-4 items-center" | ||
onClick={(e) => e.preventDefault()} | ||
> | ||
<StarButton snippetId={snippet._id} /> | ||
|
||
{user?.id === snippet.userId && ( | ||
<div className="z-10" onClick={(e) => e.preventDefault()}> | ||
<button | ||
onClick={handleDelete} | ||
disabled={isDeleting} | ||
className={`group flex items-center gap-1.5 px-3 py-1.5 rounded-lg transition-all duration-200 | ||
${ | ||
isDeleting | ||
? "bg-red-500/20 text-red-400 cursor-not-allowed" | ||
: "bg-gray-500/10 text-gray-400 hover:bg-red-500/10 hover:text-red-400" | ||
} | ||
`} | ||
> | ||
{isDeleting ? ( | ||
<div className="size-3.5 border-2 border-red-400/30 border-t-red-400 rounded-full animate-spin" /> | ||
) : ( | ||
<Trash2 className="size-3.5" /> | ||
)} | ||
</button> | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
|
||
{/* Content */} | ||
<div className="space-y-4"> | ||
<div> | ||
<h2 className="text-xl font-semibold text-white mb-2 line-clamp-1 group-hover:text-blue-400 transition-colors"> | ||
{snippet.title} | ||
</h2> | ||
<div className="flex items-center gap-3 text-sm text-gray-400"> | ||
<div className="flex items-center gap-2"> | ||
<div className="p-1 rounded-md bg-gray-800/50"> | ||
<User className="size-3" /> | ||
</div> | ||
<span className="truncate max-w-[150px]">{snippet.userName}</span> | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<div className="relative group/code"> | ||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/15 to-purple-500/5 rounded-lg opacity-0 group-hover/code:opacity-100 transition-all" /> | ||
<pre className="relative bg-black/30 rounded-lg p-4 overflow-hidden text-sm text-gray-300 font-mono line-clamp-3"> | ||
{snippet.code} | ||
</pre> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</Link> | ||
</motion.div> | ||
); | ||
} | ||
export default SnippetCard; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
const CardSkeleton = () => ( | ||
<div className="relative group"> | ||
<div className="bg-[#1e1e2e]/80 rounded-xl border border-[#313244]/50 overflow-hidden h-[280px]"> | ||
<div className="p-6 space-y-4"> | ||
{/* Header shimmer */} | ||
<div className="flex items-start justify-between"> | ||
<div className="flex items-center gap-3"> | ||
<div className="w-10 h-10 rounded-lg bg-gray-800 animate-pulse" /> | ||
<div className="space-y-2"> | ||
<div className="w-24 h-6 bg-gray-800 rounded-lg animate-pulse" /> | ||
<div className="w-20 h-4 bg-gray-800 rounded-lg animate-pulse" /> | ||
</div> | ||
</div> | ||
<div className="w-16 h-8 bg-gray-800 rounded-lg animate-pulse" /> | ||
</div> | ||
|
||
{/* Title shimmer */} | ||
<div className="space-y-2"> | ||
<div className="w-3/4 h-7 bg-gray-800 rounded-lg animate-pulse" /> | ||
<div className="w-1/2 h-5 bg-gray-800 rounded-lg animate-pulse" /> | ||
</div> | ||
|
||
{/* Code block shimmer */} | ||
<div className="space-y-2 bg-black/30 rounded-lg p-4"> | ||
<div className="w-full h-4 bg-gray-800 rounded animate-pulse" /> | ||
<div className="w-3/4 h-4 bg-gray-800 rounded animate-pulse" /> | ||
<div className="w-1/2 h-4 bg-gray-800 rounded animate-pulse" /> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
|
||
export default function SnippetsPageSkeleton() { | ||
return ( | ||
<div className="min-h-screen bg-[#0a0a0f]"> | ||
{/* Ambient background with loading pulse */} | ||
<div className="fixed inset-0 flex items-center justify-center pointer-events-none overflow-hidden"> | ||
<div className="absolute top-[20%] -left-1/4 w-96 h-96 bg-blue-500/20 rounded-full blur-3xl" /> | ||
<div className="absolute top-[20%] -right-1/4 w-96 h-96 bg-purple-500/20 rounded-full blur-3xl" /> | ||
</div> | ||
|
||
{/* Hero Section Skeleton */} | ||
<div className="relative max-w-7xl mx-auto px-4 py-12"> | ||
<div className="text-center max-w-3xl mx-auto mb-16 space-y-6"> | ||
<div className="w-48 h-8 bg-gray-800 rounded-full mx-auto animate-pulse" /> | ||
<div className="w-96 h-12 bg-gray-800 rounded-xl mx-auto animate-pulse" /> | ||
<div className="w-72 h-6 bg-gray-800 rounded-lg mx-auto animate-pulse" /> | ||
</div> | ||
|
||
{/* Search and Filters Skeleton */} | ||
<div className="max-w-5xl mx-auto mb-12 space-y-6"> | ||
{/* Search bar */} | ||
<div className="relative"> | ||
<div className="w-full h-14 bg-[#1e1e2e]/80 rounded-xl border border-[#313244] animate-pulse" /> | ||
</div> | ||
|
||
{/* Language filters */} | ||
<div className="flex flex-wrap gap-2"> | ||
{[...Array(6)].map((_, i) => ( | ||
<div | ||
key={i} | ||
className="w-24 h-8 bg-gray-800 rounded-lg animate-pulse" | ||
style={{ | ||
animationDelay: `${i * 100}ms`, | ||
}} | ||
/> | ||
))} | ||
</div> | ||
</div> | ||
|
||
{/* Grid Skeleton */} | ||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> | ||
{[...Array(6)].map((_, i) => ( | ||
<div key={i}> | ||
<CardSkeleton /> | ||
</div> | ||
))} | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.