diff --git a/website/src/collections/app.ts b/website/src/collections/app.ts index 9a1d9596..e04b02a0 100644 --- a/website/src/collections/app.ts +++ b/website/src/collections/app.ts @@ -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 = { "/": ["Home", LuHouse], "/blog": ["Blog", LuNewspaper], + "/docs/get-started/introduction": ["Docs", LuBook], "/downloads": ["Downloads", LuDownload], "/about": ["About", null], }; diff --git a/website/src/components/navigation/DocSideBar.astro b/website/src/components/navigation/DocSideBar.astro new file mode 100644 index 00000000..0c4d8b65 --- /dev/null +++ b/website/src/components/navigation/DocSideBar.astro @@ -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[]>, +][] = [ + ["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 ?? []), +]; +--- + + diff --git a/website/src/components/navigation/TopBar.astro b/website/src/components/navigation/TopBar.astro index 7dfa570d..71d1d8ac 100644 --- a/website/src/components/navigation/TopBar.astro +++ b/website/src/components/navigation/TopBar.astro @@ -6,7 +6,7 @@ import SidebarButton from "./sidebar-button"; const pathname = Astro.url.pathname; --- -
+
diff --git a/website/src/components/navigation/sidebar-button.tsx b/website/src/components/navigation/sidebar-button.tsx index 0e31004e..96139917 100644 --- a/website/src/components/navigation/sidebar-button.tsx +++ b/website/src/components/navigation/sidebar-button.tsx @@ -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(null) diff --git a/website/src/content.config.ts b/website/src/content.config.ts new file mode 100644 index 00000000..c659504e --- /dev/null +++ b/website/src/content.config.ts @@ -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 }; \ No newline at end of file diff --git a/website/src/content/docs/get-started/introduction.mdx b/website/src/content/docs/get-started/introduction.mdx new file mode 100644 index 00000000..593a29d4 --- /dev/null +++ b/website/src/content/docs/get-started/introduction.mdx @@ -0,0 +1,8 @@ +--- +layout: 'layouts/DocLayout.astro' +title: Introduction +description: Intro to Spotube Docs +order: 0 +--- + +## Spotube Docs \ No newline at end of file diff --git a/website/src/layouts/DocLayout.astro b/website/src/layouts/DocLayout.astro new file mode 100644 index 00000000..e71ab8dd --- /dev/null +++ b/website/src/layouts/DocLayout.astro @@ -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`, +// }; +--- + +
+ + + +
+ +
+ + +

{frontmatter.title ?? "(title)"}

+

+ {frontmatter.description ?? "(description)"} +

+
+ +
+ +
+ + +
+ + +
diff --git a/website/src/modules/docs/Breadcrumbs.astro b/website/src/modules/docs/Breadcrumbs.astro new file mode 100644 index 00000000..28ff06ab --- /dev/null +++ b/website/src/modules/docs/Breadcrumbs.astro @@ -0,0 +1,32 @@ +--- +const breadcrumbs = Astro.url.pathname + .split("/") + .filter((crumb) => Boolean(crumb) && crumb !== "docs"); +--- + +
    + { + breadcrumbs.map((crumb, i) => ( + <> +
  1. + {i > 0 && + i !== breadcrumbs.length - 1 && + breadcrumbs[0] !== "components" ? ( + + {crumb.replace("-", " ")} + + ) : ( + crumb.replace("-", " ") + )} +
  2. + {i !== breadcrumbs.length - 1 &&
  3. } + + )) + } +
diff --git a/website/src/modules/docs/TableOfContents.astro b/website/src/modules/docs/TableOfContents.astro new file mode 100644 index 00000000..c4eb0081 --- /dev/null +++ b/website/src/modules/docs/TableOfContents.astro @@ -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 && ( + + ) +} diff --git a/website/src/pages/docs/[...slug]/index.astro b/website/src/pages/docs/[...slug]/index.astro new file mode 100644 index 00000000..be019bdb --- /dev/null +++ b/website/src/pages/docs/[...slug]/index.astro @@ -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>; +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); + } +} +--- + + + +