87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5 | import { type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { toggleVariants } from "@/components/ui/toggle"
9 |
10 | const ToggleGroupContext = React.createContext<
11 | VariantProps
12 | >({
13 | size: "default",
14 | variant: "default",
15 | })
16 |
17 | const ToggleGroup = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef &
20 | VariantProps
21 | >(({ className, variant, size, children, ...props }, ref) => (
22 |
27 |
28 | {children}
29 |
30 |
31 | ))
32 |
33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
34 |
35 | const ToggleGroupItem = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef &
38 | VariantProps
39 | >(({ className, children, variant, size, ...props }, ref) => {
40 | const context = React.useContext(ToggleGroupContext)
41 |
42 | return (
43 |
54 | {children}
55 |
56 | )
57 | })
58 |
59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
60 |
61 | export { ToggleGroup, ToggleGroupItem }
62 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-9 px-2 min-w-9",
20 | sm: "h-8 px-1.5 min-w-8",
21 | lg: "h-10 px-2.5 min-w-10",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
19 |
28 |
29 | ))
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
31 |
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
33 |
--------------------------------------------------------------------------------
/src/env.js:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs";
2 | import { z } from "zod";
3 |
4 | export const env = createEnv({
5 | /**
6 | * Specify your server-side environment variables schema here. This way you can ensure the app
7 | * isn't built with invalid env vars.
8 | */
9 | server: {
10 | AUTH_SECRET:
11 | process.env.NODE_ENV === "production"
12 | ? z.string()
13 | : z.string().optional(),
14 | AUTH_GOOGLE_ID: z.string(),
15 | AUTH_GOOGLE_SECRET: z.string(),
16 | GEMINI_API_KEY: z.string(),
17 | DB_URL: z.string().url(),
18 | LEMONSQUEEZY_API_KEY: z.string(),
19 | LEMONSQUEEZY_WEBHOOK_SECRET: z.string(),
20 | NODE_ENV: z
21 | .enum(["development", "test", "production"])
22 | .default("development"),
23 | },
24 |
25 | /**
26 | * Specify your client-side environment variables schema here. This way you can ensure the app
27 | * isn't built with invalid env vars. To expose them to the client, prefix them with
28 | * `NEXT_PUBLIC_`.
29 | */
30 |
31 | client: {
32 | NEXT_PUBLIC_APP_URL: z.string().url(),
33 | NEXT_PUBLIC_POSTHOG_KEY: z.string(),
34 | NEXT_PUBLIC_POSTHOG_HOST: z.string(),
35 | },
36 |
37 | /**
38 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
39 | * middlewares) or client-side so we need to destruct manually.
40 | */
41 | runtimeEnv: {
42 | AUTH_SECRET: process.env.AUTH_SECRET,
43 | AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
44 | AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
45 | GEMINI_API_KEY: process.env.GEMINI_API_KEY,
46 | DB_URL: process.env.DB_URL,
47 | NODE_ENV: process.env.NODE_ENV,
48 | LEMONSQUEEZY_API_KEY: process.env.LEMONSQUEEZY_API_KEY,
49 | LEMONSQUEEZY_WEBHOOK_SECRET: process.env.LEMONSQUEEZY_WEBHOOK_SECRET,
50 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
51 | NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
52 | NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
53 | },
54 | /**
55 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
56 | * useful for Docker builds.
57 | */
58 | skipValidation: !!process.env.SKIP_ENV_VALIDATION,
59 | /**
60 | * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
61 | * `SOME_VAR=''` will throw an error.
62 | */
63 | emptyStringAsUndefined: true,
64 | });
65 |
--------------------------------------------------------------------------------
/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/src/interfaces/author.ts:
--------------------------------------------------------------------------------
1 | export type Author = {
2 | name: string;
3 | picture: string;
4 | };
--------------------------------------------------------------------------------
/src/interfaces/post.ts:
--------------------------------------------------------------------------------
1 | import { type Author } from "./author";
2 |
3 | export type Post = {
4 | slug: string;
5 | title: string;
6 | date: string;
7 | coverImage: string;
8 | author: Author;
9 | excerpt: string;
10 | ogImage: {
11 | url: string;
12 | };
13 | content: string;
14 | preview?: boolean;
15 | };
--------------------------------------------------------------------------------
/src/lib/ai-utils.ts:
--------------------------------------------------------------------------------
1 | // Re-export AI utilities from their respective modules
2 | export {
3 | determineDiagramType,
4 | generateDiagramWithAI,
5 | generateDiagramTitle,
6 | } from "./ai/diagram-generator";
7 |
8 | export { removeStyles, formatDiagramCode } from "./ai/diagram-utils";
9 |
--------------------------------------------------------------------------------
/src/lib/ai/diagram-utils.ts:
--------------------------------------------------------------------------------
1 | export const removeStyles = (diagram: string): string => {
2 | return diagram
3 | .replace(/style\s+[^\n]+/g, "")
4 | .replace(/class\s+[^\n]+/g, "")
5 | .replace(/classDef\s+[^\n]+/g, "")
6 | .replace(/linkStyle\s+[^\n]+/g, "")
7 | .replace(/\n\s*\n/g, "\n")
8 | .trim();
9 | };
10 |
11 | export const formatDiagramCode = (code: string): string => {
12 | // Remove code block markers
13 | let formattedCode = code.replace(/```mermaid\n?|\n?```/g, "").trim();
14 |
15 | // Handle potential duplicate diagram type declarations
16 | const diagramTypes = [
17 | "mindmap",
18 | "flowchart",
19 | "sequenceDiagram",
20 | "zenuml",
21 | "sankey",
22 | "timeline",
23 | "xy",
24 | "packet",
25 | "kanban",
26 | "architecture",
27 | "classDiagram",
28 | "erDiagram",
29 | "gantt",
30 | "pie",
31 | "stateDiagram",
32 | "journey",
33 | "quadrant",
34 | "requirementDiagram",
35 | "gitgraph",
36 | "c4"
37 | ];
38 | for (const type of diagramTypes) {
39 | const regex = new RegExp(`${type}\\s+${type}`, "g");
40 | formattedCode = formattedCode.replace(regex, type);
41 | }
42 |
43 | // Remove empty lines at start and end
44 | return formattedCode.replace(/^\s*[\r\n]/gm, "").trim();
45 | };
46 |
47 | export const cleanJsonResponse = (text: string): string => {
48 | // Remove markdown code block syntax and any extra whitespace
49 | const cleaned = text.replace(/```json\n?|\n?```/g, "").trim();
50 |
51 | // Find the first { and last } to extract just the JSON object
52 | const startIndex = cleaned.indexOf('{');
53 | const endIndex = cleaned.lastIndexOf('}');
54 |
55 | if (startIndex === -1 || endIndex === -1) {
56 | throw new Error("No valid JSON object found in response");
57 | }
58 |
59 | return cleaned.slice(startIndex, endIndex + 1);
60 | };
--------------------------------------------------------------------------------
/src/lib/ai/gemini-client.ts:
--------------------------------------------------------------------------------
1 | import { GoogleGenerativeAI } from "@google/generative-ai";
2 | import { env } from "@/env";
3 |
4 | const genAI = new GoogleGenerativeAI(env.GEMINI_API_KEY);
5 |
6 | export const getFlashModel = () => genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
7 | export const getFlashLiteModel = () => genAI.getGenerativeModel({ model: "gemini-2.0-flash-lite-preview-02-05" });
--------------------------------------------------------------------------------
/src/lib/ai/queue.ts:
--------------------------------------------------------------------------------
1 | import { RATE_LIMIT } from "./types";
2 |
3 | // Queue for managing API requests
4 | const requestQueue: Array<() => Promise> = [];
5 | let isProcessingQueue = false;
6 |
7 | // Sleep utility function
8 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
9 |
10 | // Process the request queue
11 | export async function processQueue() {
12 | if (isProcessingQueue || requestQueue.length === 0) return;
13 |
14 | isProcessingQueue = true;
15 | while (requestQueue.length > 0) {
16 | const request = requestQueue.shift();
17 | if (request) {
18 | await request();
19 | await sleep(RATE_LIMIT.minDelay); // Ensure minimum delay between requests
20 | }
21 | }
22 | isProcessingQueue = false;
23 | }
24 |
25 | // Wrapper for API calls with exponential backoff
26 | export async function makeAPIRequestWithRetry(
27 | apiCall: () => Promise,
28 | attempt = 0,
29 | maxAttempts = 5,
30 | ): Promise {
31 | try {
32 | return await apiCall();
33 | } catch (error) {
34 | if (
35 | error instanceof Error &&
36 | error.message.includes("429") &&
37 | attempt < maxAttempts
38 | ) {
39 | const delay = Math.min(
40 | RATE_LIMIT.maxDelay,
41 | RATE_LIMIT.initialDelay * Math.pow(2, attempt),
42 | );
43 | console.log(`Rate limited. Retrying in ${delay}ms...`);
44 | await sleep(delay);
45 | return makeAPIRequestWithRetry(apiCall, attempt + 1, maxAttempts);
46 | }
47 | throw error;
48 | }
49 | }
50 |
51 | export function addToQueue(request: () => Promise) {
52 | requestQueue.push(request);
53 | void processQueue();
54 | }
--------------------------------------------------------------------------------
/src/lib/ai/types.ts:
--------------------------------------------------------------------------------
1 | export interface DiagramTypeResponse {
2 | type: string;
3 | reasoning: string;
4 | }
5 |
6 | // Rate limiting configuration
7 | export const RATE_LIMIT = {
8 | minDelay: 1000, // Minimum delay between requests in ms
9 | maxDelay: 30000, // Maximum delay for exponential backoff
10 | initialDelay: 2000, // Initial delay for rate limiting
11 | };
12 |
13 |
14 | export interface ValidationResponse {
15 | isValid: boolean;
16 | understanding: string | null;
17 | error: string | null;
18 | }
19 |
20 | export interface TypeDeterminationResponse {
21 | type: string;
22 | reasoning: string;
23 | enhancedText: string;
24 | }
25 |
--------------------------------------------------------------------------------
/src/lib/anonymous-user.ts:
--------------------------------------------------------------------------------
1 | export interface AnonymousUserState {
2 | id: string;
3 | credits: number;
4 | lastReset: string;
5 | }
6 |
7 | let cachedAnonymousUser: AnonymousUserState | null = null;
8 |
9 | export async function getAnonymousUser(): Promise {
10 | if (typeof window === 'undefined') {
11 | return {
12 | id: '',
13 | credits: 0,
14 | lastReset: new Date().toISOString(),
15 | };
16 | }
17 |
18 | // Return cached user if available
19 | if (cachedAnonymousUser) {
20 | return cachedAnonymousUser;
21 | }
22 |
23 | try {
24 | const response = await fetch('/api/anonymous');
25 | if (!response.ok) {
26 | throw new Error('Failed to fetch anonymous user');
27 | }
28 |
29 | const data = (await response.json()) as AnonymousUserState;
30 |
31 | // Validate the response shape
32 | if (!data.id || typeof data.credits !== 'number' || !data.lastReset) {
33 | throw new Error('Invalid response format');
34 | }
35 |
36 | cachedAnonymousUser = data;
37 | return data;
38 | } catch (error) {
39 | console.error('Error fetching anonymous user:', error);
40 | return {
41 | id: '',
42 | credits: 0,
43 | lastReset: new Date().toISOString(),
44 | };
45 | }
46 | }
47 |
48 | export async function updateAnonymousCredits(newCredits: number): Promise {
49 | if (typeof window === 'undefined') return;
50 | if (cachedAnonymousUser) {
51 | cachedAnonymousUser.credits = newCredits;
52 | }
53 | }
54 |
55 | export function clearAnonymousUser(): void {
56 | cachedAnonymousUser = null;
57 | }
--------------------------------------------------------------------------------
/src/lib/api.ts:
--------------------------------------------------------------------------------
1 | import { type Post } from "@/interfaces/post";
2 | import fs from "fs";
3 | import matter from "gray-matter";
4 | import { join } from "path";
5 |
6 | const postsDirectory = join(process.cwd(), "content");
7 |
8 | export function getPostSlugs() {
9 | try {
10 | return fs.readdirSync(postsDirectory);
11 | } catch (error) {
12 | console.error('Error reading posts directory:', error);
13 | return [];
14 | }
15 | }
16 |
17 | export function getPostBySlug(slug: string) {
18 | try {
19 | const realSlug = slug.replace(/\.md$/, "");
20 | const fullPath = join(postsDirectory, `${realSlug}.md`);
21 |
22 | // Check if file exists
23 | if (!fs.existsSync(fullPath)) {
24 | return null;
25 | }
26 |
27 | const fileContents = fs.readFileSync(fullPath, "utf8");
28 | const { data, content } = matter(fileContents);
29 |
30 | if (!data || !content) {
31 | console.error('Invalid post data for slug:', slug);
32 | return null;
33 | }
34 |
35 | return { ...data, slug: realSlug, content } as Post;
36 | } catch (error) {
37 | console.error('Error reading post file:', error);
38 | return null;
39 | }
40 | }
41 |
42 | export function getAllPosts(): Post[] {
43 | try {
44 | const slugs = getPostSlugs();
45 | const posts = slugs
46 | .map((slug) => getPostBySlug(slug))
47 | .filter((post): post is Post => post !== null) // Type guard to filter out null values
48 | .sort((post1, post2) => (post1.date > post2.date ? -1 : 1));
49 | return posts;
50 | } catch (error) {
51 | console.error('Error getting all posts:', error);
52 | return [];
53 | }
54 | }
--------------------------------------------------------------------------------
/src/lib/blog.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs'
2 | import path from 'path'
3 | import matter from 'gray-matter'
4 | import { type BlogPost, type BlogMeta } from '@/types/blog'
5 |
6 | const POSTS_PATH = path.join(process.cwd(), 'content/blog')
7 |
8 | export async function getAllPosts(): Promise {
9 | const posts = fs.readdirSync(POSTS_PATH)
10 | .filter((path) => /\.mdx?$/.test(path))
11 | .map((fileName) => {
12 | const source = fs.readFileSync(path.join(POSTS_PATH, fileName), 'utf8')
13 | const slug = fileName.replace(/\.mdx?$/, '')
14 | const { data } = matter(source)
15 |
16 | return {
17 | slug,
18 | ...(data as BlogMeta),
19 | }
20 | })
21 | .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
22 |
23 | return posts
24 | }
25 |
26 | export async function getPostBySlug(slug: string): Promise<{ meta: BlogMeta; content: string }> {
27 | const postPath = path.join(POSTS_PATH, `${slug}.mdx`)
28 | const source = fs.readFileSync(postPath, 'utf8')
29 | const { data, content } = matter(source)
30 |
31 | return {
32 | meta: data as BlogMeta,
33 | content,
34 | }
35 | }
--------------------------------------------------------------------------------
/src/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const CMS_NAME = "AutoDiagram";
--------------------------------------------------------------------------------
/src/lib/lemonsqueezy.ts:
--------------------------------------------------------------------------------
1 | import { type NewCheckout, createCheckout as createLemonCheckout, lemonSqueezySetup } from '@lemonsqueezy/lemonsqueezy.js';
2 | import { env } from "@/env";
3 |
4 | // Initialize the SDK
5 | lemonSqueezySetup({
6 | apiKey: env.LEMONSQUEEZY_API_KEY,
7 | onError: (error) => console.error("Lemon Squeezy Error:", error),
8 | });
9 |
10 | /**
11 | * Creates a checkout session with Lemon Squeezy
12 | * @param variantId - The variant ID of the product
13 | * @param customerEmail - The email address of the customer
14 | * @returns Promise - The checkout URL
15 | */
16 | export async function createCheckoutSession(
17 | variantId: number,
18 | customerEmail: string
19 | ): Promise {
20 | if (!env.LEMONSQUEEZY_API_KEY) {
21 | throw new Error("LEMONSQUEEZY_API_KEY is not set");
22 | }
23 |
24 | try {
25 | const storeId = "118138"; // Your store ID
26 | const checkoutOptions: NewCheckout = {
27 | checkoutData: {
28 | email: customerEmail,
29 | custom: {
30 | user_email: customerEmail,
31 | },
32 | },
33 | testMode: env.NODE_ENV === "development",
34 | };
35 |
36 | const { error, data } = await createLemonCheckout(storeId, variantId.toString(), checkoutOptions);
37 |
38 | if (error) {
39 | console.error("LemonSqueezy API Error:", error);
40 | throw new Error(error.message);
41 | }
42 |
43 | if (!data?.data?.attributes?.url) {
44 | throw new Error("No checkout URL returned from Lemon Squeezy");
45 | }
46 |
47 | return data.data.attributes.url;
48 | } catch (error) {
49 | console.error("Error creating Lemon Squeezy checkout:", error);
50 | throw error instanceof Error
51 | ? error
52 | : new Error("Failed to create checkout session");
53 | }
54 | }
--------------------------------------------------------------------------------
/src/lib/markdownToHtml.ts:
--------------------------------------------------------------------------------
1 | import { remark } from "remark";
2 | import html from "remark-html";
3 |
4 | export default async function markdownToHtml(markdown: string) {
5 | const result = await remark().use(html).process(markdown);
6 | return result.toString();
7 | }
--------------------------------------------------------------------------------
/src/lib/mermaid-config.ts:
--------------------------------------------------------------------------------
1 | import mermaid from "mermaid";
2 |
3 | export type MermaidTheme =
4 | | "default"
5 | | "forest"
6 | | "dark"
7 | | "neutral"
8 | | "base";
9 |
10 | const defaultConfig = {
11 | startOnLoad: false,
12 | securityLevel: "strict",
13 | theme: "default",
14 | logLevel: "error",
15 | mermaid: "11.4.1",
16 | fontFamily: "arial",
17 | flowchart: {
18 | curve: "basis",
19 | padding: 20,
20 | },
21 | sequence: {
22 | actorMargin: 50,
23 | messageMargin: 40,
24 | },
25 | er: {
26 | layoutDirection: "TB",
27 | minEntityWidth: 100,
28 | },
29 | journey: {
30 | taskMargin: 50,
31 | },
32 | gitGraph: {
33 | showCommitLabel: true,
34 | },
35 | c4: {
36 | diagramMarginY: 50,
37 | c4ShapeMargin: 20,
38 | },
39 | } as const;
40 |
41 | let currentTheme: MermaidTheme = "default";
42 | const svgCache = new Map();
43 |
44 | export const initializeMermaid = async (theme: MermaidTheme = "default") => {
45 | currentTheme = theme;
46 |
47 | mermaid.initialize({
48 | ...defaultConfig,
49 | theme,
50 | });
51 | };
52 |
53 | export const validateDiagram = async (diagram: string): Promise => {
54 | try {
55 | const { svg } = await mermaid.render('validate-diagram', diagram);
56 | return !!svg;
57 | } catch (error) {
58 | console.warn('Client-side diagram validation failed:', error);
59 | return false;
60 | }
61 | };
62 |
63 | export const renderMermaidDiagram = async (diagram: string, elementId: string) => {
64 | const element = document.querySelector(elementId);
65 | if (!element) return;
66 |
67 | try {
68 | // Initialize with current theme
69 | mermaid.initialize({
70 | ...defaultConfig,
71 | theme: currentTheme
72 | });
73 |
74 | // First try parsing
75 | try {
76 | await mermaid.parse(diagram);
77 | } catch (parseError) {
78 | console.error('Mermaid parse error:', parseError);
79 | throw parseError;
80 | }
81 |
82 | // Generate unique ID
83 | const uniqueId = `mermaid-${elementId.replace(/[^a-zA-Z0-9]/g, '')}-${Math.random().toString(36).substr(2, 9)}`;
84 |
85 | // Clear previous content
86 | element.innerHTML = '';
87 |
88 | // Render if parsing succeeded
89 | try {
90 | const { svg } = await mermaid.render(uniqueId, diagram);
91 |
92 | if (svg) {
93 | element.innerHTML = svg;
94 | } else {
95 | throw new Error('Failed to generate SVG');
96 | }
97 | } catch (renderError) {
98 | console.error('Mermaid render error:', renderError);
99 | throw renderError;
100 | }
101 | } catch (error) {
102 | console.error('Failed to render diagram:', error);
103 | throw error;
104 | }
105 | };
106 |
107 | export const changeTheme = async (theme: MermaidTheme) => {
108 | if (currentTheme === theme) return;
109 |
110 | currentTheme = theme;
111 | await initializeMermaid(theme);
112 |
113 | // Clear cache when theme changes
114 | svgCache.clear();
115 | };
116 |
117 | export const getCurrentTheme = (): MermaidTheme => currentTheme;
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/mdx-components.tsx:
--------------------------------------------------------------------------------
1 | import type { MDXComponents } from 'mdx/types'
2 | import React from 'react'
3 |
4 | export function useMDXComponents(components: MDXComponents): MDXComponents {
5 | return React.useMemo(
6 | () => ({
7 | h1: ({ children }) => {children},
8 | h2: ({ children }) => {children},
9 | h3: ({ children }) => {children},
10 | p: ({ children }) => {children} ,
11 | ul: ({ children }) => ,
12 | ol: ({ children }) => {children} ,
13 | li: ({ children }) => {children},
14 | code: ({ children }) => (
15 | {children}
16 | ),
17 | pre: ({ children }) => (
18 | {children}
19 | ),
20 | ...components,
21 | }),
22 | [components]
23 | )
24 | }
--------------------------------------------------------------------------------
/src/server/api/root.ts:
--------------------------------------------------------------------------------
1 | import { postRouter } from "@/server/api/routers/post";
2 | import { aiRouter } from "./routers/ai";
3 | import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
4 | import { diagramRouter } from "./routers/diagram";
5 |
6 | /**
7 | * This is the primary router for your server.
8 | *
9 | * All routers added in /api/routers should be manually added here.
10 | */
11 | export const appRouter = createTRPCRouter({
12 | post: postRouter,
13 | ai: aiRouter,
14 | diagram: diagramRouter,
15 | });
16 |
17 | // export type definition of API
18 | export type AppRouter = typeof appRouter;
19 |
20 | /**
21 | * Create a server-side caller for the tRPC API.
22 | * @example
23 | * const trpc = createCaller(createContext);
24 | * const res = await trpc.post.all();
25 | * ^? Post[]
26 | */
27 | export const createCaller = createCallerFactory(appRouter);
28 |
--------------------------------------------------------------------------------
/src/server/api/routers/diagram.ts:
--------------------------------------------------------------------------------
1 | import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
2 | import { z } from "zod";
3 |
4 | export const diagramRouter = createTRPCRouter({
5 | getUserDiagrams: protectedProcedure.query(async ({ ctx }) => {
6 | return ctx.db.diagram.findMany({
7 | where: {
8 | userId: ctx.session.user.id,
9 | },
10 | orderBy: {
11 | createdAt: "desc",
12 | },
13 | });
14 | }),
15 |
16 | update: protectedProcedure
17 | .input(
18 | z.object({
19 | id: z.string(),
20 | content: z.string(),
21 | }),
22 | )
23 | .mutation(async ({ ctx, input }) => {
24 | // First verify the user owns this diagram
25 | const diagram = await ctx.db.diagram.findFirst({
26 | where: {
27 | id: input.id,
28 | userId: ctx.session.user.id,
29 | },
30 | });
31 |
32 | if (!diagram) {
33 | throw new Error("Diagram not found or unauthorized");
34 | }
35 |
36 | // Update the diagram
37 | return ctx.db.diagram.update({
38 | where: {
39 | id: input.id,
40 | },
41 | data: {
42 | content: input.content,
43 | },
44 | });
45 | }),
46 | });
--------------------------------------------------------------------------------
/src/server/api/routers/post.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | import {
4 | createTRPCRouter,
5 | protectedProcedure,
6 | publicProcedure,
7 | } from "@/server/api/trpc";
8 |
9 | export const postRouter = createTRPCRouter({
10 | hello: publicProcedure
11 | .input(z.object({ text: z.string() }))
12 | .query(({ input }) => {
13 | return {
14 | greeting: `Hello ${input.text}`,
15 | };
16 | }),
17 |
18 | create: protectedProcedure
19 | .input(z.object({ name: z.string().min(1) }))
20 | .mutation(async ({ ctx, input }) => {
21 | return ctx.db.post.create({
22 | data: {
23 | name: input.name,
24 | createdBy: { connect: { id: ctx.session.user.id } },
25 | },
26 | });
27 | }),
28 |
29 | getLatest: protectedProcedure.query(async ({ ctx }) => {
30 | const post = await ctx.db.post.findFirst({
31 | orderBy: { createdAt: "desc" },
32 | where: { createdBy: { id: ctx.session.user.id } },
33 | });
34 |
35 | return post ?? null;
36 | }),
37 |
38 | getSecretMessage: protectedProcedure.query(() => {
39 | return "you can now see this secret message!";
40 | }),
41 | });
42 |
--------------------------------------------------------------------------------
/src/server/auth/config.ts:
--------------------------------------------------------------------------------
1 | import { PrismaAdapter } from "@auth/prisma-adapter";
2 | import { type DefaultSession, type NextAuthConfig } from "next-auth";
3 | import GoogleProvider from "next-auth/providers/google";
4 |
5 | import { db } from "@/server/db";
6 |
7 | /**
8 | * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
9 | * object and keep type safety.
10 | *
11 | * @see https://next-auth.js.org/getting-started/typescript#module-augmentation
12 | */
13 | declare module "next-auth" {
14 | interface Session extends DefaultSession {
15 | user: {
16 | id: string;
17 | // ...other properties
18 | // role: UserRole;
19 | } & DefaultSession["user"];
20 | }
21 |
22 | // interface User {
23 | // // ...other properties
24 | // // role: UserRole;
25 | // }
26 | }
27 |
28 | /**
29 | * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
30 | *
31 | * @see https://next-auth.js.org/configuration/options
32 | */
33 | export const authConfig = {
34 | providers: [
35 | GoogleProvider,
36 | /**
37 | * ...add more providers here.
38 | *
39 | * Most other providers require a bit more work than the Discord provider. For example, the
40 | * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
41 | * model. Refer to the NextAuth.js docs for the provider you want to use. Example:
42 | *
43 | * @see https://next-auth.js.org/providers/github
44 | */
45 | ],
46 | adapter: PrismaAdapter(db),
47 | callbacks: {
48 | session: ({ session, user }) => ({
49 | ...session,
50 | user: {
51 | ...session.user,
52 | id: user.id,
53 | },
54 | }),
55 | },
56 | } satisfies NextAuthConfig;
--------------------------------------------------------------------------------
/src/server/auth/index.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth";
2 | import { cache } from "react";
3 |
4 | import { authConfig } from "./config";
5 |
6 | const { auth: uncachedAuth, handlers, signIn, signOut } = NextAuth(authConfig);
7 |
8 | const auth = cache(uncachedAuth);
9 |
10 | export { auth, handlers, signIn, signOut };
--------------------------------------------------------------------------------
/src/server/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | import { env } from "@/env";
4 |
5 | const createPrismaClient = () =>
6 | new PrismaClient({
7 | log:
8 | env.NODE_ENV === "development" ? ["error", "warn"] : ["error"],
9 | });
10 |
11 | const globalForPrisma = globalThis as unknown as {
12 | prisma: ReturnType | undefined;
13 | };
14 |
15 | export const db = globalForPrisma.prisma ?? createPrismaClient();
16 |
17 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
18 |
19 | // Run the schema push in development
20 | if (env.NODE_ENV === "development") {
21 | void db.$connect().then(() => {
22 | console.log("Connected to database");
23 | });
24 | }
25 |
--------------------------------------------------------------------------------
/src/store/diagram-store.ts:
--------------------------------------------------------------------------------
1 | import { create } from "zustand";
2 | import { persist } from "zustand/middleware";
3 |
4 | export type Diagram = {
5 | id: string;
6 | content: string;
7 | type: string;
8 | name?: string;
9 | isComplex: boolean;
10 | createdAt: Date;
11 | updatedAt: Date;
12 | };
13 |
14 | type DiagramStore = {
15 | diagrams: Diagram[];
16 | anonymousDiagramCount: number;
17 | setDiagrams: (diagrams: Diagram[]) => void;
18 | addDiagram: (diagram: Diagram) => void;
19 | incrementAnonymousCount: () => void;
20 | resetAnonymousCount: () => void;
21 | credits: number;
22 | setCredits: (credits: number) => void;
23 | };
24 |
25 | export const useDiagramStore = create()(
26 | persist(
27 | (set) => ({
28 | diagrams: [],
29 | anonymousDiagramCount: 0,
30 | credits: 0,
31 | setDiagrams: (diagrams) => set({ diagrams }),
32 | addDiagram: (diagram) =>
33 | set((state) => ({
34 | diagrams: [...state.diagrams, diagram],
35 | })),
36 | incrementAnonymousCount: () =>
37 | set((state) => ({
38 | anonymousDiagramCount: state.anonymousDiagramCount + 1,
39 | })),
40 | resetAnonymousCount: () => set({ anonymousDiagramCount: 0 }),
41 | setCredits: (credits) => set({ credits }),
42 | }),
43 | {
44 | name: "diagram-storage",
45 | partialize: (state) => ({
46 | anonymousDiagramCount: state.anonymousDiagramCount,
47 | }),
48 | },
49 | ),
50 | );
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 | @layer base {
5 | :root {
6 | --background: 0 0% 100%;
7 | --foreground: 216 32.17% 22.55%;
8 | --card: 0 0% 100%;
9 | --card-foreground: 0 0% 4%;
10 | --popover: 0 0% 96%;
11 | --popover-foreground: 0 0% 4%;
12 | --primary: 265 88.93% 49.61%;
13 | --primary-foreground: 210 40% 98%;
14 | --secondary: 267 85.62% 70%;
15 | --secondary-foreground: 210 40% 98.04%;
16 | --muted: 210 40% 96%;
17 | --muted-foreground: 215 13.79% 34.12%;
18 | --accent: 225 26.67% 94.12%;
19 | --accent-foreground: 216 32.47% 15.1%;
20 | --destructive: 0 84% 60%;
21 | --destructive-foreground: 0 0% 98%;
22 | --border: 0 0% 90%;
23 | --input: 0 0% 90%;
24 | --ring: 0 0% 4%;
25 | --chart-1: 12 76% 61%;
26 | --chart-2: 173 58% 39%;
27 | --chart-3: 197 37% 24%;
28 | --chart-4: 43 74% 66%;
29 | --chart-5: 27 87% 67%;
30 | --radius: 0.5rem;
31 | --sidebar-background: 0 0% 98%;
32 | --sidebar-foreground: 240 5.3% 26.1%;
33 | --sidebar-primary: 240 5.9% 10%;
34 | --sidebar-primary-foreground: 0 0% 98%;
35 | --sidebar-accent: 240 4.8% 95.9%;
36 | --sidebar-accent-foreground: 240 5.9% 10%;
37 | --sidebar-border: 220 13% 91%;
38 | --sidebar-ring: 217.2 91.2% 59.8%
39 | }
40 | .dark {
41 | --background: 0 0% 3.9%;
42 | --foreground: 0 0% 98%;
43 | --card: 0 0% 3.9%;
44 | --card-foreground: 0 0% 98%;
45 | --popover: 0 0% 3.9%;
46 | --popover-foreground: 0 0% 98%;
47 | --primary: 0 0% 98%;
48 | --primary-foreground: 0 0% 9%;
49 | --secondary: 0 0% 14.9%;
50 | --secondary-foreground: 0 0% 98%;
51 | --muted: 0 0% 14.9%;
52 | --muted-foreground: 0 0% 63.9%;
53 | --accent: 0 0% 14.9%;
54 | --accent-foreground: 0 0% 98%;
55 | --destructive: 0 62.8% 30.6%;
56 | --destructive-foreground: 0 0% 98%;
57 | --border: 0 0% 14.9%;
58 | --input: 0 0% 14.9%;
59 | --ring: 0 0% 83.1%;
60 | --chart-1: 220 70% 50%;
61 | --chart-2: 160 60% 45%;
62 | --chart-3: 30 80% 55%;
63 | --chart-4: 280 65% 60%;
64 | --chart-5: 340 75% 55%
65 | ;
66 | --sidebar-background: 240 5.9% 10%;
67 | --sidebar-foreground: 240 4.8% 95.9%;
68 | --sidebar-primary: 224.3 76.3% 48%;
69 | --sidebar-primary-foreground: 0 0% 100%;
70 | --sidebar-accent: 240 3.7% 15.9%;
71 | --sidebar-accent-foreground: 240 4.8% 95.9%;
72 | --sidebar-border: 240 3.7% 15.9%;
73 | --sidebar-ring: 217.2 91.2% 59.8%}
74 | }
75 | @layer base {
76 | * {
77 | @apply border-border;
78 | }
79 | body {
80 | @apply bg-background text-foreground;
81 | }
82 | }
83 | .glassmorphism {
84 | background: rgba(255, 255, 255, 0.25);
85 | backdrop-filter: blur(4px);
86 | -webkit-backdrop-filter: blur(4px);
87 | }
--------------------------------------------------------------------------------
/src/syntax/journey.md:
--------------------------------------------------------------------------------
1 | # User Journey Diagram
2 |
3 | > User journeys describe at a high level of detail exactly what steps different users take to complete a specific task within a system, application or website. This technique shows the current (as-is) user workflow, and reveals areas of improvement for the to-be workflow. (Wikipedia)
4 |
5 | Mermaid can render user journey diagrams:
6 |
7 | ```mermaid-example
8 | journey
9 | title My working day
10 | section Go to work
11 | Make tea: 5: Me
12 | Go upstairs: 3: Me
13 | Do work: 1: Me, Cat
14 | section Go home
15 | Go downstairs: 5: Me
16 | Sit down: 5: Me
17 | ```
18 |
19 | Each user journey is split into sections, these describe the part of the task
20 | the user is trying to complete.
21 |
22 | Tasks syntax is `Task name: : `
23 |
--------------------------------------------------------------------------------
/src/syntax/packet.md:
--------------------------------------------------------------------------------
1 | # Packet Diagram (v11.0.0+)
2 |
3 | ## Introduction
4 |
5 | A packet diagram is a visual representation used to illustrate the structure and contents of a network packet. Network packets are the fundamental units of data transferred over a network.
6 |
7 | ## Usage
8 |
9 | This diagram type is particularly useful for developers, network engineers, educators, and students who require a clear and concise way to represent the structure of network packets.
10 |
11 | ## Syntax
12 |
13 | ```md
14 | packet-beta
15 | start: "Block name" %% Single-bit block
16 | start-end: "Block name" %% Multi-bit blocks
17 | ... More Fields ...
18 | ```
19 |
20 | ## Examples
21 |
22 | ```mermaid-example
23 | ---
24 | title: "TCP Packet"
25 | ---
26 | packet-beta
27 | 0-15: "Source Port"
28 | 16-31: "Destination Port"
29 | 32-63: "Sequence Number"
30 | 64-95: "Acknowledgment Number"
31 | 96-99: "Data Offset"
32 | 100-105: "Reserved"
33 | 106: "URG"
34 | 107: "ACK"
35 | 108: "PSH"
36 | 109: "RST"
37 | 110: "SYN"
38 | 111: "FIN"
39 | 112-127: "Window"
40 | 128-143: "Checksum"
41 | 144-159: "Urgent Pointer"
42 | 160-191: "(Options and Padding)"
43 | 192-255: "Data (variable length)"
44 | ```
45 |
46 | ```mermaid-example
47 | packet-beta
48 | title UDP Packet
49 | 0-15: "Source Port"
50 | 16-31: "Destination Port"
51 | 32-47: "Length"
52 | 48-63: "Checksum"
53 | 64-95: "Data (variable length)"
54 | ```
55 |
56 | ## Details of Syntax
57 |
58 | - **Ranges**: Each line after the title represents a different field in the packet. The range (e.g., `0-15`) indicates the bit positions in the packet.
59 | - **Field Description**: A brief description of what the field represents, enclosed in quotes.
60 |
61 | ## Configuration
62 |
63 | Please refer to the [configuration](/config/schema-docs/config-defs-packet-diagram-config.html) guide for details.
64 |
65 |
102 |
--------------------------------------------------------------------------------
/src/syntax/pie.md:
--------------------------------------------------------------------------------
1 | # Pie chart diagrams
2 |
3 | > A pie chart (or a circle chart) is a circular statistical graphic, which is divided into slices to illustrate numerical proportion. In a pie chart, the arc length of each slice (and consequently its central angle and area), is proportional to the quantity it represents. While it is named for its resemblance to a pie which has been sliced, there are variations on the way it can be presented. The earliest known pie chart is generally credited to William Playfair's Statistical Breviary of 1801
4 | > -Wikipedia
5 |
6 | Mermaid can render Pie Chart diagrams.
7 |
8 | ```mermaid-example
9 | pie title Pets adopted by volunteers
10 | "Dogs" : 386
11 | "Cats" : 85
12 | "Rats" : 15
13 | ```
14 |
15 | ## Syntax
16 |
17 | Drawing a pie chart is really simple in mermaid.
18 |
19 | - Start with `pie` keyword to begin the diagram
20 | - `showData` to render the actual data values after the legend text. This is **_OPTIONAL_**
21 | - Followed by `title` keyword and its value in string to give a title to the pie-chart. This is **_OPTIONAL_**
22 | - Followed by dataSet. Pie slices will be ordered clockwise in the same order as the labels.
23 | - `label` for a section in the pie diagram within `" "` quotes.
24 | - Followed by `:` colon as separator
25 | - Followed by `positive numeric value` (supported up to two decimal places)
26 |
27 | [pie] [showData] (OPTIONAL)
28 | [title] [titlevalue] (OPTIONAL)
29 | "[datakey1]" : [dataValue1]
30 | "[datakey2]" : [dataValue2]
31 | "[datakey3]" : [dataValue3]
32 | .
33 | .
34 |
35 | ## Example
36 |
37 | ```mermaid-example
38 | %%{init: {"pie": {"textPosition": 0.5}, "themeVariables": {"pieOuterStrokeWidth": "5px"}} }%%
39 | pie showData
40 | title Key elements in Product X
41 | "Calcium" : 42.96
42 | "Potassium" : 50.05
43 | "Magnesium" : 10.01
44 | "Iron" : 5
45 | ```
46 |
47 | ## Configuration
48 |
49 | Possible pie diagram configuration parameters:
50 |
51 | | Parameter | Description | Default value |
52 | | -------------- | ------------------------------------------------------------------------------------------------------------ | ------------- |
53 | | `textPosition` | The axial position of the pie slice labels, from 0.0 at the center to 1.0 at the outside edge of the circle. | `0.75` |
54 |
--------------------------------------------------------------------------------
/src/trpc/query-client.ts:
--------------------------------------------------------------------------------
1 | import {
2 | defaultShouldDehydrateQuery,
3 | QueryClient,
4 | } from "@tanstack/react-query";
5 | import SuperJSON from "superjson";
6 |
7 | export const createQueryClient = () =>
8 | new QueryClient({
9 | defaultOptions: {
10 | queries: {
11 | // With SSR, we usually want to set some default staleTime
12 | // above 0 to avoid refetching immediately on the client
13 | staleTime: 30 * 1000,
14 | },
15 | dehydrate: {
16 | serializeData: SuperJSON.serialize,
17 | shouldDehydrateQuery: (query) =>
18 | defaultShouldDehydrateQuery(query) ||
19 | query.state.status === "pending",
20 | },
21 | hydrate: {
22 | deserializeData: SuperJSON.deserialize,
23 | },
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/src/trpc/react.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryClientProvider, type QueryClient } from "@tanstack/react-query";
4 | import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
5 | import { createTRPCReact } from "@trpc/react-query";
6 | import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
7 | import { useState } from "react";
8 | import SuperJSON from "superjson";
9 |
10 | import { type AppRouter } from "@/server/api/root";
11 | import { createQueryClient } from "./query-client";
12 |
13 | let clientQueryClientSingleton: QueryClient | undefined = undefined;
14 | const getQueryClient = () => {
15 | if (typeof window === "undefined") {
16 | // Server: always make a new query client
17 | return createQueryClient();
18 | }
19 | // Browser: use singleton pattern to keep the same query client
20 | return (clientQueryClientSingleton ??= createQueryClient());
21 | };
22 |
23 | export const api = createTRPCReact();
24 |
25 | /**
26 | * Inference helper for inputs.
27 | *
28 | * @example type HelloInput = RouterInputs['example']['hello']
29 | */
30 | export type RouterInputs = inferRouterInputs;
31 |
32 | /**
33 | * Inference helper for outputs.
34 | *
35 | * @example type HelloOutput = RouterOutputs['example']['hello']
36 | */
37 | export type RouterOutputs = inferRouterOutputs;
38 |
39 | export function TRPCReactProvider(props: { children: React.ReactNode }) {
40 | const queryClient = getQueryClient();
41 |
42 | const [trpcClient] = useState(() =>
43 | api.createClient({
44 | links: [
45 | loggerLink({
46 | enabled: (op) =>
47 | process.env.NODE_ENV === "development" ||
48 | (op.direction === "down" && op.result instanceof Error),
49 | }),
50 | unstable_httpBatchStreamLink({
51 | transformer: SuperJSON,
52 | url: getBaseUrl() + "/api/trpc",
53 | headers: () => {
54 | const headers = new Headers();
55 | headers.set("x-trpc-source", "nextjs-react");
56 | return headers;
57 | },
58 | }),
59 | ],
60 | })
61 | );
62 |
63 | return (
64 |
65 |
66 | {props.children}
67 |
68 |
69 | );
70 | }
71 |
72 | function getBaseUrl() {
73 | if (typeof window !== "undefined") return window.location.origin;
74 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
75 | return `http://localhost:${process.env.PORT ?? 3000}`;
76 | }
77 |
--------------------------------------------------------------------------------
/src/trpc/server.ts:
--------------------------------------------------------------------------------
1 | import "server-only";
2 |
3 | import { createHydrationHelpers } from "@trpc/react-query/rsc";
4 | import { headers } from "next/headers";
5 | import { cache } from "react";
6 |
7 | import { createCaller, type AppRouter } from "@/server/api/root";
8 | import { createTRPCContext } from "@/server/api/trpc";
9 | import { createQueryClient } from "./query-client";
10 |
11 | /**
12 | * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
13 | * handling a tRPC call from a React Server Component.
14 | */
15 | const createContext = cache(async () => {
16 | const heads = new Headers(await headers());
17 | heads.set("x-trpc-source", "rsc");
18 |
19 | return createTRPCContext({
20 | headers: heads,
21 | });
22 | });
23 |
24 | const getQueryClient = cache(createQueryClient);
25 | const caller = createCaller(createContext);
26 |
27 | export const { trpc: api, HydrateClient } = createHydrationHelpers(
28 | caller,
29 | getQueryClient
30 | );
31 |
--------------------------------------------------------------------------------
/src/types/blog.ts:
--------------------------------------------------------------------------------
1 | export interface BlogPost {
2 | slug: string
3 | title: string
4 | date: string
5 | description: string
6 | author: string
7 | readingTime: string
8 | tags: string[]
9 | image?: string
10 | }
11 |
12 | export interface BlogMeta {
13 | title: string
14 | description: string
15 | date: string
16 | author: string
17 | readingTime: string
18 | tags: string[]
19 | }
--------------------------------------------------------------------------------
/src/types/diagram-types.ts:
--------------------------------------------------------------------------------
1 | export interface DiagramType {
2 | name: string;
3 | image: string;
4 | }
5 |
6 | export const DIAGRAM_TYPES: DiagramType[] = [
7 | {
8 | name: "Mind Maps",
9 | image: "/about/mindmap.svg",
10 | },
11 | {
12 | name: "Timelines",
13 | image: "/about/timeline.svg",
14 | },
15 | {
16 | name: "ZenUML",
17 | image: "/about/ZenUML.svg",
18 | },
19 | {
20 | name: "Sankey",
21 | image: "/about/sankey.svg",
22 | },
23 | {
24 | name: "XY Charts",
25 | image: "/about/xydiagram.svg",
26 | },
27 | {
28 | name: "Packet Diagrams",
29 | image: "/about/packet.svg",
30 | },
31 | {
32 | name: "Architecture",
33 | image: "/about/architecture.svg",
34 | },
35 | ];
36 |
--------------------------------------------------------------------------------
/src/types/diagram.ts:
--------------------------------------------------------------------------------
1 | // Supported Mermaid diagram types with their descriptions
2 | export const DIAGRAM_TYPES = {
3 | flowchart: "Process flows, workflows, and general flow diagrams",
4 | sequenceDiagram: "Interactions between systems, API flows, and message sequences",
5 | classDiagram: "Object-oriented structures, class relationships, and system architecture",
6 | stateDiagram: "State machines, transitions, and system states",
7 | erDiagram: "Database schemas, entity relationships, and data models",
8 | gantt: "Project timelines, schedules, and task dependencies",
9 | pie: "Statistical distributions and proportional data",
10 | quadrant: "2x2 matrices, SWOT analysis, and strategic planning",
11 | requirementDiagram: "System requirements, dependencies, and specifications",
12 | gitgraph: "Git workflows, branching strategies, and version control",
13 | c4: "System context, containers, components, and code architecture",
14 | mindmap: "Hierarchical structures, brainstorming, and concept maps",
15 | timeline: "Chronological events, historical data, and time-based flows",
16 | zenuml: "UML sequence diagrams with a more natural syntax",
17 | sankey: "Flow quantities, data transfers, and resource distributions",
18 | xy: "Scatter plots, line graphs, and data correlations",
19 | packet: "Network packets, protocol flows, and data transmission",
20 | kanban: "Project management, task organization, and workflow status",
21 | architecture: "System architecture, component relationships, and infrastructure"
22 | } as const;
23 |
24 | export type DiagramType = keyof typeof DIAGRAM_TYPES;
25 |
26 | // Example suggestions for different diagram types
27 | export const EXAMPLE_SUGGESTIONS: Record = {
28 | flowchart: "Create a workflow showing the user registration process",
29 | sequenceDiagram: "Show the API communication between frontend and backend",
30 | classDiagram: "Describe the class structure of a blog system",
31 | stateDiagram: "Illustrate the states of an order processing system",
32 | erDiagram: "Design a database schema for a social media platform",
33 | gantt: "Plan the development phases of a new feature",
34 | pie: "Show the distribution of user types in the system",
35 | quadrant: "Create a risk assessment matrix",
36 | requirementDiagram: "Document system requirements for authentication",
37 | gitgraph: "Visualize the git branching strategy",
38 | c4: "Show the system architecture of a microservices app",
39 | mindmap: "Brainstorm features for a new product",
40 | timeline: "Display the project milestones",
41 | zenuml: "Describe the login sequence with detailed interactions",
42 | sankey: "Show data flow between system components",
43 | xy: "Plot user engagement over time",
44 | packet: "Illustrate the network protocol flow",
45 | kanban: "Organize development tasks by status",
46 | architecture: "Design the overall system infrastructure"
47 | };
--------------------------------------------------------------------------------
/src/types/mermaid.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | interface Window {
3 | mermaid: {
4 | initialize: (config: { startOnLoad: boolean; securityLevel: string }) => Promise;
5 | parse: (code: string) => Promise;
6 | render: (id: string, code: string) => Promise<{ svg: string }>;
7 | };
8 | validateDiagram: (code: string) => Promise<{ isValid: boolean; error: string | null }>;
9 | }
10 | }
11 |
12 | export interface ValidationResult {
13 | isValid: boolean;
14 | error: string | null;
15 | }
--------------------------------------------------------------------------------
/start-database.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Use this script to start a docker container for a local development database
3 |
4 | # TO RUN ON WINDOWS:
5 | # 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install
6 | # 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/
7 | # 3. Open WSL - `wsl`
8 | # 4. Run this script - `./start-database.sh`
9 |
10 | # On Linux and macOS you can run this script directly - `./start-database.sh`
11 |
12 | DB_CONTAINER_NAME="diagram-postgres"
13 |
14 | if ! [ -x "$(command -v docker)" ]; then
15 | echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/"
16 | exit 1
17 | fi
18 |
19 | if ! docker info > /dev/null 2>&1; then
20 | echo "Docker daemon is not running. Please start Docker and try again."
21 | exit 1
22 | fi
23 |
24 | if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then
25 | echo "Database container '$DB_CONTAINER_NAME' already running"
26 | exit 0
27 | fi
28 |
29 | if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then
30 | docker start "$DB_CONTAINER_NAME"
31 | echo "Existing database container '$DB_CONTAINER_NAME' started"
32 | exit 0
33 | fi
34 |
35 | # import env variables from .env
36 | set -a
37 | source .env
38 |
39 | DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}')
40 | DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}')
41 |
42 | if [ "$DB_PASSWORD" = "password" ]; then
43 | echo "You are using the default database password"
44 | read -p "Should we generate a random password for you? [y/N]: " -r REPLY
45 | if ! [[ $REPLY =~ ^[Yy]$ ]]; then
46 | echo "Please change the default password in the .env file and try again"
47 | exit 1
48 | fi
49 | # Generate a random URL-safe password
50 | DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_')
51 | sed -i -e "s#:password@#:$DB_PASSWORD@#" .env
52 | fi
53 |
54 | docker run -d \
55 | --name $DB_CONTAINER_NAME \
56 | -e POSTGRES_USER="postgres" \
57 | -e POSTGRES_PASSWORD="$DB_PASSWORD" \
58 | -e POSTGRES_DB=diagram \
59 | -p "$DB_PORT":5432 \
60 | docker.io/postgres && echo "Database container '$DB_CONTAINER_NAME' was successfully created"
61 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | import { fontFamily } from "tailwindcss/defaultTheme";
3 |
4 | const config = {
5 | darkMode: ["class"],
6 | content: [
7 | "./pages/**/*.{ts,tsx}",
8 | "./components/**/*.{ts,tsx}",
9 | "./app/**/*.{ts,tsx}",
10 | "./src/**/*.{ts,tsx}",
11 | ],
12 | prefix: "",
13 | theme: {
14 | container: {
15 | center: true,
16 | padding: "2rem",
17 | screens: {
18 | "2xl": "1200px",
19 | },
20 | },
21 | extend: {
22 | fontFamily: {
23 | sans: ["var(--font-sans)", ...fontFamily.sans],
24 | heading: ["var(--font-heading)", ...fontFamily.sans],
25 | },
26 | borderRadius: {
27 | lg: "var(--radius)",
28 | md: "calc(var(--radius) - 2px)",
29 | sm: "calc(var(--radius) - 4px)",
30 | },
31 | colors: {
32 | background: "hsl(var(--background))",
33 | foreground: "hsl(var(--foreground))",
34 | card: {
35 | DEFAULT: "hsl(var(--card))",
36 | foreground: "hsl(var(--card-foreground))",
37 | },
38 | popover: {
39 | DEFAULT: "hsl(var(--popover))",
40 | foreground: "hsl(var(--popover-foreground))",
41 | },
42 | primary: {
43 | DEFAULT: "hsl(var(--primary))",
44 | foreground: "hsl(var(--primary-foreground))",
45 | },
46 | secondary: {
47 | DEFAULT: "hsl(var(--secondary))",
48 | foreground: "hsl(var(--secondary-foreground))",
49 | },
50 | muted: {
51 | DEFAULT: "hsl(var(--muted))",
52 | foreground: "hsl(var(--muted-foreground))",
53 | },
54 | accent: {
55 | DEFAULT: "hsl(var(--accent))",
56 | foreground: "hsl(var(--accent-foreground))",
57 | },
58 | destructive: {
59 | DEFAULT: "hsl(var(--destructive))",
60 | foreground: "hsl(var(--destructive-foreground))",
61 | },
62 | border: "hsl(var(--border))",
63 | input: "hsl(var(--input))",
64 | ring: "hsl(var(--ring))",
65 | chart: {
66 | '1': 'hsl(var(--chart-1))',
67 | '2': 'hsl(var(--chart-2))',
68 | '3': 'hsl(var(--chart-3))',
69 | '4': 'hsl(var(--chart-4))',
70 | '5': 'hsl(var(--chart-5))'
71 | },
72 | sidebar: {
73 | DEFAULT: "hsl(var(--sidebar-background))",
74 | foreground: "hsl(var(--sidebar-foreground))",
75 | primary: "hsl(var(--sidebar-primary))",
76 | "primary-foreground": "hsl(var(--sidebar-primary-foreground))",
77 | accent: "hsl(var(--sidebar-accent))",
78 | "accent-foreground": "hsl(var(--sidebar-accent-foreground))",
79 | border: "hsl(var(--sidebar-border))",
80 | ring: "hsl(var(--sidebar-ring))",
81 | },
82 | },
83 | keyframes: {
84 | "accordion-down": {
85 | from: { height: "0" },
86 | to: { height: "var(--radix-accordion-content-height)" },
87 | },
88 | "accordion-up": {
89 | from: { height: "var(--radix-accordion-content-height)" },
90 | to: { height: "0" },
91 | },
92 | },
93 | animation: {
94 | "accordion-down": "accordion-down 0.2s ease-out",
95 | "accordion-up": "accordion-up 0.2s ease-out",
96 | },
97 | },
98 | },
99 | plugins: [require("tailwindcss-animate")],
100 | } satisfies Config;
101 |
102 | export default config;
103 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Base Options: */
4 | "esModuleInterop": true,
5 | "skipLibCheck": true,
6 | "target": "es2022",
7 | "allowJs": true,
8 | "resolveJsonModule": true,
9 | "moduleDetection": "force",
10 | "isolatedModules": true,
11 |
12 | /* Strictness */
13 | "strict": true,
14 | "noUncheckedIndexedAccess": true,
15 | "checkJs": true,
16 |
17 | /* Bundled projects */
18 | "lib": ["dom", "dom.iterable", "ES2022"],
19 | "noEmit": true,
20 | "module": "ESNext",
21 | "moduleResolution": "Bundler",
22 | "jsx": "preserve",
23 | "plugins": [{ "name": "next" }],
24 | "incremental": true,
25 |
26 | /* Path Aliases */
27 | "baseUrl": ".",
28 | "paths": {
29 | "@/*": ["./src/*"]
30 | }
31 | },
32 | "include": [
33 | ".eslintrc.cjs",
34 | "next-env.d.ts",
35 | "**/*.ts",
36 | "**/*.tsx",
37 | "**/*.cjs",
38 | "**/*.js",
39 | ".next/types/**/*.ts"
40 | ],
41 | "exclude": ["node_modules"]
42 | }
43 |
--------------------------------------------------------------------------------
|