Initial Commit
Some checks failed
Deploy / deploy (ubuntu-latest, 2.44.0) (push) Failing after 29s

This commit is contained in:
Braydon 2024-10-06 15:51:56 -04:00
commit 4ea7794fdc
38 changed files with 1168 additions and 0 deletions

6
.eslintrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": [
"next/core-web-vitals",
"next/typescript"
]
}

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.lockb binary diff=lockb

View File

@ -0,0 +1,31 @@
name: Deploy
on:
push:
branches: [ "master" ]
paths-ignore:
- README.md
- LICENSE
jobs:
deploy:
strategy:
matrix:
arch: [ "ubuntu-latest" ]
git-version: [ "2.44.0" ]
runs-on: ${{ matrix.arch }}
# Steps to run
steps:
# Checkout the repo
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# Deploy to Dokku
- name: Deploy to Dokku
uses: dokku/github-action@master
with:
git_remote_url: "ssh://dokku@10.10.70.73:22/docs"
ssh_private_key: ${{ secrets.DOKKU_SSH_PRIVATE_KEY }}

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
node_modules
.idea/
.vscode/
.VSCodeCounter/
.next/
.env*.local
next-env.d.ts
.sentryclirc
.env
sw.*
workbox-*
swe-worker-*
dist/

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"trailingComma": "es5",
"tabWidth": 4
}

45
Dockerfile Normal file
View File

@ -0,0 +1,45 @@
FROM imbios/bun-node AS base
# Install dependencies
FROM base AS depends
WORKDIR /usr/src/app
COPY package.json* bun.lockb* ./
RUN bun install --frozen-lockfile --quiet
# Build the app
FROM base AS builder
WORKDIR /usr/src/app
COPY --from=depends /usr/src/app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED 1
RUN bun run build
# Run the app
FROM base AS runner
WORKDIR /usr/src/app
RUN addgroup --system --gid 1007 nextjs
RUN adduser --system --uid 1007 nextjs
RUN mkdir .next
RUN chown nextjs:nextjs .next
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/.next/standalone ./
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/.next ./.next
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/public ./public
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/next.config.mjs ./next.config.mjs
COPY --from=builder --chown=nextjs:nextjs /usr/src/app/package.json ./package.json
ENV NODE_ENV production
# Exposting on port 80 so we can
# access via a reverse proxy for Dokku
ENV HOSTNAME "0.0.0.0"
EXPOSE 80
ENV PORT 80
USER nextjs
CMD node server.js

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# docs
The public documentation for Pulse App.

BIN
bun.lockb Normal file

Binary file not shown.

20
components.json Normal file
View File

@ -0,0 +1,20 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}

7
docs/bob/hello.md Normal file
View File

@ -0,0 +1,7 @@
---
title: 'Hello'
published: '10-06-2024'
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
---
# hello

7
docs/bob/hey.md Normal file
View File

@ -0,0 +1,7 @@
---
title: 'Hey'
published: '10-06-2024'
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
---
# hey

7
docs/bob/hi.md Normal file
View File

@ -0,0 +1,7 @@
---
title: 'Hi'
published: '10-06-2024'
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
---
# hi

8
docs/home.md Normal file
View File

@ -0,0 +1,8 @@
---
title: 'Home'
published: '10-06-2024'
summary: 'petentium usu tota noluisse errem elaboraret auctor.'
---
# Get started with Pulse App!
petentium usu tota noluisse errem elaboraret auctor.

14
next.config.mjs Normal file
View File

@ -0,0 +1,14 @@
/** @type {import("next").NextConfig} */
const nextConfig = {
output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "cdn.pulseapp.cc",
},
],
},
};
export default nextConfig;

44
package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "docs",
"version": "1.0.0",
"author": {
"name": "Braydon (Rainnny)",
"url": "https://rainnny.club",
"email": "braydonrainnny@gmail.com"
},
"private": true,
"scripts": {
"dev": "next dev --turbo",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"framer-motion": "^11.11.1",
"lucide-react": "^0.447.0",
"next": "^15.0.0-canary.179",
"next-themes": "^0.3.0",
"react": "^19.0.0-rc-1460d67c-20241003",
"react-dom": "^19.0.0-rc-1460d67c-20241003",
"remark-gfm": "^4.0.0",
"remote-mdx": "^0.0.8",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"eslint": "^8",
"eslint-config-next": "14.2.8"
}
}

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import("postcss-load-config").Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

4
public/media/discord.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36">
<path fill="#fff"
d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/>
</svg>

After

Width:  |  Height:  |  Size: 777 B

5
public/media/github.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 986 B

BIN
public/media/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

3
renovate.json Normal file
View File

@ -0,0 +1,3 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
}

View File

@ -0,0 +1,35 @@
import { ReactElement } from "react";
import { getDocsContent } from "@/lib/mdx";
import { notFound } from "next/navigation";
import { CustomMDX } from "@/components/mdx";
/**
* The page to render the documentation markdown content.
*
* @param params the url params
*/
const DocsPage = async ({
params,
}: {
params: Promise<{ slug: string[] }>;
}): Promise<ReactElement> => {
const slug: string = (((await params).slug as string[]) || undefined)?.join(
"/"
);
// Get the content to display based on the provided slug
const content: DocsContentMetadata | undefined = getDocsContent().find(
(metadata: DocsContentMetadata): boolean =>
metadata.slug === (slug || "home")
);
if (!content) {
notFound();
}
return (
<main>
<CustomMDX source={content.content} />
</main>
);
};
export default DocsPage;

63
src/app/layout.tsx Normal file
View File

@ -0,0 +1,63 @@
import type { Metadata, Viewport } from "next";
import "./styles/globals.css";
import { ReactElement, ReactNode } from "react";
import { ThemeProvider } from "@/components/theme-provider";
import Navbar from "@/components/navbar";
import Sidebar from "@/components/sidebar/sidebar";
/**
* The metadata for this app.
*/
export const metadata: Metadata = {
title: {
default: "Pulse App Docs",
template: "%s • Pulse App Docs",
},
description:
"A lightweight service monitoring solution for tracking the availability of whatever service your heart desires!",
openGraph: {
images: [
{
url: "https://pulseapp.cc/media/logo.png",
width: 128,
height: 128,
},
],
},
twitter: {
card: "summary",
},
};
export const viewport: Viewport = {
themeColor: "#A855F7",
};
/**
* The primary layout for this app.
*/
const RootLayout = ({
children,
}: Readonly<{
children: ReactNode;
}>): ReactElement => (
<html lang="en">
<body
className="scroll-smooth antialiased"
style={{
background:
"linear-gradient(to top, hsl(240, 6%, 10%), hsl(var(--background)))",
}}
>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
<div className="px-10 max-w-[90rem] mx-auto min-h-screen flex flex-col">
<Navbar />
<div className="w-full h-full flex flex-grow gap-5">
<Sidebar />
{children}
</div>
</div>
</ThemeProvider>
</body>
</html>
);
export default RootLayout;

View File

@ -0,0 +1,80 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: Arial, Helvetica, sans-serif;
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 271 91% 65%;
--primary-foreground: 240 5.9% 10%;
--secondary: 272 72% 47%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

46
src/app/types/mdx.ts Normal file
View File

@ -0,0 +1,46 @@
/**
* Metadata for documentation content.
*/
type DocsContentMetadata = MDXMetadata & {
/**
* The title of this content.
*/
title: string;
/**
* The date this content was published.
*/
published: string;
/**
* The summary of this content.
*/
summary: string;
};
/**
* Metadata for an MDX file.
*/
type MDXMetadata = {
/**
* The slug of the file, defined once read.
*/
slug?: string | undefined;
/**
* The extension of the file, defined once read.
*/
extension?: string | undefined;
/**
* The metadata of the file.
*/
metadata: {
[key: string]: string;
};
/**
* The content of the file.
*/
content: string;
};

103
src/components/mdx.tsx Normal file
View File

@ -0,0 +1,103 @@
import { ReactElement, ReactNode } from "react";
import { MDXRemote } from "remote-mdx/rsc";
import { cn } from "@/lib/utils";
import remarkGfm from "remark-gfm";
/**
* The MDX components to style.
*/
const components = {
h1: ({ children }: { children: ReactNode }): ReactElement => (
<Heading size={1} className="text-4xl">
{children}
</Heading>
),
h2: ({ children }: { children: ReactNode }): ReactElement => (
<Heading size={2} className="text-3xl">
{children}
</Heading>
),
h3: ({ children }: { children: ReactNode }): ReactElement => (
<Heading size={3} className="text-2xl">
{children}
</Heading>
),
h4: ({ children }: { children: ReactNode }): ReactElement => (
<Heading size={4} className="text-xl">
{children}
</Heading>
),
h5: ({ children }: { children: ReactNode }): ReactElement => (
<Heading size={5} className="text-lg">
{children}
</Heading>
),
h6: ({ children }: { children: ReactNode }): ReactElement => (
<Heading size={5} className="text-md">
{children}
</Heading>
),
a: ({
href,
children,
}: {
href: string;
children: ReactNode;
}): ReactElement => (
<a
className="text-minecraft-green-4 cursor-pointer hover:opacity-85 transition-all transform-gpu"
href={href}
>
{children}
</a>
),
p: ({ children }: { children: ReactNode }): ReactElement => (
<p className="leading-4 text-zinc-300/80">{children}</p>
),
ul: ({ children }: { children: ReactNode }): ReactElement => (
<ul className="px-3 list-disc list-inside">{children}</ul>
),
};
/**
* The custom render for MDX.
*
* @param props the props for the MDX
* @return the custom mdx
*/
export const CustomMDX = (props): ReactElement => (
<MDXRemote
{...props}
components={{
...components,
...(props.components || {}),
}}
options={{
mdxOptions: {
remarkPlugins: [remarkGfm],
},
}}
/>
);
/**
* A heading component.
*
* @param className the class name of the heading
* @param size the size of the heading
* @param children the children within the heading
* @return the heading jsx
*/
const Heading = ({
className,
size,
children,
}: {
className: string;
size: number;
children: ReactNode;
}): ReactElement => (
<h1 className={cn("pt-2.5 font-bold", size >= 2 && "pt-7", className)}>
{children}
</h1>
);

71
src/components/navbar.tsx Normal file
View File

@ -0,0 +1,71 @@
import { ReactElement } from "react";
import Link from "next/link";
import Image from "next/image";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
const Navbar = (): ReactElement => (
<nav
className={cn(
"py-4 flex justify-between items-center",
"after:absolute after:inset-x-0 after:top-[4.2rem] after:h-0.5 after:bg-muted/55"
)}
>
{/* Branding */}
<Link
className="flex gap-1 items-end hover:opacity-75 transition-all transform-gpu select-none"
href="/"
draggable={false}
>
<h1 className="text-lg font-semibold">docs.</h1>
<Image
src="/media/logo.png"
alt="Pulse App Logo"
width={36}
height={36}
/>
</Link>
{/* Right */}
<div className="flex gap-7 items-center">
{/* Search */}
<Input
className="hidden xs:flex rounded-lg"
placeholder="Search the docs..."
disabled
/>
{/* Social */}
<div className="flex gap-5 items-center">
<SocialLink
name="GitHub"
link="https://github.com/PulseAppCC"
icon="/media/github.svg"
/>
<SocialLink
name="Discord"
link="https://discord.pulseapp.cc"
icon="/media/discord.svg"
/>
</div>
</div>
</nav>
);
const SocialLink = ({
name,
link,
icon,
}: {
name: string;
link: string;
icon: string;
}): ReactElement => (
<div className="relative w-6 h-6 hover:opacity-75 transition-all transform-gpu select-none">
<Link href={link} target="_blank" draggable={false}>
<Image src={icon} alt={`${name} Logo`} fill draggable={false} />
</Link>
</div>
);
export default Navbar;

View File

@ -0,0 +1,161 @@
"use client";
import { ReactElement, useMemo, useState } from "react";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { ChevronRight } from "lucide-react";
import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
const SidebarLinks = ({
pages,
}: {
pages: DocsContentMetadata[];
}): ReactElement => {
const tree = useMemo(() => buildTree(pages), [pages]);
return (
<>
{Object.values(tree).map((node: TreeNode) => (
<CategoryItem key={node.slug} node={node} />
))}
</>
);
};
type TreeNode = {
title: string;
slug: string;
isFolder: boolean;
children: Record<string, TreeNode>;
};
const CategoryItem = ({
node,
depth = 0,
isLast = true,
}: {
node: TreeNode;
depth?: number;
isLast?: boolean;
}) => {
const path = usePathname();
const active =
(path === "/" && node.slug === "home") || path === `/${node.slug}`;
const [isOpen, setIsOpen] = useState(true);
const hasChildren = Object.keys(node.children).length > 0;
return (
<div className={`relative ${depth > 0 ? "ml-4" : ""}`}>
{/* Indentation */}
{depth > 0 && (
<div
className={`absolute left-0 top-1 bottom-0 border-l-2 border-muted`}
style={{
height: isLast ? "30px" : "100%",
}}
/>
)}
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
{/* Trigger */}
<CollapsibleTrigger asChild>
<Link
href={node.isFolder ? "#" : `/${node.slug}`}
draggable={false}
>
<Button
variant="ghost"
className={cn(
`relative ${depth > 0 ? "pl-4" : ""} w-full justify-between`,
active &&
"bg-primary/15 hover:bg-primary/20 text-primary/95 hover:text-primary"
)}
>
{node.title}
{hasChildren && (
<motion.div
initial={false}
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.2 }}
>
<ChevronRight className="h-4 w-4" />
</motion.div>
)}
</Button>
</Link>
</CollapsibleTrigger>
{/* Content */}
<AnimatePresence initial={false}>
{hasChildren && isOpen && (
<CollapsibleContent forceMount>
<motion.div
className="relative overflow-hidden"
initial="collapsed"
animate="open"
exit="collapsed"
variants={{
open: { opacity: 1, height: "auto", y: 0 },
collapsed: {
opacity: 0,
height: 0,
y: -20,
},
}}
transition={{
duration: 0.3,
ease: [0.04, 0.62, 0.23, 0.98],
}}
>
{Object.values(node.children).map(
(child, index, array) => (
<CategoryItem
key={child.slug}
node={child}
depth={depth + 1}
isLast={index === array.length - 1}
/>
)
)}
</motion.div>
</CollapsibleContent>
)}
</AnimatePresence>
</Collapsible>
</div>
);
};
const buildTree = (pages: DocsContentMetadata[]): Record<string, TreeNode> => {
const tree: Record<string, TreeNode> = {};
pages.forEach((page) => {
const parts: string[] | undefined = page.slug?.split("/");
let currentLevel = tree;
parts?.forEach((part: string, index: number) => {
if (!currentLevel[part]) {
currentLevel[part] = {
title: part,
slug: parts.slice(0, index + 1).join("/"),
isFolder: index < parts.length - 1,
children: {},
};
}
if (index === parts.length - 1) {
currentLevel[part].title = page.title;
currentLevel[part].isFolder = false;
}
currentLevel = currentLevel[part].children;
});
});
return tree;
};
export default SidebarLinks;

View File

@ -0,0 +1,24 @@
import { ReactElement } from "react";
import { Separator } from "@/components/ui/separator";
import { getDocsContent } from "@/lib/mdx";
import SidebarLinks from "@/components/sidebar/sidebar-links";
const Sidebar = (): ReactElement => {
const pages: DocsContentMetadata[] = getDocsContent();
return (
<div className="w-52 py-3 flex flex-col justify-between">
{/* Links */}
<div className="flex flex-col">
<SidebarLinks pages={pages} />
</div>
{/* Theme Switcher */}
<div className="flex flex-col">
<Separator className="mb-3" />
<span>Theme Switcher</span>
</div>
</div>
);
};
export default Sidebar;

View File

@ -0,0 +1,9 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { type ThemeProviderProps } from "next-themes/dist/types";
export const ThemeProvider = ({ children, ...props }: ThemeProviderProps) => {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
};

View File

@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

106
src/lib/mdx.ts Normal file
View File

@ -0,0 +1,106 @@
import * as fs from "node:fs";
import { Stats } from "node:fs";
import path from "node:path";
/**
* The regex to match for metadata.
*/
const METADATA_REGEX: RegExp = /---\s*([\s\S]*?)\s*---/;
/**
* The directory docs are stored in.
*/
const DOCS_DIR: string = path.join(process.cwd(), "docs");
/**
* Get the content to
* display in the docs.
*/
export const getDocsContent = (): DocsContentMetadata[] => {
const content: DocsContentMetadata[] = [];
for (const directory of getRecursiveDirectories(DOCS_DIR)) {
content.push(...getMetadata<DocsContentMetadata>(DOCS_DIR, directory));
}
return content;
};
/**
* Get the metadata of mdx
* files in the given directory.
*
* @param parent the parent directory to search
* @param directory the directory to search
*/
const getMetadata = <T extends MDXMetadata>(
parent: string,
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 {
slug: filePath
.replace(parent, "")
.replace(/\\/g, "/") // Normalize the path
.replace(/\.mdx?$/, "")
.substring(1),
extension: path.extname(file),
...parseMetadata<T>(fs.readFileSync(filePath, "utf-8")),
}; // Map each file to its metadata
});
};
/**
* Parse the metadata from
* the given content.
*
* @param content the content to parse
* @returns the metadata and content
* @template T the type of metadata
*/
const 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
const metadata: Partial<{
[key: string]: string;
}> = {}; // The metadata to return
// 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 the metadata and content. The initial
// slug is empty, and is defined later on.
return { ...metadata, content } as T;
};
/**
* Get directories recursively
* in the given directory.
*
* @param directory the directory to search
* @return the directories
*/
const getRecursiveDirectories = (directory: string): string[] => {
const directories: string[] = [directory]; // The directories to return
for (const sub of fs.readdirSync(directory)) {
const subDirPath: string = path.join(directory, sub); // The sub dir path
const stats: Stats = fs.statSync(subDirPath); // Get file stats
if (stats.isDirectory()) {
directories.push(...getRecursiveDirectories(subDirPath)); // Recursively get directories
}
}
return directories;
};

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
};

70
tailwind.config.ts Normal file
View File

@ -0,0 +1,70 @@
import type { Config } from "tailwindcss";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const defaultTheme = require("tailwindcss/defaultTheme");
const config: Config = {
darkMode: ["class"],
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
screens: {
xs: "475px",
...defaultTheme.screens,
},
extend: {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
chart: {
"1": "hsl(var(--chart-1))",
"2": "hsl(var(--chart-2))",
"3": "hsl(var(--chart-3))",
"4": "hsl(var(--chart-4))",
"5": "hsl(var(--chart-5))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [require("tailwindcss-animate")],
};
export default config;

40
tsconfig.json Normal file
View File

@ -0,0 +1,40 @@
{
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./src/*"
]
},
"target": "ES2017"
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}