Add docs navigation and tighten API request security
This commit is contained in:
66
app/docs/page.module.css
Normal file
66
app/docs/page.module.css
Normal file
@@ -0,0 +1,66 @@
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(0, 1fr);
|
||||
gap: 48px;
|
||||
}
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.menu a {
|
||||
line-height: 1.5;
|
||||
}
|
||||
.menu a[aria-current="page"] {
|
||||
color: var(--green);
|
||||
font-weight: 700;
|
||||
}
|
||||
.article {
|
||||
min-width: 0;
|
||||
max-width: 780px;
|
||||
}
|
||||
.article h1 {
|
||||
font-size: clamp(30px, 4vw, 48px);
|
||||
}
|
||||
.article a {
|
||||
color: var(--green);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.article li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.article ol { list-style: decimal; padding-left: 24px; }
|
||||
.article ul { list-style: disc; padding-left: 24px; }
|
||||
.article blockquote {
|
||||
margin: 24px 0;
|
||||
padding: 12px 20px;
|
||||
border-left: 3px solid var(--green);
|
||||
}
|
||||
.article figure {
|
||||
margin: 24px 0;
|
||||
}
|
||||
.article img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.paging {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-top: 48px;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.layout {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 32px;
|
||||
}
|
||||
.menu {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 20px;
|
||||
}
|
||||
}
|
||||
119
app/docs/page.tsx
Normal file
119
app/docs/page.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Header, Footer } from "@/components/Shell";
|
||||
import guides from "@/shared/player-docs.json";
|
||||
import styles from "./page.module.css";
|
||||
|
||||
export const metadata = { title: "Player docs" };
|
||||
|
||||
function inline(text: string) {
|
||||
return text
|
||||
.split(/(\*\*[^*]+\*\*|\[[^\]]+\]\(https:\/\/[^)]+\))/g)
|
||||
.map((part, index) => {
|
||||
if (part.startsWith("**"))
|
||||
return <strong key={index}>{part.slice(2, -2)}</strong>;
|
||||
const link = part.match(/^\[([^\]]+)\]\((https:\/\/[^)]+)\)$/);
|
||||
return link ? (
|
||||
<a key={index} href={link[2]}>
|
||||
{link[1]}
|
||||
</a>
|
||||
) : (
|
||||
part
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function content(markdown: string) {
|
||||
return markdown.split(/\n\s*\n/).map((block, index) => {
|
||||
const heading = block.match(/^(#{1,3}) (.+)$/);
|
||||
if (heading) {
|
||||
const text = inline(heading[2]);
|
||||
return heading[1].length === 1 ? (
|
||||
<h1 key={index}>{text}</h1>
|
||||
) : heading[1].length === 2 ? (
|
||||
<h2 key={index}>{text}</h2>
|
||||
) : (
|
||||
<h3 key={index}>{text}</h3>
|
||||
);
|
||||
}
|
||||
const image = block.match(/^!\[([^\]]*)\]\((https:\/\/[^)]+)\)$/);
|
||||
if (image)
|
||||
return (
|
||||
<figure key={index}>
|
||||
<img src={image[2]} alt={image[1]} loading="lazy" width="420" />
|
||||
</figure>
|
||||
);
|
||||
if (block.startsWith("> "))
|
||||
return (
|
||||
<blockquote key={index}>
|
||||
{inline(block.replace(/^> /gm, ""))}
|
||||
</blockquote>
|
||||
);
|
||||
const lines = block.split("\n");
|
||||
if (lines.every((line) => /^[-*] /.test(line)))
|
||||
return (
|
||||
<ul key={index}>
|
||||
{lines.map((line, i) => (
|
||||
<li key={i}>{inline(line.slice(2))}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
if (lines.every((line) => /^\d+\. /.test(line)))
|
||||
return (
|
||||
<ol key={index}>
|
||||
{lines.map((line, i) => (
|
||||
<li key={i}>{inline(line.replace(/^\d+\. /, ""))}</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
return <p key={index}>{inline(block)}</p>;
|
||||
});
|
||||
}
|
||||
|
||||
export default async function Docs({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
}) {
|
||||
const { page = "welcome" } = await searchParams;
|
||||
const guide = guides.find((item) => item.slug === page);
|
||||
if (!guide) notFound();
|
||||
const index = guides.indexOf(guide);
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className={`section page-content ${styles.layout}`}>
|
||||
<aside>
|
||||
<h2>Player docs</h2>
|
||||
<nav aria-label="Documentation pages" className={styles.menu}>
|
||||
{guides.map((item) => (
|
||||
<Link
|
||||
key={item.slug}
|
||||
href={`/docs?page=${item.slug}`}
|
||||
aria-current={page === item.slug ? "page" : undefined}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
<article className={`prose ${styles.article}`}>
|
||||
{content(guide.markdown)}
|
||||
<div className={styles.paging}>
|
||||
{index > 0 && (
|
||||
<Link href={`/docs?page=${guides[index - 1].slug}`}>
|
||||
← {guides[index - 1].title}
|
||||
</Link>
|
||||
)}
|
||||
{index < guides.length - 1 && (
|
||||
<Link href={`/docs?page=${guides[index + 1].slug}`}>
|
||||
{guides[index + 1].title} →
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user