feat: start FAQ page
This commit is contained in:
parent
a3ab088a82
commit
96467bd979
23
src/d.ts
23
src/d.ts
@ -1,35 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/// <reference types="@webbtc/webln-types" />
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare const __XXX: boolean;
|
||||
declare const __XXX_HOST: string;
|
||||
declare const __ZAP_STREAM_VERSION__: string;
|
||||
|
||||
declare module "*.jpg" {
|
||||
const value: unknown;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.svg" {
|
||||
const value: unknown;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.webp" {
|
||||
declare module "*.md" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.png" {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module "*.css" {
|
||||
const stylesheet: CSSStyleSheet;
|
||||
export default stylesheet;
|
||||
}
|
||||
|
||||
declare module "translations/*.json" {
|
||||
const value: Record<string, string>;
|
||||
export default value;
|
||||
|
10
src/element/async-loader.tsx
Normal file
10
src/element/async-loader.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
export function Async<T>({ loader, then }: { loader: () => Promise<T>, then: (v: T) => ReactNode }) {
|
||||
const [res, setResult] = useState<T>();
|
||||
useEffect(() => {
|
||||
loader().then(setResult);
|
||||
}, []);
|
||||
if (!res) return;
|
||||
return then(res);
|
||||
}
|
@ -1,9 +1,3 @@
|
||||
.markdown {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
.markdown a {
|
||||
color: var(--primary);
|
||||
}
|
||||
@ -26,7 +20,7 @@
|
||||
.markdown video,
|
||||
.markdown iframe,
|
||||
.markdown audio {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@ -35,3 +29,12 @@
|
||||
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,88 +1,110 @@
|
||||
import "./markdown.css";
|
||||
|
||||
import { ReactNode, forwardRef, useMemo } from "react";
|
||||
import { Token, marked } from "marked";
|
||||
import { Token, Tokens, marked } from "marked";
|
||||
import { HyperText } from "./hypertext";
|
||||
import { Text } from "./text";
|
||||
|
||||
interface MarkdownProps {
|
||||
content: string;
|
||||
tags?: Array<Array<string>>;
|
||||
}
|
||||
|
||||
function renderToken(t: Token): ReactNode {
|
||||
try {
|
||||
switch (t.type) {
|
||||
case "paragraph": {
|
||||
return <p>{t.tokens ? t.tokens.map(renderToken) : t.raw}</p>;
|
||||
}
|
||||
case "image": {
|
||||
return <img src={t.href} />;
|
||||
}
|
||||
case "heading": {
|
||||
switch (t.depth) {
|
||||
case 1:
|
||||
return <h1>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h1>;
|
||||
case 2:
|
||||
return <h2>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h2>;
|
||||
case 3:
|
||||
return <h3>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h3>;
|
||||
case 4:
|
||||
return <h4>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h4>;
|
||||
case 5:
|
||||
return <h5>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h5>;
|
||||
case 6:
|
||||
return <h6>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h6>;
|
||||
}
|
||||
throw new Error("Invalid heading");
|
||||
}
|
||||
case "codespan": {
|
||||
return <code>{t.raw}</code>;
|
||||
}
|
||||
case "code": {
|
||||
return <pre>{t.raw}</pre>;
|
||||
}
|
||||
case "br": {
|
||||
return <br />;
|
||||
}
|
||||
case "hr": {
|
||||
return <hr />;
|
||||
}
|
||||
case "blockquote": {
|
||||
return <blockquote>{t.tokens ? t.tokens.map(renderToken) : t.raw}</blockquote>;
|
||||
}
|
||||
case "link": {
|
||||
return <HyperText link={t.href}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</HyperText>;
|
||||
}
|
||||
case "list": {
|
||||
if (t.ordered) {
|
||||
return <ol>{t.items.map(renderToken)}</ol>;
|
||||
} else {
|
||||
return <ul>{t.items.map(renderToken)}</ul>;
|
||||
}
|
||||
}
|
||||
case "list_item": {
|
||||
return <li>{t.tokens ? t.tokens.map(renderToken) : t.raw}</li>;
|
||||
}
|
||||
case "em": {
|
||||
return <em>{t.tokens ? t.tokens.map(renderToken) : t.raw}</em>;
|
||||
}
|
||||
case "del": {
|
||||
return <s>{t.tokens ? t.tokens.map(renderToken) : t.raw}</s>;
|
||||
}
|
||||
default: {
|
||||
if ("tokens" in t) {
|
||||
return (t.tokens as Array<Token>).map(renderToken);
|
||||
}
|
||||
return <Text content={t.raw} tags={[]} />;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
// Render plain text directly without parsing nostr/http links
|
||||
plainText?: boolean;
|
||||
}
|
||||
|
||||
const Markdown = forwardRef<HTMLDivElement, MarkdownProps>((props: MarkdownProps, ref) => {
|
||||
|
||||
function renderToken(t: Token): ReactNode {
|
||||
try {
|
||||
switch (t.type) {
|
||||
case "paragraph": {
|
||||
return <p>{t.tokens ? t.tokens.map(renderToken) : t.raw}</p>;
|
||||
}
|
||||
case "image": {
|
||||
return <img src={t.href} />;
|
||||
}
|
||||
case "heading": {
|
||||
switch (t.depth) {
|
||||
case 1:
|
||||
return <h1>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h1>;
|
||||
case 2:
|
||||
return <h2>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h2>;
|
||||
case 3:
|
||||
return <h3>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h3>;
|
||||
case 4:
|
||||
return <h4>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h4>;
|
||||
case 5:
|
||||
return <h5>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h5>;
|
||||
case 6:
|
||||
return <h6>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h6>;
|
||||
}
|
||||
throw new Error("Invalid heading");
|
||||
}
|
||||
case "codespan": {
|
||||
return <code>{t.raw}</code>;
|
||||
}
|
||||
case "code": {
|
||||
return <pre>{t.raw}</pre>;
|
||||
}
|
||||
case "br": {
|
||||
return <br />;
|
||||
}
|
||||
case "hr": {
|
||||
return <hr />;
|
||||
}
|
||||
case "blockquote": {
|
||||
return <blockquote>{t.tokens ? t.tokens.map(renderToken) : t.raw}</blockquote>;
|
||||
}
|
||||
case "link": {
|
||||
return <HyperText link={t.href}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</HyperText>;
|
||||
}
|
||||
case "list": {
|
||||
if (t.ordered) {
|
||||
return <ol>{t.items.map(renderToken)}</ol>;
|
||||
} else {
|
||||
return <ul>{t.items.map(renderToken)}</ul>;
|
||||
}
|
||||
}
|
||||
case "list_item": {
|
||||
return <li>{t.tokens ? t.tokens.map(renderToken) : t.raw}</li>;
|
||||
}
|
||||
case "em": {
|
||||
return <em>{t.tokens ? t.tokens.map(renderToken) : t.raw}</em>;
|
||||
}
|
||||
case "del": {
|
||||
return <s>{t.tokens ? t.tokens.map(renderToken) : t.raw}</s>;
|
||||
}
|
||||
case "table": {
|
||||
return <table className="table-auto border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
{(t.header as Tokens.TableCell[]).map(v => <th className="border">{v.tokens ? v.tokens.map(renderToken) : v.text}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(t.rows as Tokens.TableCell[][]).map(v => <tr>
|
||||
{v.map(d => <td className="border px-2 py-1">{d.tokens ? d.tokens.map(renderToken) : d.text}</td>)}
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>;
|
||||
}
|
||||
default: {
|
||||
if ("tokens" in t) {
|
||||
return (t.tokens as Array<Token>).map(renderToken);
|
||||
}
|
||||
if (props.plainText ?? false) {
|
||||
return t.raw;
|
||||
}
|
||||
return <Text content={t.raw} tags={[]} />;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const parsed = useMemo(() => {
|
||||
return marked.lexer(props.content);
|
||||
}, [props.content, props.tags]);
|
||||
|
13
src/faq.md
Normal file
13
src/faq.md
Normal file
@ -0,0 +1,13 @@
|
||||
# FAQ
|
||||
|
||||
# Reccomended Stream Settings
|
||||
| Name | Value |
|
||||
| - | - |
|
||||
| Video Codec | h264 |
|
||||
| Audio Codec | AAC |
|
||||
| Max Video Bitrate | 7000k |
|
||||
| Max Audio Bitrate | 320k |
|
||||
| Keyframe Interval | 2s |
|
||||
|
||||
### Example settings in OBS (Apple M1 Mac):
|
||||
![OBS Apple](https://void.cat/d/VQQ75R6tmbVQJ9eqiwJhoj.webp)
|
@ -27,8 +27,12 @@ import { WidgetsPage } from "@/pages/widgets";
|
||||
import { AlertsPage } from "@/pages/alerts";
|
||||
import { StreamSummaryPage } from "@/pages/summary";
|
||||
import { EmbededPage } from "./pages/embed";
|
||||
import Markdown from "./element/markdown";
|
||||
const DashboardPage = lazy(() => import("./pages/dashboard"));
|
||||
|
||||
import Faq from "@/faq.md";
|
||||
import { Async } from "./element/async-loader";
|
||||
|
||||
const db = new SnortSystemDb();
|
||||
const System = new NostrSystem({
|
||||
db,
|
||||
@ -103,6 +107,13 @@ const router = createBrowserRouter([
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/faq",
|
||||
element: <Async loader={async () => {
|
||||
const req = await fetch(Faq);
|
||||
return await req.text();
|
||||
}} then={(v) => <Markdown content={v} tags={[]} plainText={true} />} />
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
element: <CatchAllRoutePage />,
|
||||
|
@ -28,6 +28,7 @@ export default defineConfig({
|
||||
filename: "build/stats.html",
|
||||
}),
|
||||
],
|
||||
assetsInclude: ["**/*.md"],
|
||||
build: {
|
||||
outDir: "build",
|
||||
sourcemap: true,
|
||||
|
Loading…
x
Reference in New Issue
Block a user