Skip to content
Open
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
6 changes: 6 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import GitHubCorner from "./components/GitHubCorner"
import Toast from "./components/toast/Toast"
import EditorPage from "./pages/EditorPage"
import HomePage from "./pages/HomePage"
import Login from "./auth/Login"
import Register from "./auth/Register"
import Profile from "./pages/profile/Profile"

const App = () => {
return (
<>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/profile" element={<Profile />} />
<Route path="/" element={<HomePage />} />
<Route path="/editor/:roomId" element={<EditorPage />} />
</Routes>
Expand Down
96 changes: 96 additions & 0 deletions client/src/auth/Login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import logo from "@/assets/logo.svg"
import { useAppContext } from "@/context/AppContext";
import axios from "axios";
import { useState } from "react";
import { toast } from "react-hot-toast"
import { useNavigate } from "react-router-dom";

function Login() {
const navigate = useNavigate();
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:3000"

const {setUserInfo} = useAppContext();

const [user, setUser] = useState({
username: "",
password: ""
});

const validateForm = () => {
if (user.username.trim().length === 0) {
toast.error("Enter your username");
}
if (user.password.trim().length === 0) {
toast.error("Enter your password");
}
return user.username.trim().length > 0 &&
user.password.trim().length > 0;
}

const submitLogin = async (e: React.FormEvent<HTMLFormElement>, user: {username: string, password: string}) => {
e.preventDefault();
validateForm();

try {
const res = await axios.post(BACKEND_URL + "/api/users/login", user)

if(res.status === 201) {
toast.success("Login successful");
setUserInfo({username: user.username, token: res.data.token, email: res.data.email});
localStorage.setItem("userInfo", JSON.stringify({username: user.username, token: res.data.token, email: res.data.email}));
navigate("/profile");
}
} catch (error: unknown) {
if(error instanceof axios.AxiosError && error.response && error.response.status === 400) {
const errorData = error.response.data as { message: string };
toast.error(errorData.message);
} else {
toast.error("Login failed. Please try again.");
}
}
}

return (
<div className='flex flex-col items-center justify-center h-screen'>
<div className="flex w-full justify-center sm:w-1/2 max-w-[500px] sm:pl-4">
<img src={logo} alt="Logo" className="w-full"/>
</div>
<form className="flex w-full max-w-[500px] flex-col items-center justify-center gap-4 p-4 sm:w-[500px] sm:p-8"
onSubmit={(e) => {
submitLogin(e, user);
}}
>
<h2 className="mb-4 text-2xl font-bold">Login</h2>
<input
type="text"
name="username"
placeholder="Username"
value={user.username}
onChange={(e) => setUser({ ...user, username: e.target.value })}
className="w-full rounded-md border border-gray-500 bg-darkHover px-3 py-3 focus:outline-none"
/>
<input
type="password"
name="password"
placeholder="Password"
value={user.password}
onChange={(e) => setUser({ ...user, password: e.target.value })}
className="w-full rounded-md border border-gray-500 bg-darkHover px-3 py-3 focus:outline-none"
/>
<button
type="submit"
className="mt-2 w-full rounded-md bg-primary px-8 py-3 text-lg font-semibold text-black"
>
Login
</button>
</form>
<div className="mt-4">
<a href="/register" className="text-sm text-gray-500 hover:underline">
Don't have an account? Register here.
</a>
</div>
</div>
)
}

export default Login
124 changes: 124 additions & 0 deletions client/src/auth/Register.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import logo from "@/assets/logo.svg"
import { useState } from "react";
import { IoEye } from "react-icons/io5";
import { IoEyeOff } from "react-icons/io5";
import { toast } from "react-hot-toast";
import axios, { AxiosError } from "axios";
import { useNavigate } from "react-router-dom";


function Register() {
const navigate = useNavigate();
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:3000"

const [user, setUser] = useState({
username: "",
email: "",
password: ""
});
const [showPassword, setShowPassword] = useState(false);

const validateForm = () => {
if (user.username.trim().length === 0) {
toast.error("Enter your username");
}
if (user.email.trim().length === 0) {
toast.error("Enter your email");
}
if (user.password.trim().length === 0) {
toast.error("Enter your password");
}
if (user.password.trim().length < 6) {
toast.error("Password must be at least 6 characters long");
}
return user.username.trim().length > 0 &&
user.email.trim().length > 0 &&
user.password.trim().length >= 6;
}

const submitRegisteration = async (e: React.FormEvent<HTMLFormElement>, user: {username: string, email: string, password: string}) => {
e.preventDefault();
validateForm();

try {
const res = await axios.post(BACKEND_URL + "/api/users/register", user)

if(res.status === 201) {
toast.success("Registration successful");
navigate("/login");
}
} catch (error: unknown) {
if(error instanceof AxiosError && error.response && error.response.status === 400) {
const errorData = error.response.data as { message: string };
toast.error(errorData.message);
} else {
toast.error("Registration failed. Please try again.");
}
}
}

return (
<div className='flex flex-col items-center justify-center h-screen'>
<div className="flex w-full justify-center sm:w-1/2 max-w-[500px] sm:pl-4">
<img src={logo} alt="Logo" className="w-full"/>
</div>
<form className="flex w-full max-w-[500px] flex-col items-center justify-center gap-4 p-4 sm:w-[500px] sm:p-8"
onSubmit={(e) => {
submitRegisteration(e, user);
}}
>
<h2 className="mb-4 text-2xl font-bold">Register</h2>
<input
type="text"
name="username"
placeholder="Username"
value={user.username}
onChange={(e) => setUser({ ...user, username: e.target.value })}
className="w-full rounded-md border border-gray-500 bg-darkHover px-3 py-3 focus:outline-none"
/>
<input
type="email"
name="email"
placeholder="Email"
value={user.email}
onChange={(e) => setUser({ ...user, email: e.target.value })}
className="w-full rounded-md border border-gray-500 bg-darkHover px-3 py-3 focus:outline-none"
/>
<div className="w-full relative">
<input
type={showPassword ? "text" : "password"}
name="password"
placeholder="Password"
value={user.password}
onChange={(e) => setUser({ ...user, password: e.target.value })}
className="w-full rounded-md border border-gray-500 bg-darkHover px-3 py-3 focus:outline-none"
/>
{
showPassword ?
<IoEyeOff
className="absolute right-3 top-4 cursor-pointer text-gray-500"
onClick={() => setShowPassword(!showPassword)}
/> :
<IoEye
className="absolute right-3 top-4 cursor-pointer text-gray-500"
onClick={() => setShowPassword(!showPassword)}
/>
}
</div>
<button
type="submit"
className="mt-2 w-full rounded-md bg-primary px-8 py-3 text-lg font-semibold text-black"
>
Register
</button>
</form>
<div className="mt-4">
<a href="/login" className="text-sm text-gray-500 hover:underline">
Already have an account? Login here.
</a>
</div>
</div>
)
}

export default Register
78 changes: 78 additions & 0 deletions client/src/components/cards/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useAppContext } from "@/context/AppContext";
import { useSocket } from "@/context/SocketContext";
import { MouseEvent, useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { USER_STATUS } from "@/types/user"
import { SocketEvent } from "@/types/socket";
import toast from "react-hot-toast";


function Card({data}: {data: {name: string, roomId: string} }) {

const userInfo = localStorage.getItem("userInfo") ? JSON.parse(localStorage.getItem("userInfo") || "") : null;

const location = useLocation()
const { currentUser, setCurrentUser, status, setStatus } = useAppContext()
const { socket } = useSocket()

const navigate = useNavigate()

const joinRoom = (e: MouseEvent<HTMLButtonElement>) => {

setCurrentUser({ ...currentUser, roomId: data.roomId })
setCurrentUser({ ...currentUser, username: userInfo.name })

e.preventDefault()
if (status === USER_STATUS.ATTEMPTING_JOIN) return

toast.loading("Joining room...")
setStatus(USER_STATUS.ATTEMPTING_JOIN)
socket.emit(SocketEvent.JOIN_REQUEST, currentUser)
}

useEffect(() => {
if (status === USER_STATUS.DISCONNECTED && !socket.connected) {
socket.connect()
return
}

const isRedirect = sessionStorage.getItem("redirect") || false

if (status === USER_STATUS.JOINED && !isRedirect) {
const username = currentUser.username
sessionStorage.setItem("redirect", "true")
navigate(`/editor/${data.roomId}`, {
state: {
username,
},
})
} else if (status === USER_STATUS.JOINED && isRedirect) {
sessionStorage.removeItem("redirect")
setStatus(USER_STATUS.DISCONNECTED)
socket.disconnect()
socket.connect()
}
}, [currentUser, location.state?.redirect, navigate, setStatus, socket, status])


return (
<div
className=''
>
<img src="https://plus.unsplash.com/premium_photo-1661877737564-3dfd7282efcb?w=600&auto=format&fit=crop&q=60&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8Y29kaW5nfGVufDB8fDB8fHww" alt="Card Image"
className="w-full h-48 object-cover rounded-md"

/>
<h3 className="mt-2 text-lg font-semibold text-white">{data.name}</h3>
<p>Room ID : {data.roomId}</p>
<a href='/'>
<button
className="mt-2 w-full rounded-md bg-primary px-5 py-2 text-lg font-semibold text-black"
onClick={joinRoom}
>Join Room</button>
</a>
</div>
)
}

export default Card
5 changes: 4 additions & 1 deletion client/src/context/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
AppContext as AppContextType,
DrawingData,
} from "@/types/app"
import { RemoteUser, USER_STATUS, User } from "@/types/user"
import { RemoteUser, USER_STATUS, User, UserInfo } from "@/types/user"
import { ReactNode, createContext, useContext, useState } from "react"

const AppContext = createContext<AppContextType | null>(null)
Expand All @@ -25,6 +25,7 @@ function AppContextProvider({ children }: { children: ReactNode }) {
username: "",
roomId: "",
})
const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
const [activityState, setActivityState] = useState<ACTIVITY_STATE>(
ACTIVITY_STATE.CODING,
)
Expand All @@ -43,6 +44,8 @@ function AppContextProvider({ children }: { children: ReactNode }) {
setActivityState,
drawingData,
setDrawingData,
userInfo,
setUserInfo,
}}
>
{children}
Expand Down
Loading