- 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.
63 lines
2.2 KiB
TypeScript
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;
|
|
}
|
|
},
|
|
});
|
|
}
|