diff --git a/convex/schema.ts b/convex/schema.ts
index 7ceb9b3..cb5ca2d 100644
--- a/convex/schema.ts
+++ b/convex/schema.ts
@@ -36,8 +36,8 @@ export default defineSchema({
content: v.string(),
}).index("by_snippet_id", ["snippetId"]),
- stars: defineTable({
- userId: v.id("users"),
+ stars: defineTable({
+ userId: v.string(),
snippetId: v.id("snippets"),
})
.index("by_user_id", ["userId"])
diff --git a/convex/snippets.ts b/convex/snippets.ts
index e33112c..b48f588 100644
--- a/convex/snippets.ts
+++ b/convex/snippets.ts
@@ -1,5 +1,5 @@
import { v } from "convex/values";
-import { mutation } from "./_generated/server";
+import { mutation, query } from "./_generated/server";
export const createSnippet = mutation({
args: {
@@ -30,3 +30,112 @@ export const createSnippet = mutation({
return snippetId;
},
});
+
+export const getSnippets = query({
+ handler: async (ctx) => {
+ const snippets = await ctx.db.query("snippets").order("desc").collect();
+ return snippets;
+ }
+})
+
+export const isSnippetStarred = query({
+ args: {
+ snippetId: v.id("snippets")
+ },
+ handler: async (ctx, args) => {
+ const identity = await ctx.auth.getUserIdentity();
+ if (!identity) return false;
+
+ const star = await ctx.db
+ .query("stars")
+ .withIndex("by_user_id_and_snippet_id")
+ .filter((q)=> q.eq(q.field("userId"),identity.subject) && q.eq(q.field("snippetId"),args.snippetId))
+ .first();
+
+ return !!star;
+ }
+})
+
+export const getSnippetStarCount = query({
+ args: {snippetId: v.id("snippets")},
+ handler: async (ctx, args) => {
+ const stars = await ctx.db
+ .query("stars")
+ .withIndex("by_snippet_id")
+ .filter((q)=>q.eq(q.field("snippetId"),args.snippetId))
+ .collect();
+
+ return stars.length;
+
+ }
+})
+
+
+
+export const deleteSnippet = mutation({
+ args: {
+ snippetId: v.id("snippets"),
+ },
+
+ handler: async (ctx, args) => {
+ const identity = await ctx.auth.getUserIdentity();
+ if (!identity) throw new Error("Not authenticated");
+
+ const snippet = await ctx.db.get(args.snippetId);
+ if (!snippet) throw new Error("Snippet not found");
+
+ if (snippet.userId !== identity.subject) {
+ throw new Error("Not authorized to delete this snippet");
+ }
+
+ const comments = await ctx.db
+ .query("snippetComments")
+ .withIndex("by_snippet_id")
+ .filter((q) => q.eq(q.field("snippetId"), args.snippetId))
+ .collect();
+
+ for (const comment of comments) {
+ await ctx.db.delete(comment._id);
+ }
+
+ const stars = await ctx.db
+ .query("stars")
+ .withIndex("by_snippet_id")
+ .filter((q) => q.eq(q.field("snippetId"), args.snippetId))
+ .collect();
+
+ for (const star of stars) {
+ await ctx.db.delete(star._id);
+ }
+
+ await ctx.db.delete(args.snippetId);
+ },
+});
+
+export const starSnippet = mutation({
+ args: {
+ snippetId: v.id("snippets"),
+ },
+ handler: async (ctx, args) => {
+ const identity = await ctx.auth.getUserIdentity();
+ if (!identity) throw new Error("Not authenticated");
+
+ const existing = await ctx.db
+ .query("stars")
+ .withIndex("by_user_id_and_snippet_id")
+ .filter(
+ (q) =>
+ q.eq(q.field("userId"), identity.subject) && q.eq(q.field("snippetId"), args.snippetId)
+ )
+ .first();
+
+ if (existing) {
+ await ctx.db.delete(existing._id);
+ } else {
+ await ctx.db.insert("stars", {
+ userId: identity.subject,
+ snippetId: args.snippetId,
+ });
+ }
+ },
+});
\ No newline at end of file
diff --git a/src/app/snippets/_components/SnippetCard.tsx b/src/app/snippets/_components/SnippetCard.tsx
new file mode 100644
index 0000000..a8f52c0
--- /dev/null
+++ b/src/app/snippets/_components/SnippetCard.tsx
@@ -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 (
+
+
+
+
+ {/* Header */}
+
+
+
+
+
+ {snippet.language}
+
+
+
+ {new Date(snippet._creationTime).toLocaleDateString()}
+
+
+
+
e.preventDefault()}
+ >
+
+
+ {user?.id === snippet.userId && (
+
e.preventDefault()}>
+
+
+ )}
+
+
+
+ {/* Content */}
+
+
+
+ {snippet.title}
+
+
+
+
+
+
+
{snippet.userName}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+export default SnippetCard;
\ No newline at end of file
diff --git a/src/app/snippets/_components/SnippetPageSkeleton.tsx b/src/app/snippets/_components/SnippetPageSkeleton.tsx
new file mode 100644
index 0000000..9f1b15f
--- /dev/null
+++ b/src/app/snippets/_components/SnippetPageSkeleton.tsx
@@ -0,0 +1,83 @@
+const CardSkeleton = () => (
+
+
+
+ {/* Header shimmer */}
+
+
+ {/* Title shimmer */}
+
+
+ {/* Code block shimmer */}
+
+
+
+
+ );
+
+ export default function SnippetsPageSkeleton() {
+ return (
+
+ {/* Ambient background with loading pulse */}
+
+
+ {/* Hero Section Skeleton */}
+
+
+
+ {/* Search and Filters Skeleton */}
+
+ {/* Search bar */}
+
+
+ {/* Language filters */}
+
+ {[...Array(6)].map((_, i) => (
+
+ ))}
+
+
+
+ {/* Grid Skeleton */}
+
+ {[...Array(6)].map((_, i) => (
+
+
+
+ ))}
+
+
+
+ );
+ }
\ No newline at end of file
diff --git a/src/app/snippets/page.tsx b/src/app/snippets/page.tsx
new file mode 100644
index 0000000..5f237c3
--- /dev/null
+++ b/src/app/snippets/page.tsx
@@ -0,0 +1,227 @@
+"use client";
+
+import { useQuery } from "convex/react";
+import React from "react";
+import { api } from "../../../convex/_generated/api";
+import SnippetsPageSkeleton from "./_components/SnippetPageSkeleton";
+import NavigationHeader from "@/components/ui/NavigationHeader";
+import { AnimatePresence, motion } from "framer-motion";
+import { BookOpen, Code, Grid, Layers, Search, Tag, X } from "lucide-react";
+import SnippetCard from "./_components/SnippetCard";
+
+function SnippetsPage() {
+ const snippets = useQuery(api.snippets.getSnippets);
+ const [searchQuery, setSearchQuery] = React.useState("");
+ const [selectedLanguage, setSelectedLanguage] = React.useState(
+ null
+ );
+ const [view, setView] = React.useState<"grid" | "list">("grid");
+
+ if (snippets === undefined) {
+ return (
+
+
+
+
+ );
+ }
+
+ const languages = [...new Set(snippets.map((s) => s.language))];
+ const popularLanguages = languages.slice(0, 5);
+
+ const filteredSnippets = snippets.filter((snippet) => {
+ const matchesSearch =
+ snippet.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ snippet.language.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ snippet.userName.toLowerCase().includes(searchQuery.toLowerCase());
+
+ const matchesLanguage =
+ !selectedLanguage || snippet.language === selectedLanguage;
+
+ return matchesSearch && matchesLanguage;
+ });
+
+ return (
+
+
+
+
+
+
+ Community Code Library
+
+
+ Discover & Share Code Snippets
+
+
+ Explore a curated collection of code snippets from the community
+
+
+
+
+
+ {/* Search */}
+
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Search snippets by title, language, or author..."
+ className="w-full pl-12 pr-4 py-4 bg-[#1e1e2e]/80 hover:bg-[#1e1e2e] text-white
+ rounded-xl border border-[#313244] hover:border-[#414155] transition-all duration-200
+ placeholder:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
+ />
+
+
+
+ {/* Filters Bar */}
+
+
+
+ Languages:
+
+
+ {popularLanguages.map((lang) => (
+
+ ))}
+
+ {selectedLanguage && (
+
+ )}
+
+
+
+ {filteredSnippets.length} snippets found
+
+
+ {/* View Toggle */}
+
+
+
+
+
+
+
+
+
+
+ {filteredSnippets.map((snippet) => (
+
+ ))}
+
+
+
+ {/* edge case: empty state */}
+ {filteredSnippets.length === 0 && (
+
+
+
+
+
+
No snippets found
+
+ {searchQuery || selectedLanguage
+ ? "Try adjusting your search query or filters"
+ : "Be the first to share a code snippet with the community"}
+
+
+ {(searchQuery || selectedLanguage) && (
+
+ )}
+
+
+ )}
+
+
+ );
+}
+
+export default SnippetsPage;
diff --git a/src/components/ui/NavigationHeader.tsx b/src/components/ui/NavigationHeader.tsx
new file mode 100644
index 0000000..b970c50
--- /dev/null
+++ b/src/components/ui/NavigationHeader.tsx
@@ -0,0 +1,82 @@
+import HeaderProfileBtn from "@/app/(home)/_components/HeaderProfileBtn";
+import { SignedOut } from "@clerk/nextjs";
+import { Blocks, Code2, Sparkles } from "lucide-react";
+import Link from "next/link";
+
+function NavigationHeader() {
+ return (
+
+
+
+
+
+ {/* Logo */}
+
+ {/* logo hover effect */}
+
+
+ {/* Logo */}
+
+
+
+
+
+
+ CodeCraft
+
+
+ Interactive Code Editor
+
+
+
+
+ {/* snippets Link */}
+
+
+
+
+ Snippets
+
+
+
+
+ {/* right rection */}
+
+
+
+
+
+ Pro
+
+
+
+
+ {/* profile button */}
+
+
+
+
+
+ );
+}
+
+export default NavigationHeader;
\ No newline at end of file
diff --git a/src/components/ui/StarButton.tsx b/src/components/ui/StarButton.tsx
new file mode 100644
index 0000000..f50c2a6
--- /dev/null
+++ b/src/components/ui/StarButton.tsx
@@ -0,0 +1,39 @@
+import { useAuth } from "@clerk/nextjs";
+import { useMutation, useQuery } from "convex/react";
+import { Star } from "lucide-react";
+import { api } from "../../../convex/_generated/api";
+import { Id } from "../../../convex/_generated/dataModel";
+
+function StarButton({ snippetId }: { snippetId: Id<"snippets"> }) {
+ const { isSignedIn } = useAuth();
+
+ const isStarred = useQuery(api.snippets.isSnippetStarred, { snippetId });
+ const starCount = useQuery(api.snippets.getSnippetStarCount, { snippetId });
+ const star = useMutation(api.snippets.starSnippet);
+
+ const handleStar = async () => {
+ if (!isSignedIn) return;
+ await star({ snippetId });
+ };
+
+ return (
+
+ );
+}
+
+export default StarButton;
\ No newline at end of file