+
!working && onClose()}
+ aria-hidden
+ />
+
+
+ Report comment
+
+
+ Reports are private and reviewed by our team.
+
+
+ {REPORT_CATEGORIES.map((c) => (
+
+ ))}
+
+
+
+ );
+}
+
+// ───────────────────────────────────────────────────────────────────────────
+
+function Avatar({ src, name }: { src: string | null; name: string }) {
+ if (src) {
+ return (
+
+ );
+ }
+ const initials =
+ name
+ .replace(/^@/, "")
+ .split(/\s+/)
+ .map((p) => p[0])
+ .slice(0, 2)
+ .join("")
+ .toUpperCase() || "?";
+ return (
+
+ {initials}
+
+ );
+}
+
+/** Compact relative time ("just now", "5m ago", "3h ago", "2d ago"). */
+function timeAgo(iso: string): string {
+ const then = new Date(iso).getTime();
+ if (Number.isNaN(then)) return "";
+ const secs = Math.max(0, Math.floor((Date.now() - then) / 1000));
+ if (secs < 45) return "just now";
+ const mins = Math.floor(secs / 60);
+ if (mins < 60) return `${mins}m ago`;
+ const hours = Math.floor(mins / 60);
+ if (hours < 24) return `${hours}h ago`;
+ const days = Math.floor(hours / 24);
+ if (days < 7) return `${days}d ago`;
+ return new Date(iso).toLocaleDateString();
+}
diff --git a/components/ui/drawer.tsx b/components/ui/drawer.tsx
index 5d21c474..9715c2a0 100644
--- a/components/ui/drawer.tsx
+++ b/components/ui/drawer.tsx
@@ -46,10 +46,14 @@ const DrawerContent = React.forwardRef<
React.ComponentRef
,
React.ComponentPropsWithoutRef & {
direction?: "top" | "bottom" | "left" | "right";
+ /** Override the scrim styling — e.g. "bg-transparent" for a see-through
+ * overlay that keeps the content behind the drawer visible while still
+ * catching outside clicks to dismiss. */
+ overlayClassName?: string;
}
->(({ className, children, direction = "bottom", ...props }, ref) => (
+>(({ className, children, direction = "bottom", overlayClassName, ...props }, ref) => (
-
+
(
+ `/posts/${postId}/comments?${commentPageQuery(params)}`,
+ );
+ },
+ /** A thread's replies, oldest first. Public. */
+ replies(commentId: string, params: CommentPageParams = {}) {
+ return call(
+ `/comments/${commentId}/replies?${commentPageQuery(params)}`,
+ );
+ },
+ /** Post a comment, or a reply when `parentId` is set. */
+ create(postId: string, body: string, parentId?: string) {
+ return call(`/posts/${postId}/comments`, {
+ method: "POST",
+ body: JSON.stringify({ body, parent_id: parentId ?? null }),
+ });
+ },
+ /** Like / unlike a comment; returns the new like count. */
+ like(commentId: string) {
+ return call<{ likes_count: number }>(`/comments/${commentId}/like`, {
+ method: "POST",
+ });
+ },
+ unlike(commentId: string) {
+ return call<{ likes_count: number }>(`/comments/${commentId}/like`, {
+ method: "DELETE",
+ });
+ },
+ /** Pin / unpin a comment (post author only; top-level only). */
+ pin(commentId: string) {
+ return call(`/comments/${commentId}/pin`, { method: "POST" });
+ },
+ unpin(commentId: string) {
+ return call(`/comments/${commentId}/pin`, { method: "DELETE" });
+ },
+ /** Delete a comment (author or post owner). Removes the whole thread. */
+ delete(commentId: string) {
+ return call(`/comments/${commentId}`, { method: "DELETE" });
+ },
+ /** Report a comment. */
+ report(commentId: string, category: PostReportCategory, reason: string) {
+ return call(`/comments/${commentId}/report`, {
+ method: "POST",
+ body: JSON.stringify({ category, reason }),
+ });
+ },
+};
+
// ─── Bookmarks ───────────────────────────────────────────────────────────
export const bookmarks = {
@@ -486,6 +574,10 @@ export type AppNotification = {
* row represents (renders "X and N-1 others liked your post"). */
post_id?: string | null;
actor_count?: number;
+ /** Handle of the post's author (when the row references a post), so comment
+ * / reply notifications can deep-link to the post even though the actor
+ * isn't its owner. */
+ post_author_handle?: string | null;
};
export const notifications = {