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
|
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
|
# Getting Started
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Java Library
|
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
|
# Java Library
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Javascript Library
|
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
|
# Javascript Library
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
import { getDocumentation } from "@/app/common/documentation";
|
|
||||||
import { CustomMDX } from "@/app/components/mdx-components";
|
import { CustomMDX } from "@/app/components/mdx-components";
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { generateEmbed } from "@/app/common/embed";
|
import { generateEmbed } from "@/app/common/embed";
|
||||||
import { Title } from "@/app/components/title";
|
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 = {
|
type DocumentationPageParams = {
|
||||||
params: {
|
params: {
|
||||||
@ -11,36 +20,15 @@ type DocumentationPageParams = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
export async function generateStaticParams() {
|
||||||
let documentationPages = getDocumentation();
|
let documentationPages = getDocsContent();
|
||||||
|
|
||||||
return documentationPages.map(page => ({
|
return documentationPages.map(page => ({
|
||||||
slug: [page.slug],
|
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> {
|
export async function generateMetadata({ params: { slug } }: DocumentationPageParams): Promise<Metadata> {
|
||||||
const page = getPage(slug?.join("/"));
|
const page = getDocContent(slug);
|
||||||
|
|
||||||
// Fallback to page not found
|
// Fallback to page not found
|
||||||
if (!page) {
|
if (!page) {
|
||||||
@ -51,13 +39,13 @@ export async function generateMetadata({ params: { slug } }: DocumentationPagePa
|
|||||||
}
|
}
|
||||||
|
|
||||||
return generateEmbed({
|
return generateEmbed({
|
||||||
title: `${page.metadata.title} - Documentation`,
|
title: `${page.title} - Documentation`,
|
||||||
description: `${page.metadata.description}\n\nClick to view this page`,
|
description: `${page.summary}\n\nClick to view this page`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Page({ params: { slug } }: DocumentationPageParams) {
|
export default function Page({ params: { slug } }: DocumentationPageParams) {
|
||||||
const page = getPage(slug?.join("/"));
|
const page = getDocContent(slug);
|
||||||
|
|
||||||
// Page was not found, show an error page
|
// Page was not found, show an error page
|
||||||
if (!page) {
|
if (!page) {
|
||||||
@ -69,11 +57,35 @@ export default function Page({ params: { slug } }: DocumentationPageParams) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const slugParts = page.slug.split("/");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-4 flex flex-col gap-4">
|
<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 */}
|
{/* The documentation page title and description */}
|
||||||
<div className="text-center mb-4">
|
<div className="text-center mb-4">
|
||||||
<Title title={page.metadata.title} subtitle={page.metadata.description} />
|
<Title title={page.title} subtitle={page.summary} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The content of the documentation page */}
|
{/* The content of the documentation page */}
|
||||||
|
@ -1,99 +1,147 @@
|
|||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
// @ts-ignore
|
|
||||||
import read from "read-file";
|
|
||||||
|
|
||||||
type Metadata = {
|
const docsDir = path.join(process.cwd(), "documentation");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The title of the documentation page.
|
* Metadata for documentation content.
|
||||||
|
*/
|
||||||
|
type DocsContentMetadata = MDXMetadata & {
|
||||||
|
/**
|
||||||
|
* The title of this content.
|
||||||
*/
|
*/
|
||||||
title: string;
|
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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all the documentation files recursively.
|
* The metadata of the file.
|
||||||
*
|
|
||||||
* @param dirPath the directory path to search for documentation files.
|
|
||||||
*/
|
*/
|
||||||
function getDocumentationFiles(dirPath: string): string[] {
|
metadata: {
|
||||||
let files: string[] = [];
|
[key: string]: string;
|
||||||
const items = fs.readdirSync(dirPath);
|
};
|
||||||
|
|
||||||
items.forEach(item => {
|
/**
|
||||||
const itemPath = path.join(dirPath, item);
|
* The content of the file.
|
||||||
|
*/
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The regex to match for metadata.
|
||||||
|
*/
|
||||||
|
const METADATA_REGEX: RegExp = /---\s*([\s\S]*?)\s*---/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
const stat = fs.statSync(itemPath);
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
// Recursively traverse directories
|
dirs.push(...getDocsDirectories(itemPath));
|
||||||
files.push(...getDocumentationFiles(itemPath));
|
|
||||||
} else if (stat.isFile() && path.extname(item) === ".md") {
|
|
||||||
// Collect markdown files
|
|
||||||
files.push(itemPath);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
return dirs;
|
||||||
return files;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) {
|
export function getMetadata<T extends MDXMetadata>(directory: string): T[] {
|
||||||
return read.sync(file, "utf8");
|
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
|
||||||
* Gets all the documentation pages.
|
return files.map((file: string): T => {
|
||||||
*/
|
const filePath: string = path.join(directory, file); // The path of the file
|
||||||
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
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
metadata,
|
...parseMetadata<T>(fs.readFileSync(filePath, "utf-8")),
|
||||||
content,
|
slug: filePath
|
||||||
slug,
|
.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) {
|
function parseMetadata<T extends MDXMetadata>(content: string): T {
|
||||||
let frontmatterRegex = /---\s*([\s\S]*?)\s*---/;
|
const metadataBlock: string = METADATA_REGEX.exec(content)![1]; // Get the block of metadata
|
||||||
let match = frontmatterRegex.exec(fileContent);
|
content = content.replace(METADATA_REGEX, "").trim(); // Remove the metadata block from the content
|
||||||
let frontMatterBlock = match![1];
|
let metadata: Partial<{
|
||||||
let content = fileContent.replace(frontmatterRegex, "").trim();
|
[key: string]: string;
|
||||||
let frontMatterLines = frontMatterBlock.trim().split("\n");
|
}> = {}; // The metadata to return
|
||||||
let metadata: Partial<Metadata> = {};
|
|
||||||
|
|
||||||
frontMatterLines.forEach(line => {
|
// Parse the metadata block as a key-value pair
|
||||||
let [key, ...valueArr] = line.split(": ");
|
metadataBlock
|
||||||
let value = valueArr.join(": ").trim();
|
.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
|
value = value.replace(/^['"](.*)['"]$/, "$1"); // Remove quotes
|
||||||
metadata[key.trim() as keyof Metadata] = value;
|
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…
Reference in New Issue
Block a user