Initial Commit

This commit is contained in:
ExilProductions
2026-01-14 17:52:35 +01:00
parent 2d3b97c5db
commit cf066ef305
28 changed files with 1922 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { LoginModal } from '../components/LoginModal';
interface User {
id: number;
github_id: string;
username: string;
display_name: string;
avatar_url: string;
email: string;
}
interface AuthContextType {
user: User | null;
login: () => void;
logout: () => Promise<void>;
loading: boolean;
showLoginPrompt: () => void;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [showLoginModal, setShowLoginModal] = useState(false);
useEffect(() => {
checkAuthStatus();
}, []);
const checkAuthStatus = async () => {
try {
const response = await fetch('/api/user', {
credentials: 'include'
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
setShowLoginModal(false); // Close modal on auth
} else {
setUser(null);
}
} catch (error) {
console.error('Auth check failed:', error);
setUser(null);
} finally {
setLoading(false);
}
};
const login = () => {
window.location.href = '/auth/github';
};
const logout = async () => {
try {
await fetch('/api/logout', {
method: 'POST',
credentials: 'include'
});
setUser(null);
} catch (error) {
console.error('Logout failed:', error);
}
};
const showLoginPrompt = () => {
setShowLoginModal(true);
};
return (
<AuthContext.Provider value={{ user, login, logout, loading, showLoginPrompt }}>
{children}
<LoginModal isOpen={showLoginModal} />
</AuthContext.Provider>
);
};

View File

@@ -0,0 +1,122 @@
import { io, Socket } from 'socket.io-client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { useAuth } from './AuthContext';
import { Pixel, Cursor } from '../types';
interface SocketContextType {
socket: Socket | null;
isConnected: boolean;
cursors: Cursor[];
sendCursorMove: (x: number, y: number) => void;
sendPaintPixels: (pixels: Pixel[]) => void;
}
const SocketContext = createContext<SocketContextType | undefined>(undefined);
export function SocketProvider({ children }: { children: ReactNode }) {
const [socket, setSocket] = useState<Socket | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [cursors, setCursors] = useState<Cursor[]>([]);
const { user } = useAuth();
useEffect(() => {
// Only connect sock if user is authed
if (!user) {
setSocket(null);
setIsConnected(false);
return;
}
// Get socket auth token first
fetch('/api/socket-token', {
credentials: 'include'
})
.then(res => {
if (!res.ok) {
throw new Error('Not authenticated');
}
return res.json();
})
.then(data => {
if (data.token) {
// Conn with token
const newSocket = io('', {
transports: ['websocket']
});
newSocket.on('connect', () => {
setIsConnected(true);
// auth when logged in
newSocket.emit('join-canvas', data.token);
});
newSocket.on('disconnect', () => {
setIsConnected(false);
});
newSocket.on('canvas-state', (pixels: Pixel[]) => {
window.dispatchEvent(new CustomEvent('canvas-state', { detail: pixels }));
});
newSocket.on('canvas-update', (update: { pixels: Pixel[] }) => {
window.dispatchEvent(new CustomEvent('canvas-update', { detail: update.pixels }));
});
newSocket.on('cursor-update', (updatedCursors: Cursor[]) => {
setCursors(updatedCursors);
});
newSocket.on('error', (message: string) => {
console.error('Socket error:', message);
// If auth fails we disconnect and retry after delay
if (message.includes('Authentication required') || message.includes('expired')) {
newSocket.disconnect();
setTimeout(() => {
// Trigger reconn
}, 2000);
}
});
newSocket.on('auth-success', () => {
});
setSocket(newSocket);
return () => {
newSocket.disconnect();
};
}
})
.catch(error => {
console.error('Failed to get socket token:', error);
setSocket(null);
setIsConnected(false);
});
}, [user]);
const sendCursorMove = (x: number, y: number) => {
if (socket && isConnected) {
socket.emit('cursor-move', { x, y });
}
};
const sendPaintPixels = (pixels: Pixel[]) => {
if (socket && isConnected) {
socket.emit('paint-pixels', pixels);
}
};
return (
<SocketContext.Provider value={{ socket, isConnected, cursors, sendCursorMove, sendPaintPixels }}>
{children}
</SocketContext.Provider>
);
}
export function useSocket() {
const context = useContext(SocketContext);
if (context === undefined) {
throw new Error('useSocket must be used within a SocketProvider');
}
return context;
}