mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
website: add documentation structure and navigation components
This commit is contained in:
parent
f228937e3e
commit
a734ded6f8
@ -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],
|
||||
};
|
||||
|
118
website/src/components/navigation/DocSideBar.astro
Normal file
118
website/src/components/navigation/DocSideBar.astro
Normal 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>
|
@ -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 />
|
||||
|
@ -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)
|
||||
|
18
website/src/content.config.ts
Normal file
18
website/src/content.config.ts
Normal 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 };
|
8
website/src/content/docs/get-started/introduction.mdx
Normal file
8
website/src/content/docs/get-started/introduction.mdx
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
layout: 'layouts/DocLayout.astro'
|
||||
title: Introduction
|
||||
description: Intro to Spotube Docs
|
||||
order: 0
|
||||
---
|
||||
|
||||
## Spotube Docs
|
85
website/src/layouts/DocLayout.astro
Normal file
85
website/src/layouts/DocLayout.astro
Normal 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>
|
32
website/src/modules/docs/Breadcrumbs.astro
Normal file
32
website/src/modules/docs/Breadcrumbs.astro
Normal 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">›</li>}
|
||||
</>
|
||||
))
|
||||
}
|
||||
</ol>
|
47
website/src/modules/docs/TableOfContents.astro
Normal file
47
website/src/modules/docs/TableOfContents.astro
Normal 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>
|
||||
)
|
||||
}
|
33
website/src/pages/docs/[...slug]/index.astro
Normal file
33
website/src/pages/docs/[...slug]/index.astro
Normal 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>
|
Loading…
Reference in New Issue
Block a user