feat: news page
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-03-06 11:00:05 +00:00
parent 0b93b0d4f9
commit 1aab7c9372
6 changed files with 121 additions and 55 deletions

View File

@ -1,39 +0,0 @@
.markdown a {
@apply underline;
}
.markdown blockquote {
margin: 0;
padding-left: 12px;
@apply border-l-neutral-800 border-2 text-neutral-400;
}
.markdown hr {
border: 0;
height: 1px;
margin: 20px;
@apply bg-neutral-800;
}
.markdown img:not(.custom-emoji),
.markdown video,
.markdown iframe,
.markdown audio {
max-width: 100%;
display: block;
}
.markdown iframe,
.markdown video {
width: -webkit-fill-available;
aspect-ratio: 16 / 9;
}
.markdown h1,
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
margin: 0.5em 0;
}

View File

@ -1,5 +1,3 @@
import "./markdown.css";
import { ReactNode, forwardRef, useMemo } from "react";
import { Token, Tokens, marked } from "marked";
import { Link } from "react-router-dom";
@ -16,9 +14,9 @@ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
switch (t.type) {
case "paragraph": {
return (
<div key={ctr++}>
<p key={ctr++} className="py-2">
{t.tokens ? t.tokens.map(renderToken) : t.raw}
</div>
</p>
);
}
case "image": {
@ -28,37 +26,37 @@ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
switch (t.depth) {
case 1:
return (
<h1 key={ctr++}>
<h1 key={ctr++} className="my-6 text-2xl">
{t.tokens ? t.tokens.map(renderToken) : t.raw}
</h1>
);
case 2:
return (
<h2 key={ctr++}>
<h2 key={ctr++} className="my-5 text-xl">
{t.tokens ? t.tokens.map(renderToken) : t.raw}
</h2>
);
case 3:
return (
<h3 key={ctr++}>
<h3 key={ctr++} className="my-4 text-lg">
{t.tokens ? t.tokens.map(renderToken) : t.raw}
</h3>
);
case 4:
return (
<h4 key={ctr++}>
<h4 key={ctr++} className="my-3 text-md">
{t.tokens ? t.tokens.map(renderToken) : t.raw}
</h4>
);
case 5:
return (
<h5 key={ctr++}>
<h5 key={ctr++} className="my-2">
{t.tokens ? t.tokens.map(renderToken) : t.raw}
</h5>
);
case 6:
return (
<h6 key={ctr++}>
<h6 key={ctr++} className="my-2">
{t.tokens ? t.tokens.map(renderToken) : t.raw}
</h6>
);
@ -66,7 +64,11 @@ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
throw new Error("Invalid heading");
}
case "codespan": {
return <code key={ctr++}>{t.raw}</code>;
return (
<code key={ctr++} className="bg-neutral-900 px-2">
{t.raw.substring(1, t.raw.length - 1)}
</code>
);
}
case "code": {
return <pre key={ctr++}>{t.raw}</pre>;
@ -84,23 +86,34 @@ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
}
case "blockquote": {
return (
<blockquote key={ctr++}>
<blockquote
key={ctr++}
className="outline-l-neutral-900 outline text-neutral-300 p-3"
>
{t.tokens ? t.tokens.map(renderToken) : t.raw}
</blockquote>
);
}
case "link": {
return (
<Link to={t.href} key={ctr++}>
<Link to={t.href} key={ctr++} className="underline">
{t.tokens ? t.tokens.map(renderToken) : t.raw}
</Link>
);
}
case "list": {
if (t.ordered) {
return <ol key={ctr++}>{t.items.map(renderToken)}</ol>;
return (
<ol key={ctr++} className="list-decimal list-outside">
{t.items.map(renderToken)}
</ol>
);
} else {
return <ul key={ctr++}>{t.items.map(renderToken)}</ul>;
return (
<ul key={ctr++} className="list-disc list-outside">
{t.items.map(renderToken)}
</ul>
);
}
}
case "list_item": {
@ -170,7 +183,7 @@ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
return marked.lexer(props.content);
}, [props.content]);
return (
<div className="markdown" ref={ref}>
<div className="leading-8 text-pretty break-words" ref={ref}>
{parsed
.filter((a) => a.type !== "footnote" && a.type !== "footnotes")
.map((a) => renderToken(a))}

View File

@ -15,6 +15,8 @@ import { StatusPage } from "./pages/status.tsx";
import { AccountSettings } from "./pages/account-settings.tsx";
import { VmBillingPage } from "./pages/vm-billing.tsx";
import { VmGraphsPage } from "./pages/vm-graphs.tsx";
import { NewsPage } from "./pages/news.tsx";
import { NewsPost } from "./pages/news-post.tsx";
const system = new NostrSystem({
automaticOutboxModel: false,
@ -71,6 +73,14 @@ const router = createBrowserRouter([
path: "/status",
element: <StatusPage />,
},
{
path: "/news",
element: <NewsPage />,
},
{
path: "/news/:id",
element: <NewsPost />,
},
],
},
]);

View File

@ -40,6 +40,8 @@ export default function HomePage() {
{" | "}
<Link to="/tos">Terms</Link>
{" | "}
<Link to="/news">News</Link>
{" | "}
<a
href={`https://snort.social/${NostrProfile.encode()}`}
target="_blank"

25
src/pages/news-post.tsx Normal file
View File

@ -0,0 +1,25 @@
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { useLocation } from "react-router-dom";
import Markdown from "../components/markdown";
import Profile from "../components/profile";
export function NewsPost() {
const { state } = useLocation() as { state?: TaggedNostrEvent };
if (!state) return;
const title = state.tags.find((a) => a[0] == "title")?.[1];
const posted = Number(
state.tags.find((a) => a[0] == "published_at")?.[1] ?? state.created_at,
);
return (
<div>
<div className="text-2xl">{title}</div>
<div className="flex items-center justify-between py-8">
<Profile link={NostrLink.profile(state.pubkey, state.relays)} />
<div>{new Date(posted * 1000).toLocaleString()}</div>
</div>
<Markdown content={state.content} />
</div>
);
}

55
src/pages/news.tsx Normal file
View File

@ -0,0 +1,55 @@
import { EventKind, NostrLink, RequestBuilder } from "@snort/system";
import { NostrProfile } from "../const";
import { useRequestBuilder } from "@snort/system-react";
import { Link } from "react-router-dom";
export function NewsPage() {
const req = new RequestBuilder("news");
req
.withFilter()
.kinds([EventKind.LongFormTextNote])
.authors([NostrProfile.id])
.limit(10);
const posts = useRequestBuilder(req);
return (
<div className="flex flex-col gap-4">
<div className="text-2xl">News</div>
{posts
.sort((a, b) => {
const a_posted = Number(
a.tags.find((a) => a[0] == "published_at")?.[1] ?? a.created_at,
);
const b_posted = Number(
b.tags.find((z) => z[0] == "published_at")?.[1] ?? b.created_at,
);
return b_posted - a_posted;
})
.map((a) => {
const link = NostrLink.fromEvent(a);
const title = a.tags.find((a) => a[0] == "title")?.[1];
const posted = Number(
a.tags.find((a) => a[0] == "published_at")?.[1] ?? a.created_at,
);
const slug = title
?.toLocaleLowerCase()
.replace(/[:/]/g, "")
.trimStart()
.trimEnd()
.replace(/ /g, "-");
return (
<Link to={`/news/${slug}`} state={a} key={link.tagKey}>
<div className="flex flex-col rounded-xl bg-neutral-900 px-3 py-4">
<div className="text-xl flex items-center justify-between">
<div>{title}</div>
<div>{new Date(posted * 1000).toDateString()}</div>
</div>
</div>
</Link>
);
})}
{posts.length === 0 && <div>No posts yet..</div>}
</div>
);
}