diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json deleted file mode 100644 index 9e26dfeeb..000000000 --- a/frontend/public/locales/en/translation.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 13b129c63..2cf40f1ce 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { MantineProvider } from '@mantine/core'; -import { mantineTheme } from './theme/mantineTheme'; +import { RainbowThemeProvider } from './components/RainbowThemeProvider'; import HomePage from './pages/HomePage'; // Import global styles @@ -9,8 +8,8 @@ import './index.css'; export default function App() { return ( - + - + ); } diff --git a/frontend/src/components/RainbowThemeProvider.tsx b/frontend/src/components/RainbowThemeProvider.tsx new file mode 100644 index 000000000..90261381e --- /dev/null +++ b/frontend/src/components/RainbowThemeProvider.tsx @@ -0,0 +1,55 @@ +import React, { createContext, useContext, ReactNode } from 'react'; +import { MantineProvider, ColorSchemeScript } from '@mantine/core'; +import { useRainbowTheme } from '../hooks/useRainbowTheme'; +import { mantineTheme } from '../theme/mantineTheme'; +import rainbowStyles from '../styles/rainbow.module.css'; + +interface RainbowThemeContextType { + themeMode: 'light' | 'dark' | 'rainbow'; + isRainbowMode: boolean; + isToggleDisabled: boolean; + toggleTheme: () => void; + activateRainbow: () => void; + deactivateRainbow: () => void; +} + +const RainbowThemeContext = createContext(null); + +export function useRainbowThemeContext() { + const context = useContext(RainbowThemeContext); + if (!context) { + throw new Error('useRainbowThemeContext must be used within RainbowThemeProvider'); + } + return context; +} + +interface RainbowThemeProviderProps { + children: ReactNode; +} + +export function RainbowThemeProvider({ children }: RainbowThemeProviderProps) { + const rainbowTheme = useRainbowTheme(); + + // Determine the Mantine color scheme + const mantineColorScheme = rainbowTheme.themeMode === 'rainbow' ? 'dark' : rainbowTheme.themeMode; + + return ( + <> + + + +
+ {children} +
+
+
+ + ); +} diff --git a/frontend/src/components/TopControls.tsx b/frontend/src/components/TopControls.tsx index 40068e10f..c0316d9bd 100644 --- a/frontend/src/components/TopControls.tsx +++ b/frontend/src/components/TopControls.tsx @@ -1,9 +1,11 @@ import React from "react"; import { Button, SegmentedControl } from "@mantine/core"; -import { useMantineColorScheme } from "@mantine/core"; +import { useRainbowThemeContext } from "./RainbowThemeProvider"; import LanguageSelector from "./LanguageSelector"; +import rainbowStyles from '../styles/rainbow.module.css'; import DarkModeIcon from '@mui/icons-material/DarkMode'; import LightModeIcon from '@mui/icons-material/LightMode'; +import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; import VisibilityIcon from "@mui/icons-material/Visibility"; import EditNoteIcon from "@mui/icons-material/EditNote"; import InsertDriveFileIcon from "@mui/icons-material/InsertDriveFile"; @@ -45,18 +47,34 @@ const TopControls: React.FC = ({ currentView, setCurrentView, }) => { - const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + const { themeMode, isRainbowMode, isToggleDisabled, toggleTheme } = useRainbowThemeContext(); + + const getThemeIcon = () => { + if (isRainbowMode) return ; + if (themeMode === "dark") return ; + return ; + }; return ( -
+
@@ -69,6 +87,7 @@ const TopControls: React.FC = ({ radius="xl" size="md" fullWidth + className={isRainbowMode ? rainbowStyles.rainbowSegmentedControl : ''} />
diff --git a/frontend/src/hooks/useRainbowTheme.ts b/frontend/src/hooks/useRainbowTheme.ts new file mode 100644 index 000000000..a3c8f6e67 --- /dev/null +++ b/frontend/src/hooks/useRainbowTheme.ts @@ -0,0 +1,200 @@ +import { useState, useCallback, useRef, useEffect } from 'react'; + +type ThemeMode = 'light' | 'dark' | 'rainbow'; + +interface RainbowThemeHook { + themeMode: ThemeMode; + isRainbowMode: boolean; + isToggleDisabled: boolean; + toggleTheme: () => void; + activateRainbow: () => void; + deactivateRainbow: () => void; +} + +export function useRainbowTheme(initialTheme: 'light' | 'dark' = 'light'): RainbowThemeHook { + // Get theme from localStorage or use initial + const [themeMode, setThemeMode] = useState(() => { + const stored = localStorage.getItem('stirling-theme'); + if (stored && ['light', 'dark', 'rainbow'].includes(stored)) { + return stored as ThemeMode; + } + return initialTheme; + }); + + // Track rapid toggles for easter egg + const toggleCount = useRef(0); + const lastToggleTime = useRef(Date.now()); + const [isToggleDisabled, setIsToggleDisabled] = useState(false); + + // Save theme to localStorage whenever it changes + useEffect(() => { + localStorage.setItem('stirling-theme', themeMode); + + // Apply rainbow class to body if in rainbow mode + if (themeMode === 'rainbow') { + document.body.classList.add('rainbow-mode-active'); + + // Show easter egg notification + showRainbowNotification(); + } else { + document.body.classList.remove('rainbow-mode-active'); + } + }, [themeMode]); + + const showRainbowNotification = () => { + // Remove any existing notification + const existingNotification = document.getElementById('rainbow-notification'); + if (existingNotification) { + existingNotification.remove(); + } + + // Create and show rainbow notification + const notification = document.createElement('div'); + notification.id = 'rainbow-notification'; + notification.innerHTML = '🌈 RAINBOW MODE ACTIVATED! 🌈
Button disabled for 3 seconds, then click to exit'; + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: linear-gradient(45deg, #ff0000, #ff8800, #ffff00, #88ff00, #00ff88, #00ffff, #0088ff, #8800ff); + background-size: 300% 300%; + animation: rainbowBackground 1s ease infinite; + color: white; + padding: 15px 20px; + border-radius: 25px; + font-weight: bold; + font-size: 16px; + z-index: 1000; + border: 2px solid white; + box-shadow: 0 0 20px rgba(255, 255, 255, 0.8); + user-select: none; + pointer-events: none; + transition: opacity 0.3s ease; + `; + + document.body.appendChild(notification); + + // Auto-remove notification after 3 seconds + setTimeout(() => { + if (notification) { + notification.style.opacity = '0'; + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 300); + } + }, 3000); + }; + + const showExitNotification = () => { + // Remove any existing notification + const existingNotification = document.getElementById('rainbow-exit-notification'); + if (existingNotification) { + existingNotification.remove(); + } + + // Create and show exit notification + const notification = document.createElement('div'); + notification.id = 'rainbow-exit-notification'; + notification.innerHTML = '🌙 Rainbow mode deactivated'; + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: linear-gradient(45deg, #333, #666); + color: white; + padding: 15px 20px; + border-radius: 25px; + font-weight: bold; + font-size: 16px; + z-index: 1000; + border: 2px solid #999; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.3); + user-select: none; + pointer-events: none; + transition: opacity 0.3s ease; + `; + + document.body.appendChild(notification); + + // Auto-remove notification after 2 seconds + setTimeout(() => { + if (notification) { + notification.style.opacity = '0'; + setTimeout(() => { + if (notification.parentNode) { + notification.parentNode.removeChild(notification); + } + }, 300); + } + }, 2000); + }; + + const toggleTheme = useCallback(() => { + // Don't allow toggle if disabled + if (isToggleDisabled) { + return; + } + + const currentTime = Date.now(); + + // Simple exit from rainbow mode with single click (after cooldown period) + if (themeMode === 'rainbow') { + setThemeMode('light'); + console.log('🌈 Rainbow mode deactivated. Thanks for trying it!'); + showExitNotification(); + return; + } + + // Reset counter if too much time has passed (2.5 seconds) + if (currentTime - lastToggleTime.current > 2500) { + toggleCount.current = 1; + } else { + toggleCount.current++; + } + lastToggleTime.current = currentTime; + + // Easter egg: Activate rainbow mode after 6 rapid toggles + if (toggleCount.current >= 6) { + setThemeMode('rainbow'); + console.log('🌈 RAINBOW MODE ACTIVATED! 🌈 You found the secret easter egg!'); + console.log('🌈 Button will be disabled for 3 seconds, then click once to exit!'); + + // Disable toggle for 3 seconds + setIsToggleDisabled(true); + setTimeout(() => { + setIsToggleDisabled(false); + console.log('🌈 Theme toggle re-enabled! Click once to exit rainbow mode.'); + }, 3000); + + // Reset counter + toggleCount.current = 0; + return; + } + + // Normal theme switching + setThemeMode(prevMode => prevMode === 'light' ? 'dark' : 'light'); + }, [themeMode, isToggleDisabled]); + + const activateRainbow = useCallback(() => { + setThemeMode('rainbow'); + console.log('🌈 Rainbow mode manually activated!'); + }, []); + + const deactivateRainbow = useCallback(() => { + if (themeMode === 'rainbow') { + setThemeMode('light'); + console.log('🌈 Rainbow mode manually deactivated.'); + } + }, [themeMode]); + + return { + themeMode, + isRainbowMode: themeMode === 'rainbow', + isToggleDisabled, + toggleTheme, + activateRainbow, + deactivateRainbow, + }; +} \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css index 17df0e7ec..79e619244 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,6 +2,7 @@ @tailwind components; @tailwind utilities; + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', @@ -11,6 +12,7 @@ body { -moz-osx-font-smoothing: grayscale; } + code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index daab25802..90ff48329 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -5,7 +5,9 @@ import { useToolParams } from "../hooks/useToolParams"; import AddToPhotosIcon from "@mui/icons-material/AddToPhotos"; import ContentCutIcon from "@mui/icons-material/ContentCut"; import ZoomInMapIcon from "@mui/icons-material/ZoomInMap"; -import { Group, Paper, Box, Button, useMantineTheme, useMantineColorScheme } from "@mantine/core"; +import { Group, Paper, Box, Button, useMantineTheme } from "@mantine/core"; +import { useRainbowThemeContext } from "../components/RainbowThemeProvider"; +import rainbowStyles from '../styles/rainbow.module.css'; import ToolPicker from "../components/ToolPicker"; import FileManager from "../components/FileManager"; @@ -41,7 +43,7 @@ export default function HomePage() { const { t } = useTranslation(); const [searchParams] = useSearchParams(); const theme = useMantineTheme(); - const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + const { isRainbowMode } = useRainbowThemeContext(); // Core app state const [selectedToolKey, setSelectedToolKey] = useState(searchParams.get("t") || "split"); @@ -81,7 +83,7 @@ export default function HomePage() { {/* Left: Tool Picker */} {sidebarsVisible && (