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 (
{/* Issue Header */} -
- {/* Status Badge */} -
- {statusLoading ? ( - - - Loading status... - - ) : ( - - - {statusType || "open"} - - )} -
- +
{/* Title */} -

{title || "Untitled Issue"}

+

{title || "Untitled Issue"}

+ + {/* Status Badge (below title) */} + {/* Repository Link */} {repoAddress && ( diff --git a/src/components/nostr/kinds/IssueRenderer.tsx b/src/components/nostr/kinds/IssueRenderer.tsx index 8d6efc3..03e9d6e 100644 --- a/src/components/nostr/kinds/IssueRenderer.tsx +++ b/src/components/nostr/kinds/IssueRenderer.tsx @@ -1,5 +1,4 @@ import { useMemo } from "react"; -import { CircleDot, CheckCircle2, XCircle, FileEdit } from "lucide-react"; import { BaseEventContainer, type BaseEventProps, @@ -10,7 +9,6 @@ import { getIssueLabels, getIssueRepositoryAddress, getRepositoryRelays, - getStatusType, getValidStatusAuthors, findCurrentStatus, } from "@/lib/nip34-helpers"; @@ -18,47 +16,11 @@ 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 { 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 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"; - } -} - /** * Renderer for Kind 1621 - Issue * Displays as a compact issue card in feed view with status @@ -143,52 +105,31 @@ export function IssueRenderer({ event }: BaseEventProps) { [statusEvents, validAuthors], ); - // Status display - const statusType = currentStatus ? getStatusType(currentStatus.kind) : "open"; - const StatusIcon = currentStatus - ? getStatusIcon(currentStatus.kind) - : CircleDot; - const statusColorClass = currentStatus - ? getStatusColorClass(currentStatus.kind) - : "text-foreground"; - return (
-
- {/* Status and Title */} -
- - - {title || "Untitled Issue"} - -
+ {/* Title */} + + {title || "Untitled Issue"} + - {/* Status label (compact) */} -
- {statusType} - {repoAddress && ( - <> - in - - - )} -
+ {/* Status and Repository */} +
+ + {repoAddress && ( + <> + in + + + )}
{/* Labels */} {labels.length > 0 && ( -
+
{labels.map((label, idx) => ( ))} diff --git a/src/components/nostr/kinds/PatchDetailRenderer.tsx b/src/components/nostr/kinds/PatchDetailRenderer.tsx index 729e03b..65593cf 100644 --- a/src/components/nostr/kinds/PatchDetailRenderer.tsx +++ b/src/components/nostr/kinds/PatchDetailRenderer.tsx @@ -1,15 +1,5 @@ import { useMemo } from "react"; -import { - GitCommit, - User, - Copy, - CopyCheck, - CircleDot, - CheckCircle2, - XCircle, - FileEdit, - Loader2, -} from "lucide-react"; +import { GitCommit, User, Copy, CopyCheck } from "lucide-react"; import { UserName } from "../UserName"; import { CodeCopyButton } from "@/components/CodeCopyButton"; import { useCopy } from "@/hooks/useCopy"; @@ -32,47 +22,11 @@ import { import { parseReplaceableAddress } from "applesauce-core/helpers/pointers"; import { getOutboxes } from "applesauce-core/helpers"; import { RepositoryLink } from "../RepositoryLink"; +import { StatusIndicator } from "../StatusIndicator"; import { useTimeline } from "@/hooks/useTimeline"; import { useNostrEvent } from "@/hooks/useNostrEvent"; 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: // 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 1617 - Patch * Displays full patch metadata and content with status @@ -158,55 +112,33 @@ export function PatchDetailRenderer({ event }: { event: NostrEvent }) { // Format created date using locale utility const createdDate = formatTimestamp(event.created_at, "long"); - // Status display - for patches, 1631 means "merged" - const statusType = currentStatus - ? currentStatus.kind === 1631 - ? "merged" - : getStatusType(currentStatus.kind) - : "open"; - const StatusIcon = currentStatus - ? getStatusIcon(currentStatus.kind) - : CircleDot; - const statusBadgeClasses = currentStatus - ? getStatusBadgeClasses(currentStatus.kind) - : "bg-muted/50 text-foreground border-border"; - return (
{/* Patch Header */} -
- {/* Status Badge */} -
- {statusLoading ? ( - - - Loading status... - - ) : ( - - - {statusType} - - )} +
+ {/* Title */} +

{subject || "Untitled Patch"}

- {/* Root badges */} + {/* Status and Root badges (below title) */} +
+ {isRoot && ( - + Root Patch )} {isRootRevision && ( - + Root Revision )}
- {/* Title */} -

{subject || "Untitled Patch"}

- {/* Repository Link */} {repoAddress && (
diff --git a/src/components/nostr/kinds/PatchRenderer.tsx b/src/components/nostr/kinds/PatchRenderer.tsx index e29fde8..60649a1 100644 --- a/src/components/nostr/kinds/PatchRenderer.tsx +++ b/src/components/nostr/kinds/PatchRenderer.tsx @@ -1,5 +1,4 @@ import { useMemo } from "react"; -import { CircleDot, CheckCircle2, XCircle, FileEdit } from "lucide-react"; import { BaseEventContainer, type BaseEventProps, @@ -10,54 +9,17 @@ import { getPatchCommitId, getPatchRepositoryAddress, getRepositoryRelays, - getStatusType, getValidStatusAuthors, findCurrentStatus, } from "@/lib/nip34-helpers"; import { parseReplaceableAddress } from "applesauce-core/helpers/pointers"; import { getOutboxes } from "applesauce-core/helpers"; import { RepositoryLink } from "../RepositoryLink"; +import { StatusIndicator } from "../StatusIndicator"; import { useTimeline } from "@/hooks/useTimeline"; import { useNostrEvent } from "@/hooks/useNostrEvent"; 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 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: // Merged - positive - return "text-accent"; - case 1632: // Closed - negative - return "text-destructive"; - case 1633: // Draft - muted - return "text-muted-foreground"; - default: - return "text-foreground"; - } -} - /** * Renderer for Kind 1617 - Patch * Displays as a compact patch card in feed view with status @@ -145,36 +107,20 @@ export function PatchRenderer({ event }: BaseEventProps) { [statusEvents, validAuthors], ); - // Status display - for patches, 1631 means "merged" not "resolved" - const statusType = currentStatus - ? currentStatus.kind === 1631 - ? "merged" - : getStatusType(currentStatus.kind) - : "open"; - const StatusIcon = currentStatus - ? getStatusIcon(currentStatus.kind) - : CircleDot; - const statusColorClass = currentStatus - ? getStatusColorClass(currentStatus.kind) - : "text-foreground"; - return (
- {/* Status and Subject */} -
- - - {subject || "Untitled Patch"} - -
+ {/* Subject/Title */} + + {subject || "Untitled Patch"} + - {/* Metadata */} + {/* Status and Metadata */}
- {statusType} + {repoAddress && ( <> in diff --git a/src/components/nostr/kinds/PullRequestDetailRenderer.tsx b/src/components/nostr/kinds/PullRequestDetailRenderer.tsx index 55d7afd..05dd726 100644 --- a/src/components/nostr/kinds/PullRequestDetailRenderer.tsx +++ b/src/components/nostr/kinds/PullRequestDetailRenderer.tsx @@ -1,15 +1,5 @@ import { useMemo } from "react"; -import { - GitBranch, - Tag, - Copy, - CopyCheck, - CircleDot, - CheckCircle2, - XCircle, - FileEdit, - Loader2, -} from "lucide-react"; +import { GitBranch, Tag, Copy, CopyCheck } from "lucide-react"; import { UserName } from "../UserName"; import { MarkdownContent } from "../MarkdownContent"; import { useCopy } from "@/hooks/useCopy"; @@ -32,47 +22,11 @@ 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 { 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: // 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 1618 - Pull Request * Displays full PR content with markdown rendering and status @@ -158,45 +112,23 @@ export function PullRequestDetailRenderer({ event }: { event: NostrEvent }) { // Format created date using locale utility const createdDate = formatTimestamp(event.created_at, "long"); - // Status display - for PRs, 1631 means "merged" - const statusType = currentStatus - ? currentStatus.kind === 1631 - ? "merged" - : getStatusType(currentStatus.kind) - : "open"; - const StatusIcon = currentStatus - ? getStatusIcon(currentStatus.kind) - : CircleDot; - const statusBadgeClasses = currentStatus - ? getStatusBadgeClasses(currentStatus.kind) - : "bg-muted/50 text-foreground border-border"; - return (
{/* PR Header */} -
- {/* Status Badge */} -
- {statusLoading ? ( - - - Loading status... - - ) : ( - - - {statusType} - - )} -
- +
{/* Title */} -

+

{subject || "Untitled Pull Request"}

+ {/* Status Badge (below title) */} + + {/* Repository Link */} {repoAddress && (
diff --git a/src/components/nostr/kinds/PullRequestRenderer.tsx b/src/components/nostr/kinds/PullRequestRenderer.tsx index 38aff45..6466a7d 100644 --- a/src/components/nostr/kinds/PullRequestRenderer.tsx +++ b/src/components/nostr/kinds/PullRequestRenderer.tsx @@ -1,11 +1,5 @@ import { useMemo } from "react"; -import { - CircleDot, - CheckCircle2, - XCircle, - FileEdit, - GitBranch, -} from "lucide-react"; +import { GitBranch } from "lucide-react"; import { BaseEventContainer, type BaseEventProps, @@ -17,7 +11,6 @@ import { getPullRequestBranchName, getPullRequestRepositoryAddress, getRepositoryRelays, - getStatusType, getValidStatusAuthors, findCurrentStatus, } from "@/lib/nip34-helpers"; @@ -25,47 +18,11 @@ 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 { 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 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: // Merged - positive - return "text-accent"; - case 1632: // Closed - negative - return "text-destructive"; - case 1633: // Draft - muted - return "text-muted-foreground"; - default: - return "text-foreground"; - } -} - /** * Renderer for Kind 1618 - Pull Request * Displays as a compact PR card in feed view with status @@ -151,37 +108,21 @@ export function PullRequestRenderer({ event }: BaseEventProps) { [statusEvents, validAuthors], ); - // Status display - for PRs, 1631 means "merged" not "resolved" - const statusType = currentStatus - ? currentStatus.kind === 1631 - ? "merged" - : getStatusType(currentStatus.kind) - : "open"; - const StatusIcon = currentStatus - ? getStatusIcon(currentStatus.kind) - : CircleDot; - const statusColorClass = currentStatus - ? getStatusColorClass(currentStatus.kind) - : "text-foreground"; - return (
- {/* Status and PR Title */} -
- - - {subject || "Untitled Pull Request"} - -
+ {/* PR Title */} + + {subject || "Untitled Pull Request"} +
{/* Status and Repository */}
- {statusType} + {repoAddress && ( <> in diff --git a/src/index.css b/src/index.css index 5d7637e..3d10fc4 100644 --- a/src/index.css +++ b/src/index.css @@ -115,7 +115,7 @@ --muted-foreground: 215 20.2% 70%; --accent: 270 100% 70%; --accent-foreground: 222.2 84% 4.9%; - --destructive: 0 72% 50%; + --destructive: 0 90% 65%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%;