initial docs

This commit is contained in:
2025-04-12 14:34:53 +07:00
commit ea9a71e23c
138 changed files with 23045 additions and 0 deletions

76
lib/changelog.ts Normal file
View File

@@ -0,0 +1,76 @@
import { promises as fs } from "fs";
import path from "path";
interface ChangelogEntry {
version: string;
date: string;
description?: string;
image?: string;
changes: {
Added?: string[];
Improved?: string[];
Fixed?: string[];
Deprecated?: string[];
Removed?: string[];
};
}
export async function getChangelogEntries(): Promise<ChangelogEntry[]> {
const filePath = path.join(process.cwd(), "CHANGELOG.md");
const content = await fs.readFile(filePath, "utf-8");
const entries: ChangelogEntry[] = [];
let currentEntry: Partial<ChangelogEntry> = {};
let currentSection: keyof ChangelogEntry["changes"] | null = null;
const lines = content.split("\n");
for (const line of lines) {
// Version and date
const versionMatch = line.match(/## \[(.+)\] - (\d{4}-\d{2}-\d{2})/);
if (versionMatch) {
if (Object.keys(currentEntry).length > 0) {
entries.push(currentEntry as ChangelogEntry);
}
currentEntry = {
version: versionMatch[1],
date: versionMatch[2].split("-").reverse().join("-"),
changes: {}
};
continue;
}
// Description
if (line.startsWith("> ")) {
currentEntry.description = line.slice(2);
continue;
}
// Image
const imageMatch = line.match(/!\[.*\]\((.*)\)/);
if (imageMatch) {
currentEntry.image = imageMatch[1];
continue;
}
// Change type
const sectionMatch = line.match(/### (Added|Improved|Fixed|Deprecated|Removed)/);
if (sectionMatch) {
currentSection = sectionMatch[1] as keyof ChangelogEntry["changes"];
currentEntry.changes = currentEntry.changes || {};
currentEntry.changes[currentSection] = [];
continue;
}
// Change item
if (line.startsWith("- ") && currentSection && currentEntry.changes) {
currentEntry.changes[currentSection]?.push(line.slice(2));
}
}
if (Object.keys(currentEntry).length > 0) {
entries.push(currentEntry as ChangelogEntry);
}
return entries;
}

229
lib/markdown.ts Normal file
View File

@@ -0,0 +1,229 @@
import { compileMDX } from "next-mdx-remote/rsc";
import path from "path";
import { promises as fs } from "fs";
import remarkGfm from "remark-gfm";
import rehypePrism from "rehype-prism-plus";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeSlug from "rehype-slug";
import rehypeCodeTitles from "rehype-code-titles";
import { page_routes, ROUTES } from "./routes-config";
import { visit } from "unist-util-visit";
import matter from "gray-matter";
// custom components imports
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import Pre from "@/components/markdown/pre";
import Note from "@/components/markdown/note";
import { Stepper, StepperItem } from "@/components/markdown/stepper";
import Image from "@/components/markdown/image";
import Link from "@/components/markdown/link";
import Outlet from "@/components/markdown/outlet";
import Youtube from "@/components/markdown/youtube";
import Tooltip from "@/components/markdown/tooltips";
import Card from "@/components/markdown/card";
import Button from "@/components/markdown/button";
import Accordion from "@/components/markdown/accordion";
import CardGroup from "@/components/markdown/cardgroup";
// add custom components
const components = {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
pre: Pre,
Note,
Stepper,
StepperItem,
img: Image,
a: Link,
Outlet,
Youtube,
Tooltip,
Card,
Button,
Accordion,
CardGroup,
};
// can be used for other pages like blogs, Guides etc
async function parseMdx<Frontmatter>(rawMdx: string) {
return await compileMDX<Frontmatter>({
source: rawMdx,
options: {
parseFrontmatter: true,
mdxOptions: {
rehypePlugins: [
preProcess,
rehypeCodeTitles,
rehypePrism,
rehypeSlug,
rehypeAutolinkHeadings,
postProcess,
],
remarkPlugins: [remarkGfm],
},
},
components,
});
}
// logic for docs
export type BaseMdxFrontmatter = {
title: string;
description: string;
image: string;
date: string;
};
export async function getDocsForSlug(slug: string) {
try {
const contentPath = getDocsContentPath(slug);
const rawMdx = await fs.readFile(contentPath, "utf-8");
return await parseMdx<BaseMdxFrontmatter>(rawMdx);
} catch (err) {
console.log(err);
}
}
export async function getDocsTocs(slug: string) {
const contentPath = getDocsContentPath(slug);
const rawMdx = await fs.readFile(contentPath, "utf-8");
// captures between ## - #### can modify accordingly
const headingsRegex = /^(#{2,4})\s(.+)$/gm;
let match;
const extractedHeadings = [];
while ((match = headingsRegex.exec(rawMdx)) !== null) {
const headingLevel = match[1].length;
const headingText = match[2].trim();
const slug = sluggify(headingText);
extractedHeadings.push({
level: headingLevel,
text: headingText,
href: `#${slug}`,
});
}
return extractedHeadings;
}
export function getPreviousNext(path: string) {
const index = page_routes.findIndex(({ href }) => href == `/${path}`);
return {
prev: page_routes[index - 1],
next: page_routes[index + 1],
};
}
function sluggify(text: string) {
const slug = text.toLowerCase().replace(/\s+/g, "-");
return slug.replace(/[^a-z0-9-]/g, "");
}
function getDocsContentPath(slug: string) {
return path.join(process.cwd(), "/contents/docs/", `${slug}/index.mdx`);
}
function justGetFrontmatterFromMD<Frontmatter>(rawMd: string): Frontmatter {
return matter(rawMd).data as Frontmatter;
}
export async function getAllChilds(pathString: string) {
const items = pathString.split("/").filter((it) => it != "");
let page_routes_copy = ROUTES;
let prevHref = "";
for (let it of items) {
const found = page_routes_copy.find((innerIt) => innerIt.href == `/${it}`);
if (!found) break;
prevHref += found.href;
page_routes_copy = found.items ?? [];
}
if (!prevHref) return [];
return await Promise.all(
page_routes_copy.map(async (it) => {
const totalPath = path.join(
process.cwd(),
"/contents/docs/",
prevHref,
it.href,
"index.mdx"
);
const raw = await fs.readFile(totalPath, "utf-8");
return {
...justGetFrontmatterFromMD<BaseMdxFrontmatter>(raw),
href: `/docs${prevHref}${it.href}`,
};
})
);
}
// for copying the code in pre
const preProcess = () => (tree: any) => {
visit(tree, (node) => {
if (node?.type === "element" && node?.tagName === "pre") {
const [codeEl] = node.children;
if (codeEl.tagName !== "code") return;
node.raw = codeEl.children?.[0].value;
}
});
};
const postProcess = () => (tree: any) => {
visit(tree, "element", (node) => {
if (node?.type === "element" && node?.tagName === "pre") {
node.properties["raw"] = node.raw;
}
});
};
export type Author = {
avatar?: string;
handle: string;
username: string;
handleUrl: string;
};
export type BlogMdxFrontmatter = BaseMdxFrontmatter & {
date: string;
authors: Author[];
cover: string;
};
export async function getAllBlogStaticPaths() {
try {
const blogFolder = path.join(process.cwd(), "/contents/blogs/");
const res = await fs.readdir(blogFolder);
return res.map((file) => file.split(".")[0]);
} catch (err) {
console.log(err);
}
}
export async function getAllBlogs() {
const blogFolder = path.join(process.cwd(), "/contents/blogs/");
const files = await fs.readdir(blogFolder);
const uncheckedRes = await Promise.all(
files.map(async (file) => {
if (!file.endsWith(".mdx")) return undefined;
const filepath = path.join(process.cwd(), `/contents/blogs/${file}`);
const rawMdx = await fs.readFile(filepath, "utf-8");
return {
...justGetFrontmatterFromMD<BlogMdxFrontmatter>(rawMdx),
slug: file.split(".")[0],
};
})
);
return uncheckedRes.filter((it) => !!it) as (BlogMdxFrontmatter & {
slug: string;
})[];
}
export async function getBlogForSlug(slug: string) {
const blogFile = path.join(process.cwd(), "/contents/blogs/", `${slug}.mdx`);
try {
const rawMdx = await fs.readFile(blogFile, "utf-8");
return await parseMdx<BlogMdxFrontmatter>(rawMdx);
} catch {
return undefined;
}
}

28
lib/routes-config.ts Normal file
View File

@@ -0,0 +1,28 @@
import docuConfig from "@/docu.json"; // Import JSON file
export type EachRoute = {
title: string;
href: string;
noLink?: boolean; // Sekarang mendukung boolean
items?: EachRoute[];
};
export const ROUTES: EachRoute[] = docuConfig.routes;
type Page = { title: string; href: string };
function getRecursiveAllLinks(node: EachRoute): Page[] {
const ans: Page[] = [];
if (!node.noLink) {
ans.push({ title: node.title, href: node.href });
}
node.items?.forEach((subNode) => {
const temp = { ...subNode, href: `${node.href}${subNode.href}` };
ans.push(...getRecursiveAllLinks(temp));
});
return ans;
}
export const page_routes: Page[] = ROUTES.map((route) =>
getRecursiveAllLinks(route)
).flat();

80
lib/utils.ts Normal file
View File

@@ -0,0 +1,80 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { EachRoute, ROUTES } from "./routes-config";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function helperSearch(
query: string,
node: EachRoute,
prefix: string,
currenLevel: number,
maxLevel?: number
) {
const res: EachRoute[] = [];
let parentHas = false;
const nextLink = `${prefix}${node.href}`;
if (!node.noLink && node.title.toLowerCase().includes(query.toLowerCase())) {
res.push({ ...node, items: undefined, href: nextLink });
parentHas = true;
}
const goNext = maxLevel ? currenLevel < maxLevel : true;
if (goNext)
node.items?.forEach((item) => {
const innerRes = helperSearch(
query,
item,
nextLink,
currenLevel + 1,
maxLevel
);
if (!!innerRes.length && !parentHas && !node.noLink) {
res.push({ ...node, items: undefined, href: nextLink });
parentHas = true;
}
res.push(...innerRes);
});
return res;
}
export function advanceSearch(query: string) {
return ROUTES.map((node) =>
helperSearch(query, node, "", 1, query.length == 0 ? 2 : undefined)
).flat();
}
// Thursday, May 23, 2024
export function formatDate(dateStr: string): string {
const [day, month, year] = dateStr.split("-").map(Number);
const date = new Date(year, month - 1, day);
const options: Intl.DateTimeFormatOptions = {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
};
return date.toLocaleDateString("en-US", options);
}
// May 23, 2024
export function formatDate2(dateStr: string): string {
const [day, month, year] = dateStr.split("-").map(Number);
const date = new Date(year, month - 1, day);
const options: Intl.DateTimeFormatOptions = {
month: "short",
day: "numeric",
year: "numeric",
};
return date.toLocaleDateString("en-US", options);
}
export function stringToDate(date: string) {
const [day, month, year] = date.split("-").map(Number);
return new Date(year, month - 1, day);
}