initial docs
This commit is contained in:
76
lib/changelog.ts
Normal file
76
lib/changelog.ts
Normal 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
229
lib/markdown.ts
Normal 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
28
lib/routes-config.ts
Normal 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
80
lib/utils.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user