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:
William Valentin
2025-11-01 11:01:06 -07:00
parent 223dbb14b7
commit 2df5a303ed
38 changed files with 25312 additions and 3 deletions

View 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;

View 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;
};