refactor: convert frontend from submodule to true monorepo
Convert frontend from Git submodule to a regular monorepo directory for simplified development workflow. Changes: - Remove frontend submodule tracking (mode 160000 gitlink) - Add all frontend source files directly to main repository - Remove frontend/.git directory - Update CLAUDE.md to clarify true monorepo structure - Update Frontend Architecture documentation (React Router v6, Socket.IO, Leaflet, ErrorBoundary) Benefits of Monorepo: - Single git clone for entire project - Unified commit history - Simpler CI/CD pipeline - Easier for new developers - No submodule sync issues - Atomic commits across frontend and backend Frontend Files Added: - All React components (MapView, ErrorBoundary, TaskList, SocialFeed, etc.) - Context providers (AuthContext, SocketContext) - Complete test suite with MSW - Dependencies and configuration files Branch Cleanup: - Using 'main' as default branch (develop deleted) - Frontend no longer has separate Git history 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
143
frontend/src/context/AuthContext.js
Normal file
143
frontend/src/context/AuthContext.js
Normal file
@@ -0,0 +1,143 @@
|
||||
import React, { createContext, useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const AuthContext = createContext();
|
||||
|
||||
const AuthProvider = ({ children }) => {
|
||||
const [auth, setAuth] = useState({
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
loading: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
axios.defaults.headers.common["x-auth-token"] = token;
|
||||
try {
|
||||
const res = await axios.get("/api/auth");
|
||||
setAuth({
|
||||
token,
|
||||
isAuthenticated: true,
|
||||
user: res.data,
|
||||
loading: false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to load user:", error);
|
||||
localStorage.removeItem("token");
|
||||
delete axios.defaults.headers.common["x-auth-token"];
|
||||
setAuth({
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setAuth({
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
const login = async (email, password) => {
|
||||
try {
|
||||
const res = await axios.post("/api/auth/login", { email, password });
|
||||
localStorage.setItem("token", res.data.token);
|
||||
axios.defaults.headers.common["x-auth-token"] = res.data.token;
|
||||
|
||||
try {
|
||||
const userRes = await axios.get("/api/auth");
|
||||
setAuth({
|
||||
token: res.data.token,
|
||||
isAuthenticated: true,
|
||||
user: userRes.data,
|
||||
loading: false,
|
||||
});
|
||||
toast.success("Login successful!");
|
||||
return { success: true };
|
||||
} catch (userError) {
|
||||
console.error("Failed to fetch user after login:", userError);
|
||||
localStorage.removeItem("token");
|
||||
delete axios.defaults.headers.common["x-auth-token"];
|
||||
toast.error("Login succeeded but failed to load user data");
|
||||
return { success: false, error: "Failed to load user data" };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Login error:", error);
|
||||
const errorMessage =
|
||||
error.response?.data?.msg ||
|
||||
error.response?.data?.message ||
|
||||
"Login failed. Please check your credentials.";
|
||||
toast.error(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (name, email, password) => {
|
||||
try {
|
||||
const res = await axios.post("/api/auth/register", {
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
});
|
||||
localStorage.setItem("token", res.data.token);
|
||||
axios.defaults.headers.common["x-auth-token"] = res.data.token;
|
||||
|
||||
try {
|
||||
const userRes = await axios.get("/api/auth");
|
||||
setAuth({
|
||||
token: res.data.token,
|
||||
isAuthenticated: true,
|
||||
user: userRes.data,
|
||||
loading: false,
|
||||
});
|
||||
toast.success("Registration successful! Welcome aboard!");
|
||||
return { success: true };
|
||||
} catch (userError) {
|
||||
console.error("Failed to fetch user after registration:", userError);
|
||||
localStorage.removeItem("token");
|
||||
delete axios.defaults.headers.common["x-auth-token"];
|
||||
toast.error("Registration succeeded but failed to load user data");
|
||||
return { success: false, error: "Failed to load user data" };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
const errorMessage =
|
||||
error.response?.data?.msg ||
|
||||
error.response?.data?.message ||
|
||||
"Registration failed. Please try again.";
|
||||
toast.error(errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem("token");
|
||||
delete axios.defaults.headers.common["x-auth-token"];
|
||||
setAuth({
|
||||
token: null,
|
||||
isAuthenticated: false,
|
||||
user: null,
|
||||
loading: false,
|
||||
});
|
||||
toast.info("You have been logged out");
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ auth, login, register, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthProvider;
|
||||
188
frontend/src/context/SocketContext.js
Normal file
188
frontend/src/context/SocketContext.js
Normal file
@@ -0,0 +1,188 @@
|
||||
import React, { createContext, useContext, useEffect, useState, useCallback } from "react";
|
||||
import { io } from "socket.io-client";
|
||||
import { AuthContext } from "./AuthContext";
|
||||
|
||||
export const SocketContext = createContext();
|
||||
|
||||
/**
|
||||
* SocketProvider manages WebSocket connections and real-time event handling
|
||||
* Automatically reconnects on disconnection and provides event subscription methods
|
||||
*/
|
||||
const SocketProvider = ({ children }) => {
|
||||
const { auth } = useContext(AuthContext);
|
||||
const [socket, setSocket] = useState(null);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
// Initialize socket connection
|
||||
const socketInstance = io("http://localhost:5000", {
|
||||
autoConnect: false,
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1000,
|
||||
reconnectionDelayMax: 5000,
|
||||
timeout: 20000,
|
||||
});
|
||||
|
||||
// Connection event handlers
|
||||
socketInstance.on("connect", () => {
|
||||
console.log("Socket.IO connected:", socketInstance.id);
|
||||
setConnected(true);
|
||||
});
|
||||
|
||||
socketInstance.on("disconnect", (reason) => {
|
||||
console.log("Socket.IO disconnected:", reason);
|
||||
setConnected(false);
|
||||
|
||||
// Automatically reconnect if disconnection was unexpected
|
||||
if (reason === "io server disconnect") {
|
||||
// Server initiated disconnect, reconnect manually
|
||||
socketInstance.connect();
|
||||
}
|
||||
});
|
||||
|
||||
socketInstance.on("connect_error", (error) => {
|
||||
console.error("Socket.IO connection error:", error);
|
||||
setConnected(false);
|
||||
});
|
||||
|
||||
socketInstance.on("reconnect", (attemptNumber) => {
|
||||
console.log("Socket.IO reconnected after", attemptNumber, "attempts");
|
||||
setConnected(true);
|
||||
});
|
||||
|
||||
socketInstance.on("reconnect_attempt", (attemptNumber) => {
|
||||
console.log("Socket.IO reconnection attempt", attemptNumber);
|
||||
});
|
||||
|
||||
socketInstance.on("reconnect_error", (error) => {
|
||||
console.error("Socket.IO reconnection error:", error);
|
||||
});
|
||||
|
||||
socketInstance.on("reconnect_failed", () => {
|
||||
console.error("Socket.IO reconnection failed");
|
||||
});
|
||||
|
||||
// Generic notification handler
|
||||
socketInstance.on("notification", (data) => {
|
||||
console.log("Received notification:", data);
|
||||
setNotifications((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: Date.now(),
|
||||
timestamp: new Date(),
|
||||
...data,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
setSocket(socketInstance);
|
||||
|
||||
// Connect socket if user is authenticated
|
||||
if (auth.isAuthenticated) {
|
||||
socketInstance.connect();
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
return () => {
|
||||
socketInstance.disconnect();
|
||||
socketInstance.removeAllListeners();
|
||||
};
|
||||
}, [auth.isAuthenticated]);
|
||||
|
||||
// Join a specific event room
|
||||
const joinEvent = useCallback(
|
||||
(eventId) => {
|
||||
if (socket && connected) {
|
||||
console.log("Joining event room:", eventId);
|
||||
socket.emit("joinEvent", eventId);
|
||||
}
|
||||
},
|
||||
[socket, connected]
|
||||
);
|
||||
|
||||
// Leave a specific event room
|
||||
const leaveEvent = useCallback(
|
||||
(eventId) => {
|
||||
if (socket && connected) {
|
||||
console.log("Leaving event room:", eventId);
|
||||
socket.emit("leaveEvent", eventId);
|
||||
}
|
||||
},
|
||||
[socket, connected]
|
||||
);
|
||||
|
||||
// Subscribe to a specific event
|
||||
const on = useCallback(
|
||||
(event, callback) => {
|
||||
if (socket) {
|
||||
socket.on(event, callback);
|
||||
}
|
||||
},
|
||||
[socket]
|
||||
);
|
||||
|
||||
// Unsubscribe from a specific event
|
||||
const off = useCallback(
|
||||
(event, callback) => {
|
||||
if (socket) {
|
||||
socket.off(event, callback);
|
||||
}
|
||||
},
|
||||
[socket]
|
||||
);
|
||||
|
||||
// Emit an event
|
||||
const emit = useCallback(
|
||||
(event, data) => {
|
||||
if (socket && connected) {
|
||||
socket.emit(event, data);
|
||||
}
|
||||
},
|
||||
[socket, connected]
|
||||
);
|
||||
|
||||
// Clear a notification
|
||||
const clearNotification = useCallback((notificationId) => {
|
||||
setNotifications((prev) =>
|
||||
prev.filter((notification) => notification.id !== notificationId)
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Clear all notifications
|
||||
const clearAllNotifications = useCallback(() => {
|
||||
setNotifications([]);
|
||||
}, []);
|
||||
|
||||
const value = {
|
||||
socket,
|
||||
connected,
|
||||
notifications,
|
||||
joinEvent,
|
||||
leaveEvent,
|
||||
on,
|
||||
off,
|
||||
emit,
|
||||
clearNotification,
|
||||
clearAllNotifications,
|
||||
};
|
||||
|
||||
return (
|
||||
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocketProvider;
|
||||
|
||||
/**
|
||||
* Custom hook to use socket context
|
||||
* @returns {Object} Socket context value
|
||||
*/
|
||||
export const useSocket = () => {
|
||||
const context = useContext(SocketContext);
|
||||
if (!context) {
|
||||
throw new Error("useSocket must be used within a SocketProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user