mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-18 03:17:04 +02:00
feat: Add P2P orders renderer and detail (#116)
This commit is contained in:
173
src/components/nostr/kinds/P2pOrderDetailRenderer.tsx
Normal file
173
src/components/nostr/kinds/P2pOrderDetailRenderer.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import { UserName } from "../UserName";
|
||||
import { Copy } from "lucide-react";
|
||||
import {
|
||||
getOrderType,
|
||||
getFiatAmount,
|
||||
getSatsAmount,
|
||||
getCurrency,
|
||||
getPaymentMethods,
|
||||
getPlatform,
|
||||
getPremium,
|
||||
getOrderStatus,
|
||||
getSource,
|
||||
getBitcoinLayer,
|
||||
getBitcoinNetwork,
|
||||
getUsername,
|
||||
getExpiration,
|
||||
} from "@/lib/nip69-helpers";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface P2pOrderDetailRendererProps {
|
||||
event: NostrEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detail renderer for Kind 38383 - P2P Order
|
||||
* Shows order details and links
|
||||
*/
|
||||
export function P2pOrderDetailRenderer({ event }: P2pOrderDetailRendererProps) {
|
||||
const orderType = getOrderType(event);
|
||||
const fiatAmount = getFiatAmount(event);
|
||||
const satsAmount = getSatsAmount(event);
|
||||
const currency = getCurrency(event);
|
||||
const paymentMethods = getPaymentMethods(event);
|
||||
const platform = getPlatform(event);
|
||||
const premium = getPremium(event);
|
||||
const orderStatus = getOrderStatus(event);
|
||||
const source = getSource(event);
|
||||
const bitcoinLayer = getBitcoinLayer(event);
|
||||
const bitcoinNetwork = getBitcoinNetwork(event);
|
||||
const username = getUsername(event);
|
||||
const expiration = getExpiration(event);
|
||||
|
||||
const handleCopy = (value: string) => {
|
||||
navigator.clipboard.writeText(value);
|
||||
toast.success(`External link copied to clipboard`);
|
||||
};
|
||||
|
||||
const getTradetitle: () => string = () => {
|
||||
if (!orderType) return "-";
|
||||
|
||||
let title = `${orderType?.toLocaleUpperCase()} `;
|
||||
|
||||
const fiat = fiatAmount
|
||||
? fiatAmount.length < 2
|
||||
? fiatAmount[0]
|
||||
: `${fiatAmount[0]} - ${fiatAmount[1]}`
|
||||
: "";
|
||||
|
||||
if (satsAmount) {
|
||||
if (fiat) {
|
||||
title += `${fiat} ${currency} (${satsAmount} sats)`;
|
||||
} else {
|
||||
title += `${fiat} sats (Premium ${premium}%)`;
|
||||
}
|
||||
} else {
|
||||
title += `${fiat} ${currency} (Premium ${premium}%)`;
|
||||
}
|
||||
|
||||
return title;
|
||||
};
|
||||
|
||||
const getExpirationDate = () => {
|
||||
if (!expiration) return "-";
|
||||
|
||||
const date = new Date(expiration * 1000);
|
||||
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 max-w-4xl mx-auto">
|
||||
{/* Header Section */}
|
||||
<div className="flex gap-4">
|
||||
{/* Order Title */}
|
||||
<div className="flex flex-col g4p-2 flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<h1 className="text-3xl font-bold">{getTradetitle()}</h1>
|
||||
{orderStatus === "pending" && source && (
|
||||
<button
|
||||
onClick={() => handleCopy(source)}
|
||||
title={`Order profile in ${platform}`}
|
||||
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-primary-foreground bg-primary rounded-lg hover:bg-primary/90 transition-colors flex-shrink-0"
|
||||
>
|
||||
<Copy className="size-3" />
|
||||
{"Link"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata Grid */}
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
{/* Publisher */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Profile</h3>
|
||||
<UserName pubkey={event.pubkey} />
|
||||
</div>
|
||||
|
||||
{/* Host */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Host</h3>
|
||||
<code className="font-mono text-sm truncate" title={platform}>
|
||||
{platform ?? "-"}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Username</h3>
|
||||
<code className="font-mono text-sm truncate" title={username}>
|
||||
{username ?? "-"}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Status</h3>
|
||||
<code className="font-mono text-sm truncate" title={orderStatus}>
|
||||
{orderStatus ?? "-"}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
{/* Layer */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Layer</h3>
|
||||
<code className="font-mono text-sm truncate" title={bitcoinLayer}>
|
||||
{bitcoinLayer ?? "-"}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
{/* Network */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-muted-foreground">Network</h3>
|
||||
<code className="font-mono text-sm truncate" title={bitcoinNetwork}>
|
||||
{bitcoinNetwork ?? "-"}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Platforms Section */}
|
||||
{paymentMethods && paymentMethods.length > 0 && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-xl font-semibold">Payment Methods</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{paymentMethods.map((pm) => (
|
||||
<div className="flex items-center gap-2 px-3 py-2 bg-muted/30 rounded-lg">
|
||||
<span className="text-sm font-medium">{pm}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expiration */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="text-xl font-semibold">Expiration Date</h2>
|
||||
<div className="flex flex-wrap gap-2">{getExpirationDate()}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
149
src/components/nostr/kinds/P2pOrderRenderer.tsx
Normal file
149
src/components/nostr/kinds/P2pOrderRenderer.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import {
|
||||
BaseEventContainer,
|
||||
BaseEventProps,
|
||||
ClickableEventTitle,
|
||||
} from "./BaseEventRenderer";
|
||||
import {
|
||||
Ban,
|
||||
Check,
|
||||
ClockAlert,
|
||||
Copy,
|
||||
Layers,
|
||||
Loader,
|
||||
Scale,
|
||||
Tickets,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
getBitcoinLayer,
|
||||
getCurrency,
|
||||
getFiatAmount,
|
||||
getOrderStatus,
|
||||
getOrderType,
|
||||
getPaymentMethods,
|
||||
getPlatform,
|
||||
getPremium,
|
||||
getSatsAmount,
|
||||
getSource,
|
||||
} from "@/lib/nip69-helpers";
|
||||
import { toast } from "sonner";
|
||||
|
||||
/**
|
||||
* Renderer for Kind 38383 - P2P Order
|
||||
* Clean feed view with order details and links
|
||||
*/
|
||||
export function P2pOrderRenderer({ event }: BaseEventProps) {
|
||||
const orderType = getOrderType(event);
|
||||
const fiatAmount = getFiatAmount(event);
|
||||
const satsAmount = getSatsAmount(event);
|
||||
const currency = getCurrency(event);
|
||||
const paymentMethods = getPaymentMethods(event);
|
||||
const platform = getPlatform(event);
|
||||
const premium = getPremium(event);
|
||||
const orderStatus = getOrderStatus(event);
|
||||
const source = getSource(event);
|
||||
const bitcoinLayer = getBitcoinLayer(event);
|
||||
|
||||
const handleCopy = (value: string) => {
|
||||
navigator.clipboard.writeText(value);
|
||||
toast.success(`External link copied to clipboard`);
|
||||
};
|
||||
|
||||
const getTradetitle: () => string = () => {
|
||||
let title = `${orderType?.toLocaleUpperCase()} `;
|
||||
|
||||
const fiat = fiatAmount
|
||||
? fiatAmount.length < 2
|
||||
? fiatAmount[0]
|
||||
: `${fiatAmount[0]} - ${fiatAmount[1]}`
|
||||
: "";
|
||||
|
||||
if (satsAmount) {
|
||||
if (fiat) {
|
||||
title += `${fiat} ${currency} (${satsAmount} sats)`;
|
||||
} else {
|
||||
title += `${fiat} sats (Premium ${premium}%)`;
|
||||
}
|
||||
} else {
|
||||
title += `${fiat} ${currency} (Premium ${premium}%)`;
|
||||
}
|
||||
|
||||
return title;
|
||||
};
|
||||
|
||||
const getStatusTag = () => {
|
||||
const className = "size-3";
|
||||
const icon = {
|
||||
pending: <Tickets className={className} />,
|
||||
canceled: <Ban className={className} />,
|
||||
"in-progress": <Loader className={className} />,
|
||||
success: <Check className={className} />,
|
||||
expired: <ClockAlert className={className} />,
|
||||
};
|
||||
|
||||
return orderStatus ? (
|
||||
<>
|
||||
{icon[orderStatus]}
|
||||
<span className="text-xs text-muted-foreground">{orderStatus}</span>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseEventContainer event={event}>
|
||||
{orderType && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<ClickableEventTitle
|
||||
event={event}
|
||||
className="text-base font-semibold text-foreground"
|
||||
>
|
||||
{getTradetitle()}
|
||||
</ClickableEventTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{orderStatus === "pending" && source && (
|
||||
<button
|
||||
onClick={() => handleCopy(source)}
|
||||
className="flex items-center gap-1.5 px-2 py-1 text-xs font-medium text-primary border border-primary/20 rounded hover:bg-primary/10 transition-colors flex-shrink-0"
|
||||
title={`Order profile in ${platform}`}
|
||||
>
|
||||
<Copy className="size-3" />
|
||||
{"Link"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{orderType && (
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{paymentMethods?.join(" ")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{(platform || bitcoinLayer) && (
|
||||
<div className="flex items-center gap-2">
|
||||
{platform && (
|
||||
<>
|
||||
<Scale className="size-3" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{platform}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{bitcoinLayer && (
|
||||
<>
|
||||
<Layers className="size-3" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{bitcoinLayer}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{getStatusTag()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</BaseEventContainer>
|
||||
);
|
||||
}
|
||||
@@ -140,6 +140,8 @@ import {
|
||||
} from "./StarterPackRenderer";
|
||||
import { NostrEvent } from "@/types/nostr";
|
||||
import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer";
|
||||
import { P2pOrderRenderer } from "./P2pOrderRenderer";
|
||||
import { P2pOrderDetailRenderer } from "./P2pOrderDetailRenderer";
|
||||
|
||||
/**
|
||||
* Registry of kind-specific renderers
|
||||
@@ -215,6 +217,7 @@ const kindRenderers: Record<number, React.ComponentType<BaseEventProps>> = {
|
||||
31989: HandlerRecommendationRenderer, // Handler Recommendation (NIP-89)
|
||||
31990: ApplicationHandlerRenderer, // Application Handler (NIP-89)
|
||||
32267: ZapstoreAppRenderer, // Zapstore App
|
||||
38383: P2pOrderRenderer, // P2P Orders
|
||||
39000: GroupMetadataRenderer, // Group Metadata (NIP-29)
|
||||
39089: StarterPackRenderer, // Starter Pack (NIP-51)
|
||||
39092: MediaStarterPackRenderer, // Media Starter Pack (NIP-51)
|
||||
@@ -305,6 +308,7 @@ const detailRenderers: Record<
|
||||
31989: HandlerRecommendationDetailRenderer, // Handler Recommendation Detail (NIP-89)
|
||||
31990: ApplicationHandlerDetailRenderer, // Application Handler Detail (NIP-89)
|
||||
32267: ZapstoreAppDetailRenderer, // Zapstore App Detail
|
||||
38383: P2pOrderDetailRenderer, // P2P Order Detail
|
||||
39089: StarterPackDetailRenderer, // Starter Pack Detail (NIP-51)
|
||||
39092: MediaStarterPackDetailRenderer, // Media Starter Pack Detail (NIP-51)
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user