- Extend SkateCrew with `sponsorRef` (sponsoring shop, NIP-15 stall coord) and `coordinator` (Cordn/MLS encrypted-chat Delivery Service pointer), plus parser. - CrewSponsorBanner: "Sponsored by [Shop]" banner that resolves and links the backing shop; renders nothing if the shop can't be resolved. - CrewCoordinatorCard: surfaces the crew's private-chat coordinator (E2E · MLS), explains what the coordinator can/can't see, shows the operator, and prompts the leader to set one up when absent — the UI foundation for Cordn chat. - Wire the sponsor banner (under hero) and coordinator card (sidebar) into CrewDetailPage. - CrewForm: add "Sponsored by" and "Private Chat Coordinator" inputs with validation; publish `sponsor` and `coordinator` tags. - NIP.md: document the `sponsor` and `coordinator` tags, the structured invite kind 36229 (with accept/decline flow), and the Cordn/MLS private-chat model and why it's the chosen fit for crews. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
62 lines
2.6 KiB
TypeScript
62 lines
2.6 KiB
TypeScript
import { Link } from 'react-router-dom';
|
|
import { Store, ChevronRight, Loader2, BadgeCheck } from 'lucide-react';
|
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
import { useSkateShop } from '@/hooks/useSkateShops';
|
|
import { parseAddrCoord } from '@/lib/types';
|
|
|
|
/**
|
|
* "Sponsored by [Shop]" banner for a crew backed by a local skate shop.
|
|
*
|
|
* Renders nothing if the crew has no sponsor, or the referenced shop can't be
|
|
* resolved — a sponsor should be a real, linkable shop (NIP-15 stall), not a
|
|
* dangling claim.
|
|
*/
|
|
export function CrewSponsorBanner({ sponsorRef }: { sponsorRef: string | undefined }) {
|
|
const coord = sponsorRef ? parseAddrCoord(sponsorRef) : null;
|
|
const { data: shop, isLoading } = useSkateShop(coord?.pubkey, coord?.identifier);
|
|
|
|
if (!coord) return null;
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center gap-3 rounded-2xl border border-amber-500/25 bg-amber-500/[0.05] px-4 py-3">
|
|
<Loader2 className="h-4 w-4 animate-spin text-amber-400" />
|
|
<span className="text-sm text-gray-400">Loading sponsor…</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!shop) return null;
|
|
|
|
const logo = shop.images?.[0];
|
|
|
|
return (
|
|
<Link
|
|
to={`/shop/${coord.pubkey}/${coord.identifier}`}
|
|
className="group relative flex items-center gap-3 overflow-hidden rounded-2xl border border-amber-500/30 bg-gradient-to-r from-amber-500/[0.12] via-amber-500/[0.04] to-transparent px-4 py-3 transition-all hover:border-amber-500/50 active:scale-[0.99]"
|
|
>
|
|
<div
|
|
aria-hidden
|
|
className="pointer-events-none absolute -left-10 -top-10 h-32 w-32 rounded-full bg-amber-500/20 blur-2xl"
|
|
/>
|
|
<Avatar className="relative h-12 w-12 rounded-xl border-2 border-amber-500/40">
|
|
<AvatarImage src={logo} alt={shop.name} className="object-cover" />
|
|
<AvatarFallback className="rounded-xl bg-amber-500/15 text-amber-300">
|
|
<Store className="h-5 w-5" />
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
|
|
<div className="relative min-w-0 flex-1">
|
|
<p className="flex items-center gap-1 text-[10px] font-black uppercase tracking-widest text-amber-400/80">
|
|
<BadgeCheck className="h-3 w-3" /> Sponsored by
|
|
</p>
|
|
<p className="truncate text-base font-black text-white">{shop.name}</p>
|
|
{shop.location && (
|
|
<p className="truncate text-xs text-gray-400">{shop.location}</p>
|
|
)}
|
|
</div>
|
|
|
|
<ChevronRight className="relative h-5 w-5 shrink-0 text-amber-400/60 transition-transform group-hover:translate-x-0.5 group-hover:text-amber-400" />
|
|
</Link>
|
|
);
|
|
}
|