diff --git a/frontend/app/api/complete-diagram/route.ts b/frontend/app/api/complete-diagram/route.ts
index 71c215b..082ab9a 100644
--- a/frontend/app/api/complete-diagram/route.ts
+++ b/frontend/app/api/complete-diagram/route.ts
@@ -7,15 +7,20 @@ export const maxDuration = 60;
export async function POST(request: NextRequest) {
try {
- // Server-side only API key (no NEXT_PUBLIC prefix)
- const apiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
+ // Prefer user-provided key from header, fall back to server env var
+ const userKey = request.headers.get("x-gemini-api-key");
+ const apiKey = userKey?.trim() || process.env.GOOGLE_GENERATIVE_AI_API_KEY;
+
if (!apiKey) {
return NextResponse.json(
- { error: "Missing GOOGLE_GENERATIVE_AI_API_KEY" },
- { status: 500 }
+ { error: "NO_API_KEY", message: "No Gemini API key configured. Please add your API key in the board settings." },
+ { status: 401 }
);
}
+ // Inject the key into the environment for the @ai-sdk/google provider
+ process.env.GOOGLE_GENERATIVE_AI_API_KEY = apiKey;
+
const { prompt, image_data } = (await request.json()) as {
prompt: string;
image_data?: string;
diff --git a/frontend/app/api/validate-key/route.ts b/frontend/app/api/validate-key/route.ts
new file mode 100644
index 0000000..0a25ca3
--- /dev/null
+++ b/frontend/app/api/validate-key/route.ts
@@ -0,0 +1,73 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export const runtime = "nodejs";
+
+export async function POST(request: NextRequest) {
+ try {
+ const { api_key } = (await request.json()) as { api_key: string };
+
+ if (!api_key?.trim()) {
+ return NextResponse.json(
+ { valid: false, reason: "API key is required." },
+ { status: 400 }
+ );
+ }
+
+ if (!api_key.startsWith("AIza")) {
+ return NextResponse.json({
+ valid: false,
+ reason: "That doesn't look like a valid Gemini API key. Keys start with 'AIza'.",
+ });
+ }
+
+ // Make a real but minimal call to Gemini to validate the key
+ const response = await fetch(
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${api_key}`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ contents: [{ parts: [{ text: "Hi" }] }],
+ }),
+ }
+ );
+
+ if (response.ok) {
+ return NextResponse.json({ valid: true });
+ }
+
+ const data = await response.json().catch(() => ({}));
+ const errorCode = data?.error?.status ?? "";
+ const errorMessage = data?.error?.message ?? "";
+
+ if (response.status === 400 && errorMessage.includes("API_KEY_INVALID")) {
+ return NextResponse.json({
+ valid: false,
+ reason: "Invalid API key. Please check and try again.",
+ });
+ }
+
+ if (response.status === 403 || errorCode === "PERMISSION_DENIED") {
+ return NextResponse.json({
+ valid: false,
+ reason: "API key is valid but doesn't have permission to use Gemini. Enable the Generative Language API in your Google Cloud project.",
+ });
+ }
+
+ if (response.status === 429 || errorCode === "RESOURCE_EXHAUSTED") {
+ // Key works, just rate-limited — treat as valid
+ return NextResponse.json({ valid: true });
+ }
+
+ return NextResponse.json({
+ valid: false,
+ reason: `Gemini returned an error: ${errorMessage || response.statusText}`,
+ });
+ } catch (error) {
+ console.error("Key validation error:", error);
+ return NextResponse.json(
+ { valid: false, reason: "Could not reach Gemini API. Check your internet connection." },
+ { status: 500 }
+ );
+ }
+}
diff --git a/frontend/app/board/page.tsx b/frontend/app/board/page.tsx
new file mode 100644
index 0000000..ae7ebfa
--- /dev/null
+++ b/frontend/app/board/page.tsx
@@ -0,0 +1,20 @@
+import { auth } from "@clerk/nextjs/server";
+import { redirect } from "next/navigation";
+import { BoardClient } from "@/components/board-client";
+
+/**
+ * BoardPage is a server component that handles authentication checks
+ * before rendering the whiteboard client application.
+ */
+export default async function BoardPage() {
+ // Use the latest Clerk auth() API with async/await
+ const { userId } = await auth();
+
+ // If no user is authenticated, redirect to the landing page
+ // Note: middleware.ts also handles this, but this is a secondary safety check
+ if (!userId) {
+ redirect("/");
+ }
+
+ return ;
+}
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index bf36c66..d58ae9f 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -1,19 +1,26 @@
@import "tailwindcss";
+
+
:root {
- /* Minimal Monochrome Palette */
- --background: #ffffff;
- --foreground: #000000;
+ --background: #fafaf7;
+ --foreground: #1a1a1a;
+
+ /* Chalk AI Design Tokens */
+ --chalk-ink: #1a1a1a;
+ --chalk-ink-soft: #3d3d3d;
+ --chalk-gray: #888;
+ --chalk-bg: #fafaf7;
+ --chalk-white: #ffffff;
+ --chalk-purple: #7b5ea7;
+ --chalk-purple-light: #ede8f5;
+ --chalk-blue-soft: #e8eef8;
+ --chalk-border: #d0cfc7;
+ --chalk-border-soft: #e8e7e0;
- /* Modern Accents */
- --muted: #f4f4f5;
- --muted-foreground: #737373;
- --border: #eaeaea;
- --input: #f4f4f5;
- --primary: #000000;
- --primary-foreground: #ffffff;
+ --font-sketch: var(--font-sketch);
+ --font-body: var(--font-body);
- --ring: #000000;
--radius: 0.75rem;
}
@@ -28,8 +35,27 @@
--color-primary-foreground: var(--primary-foreground);
--color-ring: var(--ring);
--radius-lg: var(--radius);
- --font-sans: var(--font-geist-sans), system-ui, sans-serif;
+ --color-chalk-ink: var(--chalk-ink);
+ --color-chalk-ink-soft: var(--chalk-ink-soft);
+ --color-chalk-gray: var(--chalk-gray);
+ --color-chalk-bg: var(--chalk-bg);
+ --color-chalk-white: var(--chalk-white);
+ --color-chalk-purple: var(--chalk-purple);
+ --color-chalk-purple-light: var(--chalk-purple-light);
+ --color-chalk-blue-soft: var(--chalk-blue-soft);
+ --color-chalk-border: var(--chalk-border);
+ --color-chalk-border-soft: var(--chalk-border-soft);
+
+ --font-sans: var(--font-body), system-ui, sans-serif;
--font-mono: var(--font-geist-mono), monospace;
+ --font-sketch: var(--font-sketch);
+
+ --animate-marquee: marquee 28s linear infinite;
+
+ @keyframes marquee {
+ from { transform: translateX(0); }
+ to { transform: translateX(-50%); }
+ }
}
body {
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index bf7d025..823972b 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -1,7 +1,37 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
+import Link from "next/link";
+import { ClerkProvider, SignInButton, SignUpButton, UserButton } from "@clerk/nextjs";
+import { Show } from "@/components/clerk-helpers";
+import { UIProvider } from "@/components/ui-provider";
+import { ApiKeyProvider } from "@/components/api-key-provider";
+import { Header } from "@/components/header";
+import { LayoutWrapper } from "@/components/layout-wrapper";
+import { Caveat, Nunito, Kalam, Inter } from "next/font/google";
import "./globals.css";
+const kalam = Kalam({
+ subsets: ["latin"],
+ weight: ["300", "400", "700"],
+ style: ["normal"],
+ variable: "--font-sketch",
+});
+
+const inter = Inter({
+ subsets: ["latin"],
+ weight: ["300", "400", "500", "600", "700"],
+ style: ["normal", "italic"],
+ variable: "--font-body",
+});
+
+// const nunito = Nunito({
+// subsets: ["latin"],
+// weight: ["300", "400", "500", "600", "700"],
+// style: ["normal", "italic"],
+// variable: "--font-body",
+// });
+
+
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
@@ -61,9 +91,42 @@ export default function RootLayout({
return (
- {children}
+
+
+
+
+
+
+
+ Log in
+
+
+
+
+ Get Started
+
+
+
+
+
+
+ Board
+
+
+
+
+
+
+ {children}
+
+
+
+
);
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index abe0295..e27c945 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -1,468 +1,23 @@
"use client";
-import { useState, useCallback, useRef, useEffect } from "react";
-import { Whiteboard } from "@/components/whiteboard";
-import { IntentInput } from "@/components/intent-input";
-import { Button } from "@/components/ui/button";
-import { Loader2, Plus, RotateCcw, Sparkles, Check, X, Mic, MicOff } from "lucide-react";
-import { motion, AnimatePresence } from "framer-motion";
-
-export default function Home() {
- const [intent, setIntent] = useState("");
- const [generatedImage, setGeneratedImage] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
- const [resetSignal, setResetSignal] = useState(0);
- const exportFnRef = useRef<(() => Promise) | null>(null);
- const [isMounted, setIsMounted] = useState(false);
-
- useEffect(() => {
- setIsMounted(true);
- }, []);
-
- // Voice input state
- const [isListening, setIsListening] = useState(false);
- const [agentTranscript, setAgentTranscript] = useState("");
- const agentTranscriptRef = useRef("");
- const recognitionRef = useRef(null);
- const idleCheckIntervalRef = useRef(null);
- const isListeningRef = useRef(false);
- const lastActivityTimeRef = useRef(Date.now());
-
- // Sync refs with state
- useEffect(() => {
- agentTranscriptRef.current = agentTranscript;
- }, [agentTranscript]);
-
- useEffect(() => {
- isListeningRef.current = isListening;
- }, [isListening]);
-
- const handleExportReady = useCallback(
- (exportFn: () => Promise) => {
- exportFnRef.current = exportFn;
- },
- []
- );
-
- const handleRequestSuggestion = useCallback(async (overridePrompt?: string) => {
- if (!exportFnRef.current) {
- setError("Canvas not ready. Please wait a moment and try again.");
- return;
- }
-
- // Determine the prompt to use: override or current intent
- // If it's an event object (from button click), ignore it
- const promptText = typeof overridePrompt === "string" ? overridePrompt : intent;
-
- if (!promptText.trim()) {
- setError("Please provide an intent description");
- return;
- }
-
- setIsLoading(true);
- setError(null);
-
- try {
- // Export canvas to PNG
- const imageDataUrl = await exportFnRef.current();
-
- let base64Image: string | undefined;
-
- // Convert data URL to base64 string
- if (imageDataUrl) {
- if (imageDataUrl.startsWith("data:image/png;base64,")) {
- // Extract base64 from data URL
- base64Image = imageDataUrl.replace("data:image/png;base64,", "");
- } else {
- // Fetch and convert
- const response = await fetch(imageDataUrl);
- const blob = await response.blob();
- const reader = new FileReader();
- await new Promise((resolve, reject) => {
- reader.onloadend = resolve;
- reader.onerror = reject;
- reader.readAsDataURL(blob);
- });
- const dataUrl = reader.result as string;
- base64Image = dataUrl.replace("data:image/png;base64,", "");
- }
- }
-
- // Call API with JSON
- const apiResponse = await fetch("/api/complete-diagram", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- prompt: promptText,
- image_data: base64Image,
- }),
- });
-
- if (!apiResponse.ok) {
- const errorData = await apiResponse.json();
- throw new Error(errorData.error || "Failed to get AI suggestion");
- }
-
- const data = await apiResponse.json();
- setGeneratedImage(data.image_data);
- } catch (err) {
- console.error("Error:", err);
- setError(
- err instanceof Error ? err.message : "Failed to get AI suggestion"
- );
- } finally {
- setIsLoading(false);
- }
- }, [intent]);
-
- const handleAcceptSuggestion = useCallback(() => {
- setGeneratedImage(null);
- setIntent("");
- setResetSignal((prev) => prev + 1);
- // The whiteboard component handles the actual insertion
- }, []);
-
- const handleClearCanvas = useCallback(() => {
- setGeneratedImage(null);
- setError(null);
- setIntent("");
- setResetSignal((prev) => prev + 1);
- }, []);
-
- const handleRejectSuggestion = useCallback(() => {
- setGeneratedImage(null);
- // We keep the intent so the user can modify it significantly if needed
- }, []);
-
- // Handle keyboard shortcuts (Esc to reject)
- useEffect(() => {
- if (!generatedImage) return;
-
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === "Escape") {
- e.preventDefault();
- handleRejectSuggestion();
- }
- };
-
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [generatedImage, handleRejectSuggestion]);
-
- // Initialize Speech Recognition
- useEffect(() => {
- if (typeof window === "undefined") return;
-
- const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition;
- if (!SpeechRecognitionAPI) {
- console.warn("Speech Recognition not supported in this browser");
- return;
- }
-
- const recognition = new SpeechRecognitionAPI();
- recognition.continuous = true;
- recognition.interimResults = true;
- recognition.lang = "en-US";
-
- recognition.onresult = (event: SpeechRecognitionEvent) => {
- let finalTranscript = "";
- for (let i = event.resultIndex; i < event.results.length; i++) {
- const transcript = event.results[i][0].transcript;
- if (event.results[i].isFinal) {
- finalTranscript += transcript + " ";
- }
- }
- if (finalTranscript) {
- console.log("Voice input:", finalTranscript);
- setAgentTranscript((prev) => prev + finalTranscript);
- lastActivityTimeRef.current = Date.now(); // Reset idle timer on speech
- }
- };
-
- recognition.onend = () => {
- // Auto-restart if still listening
- if (isListeningRef.current) {
- try {
- recognition.start();
- } catch (e) {
- console.error("Recognition restart failed:", e);
- }
- }
- };
-
- recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
- console.error("Speech recognition error:", event.error);
- if (event.error !== "no-speech") {
- setIsListening(false);
- isListeningRef.current = false;
- }
- };
-
- recognitionRef.current = recognition;
-
- return () => {
- recognition.stop();
- };
- }, []);
-
- // Toggle listening
- const toggleListening = useCallback(() => {
- const recognition = recognitionRef.current;
- if (!recognition) {
- alert("Speech recognition is not supported in your browser.");
- return;
- }
-
- if (isListening) {
- // Stop listening
- recognition.stop();
- if (idleCheckIntervalRef.current) {
- clearInterval(idleCheckIntervalRef.current);
- idleCheckIntervalRef.current = null;
- }
- setIsListening(false);
- isListeningRef.current = false;
-
- // If we have a transcript, use it for generation
- if (agentTranscriptRef.current.trim()) {
- handleRequestSuggestion(agentTranscriptRef.current.trim());
- }
- } else {
- // Start listening
- setAgentTranscript("");
- agentTranscriptRef.current = "";
- lastActivityTimeRef.current = Date.now();
- try {
- recognition.start();
- setIsListening(true);
- isListeningRef.current = true;
-
- // Idle detection: check every 500ms if idle for 4.5 seconds
- idleCheckIntervalRef.current = setInterval(() => {
- const idleTime = Date.now() - lastActivityTimeRef.current;
- if (idleTime > 4500 && agentTranscriptRef.current.trim() && isListeningRef.current) {
- console.log("Idle detected, auto-generating...");
- handleRequestSuggestion(agentTranscriptRef.current.trim());
- // Stop listening after auto-generate
- recognition.stop();
- setIsListening(false);
- isListeningRef.current = false;
- if (idleCheckIntervalRef.current) {
- clearInterval(idleCheckIntervalRef.current);
- idleCheckIntervalRef.current = null;
- }
- }
- }, 500);
- } catch (e) {
- console.error("Failed to start recognition:", e);
- }
- }
- }, [isListening, handleRequestSuggestion]);
-
- // Handle drawing activity - reset idle timer
- const handleDrawingActivity = useCallback(() => {
- lastActivityTimeRef.current = Date.now();
- }, []);
-
- if (!isMounted) return null;
-
+import { Hero } from "@/components/landing/hero";
+import { Features } from "@/components/landing/features";
+import { HowItWorks } from "@/components/landing/how-it-works";
+import { Testimonials } from "@/components/landing/testimonials";
+import { Founders } from "@/components/landing/founders";
+import { CTA } from "@/components/landing/cta";
+import { Footer } from "@/components/landing/footer";
+
+export default function ChalkAILanding() {
return (
-
- {/* 1. Fullscreen Whiteboard (Background Layer) */}
-
-
-
-
- {/* 2. Floating Header (Top Left) */}
- {/*
-
-
- ChalkAI
-
- */}
-
- {/* 3. Floating Control Island (Bottom Center) */}
-
-
- {/* Input Row */}
-
-
handleRequestSuggestion("Enhance this sketch into a professional diagram.")}
- disabled={isLoading}
- variant="ghost"
- size="icon"
- className="h-9 w-9 shrink-0 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/50"
- title="Improvise (Quick Enhance)"
- >
-
-
-
-
-
-
handleRequestSuggestion()}
- disabled={isLoading || (!intent.trim() && !agentTranscript.trim())}
- size="icon"
- className="h-10 w-10 shrink-0 rounded-xl bg-primary text-primary-foreground hover:scale-105 transition-transform shadow-sm"
- >
- {isLoading ? (
-
- ) : (
-
- )}
-
-
- {/* Mic Button */}
-
- {isListening ? : }
-
-
-
- {/* Live Transcript (when listening) */}
-
- {isListening && (
-
-
-
-
- {agentTranscript || "Start speaking..."}
-
-
-
- )}
-
-
- {/* Error Message (Collapsible) */}
-
- {error && (
-
-
- {error}
-
-
- )}
-
-
-
- {/* Reset Button (Floating nearby) */}
-
-
-
-
-
- {/* 4. Preview Card (Bottom Right) */}
- {/* 4. Large Preview Popover (Bottom Right) */}
-
- {generatedImage && (
-
- {/* Header */}
-
-
-
- AI
-
- Suggestion
-
-
-
-
-
-
- {/* Image Container */}
-
-
-
-
- {/* Footer Actions */}
-
-
-
- Reject (Esc)
-
-
-
- Accept (Tab)
-
-
-
- )}
-
+
+
+
+
+
+
+
+
);
-}
+}
\ No newline at end of file
diff --git a/frontend/bun.lock b/frontend/bun.lock
index bb9cdc8..d9a9bee 100644
--- a/frontend/bun.lock
+++ b/frontend/bun.lock
@@ -19,6 +19,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.23.26",
+ "lightningcss-win32-x64-msvc": "^1.30.2",
"lucide-react": "^0.562.0",
"next": "16.1.1",
"react": "19.2.3",
diff --git a/frontend/components/api-key-modal.tsx b/frontend/components/api-key-modal.tsx
new file mode 100644
index 0000000..79fd57e
--- /dev/null
+++ b/frontend/components/api-key-modal.tsx
@@ -0,0 +1,236 @@
+"use client";
+
+import React, { useState, useRef, useEffect } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import { Eye, EyeOff, Key, ExternalLink, Check, AlertCircle, Loader2, X, Sparkles } from "lucide-react";
+import { useApiKey } from "@/components/api-key-provider";
+
+interface ApiKeyModalProps {
+ isOpen: boolean;
+ onClose?: () => void;
+ /** If true, the modal cannot be dismissed without saving a key */
+ required?: boolean;
+}
+
+export function ApiKeyModal({ isOpen, onClose, required = false }: ApiKeyModalProps) {
+ const { setApiKey, apiKey } = useApiKey();
+ const [inputValue, setInputValue] = useState(apiKey ?? "");
+ const [showKey, setShowKey] = useState(false);
+ const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
+ const [errorMessage, setErrorMessage] = useState("");
+ const inputRef = useRef
(null);
+
+ useEffect(() => {
+ if (isOpen) {
+ setInputValue(apiKey ?? "");
+ setStatus("idle");
+ setErrorMessage("");
+ setTimeout(() => inputRef.current?.focus(), 100);
+ }
+ }, [isOpen, apiKey]);
+
+ const handleSave = async () => {
+ const trimmed = inputValue.trim();
+ if (!trimmed) {
+ setStatus("error");
+ setErrorMessage("Please enter your Gemini API key.");
+ return;
+ }
+
+ setStatus("loading");
+ setErrorMessage("");
+
+ try {
+ const res = await fetch("/api/validate-key", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ api_key: trimmed }),
+ });
+ const data = await res.json();
+
+ if (data.valid) {
+ setApiKey(trimmed);
+ setStatus("success");
+ setTimeout(() => {
+ onClose?.();
+ }, 1200);
+ } else {
+ setStatus("error");
+ setErrorMessage(data.reason ?? "Invalid API key. Please try again.");
+ }
+ } catch {
+ setStatus("error");
+ setErrorMessage("Could not connect to validation service. Check your internet connection.");
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") handleSave();
+ if (e.key === "Escape" && !required) onClose?.();
+ };
+
+ const isExistingKey = !!apiKey;
+
+ return (
+
+ {isOpen && (
+
+ {/* Backdrop */}
+
+
+ {/* Modal */}
+
+ {/* Close button */}
+ {!required && (
+
+
+
+ )}
+
+ {/* Purple accent header stripe */}
+
+
+
+ {/* Icon + Title */}
+
+
+
+
+
+
+ {isExistingKey ? "Update API Key" : "Connect Your Gemini Key"}
+
+
+ {isExistingKey
+ ? "Replace your existing Gemini API key."
+ : "Chalk AI uses Google Gemini to enhance your drawings. Your key is stored locally and never sent to our servers."}
+
+
+
+
+ {/* Input */}
+
+
+ Gemini API Key
+
+
+
+
+
+
{
+ setInputValue(e.target.value);
+ if (status !== "idle") { setStatus("idle"); setErrorMessage(""); }
+ }}
+ onKeyDown={handleKeyDown}
+ placeholder="AIza..."
+ className="w-full pl-10 pr-12 py-3.5 bg-white border-[2px] border-chalk-border rounded-xl font-mono text-sm text-chalk-ink placeholder:text-chalk-gray/50 outline-none focus:border-chalk-purple focus:shadow-[0_0_0_3px_var(--chalk-purple-light)] transition-all duration-200 shadow-[2px_2px_0px_0px_var(--chalk-border)]"
+ />
+
setShowKey(!showKey)}
+ className="absolute right-3.5 top-1/2 -translate-y-1/2 text-chalk-gray hover:text-chalk-ink transition-colors"
+ >
+ {showKey ? : }
+
+
+
+
+ {/* Status / Error message */}
+
+ {status === "error" && errorMessage && (
+
+
+
+ )}
+ {status === "success" && (
+
+
+
+ API key verified and saved! You're all set.
+
+
+ )}
+
+
+ {/* Save Button */}
+
+ {status === "loading" ? (
+ <>
+
+ Verifying key…
+ >
+ ) : status === "success" ? (
+ <>
+
+ Verified!
+ >
+ ) : (
+ <>
+
+ {isExistingKey ? "Update & Verify Key" : "Verify & Save Key"}
+ >
+ )}
+
+
+ {/* Get key link */}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/components/api-key-provider.tsx b/frontend/components/api-key-provider.tsx
new file mode 100644
index 0000000..51bf95b
--- /dev/null
+++ b/frontend/components/api-key-provider.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import React, { createContext, useContext, useState, useEffect, useCallback } from "react";
+
+const STORAGE_KEY = "chalk_ai_gemini_key";
+
+interface ApiKeyContextValue {
+ apiKey: string | null;
+ hasKey: boolean;
+ setApiKey: (key: string) => void;
+ clearApiKey: () => void;
+ isLoaded: boolean;
+}
+
+const ApiKeyContext = createContext({
+ apiKey: null,
+ hasKey: false,
+ setApiKey: () => {},
+ clearApiKey: () => {},
+ isLoaded: false,
+});
+
+export function ApiKeyProvider({ children }: { children: React.ReactNode }) {
+ const [apiKey, setApiKeyState] = useState(null);
+ const [isLoaded, setIsLoaded] = useState(false);
+
+ useEffect(() => {
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY);
+ if (stored) setApiKeyState(stored);
+ } catch {}
+ setIsLoaded(true);
+ }, []);
+
+ const setApiKey = useCallback((key: string) => {
+ try {
+ localStorage.setItem(STORAGE_KEY, key);
+ } catch {}
+ setApiKeyState(key);
+ }, []);
+
+ const clearApiKey = useCallback(() => {
+ try {
+ localStorage.removeItem(STORAGE_KEY);
+ } catch {}
+ setApiKeyState(null);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useApiKey() {
+ return useContext(ApiKeyContext);
+}
diff --git a/frontend/components/board-client.tsx b/frontend/components/board-client.tsx
new file mode 100644
index 0000000..13713e2
--- /dev/null
+++ b/frontend/components/board-client.tsx
@@ -0,0 +1,492 @@
+"use client";
+
+import { useState, useCallback, useRef, useEffect } from "react";
+import { Whiteboard } from "@/components/whiteboard";
+import { IntentInput } from "@/components/intent-input";
+import { Button } from "@/components/ui/button";
+import { Loader2, Plus, RotateCcw, Sparkles, Check, X, Mic, MicOff, Maximize2, Minimize2, Key } from "lucide-react";
+import { motion, AnimatePresence } from "framer-motion";
+import { useUI } from "@/components/ui-provider";
+import { cn } from "@/lib/utils";
+import { useApiKey } from "@/components/api-key-provider";
+import { ApiKeyModal } from "@/components/api-key-modal";
+
+
+export function BoardClient() {
+ const { isFullscreen, toggleFullscreen } = useUI();
+ const { apiKey, hasKey, isLoaded } = useApiKey();
+ const [showApiKeyModal, setShowApiKeyModal] = useState(false);
+ const [intent, setIntent] = useState("");
+ const [generatedImage, setGeneratedImage] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [resetSignal, setResetSignal] = useState(0);
+ const exportFnRef = useRef<(() => Promise) | null>(null);
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ // Voice input state
+ const [isListening, setIsListening] = useState(false);
+ const [agentTranscript, setAgentTranscript] = useState("");
+ const agentTranscriptRef = useRef("");
+ const recognitionRef = useRef(null);
+ const idleCheckIntervalRef = useRef(null);
+ const isListeningRef = useRef(false);
+ const lastActivityTimeRef = useRef(Date.now());
+
+ // Sync refs with state
+ useEffect(() => {
+ agentTranscriptRef.current = agentTranscript;
+ }, [agentTranscript]);
+
+ useEffect(() => {
+ isListeningRef.current = isListening;
+ }, [isListening]);
+
+ const handleExportReady = useCallback(
+ (exportFn: () => Promise) => {
+ exportFnRef.current = exportFn;
+ },
+ []
+ );
+
+ const handleRequestSuggestion = useCallback(async (overridePrompt?: string) => {
+ if (!exportFnRef.current) {
+ setError("Canvas not ready. Please wait a moment and try again.");
+ return;
+ }
+
+ const promptText = typeof overridePrompt === "string" ? overridePrompt : intent;
+
+ if (!promptText.trim()) {
+ setError("Please provide an intent description");
+ return;
+ }
+
+ setIsLoading(true);
+ setError(null);
+
+ try {
+ const imageDataUrl = await exportFnRef.current();
+ let base64Image: string | undefined;
+
+ if (imageDataUrl) {
+ if (imageDataUrl.startsWith("data:image/png;base64,")) {
+ base64Image = imageDataUrl.replace("data:image/png;base64,", "");
+ } else {
+ const response = await fetch(imageDataUrl);
+ const blob = await response.blob();
+ const reader = new FileReader();
+ await new Promise((resolve, reject) => {
+ reader.onloadend = resolve;
+ reader.onerror = reject;
+ reader.readAsDataURL(blob);
+ });
+ const dataUrl = reader.result as string;
+ base64Image = dataUrl.replace("data:image/png;base64,", "");
+ }
+ }
+
+ const apiResponse = await fetch("/api/complete-diagram", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ ...(apiKey ? { "x-gemini-api-key": apiKey } : {}),
+ },
+ body: JSON.stringify({
+ prompt: promptText,
+ image_data: base64Image,
+ }),
+ });
+
+ if (!apiResponse.ok) {
+ const errorData = await apiResponse.json();
+ if (errorData.error === "NO_API_KEY") {
+ setShowApiKeyModal(true);
+ throw new Error("Please set up your Gemini API key to use AI features.");
+ }
+ throw new Error(errorData.error || "Failed to get AI suggestion");
+ }
+
+ const data = await apiResponse.json();
+ setGeneratedImage(data.image_data);
+ } catch (err) {
+ console.error("Error:", err);
+ setError(
+ err instanceof Error ? err.message : "Failed to get AI suggestion"
+ );
+ } finally {
+ setIsLoading(false);
+ }
+ }, [intent]);
+
+ const handleAcceptSuggestion = useCallback(() => {
+ setGeneratedImage(null);
+ setIntent("");
+ setResetSignal((prev) => prev + 1);
+ }, []);
+
+ const handleClearCanvas = useCallback(() => {
+ setGeneratedImage(null);
+ setError(null);
+ setIntent("");
+ setResetSignal((prev) => prev + 1);
+ }, []);
+
+ const handleRejectSuggestion = useCallback(() => {
+ setGeneratedImage(null);
+ }, []);
+
+ useEffect(() => {
+ if (!generatedImage) return;
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape") {
+ e.preventDefault();
+ handleRejectSuggestion();
+ }
+ };
+ window.addEventListener("keydown", handleKeyDown);
+ return () => window.removeEventListener("keydown", handleKeyDown);
+ }, [generatedImage, handleRejectSuggestion]);
+
+ useEffect(() => {
+ if (typeof window === "undefined") return;
+ const SpeechRecognitionAPI = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
+ if (!SpeechRecognitionAPI) return;
+
+ const recognition = new SpeechRecognitionAPI();
+ recognition.continuous = true;
+ recognition.interimResults = true;
+ recognition.lang = "en-US";
+
+ recognition.onresult = (event: any) => {
+ let finalTranscript = "";
+ for (let i = event.resultIndex; i < event.results.length; i++) {
+ const transcript = event.results[i][0].transcript;
+ if (event.results[i].isFinal) {
+ finalTranscript += transcript + " ";
+ }
+ }
+ if (finalTranscript) {
+ setAgentTranscript((prev) => prev + finalTranscript);
+ lastActivityTimeRef.current = Date.now();
+ }
+ };
+
+ recognition.onend = () => {
+ if (isListeningRef.current) {
+ try { recognition.start(); } catch (e) {}
+ }
+ };
+
+ recognitionRef.current = recognition;
+ return () => recognition.stop();
+ }, []);
+
+ const toggleListening = useCallback(() => {
+ const recognition = recognitionRef.current;
+ if (!recognition) return;
+
+ if (isListening) {
+ recognition.stop();
+ if (idleCheckIntervalRef.current) {
+ clearInterval(idleCheckIntervalRef.current);
+ idleCheckIntervalRef.current = null;
+ }
+ setIsListening(false);
+ isListeningRef.current = false;
+ if (agentTranscriptRef.current.trim()) {
+ handleRequestSuggestion(agentTranscriptRef.current.trim());
+ }
+ } else {
+ setAgentTranscript("");
+ agentTranscriptRef.current = "";
+ lastActivityTimeRef.current = Date.now();
+ try {
+ recognition.start();
+ setIsListening(true);
+ isListeningRef.current = true;
+ idleCheckIntervalRef.current = setInterval(() => {
+ const idleTime = Date.now() - lastActivityTimeRef.current;
+ if (idleTime > 4500 && agentTranscriptRef.current.trim() && isListeningRef.current) {
+ handleRequestSuggestion(agentTranscriptRef.current.trim());
+ recognition.stop();
+ setIsListening(false);
+ isListeningRef.current = false;
+ if (idleCheckIntervalRef.current) {
+ clearInterval(idleCheckIntervalRef.current);
+ idleCheckIntervalRef.current = null;
+ }
+ }
+ }, 500);
+ } catch (e) {}
+ }
+ }, [isListening, handleRequestSuggestion]);
+
+ const handleDrawingActivity = useCallback(() => {
+ lastActivityTimeRef.current = Date.now();
+ }, []);
+
+ if (!isMounted) return null;
+
+ // Show API key setup if key not loaded yet or missing
+ if (isLoaded && !hasKey) {
+ return (
+
+ {/* Dot grid background */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Almost there!
+
+ Chalk AI needs your Gemini API key to power AI diagram generation. Your key is stored locally and never shared.
+
+
+
setShowApiKeyModal(true)}
+ className="inline-flex items-center gap-2.5 px-8 py-4 bg-chalk-ink text-white font-bold rounded-2xl border-2 border-chalk-ink
+ shadow-[5px_5px_0px_0px_var(--chalk-purple)] hover:shadow-[2px_2px_0px_0px_var(--chalk-purple)]
+ hover:-translate-y-0.5 active:translate-y-0 active:shadow-none
+ transition-all duration-150"
+ >
+
+ Connect Gemini API Key
+
+
+ Get a free key at Google AI Studio →
+
+
+
+
setShowApiKeyModal(false)}
+ required={!hasKey}
+ />
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
handleRequestSuggestion("Enhance this sketch into a professional diagram.")}
+ disabled={isLoading}
+ variant="ghost"
+ size="icon"
+ className="h-9 w-9 shrink-0 rounded-full text-muted-foreground hover:text-foreground hover:bg-muted/50"
+ title="Improvise (Quick Enhance)"
+ >
+
+
+
+
+
+
handleRequestSuggestion()}
+ disabled={isLoading || (!intent.trim() && !agentTranscript.trim())}
+ size="icon"
+ className="h-10 w-10 shrink-0 rounded-xl bg-primary text-primary-foreground hover:scale-105 transition-transform shadow-sm"
+ >
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isListening ? : }
+
+
+ {/* API Key Settings */}
+
setShowApiKeyModal(true)}
+ variant="ghost"
+ size="icon"
+ className="h-10 w-10 shrink-0 rounded-xl text-muted-foreground hover:text-foreground hover:bg-muted/50"
+ title="Gemini API Key Settings"
+ >
+
+
+
+ {/* Fullscreen Button */}
+
+ {isFullscreen ? : }
+
+
+
+
+ {isListening && (
+
+
+
+
+ {agentTranscript || "Start speaking..."}
+
+
+
+ )}
+
+
+
+ {error && (
+
+
+ {error}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {generatedImage && (
+
+
+
+
+ AI
+
+ Suggestion
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reject (Esc)
+
+
+
+ Accept (Tab)
+
+
+
+ )}
+
+
+ {/* API Key Modal */}
+
setShowApiKeyModal(false)}
+ required={false}
+ />
+
+ );
+}
diff --git a/frontend/components/clerk-helpers.tsx b/frontend/components/clerk-helpers.tsx
new file mode 100644
index 0000000..4b82c95
--- /dev/null
+++ b/frontend/components/clerk-helpers.tsx
@@ -0,0 +1,26 @@
+import { auth } from "@clerk/nextjs/server";
+import { ReactNode } from "react";
+
+interface ShowProps {
+ when: "signed-in" | "signed-out";
+ children: ReactNode;
+}
+
+/**
+ * A helper component to conditionally show content based on authentication state.
+ * Uses the latest Clerk auth() API.
+ */
+export async function Show({ when, children }: ShowProps) {
+ const { userId } = await auth();
+ const isSignedIn = !!userId;
+
+ if (when === "signed-in" && isSignedIn) {
+ return <>{children}>;
+ }
+
+ if (when === "signed-out" && !isSignedIn) {
+ return <>{children}>;
+ }
+
+ return null;
+}
diff --git a/frontend/components/header.tsx b/frontend/components/header.tsx
new file mode 100644
index 0000000..920f639
--- /dev/null
+++ b/frontend/components/header.tsx
@@ -0,0 +1,92 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useUI } from "@/components/ui-provider";
+import { cn } from "@/lib/utils";
+import { Maximize2, Minimize2, Pencil } from "lucide-react";
+import { Button } from "@/components/ui/button";
+
+interface HeaderProps {
+ children?: React.ReactNode;
+}
+
+export function Header({ children }: HeaderProps) {
+ const { isFullscreen, toggleFullscreen } = useUI();
+ const pathname = usePathname();
+ const isHomePage = pathname === "/";
+ const isBoardPage = pathname === "/board";
+
+ if (isBoardPage) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/frontend/components/landing/cta.tsx b/frontend/components/landing/cta.tsx
new file mode 100644
index 0000000..c1457ba
--- /dev/null
+++ b/frontend/components/landing/cta.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+import { FadeIn } from "./fade-in";
+import { LogoIcon, ArrowRightIcon } from "./icons";
+import { SignInButton, SignUpButton, useAuth } from "@clerk/nextjs";
+import { DecorativeIcon } from "./decorative-icon";
+
+export function CTA() {
+ const { isSignedIn } = useAuth();
+
+ return (
+
+ {/* Decorative Icons */}
+
+
+
+
+ {/* Decorative circles */}
+
+
+
+
+
+
+
+
+ Ready to transform the way you teach?
+
+
+ Chalk AI is completely open. Just plug in your Gemini API key and start creating.
+
+
+
+ {!isSignedIn ? (
+
+
+ Get Started
+
+
+
+ ) : (
+
+
+ Go to My Board
+
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/frontend/components/landing/decorative-icon.tsx b/frontend/components/landing/decorative-icon.tsx
new file mode 100644
index 0000000..61f92e2
--- /dev/null
+++ b/frontend/components/landing/decorative-icon.tsx
@@ -0,0 +1,48 @@
+"use client";
+
+import Image from "next/image";
+import { cn } from "@/lib/utils";
+import { FadeIn } from "./fade-in";
+
+interface DecorativeIconProps {
+ src: string;
+ alt: string;
+ className?: string;
+ rotate?: number;
+ size?: number;
+ delay?: number;
+ opacity?: number;
+}
+
+export function DecorativeIcon({
+ src,
+ alt,
+ className,
+ rotate = 0,
+ size = 60,
+ delay = 0,
+ opacity = 0.5,
+}: DecorativeIconProps) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/frontend/components/landing/doodles.tsx b/frontend/components/landing/doodles.tsx
new file mode 100644
index 0000000..11ef057
--- /dev/null
+++ b/frontend/components/landing/doodles.tsx
@@ -0,0 +1,27 @@
+export const Sparkle = () => (
+
+
+
+
+
+
+);
diff --git a/frontend/components/landing/fade-in.tsx b/frontend/components/landing/fade-in.tsx
new file mode 100644
index 0000000..0252cab
--- /dev/null
+++ b/frontend/components/landing/fade-in.tsx
@@ -0,0 +1,35 @@
+"use client";
+
+import React, { useState, useEffect, useRef } from "react";
+
+function useIntersection(ref: React.RefObject, threshold = 0.15) {
+ const [visible, setVisible] = useState(false);
+ useEffect(() => {
+ const obs = new IntersectionObserver(
+ ([e]) => { if (e.isIntersecting) { setVisible(true); obs.disconnect(); } },
+ { threshold }
+ );
+ if (ref.current) obs.observe(ref.current);
+ return () => obs.disconnect();
+ }, [ref, threshold]);
+ return visible;
+}
+
+export function FadeIn({ children, delay = 0, className = "" }: { children: React.ReactNode, delay?: number, className?: string }) {
+ const ref = useRef(null);
+ const visible = useIntersection(ref);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/components/landing/features.tsx b/frontend/components/landing/features.tsx
new file mode 100644
index 0000000..1e412aa
--- /dev/null
+++ b/frontend/components/landing/features.tsx
@@ -0,0 +1,183 @@
+"use client";
+
+import React from "react";
+import { FadeIn } from "./fade-in";
+import { Cloud, Lightbulb, PencilRuler, User } from "lucide-react";
+import { DecorativeIcon } from "./decorative-icon";
+
+const SketchMark = ({ flip = false }: { flip?: boolean }) => (
+
+
+
+
+
+);
+
+const features = [
+ {
+ icon: ,
+ title: "Smart Drawing",
+ desc: "AI helps you refine your sketches, flowcharts, and complex diagrams in real-time.",
+ },
+ {
+ icon: ,
+ title: "AI Suggestions",
+ desc: "Get intelligent contextual prompts to improve your math proof steps and explanations.",
+ },
+ {
+ icon: ,
+ title: "Interactive Lessons",
+ desc: "Engage students with clean real-time whiteboard spaces built for digital collaboration.",
+ },
+ {
+ icon: ,
+ title: "Save & Share",
+ desc: "Instantly capture your finished sketch boards and generate quick review links.",
+ },
+];
+
+/* ==========================================================================
+ SKETCHED CARD BACKGROUND COMPONENT
+ ========================================================================== */
+function SketchedCardFrame() {
+ return (
+
+ {/* Brutalist Shadow Layer */}
+
+
+
+
+ {/* Main Front Frame Stroke & Canvas Fill */}
+
+
+
+
+ );
+}
+
+export function Features() {
+ return (
+
+ {/* Decorative Icons */}
+
+
+
+ {/* HEADER SECTION WITH SKETCH UNDERLINE ATTRIBUTES */}
+
+
+
+
+
+ Everything you need on one{" "}
+
+ intelligent
+
+
+ {" "}
+ board
+
+
+
+
+
+ {/* FEATURE GRID SYSTEM MAPPED WITH THE CUSTOM ASSET FRAME */}
+
+ {features.map((f, i) => (
+
+
+
+ {/* Dynamic Hand-drawn Frame Injector Asset */}
+
+
+ {/* CARD CONTAINER CONTENT MARKUP LAYER */}
+
+
+
+ {/* ICON BUBBLE ASSEMBLY */}
+
+
+ {f.icon}
+
+
+
+ {/* HEADER CONTENT COPY ELEMENT */}
+
+
+ {f.title}
+
+
+
+ {/* BODY DESCRIPTIONS PRIMITIVE */}
+
+ {f.desc}
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/landing/footer.tsx b/frontend/components/landing/footer.tsx
new file mode 100644
index 0000000..9c12fdb
--- /dev/null
+++ b/frontend/components/landing/footer.tsx
@@ -0,0 +1,142 @@
+"use client";
+
+import React from "react";
+import { Github, Linkedin, Twitter } from "lucide-react";
+import { LogoIcon } from "./icons";
+
+export function Footer() {
+ const currentYear = new Date().getFullYear();
+
+ return (
+
+ );
+}
diff --git a/frontend/components/landing/founders.tsx b/frontend/components/landing/founders.tsx
new file mode 100644
index 0000000..593e87a
--- /dev/null
+++ b/frontend/components/landing/founders.tsx
@@ -0,0 +1,227 @@
+"use client";
+
+import React, { useState } from "react";
+import Image from "next/image";
+import { FadeIn } from "./fade-in";
+import { Linkedin, Twitter, Mail } from "lucide-react";
+
+const foundersData = [
+ {
+ name: "Jaydeep",
+ role: "Co-founder",
+ bio: "Computer Science student obsessed with turning grand engineering concepts into lightweight, high-performance web systems for classrooms worldwide.",
+ initials: "J",
+ imageSrc: "/jay.jpg",
+ socials: {
+ twitter: "https://x.com/jayydeeppp",
+ linkedin: "https://www.linkedin.com/in/jaydeepwagaskar/",
+ email: "mailto:jaydeepwaghaskar@gmail.com",
+ },
+ },
+ {
+ name: "Abhishek",
+ role: "Co-founder",
+ bio: "Engineering student passionate about building delightful, high-end motion tools that empower tutors and redefine modern digital learning.",
+ initials: "A",
+ imageSrc: "/abhi.jpg",
+ socials: {
+ twitter: "https://x.com/Abhi_SDev",
+ linkedin: "https://www.linkedin.com/in/abhishek-sonje-83a333209/",
+ email: "mailto:work.abhishek036@gmail.com",
+ },
+ },
+];
+
+/* ==========================================================================
+ SKETCHED CARD BACKGROUND COMPONENT (WITH PAPER CANVAS FILL)
+ ========================================================================== */
+function SketchedPaperFrame() {
+ return (
+
+ {/* Brutalist Shadow Layer */}
+
+
+
+
+ {/* Main Front Frame Stroke & Textured Paper Fill */}
+
+
+
+
+ {/* Fine Drafting Paper Dot-Matrix Overlay */}
+
+
+ );
+}
+
+interface FounderCardProps {
+ founder: (typeof foundersData)[0];
+}
+
+function FounderCard({ founder }: FounderCardProps) {
+ const [imgError, setImgError] = useState(false);
+
+ return (
+
+ {/* Hand-drawn Frame Asset with Paper Overlay */}
+
+
+ {/* AVATAR FRAME SYSTEM */}
+
+ {!imgError ? (
+ setImgError(true)}
+ />
+ ) : (
+ {founder.initials}
+ )}
+
+
+ {/* STRATIFIED PROFILE COPY */}
+
+
+
+ {founder.name}
+
+
+ {founder.role}
+
+
+ {founder.bio}
+
+
+
+ {/* BRUTALIST SOCIAL ACTION BUTTONS */}
+
+
+
+ );
+}
+
+const SketchMark = ({ flip = false }: { flip?: boolean }) => (
+
+
+
+
+
+);
+
+export function Founders() {
+ return (
+
+
+ {/* SECTION HEADER LAYOUT */}
+
+
+
+
+ Created by students, for educators
+
+
+
+
+
+
+ {/* TARGET RESPONSIVE MAPPED LAYOUT LOOP GRID */}
+
+ {foundersData.map((founder, i) => (
+
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/components/landing/hero.tsx b/frontend/components/landing/hero.tsx
new file mode 100644
index 0000000..2c27f66
--- /dev/null
+++ b/frontend/components/landing/hero.tsx
@@ -0,0 +1,285 @@
+"use client";
+
+import React, { useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
+import { FadeIn } from "./fade-in";
+import { PlayIcon, SparkleIcon } from "./icons";
+import { ArrowRight, Sparkles } from "lucide-react";
+import { VideoModal } from "./video-modal";
+import { SignInButton, SignUpButton, useAuth } from "@clerk/nextjs";
+import { DecorativeIcon } from "./decorative-icon";
+
+const SketchMark = ({ flip = false }: { flip?: boolean }) => (
+
+
+
+
+
+);
+
+/* =========================
+ Triangle Sketch (RE-POSITIONED TO MATCH NEW FRAME)
+ ========================= */
+const TriangleSketch = () => (
+
+ {/* Grid dots */}
+ {[...Array(6)].map((_, r) =>
+ [...Array(8)].map((_, c) => (
+
+ )),
+ )}
+
+ {/* Triangle */}
+
+
+ {/* Hatching */}
+ {[
+ ["175 170", "225 80"],
+ ["185 182", "245 90"],
+ ["195 192", "265 100"],
+ ["207 199", "283 112"],
+ ["220 200", "300 125"],
+ ["237 200", "316 138"],
+ ].map(([a, b], i) => (
+
+ ))}
+
+ {/* Labels */}
+
+ A
+
+
+ B
+
+
+ C
+
+
+ {/* Altitude */}
+
+
+ h
+
+
+ {/* Formula */}
+
+ Area =
+
+
+ 1
+
+
+
+ 2
+
+
+ × b × h
+
+
+);
+
+/* =========================
+ HERO
+ ========================= */
+export function Hero() {
+ const [isVideoOpen, setIsVideoOpen] = useState(false);
+ const { isSignedIn } = useAuth();
+
+ return (
+
+ setIsVideoOpen(false)} />
+
+ {/* Decorative Icons */}
+
+
+ {/* REVAMPED LEFT CONTENT */}
+
+ {/* PREMIUM FLOATING SUB-ACCENT BADGE */}
+
+
+
+
+
+ AI for Smarter Teaching
+
+
+
+
+
+
+
+ {/* HIGH-CONTRAST SKETCH TYPOGRAPHY */}
+
+
+ Teach. Draw.
+
+ Improve with{" "}
+
+ AI.
+
+
+
+
+
+ {/* REBALANCED SYSTEM DESCRIPTIVE PARAGRAPH */}
+
+
+ Chalk AI functions as your contextual, real-time smart board. Sketch
+ hardware configurations, geometry parameters, or raw equations, and
+ let custom vector vision models clean, align, and enhance your work
+ live.
+
+
+
+ {/* TACTILE CALL TO ACTIONS BAR */}
+
+
+ {/* Main Action Call */}
+ {!isSignedIn ? (
+
+
+ Start Your Journey
+
+
+
+ ) : (
+
+
+ Go to My Board
+
+
+
+ )}
+
+ {/* Video Interactive Layer Hook */}
+
setIsVideoOpen(true)}
+ className="group inline-flex items-center gap-3 px-6 py-4 bg-white text-chalk-ink font-bold rounded-2xl border-2 border-chalk-border
+ shadow-[4px_4px_0px_0px_var(--chalk-border)] hover:shadow-[2px_2px_0px_0px_var(--chalk-border)]
+ hover:-translate-y-0.5 active:translate-y-0 active:shadow-[0px_0px_0px_0px_transparent]
+ transition-all duration-150"
+ >
+
+
+
+ Watch Live Demo
+
+
+
+
+
+ {/* RIGHT FRAME - UNTOUCHED & MAINTAINING 20% OVERFLOW */}
+
+
+
+ {/* Frame PNG */}
+
+
+ {/* Content inside frame - Inset and aligned to the whiteboard surface */}
+
+
+
+
+ {/* AI Assistant */}
+
+
+
+
+
+
AI Assistant
+
+ Suggesting...
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/components/landing/how-it-works.tsx b/frontend/components/landing/how-it-works.tsx
new file mode 100644
index 0000000..53a48ff
--- /dev/null
+++ b/frontend/components/landing/how-it-works.tsx
@@ -0,0 +1,118 @@
+"use client";
+
+import React from "react";
+import { FadeIn } from "./fade-in";
+import {
+ PenTool,
+ Brain,
+ Sparkles,
+ GraduationCap,
+ ArrowRight,
+} from "lucide-react";
+import { DecorativeIcon } from "./decorative-icon";
+
+const steps = [
+ {
+ num: "01",
+ icon: ,
+ title: "Plug in your Key",
+ desc: "Simply add your Gemini API key in the board settings to unlock full AI power for free.",
+ },
+ {
+ num: "02",
+ icon: ,
+ title: "You Draw",
+ desc: "Sketch your geometric shapes or ideas naturally on the smart board.",
+ },
+ {
+ num: "03",
+ icon: ,
+ title: "AI Understands",
+ desc: "Our vision model instantly recognizes your drawings and mathematical context.",
+ },
+ {
+ num: "04",
+ icon: ,
+ title: "You Teach Better",
+ desc: "Deliver clear explanations and engage students with AI-enhanced visuals.",
+ },
+];
+
+export function HowItWorks() {
+ return (
+
+ {/* Decorative Icons */}
+
+
+ {/* SECTION HEADER */}
+
+
+
+ How Chalk AI works
+
+
+
+
+
+ {/* PROCESS FLOW WRAPPER */}
+
+ {/* DESKTOP BACKGROUND CONNECTOR LINE */}
+
+
+ {steps.map((step, i) => (
+
+
+
+ {/* STEP BUBBLE HEADER */}
+
+ STEP {step.num}
+
+
+ {/* PREMIUM THEMED ICON BOX */}
+
+ {step.icon}
+
+
+ {/* STEP TITLE */}
+
+
+ {step.title}
+
+
+
+ {/* STEP DESCRIPTION */}
+
+ {step.desc}
+
+
+
+
+ {/* TRANSLATING CONNECTOR ARROW FOR IN-BETWEEN STEPS */}
+ {i < steps.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/components/landing/icons.tsx b/frontend/components/landing/icons.tsx
new file mode 100644
index 0000000..561ce8c
--- /dev/null
+++ b/frontend/components/landing/icons.tsx
@@ -0,0 +1,86 @@
+
+export const StarIcon = () => (
+
+
+
+);
+
+export const PencilIcon = () => (
+
+
+
+);
+
+export const BulbIcon = () => (
+
+
+
+);
+
+export const UsersIcon = () => (
+
+
+
+);
+
+export const CloudIcon = () => (
+
+
+
+);
+
+export const SparkleIcon = () => (
+
+
+
+);
+
+export const PlayIcon = () => (
+
+
+
+);
+
+export const ArrowRightIcon = () => (
+
+
+
+);
+
+export const BrainIcon = () => (
+
+
+
+);
+
+export const DrawIcon = () => (
+
+
+
+
+);
+
+export const ImproveIcon = () => (
+
+
+
+
+
+
+);
+
+export const TeachIcon = () => (
+
+
+
+
+
+
+);
+
+export const LogoIcon = () => (
+
+
+
+
+);
diff --git a/frontend/components/landing/testimonials.tsx b/frontend/components/landing/testimonials.tsx
new file mode 100644
index 0000000..61de6ef
--- /dev/null
+++ b/frontend/components/landing/testimonials.tsx
@@ -0,0 +1,221 @@
+"use client";
+
+import React, { useState } from "react";
+import { FadeIn } from "./fade-in";
+import { Star } from "lucide-react";
+import { DecorativeIcon } from "./decorative-icon";
+
+const reviews = [
+ {
+ quote: "The AI suggestions are spot on! It helps me explain complex topics in a simpler way.",
+ name: "Priya Mehta",
+ role: "Physics Tutor",
+ initials: "PM",
+ },
+ {
+ quote: "My students are more engaged than ever. Chalk AI makes my lessons interactive and fun.",
+ name: "Rohit Verma",
+ role: "Chemistry Tutor",
+ initials: "RV",
+ },
+ {
+ quote: "I save so much time with AI enhancements. It's a must-have tool for every tutor.",
+ name: "Sneha Iyer",
+ role: "Biology Tutor",
+ initials: "SI",
+ },
+ {
+ quote: "Drawing diagrams used to take forever. Now it's done in seconds with smart suggestions.",
+ name: "Kavya Nair",
+ role: "Math Tutor",
+ initials: "KN",
+ },
+ {
+ quote: "The collaboration feature is brilliant. My students can see changes in real-time.",
+ name: "Arjun Sinha",
+ role: "Science Tutor",
+ initials: "AS",
+ },
+];
+
+/* ==========================================================================
+ SKETCHED PAPER BACKDROP FRAME ARCHITECTURE
+ ========================================================================== */
+function SketchedPaperFrame() {
+ return (
+
+ {/* Brutalist Shadow Background Path Layer */}
+
+
+
+
+ {/* Main Front Custom Outline & Premium Off-White Canvas Core */}
+
+
+
+
+ {/* Fine-grain Technical Grid Dot Matrix Blueprint Overlay */}
+
+
+ );
+}
+
+export function Testimonials() {
+ const reviewSet = [...reviews, ...reviews, ...reviews];
+
+ return (
+
+
+ {/* Decorative Assets */}
+
+
+
+
+ {/* HERO FEATURED TESTIMONIAL VIEW SYSTEM */}
+
+
+
+ {/* Structural Hand-drawn Backdrop Core */}
+
+
+ {/* Inner Content Grid Frame */}
+
+
+
+
+ “
+
+ Chalk AI has changed the way I teach. It's like having an{" "}
+
+ intelligent co-teacher
+ {" "}
+ live on my board.
+
+
+
+
+ AS
+
+
+
+ Amit Sharma
+
+
+ Math Tutor & Content Creator
+
+
+
+
+
+ {/* STICKY BADGE INSIGNIA */}
+
+
+
+
+
+
+
+
+ {/* MARQUEE ANCHOR HEADINGS */}
+
+
+
+
+ ✦
+
+ Loved by tutors worldwide
+
+ ✦
+
+
+
+
+
+ {/* INFINITE SMOOTH MARQUEE INTERFACE TRACK */}
+
+ {/* Feathered Mask Shims */}
+
+
+
+ {/* Running Tape Track Component */}
+
+ {reviewSet.map((r, i) => (
+
+ {/* Structural Paper Frame for each item in the tape slider */}
+
+
+ {/* Card Content Elements */}
+
+
+
+ “
+
+
+ {r.quote}
+
+
+
+
+
+ {r.initials}
+
+
+
+ {r.name}
+
+
+ {r.role}
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/components/landing/video-modal.tsx b/frontend/components/landing/video-modal.tsx
new file mode 100644
index 0000000..d03e669
--- /dev/null
+++ b/frontend/components/landing/video-modal.tsx
@@ -0,0 +1,69 @@
+"use client";
+
+import React from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import { X } from "lucide-react";
+
+interface VideoModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ videoUrl?: string;
+}
+
+export function VideoModal({ isOpen, onClose, videoUrl }: VideoModalProps) {
+ return (
+
+ {isOpen && (
+
+ {/* Backdrop */}
+
+
+ {/* Modal Content */}
+
+ {/* Close Button */}
+
+
+
+
+ {/* Video Placeholder / Player */}
+
+ {videoUrl ? (
+
+ ) : (
+
+
+
Demo Video Coming Soon
+
+ We're currently polishing the ultimate Chalk AI demonstration. Stay tuned for a full walkthrough of our AI-powered intelligent board!
+
+
+ )}
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend/components/layout-wrapper.tsx b/frontend/components/layout-wrapper.tsx
new file mode 100644
index 0000000..ea7c6d2
--- /dev/null
+++ b/frontend/components/layout-wrapper.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { usePathname } from "next/navigation";
+import { useUI } from "@/components/ui-provider";
+import { cn } from "@/lib/utils";
+
+export function LayoutWrapper({ children }: { children: React.ReactNode }) {
+ const { isFullscreen } = useUI();
+ const pathname = usePathname();
+ const isBoardPage = pathname === "/board";
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/frontend/components/ui-provider.tsx b/frontend/components/ui-provider.tsx
new file mode 100644
index 0000000..fb3899c
--- /dev/null
+++ b/frontend/components/ui-provider.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import React, { createContext, useContext, useState } from "react";
+
+interface UIContextType {
+ isFullscreen: boolean;
+ setIsFullscreen: (value: boolean) => void;
+ toggleFullscreen: () => void;
+}
+
+const UIContext = createContext(undefined);
+
+export function UIProvider({ children }: { children: React.ReactNode }) {
+ const [isFullscreen, setIsFullscreen] = useState(false);
+
+ const toggleFullscreen = () => setIsFullscreen((prev) => !prev);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useUI() {
+ const context = useContext(UIContext);
+ if (context === undefined) {
+ throw new Error("useUI must be used within a UIProvider");
+ }
+ return context;
+}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 7d29585..7be1b8a 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -11,6 +11,7 @@
"@ai-sdk/anthropic": "^3.0.1",
"@ai-sdk/google": "^3.0.1",
"@ai-sdk/openai": "^3.0.1",
+ "@clerk/nextjs": "^7.3.5",
"@tiptap/core": "^3.6.2",
"@tiptap/extension-list": "^3.6.2",
"@tiptap/pm": "^3.6.2",
@@ -179,7 +180,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -389,6 +389,87 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@clerk/backend": {
+ "version": "3.4.9",
+ "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-3.4.9.tgz",
+ "integrity": "sha512-2sbnGrHga3t/IFAkyvWO6H9QG1bnj7KUA13hpRB9utOYGCRdCn6oLxtiSdH6uGUzjX/km1dtw0uwZIxYgHbuVA==",
+ "license": "MIT",
+ "dependencies": {
+ "@clerk/shared": "^4.12.0",
+ "standardwebhooks": "^1.0.0",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ }
+ },
+ "node_modules/@clerk/nextjs": {
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-7.3.5.tgz",
+ "integrity": "sha512-Q2VaWLqnZrvWrbyQdji34fIBBHdZtRHjhd+BmW8a70Gxk2MKGgd9S7CzDFUBC/Q9jBjgGeZAZkH47WEK0opZow==",
+ "license": "MIT",
+ "dependencies": {
+ "@clerk/backend": "^3.4.9",
+ "@clerk/react": "^6.6.4",
+ "@clerk/shared": "^4.12.0",
+ "server-only": "0.0.1",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "peerDependencies": {
+ "next": "^15.2.8 || ^15.3.8 || ^15.4.10 || ^15.5.9 || ^15.6.0-0 || ^16.0.10 || ^16.1.0-0",
+ "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0",
+ "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0"
+ }
+ },
+ "node_modules/@clerk/react": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@clerk/react/-/react-6.6.4.tgz",
+ "integrity": "sha512-jhXqn1GFv309/gvVOdm3pBbeHDVVolyKbbrnHmvB+sAxEiwT7fIMP1JRUe2Rr1ycevMXRnxDzZmidra5xoFPKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@clerk/shared": "^4.12.0",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0",
+ "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0"
+ }
+ },
+ "node_modules/@clerk/shared": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-4.12.0.tgz",
+ "integrity": "sha512-LEe3lpi1r2DhFH9FuqzVrDQ2UkQJ3TA6XkkqnfHHRBfAJdh+jnYXYiivQpj3znMCihm3FVLOMXcpSqBz80XoQg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "^5.100.6",
+ "dequal": "2.0.3",
+ "glob-to-regexp": "0.4.1",
+ "js-cookie": "3.0.5",
+ "std-env": "^3.9.0"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0",
+ "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
@@ -580,7 +661,6 @@
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@floating-ui/core": "^1.7.3",
"@floating-ui/utils": "^0.2.10"
@@ -2898,6 +2978,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@stablelib/base64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
+ "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
+ "license": "MIT"
+ },
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -3184,12 +3270,21 @@
"tailwindcss": "4.1.18"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.100.10",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz",
+ "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
"node_modules/@tiptap/core": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.14.0.tgz",
"integrity": "sha512-nm0VWVA1Vq/jaKY3wyRXViL/kf78yMdH7qETpv4qZXDQLU+pdWV3IGoRTQTKESc7d8L1wL/2uCeByLNUJfrSIw==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -3425,7 +3520,6 @@
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.14.0.tgz",
"integrity": "sha512-rsjFH0Vd/4UbDsjwMLay7oz72VVu1r35t8ofAzy5587jn5JAjflaZs05XbRRMD2imUTK41dyajVSh8CqSnDEJw==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -3531,7 +3625,6 @@
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.14.0.tgz",
"integrity": "sha512-qQBVKqzU4ZVjRn8W0UbdfE4LaaIgcIWHOMrNnJ+PutrRzQ6ZzhmD/kRONvRWBfG9z3DU7pSKGwVYSR2hztsGuQ==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -3680,7 +3773,6 @@
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.6.2.tgz",
"integrity": "sha512-g+NXjqjbj6NfHOMl22uNWVYIu8oCq7RFfbnpohPMsSKJLaHYE8mJR++7T6P5R9FoqhIFdwizg1jTpwRU5CHqXQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-collab": "^1.3.1",
@@ -3868,7 +3960,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3878,7 +3969,6 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -3934,7 +4024,6 @@
"integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.50.1",
"@typescript-eslint/types": "8.50.1",
@@ -4461,7 +4550,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4831,7 +4919,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -5170,6 +5257,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -5459,7 +5555,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5645,7 +5740,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -5944,6 +6038,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-sha256": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
+ "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
+ "license": "Unlicense"
+ },
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@@ -6223,6 +6323,12 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@@ -6929,6 +7035,15 @@
"node": ">=18.0.0"
}
},
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8095,7 +8210,6 @@
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"orderedmap": "^2.0.0"
}
@@ -8125,7 +8239,6 @@
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
@@ -8174,7 +8287,6 @@
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.4.tgz",
"integrity": "sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"prosemirror-model": "^1.20.0",
"prosemirror-state": "^1.0.0",
@@ -8303,7 +8415,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8313,7 +8424,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -8594,6 +8704,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/server-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
+ "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
+ "license": "MIT"
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -8816,6 +8932,22 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/standardwebhooks": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
+ "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==",
+ "license": "MIT",
+ "dependencies": {
+ "@stablelib/base64": "^1.0.0",
+ "fast-sha256": "^1.3.0"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "license": "MIT"
+ },
"node_modules/stop-iteration-iterator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
@@ -9087,7 +9219,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -9126,7 +9257,6 @@
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.6.2.tgz",
"integrity": "sha512-XKZYrCVFsyQGF6dXQR73YR222l/76wkKfZ+2/4LCrem5qtcOarmv5pYxjUBG8mRuBPskTTBImSFTeQltJIUNCg==",
"license": "MIT",
- "peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
@@ -9167,7 +9297,6 @@
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.6.2.tgz",
"integrity": "sha512-g+NXjqjbj6NfHOMl22uNWVYIu8oCq7RFfbnpohPMsSKJLaHYE8mJR++7T6P5R9FoqhIFdwizg1jTpwRU5CHqXQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-collab": "^1.3.1",
@@ -9384,7 +9513,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -9723,7 +9851,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/frontend/package.json b/frontend/package.json
index 3991b98..e3fd769 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,6 +12,7 @@
"@ai-sdk/anthropic": "^3.0.1",
"@ai-sdk/google": "^3.0.1",
"@ai-sdk/openai": "^3.0.1",
+ "@clerk/nextjs": "^7.3.5",
"@tiptap/core": "^3.6.2",
"@tiptap/extension-list": "^3.6.2",
"@tiptap/pm": "^3.6.2",
diff --git a/frontend/proxy.ts b/frontend/proxy.ts
new file mode 100644
index 0000000..0b72876
--- /dev/null
+++ b/frontend/proxy.ts
@@ -0,0 +1,21 @@
+import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
+
+// Define which routes are public. All other routes will require authentication.
+// / is public, /board is protected.
+const isPublicRoute = createRouteMatcher(["/"]);
+
+export default clerkMiddleware(async (auth, request) => {
+ if (!isPublicRoute(request)) {
+ // Protect all non-public routes
+ await auth.protect();
+ }
+});
+
+export const config = {
+ matcher: [
+ // Skip Next.js internals and all static files, unless found in search params
+ "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
+ // Always run for API routes
+ "/(api|trpc)(.*)",
+ ],
+};
diff --git a/frontend/public/abhi.jpg b/frontend/public/abhi.jpg
new file mode 100644
index 0000000..2cacfea
Binary files /dev/null and b/frontend/public/abhi.jpg differ
diff --git a/frontend/public/jay.jpg b/frontend/public/jay.jpg
new file mode 100644
index 0000000..e027c9e
Binary files /dev/null and b/frontend/public/jay.jpg differ
diff --git a/frontend/public/landing/frame.png b/frontend/public/landing/frame.png
new file mode 100644
index 0000000..a4e8a1d
Binary files /dev/null and b/frontend/public/landing/frame.png differ
diff --git a/frontend/public/landing/icons8-brain-100.png b/frontend/public/landing/icons8-brain-100.png
new file mode 100644
index 0000000..0d55f2a
Binary files /dev/null and b/frontend/public/landing/icons8-brain-100.png differ
diff --git a/frontend/public/landing/icons8-calendar-100.png b/frontend/public/landing/icons8-calendar-100.png
new file mode 100644
index 0000000..b7da5c1
Binary files /dev/null and b/frontend/public/landing/icons8-calendar-100.png differ
diff --git a/frontend/public/landing/icons8-cloud-100.png b/frontend/public/landing/icons8-cloud-100.png
new file mode 100644
index 0000000..1cb2105
Binary files /dev/null and b/frontend/public/landing/icons8-cloud-100.png differ
diff --git a/frontend/public/landing/icons8-hourglass-100.png b/frontend/public/landing/icons8-hourglass-100.png
new file mode 100644
index 0000000..9a95920
Binary files /dev/null and b/frontend/public/landing/icons8-hourglass-100.png differ
diff --git a/frontend/public/landing/icons8-note-100.png b/frontend/public/landing/icons8-note-100.png
new file mode 100644
index 0000000..4cb4769
Binary files /dev/null and b/frontend/public/landing/icons8-note-100.png differ
diff --git a/frontend/public/landing/icons8-pin-100.png b/frontend/public/landing/icons8-pin-100.png
new file mode 100644
index 0000000..612b7c3
Binary files /dev/null and b/frontend/public/landing/icons8-pin-100.png differ