diff --git a/src/app/page.tsx b/src/app/page.tsx index df50c3a..38924d6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,21 +1,516 @@ 'use client'; -import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; +import Link from 'next/link'; +import { motion } from 'framer-motion'; +import TileWaveCanvas from '@/shared/ui/marketing/TileWaveCanvas'; import { useTestAuth } from '@/features/test-module/hooks/use-test-auth'; +import { useThemeStore } from '@/shared/ui/theme/store'; +import { THEME_OPTIONS } from '@/shared/ui/theme/theme'; +import { + ArrowRight, + BookOpen, + Check, + CircuitBoard, + FileText, + FunctionSquare, + Palette, + PenTool, + RefreshCw, + ShieldCheck, + Sigma, + Sparkles, + Terminal, + UsersRound, +} from 'lucide-react'; +import { useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'; + +const tools: { title: string; description: string; href: string; icon: ReactNode }[] = [ + { title: 'Guided Learn', description: 'Structured DBMS lessons with visual walkthroughs and 86+ copyable references.', href: '/learn', icon: }, + { title: 'SQL Sandbox', description: 'Run SQL live with autocomplete, result tables, and query history.', href: '/sandbox', icon: }, + { title: 'Table Generator', description: 'Generate realistic datasets and SQL INSERT statements instantly.', href: '/generator', icon: }, + { title: 'Relational Algebra', description: 'Compose expressions, inspect evaluations, and map to SQL.', href: '/algebra', icon: }, + { title: 'Tuple Calculus', description: 'Use TRC notation with quantifiers and convert it to SQL.', href: '/tuple-calculus', icon: }, + { title: 'ER Builder', description: 'Design models visually and convert diagrams to relational schema.', href: '/er-builder', icon: }, + { title: 'Normalizer Studio', description: 'Visualize table normalization from UNF to 5NF on a free canvas.', href: '/normalizer', icon: }, +]; + +const fadeUp = { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0 } }; + +const emptySubscribe = () => () => {}; +function useHydrated() { + return useSyncExternalStore( + emptySubscribe, + () => true, + () => false + ); +} + +type ClickBurst = { + id: number; + x: number; + y: number; +}; + +type NetworkConnection = { + saveData?: boolean; +}; + +type NavigatorWithHints = Navigator & { + connection?: NetworkConnection; + deviceMemory?: number; +}; + +const shouldUseLiteMode = () => { + if (typeof window === 'undefined') return false; + + const navigatorHints = window.navigator as NavigatorWithHints; + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const saveData = navigatorHints.connection?.saveData ?? false; + + return prefersReducedMotion || saveData; +}; export default function Home() { - const router = useRouter(); - const { isAuthenticated, hydrated, user } = useTestAuth(); + const { isAuthenticated, hydrated: authHydrated } = useTestAuth(); + const { theme, setTheme } = useThemeStore(); + const pageHydrated = useHydrated(); + const canUseAuthedRoutes = pageHydrated && authHydrated && isAuthenticated; + const [clickBursts, setClickBursts] = useState([]); + const [themeMenuOpen, setThemeMenuOpen] = useState(false); + const TRAIL_SEGMENTS = 7; + const ringRef = useRef(null); + const dotRef = useRef(null); + const trailSegmentRefs = useRef>([]); + const pointerTargetRef = useRef({ x: -120, y: -120 }); + const pointerTrailRef = useRef(Array.from({ length: TRAIL_SEGMENTS }, () => ({ x: -120, y: -120 }))); + const pointerInitializedRef = useRef(false); + const frameRef = useRef(null); + const themeMenuRef = useRef(null); + + const liteMode = useSyncExternalStore( + (onStoreChange) => { + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + const handlePreferenceChange = () => onStoreChange(); + + mediaQuery.addEventListener('change', handlePreferenceChange); + return () => { + mediaQuery.removeEventListener('change', handlePreferenceChange); + }; + }, + () => shouldUseLiteMode(), + () => false + ); + + const showMouseFx = useSyncExternalStore( + (onStoreChange) => { + const mediaQuery = window.matchMedia('(pointer: coarse)'); + const handleModeChange = () => onStoreChange(); + + mediaQuery.addEventListener('change', handleModeChange); + return () => { + mediaQuery.removeEventListener('change', handleModeChange); + }; + }, + () => !liteMode && !window.matchMedia('(pointer: coarse)').matches, + () => false + ); useEffect(() => { - if (!hydrated) return; - if (!isAuthenticated || !user) { - router.replace('/login'); - return; - } - router.replace(user.role === 'admin' ? '/admin' : '/dashboard'); - }, [hydrated, isAuthenticated, router, user]); - - return null; + if (!themeMenuOpen) return; + const handleOutsideClick = (e: MouseEvent) => { + if (themeMenuRef.current && !themeMenuRef.current.contains(e.target as Node)) { + setThemeMenuOpen(false); + } + }; + document.addEventListener('mousedown', handleOutsideClick); + return () => document.removeEventListener('mousedown', handleOutsideClick); + }, [themeMenuOpen]); + + useEffect(() => { + if (!showMouseFx) return; + + const applyPointerPosition = (x: number, y: number) => { + const ring = ringRef.current; + const dot = dotRef.current; + + if (ring) { + ring.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%) rotate(45deg)`; + } + + if (dot) { + dot.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%)`; + } + }; + + const resetTrail = (x: number, y: number) => { + pointerTrailRef.current.forEach((point) => { + point.x = x; + point.y = y; + }); + }; + + const animatePointer = () => { + const segments = trailSegmentRefs.current; + if (segments.length === 0) { + frameRef.current = null; + return; + } + + const targetX = pointerTargetRef.current.x; + const targetY = pointerTargetRef.current.y; + const trailPoints = pointerTrailRef.current; + + for (let index = 0; index < trailPoints.length; index += 1) { + const previousPoint = index === 0 ? { x: targetX, y: targetY } : trailPoints[index - 1]; + const point = trailPoints[index]; + const smoothing = index === 0 ? 0.45 : Math.max(0.2, 0.42 - index * 0.035); + + point.x += (previousPoint.x - point.x) * smoothing; + point.y += (previousPoint.y - point.y) * smoothing; + + const segment = segments[index]; + if (segment) { + segment.style.transform = `translate3d(${point.x}px, ${point.y}px, 0) translate(-50%, -50%)`; + } + } + + const tailEnd = trailPoints[trailPoints.length - 1]; + const trailDelta = Math.abs(targetX - tailEnd.x) + Math.abs(targetY - tailEnd.y); + + // Sleep the animation loop while idle; pointermove restarts it. + if (trailDelta < 0.2) { + frameRef.current = null; + return; + } + + frameRef.current = window.requestAnimationFrame(animatePointer); + }; + + const scheduleAnimation = () => { + if (frameRef.current === null) { + frameRef.current = window.requestAnimationFrame(animatePointer); + } + }; + + const updatePointer = (event: PointerEvent) => { + pointerTargetRef.current.x = event.clientX; + pointerTargetRef.current.y = event.clientY; + applyPointerPosition(event.clientX, event.clientY); + + if (!pointerInitializedRef.current) { + pointerInitializedRef.current = true; + resetTrail(event.clientX, event.clientY); + } + + scheduleAnimation(); + }; + + const handlePointerDown = (event: PointerEvent) => { + if (liteMode) return; + + const burstId = event.timeStamp + Math.random(); + setClickBursts((previous) => [...previous.slice(-4), { id: burstId, x: event.clientX, y: event.clientY }]); + + window.setTimeout(() => { + setClickBursts((previous) => previous.filter((burst) => burst.id !== burstId)); + }, 420); + }; + + const hidePointer = () => { + pointerInitializedRef.current = false; + pointerTargetRef.current.x = -120; + pointerTargetRef.current.y = -120; + + applyPointerPosition(-120, -120); + + scheduleAnimation(); + }; + + window.addEventListener('pointermove', updatePointer, { passive: true }); + window.addEventListener('pointerdown', handlePointerDown); + window.addEventListener('pointerleave', hidePointer); + + return () => { + if (frameRef.current !== null) { + window.cancelAnimationFrame(frameRef.current); + frameRef.current = null; + } + window.removeEventListener('pointermove', updatePointer); + window.removeEventListener('pointerdown', handlePointerDown); + window.removeEventListener('pointerleave', hidePointer); + }; + }, [liteMode, showMouseFx]); + + return ( +
+ {/* Animated tile background — canvas-based for performance */} + {!liteMode && } + + {showMouseFx && ( + <> + {Array.from({ length: TRAIL_SEGMENTS }, (_, index) => ( +
{ + trailSegmentRefs.current[index] = element; + }} + aria-hidden + className="pointer-events-none fixed left-0 top-0 z-[29] h-2 w-2 rounded-full bg-primary/70 will-change-transform" + style={{ + opacity: Math.max(0.08, 0.32 - index * 0.035), + scale: `${Math.max(0.45, 1 - index * 0.1)}`, + }} + /> + ))} + +
+
+ + {clickBursts.map((burst) => ( + + ))} + + )} + + + +
+ {/* Header */} +
+
+
+
+ +
+ + QueryCraft + +
+ +
+ {/* Theme switcher */} +
+ + + {themeMenuOpen && ( +
+
+
+

Appearance

+ + {theme.replace('-', ' ')} + +
+
+ {THEME_OPTIONS.map((option) => { + const isActive = theme === option.value; + return ( + + ); + })} +
+
+
+ )} +
+ + + Launch App + +
+
+
+ +
+ {/* Hero */} +
+ + {/* Soft glass backdrop behind text for readability */} +
+

Database Learning Studio

+

+ Master SQL.
+ Understand the theory. +

+

+ One workspace for SQL, relational algebra, ER diagrams, and normalization — so every concept reinforces the next. +

+
+ + Get Started + + + Browse SQL Reference + +
+ +
+ + {/* Divider */} +
+ + {/* Tools */} +
+ +

7 Workspaces

+
+ {tools.map((tool, i) => ( + + +
+
+
+ {tool.icon} +
+ +
+
+

{tool.title}

+

{tool.description}

+
+

Open Workspace

+ + + ))} +
+
+
+ + {/* Simple CTA */} +
+ +

Ready to build real database intuition?

+

Free to use. No credit card required.

+ + Start Learning + +
+
+
+ + {/* Footer */} + +
+
+ ); }