This commit is contained in:
@ -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;
|
|
||||||
}
|
|
@ -1,5 +1,3 @@
|
|||||||
import "./markdown.css";
|
|
||||||
|
|
||||||
import { ReactNode, forwardRef, useMemo } from "react";
|
import { ReactNode, forwardRef, useMemo } from "react";
|
||||||
import { Token, Tokens, marked } from "marked";
|
import { Token, Tokens, marked } from "marked";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
@ -16,9 +14,9 @@ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
|
|||||||
switch (t.type) {
|
switch (t.type) {
|
||||||
case "paragraph": {
|
case "paragraph": {
|
||||||
return (
|
return (
|
||||||
<div key={ctr++}>
|
<p key={ctr++} className="py-2">
|
||||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||||
</div>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "image": {
|
case "image": {
|
||||||
@ -28,37 +26,37 @@ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
|
|||||||
switch (t.depth) {
|
switch (t.depth) {
|
||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
<h1 key={ctr++}>
|
<h1 key={ctr++} className="my-6 text-2xl">
|
||||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||||
</h1>
|
</h1>
|
||||||
);
|
);
|
||||||
case 2:
|
case 2:
|
||||||
return (
|
return (
|
||||||
<h2 key={ctr++}>
|
<h2 key={ctr++} className="my-5 text-xl">
|
||||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||||
</h2>
|
</h2>
|
||||||
);
|
);
|
||||||
case 3:
|
case 3:
|
||||||
return (
|
return (
|
||||||
<h3 key={ctr++}>
|
<h3 key={ctr++} className="my-4 text-lg">
|
||||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||||
</h3>
|
</h3>
|
||||||
);
|
);
|
||||||
case 4:
|
case 4:
|
||||||
return (
|
return (
|
||||||
<h4 key={ctr++}>
|
<h4 key={ctr++} className="my-3 text-md">
|
||||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||||
</h4>
|
</h4>
|
||||||
);
|
);
|
||||||
case 5:
|
case 5:
|
||||||
return (
|
return (
|
||||||
<h5 key={ctr++}>
|
<h5 key={ctr++} className="my-2">
|
||||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||||
</h5>
|
</h5>
|
||||||
);
|
);
|
||||||
case 6:
|
case 6:
|
||||||
return (
|
return (
|
||||||
<h6 key={ctr++}>
|
<h6 key={ctr++} className="my-2">
|
||||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||||
</h6>
|
</h6>
|
||||||
);
|
);
|
||||||
@ -66,7 +64,11 @@ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
|
|||||||
throw new Error("Invalid heading");
|
throw new Error("Invalid heading");
|
||||||
}
|
}
|
||||||
case "codespan": {
|
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": {
|
case "code": {
|
||||||
return <pre key={ctr++}>{t.raw}</pre>;
|
return <pre key={ctr++}>{t.raw}</pre>;
|
||||||
@ -84,23 +86,34 @@ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
|
|||||||
}
|
}
|
||||||
case "blockquote": {
|
case "blockquote": {
|
||||||
return (
|
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}
|
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "link": {
|
case "link": {
|
||||||
return (
|
return (
|
||||||
<Link to={t.href} key={ctr++}>
|
<Link to={t.href} key={ctr++} className="underline">
|
||||||
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
{t.tokens ? t.tokens.map(renderToken) : t.raw}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "list": {
|
case "list": {
|
||||||
if (t.ordered) {
|
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 {
|
} 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": {
|
case "list_item": {
|
||||||
@ -170,7 +183,7 @@ const Markdown = forwardRef<HTMLDivElement, MarkdownProps>(
|
|||||||
return marked.lexer(props.content);
|
return marked.lexer(props.content);
|
||||||
}, [props.content]);
|
}, [props.content]);
|
||||||
return (
|
return (
|
||||||
<div className="markdown" ref={ref}>
|
<div className="leading-8 text-pretty break-words" ref={ref}>
|
||||||
{parsed
|
{parsed
|
||||||
.filter((a) => a.type !== "footnote" && a.type !== "footnotes")
|
.filter((a) => a.type !== "footnote" && a.type !== "footnotes")
|
||||||
.map((a) => renderToken(a))}
|
.map((a) => renderToken(a))}
|
||||||
|
10
src/main.tsx
10
src/main.tsx
@ -15,6 +15,8 @@ import { StatusPage } from "./pages/status.tsx";
|
|||||||
import { AccountSettings } from "./pages/account-settings.tsx";
|
import { AccountSettings } from "./pages/account-settings.tsx";
|
||||||
import { VmBillingPage } from "./pages/vm-billing.tsx";
|
import { VmBillingPage } from "./pages/vm-billing.tsx";
|
||||||
import { VmGraphsPage } from "./pages/vm-graphs.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({
|
const system = new NostrSystem({
|
||||||
automaticOutboxModel: false,
|
automaticOutboxModel: false,
|
||||||
@ -71,6 +73,14 @@ const router = createBrowserRouter([
|
|||||||
path: "/status",
|
path: "/status",
|
||||||
element: <StatusPage />,
|
element: <StatusPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/news",
|
||||||
|
element: <NewsPage />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/news/:id",
|
||||||
|
element: <NewsPost />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
@ -40,6 +40,8 @@ export default function HomePage() {
|
|||||||
{" | "}
|
{" | "}
|
||||||
<Link to="/tos">Terms</Link>
|
<Link to="/tos">Terms</Link>
|
||||||
{" | "}
|
{" | "}
|
||||||
|
<Link to="/news">News</Link>
|
||||||
|
{" | "}
|
||||||
<a
|
<a
|
||||||
href={`https://snort.social/${NostrProfile.encode()}`}
|
href={`https://snort.social/${NostrProfile.encode()}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
25
src/pages/news-post.tsx
Normal file
25
src/pages/news-post.tsx
Normal 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
55
src/pages/news.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
Reference in New Issue
Block a user