Add Google OAuth integration, routing, and theme toggle; implement login and protected routes
This commit is contained in:
67
Frontend/package-lock.json
generated
67
Frontend/package-lock.json
generated
@@ -9,11 +9,13 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@react-oauth/google": "^0.12.2",
|
||||
"lucide-react": "^0.483.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-resizable-panels": "^2.1.7"
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
@@ -1041,6 +1043,16 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-oauth/google": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz",
|
||||
"integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz",
|
||||
@@ -1602,6 +1614,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2745,6 +2766,44 @@
|
||||
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
|
||||
"integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.9.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz",
|
||||
"integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -2810,6 +2869,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@react-oauth/google": "^0.12.2",
|
||||
"lucide-react": "^0.483.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-resizable-panels": "^2.1.7"
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
|
||||
BIN
Frontend/public/BG-login(2).jpg
Normal file
BIN
Frontend/public/BG-login(2).jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 677 KiB |
BIN
Frontend/public/Bottom.png
Normal file
BIN
Frontend/public/Bottom.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Frontend/public/Vidyavihar@3x.png
Normal file
BIN
Frontend/public/Vidyavihar@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
BIN
Frontend/public/kjsce2x.png
Normal file
BIN
Frontend/public/kjsce2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -1,20 +1,47 @@
|
||||
import CodeChallenge from "./components/CodeChallenge.jsx"
|
||||
import "./index.css"
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { GoogleOAuthProvider } from '@react-oauth/google';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import Login from './components/Login';
|
||||
import CodeChallenge from "./components/CodeChallenge.jsx";
|
||||
import Header from './components/Header';
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
import "./index.css";
|
||||
|
||||
function App() {
|
||||
// Google OAuth Client ID - in production, this should be in environment variables
|
||||
const GOOGLE_CLIENT_ID = "586378657128-smg8t52eqbji66c3eg967f70hsr54q5r.apps.googleusercontent.com";
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<CodeChallenge />
|
||||
<footer className="footer-bar fixed bottom-0 left-0 right-0 border-t border-slate-200/40 dark:border-gray-800/20 bg-black">
|
||||
<div className="flex items-center justify-center h-7">
|
||||
<span className="text-xs text-slate-400 dark:text-gray-400 flex items-center">
|
||||
Copyright © 2025. Made with
|
||||
♡ by Ishika and Arnab.
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
<GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<div className="App">
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/editor"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Header />
|
||||
<CodeChallenge />
|
||||
<footer className="footer-bar fixed bottom-0 left-0 right-0 border-t border-slate-200/40 dark:border-gray-800/20 bg-black">
|
||||
<div className="flex items-center justify-center h-7">
|
||||
<span className="text-xs text-slate-400 dark:text-gray-400 flex items-center">
|
||||
Copyright © 2025. Made with
|
||||
♡ by Ishika and Arnab.
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<Navigate to="/editor" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
</GoogleOAuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
41
Frontend/src/components/Header.jsx
Normal file
41
Frontend/src/components/Header.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Header = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="bg-gray-800 border-b border-gray-700 px-4 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h1 className="text-white font-bold text-lg">Monaco Editor</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{user?.picture && (
|
||||
<img
|
||||
src={user.picture}
|
||||
alt="Profile"
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<span className="text-gray-300 text-sm">
|
||||
Welcome, {user?.name || user?.email}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="bg-red-600 hover:bg-red-700 text-white text-sm px-3 py-1 rounded transition-colors duration-200"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
161
Frontend/src/components/Login.jsx
Normal file
161
Frontend/src/components/Login.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { GoogleLogin } from '@react-oauth/google';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import ThemeToggle from './ThemeToggle';
|
||||
|
||||
const Login = () => {
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuth();
|
||||
|
||||
// Check if user is already logged in
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('monaco_token');
|
||||
if (token) {
|
||||
navigate('/editor');
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const handleLoginSuccess = async (credentialResponse) => {
|
||||
console.log('Google login success:', credentialResponse);
|
||||
|
||||
if (credentialResponse.credential) {
|
||||
try {
|
||||
console.log('Processing Google credential...');
|
||||
|
||||
// For demo purposes, we'll decode the JWT token to get user info
|
||||
// In a real app, you would send this to your backend for verification
|
||||
const base64Url = credentialResponse.credential.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
|
||||
const userInfo = JSON.parse(jsonPayload);
|
||||
console.log('User info:', userInfo);
|
||||
|
||||
const success = await login(userInfo.email, credentialResponse.credential, userInfo);
|
||||
|
||||
if (success) {
|
||||
navigate('/editor');
|
||||
} else {
|
||||
throw new Error('Authentication failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during login:', error);
|
||||
setError(`Authentication failed: ${error.message || 'Please try again.'}`);
|
||||
}
|
||||
} else {
|
||||
console.error('No credential received from Google');
|
||||
setError('No credential received from Google. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginError = () => {
|
||||
console.error('Google login failed - checking network and configuration...');
|
||||
|
||||
// Check if we're online
|
||||
if (!navigator.onLine) {
|
||||
setError('You appear to be offline. Please check your internet connection and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Google Identity Services script loaded
|
||||
if (typeof window !== 'undefined' && !window.google) {
|
||||
console.error('Google Identity Services script not loaded');
|
||||
setError('Google authentication service is not available. Please refresh the page and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
setError('Google login failed. This might be due to network connectivity issues, browser compatibility, or Google account configuration. Please try again or contact support if the problem persists.');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-container">
|
||||
{/* Theme Toggle - Top Right */}
|
||||
<div className="theme-toggle">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* Left Side - Background Image */}
|
||||
<div className="login-left">
|
||||
<img
|
||||
src="/BG-login(2).jpg"
|
||||
alt="Login Background"
|
||||
className="login-bg-image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Login Form */}
|
||||
<div className="login-right">
|
||||
<div className="login-form-container">
|
||||
{/* Logos */}
|
||||
<div className="login-logos">
|
||||
<img
|
||||
src="/Vidyavihar@3x.png"
|
||||
alt="KJSCE"
|
||||
className="login-logo"
|
||||
/>
|
||||
<img
|
||||
src="/kjsce2x.png"
|
||||
alt="Somaiya Vidyavihar"
|
||||
className="login-logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="login-title">
|
||||
Welcome To Monaco Editor
|
||||
</h1>
|
||||
<p className="login-subtitle">
|
||||
Please sign in with your Google account to continue.
|
||||
</p>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="login-error">
|
||||
<p style={{ fontSize: '0.875rem', margin: 0 }}>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Google Login Button */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '1.5rem' }}>
|
||||
<GoogleLogin
|
||||
onSuccess={handleLoginSuccess}
|
||||
onError={handleLoginError}
|
||||
useOneTap
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="login-footer">
|
||||
<p className="login-footer-text">
|
||||
Need help?{' '}
|
||||
<button className="login-footer-link">
|
||||
Contact admin
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="login-demo-note">
|
||||
<p className="login-demo-text">
|
||||
Sign in with your Google account to access the Monaco Editor
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Trust Logo */}
|
||||
<div className="login-trust-logo">
|
||||
<img
|
||||
src="/Bottom.png"
|
||||
alt="Somaiya Trust"
|
||||
className="trust-logo-img"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
21
Frontend/src/components/ProtectedRoute.jsx
Normal file
21
Frontend/src/components/ProtectedRoute.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-black">
|
||||
<div className="text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
||||
<p className="mt-4 text-white">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return isAuthenticated ? children : <Navigate to="/login" replace />;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
61
Frontend/src/components/ThemeToggle.jsx
Normal file
61
Frontend/src/components/ThemeToggle.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const [theme, setTheme] = useState('dark');
|
||||
|
||||
useEffect(() => {
|
||||
// Check if there's a saved theme preference
|
||||
const savedTheme = localStorage.getItem('monaco_theme');
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
} else {
|
||||
// Check system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setTheme(prefersDark ? 'dark' : 'light');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Update the HTML class and CSS variables
|
||||
const root = document.documentElement;
|
||||
root.className = theme;
|
||||
|
||||
// Save theme preference
|
||||
localStorage.setItem('monaco_theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="relative p-2 rounded-md border border-gray-600 bg-gray-800 hover:bg-gray-700 transition-colors"
|
||||
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`}
|
||||
>
|
||||
{/* Sun Icon for Light Mode */}
|
||||
<svg
|
||||
className={`h-5 w-5 transition-all ${theme === 'dark' ? 'rotate-90 scale-0' : 'rotate-0 scale-100'}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<path d="M12 1v2m0 18v2M4.22 4.22l1.42 1.42m12.72 12.72l1.42 1.42M1 12h2m18 0h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
||||
</svg>
|
||||
|
||||
{/* Moon Icon for Dark Mode */}
|
||||
<svg
|
||||
className={`absolute top-2 left-2 h-5 w-5 transition-all ${theme === 'dark' ? 'rotate-0 scale-100' : 'rotate-90 scale-0'}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
98
Frontend/src/contexts/AuthContext.jsx
Normal file
98
Frontend/src/contexts/AuthContext.jsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Check for existing session on app load
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = () => {
|
||||
try {
|
||||
const savedUser = localStorage.getItem('monaco_user');
|
||||
const savedToken = localStorage.getItem('monaco_token');
|
||||
if (savedUser && savedToken) {
|
||||
setUser(JSON.parse(savedUser));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking auth status:', error);
|
||||
localStorage.removeItem('monaco_user');
|
||||
localStorage.removeItem('monaco_token');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuthStatus();
|
||||
}, []);
|
||||
|
||||
const login = async (email, googleToken, userInfo = null) => {
|
||||
try {
|
||||
// For Google OAuth login
|
||||
if (googleToken && userInfo) {
|
||||
const userData = {
|
||||
id: userInfo.sub || Date.now(),
|
||||
email: email,
|
||||
name: userInfo.name || email.split('@')[0],
|
||||
picture: userInfo.picture || null,
|
||||
loginTime: new Date().toISOString(),
|
||||
googleToken: googleToken
|
||||
};
|
||||
|
||||
setUser(userData);
|
||||
localStorage.setItem('monaco_user', JSON.stringify(userData));
|
||||
localStorage.setItem('monaco_token', googleToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback for demo purposes (though we're moving to Google-only)
|
||||
if (email && email.includes('@')) {
|
||||
const userData = {
|
||||
id: Date.now(),
|
||||
email: email,
|
||||
name: email.split('@')[0],
|
||||
loginTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
setUser(userData);
|
||||
localStorage.setItem('monaco_user', JSON.stringify(userData));
|
||||
localStorage.setItem('monaco_token', 'demo_token');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
localStorage.removeItem('monaco_user');
|
||||
localStorage.removeItem('monaco_token');
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
isAuthenticated: !!user,
|
||||
isLoading
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,25 @@
|
||||
:root {
|
||||
/* Light theme variables */
|
||||
--background: #ffffff;
|
||||
--foreground: #0a0a0a;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #0a0a0a;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #0a0a0a;
|
||||
--primary: #0f172a;
|
||||
--primary-foreground: #f8fafc;
|
||||
--secondary: #f1f5f9;
|
||||
--secondary-foreground: #0f172a;
|
||||
--muted: #f1f5f9;
|
||||
--muted-foreground: #64748b;
|
||||
--accent: #f1f5f9;
|
||||
--accent-foreground: #0f172a;
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: #e2e8f0;
|
||||
--input: #e2e8f0;
|
||||
--ring: #94a3b8;
|
||||
|
||||
--vscode-background: #000000;
|
||||
--vscode-foreground: #d4d4d4;
|
||||
--vscode-activityBar-background: #333333;
|
||||
@@ -18,6 +39,29 @@
|
||||
--vscode-tab-border: #252526;
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Dark theme variables */
|
||||
--background: #000000;
|
||||
--foreground: #ffffff;
|
||||
--card: #1a1a1a;
|
||||
--card-foreground: #e5e5e5;
|
||||
--popover: #1a1a1a;
|
||||
--popover-foreground: #e5e5e5;
|
||||
--primary: #262626;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #1a1a1a;
|
||||
--secondary-foreground: #ffffff;
|
||||
--muted: #1a1a1a;
|
||||
--muted-foreground: #a1a1a1;
|
||||
--accent: #1a1a1a;
|
||||
--accent-foreground: #ffffff;
|
||||
--destructive: #991b1b;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
--input: rgba(255, 255, 255, 0.1);
|
||||
--ring: #525252;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@@ -226,7 +270,7 @@ body {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: calc(100% - ${sidebarVisible ? sidebarWidth : 0}px);
|
||||
width: 100%;
|
||||
transition: margin-left 0.2s ease, width 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -1354,6 +1398,183 @@ body {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Login Page Styles */
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.login-left {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login-bg-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.dark .login-bg-image {
|
||||
filter: brightness(0.75);
|
||||
}
|
||||
|
||||
.login-right {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
.login-form-container {
|
||||
width: 100%;
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.login-logos {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
height: 3rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
color: var(--foreground);
|
||||
font-family: monospace;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
color: var(--muted-foreground);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.login-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.login-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--input);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.login-input:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #3b82f6;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.login-error {
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: #f87171;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background-color: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-footer-text {
|
||||
font-size: 0.875rem;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.login-footer-link {
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-footer-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.login-demo-note {
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.login-demo-text {
|
||||
font-size: 0.75rem;
|
||||
text-align: center;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.login-trust-logo {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.trust-logo-img {
|
||||
height: 2rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Footer Styles */
|
||||
.fixed {
|
||||
position: fixed;
|
||||
|
||||
Reference in New Issue
Block a user