Build a full-featured comments system in Google Antigravity with nested replies and reactions.
# Comments System for Google Antigravity
Build a complete comments system with nested replies, reactions, and moderation.
## Database Schema
```sql
CREATE TABLE public.comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_id UUID REFERENCES public.posts(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
parent_id UUID REFERENCES public.comments(id) ON DELETE CASCADE,
content TEXT NOT NULL,
edited_at TIMESTAMPTZ,
deleted BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE public.comment_reactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
comment_id UUID REFERENCES public.comments(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
reaction TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(comment_id, user_id, reaction)
);
CREATE INDEX idx_comments_post ON public.comments(post_id, created_at);
CREATE INDEX idx_comments_parent ON public.comments(parent_id);
CREATE INDEX idx_reactions_comment ON public.comment_reactions(comment_id);
ALTER TABLE public.comments ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Comments are viewable" ON public.comments FOR SELECT USING (true);
CREATE POLICY "Users can insert comments" ON public.comments FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own comments" ON public.comments FOR UPDATE USING (auth.uid() = user_id);
```
## Comments Hook
```typescript
// hooks/useComments.ts
"use client";
import { useState, useEffect, useCallback } from "react";
import { createClient } from "@/lib/supabase/client";
interface Comment {
id: string;
post_id: string;
user_id: string;
parent_id: string | null;
content: string;
created_at: string;
edited_at: string | null;
deleted: boolean;
user: { id: string; full_name: string; avatar_url: string };
reactions: { reaction: string; count: number }[];
replies?: Comment[];
}
export function useComments(postId: string) {
const [comments, setComments] = useState<Comment[]>([]);
const [loading, setLoading] = useState(true);
const supabase = createClient();
const fetchComments = useCallback(async () => {
const { data } = await supabase.from("comments").select(`*, user:profiles(id, full_name, avatar_url), reactions:comment_reactions(reaction)`).eq("post_id", postId).order("created_at", { ascending: true });
const nested = nestComments(data || []);
setComments(nested);
setLoading(false);
}, [postId, supabase]);
useEffect(() => {
fetchComments();
const channel = supabase.channel(`comments:${postId}`).on("postgres_changes", { event: "*", schema: "public", table: "comments", filter: `post_id=eq.${postId}` }, () => { fetchComments(); }).subscribe();
return () => { channel.unsubscribe(); };
}, [postId, fetchComments, supabase]);
const addComment = async (content: string, parentId?: string) => {
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error("Not authenticated");
await supabase.from("comments").insert({ post_id: postId, user_id: user.id, content, parent_id: parentId });
};
const editComment = async (id: string, content: string) => {
await supabase.from("comments").update({ content, edited_at: new Date().toISOString() }).eq("id", id);
};
const deleteComment = async (id: string) => {
await supabase.from("comments").update({ deleted: true, content: "[deleted]" }).eq("id", id);
};
return { comments, loading, addComment, editComment, deleteComment };
}
function nestComments(flat: Comment[]): Comment[] {
const map = new Map<string, Comment>();
const roots: Comment[] = [];
flat.forEach((c) => map.set(c.id, { ...c, replies: [] }));
flat.forEach((c) => {
const comment = map.get(c.id)!;
if (c.parent_id) map.get(c.parent_id)?.replies?.push(comment);
else roots.push(comment);
});
return roots;
}
```
## Comment Component
```typescript
// components/Comment.tsx
"use client";
import { useState } from "react";
import { formatDistanceToNow } from "date-fns";
interface CommentProps {
comment: any;
onReply: (parentId: string, content: string) => void;
onEdit: (id: string, content: string) => void;
onDelete: (id: string) => void;
currentUserId?: string;
depth?: number;
}
export function Comment({ comment, onReply, onEdit, onDelete, currentUserId, depth = 0 }: CommentProps) {
const [replying, setReplying] = useState(false);
const [editing, setEditing] = useState(false);
const [content, setContent] = useState(comment.content);
const [replyContent, setReplyContent] = useState("");
const handleReply = () => {
onReply(comment.id, replyContent);
setReplyContent("");
setReplying(false);
};
const handleEdit = () => {
onEdit(comment.id, content);
setEditing(false);
};
const isOwner = currentUserId === comment.user_id;
return (
<div className="comment" style={{ marginLeft: depth * 20 }}>
<div className="comment-header">
<img src={comment.user.avatar_url} alt="" className="avatar" />
<span className="name">{comment.user.full_name}</span>
<span className="time">{formatDistanceToNow(new Date(comment.created_at))} ago</span>
{comment.edited_at && <span className="edited">(edited)</span>}
</div>
{editing ? (
<div className="edit-form">
<textarea value={content} onChange={(e) => setContent(e.target.value)} />
<button onClick={handleEdit}>Save</button>
<button onClick={() => setEditing(false)}>Cancel</button>
</div>
) : (
<p className="comment-content">{comment.content}</p>
)}
<div className="comment-actions">
<button onClick={() => setReplying(!replying)}>Reply</button>
{isOwner && !comment.deleted && (
<>
<button onClick={() => setEditing(true)}>Edit</button>
<button onClick={() => onDelete(comment.id)}>Delete</button>
</>
)}
</div>
{replying && (
<div className="reply-form">
<textarea value={replyContent} onChange={(e) => setReplyContent(e.target.value)} placeholder="Write a reply..." />
<button onClick={handleReply}>Submit</button>
</div>
)}
{comment.replies?.map((reply: any) => (
<Comment key={reply.id} comment={reply} onReply={onReply} onEdit={onEdit} onDelete={onDelete} currentUserId={currentUserId} depth={depth + 1} />
))}
</div>
);
}
```
## Comments Section
```typescript
// components/CommentsSection.tsx
"use client";
import { useState } from "react";
import { useComments } from "@/hooks/useComments";
import { Comment } from "./Comment";
import { useAuth } from "@/components/auth/AuthProvider";
export function CommentsSection({ postId }: { postId: string }) {
const { comments, loading, addComment, editComment, deleteComment } = useComments(postId);
const { user } = useAuth();
const [newComment, setNewComment] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!newComment.trim()) return;
await addComment(newComment);
setNewComment("");
};
if (loading) return <div>Loading comments...</div>;
return (
<div className="comments-section">
<h3>Comments ({comments.length})</h3>
{user && (
<form onSubmit={handleSubmit}>
<textarea value={newComment} onChange={(e) => setNewComment(e.target.value)} placeholder="Write a comment..." />
<button type="submit">Post Comment</button>
</form>
)}
{comments.map((comment) => (
<Comment key={comment.id} comment={comment} onReply={(parentId, content) => addComment(content, parentId)} onEdit={editComment} onDelete={deleteComment} currentUserId={user?.id} />
))}
</div>
);
}
```
## Best Practices
1. **Nested Structure**: Support nested replies with depth limit
2. **Real-time**: Use Supabase Realtime for live updates
3. **Soft Delete**: Use soft delete to preserve thread structure
4. **Moderation**: Add reporting and admin moderation
5. **Pagination**: Paginate for posts with many commentsThis comments prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their comments implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For comments projects, consider mentioning your framework version, coding style, and any specific libraries you're using.