SkateSpots/src/hooks/useNewsletterDraft.ts
SkateSpots f029b5a516 feat(newsletter): weekly generated drafts, covers, and admin preview
- Add a browser-safe editorial generator with rotating satire and shipped highlights.
- Generate a fresh 1200x630 cover from the Drop #006 foundation and upload it to Blossom.
- Publish weekly drafts as NIP-78; add the admin Load draft/Preview composer flow and banner.
- Add the Monday user timer (draft by default) and a manual newsletter:weekly script.
- Remove static/live issue duplicates; translate the new admin strings to all 24 locales.
2026-10-28 09:42:44 +07:00

63 lines
2.2 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { useSkateRelays } from '@/hooks/useSkateRelays';
import { ADMIN_PUBKEYS } from '@/lib/admins';
import { APP_DATA_KIND } from '@/lib/types';
import type { NewsletterDraft } from '@/lib/newsletterContent';
const DRAFT_D = 'skatespots:newsletter:draft';
/**
* The latest weekly draft published by the generator (NIP-78, admin-authored).
* Public data — it contains no private keys or subscriber information.
*/
export function useNewsletterDraft(enabled = true) {
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
return useQuery({
queryKey: ['newsletter-draft', getQueryKeySuffix()],
enabled,
staleTime: 60_000,
queryFn: async (c): Promise<NewsletterDraft | null> => {
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(6000)]);
try {
const events = await querySkateContent(
[
{
kinds: [APP_DATA_KIND],
authors: ADMIN_PUBKEYS,
'#d': [DRAFT_D],
limit: 1,
},
],
{ signal },
);
const event = events.sort((a, b) => b.created_at - a.created_at)[0];
if (!event) return null;
const parsed = JSON.parse(event.content) as Partial<NewsletterDraft>;
if (
typeof parsed.number !== 'number' ||
typeof parsed.title !== 'string' ||
!parsed.title ||
typeof parsed.slug !== 'string' ||
typeof parsed.body !== 'string' ||
parsed.body.length < 20
) {
return null;
}
return {
number: parsed.number,
title: parsed.title,
slug: parsed.slug,
summary: typeof parsed.summary === 'string' ? parsed.summary : '',
image: typeof parsed.image === 'string' ? parsed.image : '',
body: parsed.body,
nostrText: typeof parsed.nostrText === 'string' ? parsed.nostrText : '',
coverTitle: typeof parsed.coverTitle === 'string' ? parsed.coverTitle : '',
createdAt: typeof parsed.createdAt === 'number' ? parsed.createdAt : event.created_at,
};
} catch {
return null;
}
},
});
}