From f5bbe2d8aed455b1e0dfcb95b4677e9ee3b272df Mon Sep 17 00:00:00 2001 From: Abhishek Sonje Date: Sat, 16 May 2026 23:46:07 +0530 Subject: [PATCH 1/3] feat: Completed landing page --- frontend/app/board/page.tsx | 20 + frontend/app/globals.css | 50 +- frontend/app/layout.tsx | 64 ++- frontend/app/page.tsx | 481 +------------------ frontend/bun.lock | 1 + frontend/components/board-client.tsx | 410 ++++++++++++++++ frontend/components/clerk-helpers.tsx | 26 + frontend/components/header.tsx | 92 ++++ frontend/components/landing/cta.tsx | 53 ++ frontend/components/landing/fade-in.tsx | 35 ++ frontend/components/landing/features.tsx | 117 +++++ frontend/components/landing/footer.tsx | 142 ++++++ frontend/components/landing/founders.tsx | 137 ++++++ frontend/components/landing/hero.tsx | 234 +++++++++ frontend/components/landing/how-it-works.tsx | 108 +++++ frontend/components/landing/icons.tsx | 86 ++++ frontend/components/landing/testimonials.tsx | 150 ++++++ frontend/components/landing/video-modal.tsx | 69 +++ frontend/components/layout-wrapper.tsx | 20 + frontend/components/ui-provider.tsx | 31 ++ frontend/package-lock.json | 173 ++++++- frontend/package.json | 1 + frontend/proxy.ts | 21 + frontend/public/abhi.jpg | Bin 0 -> 10967 bytes frontend/public/jay.jpg | Bin 0 -> 35181 bytes frontend/public/landing/frame.png | Bin 0 -> 2236206 bytes 26 files changed, 2021 insertions(+), 500 deletions(-) create mode 100644 frontend/app/board/page.tsx create mode 100644 frontend/components/board-client.tsx create mode 100644 frontend/components/clerk-helpers.tsx create mode 100644 frontend/components/header.tsx create mode 100644 frontend/components/landing/cta.tsx create mode 100644 frontend/components/landing/fade-in.tsx create mode 100644 frontend/components/landing/features.tsx create mode 100644 frontend/components/landing/footer.tsx create mode 100644 frontend/components/landing/founders.tsx create mode 100644 frontend/components/landing/hero.tsx create mode 100644 frontend/components/landing/how-it-works.tsx create mode 100644 frontend/components/landing/icons.tsx create mode 100644 frontend/components/landing/testimonials.tsx create mode 100644 frontend/components/landing/video-modal.tsx create mode 100644 frontend/components/layout-wrapper.tsx create mode 100644 frontend/components/ui-provider.tsx create mode 100644 frontend/proxy.ts create mode 100644 frontend/public/abhi.jpg create mode 100644 frontend/public/jay.jpg create mode 100644 frontend/public/landing/frame.png 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..ca1daaf 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,7 +1,36 @@ 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 { 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 +90,40 @@ export default function RootLayout({ return ( - {children} + + +
+ + + + + + + + + +
+ + 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 */} -
- - - - - - - {/* Mic Button */} - -
- - {/* Live Transcript (when listening) */} - - {isListening && ( - -
-
-
- Listening... -
-

- {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 */} -
- Refined diagram -
- - {/* Footer Actions */} -
- - -
-
- )} -
+
+ + + + + + +
); -} +} \ 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/board-client.tsx b/frontend/components/board-client.tsx new file mode 100644 index 0000000..ab4c05f --- /dev/null +++ b/frontend/components/board-client.tsx @@ -0,0 +1,410 @@ +"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 } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useUI } from "@/components/ui-provider"; +import { cn } from "@/lib/utils"; + + +export function BoardClient() { + const { isFullscreen, toggleFullscreen } = useUI(); + 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" }, + 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); + }, []); + + 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; + + return ( +
+
+ +
+ +
+ +
+ + + + + + + + + {/* Fullscreen Button */} + +
+ + + {isListening && ( + +
+
+
+ Listening... +
+

+ {agentTranscript || "Start speaking..."} +

+
+ + )} + + + + {error && ( + +
+ {error} +
+
+ )} +
+ + + + + +
+ + + {generatedImage && ( + +
+
+ + AI + + Suggestion +
+ +
+ +
+ Refined diagram +
+ +
+ + +
+
+ )} +
+
+ ); +} 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 ( +
+ +
+ +
+ Home + +
+ ); + } + + return ( +
+
+ {/* BRAND SIGNATURE IDENTIFIER */} + + {/* Stylized Modern Pencil Container */} +
+ +
+ + Chalk AI + + + + + {/* ACTIONS RACK WRAPPER */} +
+ {/* MODERN FULLSCREEN TOGGLE */} + {!isHomePage && ( + + )} + + {/* CHALK-STYLE CONTAINER DIVIDER */} + {!isHomePage &&
} + + {/* AUTH SLOTS / CHILD INJECTIONS */} +
{children}
+
+
+
+ ); +} diff --git a/frontend/components/landing/cta.tsx b/frontend/components/landing/cta.tsx new file mode 100644 index 0000000..c6b5a7b --- /dev/null +++ b/frontend/components/landing/cta.tsx @@ -0,0 +1,53 @@ +"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"; + +export function CTA() { + const { isSignedIn } = useAuth(); + + return ( +
+
+ +
+ {/* 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 ? ( + + + + ) : ( + + + + )} +
+ +
+
+ ); +} 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..5467cd2 --- /dev/null +++ b/frontend/components/landing/features.tsx @@ -0,0 +1,117 @@ +"use client"; + +import React from "react"; +import { FadeIn } from "./fade-in"; +import { PencilIcon, BulbIcon, UsersIcon, CloudIcon } from "./icons"; +import { Cloud, Lightbulb, PencilRulerIcon, User } from "lucide-react"; + +const features = [ + { + icon: , + title: "Smart Drawing", + desc: "AI helps you refine your sketches and diagrams in real-time.", + }, + { + icon: , + title: "AI Suggestions", + desc: "Get intelligent suggestions to improve your teaching and explanations.", + }, + { + icon: , + title: "Interactive Lessons", + desc: "Engage students with live collaboration and interactive whiteboard.", + }, + { + icon: , + title: "Save & Share", + desc: "Save your board and share instantly with your students.", + }, +]; + +export function Features() { + return ( +
+ +

+ Everything you need on one{" "} + + intelligent + {" "} + board +

+
+ +
+ {features.map((f, i) => ( + +
+ {/* Subtle background ink / chalk bleed */} +
+ + {/* Card content */} +
+ {/* Icon */} +
+ + {f.icon} + +
+ + {/* Heading */} +

+ + {f.title} + +

+ + {/* Description */} +

+ {f.desc} +

+
+
+ + ))} +
+
+ ); +} 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 ( +
+ {/* Subtle hand-drawn aesthetic grid pattern texture overlay */} +
+ + + + + + + + +
+ +
+
+ {/* BRAND SIGNATURE IDENTIFIER */} +
+
+ + Chalk AI +
+

+ The AI-powered smart board built by students, transforming how + educators sketch and explain complex ideas. +

+
+ + {/* BRUTALIST CALL TO ACTIONS (GITHUB & SOCIAL CHANNELS) */} +
+ {/* TARGET HIGH-AGENCY GITHUB CONNECTOR */} + + + Star on GitHub + + + {/* VERTICAL DIVIDER LINK SHIM */} + | + + {/* TARGET SOCIAL LINKS BUNDLE */} + +
+
+ + {/* REINFORCED CHALKBOARD RULER DIVIDER */} +
+ + {/* BOTTOM METADATA LAYER */} +
+ + © {currentYear} Chalk AI. Handcrafted with care. All rights + reserved. + +
+ Built by + + Jaydeep + + & + + Abhishek + +
+
+
+
+ ); +} diff --git a/frontend/components/landing/founders.tsx b/frontend/components/landing/founders.tsx new file mode 100644 index 0000000..26582dc --- /dev/null +++ b/frontend/components/landing/founders.tsx @@ -0,0 +1,137 @@ +"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", + }, + }, +]; + +interface FounderCardProps { + founder: (typeof foundersData)[0]; +} + +function FounderCard({ founder }: FounderCardProps) { + const [imgError, setImgError] = useState(false); + + return ( +
+ {/* Subtle background context tint */} +
+ + {/* AVATAR FRAME SYSTEM WITH AUTONOMOUS FALLBACK */} +
+ {!imgError ? ( + {`${founder.name} setImgError(true)} + /> + ) : ( + {founder.initials} + )} +
+ + {/* STRATIFIED PROFILE COPY */} +
+
+

+ {founder.name} +

+
+ {founder.role} +
+

+ {founder.bio} +

+
+ + {/* BRUTALIST SOCIAL BUTTON SLOTS */} + +
+
+ ); +} + +export function Founders() { + return ( +
+
+ {/* LIGHT NON-BOLD HEADING LAYOUT */} +
+ +

+ Created by students, for educators + +

+
+
+ + {/* TARGET 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..5edeaaa --- /dev/null +++ b/frontend/components/landing/hero.tsx @@ -0,0 +1,234 @@ +"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"; + +/* ========================= + 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)} /> + + {/* 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 ? ( + + + + ) : ( + + + + )} + + {/* Video Interactive Layer Hook */} + +
+
+
+ + {/* RIGHT FRAME - UNTOUCHED & MAINTAINING 20% OVERFLOW */} +
+ +
+ {/* Frame PNG */} + Chalk Board Frame + + {/* Content inside frame - Inset and aligned to the whiteboard surface */} +
+ +
+ + {/* AI Assistant */} +
+
+ +
+
+
AI Assistant
+
+ Suggesting... +
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/components/landing/how-it-works.tsx b/frontend/components/landing/how-it-works.tsx new file mode 100644 index 0000000..ee0011b --- /dev/null +++ b/frontend/components/landing/how-it-works.tsx @@ -0,0 +1,108 @@ +"use client"; + +import React from "react"; +import { FadeIn } from "./fade-in"; +import { + PenTool, + Brain, + Sparkles, + GraduationCap, + ArrowRight, +} from "lucide-react"; + +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 ( +
+
+ {/* 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..bc8357a --- /dev/null +++ b/frontend/components/landing/testimonials.tsx @@ -0,0 +1,150 @@ +"use client"; + +import React from "react"; +import { FadeIn } from "./fade-in"; +import { Star } from "lucide-react"; + +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", + }, +]; + +export function Testimonials() { + // Triple items list to prevent visual clipping or gaps on extra-wide monitors + const reviewSet = [...reviews, ...reviews, ...reviews]; + + return ( +
+
+ {/* HERO FEATURED TESTIMONIAL VIEW */} + +
+
+ +
+

+ + “ + + 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 */} +
+ +
+
+ + + {/* SECTION MARQUEE HEADER */} +
+ +

+ + ✦ + + Loved by tutors worldwide + + ✦ + +

+
+
+ + {/* INFINITE SMOOTH MARQUEE INTERFACE SECTION */} +
+ {/* Edge Feathering Shims Overlays */} +
+
+ + {/* Sliding Track */} +
+ {reviewSet.map((r, i) => ( +
+
+
+ “ +
+

+ {r.quote} +

+
+ +
+
+ {r.initials} +
+
+
+ {r.name} +
+
+ {r.role} +
+
+
+
+ ))} +
+
+
+
+ ); +} 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 ? ( +