Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
position: relative;
overflow-x: hidden;
transition: background-color 0.3s ease;
}

/* Dark mode background */
:root.dark #root {
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
}

/* Animated Background Effect */
Expand All @@ -42,6 +48,15 @@
radial-gradient(circle at 40% 80%, rgba(37, 117, 252, 0.08) 0%, transparent 50%);
z-index: -1;
animation: float 20s ease-in-out infinite;
transition: background 0.3s ease;
}

/* Dark mode animated background */
:root.dark #root::before {
background:
radial-gradient(circle at 20% 50%, rgba(255, 126, 95, 0.04) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(106, 17, 203, 0.04) 0%, transparent 50%),
radial-gradient(circle at 40% 80%, rgba(37, 117, 252, 0.04) 0%, transparent 50%);
}

@keyframes float {
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { issuesApi, miscApi } from './api';
import AppHeader from './components/AppHeader';
import FloatingButtonsManager from './components/FloatingButtonsManager';
import LoadingSpinner from './components/LoadingSpinner';
import { DarkModeProvider, useDarkMode } from './contexts/DarkModeContext';

// Lazy Load Views
const Landing = React.lazy(() => import('./views/Landing'));
Expand Down Expand Up @@ -49,6 +50,7 @@ import AdminDashboard from './views/AdminDashboard';
function AppContent() {
const navigate = useNavigate();
const location = useLocation();
const { isDarkMode } = useDarkMode();
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Remove the unused isDarkMode destructuring or use it; leaving it unused will fail linting and adds dead code.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At frontend/src/App.jsx, line 53:

<comment>Remove the unused `isDarkMode` destructuring or use it; leaving it unused will fail linting and adds dead code.</comment>

<file context>
@@ -49,6 +50,7 @@ import AdminDashboard from './views/AdminDashboard';
 function AppContent() {
   const navigate = useNavigate();
   const location = useLocation();
+  const { isDarkMode } = useDarkMode();
   const [responsibilityMap, setResponsibilityMap] = useState(null);
   const [actionPlan, setActionPlan] = useState(null);
</file context>
Fix with Cubic

const [responsibilityMap, setResponsibilityMap] = useState(null);
const [actionPlan, setActionPlan] = useState(null);
const [maharashtraRepInfo, setMaharashtraRepInfo] = useState(null);
Expand Down Expand Up @@ -152,7 +154,7 @@ function AppContent() {
if (isLandingPage) {
return (
<Suspense fallback={
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-gray-50 to-blue-50">
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-gray-50 to-blue-50 dark:from-gray-950 dark:to-blue-950 transition-colors duration-300">
<LoadingSpinner size="xl" variant="primary" />
</div>
}>
Expand All @@ -163,18 +165,19 @@ function AppContent() {

// Otherwise render the main app layout
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-100 text-gray-900 font-sans overflow-hidden">
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-blue-50/30 to-gray-100 dark:from-gray-950 dark:via-blue-950/30 dark:to-gray-900 text-gray-900 dark:text-gray-100 font-sans overflow-hidden transition-colors duration-300">
{/* Animated background elements */}
<div className="fixed inset-0 z-0 pointer-events-none">
<div className="absolute top-1/4 left-1/4 w-72 h-72 bg-orange-300/10 rounded-full blur-3xl animate-pulse-slow"></div>
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-blue-300/10 rounded-full blur-3xl animate-pulse-slow animation-delay-1000"></div>
<div className="absolute top-1/4 left-1/4 w-72 h-72 bg-orange-300/10 dark:bg-orange-300/5 rounded-full blur-3xl animate-pulse-slow transition-colors duration-300"></div>
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-blue-300/10 dark:bg-blue-300/5 rounded-full blur-3xl animate-pulse-slow animation-delay-1000 transition-colors duration-300"></div>
</div>

<FloatingButtonsManager setView={navigateToView} />

<div className="relative z-10">
<AppHeader />


<Suspense fallback={
<div className="flex justify-center my-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-500"></div>
Expand Down Expand Up @@ -314,9 +317,11 @@ function AppContent() {
function App() {
return (
<Router>
<AuthProvider>
<AppContent />
</AuthProvider>
<DarkModeProvider>
<AuthProvider>
<AppContent />
</AuthProvider>
</DarkModeProvider>
</Router>
);
}
Expand Down
33 changes: 24 additions & 9 deletions frontend/src/components/AppHeader.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useNavigate } from 'react-router-dom';
import { Menu, User, LogOut } from 'lucide-react';
import { Menu, User, LogOut, Sun, Moon } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
import { useDarkMode } from '../contexts/DarkModeContext';

const AppHeader = () => {
const navigate = useNavigate();
const { user, logout } = useAuth(); // useAuth returns user, not currentUser
const { isDarkMode, toggleDarkMode } = useDarkMode();
const [isMenuOpen, setIsMenuOpen] = useState(false);

const handleLogout = async () => {
Expand All @@ -19,7 +21,7 @@ const AppHeader = () => {
};

return (
<header className="bg-white shadow-sm sticky top-0 z-40">
<header className="bg-white dark:bg-gray-900 shadow-sm sticky top-0 z-40 transition-colors duration-300">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16 items-center">
<div className="flex items-center cursor-pointer" onClick={() => navigate('/')}>
Expand All @@ -29,26 +31,39 @@ const AppHeader = () => {
</div>

<div className="flex items-center gap-4">
{/* Dark Mode Toggle Button */}
<button
onClick={toggleDarkMode}
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
>
{isDarkMode ? (
<Sun size={20} className="text-yellow-400" />
) : (
<Moon size={20} className="text-gray-700" />
)}
</button>
Comment on lines +35 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Add aria-label for screen reader accessibility.

The toggle button uses title for a tooltip, but screen readers primarily rely on aria-label. Without it, the button is announced without a meaningful label.

Proposed fix
             <button
               onClick={toggleDarkMode}
               className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200"
               title={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
+              aria-label={isDarkMode ? 'Switch to light mode' : 'Switch to dark mode'}
             >
πŸ€– Prompt for AI Agents
In `@frontend/src/components/AppHeader.jsx` around lines 35 - 45, The toggle
button currently only sets a title and lacks an accessible label; update the
button element that uses toggleDarkMode and isDarkMode to include an appropriate
aria-label (e.g., aria-label={isDarkMode ? 'Switch to light mode' : 'Switch to
dark mode'}) so screen readers receive the same descriptive text as the visual
tooltip while keeping the existing title and icon logic intact.


{user ? (
<div className="relative">
<button onClick={() => setIsMenuOpen(!isMenuOpen)} className="flex items-center gap-2 text-gray-700 hover:text-blue-600 focus:outline-none">
<div className="bg-blue-100 p-2 rounded-full">
<User size={20} className="text-blue-600" />
<button onClick={() => setIsMenuOpen(!isMenuOpen)} className="flex items-center gap-2 text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 focus:outline-none transition-colors duration-200">
<div className="bg-blue-100 dark:bg-blue-900 p-2 rounded-full">
<User size={20} className="text-blue-600 dark:text-blue-400" />
</div>
<span className="hidden sm:inline font-medium text-sm">{user.email}</span>
</button>

{isMenuOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5 z-50">
<Link to="/my-reports" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={() => setIsMenuOpen(false)}>My Reports</Link>
<button onClick={handleLogout} className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2">
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 ring-1 ring-black ring-opacity-5 z-50">
<Link to="/my-reports" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200" onClick={() => setIsMenuOpen(false)}>My Reports</Link>
<button onClick={handleLogout} className="block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2 transition-colors duration-200">
<LogOut size={14} /> Logout
</button>
</div>
)}
</div>
) : (
<Link to="/login" className="text-sm font-medium text-blue-600 hover:text-blue-500 px-4 py-2 rounded-full hover:bg-blue-50 transition-colors">Login</Link>
<Link to="/login" className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-500 dark:hover:text-blue-300 px-4 py-2 rounded-full hover:bg-blue-50 dark:hover:bg-gray-800 transition-colors duration-200">Login</Link>
)}
</div>
</div>
Expand Down
44 changes: 44 additions & 0 deletions frontend/src/contexts/DarkModeContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { createContext, useContext, useState, useEffect } from 'react';

const DarkModeContext = createContext(null);

export const DarkModeProvider = ({ children }) => {
const [isDarkMode, setIsDarkMode] = useState(() => {
// Initialize from localStorage, default to light mode (false)
const savedMode = localStorage.getItem('darkMode');
if (savedMode !== null) {
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Guard JSON.parse when reading darkMode from localStorage to avoid crashing on malformed values.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At frontend/src/contexts/DarkModeContext.jsx, line 9:

<comment>Guard JSON.parse when reading `darkMode` from localStorage to avoid crashing on malformed values.</comment>

<file context>
@@ -0,0 +1,44 @@
+  const [isDarkMode, setIsDarkMode] = useState(() => {
+    // Initialize from localStorage, default to light mode (false)
+    const savedMode = localStorage.getItem('darkMode');
+    if (savedMode !== null) {
+      return JSON.parse(savedMode);
+    }
</file context>
Fix with Cubic

return JSON.parse(savedMode);
}
// Default to light mode
return false;
Comment on lines +6 to +13
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing system preference detection (prefers-color-scheme).

The PR objectives and linked issue #329 explicitly require: "Default theme should respect system preference via prefers-color-scheme." Currently, when no localStorage value exists, the code defaults to false (light mode), ignoring the user's OS-level preference.

Proposed fix
   const [isDarkMode, setIsDarkMode] = useState(() => {
     const savedMode = localStorage.getItem('darkMode');
     if (savedMode !== null) {
-      return JSON.parse(savedMode);
+      try {
+        return JSON.parse(savedMode);
+      } catch {
+        return false;
+      }
     }
-    // Default to light mode
-    return false;
+    // Respect system preference
+    return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
   });
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [isDarkMode, setIsDarkMode] = useState(() => {
// Initialize from localStorage, default to light mode (false)
const savedMode = localStorage.getItem('darkMode');
if (savedMode !== null) {
return JSON.parse(savedMode);
}
// Default to light mode
return false;
const [isDarkMode, setIsDarkMode] = useState(() => {
// Initialize from localStorage, default to light mode (false)
const savedMode = localStorage.getItem('darkMode');
if (savedMode !== null) {
try {
return JSON.parse(savedMode);
} catch {
return false;
}
}
// Respect system preference
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
πŸ€– Prompt for AI Agents
In `@frontend/src/contexts/DarkModeContext.jsx` around lines 6 - 13, The useState
initializer for isDarkMode currently defaults to false when no localStorage
value exists and should instead respect the system preference; update the
initializer in DarkModeContext.jsx (where isDarkMode and setIsDarkMode are
created via useState) to check for window.matchMedia('(prefers-color-scheme:
dark)').matches (guarding against SSR by ensuring window and matchMedia exist)
and return true when the media query indicates dark mode, otherwise return
false; keep the existing localStorage parse logic but only fall back to the
prefers-color-scheme result when savedMode is null.

});

// Apply dark mode class to document root
useEffect(() => {
const htmlElement = document.documentElement;
if (isDarkMode) {
htmlElement.classList.add('dark');
} else {
htmlElement.classList.remove('dark');
}
localStorage.setItem('darkMode', JSON.stringify(isDarkMode));
}, [isDarkMode]);

const toggleDarkMode = () => {
setIsDarkMode(prev => !prev);
};

return (
<DarkModeContext.Provider value={{ isDarkMode, toggleDarkMode }}>
{children}
</DarkModeContext.Provider>
);
};

export const useDarkMode = () => {
const context = useContext(DarkModeContext);
if (!context) {
throw new Error('useDarkMode must be used within DarkModeProvider');
}
return context;
};
Loading