newsletter: fix Edit first button — opens inline textarea, Preview toggle, publish with edited text

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-07-23 20:35:59 +07:00
parent 05463eabb4
commit 4d2b27107a

View file

@ -3,6 +3,7 @@ import { useSeoMeta } from '@unhead/react';
import { Layout } from '@/components/Layout'; import { Layout } from '@/components/Layout';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { import {
@ -249,7 +250,7 @@ function NostrPreviewModal({
issue: NewsletterIssue; issue: NewsletterIssue;
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onConfirm: () => void; onConfirm: (text: string) => void;
isPending: boolean; isPending: boolean;
}) { }) {
const { user } = useCurrentUser(); const { user } = useCurrentUser();
@ -260,16 +261,16 @@ function NostrPreviewModal({
const now = new Date(); const now = new Date();
const timeStr = now.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); 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 [editing, setEditing] = useState(false);
const [editedText, setEditedText] = useState(issue.nostrText);
const currentText = editing ? editedText : issue.nostrText;
const renderPreviewText = (text: string) => { const renderPreviewText = (text: string) => {
return text.split('\n').map((line, i) => { return text.split('\n').map((line, i) => {
const parts = line.split(/(\s+)/).map((word, j) => { const parts = line.split(/(\s+)/).map((word, j) => {
if (word.startsWith('#')) { if (word.startsWith('#')) return <span key={j} className="text-[#1d9bf0]">{word}</span>;
return <span key={j} className="text-[#1d9bf0]">{word}</span>; if (word.startsWith('https://')) return <span key={j} className="text-[#1d9bf0] underline underline-offset-2">{word}</span>;
}
if (word.startsWith('https://')) {
return <span key={j} className="text-[#1d9bf0] underline underline-offset-2">{word}</span>;
}
return <span key={j}>{word}</span>; return <span key={j}>{word}</span>;
}); });
return <p key={i} className={line === '' ? 'h-3' : ''}>{parts}</p>; return <p key={i} className={line === '' ? 'h-3' : ''}>{parts}</p>;
@ -282,64 +283,92 @@ function NostrPreviewModal({
<DialogHeader> <DialogHeader>
<DialogTitle className="text-white flex items-center gap-2"> <DialogTitle className="text-white flex items-center gap-2">
<Eye className="h-4 w-4 text-[#ccff00]" /> <Eye className="h-4 w-4 text-[#ccff00]" />
Preview — Drop #{String(issue.number).padStart(3, '0')} {editing ? 'Edit' : 'Preview'} — Drop #{String(issue.number).padStart(3, '0')}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-gray-500 text-xs"> <DialogDescription className="text-gray-500 text-xs">
This is how the note will appear on most Nostr clients. Formatting varies by client. {editing
? 'Edit the note text below. Changes only affect this publish — the source file stays unchanged.'
: 'This is how the note will appear on most Nostr clients. Formatting varies by client.'}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{/* Mock Nostr post card */} {editing ? (
<div className="mt-2 rounded-2xl border border-white/10 bg-[#0f0f0f] overflow-hidden"> /* ── Edit mode ── */
{/* Post header */} <div className="space-y-2 mt-2">
<div className="flex items-center gap-3 px-4 pt-4 pb-2"> <Textarea
<Avatar className="h-10 w-10 border-2 border-[#ccff00]/30 flex-shrink-0"> value={editedText}
<AvatarImage src={avatar} alt={displayName} /> onChange={e => setEditedText(e.target.value)}
<AvatarFallback className="bg-[#ccff00]/10 text-[#ccff00] text-xs font-bold"> className="bg-[#0f0f0f] border-white/10 text-gray-200 text-sm font-mono leading-relaxed min-h-[200px] resize-none focus:border-[#ccff00]/40"
{displayName.slice(0, 2).toUpperCase()} autoFocus
</AvatarFallback> />
</Avatar> <p className="text-[10px] text-gray-600 text-right">{editedText.length} characters</p>
<div className="flex-1 min-w-0"> </div>
<p className="text-white font-bold text-sm truncate">{displayName}</p> ) : (
<p className="text-gray-500 text-xs">{timeStr} · via skate-spots.org</p> /* ── Preview mode ── */
<>
<div className="mt-2 rounded-2xl border border-white/10 bg-[#0f0f0f] overflow-hidden">
<div className="flex items-center gap-3 px-4 pt-4 pb-2">
<Avatar className="h-10 w-10 border-2 border-[#ccff00]/30 flex-shrink-0">
<AvatarImage src={avatar} alt={displayName} />
<AvatarFallback className="bg-[#ccff00]/10 text-[#ccff00] text-xs font-bold">
{displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-white font-bold text-sm truncate">{displayName}</p>
<p className="text-gray-500 text-xs">{timeStr} · via skate-spots.org</p>
</div>
</div>
<div className="px-4 pb-4 text-sm text-gray-200 leading-relaxed space-y-0">
{renderPreviewText(currentText)}
</div>
<div className="flex items-center gap-5 px-4 py-3 border-t border-white/5 text-gray-600 text-xs">
<span>💬 0</span><span>🔁 0</span><span>⚡ 0</span>
</div>
</div> </div>
</div> <p className="text-[10px] text-gray-600 text-right -mt-1">{currentText.length} characters</p>
</>
{/* Post body */} )}
<div className="px-4 pb-4 text-sm text-gray-200 leading-relaxed space-y-0">
{renderPreviewText(issue.nostrText)}
</div>
{/* Fake engagement bar */}
<div className="flex items-center gap-5 px-4 py-3 border-t border-white/5 text-gray-600 text-xs">
<span>💬 0</span>
<span>🔁 0</span>
<span>⚡ 0</span>
</div>
</div>
{/* Character count */}
<p className="text-[10px] text-gray-600 text-right -mt-1">
{issue.nostrText.length} characters
</p>
{/* Actions */} {/* Actions */}
<div className="flex gap-3 mt-1"> <div className="flex gap-3 mt-1">
<Button {editing ? (
variant="outline" <>
onClick={onClose} <Button
className="flex-1 border-gray-700 text-gray-300 hover:text-white h-11" variant="outline"
> onClick={() => setEditing(false)}
Edit first className="flex-1 border-gray-700 text-gray-300 hover:text-white h-11"
</Button> >
<Button Preview
onClick={onConfirm} </Button>
disabled={isPending} <Button
className="flex-1 gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-black uppercase text-xs h-11" onClick={() => onConfirm(editedText)}
> disabled={isPending || editedText.trim().length === 0}
<Send className="h-3.5 w-3.5" /> className="flex-1 gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-black uppercase text-xs h-11"
{isPending ? 'Publishing...' : 'Publish'} >
</Button> <Send className="h-3.5 w-3.5" />
{isPending ? 'Publishing...' : 'Publish'}
</Button>
</>
) : (
<>
<Button
variant="outline"
onClick={() => { setEditedText(issue.nostrText); setEditing(true); }}
className="flex-1 border-gray-700 text-gray-300 hover:text-white h-11"
>
Edit first
</Button>
<Button
onClick={() => onConfirm(currentText)}
disabled={isPending}
className="flex-1 gap-2 bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-black uppercase text-xs h-11"
>
<Send className="h-3.5 w-3.5" />
{isPending ? 'Publishing...' : 'Publish'}
</Button>
</>
)}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@ -365,11 +394,11 @@ function PublishToNostrButton({ issue }: { issue: NewsletterIssue }) {
); );
} }
const handleConfirmPublish = async () => { const handleConfirmPublish = async (text: string) => {
try { try {
await publishEvent({ await publishEvent({
kind: 1, kind: 1,
content: issue.nostrText, content: text,
tags: [ tags: [
['t', 'skateboarding'], ['t', 'skateboarding'],
['t', 'nostr'], ['t', 'nostr'],