Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 334 additions & 0 deletions client/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@tailwindcss/vite": "^4.1.14",
"axios": "^1.12.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"jspdf": "^3.0.3",
"lucide-react": "^0.545.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.4",
"socket.io-client": "^4.8.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
Expand Down
27 changes: 23 additions & 4 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
import { Canvas } from "./components/Canvas";
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
import {Canvas} from "./components/Canvas";
import Login from "./pages/Login";
import Register from "./pages/Register";

function App() {
const token = localStorage.getItem("token");

return (
<><Canvas/></>
);
return (
<Router>
<Routes>
{/* Redirect to Canvas if already logged in, otherwise to Login */}
<Route path="/" element={<Canvas />} />

{/* Auth Pages */}
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />

{/* Protected Canvas Route */}
<Route
path="/canvas"
element={<Canvas />}
/>
</Routes>
</Router>
);
}

export default App;
9 changes: 9 additions & 0 deletions client/src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import axios from "axios";

const API = axios.create({
baseURL: "http://localhost:3000/api",
});

export const registerUser = (data) => API.post("/auth/signup", data);
export const loginUser = (data) => API.post("/auth/login", data);

10 changes: 10 additions & 0 deletions client/src/components/AuthLayout.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default function AuthLayout({ children, title }) {
return (
<div className="flex items-center justify-center h-screen bg-canvas relative overflow-hidden">
<div className="bg-white/90 border border-gray-300 rounded-2xl shadow-2xl p-8 w-[90%] sm:w-[400px] text-center backdrop-blur-sm">
<h1 className="text-2xl font-bold mb-6 text-gray-800">{title}</h1>
{children}
</div>
</div>
);
}
82 changes: 82 additions & 0 deletions client/src/components/Canvas.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,33 @@ export const Canvas = () => {
const [socket, setSocket] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);

const [isLoggedIn, setIsLoggedIn] = useState(
!!localStorage.getItem("token")
);

const handleLogout = async () => {
const token = localStorage.getItem("token");
if (!token) return;
try {
const res = await fetch("http://localhost:3000/api/auth/logout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
});
const data = await res.json();
if (res.ok) {
localStorage.removeItem("token");
setIsLoggedIn(false);
toast.success("Logged out successfully!");
} else {
toast.error(data.message);
}
} catch (err) {
console.error(err);
toast.error("Logout failed!");
}
};

useEffect(() => {
const s = io("http://localhost:3000");
setSocket(s);
Expand Down Expand Up @@ -181,6 +208,61 @@ export const Canvas = () => {

return (
<div className="relative w-full h-screen overflow-hidden bg-canvas">

{/* 🔹 Login / Logout buttons */}
<div className="fixed top-4 left-4 z-[9999]">
{isLoggedIn ? (
// Logout button if logged in
<button
onClick={handleLogout}
className="bg-red-600 text-white px-4 py-2 rounded-lg shadow hover:bg-red-700"
>
Logout
</button>
) : (
<>
{/* Desktop view: two separate buttons */}
<div className="hidden sm:flex gap-3">
<button
onClick={() => window.location.href = "/login"}
className="bg-blue-600 text-white px-4 py-2 rounded-lg shadow hover:bg-blue-700"
>
Sign In
</button>
<button
onClick={() => window.location.href = "/register"}
className="bg-blue-600 text-white px-4 py-2 rounded-lg shadow hover:bg-blue-700"
>
Sign Up
</button>
</div>

{/* Mobile view: dropdown */}
<div className="sm:hidden relative">
<details className="bg-blue-600 text-white px-4 py-2 rounded-lg shadow cursor-pointer select-none">
<summary className="outline-none list-none">Menu ☰</summary>
<div className="absolute left-0 mt-2 w-32 bg-white text-black rounded-lg shadow-lg border">
<button
onClick={() => window.location.href = "/login"}
className="block w-full text-left px-4 py-2 hover:bg-gray-100"
>
Sign In
</button>
<button
onClick={() => window.location.href = "/register"}
className="block w-full text-left px-4 py-2 hover:bg-gray-100"
>
Sign Up
</button>
</div>
</details>
</div>
</>
)}
</div>



<Toolbar
activeTool={activeTool}
onToolChange={handleToolChange}
Expand Down
14 changes: 14 additions & 0 deletions client/src/components/InputField.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default function InputField({ label, type, value, onChange }) {
return (
<div className="flex flex-col gap-1 w-full">
<label className="text-sm font-medium text-gray-700">{label}</label>
<input
type={type}
value={value}
onChange={onChange}
required
className="border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
);
}
87 changes: 87 additions & 0 deletions client/src/pages/Login.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";

export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const navigate = useNavigate();

const handleLogin = async (e) => {
e.preventDefault();
setLoading(true);
try {
const res = await fetch("http://localhost:3000/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
console.log("Response status:", res.status);
console.log("Response data:", data);

if (res.ok) {
localStorage.setItem("token", data.token);
navigate("/canvas"); // redirect after login
} else {
alert(data.message || "Invalid credentials!");
}
} catch (err) {
console.error(err);
alert("Login failed!");
}
setLoading(false);
};

return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-gray-100 to-gray-300 px-4">
<div className="w-full max-w-md bg-white rounded-2xl shadow-lg p-8 sm:p-10">
<h1 className="text-3xl font-bold text-center text-blue-700 mb-6">
Welcome Back
</h1>
<p className="text-center text-gray-500 mb-8">
Please login to continue to Collab Canvas
</p>

<form onSubmit={handleLogin} className="flex flex-col gap-5">
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition"
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition"
required
/>
<button
type="submit"
disabled={loading}
className={`bg-blue-600 text-white py-3 rounded-lg shadow hover:bg-blue-700 transition font-semibold ${loading ? 'opacity-70 cursor-not-allowed' : ''}`}
>
{loading ? "Logging in..." : "Login"}
</button>
</form>

<div className="mt-6 text-center">
<p className="text-gray-500">
Don’t have an account?{" "}
<span
onClick={() => navigate("/register")}
className="text-blue-600 font-medium cursor-pointer hover:underline"
>
Register
</span>
</p>
</div>
</div>
</div>

);
}
85 changes: 85 additions & 0 deletions client/src/pages/Register.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";

export default function Register() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const navigate = useNavigate();

const handleRegister = async (e) => {
e.preventDefault();
setLoading(true);
try {
const res = await fetch("http://localhost:3000/api/auth/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();

if (res.ok) {
alert("Registration successful! Please login.");
navigate("/login"); // redirect to login after registration
} else {
alert(data.message || "Registration failed!");
}
} catch (err) {
console.error(err);
alert("Registration failed!");
}
setLoading(false);
};

return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-gray-100 to-gray-300 px-4">
<div className="w-full max-w-md bg-white rounded-2xl shadow-lg p-8 sm:p-10">
<h1 className="text-3xl font-bold text-center text-blue-700 mb-6">
Create Account
</h1>
<p className="text-center text-gray-500 mb-8">
Sign up to start collaborating on Collab Canvas
</p>

<form onSubmit={handleRegister} className="flex flex-col gap-5">
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition"
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="border border-gray-300 rounded-lg px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:border-transparent transition"
required
/>
<button
type="submit"
disabled={loading}
className={`bg-blue-600 text-white py-3 rounded-lg shadow hover:bg-blue-700 transition font-semibold ${loading ? 'opacity-70 cursor-not-allowed' : ''}`}
>
{loading ? "Registering..." : "Register"}
</button>
</form>

<div className="mt-6 text-center">
<p className="text-gray-500">
Already have an account?{" "}
<span
onClick={() => navigate("/login")}
className="text-blue-600 font-medium cursor-pointer hover:underline"
>
Login
</span>
</p>
</div>
</div>
</div>

);
}
7 changes: 4 additions & 3 deletions server/.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PORT=5000
MONGO_URI=your_mongodb_connection_string
CORS_ORIGIN=http://localhost:3000
PORT=3000
MONGO_URI=your_mongodb_connection_string_here
CORS_ORIGIN=http://localhost:5173
JWT_SECRET=your_jwt_secret_here
Loading
Loading