Skip to content

Commit 967cd7b

Browse files
authored
Merge pull request #145 from UNLV-CS472-672/whiteboard_canvas_base_MVP
Abdulrahman 6th PR: Whiteboard Drawing Canvas + LessonConnect Landing Page (MVP)
2 parents 94d0176 + fc5f158 commit 967cd7b

File tree

8 files changed

+598
-5
lines changed

8 files changed

+598
-5
lines changed

frontend/src/App.jsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ import Calendar from "./Components/Calendar.jsx";
2525
import Inbox from "./Components/Inbox.jsx";
2626
import TutorView from "./Components/TutorView.jsx";
2727
import AssignmentPage from "./Components/AssignmentPage.jsx";
28+
import WhiteboardCanvas from "./Components/WhiteboardCanvas.jsx";
29+
import LandingPage from "./Components/LandingPage.jsx";
30+
2831
import Settings from "./Components/Settings.jsx";
2932

33+
3034
function App() {
3135
const location = useLocation();
3236

@@ -56,7 +60,7 @@ function App() {
5660
<Route path="/faqs" element={<FAQS />} />
5761
<Route path="/findTutor" element={<FindTutor />} />
5862
<Route path="/chat" element={<Chat />} />
59-
<Route path="/videocall" element={<VideoCall />} />
63+
<Route path="/WhiteboardCanvas" element={<WhiteboardCanvas/>} />
6064

6165
{/* Student Routes with Layout */}
6266
<Route path="/student/view" element={<StudentLayout><StudentView /></StudentLayout>} />
@@ -75,7 +79,8 @@ function App() {
7579
<Route path="/student/assignment" element={<StudentLayout><AssignmentPage /></StudentLayout>} />
7680
<Route path="/student/settings" element={<StudentLayout><Settings /></StudentLayout>} />
7781
<Route path="/student/inbox" element={<StudentLayout><Inbox /></StudentLayout>} />
78-
82+
<Route path="/student/WhiteboardCanvas" element={<StudentLayout><WhiteboardCanvas /></StudentLayout>} />
83+
<Route path="/student/LandingPage" element={<StudentLayout><LandingPage /></StudentLayout>} />
7984

8085
{/* Tutor Routes with Layout */}
8186
<Route path="/tutor/view" element={<StudentLayout><TutorView /></StudentLayout>} />
@@ -94,6 +99,8 @@ function App() {
9499
<Route path="/tutor/settings" element={<StudentLayout><Settings /></StudentLayout>} />
95100
<Route path="/tutor/calendar" element={<StudentLayout><Calendar /></StudentLayout>} />
96101
<Route path="/tutor/inbox" element={<StudentLayout><Inbox /></StudentLayout>} />
102+
<Route path="/tutor/WhiteboardCanvas" element={<StudentLayout><WhiteboardCanvas /></StudentLayout>} />
103+
<Route path="/tutor/LandingPage" element={<StudentLayout><LandingPage /></StudentLayout>} />
97104
</Routes>
98105
{(location.pathname !== "/login" && location.pathname !== "/dateofbirth" && location.pathname !== "/SignUp") && <Footer />}
99106
</div>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { useState } from 'react';
2+
import { useNavigate, useLocation } from "react-router-dom"; // added useLocation
3+
import '../Styles/LandingPage.css';
4+
5+
export default function LandingPage() {
6+
const [hovered, setHovered] = useState(false);
7+
const navigate = useNavigate();
8+
const location = useLocation(); // get current path
9+
10+
const handleClick = () => {
11+
const currentPath = location.pathname.toLowerCase();
12+
13+
if (currentPath.startsWith('/student')) {
14+
navigate("/student/WhiteboardCanvas");
15+
} else if (currentPath.startsWith('/tutor')) {
16+
navigate("/tutor/WhiteboardCanvas");
17+
} else {
18+
console.warn("Unrecognized path:", currentPath);
19+
}
20+
};
21+
22+
return (
23+
<div className="landing-page">
24+
<div className="landing-page__background-gradient" />
25+
<div className="landing-page__content-overlay">
26+
<h1 className="landing-page__main-heading">
27+
Welcome to <span className="landing-page__brand-name">LessonConnect</span>
28+
</h1>
29+
<p className="landing-page__tagline">
30+
Experience the next-generation interactive whiteboard.<br />
31+
Teach, learn, and collaborate with anyone, anywhere.
32+
</p>
33+
<button
34+
className={`landing-page__cta-button ${
35+
hovered ? 'landing-page__cta-button--hovered' : ''
36+
}`}
37+
onMouseEnter={() => setHovered(true)}
38+
onMouseLeave={() => setHovered(false)}
39+
onClick={handleClick}
40+
>
41+
Launch Whiteboard
42+
</button>
43+
</div>
44+
</div>
45+
);
46+
}

frontend/src/Components/Services.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export default function Services({ darkMode }) {
4242
<ul className="list-unstyled">
4343
<li><a href="#link">Games</a></li>
4444
<li><a href="#">Scheduling</a></li>
45-
<li><a href="#">Whiteboard</a></li>
45+
<li><a href="/LandingPage">Whiteboard</a></li>
4646
<li><a href="#">Find A Tutor</a></li>
4747
<li><a href="#">Find a Lesson</a></li>
4848
</ul>

frontend/src/Components/StudentView.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const dashboardOptions = [
1313
{ text: "My Calendar", img: "/assets/images/calendar_icon.png", path:"/student/calendar" },
1414
{ text: "My Assignments", img: "/assets/images/assignment_icon.png", path:"/student/assignment"},
1515
{ text: "My Messages", img: "/assets/images/messages_icon.png", path:"/student/messages" },
16-
{ text: "My Whiteboard", img: "/assets/images/whiteboard_icon.png", path:"/student/whiteboard" },
16+
{ text: "My Whiteboard", img: "/assets/images/whiteboard_icon.png", path:"/student/LandingPage" },
1717
];
1818

1919
const externalLinks = [

frontend/src/Components/TutorView.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const dashboardOptions = [
1515
];
1616

1717
const sidebarOptions = [
18-
{ text: "Resources", path:"/tutor/resources" },
18+
{ text: "Whiteboard", path:"/tutor/LandingPage" },
1919
{ text: "Settings", path:"/tutor/settings" },
2020
{ text: "Calendar", path:"/tutor/calendar"},
2121
{ text: "Student Requests", path:"/tutor/requests" },
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
// WhiteboardCanvas.jsx
2+
import { useRef, useState, useEffect } from "react";
3+
import "../Styles/WhiteboardCanvas.css";
4+
5+
export default function WhiteboardCanvas() {
6+
const canvasRef = useRef(null);
7+
const contextRef = useRef(null);
8+
9+
const [isDrawing, setIsDrawing] = useState(false);
10+
const [lineColor, setLineColor] = useState("#000000");
11+
const [lineWidth, setLineWidth] = useState(3);
12+
13+
// 1) NEW: Undo stack state
14+
const [undoStack, setUndoStack] = useState([]);
15+
16+
// Example color palette
17+
const colorOptions = [
18+
"#000000", "#7F7F7F", "#BFBFBF", "#FFFFFF",
19+
"#FF0000", "#FF7F00", "#FFFF00", "#7FFF00",
20+
"#00FF00", "#00FF7F", "#00FFFF", "#007FFF",
21+
"#0000FF", "#7F00FF", "#FF00FF", "#FF007F"
22+
];
23+
24+
/**
25+
* Set up the canvas size and context for drawing
26+
*/
27+
const resizeCanvas = () => {
28+
const canvas = canvasRef.current;
29+
if (!canvas) return;
30+
31+
const scale = window.devicePixelRatio || 1;
32+
canvas.width = canvas.clientWidth * scale;
33+
canvas.height = canvas.clientHeight * scale;
34+
35+
const context = canvas.getContext("2d");
36+
context.scale(scale, scale);
37+
context.lineCap = "round";
38+
context.lineJoin = "round";
39+
40+
contextRef.current = context;
41+
};
42+
43+
// Resize only on mount or window resize (not on color/width changes)
44+
useEffect(() => {
45+
function handleResize() {
46+
resizeCanvas();
47+
}
48+
// Initial sizing
49+
resizeCanvas();
50+
51+
// If you want dynamic resizing
52+
window.addEventListener("resize", handleResize);
53+
return () => window.removeEventListener("resize", handleResize);
54+
}, []);
55+
56+
// Update stroke style on color/width changes
57+
useEffect(() => {
58+
if (contextRef.current) {
59+
contextRef.current.strokeStyle = lineColor;
60+
contextRef.current.lineWidth = lineWidth;
61+
}
62+
}, [lineColor, lineWidth]);
63+
64+
// 2) NEW: After the canvas is set up, capture its initial state for undo
65+
useEffect(() => {
66+
if (canvasRef.current && contextRef.current) {
67+
const canvas = canvasRef.current;
68+
const ctx = contextRef.current;
69+
const initialData = ctx.getImageData(0, 0, canvas.width, canvas.height);
70+
setUndoStack([initialData]);
71+
}
72+
}, []);
73+
74+
// 3) NEW: Helper function to push the current canvas to undo stack
75+
const pushToUndoStack = () => {
76+
if (!canvasRef.current || !contextRef.current) return;
77+
const canvas = canvasRef.current;
78+
const data = contextRef.current.getImageData(0, 0, canvas.width, canvas.height);
79+
setUndoStack((prevStack) => [...prevStack, data]);
80+
};
81+
82+
// 4) NEW: Undo logic
83+
const undoLastAction = () => {
84+
if (undoStack.length > 1) {
85+
const newStack = [...undoStack];
86+
newStack.pop(); // Remove current state
87+
const previous = newStack[newStack.length - 1];
88+
contextRef.current.putImageData(previous, 0, 0);
89+
setUndoStack(newStack);
90+
} else {
91+
// If nothing left, just clear
92+
clearCanvas();
93+
}
94+
};
95+
96+
// Mouse event handlers
97+
const startDrawing = (e) => {
98+
const { offsetX, offsetY } = e.nativeEvent;
99+
contextRef.current.beginPath();
100+
contextRef.current.moveTo(offsetX, offsetY);
101+
setIsDrawing(true);
102+
};
103+
104+
const draw = (e) => {
105+
if (!isDrawing) return;
106+
const { offsetX, offsetY } = e.nativeEvent;
107+
contextRef.current.lineTo(offsetX, offsetY);
108+
contextRef.current.stroke();
109+
};
110+
111+
const stopDrawing = () => {
112+
if (isDrawing) {
113+
contextRef.current.closePath();
114+
setIsDrawing(false);
115+
// 5) NEW: After each stroke, capture the final canvas state
116+
pushToUndoStack();
117+
}
118+
};
119+
120+
// Utility buttons
121+
const clearCanvas = () => {
122+
const canvas = canvasRef.current;
123+
contextRef.current.clearRect(0, 0, canvas.width, canvas.height);
124+
};
125+
126+
const downloadCanvas = () => {
127+
const canvas = canvasRef.current;
128+
const link = document.createElement("a");
129+
link.download = "lessonconnect_drawing.png";
130+
link.href = canvas.toDataURL();
131+
link.click();
132+
};
133+
134+
// Placeholder for extra tools
135+
const handleToolClick = (toolName) => {
136+
alert(`Tool ${toolName} clicked! (Feature to be added)`);
137+
};
138+
139+
// 6) NEW: Listen for Eraser button click => switch to eraser mode
140+
useEffect(() => {
141+
const eraserButton = document.querySelector(".tool-btn[title='Eraser']");
142+
if (!eraserButton) return;
143+
144+
const handleEraserClick = () => {
145+
if (contextRef.current) {
146+
contextRef.current.globalCompositeOperation = "destination-out";
147+
// Optionally adjust eraser size:
148+
setLineWidth(20);
149+
}
150+
};
151+
152+
eraserButton.addEventListener("click", handleEraserClick);
153+
return () => {
154+
eraserButton.removeEventListener("click", handleEraserClick);
155+
};
156+
}, []);
157+
158+
// 7) NEW: Listen for Pencil button click => switch back to normal drawing
159+
useEffect(() => {
160+
const pencilButton = document.querySelector(".tool-btn[title='Pencil']");
161+
if (!pencilButton) return;
162+
163+
const handlePencilClick = () => {
164+
if (contextRef.current) {
165+
contextRef.current.globalCompositeOperation = "source-over";
166+
// Reset brush size if desired:
167+
setLineWidth(3);
168+
}
169+
};
170+
171+
pencilButton.addEventListener("click", handlePencilClick);
172+
return () => {
173+
pencilButton.removeEventListener("click", handlePencilClick);
174+
};
175+
}, []);
176+
177+
return (
178+
// Unique parent class to scope styling:
179+
<div className="whiteboard-container">
180+
<div className="whiteboard-canvas-container">
181+
{/* Top bar */}
182+
<div className="whiteboard-topbar glass-card">
183+
<div className="whiteboard-title">Whiteboard</div>
184+
<div className="whiteboard-tools">
185+
<button
186+
className="tool-btn neon-hover"
187+
title="Cursor"
188+
onClick={() => handleToolClick("Cursor")}
189+
>
190+
<i className="fas fa-mouse-pointer"></i>
191+
</button>
192+
<button
193+
className="tool-btn neon-hover"
194+
title="Pencil"
195+
onClick={() => handleToolClick("Pencil")}
196+
>
197+
<i className="fas fa-pencil-alt"></i>
198+
</button>
199+
<button
200+
className="tool-btn neon-hover"
201+
title="Rectangle"
202+
onClick={() => handleToolClick("Rectangle")}
203+
>
204+
<i className="far fa-square"></i>
205+
</button>
206+
<button
207+
className="tool-btn neon-hover"
208+
title="Circle"
209+
onClick={() => handleToolClick("Circle")}
210+
>
211+
<i className="far fa-circle"></i>
212+
</button>
213+
<button
214+
className="tool-btn neon-hover"
215+
title="Eraser"
216+
onClick={() => handleToolClick("Eraser")}
217+
>
218+
<i className="fas fa-eraser"></i>
219+
</button>
220+
</div>
221+
</div>
222+
223+
{/* Secondary toolbar */}
224+
<div className="toolbar glass-card">
225+
<label className="color-label">Color:</label>
226+
{colorOptions.map((color) => (
227+
<button
228+
key={color}
229+
className="color-button"
230+
style={{ backgroundColor: color }}
231+
onClick={() => setLineColor(color)}
232+
/>
233+
))}
234+
<label className="width-label">
235+
Brush:
236+
<input
237+
type="range"
238+
min="1"
239+
max="20"
240+
value={lineWidth}
241+
onChange={(e) => setLineWidth(e.target.value)}
242+
/>
243+
</label>
244+
<button className="action-button neon-hover" onClick={clearCanvas}>
245+
Clear
246+
</button>
247+
248+
{/* 8) NEW: Undo button */}
249+
<button className="action-button neon-hover" onClick={undoLastAction}>
250+
Undo
251+
</button>
252+
253+
<button className="action-button neon-hover" onClick={downloadCanvas}>
254+
Download
255+
</button>
256+
</div>
257+
258+
{/* Canvas */}
259+
<div
260+
className="canvas-container glass-card"
261+
onMouseDown={startDrawing}
262+
onMouseMove={draw}
263+
onMouseUp={stopDrawing}
264+
onMouseLeave={stopDrawing}
265+
>
266+
<canvas ref={canvasRef} className="drawing-canvas" />
267+
</div>
268+
</div>
269+
</div>
270+
);
271+
}

0 commit comments

Comments
 (0)