website: add documentation structure and navigation components

This commit is contained in:
Kingkor Roy Tirtho 2025-08-04 21:25:24 +06:00
parent f228937e3e
commit a734ded6f8
10 changed files with 345 additions and 3 deletions

View File

@ -9,11 +9,12 @@ import {
FaWindows,
FaRedhat,
} from "react-icons/fa6";
import { LuHouse, LuNewspaper, LuDownload } from "react-icons/lu";
import { LuHouse, LuNewspaper, LuDownload, LuBook } from "react-icons/lu";
export const routes: Record<string, [string, IconType|null]> = {
"/": ["Home", LuHouse],
"/blog": ["Blog", LuNewspaper],
"/docs/get-started/introduction": ["Docs", LuBook],
"/downloads": ["Downloads", LuDownload],
"/about": ["About", null],
};

View File

@ -0,0 +1,118 @@
---
import type { HTMLAttributes } from "astro/types";
import type { CollectionEntry } from "astro:content";
import { getCollection } from "astro:content";
interface NavigationItem extends HTMLAttributes<"a"> {
title: string;
tag?: string;
}
interface NavigationGroup {
title: string;
items: NavigationItem[];
}
interface Props {
topGroups?: NavigationGroup[];
classList?: string;
bottomGroups?: NavigationGroup[];
}
const { topGroups, bottomGroups, classList } = Astro.props;
const sortByOrder = (a: CollectionEntry<"docs">, b: CollectionEntry<"docs">) =>
a.data.order - b.data.order;
async function queryCollection(startsWith: string) {
return (
await getCollection("docs", (entry) => {
if (!entry.id.startsWith(startsWith)) return false;
if (entry.id.split("/").length > 2) return false;
if (entry.id.endsWith("meta")) return false;
return true;
})
).toSorted(sortByOrder);
}
async function queryMetaCollection(startsWith: string) {
return (
await getCollection("docs", (entry) => {
if (!entry.id.startsWith(startsWith)) return false;
if (!entry.id.endsWith("meta")) return false;
return true;
})
).toSorted(sortByOrder);
}
const toNavItems = (entries: CollectionEntry<"docs">[]) =>
entries.map((page) => ({
title: page.data.title,
href: `/docs/${page.id}`,
}));
// Define navigation sections
const sections: [
string,
string,
(prefix: string) => Promise<CollectionEntry<"docs">[]>,
][] = [
["Get Started", "get-started/", queryCollection],
["Guides", "guides/", queryCollection],
["Design System", "design/", queryCollection],
["Tailwind Components", "tailwind/", queryCollection],
["Functional Components", "components/", queryMetaCollection],
["Headless Components", "headless/", queryCollection],
["Integrations", "integrations/", queryMetaCollection],
["Resources", "resources/", queryCollection],
];
// Build navigation dynamically
const navigation: NavigationGroup[] = [
...(topGroups ?? []),
...(await Promise.all(
sections.map(async ([title, prefix, queryFn]) => ({
title,
items: toNavItems(await queryFn(prefix)),
}))
)),
...(bottomGroups ?? []),
];
---
<aside class="text-sm grid gap-10" class:list={[classList]}>
{
navigation.map((group) => (
<nav class="flex flex-col gap-2">
<span class="text-sm font-bold ml-2">{group.title}</span>
<ul class="flex flex-col gap-1">
{group.items.map((item) => (
<li>
<a
href={item.href}
title={item.title}
class="flex justify-between items-center"
>
<span
class="grow px-2 py-1 rounded-base"
class:list={[
{
"preset-tonal": Astro.url.pathname === item.href,
anchor: Astro.url.pathname !== item.href,
},
]}
>
{item.title}
</span>
{item.tag && (
<span class="no-underline preset-tonal-primary text-xs px-1 capitalize rounded">
{item.tag}
</span>
)}
</a>
</li>
))}
</ul>
</nav>
))
}
</aside>

View File

@ -6,7 +6,7 @@ import SidebarButton from "./sidebar-button";
const pathname = Astro.url.pathname;
---
<header class="flex justify-between items-center p-4 bg-surface">
<header class="flex justify-between items-center px-4 bg-surface">
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2">
<SidebarButton client:only />

View File

@ -1,7 +1,7 @@
import { useRef, useState } from "react";
import { LuMenu } from "react-icons/lu";
import { useOnClickOutside } from "usehooks-ts";
import { routes } from "~/collections/app";
import { routes } from "~/collections/app.ts";
export default function SidebarButton() {
const ref = useRef<HTMLDivElement>(null)

View File

@ -0,0 +1,18 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const docs = defineCollection({
schema: z.object({
title: z.string().optional().default('(Title)'),
description: z.string().optional().default('(Description)'),
pubDate: z.date().optional(),
tags: z.array(z.string()).optional(),
order: z.number().optional().default(0)
}),
loader: glob({
base: './src/content/docs',
pattern: ['**/*.mdx', '!**/_*.mdx']
}),
});
export const collections = { docs };

View File

@ -0,0 +1,8 @@
---
layout: 'layouts/DocLayout.astro'
title: Introduction
description: Intro to Spotube Docs
order: 0
---
## Spotube Docs

View File

@ -0,0 +1,85 @@
---
import DocSideBar from "~/components/navigation/DocSideBar.astro";
import Breadcrumbs from "~/modules/docs/Breadcrumbs.astro";
import TableOfContents from "~/modules/docs/TableOfContents.astro";
interface PageHeadings {
depth: number;
slug: string;
text: string;
}
// interface Chip {
// label: string;
// href: string;
// icon?: string;
// preset?: string;
// }
interface Props {
frontmatter: {
// Required ---
title: string;
description: string;
};
headings: PageHeadings[];
}
const { frontmatter, headings } = Astro.props;
// GitHub Settings
// const branch = "website";
// URLs
// const urls = {
// githubDocsUrl: `https://github.com/KRTirtho/spotube/tree/${branch}/website/src/content`,
// githubSpotubeUrl: `https://github.com/KRTirtho/spotube`,
// };
---
<div
class="container mx-auto grid grid-cols-1 xl:grid-cols-[240px_minmax(0px,_1fr)_280px] px-4 xl:px-10"
>
<!-- Navigation -->
<aside
class="hidden xl:block self-start sticky top-[70px] h-[calc(100vh-70px)] py-4 xl:py-10 overflow-y-auto pr-10"
data-navigation
>
<DocSideBar />
</aside>
<!-- Main -->
<main
class="px-4 xl:px-10 py-10 space-y-8 [&_.scroll-header]:scroll-mt-[calc(70px+40px)]"
>
<!-- Header -->
<header class="scroll-header space-y-4" data-pagefind-body id="_top">
<!-- Breadcrumbs -->
<Breadcrumbs />
<h1 class="h1">{frontmatter.title ?? "(title)"}</h1>
<p class="text-lg opacity-60">
{frontmatter.description ?? "(description)"}
</p>
</header>
<!-- Content -->
<article class="space-y-8" data-pagefind-body>
<slot />
</article>
<!-- Footer -->
<!-- <Footer classList="py-4 px-4 xl:px-0" /> -->
</main>
<!-- Sidebar -->
<aside
class="hidden xl:block self-start sticky top-[70px] h-[calc(100vh-70px)] py-4 xl:py-10 space-y-8 overflow-y-auto"
>
<!-- Carbon Ads -->
<script
is:inline
async
type="text/javascript"
src="//cdn.carbonads.com/carbon.js?serve=CWYD627U&placement=carbonadsnet"
id="_carbonads_js"></script>
<!-- Table of Contents -->
<TableOfContents {headings} />
<!-- Promot -->
<!-- <div class="card aspect-video flex justify-center items-center preset-tonal">(promo)</div> -->
</aside>
</div>

View File

@ -0,0 +1,32 @@
---
const breadcrumbs = Astro.url.pathname
.split("/")
.filter((crumb) => Boolean(crumb) && crumb !== "docs");
---
<ol class="text-xs flex gap-2">
{
breadcrumbs.map((crumb, i) => (
<>
<li
class="capitalize"
class:list={{ "opacity-60": i !== breadcrumbs.length - 1 }}
>
{i > 0 &&
i !== breadcrumbs.length - 1 &&
breadcrumbs[0] !== "components" ? (
<a
href={`/docs/${breadcrumbs[0]}/${crumb}`}
class="hover:underline"
>
{crumb.replace("-", " ")}
</a>
) : (
crumb.replace("-", " ")
)}
</li>
{i !== breadcrumbs.length - 1 && <li class="opacity-60">&rsaquo;</li>}
</>
))
}
</ol>

View File

@ -0,0 +1,47 @@
---
interface PageHeadings {
depth: number;
slug: string;
text: string;
}
interface Props {
headings: PageHeadings[];
}
const { headings } = Astro.props;
function setDepthClass(depth: number) {
if (depth === 3) return "ml-4";
if (depth === 4) return "ml-6";
if (depth === 5) return "ml-8";
if (depth === 6) return "ml-10";
return;
}
---
{
headings.length > 0 && (
<nav class="text-sm space-y-2">
<div class="font-bold">On This Page</div>
<ul class="space-y-2">
<li>
<a href={`#_top`} class="anchor block">
Overview
</a>
</li>
{headings.map((heading: PageHeadings) => (
<li>
<a
href={`#${heading.slug}`}
class="anchor block"
class:list={`${setDepthClass(heading.depth)}`}
>
{heading.text}
</a>
</li>
))}
</ul>
</nav>
)
}

View File

@ -0,0 +1,33 @@
---
import RootLayout from "layouts/RootLayout.astro";
import type { GetStaticPaths } from "astro";
import { render } from "astro:content";
import { getCollection, getEntry } from "astro:content";
export const getStaticPaths = (async () => {
const pages = await getCollection("docs");
return pages.map((page) => ({
params: {
slug: page.id,
},
props: {
page: page,
},
}));
}) satisfies GetStaticPaths;
const { page } = Astro.props;
const { Content, remarkPluginFrontmatter } = await render(page);
let meta: Awaited<ReturnType<typeof getEntry>>;
if (page.id.startsWith("components/") || page.id.startsWith("integrations/")) {
meta = await getEntry("docs", page.id.replace(/\/[^/]*$/, "/meta"));
if (meta !== undefined) {
Object.assign(remarkPluginFrontmatter, meta.data);
}
}
---
<RootLayout>
<Content />
</RootLayout>