From ddfea211fb7eaae8a026c00369b1347d9ab64b0b Mon Sep 17 00:00:00 2001 From: knoxiboy Date: Sat, 6 Jun 2026 19:42:04 +0530 Subject: [PATCH] feat: added peer to peer chat section --- chat-system/.gitignore | 5 + chat-system/README.md | 9 + chat-system/components.json | 16 + chat-system/components/ChatHome.tsx | 320 +++ chat-system/components/ChatPreviewCard.tsx | 155 ++ chat-system/components/ChatRoom.tsx | 34 + chat-system/components/ChatWindow.tsx | 130 ++ chat-system/components/PeerToPeerRoom.tsx | 34 + chat-system/components/ResourceShare.tsx | 31 + chat-system/components/ShapeGrid.module.css | 6 + chat-system/components/ShapeGrid.tsx | 308 +++ chat-system/components/ThemeToggle.tsx | 33 + chat-system/components/ui/button.tsx | 57 + chat-system/components/ui/card.tsx | 76 + chat-system/lib/utils.ts | 6 + chat-system/next.config.mjs | 16 + chat-system/package-lock.json | 1960 +++++++++++++++++++ chat-system/package.json | 30 + chat-system/pages/_app.tsx | 10 + chat-system/pages/chat/index.tsx | 5 + chat-system/pages/chat/peer-to-peer.tsx | 5 + chat-system/pages/index.tsx | 5 + chat-system/postcss.config.js | 5 + chat-system/server.js | 27 + chat-system/styles/globals.css | 80 + chat-system/tailwind.config.js | 59 + chat-system/tailwind.config.ts | 62 + chat-system/tsconfig.json | 40 + src/components/Sidebar.tsx | 45 +- tsconfig.json | 3 +- 30 files changed, 3570 insertions(+), 2 deletions(-) create mode 100644 chat-system/.gitignore create mode 100644 chat-system/README.md create mode 100644 chat-system/components.json create mode 100644 chat-system/components/ChatHome.tsx create mode 100644 chat-system/components/ChatPreviewCard.tsx create mode 100644 chat-system/components/ChatRoom.tsx create mode 100644 chat-system/components/ChatWindow.tsx create mode 100644 chat-system/components/PeerToPeerRoom.tsx create mode 100644 chat-system/components/ResourceShare.tsx create mode 100644 chat-system/components/ShapeGrid.module.css create mode 100644 chat-system/components/ShapeGrid.tsx create mode 100644 chat-system/components/ThemeToggle.tsx create mode 100644 chat-system/components/ui/button.tsx create mode 100644 chat-system/components/ui/card.tsx create mode 100644 chat-system/lib/utils.ts create mode 100644 chat-system/next.config.mjs create mode 100644 chat-system/package-lock.json create mode 100644 chat-system/package.json create mode 100644 chat-system/pages/_app.tsx create mode 100644 chat-system/pages/chat/index.tsx create mode 100644 chat-system/pages/chat/peer-to-peer.tsx create mode 100644 chat-system/pages/index.tsx create mode 100644 chat-system/postcss.config.js create mode 100644 chat-system/server.js create mode 100644 chat-system/styles/globals.css create mode 100644 chat-system/tailwind.config.js create mode 100644 chat-system/tailwind.config.ts create mode 100644 chat-system/tsconfig.json diff --git a/chat-system/.gitignore b/chat-system/.gitignore new file mode 100644 index 00000000..2d08a9eb --- /dev/null +++ b/chat-system/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.next/ +.env +.DS_Store +*.log diff --git a/chat-system/README.md b/chat-system/README.md new file mode 100644 index 00000000..a4a22c6f --- /dev/null +++ b/chat-system/README.md @@ -0,0 +1,9 @@ +# Real-Time Chat System + +This module implements a real-time user-to-user chat system for collaborative doubt discussion, group chats, and resource sharing. All code is isolated from the main project. + +## Features +- One-to-one and group chat +- Real-time messaging +- Resource/code sharing +- Modular and non-intrusive diff --git a/chat-system/components.json b/chat-system/components.json new file mode 100644 index 00000000..52b73f5d --- /dev/null +++ b/chat-system/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "styles/globals.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/chat-system/components/ChatHome.tsx b/chat-system/components/ChatHome.tsx new file mode 100644 index 00000000..f21f4b63 --- /dev/null +++ b/chat-system/components/ChatHome.tsx @@ -0,0 +1,320 @@ +"use client"; + +import { + Activity, + ArrowRight, + Clipboard, + LayoutGrid, + Map, + MessageCircle, + Users, +} from "lucide-react"; +import Link from "next/link"; +import { Inter, Staatliches } from "next/font/google"; + +import ChatPreviewCard from "./ChatPreviewCard"; +import ShapeGrid from "./ShapeGrid"; +import { ThemeToggle } from "./ThemeToggle"; + +const inter = Inter({ subsets: ["latin"] }); +const staatliches = Staatliches({ weight: "400", subsets: ["latin"] }); + +export default function ChatHome() { + const features = [ + { + title: "Real-time collaborative discussions", + description: "Share questions, answers, and classroom updates instantly across study groups.", + icon: MessageCircle, + }, + { + title: "Smart classroom management", + description: "Organize learning spaces, schedules, and teacher workflows with ease.", + icon: LayoutGrid, + }, + { + title: "Notes and resource sharing", + description: "Keep study materials, highlights, and shared guides organized in one hub.", + icon: Clipboard, + }, + { + title: "Learning roadmaps and guidance", + description: "Follow curated study paths that keep learners focused on milestones.", + icon: Map, + }, + { + title: "AI-powered doubt solving", + description: "Get instant, context-aware answers to questions with smart AI support.", + icon: Activity, + }, + { + title: "Organized study collaboration", + description: "Coordinate projects, peer review, and group work with clear tools and structure.", + icon: Users, + }, + ]; + + const howItWorks = [ + { + title: "Join or create a classroom", + description: "Teachers set up rooms, students join using invite codes." + }, + { + title: "Ask doubts instantly", + description: "Post questions using text or image and get AI + peer help." + }, + { + title: "Get clear answers & insights", + description: "AI explanations, teacher guidance, and analytics all in one place." + } + ]; + + const testimonials = [ + { + name: "Aarav Sharma", + role: "B.Tech Student", + text: "DoubtDesk made it so easy to clear my doubts during exam prep. The AI explanations are super clear." + }, + { + name: "Neha Verma", + role: "CS Student", + text: "No more messy WhatsApp groups. Everything is structured and easy to follow." + }, + { + name: "Rohit Mehta", + role: "Teaching Assistant", + text: "Analytics help me understand where students struggle the most." + } + ]; + + return ( +
+ {/* Navbar */} +
+
+
+ +
+ D +
+

+ DoubtDesk +

+ + +
+ + + + + Peer to Peer Discussion + +
+ +
+ + + + + + + +
+
+
+ + {/* Hero Section */} +
+
+ +
+
+
+
+
+
+
+

+ Empower
+ Your Learning
+ with{" "} + + Collaborative AI. + +

+ +
+
+ Collaborative classrooms +
+

+ A live student collaboration system for doubts, notes, and shared progress across campus groups. +

+
+ +
+ + + +
+
+ +
+ +
+
+
+ +
+
+
+
+
+ Features +
+

+ Everything your classroom needs to solve doubts, stay aligned, and move faster. +

+

+ Built for modern study teams, DoubtDesk blends AI-powered doubt solving, shared resources, and smart classroom flows into a single polished platform. +

+
+ +
+ {features.map((feature) => { + const Icon = feature.icon; + return ( +
+
+ +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ); + })} +
+
+
+ + {/* How It Works */} +
+
+

+ How it works +

+ +

+ Simple flow from doubt → solution → understanding +

+ +
+ {howItWorks.map((step, index) => ( +
+
+ {index + 1} +
+ +

+ {step.title} +

+ +

+ {step.description} +

+
+ ))} +
+
+
+ + {/* Testimonials */} +
+
+
+ Testimonials +
+

+ What students say +

+ +

+ Real feedback from learners and educators +

+ +
+ {testimonials.map((t) => ( +
+

+ “{t.text}” +

+ +
+
+ {t.name} +
+
+ {t.role} +
+
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/chat-system/components/ChatPreviewCard.tsx b/chat-system/components/ChatPreviewCard.tsx new file mode 100644 index 00000000..32168dd7 --- /dev/null +++ b/chat-system/components/ChatPreviewCard.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Circle, Sparkles } from "lucide-react"; +import { Space_Mono, Staatliches } from "next/font/google"; + +const staatliches = Staatliches({ weight: "400", subsets: ["latin"] }); +const spaceMono = Space_Mono({ weight: ["400", "700"], subsets: ["latin"] }); + +const liveUpdates = [ + "New DBMS notes uploaded", + "CN lecture summarized", + "Placement roadmap updated", +]; + +export default function ChatPreviewCard() { + const [updateIndex, setUpdateIndex] = useState(0); + + useEffect(() => { + const timer = window.setInterval(() => { + setUpdateIndex((current) => (current + 1) % liveUpdates.length); + }, 2600); + + return () => window.clearInterval(timer); + }, []); + + return ( +
+
+
+
+ + + +
+
+
+
+ Live Campus Thread +
+
+ Wave Optics +
+
+ 23 active +
+
+ +
+ + Live +
+
+ +
+
+ Discussion preview + + syncing + + | + + +
+ +
+

+ > Why does destructive interference produce dark fringes? +

+ +
+
+
+ AI summary available +
+
+ 12 replies ongoing +
+
+ +
+ + Thread summarizing +
+
+
+
+ +
+ {liveUpdates.map((update, index) => { + const isActive = index === updateIndex; + + return ( +
+ + + {update} + +
+ ); + })} +
+ +
+ + Quiet academic activity stream +
+
+
+ ); +} diff --git a/chat-system/components/ChatRoom.tsx b/chat-system/components/ChatRoom.tsx new file mode 100644 index 00000000..ef32e335 --- /dev/null +++ b/chat-system/components/ChatRoom.tsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react'; +import ChatWindow from './ChatWindow'; +import Link from 'next/link'; +import { ArrowLeft, MessageCircle } from 'lucide-react'; + +export default function ChatRoom() { + const [room] = useState('general'); + return ( +
+
+
+
+ + + Back + +
+
+ +
+
+

Classroom Chat

+

{room} room

+
+
+
+ +
+ +
+
+
+ ); +} diff --git a/chat-system/components/ChatWindow.tsx b/chat-system/components/ChatWindow.tsx new file mode 100644 index 00000000..4c88cd4c --- /dev/null +++ b/chat-system/components/ChatWindow.tsx @@ -0,0 +1,130 @@ +import React, { useEffect, useRef, useState } from 'react'; +import io from 'socket.io-client'; +import ResourceShare from './ResourceShare'; +import { Send } from 'lucide-react'; + +const socket = io(); // Will be configured for server later + +interface Message { + user: string; + text: string; +} + +export default function ChatWindow({ room }: { room: string }) { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [user] = useState('User' + Math.floor(Math.random() * 1000)); + const messagesEndRef = useRef(null); + + useEffect(() => { + socket.emit('join', room); + socket.on('message', (msg: Message) => { + setMessages((prev) => [...prev, msg]); + }); + return () => { + socket.off('message'); + socket.emit('leave', room); + }; + }, [room]); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const sendMessage = (e: React.FormEvent) => { + e.preventDefault(); + if (!input.trim()) return; + const msg = { user, text: input }; + socket.emit('message', { ...msg, room }); + setMessages((prev) => [...prev, msg]); + setInput(''); + }; + + return ( +
+
+
+
+

Live discussion

+

Room: {room}

+
+
+ Online +
+
+
+ +
+
+ {messages.length === 0 && ( +
+
+

No messages yet

+

+ Start the conversation with a doubt, note, or resource for the room. +

+
+
+ )} + + {messages.map((msg, idx) => { + const isOwnMessage = msg.user === user; + + return ( +
+ {!isOwnMessage && ( +
+ {msg.user.slice(0, 2).toUpperCase()} +
+ )} + +
+
{isOwnMessage ? 'You' : msg.user}
+
+ {msg.text} +
+
+ + {isOwnMessage && ( +
+ {user.slice(0, 2).toUpperCase()} +
+ )} +
+ ); + })} +
+
+
+ +
+
+ setInput(e.target.value)} + placeholder="Type a message..." + className="min-w-0 flex-1 rounded-2xl border border-white/10 bg-white/[0.06] px-4 py-3 text-sm text-white outline-none transition placeholder:text-slate-500 focus:border-[#8BB8FF]/50 focus:ring-2 focus:ring-[#5E8CFF]/20" + /> + +
+ { + const msg = { user, text: resource }; + socket.emit('message', { ...msg, room }); + setMessages((prev) => [...prev, msg]); + }} /> +
+
+ ); +} diff --git a/chat-system/components/PeerToPeerRoom.tsx b/chat-system/components/PeerToPeerRoom.tsx new file mode 100644 index 00000000..1515c6f2 --- /dev/null +++ b/chat-system/components/PeerToPeerRoom.tsx @@ -0,0 +1,34 @@ +import React, { useState } from 'react'; +import ChatWindow from './ChatWindow'; +import Link from 'next/link'; +import { ArrowLeft, Network } from 'lucide-react'; + +export default function PeerToPeerRoom() { + const [room] = useState('peer-to-peer'); + return ( +
+
+
+
+ + + Back + +
+
+ +
+
+

Peer to Peer

+

private room

+
+
+
+ +
+ +
+
+
+ ); +} diff --git a/chat-system/components/ResourceShare.tsx b/chat-system/components/ResourceShare.tsx new file mode 100644 index 00000000..a874e207 --- /dev/null +++ b/chat-system/components/ResourceShare.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; +import { Paperclip } from 'lucide-react'; + +export default function ResourceShare({ onShare }: { onShare: (resource: string) => void }) { + const [resource, setResource] = useState(''); + return ( +
{ + e.preventDefault(); + if (!resource.trim()) return; + onShare(resource); + setResource(''); + }} + className="mt-3 flex gap-3" + > + setResource(e.target.value)} + placeholder="Paste a link, code, or resource..." + className="min-w-0 flex-1 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-sm text-white outline-none transition placeholder:text-slate-500 focus:border-[#8BB8FF]/50 focus:ring-2 focus:ring-[#5E8CFF]/20" + /> + +
+ ); +} diff --git a/chat-system/components/ShapeGrid.module.css b/chat-system/components/ShapeGrid.module.css new file mode 100644 index 00000000..a350ec1a --- /dev/null +++ b/chat-system/components/ShapeGrid.module.css @@ -0,0 +1,6 @@ +.shapegridCanvas { + width: 100%; + height: 100%; + border: none; + display: block; +} diff --git a/chat-system/components/ShapeGrid.tsx b/chat-system/components/ShapeGrid.tsx new file mode 100644 index 00000000..bf44e934 --- /dev/null +++ b/chat-system/components/ShapeGrid.tsx @@ -0,0 +1,308 @@ +import { useRef, useEffect } from 'react'; +import styles from './ShapeGrid.module.css'; + +interface ShapeGridProps { + direction?: 'right' | 'left' | 'up' | 'down' | 'diagonal'; + speed?: number; + borderColor?: string; + squareSize?: number; + hoverFillColor?: string; + shape?: 'hexagon' | 'triangle' | 'circle' | 'square'; + hoverTrailAmount?: number; + className?: string; +} + +const ShapeGrid: React.FC = ({ + direction = 'right', + speed = 1, + borderColor = '#999', + squareSize = 40, + hoverFillColor = '#222', + shape = 'square', + hoverTrailAmount = 0, + className = '', +}) => { + const canvasRef = useRef(null); + const requestRef = useRef(null); + const gridOffset = useRef({ x: 0, y: 0 }); + const hoveredSquare = useRef<{ x: number; y: number } | null>(null); + const trailCells = useRef<{ x: number; y: number }[]>([]); + const cellOpacities = useRef>(new Map()); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const isHex = shape === 'hexagon'; + const isTri = shape === 'triangle'; + const hexHoriz = squareSize * 1.5; + const hexVert = squareSize * Math.sqrt(3); + + const resizeCanvas = () => { + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + }; + + window.addEventListener('resize', resizeCanvas); + resizeCanvas(); + + const drawHex = (cx: number, cy: number, size: number) => { + ctx.beginPath(); + for (let i = 0; i < 6; i++) { + const angle = (Math.PI / 3) * i; + const vx = cx + size * Math.cos(angle); + const vy = cy + size * Math.sin(angle); + if (i === 0) { + ctx.moveTo(vx, vy); + } else { + ctx.lineTo(vx, vy); + } + } + ctx.closePath(); + }; + + const drawCircle = (cx: number, cy: number, size: number) => { + ctx.beginPath(); + ctx.arc(cx, cy, size / 2, 0, Math.PI * 2); + ctx.closePath(); + }; + + const drawTriangle = (cx: number, cy: number, size: number, flip: boolean) => { + ctx.beginPath(); + if (flip) { + ctx.moveTo(cx, cy + size / 2); + ctx.lineTo(cx + size / 2, cy - size / 2); + ctx.lineTo(cx - size / 2, cy - size / 2); + } else { + ctx.moveTo(cx, cy - size / 2); + ctx.lineTo(cx + size / 2, cy + size / 2); + ctx.lineTo(cx - size / 2, cy + size / 2); + } + ctx.closePath(); + }; + + const drawGrid = () => { + ctx.clearRect(0, 0, canvas.width, canvas.height); + + if (isHex) { + const colShift = Math.floor(gridOffset.current.x / hexHoriz); + const offsetX = ((gridOffset.current.x % hexHoriz) + hexHoriz) % hexHoriz; + const offsetY = ((gridOffset.current.y % hexVert) + hexVert) % hexVert; + const cols = Math.ceil(canvas.width / hexHoriz) + 3; + const rows = Math.ceil(canvas.height / hexVert) + 3; + + for (let col = -2; col < cols; col++) { + for (let row = -2; row < rows; row++) { + const cx = col * hexHoriz + offsetX; + const cy = row * hexVert + ((col + colShift) % 2 !== 0 ? hexVert / 2 : 0) + offsetY; + const cellKey = `${col},${row}`; + const alpha = cellOpacities.current.get(cellKey); + if (alpha) { + ctx.globalAlpha = alpha; + drawHex(cx, cy, squareSize); + ctx.fillStyle = hoverFillColor; + ctx.fill(); + ctx.globalAlpha = 1; + } + drawHex(cx, cy, squareSize); + ctx.strokeStyle = borderColor; + ctx.stroke(); + } + } + } else if (isTri) { + const halfW = squareSize / 2; + const colShift = Math.floor(gridOffset.current.x / halfW); + const rowShift = Math.floor(gridOffset.current.y / squareSize); + const offsetX = ((gridOffset.current.x % halfW) + halfW) % halfW; + const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize; + const cols = Math.ceil(canvas.width / halfW) + 4; + const rows = Math.ceil(canvas.height / squareSize) + 4; + + for (let col = -2; col < cols; col++) { + for (let row = -2; row < rows; row++) { + const cx = col * halfW + offsetX; + const cy = row * squareSize + squareSize / 2 + offsetY; + const flip = ((col + colShift + row + rowShift) % 2 + 2) % 2 !== 0; + const cellKey = `${col},${row}`; + const alpha = cellOpacities.current.get(cellKey); + if (alpha) { + ctx.globalAlpha = alpha; + drawTriangle(cx, cy, squareSize, flip); + ctx.fillStyle = hoverFillColor; + ctx.fill(); + ctx.globalAlpha = 1; + } + drawTriangle(cx, cy, squareSize, flip); + ctx.strokeStyle = borderColor; + ctx.stroke(); + } + } + } else if (shape === 'circle') { + const offsetX = ((gridOffset.current.x % squareSize) + squareSize) % squareSize; + const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize; + const cols = Math.ceil(canvas.width / squareSize) + 3; + const rows = Math.ceil(canvas.height / squareSize) + 3; + + for (let col = -2; col < cols; col++) { + for (let row = -2; row < rows; row++) { + const cx = col * squareSize + squareSize / 2 + offsetX; + const cy = row * squareSize + squareSize / 2 + offsetY; + const cellKey = `${col},${row}`; + const alpha = cellOpacities.current.get(cellKey); + if (alpha) { + ctx.globalAlpha = alpha; + drawCircle(cx, cy, squareSize); + ctx.fillStyle = hoverFillColor; + ctx.fill(); + ctx.globalAlpha = 1; + } + drawCircle(cx, cy, squareSize); + ctx.strokeStyle = borderColor; + ctx.stroke(); + } + } + } else { + const offsetX = ((gridOffset.current.x % squareSize) + squareSize) % squareSize; + const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize; + const cols = Math.ceil(canvas.width / squareSize) + 3; + const rows = Math.ceil(canvas.height / squareSize) + 3; + + for (let col = -2; col < cols; col++) { + for (let row = -2; row < rows; row++) { + const sx = col * squareSize + offsetX; + const sy = row * squareSize + offsetY; + const cellKey = `${col},${row}`; + const alpha = cellOpacities.current.get(cellKey); + if (alpha) { + ctx.globalAlpha = alpha; + ctx.fillStyle = hoverFillColor; + ctx.fillRect(sx, sy, squareSize, squareSize); + ctx.globalAlpha = 1; + } + ctx.strokeStyle = borderColor; + ctx.strokeRect(sx, sy, squareSize, squareSize); + } + } + } + }; + + const updateCellOpacities = () => { + const targets = new Map(); + + if (hoveredSquare.current) { + targets.set(`${hoveredSquare.current.x},${hoveredSquare.current.y}`, 1); + } + + if (hoverTrailAmount > 0) { + for (let i = 0; i < trailCells.current.length; i++) { + const t = trailCells.current[i]; + const key = `${t.x},${t.y}`; + if (!targets.has(key)) { + targets.set(key, (trailCells.current.length - i) / (trailCells.current.length + 1)); + } + } + } + + Array.from(targets.keys()).forEach((key) => { + if (!cellOpacities.current.has(key)) { + cellOpacities.current.set(key, 0); + } + }); + + Array.from(cellOpacities.current.entries()).forEach(([key, opacity]) => { + const target = targets.get(key) || 0; + const next = opacity + (target - opacity) * 0.15; + if (next < 0.005) { + cellOpacities.current.delete(key); + } else { + cellOpacities.current.set(key, next); + } + }); + }; + + const updateAnimation = () => { + const effectiveSpeed = Math.max(speed, 0.1); + const wrapX = isHex ? hexHoriz * 2 : squareSize; + const wrapY = isHex ? hexVert : isTri ? squareSize * 2 : squareSize; + + switch (direction) { + case 'right': + gridOffset.current.x = (gridOffset.current.x - effectiveSpeed + wrapX) % wrapX; + break; + case 'left': + gridOffset.current.x = (gridOffset.current.x + effectiveSpeed + wrapX) % wrapX; + break; + case 'up': + gridOffset.current.y = (gridOffset.current.y + effectiveSpeed + wrapY) % wrapY; + break; + case 'down': + gridOffset.current.y = (gridOffset.current.y - effectiveSpeed + wrapY) % wrapY; + break; + case 'diagonal': + gridOffset.current.x = (gridOffset.current.x - effectiveSpeed + wrapX) % wrapX; + gridOffset.current.y = (gridOffset.current.y - effectiveSpeed + wrapY) % wrapY; + break; + default: + break; + } + + updateCellOpacities(); + drawGrid(); + requestRef.current = requestAnimationFrame(updateAnimation); + }; + + const handleMouseMove = (event: MouseEvent) => { + const rect = canvas.getBoundingClientRect(); + const mouseX = event.clientX - rect.left; + const mouseY = event.clientY - rect.top; + const offsetX = ((gridOffset.current.x % squareSize) + squareSize) % squareSize; + const offsetY = ((gridOffset.current.y % squareSize) + squareSize) % squareSize; + const col = Math.floor((mouseX - offsetX) / squareSize); + const row = Math.floor((mouseY - offsetY) / squareSize); + + if (!hoveredSquare.current || hoveredSquare.current.x !== col || hoveredSquare.current.y !== row) { + if (hoveredSquare.current && hoverTrailAmount > 0) { + trailCells.current.unshift({ ...hoveredSquare.current }); + if (trailCells.current.length > hoverTrailAmount) { + trailCells.current.length = hoverTrailAmount; + } + } + hoveredSquare.current = { x: col, y: row }; + } + }; + + const handleMouseLeave = () => { + if (hoveredSquare.current && hoverTrailAmount > 0) { + trailCells.current.unshift({ ...hoveredSquare.current }); + if (trailCells.current.length > hoverTrailAmount) { + trailCells.current.length = hoverTrailAmount; + } + } + hoveredSquare.current = null; + }; + + canvas.addEventListener('mousemove', handleMouseMove); + canvas.addEventListener('mouseleave', handleMouseLeave); + + requestRef.current = requestAnimationFrame(updateAnimation); + + return () => { + window.removeEventListener('resize', resizeCanvas); + if (requestRef.current) cancelAnimationFrame(requestRef.current); + canvas.removeEventListener('mousemove', handleMouseMove); + canvas.removeEventListener('mouseleave', handleMouseLeave); + }; + }, [direction, speed, borderColor, hoverFillColor, squareSize, shape, hoverTrailAmount]); + + return ( +