diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 391143b..239386d 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,3 +1,4 @@
+import { useEffect } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';
import Login from './pages/Login';
@@ -5,6 +6,16 @@ import Register from './pages/Register';
import Dashboard from './pages/Dashboard';
import ProductDetail from './pages/ProductDetail';
+function ThemeInitializer({ children }: { children: React.ReactNode }) {
+ useEffect(() => {
+ const saved = localStorage.getItem('theme');
+ if (saved) {
+ document.documentElement.setAttribute('data-theme', saved);
+ }
+ }, []);
+ return <>{children}>;
+}
+
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { user, isLoading } = useAuth();
@@ -97,10 +108,12 @@ function AppRoutes() {
export default function App() {
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
}
diff --git a/frontend/src/components/AuthForm.tsx b/frontend/src/components/AuthForm.tsx
index 8091d47..7a39ec1 100644
--- a/frontend/src/components/AuthForm.tsx
+++ b/frontend/src/components/AuthForm.tsx
@@ -1,4 +1,4 @@
-import { useState, FormEvent } from 'react';
+import { useState, useEffect, FormEvent } from 'react';
import { Link } from 'react-router-dom';
interface AuthFormProps {
@@ -12,6 +12,19 @@ export default function AuthForm({ mode, onSubmit }: AuthFormProps) {
const [confirmPassword, setConfirmPassword] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
+ const [theme, setTheme] = useState<'light' | 'dark'>(() => {
+ const saved = localStorage.getItem('theme');
+ return (saved as 'light' | 'dark') || 'light';
+ });
+
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', theme);
+ localStorage.setItem('theme', theme);
+ }, [theme]);
+
+ const toggleTheme = () => {
+ setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
+ };
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
@@ -94,8 +107,34 @@ export default function AuthForm({ mode, onSubmit }: AuthFormProps) {
.auth-form-footer a {
font-weight: 500;
}
+
+ .auth-theme-toggle {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 0.5rem;
+ padding: 0.5rem;
+ cursor: pointer;
+ font-size: 1.25rem;
+ line-height: 1;
+ transition: all 0.2s;
+ }
+
+ .auth-theme-toggle:hover {
+ border-color: var(--primary);
+ }
`}
+
+
👻
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx
index 496367b..de7ab11 100644
--- a/frontend/src/components/Layout.tsx
+++ b/frontend/src/components/Layout.tsx
@@ -1,4 +1,4 @@
-import { ReactNode } from 'react';
+import { ReactNode, useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
@@ -9,6 +9,19 @@ interface LayoutProps {
export default function Layout({ children }: LayoutProps) {
const { user, logout } = useAuth();
const navigate = useNavigate();
+ const [theme, setTheme] = useState<'light' | 'dark'>(() => {
+ const saved = localStorage.getItem('theme');
+ return (saved as 'light' | 'dark') || 'light';
+ });
+
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', theme);
+ localStorage.setItem('theme', theme);
+ }, [theme]);
+
+ const toggleTheme = () => {
+ setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
+ };
const handleLogout = () => {
logout();
@@ -64,7 +77,7 @@ export default function Layout({ children }: LayoutProps) {
.navbar-user {
display: flex;
align-items: center;
- gap: 1rem;
+ gap: 0.75rem;
}
.navbar-email {
@@ -72,6 +85,21 @@ export default function Layout({ children }: LayoutProps) {
font-size: 0.875rem;
}
+ .theme-toggle {
+ background: var(--background);
+ border: 1px solid var(--border);
+ border-radius: 0.5rem;
+ padding: 0.5rem;
+ cursor: pointer;
+ font-size: 1.25rem;
+ line-height: 1;
+ transition: all 0.2s;
+ }
+
+ .theme-toggle:hover {
+ border-color: var(--primary);
+ }
+
.main-content {
flex: 1;
padding: 2rem 1rem;
@@ -96,14 +124,23 @@ export default function Layout({ children }: LayoutProps) {
PriceGhost
- {user && (
-
- {user.email}
-
-
- )}
+
+
+ {user && (
+ <>
+ {user.email}
+
+ >
+ )}
+
diff --git a/frontend/src/components/PriceChart.tsx b/frontend/src/components/PriceChart.tsx
index f0a7ea8..5fb7ab6 100644
--- a/frontend/src/components/PriceChart.tsx
+++ b/frontend/src/components/PriceChart.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import {
LineChart,
Line,
@@ -11,6 +11,16 @@ import {
} from 'recharts';
import { PriceHistory } from '../api/client';
+const getThemeColors = () => {
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
+ return {
+ grid: isDark ? '#334155' : '#e2e8f0',
+ text: isDark ? '#64748b' : '#94a3b8',
+ tooltip: isDark ? '#1e293b' : 'white',
+ tooltipBorder: isDark ? '#334155' : '#e2e8f0',
+ };
+};
+
interface PriceChartProps {
prices: PriceHistory[];
currency: string;
@@ -30,6 +40,18 @@ export default function PriceChart({
onRangeChange,
}: PriceChartProps) {
const [selectedRange, setSelectedRange] = useState
(30);
+ const [themeColors, setThemeColors] = useState(getThemeColors);
+
+ useEffect(() => {
+ const observer = new MutationObserver(() => {
+ setThemeColors(getThemeColors());
+ });
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['data-theme'],
+ });
+ return () => observer.disconnect();
+ }, []);
const handleRangeChange = (days: number | undefined) => {
setSelectedRange(days);
@@ -197,16 +219,16 @@ export default function PriceChart({
-
+
@@ -222,8 +244,8 @@ export default function PriceChart({
})
}
contentStyle={{
- background: 'white',
- border: '1px solid #e2e8f0',
+ background: themeColors.tooltip,
+ border: `1px solid ${themeColors.tooltipBorder}`,
borderRadius: '0.5rem',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
}}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 6ce56b3..ac5297f 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -16,6 +16,24 @@
--border: #e2e8f0;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+ --chart-grid: #e2e8f0;
+ --chart-text: #94a3b8;
+}
+
+[data-theme="dark"] {
+ --primary: #818cf8;
+ --primary-dark: #6366f1;
+ --secondary: #34d399;
+ --danger: #f87171;
+ --background: #0f172a;
+ --surface: #1e293b;
+ --text: #f1f5f9;
+ --text-muted: #94a3b8;
+ --border: #334155;
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
+ --chart-grid: #334155;
+ --chart-text: #64748b;
}
body {
@@ -69,6 +87,8 @@ input, button {
border: 1px solid var(--border);
border-radius: 0.5rem;
font-size: 1rem;
+ background-color: var(--surface);
+ color: var(--text);
transition: border-color 0.2s, box-shadow 0.2s;
}
@@ -154,12 +174,24 @@ input, button {
border: 1px solid #fecaca;
}
+[data-theme="dark"] .alert-error {
+ background-color: #450a0a;
+ color: #fca5a5;
+ border: 1px solid #7f1d1d;
+}
+
.alert-success {
background-color: #f0fdf4;
color: #166534;
border: 1px solid #bbf7d0;
}
+[data-theme="dark"] .alert-success {
+ background-color: #052e16;
+ color: #86efac;
+ border: 1px solid #166534;
+}
+
/* Loading spinner */
.spinner {
display: inline-block;