mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55: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,
|
FaWindows,
|
||||||
FaRedhat,
|
FaRedhat,
|
||||||
} from "react-icons/fa6";
|
} 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]> = {
|
export const routes: Record<string, [string, IconType|null]> = {
|
||||||
"/": ["Home", LuHouse],
|
"/": ["Home", LuHouse],
|
||||||
"/blog": ["Blog", LuNewspaper],
|
"/blog": ["Blog", LuNewspaper],
|
||||||
|
"/docs/get-started/introduction": ["Docs", LuBook],
|
||||||
"/downloads": ["Downloads", LuDownload],
|
"/downloads": ["Downloads", LuDownload],
|
||||||
"/about": ["About", null],
|
"/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;
|
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 justify-between w-full">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<SidebarButton client:only />
|
<SidebarButton client:only />
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { LuMenu } from "react-icons/lu";
|
import { LuMenu } from "react-icons/lu";
|
||||||
import { useOnClickOutside } from "usehooks-ts";
|
import { useOnClickOutside } from "usehooks-ts";
|
||||||
import { routes } from "~/collections/app";
|
import { routes } from "~/collections/app.ts";
|
||||||
|
|
||||||
export default function SidebarButton() {
|
export default function SidebarButton() {
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
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