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:
parent
05463eabb4
commit
4d2b27107a
1 changed files with 88 additions and 59 deletions
|
|
@ -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'],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue