commit 4ea7794fdc9771f8a095c6c12c34d03045b4a5a8 Author: Rainnny7 Date: Sun Oct 6 15:51:56 2024 -0400 Initial Commit diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..6b10a5b --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": [ + "next/core-web-vitals", + "next/typescript" + ] +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..cc672fe --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.lockb binary diff=lockb \ No newline at end of file diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..5767e1b --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -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 }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..acaf3a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules +.idea/ +.vscode/ +.VSCodeCounter/ +.next/ +.env*.local +next-env.d.ts +.sentryclirc +.env +sw.* +workbox-* +swe-worker-* +dist/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5f9f341 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "trailingComma": "es5", + "tabWidth": 4 +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..822f7dc --- /dev/null +++ b/Dockerfile @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d483af3 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# docs + +The public documentation for Pulse App. \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100644 index 0000000..dddc95f Binary files /dev/null and b/bun.lockb differ diff --git a/components.json b/components.json new file mode 100644 index 0000000..7a63543 --- /dev/null +++ b/components.json @@ -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" + } +} \ No newline at end of file diff --git a/docs/bob/hello.md b/docs/bob/hello.md new file mode 100644 index 0000000..ee2a454 --- /dev/null +++ b/docs/bob/hello.md @@ -0,0 +1,7 @@ +--- +title: 'Hello' +published: '10-06-2024' +summary: 'petentium usu tota noluisse errem elaboraret auctor.' +--- + +# hello \ No newline at end of file diff --git a/docs/bob/hey.md b/docs/bob/hey.md new file mode 100644 index 0000000..6e1bd62 --- /dev/null +++ b/docs/bob/hey.md @@ -0,0 +1,7 @@ +--- +title: 'Hey' +published: '10-06-2024' +summary: 'petentium usu tota noluisse errem elaboraret auctor.' +--- + +# hey \ No newline at end of file diff --git a/docs/bob/hi.md b/docs/bob/hi.md new file mode 100644 index 0000000..865dda5 --- /dev/null +++ b/docs/bob/hi.md @@ -0,0 +1,7 @@ +--- +title: 'Hi' +published: '10-06-2024' +summary: 'petentium usu tota noluisse errem elaboraret auctor.' +--- + +# hi \ No newline at end of file diff --git a/docs/home.md b/docs/home.md new file mode 100644 index 0000000..b583eae --- /dev/null +++ b/docs/home.md @@ -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. \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..e29c5be --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,14 @@ +/** @type {import("next").NextConfig} */ +const nextConfig = { + output: "standalone", + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "cdn.pulseapp.cc", + }, + ], + }, +}; + +export default nextConfig; diff --git a/package.json b/package.json new file mode 100644 index 0000000..d502c75 --- /dev/null +++ b/package.json @@ -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" + } +} \ No newline at end of file diff --git a/postcss.config.mjs b/postcss.config.mjs new file mode 100644 index 0000000..e30ba03 --- /dev/null +++ b/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import("postcss-load-config").Config} */ +const config = { + plugins: { + tailwindcss: {}, + }, +}; + +export default config; diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..ad3987c Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/media/discord.svg b/public/media/discord.svg new file mode 100644 index 0000000..64571aa --- /dev/null +++ b/public/media/discord.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/public/media/github.svg b/public/media/github.svg new file mode 100644 index 0000000..ac671ba --- /dev/null +++ b/public/media/github.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/public/media/logo.png b/public/media/logo.png new file mode 100644 index 0000000..5046004 Binary files /dev/null and b/public/media/logo.png differ diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..7190a60 --- /dev/null +++ b/renovate.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json" +} diff --git a/src/app/[[...slug]]/page.tsx b/src/app/[[...slug]]/page.tsx new file mode 100644 index 0000000..f38ff99 --- /dev/null +++ b/src/app/[[...slug]]/page.tsx @@ -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 => { + 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 ( +
+ +
+ ); +}; +export default DocsPage; diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..64bb77a --- /dev/null +++ b/src/app/layout.tsx @@ -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 => ( + + + +
+ +
+ + {children} +
+
+
+ + +); +export default RootLayout; diff --git a/src/app/styles/globals.css b/src/app/styles/globals.css new file mode 100644 index 0000000..fdc9e42 --- /dev/null +++ b/src/app/styles/globals.css @@ -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; + } +} diff --git a/src/app/types/mdx.ts b/src/app/types/mdx.ts new file mode 100644 index 0000000..4ac0934 --- /dev/null +++ b/src/app/types/mdx.ts @@ -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; +}; diff --git a/src/components/mdx.tsx b/src/components/mdx.tsx new file mode 100644 index 0000000..66be54d --- /dev/null +++ b/src/components/mdx.tsx @@ -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 => ( + + {children} + + ), + h2: ({ children }: { children: ReactNode }): ReactElement => ( + + {children} + + ), + h3: ({ children }: { children: ReactNode }): ReactElement => ( + + {children} + + ), + h4: ({ children }: { children: ReactNode }): ReactElement => ( + + {children} + + ), + h5: ({ children }: { children: ReactNode }): ReactElement => ( + + {children} + + ), + h6: ({ children }: { children: ReactNode }): ReactElement => ( + + {children} + + ), + a: ({ + href, + children, + }: { + href: string; + children: ReactNode; + }): ReactElement => ( + + {children} + + ), + p: ({ children }: { children: ReactNode }): ReactElement => ( +

{children}

+ ), + ul: ({ children }: { children: ReactNode }): ReactElement => ( + + ), +}; + +/** + * The custom render for MDX. + * + * @param props the props for the MDX + * @return the custom mdx + */ +export const CustomMDX = (props): ReactElement => ( + +); + +/** + * 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 => ( +

= 2 && "pt-7", className)}> + {children} +

+); diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx new file mode 100644 index 0000000..61b5657 --- /dev/null +++ b/src/components/navbar.tsx @@ -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 => ( + +); + +const SocialLink = ({ + name, + link, + icon, +}: { + name: string; + link: string; + icon: string; +}): ReactElement => ( +
+ + {`${name} + +
+); + +export default Navbar; diff --git a/src/components/sidebar/sidebar-links.tsx b/src/components/sidebar/sidebar-links.tsx new file mode 100644 index 0000000..e4842d0 --- /dev/null +++ b/src/components/sidebar/sidebar-links.tsx @@ -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) => ( + + ))} + + ); +}; + +type TreeNode = { + title: string; + slug: string; + isFolder: boolean; + children: Record; +}; + +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 ( +
0 ? "ml-4" : ""}`}> + {/* Indentation */} + {depth > 0 && ( +
+ )} + + + {/* Trigger */} + + + + + + + {/* Content */} + + {hasChildren && isOpen && ( + + + {Object.values(node.children).map( + (child, index, array) => ( + + ) + )} + + + )} + + +
+ ); +}; + +const buildTree = (pages: DocsContentMetadata[]): Record => { + const tree: Record = {}; + + 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; diff --git a/src/components/sidebar/sidebar.tsx b/src/components/sidebar/sidebar.tsx new file mode 100644 index 0000000..aecdf36 --- /dev/null +++ b/src/components/sidebar/sidebar.tsx @@ -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 ( +
+ {/* Links */} +
+ +
+ + {/* Theme Switcher */} +
+ + Theme Switcher +
+
+ ); +}; + +export default Sidebar; diff --git a/src/components/theme-provider.tsx b/src/components/theme-provider.tsx new file mode 100644 index 0000000..9417fc0 --- /dev/null +++ b/src/components/theme-provider.tsx @@ -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 {children}; +}; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..0270f64 --- /dev/null +++ b/src/components/ui/button.tsx @@ -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, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..9fa4894 --- /dev/null +++ b/src/components/ui/collapsible.tsx @@ -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 } diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..5af26b2 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..12d81c4 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -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, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/src/lib/mdx.ts b/src/lib/mdx.ts new file mode 100644 index 0000000..98b9dac --- /dev/null +++ b/src/lib/mdx.ts @@ -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(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 = ( + 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(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 = (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; +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..30bb919 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export const cn = (...inputs: ClassValue[]) => { + return twMerge(clsx(inputs)); +}; diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..750f8ca --- /dev/null +++ b/tailwind.config.ts @@ -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; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f48e7ee --- /dev/null +++ b/tsconfig.json @@ -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" + ] +}