From 9ed2c12de185d8de4bb1a6c57e2b5c6767e2d5a2 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Thu, 18 Jun 2026 15:12:37 +0700 Subject: [PATCH] fix: remove sponsor mention + add Nostr preview modal before publishing - "Sponsors see the organic number" -> "Users can now see both numbers" - PublishToNostrButton now opens a preview modal first (NostrPreviewModal): - Mock Nostr post card: avatar, display name, timestamp, post body - nostrText rendered with hashtags in blue and URL underlined - Fake engagement bar (replies/reposts/zaps at 0) for visual context - Character count shown below the card - Two actions: "Edit first" (closes modal) / "Publish" (confirms) - Full-width on mobile, correct safe-area handling via Dialog component - isPending state flows through so button shows "Publishing..." while signing Co-authored-by: shakespeare.diy --- src/pages/NewsletterPage.tsx | 153 ++++++++++++++++++++++++++++++++--- 1 file changed, 141 insertions(+), 12 deletions(-) diff --git a/src/pages/NewsletterPage.tsx b/src/pages/NewsletterPage.tsx index d537f85..ae2bac9 100644 --- a/src/pages/NewsletterPage.tsx +++ b/src/pages/NewsletterPage.tsx @@ -5,15 +5,25 @@ import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Link } from 'react-router-dom'; import { Skeleton } from '@/components/ui/skeleton'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, +} from '@/components/ui/dialog'; import { Zap, MapPin, Shield, Layers, Crown, Radio, ArrowRight, Share2, CheckCircle2, Users, Store, Calendar, - GraduationCap, Route, + GraduationCap, Route, Eye, Send, } from 'lucide-react'; +import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useGrowthStats } from '@/hooks/useGrowthStats'; import { useToast } from '@/hooks/useToast'; +import { genUserName } from '@/lib/genUserName'; +import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; // ─── Admin pubkeys allowed to publish newsletter as kind 1 ─────────────────── @@ -128,7 +138,7 @@ export const NEWSLETTER_ISSUES: NewsletterIssue[] = [ icon: 'feature', title: 'Trusted count on community stats', tag: 'Transparency', - body: "The stats page now shows spots from 'active accounts' — pubkeys that have published at least 2 items — alongside the raw total. Sponsors see the organic number.", + body: "The stats page now shows spots from 'active accounts' — pubkeys that have published at least 2 items — alongside the raw total. Users can now see both the raw number and the organic number side by side.", }, { icon: 'shield', @@ -156,16 +166,125 @@ https://skate-spots.org/newsletter }, ]; -// ─── Publish to Nostr button ────────────────────────────────────────────────── +// ─── Nostr post preview + publish ──────────────────────────────────────────── + +function NostrPreviewModal({ + issue, + open, + onClose, + onConfirm, + isPending, +}: { + issue: NewsletterIssue; + open: boolean; + onClose: () => void; + onConfirm: () => void; + isPending: boolean; +}) { + const { user } = useCurrentUser(); + const author = useAuthor(user?.pubkey ?? ''); + const metadata = author.data?.metadata; + const displayName = metadata?.name ?? genUserName(user?.pubkey ?? ''); + const avatar = metadata?.picture; + const now = new Date(); + const timeStr = now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); + + // Render the nostrText with hashtags highlighted and URL as a link-style span + const renderPreviewText = (text: string) => { + return text.split('\n').map((line, i) => { + const parts = line.split(/(\s+)/).map((word, j) => { + if (word.startsWith('#')) { + return {word}; + } + if (word.startsWith('https://')) { + return {word}; + } + return {word}; + }); + return

{parts}

; + }); + }; + + return ( + + + + + + Preview — Drop #{String(issue.number).padStart(3, '0')} + + + This is how the note will appear on most Nostr clients. Formatting varies by client. + + + + {/* Mock Nostr post card */} +
+ {/* Post header */} +
+ + + + {displayName.slice(0, 2).toUpperCase()} + + +
+

{displayName}

+

{timeStr} · via skate-spots.org

+
+
+ + {/* Post body */} +
+ {renderPreviewText(issue.nostrText)} +
+ + {/* Fake engagement bar */} +
+ 💬 0 + 🔁 0 + ⚡ 0 +
+
+ + {/* Character count */} +

+ {issue.nostrText.length} characters +

+ + {/* Actions */} +
+ + +
+
+
+ ); +} function PublishToNostrButton({ issue }: { issue: NewsletterIssue }) { const { user } = useCurrentUser(); const { mutateAsync: publishEvent, isPending } = useNostrPublish(); const { toast } = useToast(); const [published, setPublished] = useState(false); + const [showPreview, setShowPreview] = useState(false); const isAdmin = NEWSLETTER_ADMIN_PUBKEYS.includes(user?.pubkey ?? ''); if (!user || !isAdmin) return null; + if (published) { return (
@@ -175,7 +294,7 @@ function PublishToNostrButton({ issue }: { issue: NewsletterIssue }) { ); } - const handlePublish = async () => { + const handleConfirmPublish = async () => { try { await publishEvent({ kind: 1, @@ -191,6 +310,7 @@ function PublishToNostrButton({ issue }: { issue: NewsletterIssue }) { ], }); setPublished(true); + setShowPreview(false); toast({ title: 'Published!', description: `Drop #${String(issue.number).padStart(3, '0')} posted to Nostr.` }); } catch (err) { toast({ @@ -202,14 +322,23 @@ function PublishToNostrButton({ issue }: { issue: NewsletterIssue }) { }; return ( - + <> + + + setShowPreview(false)} + onConfirm={handleConfirmPublish} + isPending={isPending} + /> + ); }