diff --git a/src/components/nostr/StatusIndicator.tsx b/src/components/nostr/StatusIndicator.tsx
new file mode 100644
index 0000000..af34600
--- /dev/null
+++ b/src/components/nostr/StatusIndicator.tsx
@@ -0,0 +1,135 @@
+import {
+ CircleDot,
+ CheckCircle2,
+ XCircle,
+ FileEdit,
+ Loader2,
+} from "lucide-react";
+import { getStatusType } from "@/lib/nip34-helpers";
+
+/**
+ * Get the icon component for a status kind
+ */
+function getStatusIcon(kind: number) {
+ switch (kind) {
+ case 1630:
+ return CircleDot;
+ case 1631:
+ return CheckCircle2;
+ case 1632:
+ return XCircle;
+ case 1633:
+ return FileEdit;
+ default:
+ return CircleDot;
+ }
+}
+
+/**
+ * Get the color class for a status kind
+ * Uses theme semantic colors
+ */
+function getStatusColorClass(kind: number): string {
+ switch (kind) {
+ case 1630: // Open - neutral
+ return "text-foreground";
+ case 1631: // Resolved/Merged - positive
+ return "text-accent";
+ case 1632: // Closed - negative
+ return "text-destructive";
+ case 1633: // Draft - muted
+ return "text-muted-foreground";
+ default:
+ return "text-foreground";
+ }
+}
+
+/**
+ * Get the background/border classes for a status badge
+ * Uses theme semantic colors
+ */
+function getStatusBadgeClasses(kind: number): string {
+ switch (kind) {
+ case 1630: // Open - neutral
+ return "bg-muted/50 text-foreground border-border";
+ case 1631: // Resolved/Merged - positive
+ return "bg-accent/20 text-accent border-accent/30";
+ case 1632: // Closed - negative
+ return "bg-destructive/20 text-destructive border-destructive/30";
+ case 1633: // Draft - muted
+ return "bg-muted text-muted-foreground border-muted-foreground/30";
+ default:
+ return "bg-muted/50 text-foreground border-border";
+ }
+}
+
+export interface StatusIndicatorProps {
+ /** The status event kind (1630-1633) or undefined for default "open" */
+ statusKind?: number;
+ /** Whether status is loading */
+ loading?: boolean;
+ /** Event type for appropriate labeling (affects "resolved" vs "merged") */
+ eventType?: "issue" | "patch" | "pr";
+ /** Display variant */
+ variant?: "inline" | "badge";
+ /** Optional custom class */
+ className?: string;
+}
+
+/**
+ * Reusable status indicator for NIP-34 events (issues, patches, PRs)
+ * Displays status icon and text with appropriate styling
+ */
+export function StatusIndicator({
+ statusKind,
+ loading = false,
+ eventType = "issue",
+ variant = "inline",
+ className = "",
+}: StatusIndicatorProps) {
+ if (loading) {
+ return (
+
+
+ Loading...
+
+ );
+ }
+
+ // Default to "open" if no status
+ const effectiveKind = statusKind ?? 1630;
+
+ // For patches/PRs, kind 1631 means "merged" not "resolved"
+ const statusText =
+ effectiveKind === 1631 && (eventType === "patch" || eventType === "pr")
+ ? "merged"
+ : getStatusType(effectiveKind) || "open";
+
+ const StatusIcon = getStatusIcon(effectiveKind);
+
+ if (variant === "badge") {
+ const badgeClasses = getStatusBadgeClasses(effectiveKind);
+ return (
+
+
+ {statusText}
+
+ );
+ }
+
+ // Inline variant (default)
+ const colorClass = getStatusColorClass(effectiveKind);
+ return (
+
+
+ {statusText}
+
+ );
+}
+
+// Re-export utilities for use in feed renderers that need just the icon/color
+export { getStatusIcon, getStatusColorClass, getStatusBadgeClasses };
diff --git a/src/components/nostr/kinds/IssueDetailRenderer.tsx b/src/components/nostr/kinds/IssueDetailRenderer.tsx
index 41b0ef2..c891d36 100644
--- a/src/components/nostr/kinds/IssueDetailRenderer.tsx
+++ b/src/components/nostr/kinds/IssueDetailRenderer.tsx
@@ -1,12 +1,5 @@
import { useMemo } from "react";
-import {
- Tag,
- CircleDot,
- CheckCircle2,
- XCircle,
- FileEdit,
- Loader2,
-} from "lucide-react";
+import { Tag } from "lucide-react";
import { UserName } from "../UserName";
import { MarkdownContent } from "../MarkdownContent";
import type { NostrEvent } from "@/types/nostr";
@@ -23,48 +16,12 @@ import { parseReplaceableAddress } from "applesauce-core/helpers/pointers";
import { getOutboxes } from "applesauce-core/helpers";
import { Label } from "@/components/ui/label";
import { RepositoryLink } from "../RepositoryLink";
+import { StatusIndicator } from "../StatusIndicator";
import { useTimeline } from "@/hooks/useTimeline";
import { useNostrEvent } from "@/hooks/useNostrEvent";
import { formatTimestamp } from "@/hooks/useLocale";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
-/**
- * Get the icon for a status kind
- */
-function getStatusIcon(kind: number) {
- switch (kind) {
- case 1630:
- return CircleDot;
- case 1631:
- return CheckCircle2;
- case 1632:
- return XCircle;
- case 1633:
- return FileEdit;
- default:
- return CircleDot;
- }
-}
-
-/**
- * Get the color classes for a status badge
- * Uses theme semantic colors
- */
-function getStatusBadgeClasses(kind: number): string {
- switch (kind) {
- case 1630: // Open - neutral
- return "bg-muted/50 text-foreground border-border";
- case 1631: // Resolved/Merged - positive
- return "bg-accent/20 text-accent border-accent/30";
- case 1632: // Closed - negative
- return "bg-destructive/20 text-destructive border-destructive/30";
- case 1633: // Draft - muted
- return "bg-muted text-muted-foreground border-muted-foreground/30";
- default:
- return "bg-muted/50 text-foreground border-border";
- }
-}
-
/**
* Detail renderer for Kind 1621 - Issue (NIP-34)
* Full view with repository context and markdown description
@@ -153,38 +110,20 @@ export function IssueDetailRenderer({ event }: { event: NostrEvent }) {
// Format created date using locale utility
const createdDate = formatTimestamp(event.created_at, "long");
- // Get status display info
- const statusType = currentStatus ? getStatusType(currentStatus.kind) : null;
- const StatusIcon = currentStatus
- ? getStatusIcon(currentStatus.kind)
- : CircleDot;
- const statusBadgeClasses = currentStatus
- ? getStatusBadgeClasses(currentStatus.kind)
- : "bg-muted/50 text-foreground border-border";
-
return (