73 lines
2.1 KiB
TypeScript
73 lines
2.1 KiB
TypeScript
|
|
/**
|
|||
|
|
* "What's New" modal — shows the next pending product-news post when the
|
|||
|
|
* user enters the editor. At most one per day, each post only ever once;
|
|||
|
|
* the queue/seen logic lives in lib/newsSource.ts (OSS: localStorage over
|
|||
|
|
* the local /api/news/feed proxy; pro overlay: server-backed per-user).
|
|||
|
|
*
|
|||
|
|
* The fetch is delayed a few seconds so it never competes with editor
|
|||
|
|
* startup, and every failure path resolves to "no modal".
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { useEffect, useState } from 'react';
|
|||
|
|
import { createPortal } from 'react-dom';
|
|||
|
|
import ReactMarkdown from 'react-markdown';
|
|||
|
|
import remarkGfm from 'remark-gfm';
|
|||
|
|
import { getNextNews, type NewsPost } from '../../lib/newsSource';
|
|||
|
|
import './NewsModal.css';
|
|||
|
|
|
|||
|
|
const FETCH_DELAY_MS = 4000;
|
|||
|
|
|
|||
|
|
export function NewsModal() {
|
|||
|
|
const [post, setPost] = useState<NewsPost | null>(null);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
let cancelled = false;
|
|||
|
|
const timer = setTimeout(() => {
|
|||
|
|
void getNextNews().then((p) => {
|
|||
|
|
if (!cancelled && p) setPost(p);
|
|||
|
|
});
|
|||
|
|
}, FETCH_DELAY_MS);
|
|||
|
|
return () => {
|
|||
|
|
cancelled = true;
|
|||
|
|
clearTimeout(timer);
|
|||
|
|
};
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
if (!post) return null;
|
|||
|
|
|
|||
|
|
return createPortal(
|
|||
|
|
<div className="velxio-news-overlay" onClick={() => setPost(null)}>
|
|||
|
|
<div
|
|||
|
|
className="velxio-news-modal"
|
|||
|
|
role="dialog"
|
|||
|
|
aria-modal="true"
|
|||
|
|
aria-label={post.title}
|
|||
|
|
onClick={(e) => e.stopPropagation()}
|
|||
|
|
>
|
|||
|
|
<div className="velxio-news-header">
|
|||
|
|
<div>
|
|||
|
|
<span className="velxio-news-kicker">What's new</span>
|
|||
|
|
<h2 className="velxio-news-title">{post.title}</h2>
|
|||
|
|
</div>
|
|||
|
|
<button
|
|||
|
|
className="velxio-news-close"
|
|||
|
|
aria-label="Close"
|
|||
|
|
onClick={() => setPost(null)}
|
|||
|
|
>
|
|||
|
|
×
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
<div className="velxio-news-body">
|
|||
|
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{post.body_md}</ReactMarkdown>
|
|||
|
|
</div>
|
|||
|
|
<div className="velxio-news-footer">
|
|||
|
|
<button className="velxio-news-ok" onClick={() => setPost(null)}>
|
|||
|
|
Got it
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>,
|
|||
|
|
document.body,
|
|||
|
|
);
|
|||
|
|
}
|