diff --git a/Frontend/package-lock.json b/Frontend/package-lock.json index 0b733ce..0dab426 100644 --- a/Frontend/package-lock.json +++ b/Frontend/package-lock.json @@ -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", diff --git a/Frontend/package.json b/Frontend/package.json index 59d1853..8a5868f 100644 --- a/Frontend/package.json +++ b/Frontend/package.json @@ -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", diff --git a/Frontend/public/BG-login(2).jpg b/Frontend/public/BG-login(2).jpg new file mode 100644 index 0000000..86c0f3a Binary files /dev/null and b/Frontend/public/BG-login(2).jpg differ diff --git a/Frontend/public/Bottom.png b/Frontend/public/Bottom.png new file mode 100644 index 0000000..dc6805e Binary files /dev/null and b/Frontend/public/Bottom.png differ diff --git a/Frontend/public/Vidyavihar@3x.png b/Frontend/public/Vidyavihar@3x.png new file mode 100644 index 0000000..8b62498 Binary files /dev/null and b/Frontend/public/Vidyavihar@3x.png differ diff --git a/Frontend/public/kjsce2x.png b/Frontend/public/kjsce2x.png new file mode 100644 index 0000000..6aa8433 Binary files /dev/null and b/Frontend/public/kjsce2x.png differ diff --git a/Frontend/src/App.jsx b/Frontend/src/App.jsx index 8002bc3..b2fc964 100644 --- a/Frontend/src/App.jsx +++ b/Frontend/src/App.jsx @@ -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 ( -
- - -
- ) + + + +
+ + } /> + +
+ +
+
+ + Copyright © 2025. Made with + ♡ by Ishika and Arnab. + +
+
+ + } + /> + } /> + +
+
+
+
+ ); } export default App diff --git a/Frontend/src/components/Header.jsx b/Frontend/src/components/Header.jsx new file mode 100644 index 0000000..7eaf0dd --- /dev/null +++ b/Frontend/src/components/Header.jsx @@ -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 ( +
+
+

Monaco Editor

+
+ +
+ {user?.picture && ( + Profile + )} + + Welcome, {user?.name || user?.email} + + +
+
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/Frontend/src/components/Login.jsx b/Frontend/src/components/Login.jsx new file mode 100644 index 0000000..3efe677 --- /dev/null +++ b/Frontend/src/components/Login.jsx @@ -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 ( +
+ {/* Theme Toggle - Top Right */} +
+ +
+ + {/* Left Side - Background Image */} +
+ Login Background +
+ + {/* Right Side - Login Form */} +
+
+ {/* Logos */} +
+ KJSCE + Somaiya Vidyavihar +
+ + {/* Title */} +

+ Welcome To Monaco Editor +

+

+ Please sign in with your Google account to continue. +

+ + {/* Error Display */} + {error && ( +
+

{error}

+
+ )} + + {/* Google Login Button */} +
+ +
+ +
+

+ Need help?{' '} + +

+
+ +
+

+ Sign in with your Google account to access the Monaco Editor +

+
+ + {/* Trust Logo */} +
+ Somaiya Trust +
+
+
+
+ ); +}; + +export default Login; \ No newline at end of file diff --git a/Frontend/src/components/ProtectedRoute.jsx b/Frontend/src/components/ProtectedRoute.jsx new file mode 100644 index 0000000..527ba5e --- /dev/null +++ b/Frontend/src/components/ProtectedRoute.jsx @@ -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 ( +
+
+
+

Loading...

+
+
+ ); + } + + return isAuthenticated ? children : ; +}; + +export default ProtectedRoute; \ No newline at end of file diff --git a/Frontend/src/components/ThemeToggle.jsx b/Frontend/src/components/ThemeToggle.jsx new file mode 100644 index 0000000..0e4f6e2 --- /dev/null +++ b/Frontend/src/components/ThemeToggle.jsx @@ -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 ( + + ); +}; + +export default ThemeToggle; \ No newline at end of file diff --git a/Frontend/src/contexts/AuthContext.jsx b/Frontend/src/contexts/AuthContext.jsx new file mode 100644 index 0000000..ddde4c8 --- /dev/null +++ b/Frontend/src/contexts/AuthContext.jsx @@ -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 ( + + {children} + + ); +}; \ No newline at end of file diff --git a/Frontend/src/index.css b/Frontend/src/index.css index edf7378..4d1145a 100644 --- a/Frontend/src/index.css +++ b/Frontend/src/index.css @@ -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;