add breadcrumb and cleanup docs loader
All checks were successful
Deploy App / docker (ubuntu-latest) (push) Successful in 2m8s
All checks were successful
Deploy App / docker (ubuntu-latest) (push) Successful in 2m8s
This commit is contained in:
parent
325fe62569
commit
f7a3bb00a5
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Minecraft Utilities Documentation
|
||||
description: Welcome to the Minecraft Utilities documentation! Here you can find information on how to use the various features of the API.
|
||||
summary: Welcome to the Minecraft Utilities documentation! Here you can find information on how to use the various features of the API.
|
||||
---
|
||||
|
||||
# Getting Started
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Java Library
|
||||
description: The Java library for Minecraft Utilities is a simple way to interact with the API using Java!
|
||||
summary: The Java library for Minecraft Utilities is a simple way to interact with the API using Java!
|
||||
---
|
||||
|
||||
# Java Library
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Javascript Library
|
||||
description: The Javascript library for Minecraft Utilities is a simple way to interact with the API using Javascript!
|
||||
summary: The Javascript library for Minecraft Utilities is a simple way to interact with the API using Javascript!
|
||||
---
|
||||
|
||||
# Javascript Library
|
||||
|
@ -1,8 +1,17 @@
|
||||
import { getDocumentation } from "@/app/common/documentation";
|
||||
import { CustomMDX } from "@/app/components/mdx-components";
|
||||
import { Metadata } from "next";
|
||||
import { generateEmbed } from "@/app/common/embed";
|
||||
import { Title } from "@/app/components/title";
|
||||
import { getDocContent, getDocsContent, getDocsDirectories, getMetadata } from "@/app/common/documentation";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/app/components/ui/breadcrumb";
|
||||
import { capitalizeFirstLetter } from "@/app/common/string-utils";
|
||||
|
||||
type DocumentationPageParams = {
|
||||
params: {
|
||||
@ -11,36 +20,15 @@ type DocumentationPageParams = {
|
||||
};
|
||||
|
||||
export async function generateStaticParams() {
|
||||
let documentationPages = getDocumentation();
|
||||
let documentationPages = getDocsContent();
|
||||
|
||||
return documentationPages.map(page => ({
|
||||
slug: [page.slug],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a documentation page by its slug.
|
||||
*
|
||||
* @param slug The slug of the documentation page.
|
||||
*/
|
||||
function getPage(slug?: string) {
|
||||
const documentationPages = getDocumentation();
|
||||
let page = documentationPages.find(page => page.slug === slug);
|
||||
|
||||
// Fallback to the landing page
|
||||
if (!page && !slug) {
|
||||
page = documentationPages.find(page => page.slug === "landing");
|
||||
}
|
||||
|
||||
// We still can't find the page, return undefined
|
||||
if (!page) {
|
||||
return undefined;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params: { slug } }: DocumentationPageParams): Promise<Metadata> {
|
||||
const page = getPage(slug?.join("/"));
|
||||
const page = getDocContent(slug);
|
||||
|
||||
// Fallback to page not found
|
||||
if (!page) {
|
||||
@ -51,13 +39,13 @@ export async function generateMetadata({ params: { slug } }: DocumentationPagePa
|
||||
}
|
||||
|
||||
return generateEmbed({
|
||||
title: `${page.metadata.title} - Documentation`,
|
||||
description: `${page.metadata.description}\n\nClick to view this page`,
|
||||
title: `${page.title} - Documentation`,
|
||||
description: `${page.summary}\n\nClick to view this page`,
|
||||
});
|
||||
}
|
||||
|
||||
export default function Page({ params: { slug } }: DocumentationPageParams) {
|
||||
const page = getPage(slug?.join("/"));
|
||||
const page = getDocContent(slug);
|
||||
|
||||
// Page was not found, show an error page
|
||||
if (!page) {
|
||||
@ -69,11 +57,35 @@ export default function Page({ params: { slug } }: DocumentationPageParams) {
|
||||
);
|
||||
}
|
||||
|
||||
const slugParts = page.slug.split("/");
|
||||
|
||||
return (
|
||||
<div className="w-full px-4 flex flex-col gap-4">
|
||||
{slugParts.length > 1 && (
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/documentation">Home</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
{slugParts.map((slug, index, array) => {
|
||||
const path = array.slice(0, index + 1).join("/");
|
||||
|
||||
return (
|
||||
<>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem key={slug}>
|
||||
<BreadcrumbLink href={`/documentation/${path}`}>{capitalizeFirstLetter(slug)}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
)}
|
||||
|
||||
{/* The documentation page title and description */}
|
||||
<div className="text-center mb-4">
|
||||
<Title title={page.metadata.title} subtitle={page.metadata.description} />
|
||||
<Title title={page.title} subtitle={page.summary} />
|
||||
</div>
|
||||
|
||||
{/* The content of the documentation page */}
|
||||
|
@ -1,99 +1,147 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import path from "node:path";
|
||||
// @ts-ignore
|
||||
import read from "read-file";
|
||||
|
||||
type Metadata = {
|
||||
const docsDir = path.join(process.cwd(), "documentation");
|
||||
|
||||
/**
|
||||
* Metadata for documentation content.
|
||||
*/
|
||||
type DocsContentMetadata = MDXMetadata & {
|
||||
/**
|
||||
* The title of the documentation page.
|
||||
* The title of this content.
|
||||
*/
|
||||
title: string;
|
||||
|
||||
/**
|
||||
* The description of the documentation page.
|
||||
* The date this content was published.
|
||||
*/
|
||||
description: string;
|
||||
published: string;
|
||||
|
||||
/**
|
||||
* The summary of this content.
|
||||
*/
|
||||
summary: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The directory where the documentation files are stored.
|
||||
* Metadata for an MDX file.
|
||||
*/
|
||||
const documentationDirectory = path.join(process.cwd(), "documentation");
|
||||
type MDXMetadata = {
|
||||
/**
|
||||
* The slug of the file, defined once read.
|
||||
*/
|
||||
slug: string;
|
||||
|
||||
/**
|
||||
* The metadata of the file.
|
||||
*/
|
||||
metadata: {
|
||||
[key: string]: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The content of the file.
|
||||
*/
|
||||
content: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets all the documentation files recursively.
|
||||
*
|
||||
* @param dirPath the directory path to search for documentation files.
|
||||
* The regex to match for metadata.
|
||||
*/
|
||||
function getDocumentationFiles(dirPath: string): string[] {
|
||||
let files: string[] = [];
|
||||
const items = fs.readdirSync(dirPath);
|
||||
const METADATA_REGEX: RegExp = /---\s*([\s\S]*?)\s*---/;
|
||||
|
||||
items.forEach(item => {
|
||||
const itemPath = path.join(dirPath, item);
|
||||
/**
|
||||
* Get the directories in the
|
||||
* given directory.
|
||||
*/
|
||||
export function getDocsDirectories(dir: string) {
|
||||
const dirs: string[] = [dir];
|
||||
const paths = fs.readdirSync(dir);
|
||||
|
||||
for (const item of paths) {
|
||||
const itemPath = path.join(dir, item);
|
||||
const stat = fs.statSync(itemPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
// Recursively traverse directories
|
||||
files.push(...getDocumentationFiles(itemPath));
|
||||
} else if (stat.isFile() && path.extname(item) === ".md") {
|
||||
// Collect markdown files
|
||||
files.push(itemPath);
|
||||
dirs.push(...getDocsDirectories(itemPath));
|
||||
}
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content of a documentation file.
|
||||
* Get the content to
|
||||
* display in the docs.
|
||||
*/
|
||||
export function getDocsContent() {
|
||||
const directories = getDocsDirectories(docsDir);
|
||||
const content: DocsContentMetadata[] = [];
|
||||
|
||||
for (let directory of directories) {
|
||||
content.push(...getMetadata<DocsContentMetadata>(directory));
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
export function getDocContent(path?: string[]) {
|
||||
const docs = getDocsContent();
|
||||
const slug = path ? path.join("/") : "landing";
|
||||
|
||||
return docs.find(doc => doc.slug === slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the metadata of mdx
|
||||
* files in the given directory.
|
||||
*
|
||||
* @param file the file to get the content of.
|
||||
* @param directory the directory to search
|
||||
*/
|
||||
function getDocumentationFileContent(file: string) {
|
||||
return read.sync(file, "utf8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the documentation pages.
|
||||
*/
|
||||
export function getDocumentation() {
|
||||
const files = getDocumentationFiles(documentationDirectory);
|
||||
|
||||
return files.map(file => {
|
||||
const { metadata, content } = parseFrontmatter(getDocumentationFileContent(file));
|
||||
let slug = path.relative(documentationDirectory, file).replace(/\.(md)$/, "");
|
||||
slug = slug.replace(/\\/g, "/"); // Normalize path separators
|
||||
|
||||
export function getMetadata<T extends MDXMetadata>(directory: string): T[] {
|
||||
const files: string[] = fs.readdirSync(directory).filter((file: string): boolean => {
|
||||
const extension: string = path.extname(file); // The file extension
|
||||
return extension === ".md" || extension === ".mdx";
|
||||
}); // Read the MDX files
|
||||
return files.map((file: string): T => {
|
||||
const filePath: string = path.join(directory, file); // The path of the file
|
||||
return {
|
||||
metadata,
|
||||
content,
|
||||
slug,
|
||||
};
|
||||
...parseMetadata<T>(fs.readFileSync(filePath, "utf-8")),
|
||||
slug: filePath
|
||||
.replace(docsDir, "")
|
||||
.replace(/\.mdx?$/, "")
|
||||
.replace(/\\/g, "/")
|
||||
.substring(1),
|
||||
}; // Map each file to its metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the frontmatter of a file.
|
||||
* Parse the metadata from
|
||||
* the given content.
|
||||
*
|
||||
* @param fileContent the content of the file.
|
||||
* @param content the content to parse
|
||||
* @returns the metadata and content
|
||||
* @template T the type of metadata
|
||||
*/
|
||||
function parseFrontmatter(fileContent: string) {
|
||||
let frontmatterRegex = /---\s*([\s\S]*?)\s*---/;
|
||||
let match = frontmatterRegex.exec(fileContent);
|
||||
let frontMatterBlock = match![1];
|
||||
let content = fileContent.replace(frontmatterRegex, "").trim();
|
||||
let frontMatterLines = frontMatterBlock.trim().split("\n");
|
||||
let metadata: Partial<Metadata> = {};
|
||||
function parseMetadata<T extends MDXMetadata>(content: string): T {
|
||||
const metadataBlock: string = METADATA_REGEX.exec(content)![1]; // Get the block of metadata
|
||||
content = content.replace(METADATA_REGEX, "").trim(); // Remove the metadata block from the content
|
||||
let metadata: Partial<{
|
||||
[key: string]: string;
|
||||
}> = {}; // The metadata to return
|
||||
|
||||
frontMatterLines.forEach(line => {
|
||||
let [key, ...valueArr] = line.split(": ");
|
||||
let value = valueArr.join(": ").trim();
|
||||
value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes
|
||||
metadata[key.trim() as keyof Metadata] = value;
|
||||
});
|
||||
// Parse the metadata block as a key-value pair
|
||||
metadataBlock
|
||||
.trim() // Trim any leading or trailing whitespace
|
||||
.split("\n") // Get each line
|
||||
.forEach((line: string): void => {
|
||||
const split: string[] = line.split(": "); // Split the metadata by the colon
|
||||
let value: string = split[1].trim(); // The value of the metadata
|
||||
value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes
|
||||
metadata[split[0].trim()] = value; // Add the metadata to the object
|
||||
});
|
||||
|
||||
return { metadata: metadata as Metadata, content };
|
||||
// Return the metadata and content. The initial
|
||||
// slug is empty, and is defined later on.
|
||||
return { ...metadata, content } as T;
|
||||
}
|
||||
|
90
src/app/components/ui/breadcrumb.tsx
Normal file
90
src/app/components/ui/breadcrumb.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
|
||||
import { cn } from "@/app/common/utils";
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"nav"> & {
|
||||
separator?: React.ReactNode;
|
||||
}
|
||||
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
|
||||
Breadcrumb.displayName = "Breadcrumb";
|
||||
|
||||
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
|
||||
({ className, ...props }, ref) => (
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
BreadcrumbList.displayName = "BreadcrumbList";
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
|
||||
({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
|
||||
),
|
||||
);
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem";
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<"a"> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />;
|
||||
});
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink";
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
|
||||
({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage";
|
||||
|
||||
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
|
||||
<li role="presentation" aria-hidden="true" className={cn("[&>svg]:size-3.5", className)} {...props}>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||
|
||||
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user