mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-06-15 09:08:43 +02:00
refactor(nip34): Extract StatusIndicator component, improve UI layout
- Create reusable StatusIndicator component for issues/patches/PRs - Move status icon next to status text in feed renderers (not title) - Place status badge below title in detail renderers - Fix dark theme destructive color contrast (0 90% 65%) - Remove duplicate getStatusIcon/getStatusColorClass functions https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
This commit is contained in:
135
src/components/nostr/StatusIndicator.tsx
Normal file
135
src/components/nostr/StatusIndicator.tsx
Normal file
@@ -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 (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 text-sm text-muted-foreground ${className}`}
|
||||
>
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
<span>Loading...</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 px-2 py-1 text-xs font-medium border rounded-sm ${badgeClasses} ${className}`}
|
||||
>
|
||||
<StatusIcon className="size-3.5" />
|
||||
<span className="capitalize">{statusText}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Inline variant (default)
|
||||
const colorClass = getStatusColorClass(effectiveKind);
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 text-xs ${className}`}>
|
||||
<StatusIcon className={`size-3 ${colorClass}`} />
|
||||
<span className={colorClass}>{statusText}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Re-export utilities for use in feed renderers that need just the icon/color
|
||||
export { getStatusIcon, getStatusColorClass, getStatusBadgeClasses };
|
||||
@@ -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 (
|
||||
<div className="flex flex-col gap-4 p-4 max-w-3xl mx-auto">
|
||||
{/* Issue Header */}
|
||||
<header className="flex flex-col gap-4 pb-4 border-b border-border">
|
||||
{/* Status Badge */}
|
||||
<div className="flex items-center gap-3">
|
||||
{statusLoading ? (
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1.5 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<span>Loading status...</span>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium border ${statusBadgeClasses}`}
|
||||
>
|
||||
<StatusIcon className="size-4" />
|
||||
<span className="capitalize">{statusType || "open"}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<header className="flex flex-col gap-3 pb-4 border-b border-border">
|
||||
{/* Title */}
|
||||
<h1 className="text-3xl font-bold">{title || "Untitled Issue"}</h1>
|
||||
<h1 className="text-2xl font-bold">{title || "Untitled Issue"}</h1>
|
||||
|
||||
{/* Status Badge (below title) */}
|
||||
<StatusIndicator
|
||||
statusKind={currentStatus?.kind}
|
||||
loading={statusLoading}
|
||||
eventType="issue"
|
||||
variant="badge"
|
||||
/>
|
||||
|
||||
{/* Repository Link */}
|
||||
{repoAddress && (
|
||||
|
||||
@@ -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 (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* Status and Title */}
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon
|
||||
className={`size-4 flex-shrink-0 ${statusColorClass}`}
|
||||
/>
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="font-semibold text-foreground"
|
||||
>
|
||||
{title || "Untitled Issue"}
|
||||
</ClickableEventTitle>
|
||||
</div>
|
||||
{/* Title */}
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="font-semibold text-foreground"
|
||||
>
|
||||
{title || "Untitled Issue"}
|
||||
</ClickableEventTitle>
|
||||
|
||||
{/* Status label (compact) */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className={statusColorClass}>{statusType}</span>
|
||||
{repoAddress && (
|
||||
<>
|
||||
<span className="text-muted-foreground">in</span>
|
||||
<RepositoryLink repoAddress={repoAddress} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Status and Repository */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<StatusIndicator statusKind={currentStatus?.kind} eventType="issue" />
|
||||
{repoAddress && (
|
||||
<>
|
||||
<span className="text-muted-foreground">in</span>
|
||||
<RepositoryLink repoAddress={repoAddress} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Labels */}
|
||||
{labels.length > 0 && (
|
||||
<div
|
||||
className="flex
|
||||
flex-wrap
|
||||
line-clamp-2
|
||||
items-center gap-1 overflow-x-scroll my-1"
|
||||
>
|
||||
<div className="flex flex-wrap line-clamp-2 items-center gap-1 overflow-x-scroll my-1">
|
||||
{labels.map((label, idx) => (
|
||||
<Label key={idx}>{label}</Label>
|
||||
))}
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col gap-4 p-4 max-w-3xl mx-auto">
|
||||
{/* Patch Header */}
|
||||
<header className="flex flex-col gap-4 pb-4 border-b border-border">
|
||||
{/* Status Badge */}
|
||||
<div className="flex items-center gap-3">
|
||||
{statusLoading ? (
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1.5 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<span>Loading status...</span>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium border ${statusBadgeClasses}`}
|
||||
>
|
||||
<StatusIcon className="size-4" />
|
||||
<span className="capitalize">{statusType}</span>
|
||||
</span>
|
||||
)}
|
||||
<header className="flex flex-col gap-3 pb-4 border-b border-border">
|
||||
{/* Title */}
|
||||
<h1 className="text-2xl font-bold">{subject || "Untitled Patch"}</h1>
|
||||
|
||||
{/* Root badges */}
|
||||
{/* Status and Root badges (below title) */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<StatusIndicator
|
||||
statusKind={currentStatus?.kind}
|
||||
loading={statusLoading}
|
||||
eventType="patch"
|
||||
variant="badge"
|
||||
/>
|
||||
{isRoot && (
|
||||
<span className="px-3 py-1 bg-accent/20 text-accent text-sm border border-accent/30">
|
||||
<span className="px-2 py-1 bg-accent/20 text-accent text-xs border border-accent/30 rounded-sm">
|
||||
Root Patch
|
||||
</span>
|
||||
)}
|
||||
{isRootRevision && (
|
||||
<span className="px-3 py-1 bg-primary/20 text-primary text-sm border border-primary/30">
|
||||
<span className="px-2 py-1 bg-primary/20 text-primary text-xs border border-primary/30 rounded-sm">
|
||||
Root Revision
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="text-3xl font-bold">{subject || "Untitled Patch"}</h1>
|
||||
|
||||
{/* Repository Link */}
|
||||
{repoAddress && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
|
||||
@@ -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 (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Status and Subject */}
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon className={`size-4 flex-shrink-0 ${statusColorClass}`} />
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="font-semibold text-foreground"
|
||||
>
|
||||
{subject || "Untitled Patch"}
|
||||
</ClickableEventTitle>
|
||||
</div>
|
||||
{/* Subject/Title */}
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="font-semibold text-foreground"
|
||||
>
|
||||
{subject || "Untitled Patch"}
|
||||
</ClickableEventTitle>
|
||||
|
||||
{/* Metadata */}
|
||||
{/* Status and Metadata */}
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
|
||||
<span className={statusColorClass}>{statusType}</span>
|
||||
<StatusIndicator statusKind={currentStatus?.kind} eventType="patch" />
|
||||
{repoAddress && (
|
||||
<>
|
||||
<span className="text-muted-foreground">in</span>
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col gap-4 p-4 max-w-3xl mx-auto">
|
||||
{/* PR Header */}
|
||||
<header className="flex flex-col gap-4 pb-4 border-b border-border">
|
||||
{/* Status Badge */}
|
||||
<div className="flex items-center gap-3">
|
||||
{statusLoading ? (
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1.5 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<span>Loading status...</span>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium border ${statusBadgeClasses}`}
|
||||
>
|
||||
<StatusIcon className="size-4" />
|
||||
<span className="capitalize">{statusType}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<header className="flex flex-col gap-3 pb-4 border-b border-border">
|
||||
{/* Title */}
|
||||
<h1 className="text-3xl font-bold">
|
||||
<h1 className="text-2xl font-bold">
|
||||
{subject || "Untitled Pull Request"}
|
||||
</h1>
|
||||
|
||||
{/* Status Badge (below title) */}
|
||||
<StatusIndicator
|
||||
statusKind={currentStatus?.kind}
|
||||
loading={statusLoading}
|
||||
eventType="pr"
|
||||
variant="badge"
|
||||
/>
|
||||
|
||||
{/* Repository Link */}
|
||||
{repoAddress && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
|
||||
@@ -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 (
|
||||
<BaseEventContainer event={event}>
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Status and PR Title */}
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIcon className={`size-4 flex-shrink-0 ${statusColorClass}`} />
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="font-semibold text-foreground"
|
||||
>
|
||||
{subject || "Untitled Pull Request"}
|
||||
</ClickableEventTitle>
|
||||
</div>
|
||||
{/* PR Title */}
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="font-semibold text-foreground"
|
||||
>
|
||||
{subject || "Untitled Pull Request"}
|
||||
</ClickableEventTitle>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
{/* Status and Repository */}
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className={statusColorClass}>{statusType}</span>
|
||||
<StatusIndicator statusKind={currentStatus?.kind} eventType="pr" />
|
||||
{repoAddress && (
|
||||
<>
|
||||
<span className="text-muted-foreground">in</span>
|
||||
|
||||
@@ -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%;
|
||||
|
||||
Reference in New Issue
Block a user