fix: Image upload, remove WP login branding, implement dark mode

## 1. Fix Image Upload 

**Issue:** Image upload failing due to missing nonce

**Fix:**
- Better nonce detection (wpApiSettings, WooNooW, meta tag)
- Added credentials: 'same-origin'
- Better error handling with error messages
- Clarified image size recommendations (not strict requirements)

**Changes:**
- Logo: "Recommended size: 200x60px (or similar ratio)"
- Icon: "Recommended: 32x32px or larger square image"

---

## 2. Remove WordPress Login Page Branding 

**Issue:** Misunderstood - implemented WP login branding instead of SPA login

**Fix:**
- Removed all WordPress login page customization
- Removed login_enqueue_scripts hook
- Removed login_headerurl filter
- Removed login_headertext filter
- Removed customize_login_page() method
- Removed login_logo_url() method
- Removed login_logo_title() method

**Note:** WooNooW uses standalone SPA login, not WordPress login page

---

## 3. Implement Dark/Light Mode 

### Components Created:

**ThemeProvider.tsx:**
- Theme context (light, dark, system)
- Automatic system theme detection
- localStorage persistence (woonoow_theme)
- Applies .light or .dark class to <html>
- Listens for system theme changes

**ThemeToggle.tsx:**
- Dropdown menu with 3 options:
  - ☀️ Light
  - 🌙 Dark
  - 🖥️ System
- Shows current selection with checkmark
- Icon changes based on actual theme

### Integration:
- Wrapped App with ThemeProvider in main.tsx
- Added ThemeToggle to header (before fullscreen button)
- Uses existing dark mode CSS variables (already configured)

### Features:
-  Light mode
-  Dark mode
-  System preference (auto)
-  Persists in localStorage
-  Smooth transitions
-  Icon updates dynamically

### CSS:
- Already configured: darkMode: ["class"] in tailwind.config.js
- Dark mode variables already defined in index.css
- No additional CSS needed

---

## Result

 Image upload fixed with better error handling
 WordPress login branding removed (not needed)
 Dark/Light mode fully functional
 Theme toggle in header
 System preference support
 Persists across sessions

**Ready to test!**
This commit is contained in:
dwindown
2025-11-10 23:18:56 +07:00
parent e369d31974
commit 64cfa39b75
7 changed files with 145 additions and 118 deletions

View File

@@ -38,6 +38,7 @@ import { FAB } from '@/components/FAB';
import { useActiveSection } from '@/hooks/useActiveSection';
import { NAV_TREE_VERSION } from '@/nav/tree';
import { __ } from '@/lib/i18n';
import { ThemeToggle } from '@/components/ThemeToggle';
function useFullscreen() {
const [on, setOn] = useState<boolean>(() => {
@@ -362,6 +363,7 @@ function Header({ onFullscreen, fullscreen, showToggle = true, scrollContainerRe
</button>
</>
)}
<ThemeToggle />
{showToggle && (
<button
onClick={onFullscreen}

View File

@@ -0,0 +1,78 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'light' | 'dark' | 'system';
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
actualTheme: 'light' | 'dark';
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setThemeState] = useState<Theme>(() => {
const stored = localStorage.getItem('woonoow_theme');
return (stored as Theme) || 'system';
});
const [actualTheme, setActualTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
const root = window.document.documentElement;
// Remove previous theme classes
root.classList.remove('light', 'dark');
let effectiveTheme: 'light' | 'dark';
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
effectiveTheme = systemTheme;
} else {
effectiveTheme = theme;
}
root.classList.add(effectiveTheme);
setActualTheme(effectiveTheme);
}, [theme]);
// Listen for system theme changes
useEffect(() => {
if (theme !== 'system') return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
const systemTheme = mediaQuery.matches ? 'dark' : 'light';
root.classList.add(systemTheme);
setActualTheme(systemTheme);
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, [theme]);
const setTheme = (newTheme: Theme) => {
localStorage.setItem('woonoow_theme', newTheme);
setThemeState(newTheme);
};
return (
<ThemeContext.Provider value={{ theme, setTheme, actualTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

View File

@@ -0,0 +1,46 @@
import React from 'react';
import { Moon, Sun, Monitor } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useTheme } from './ThemeProvider';
export function ThemeToggle() {
const { theme, setTheme, actualTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-9 w-9">
{actualTheme === 'dark' ? (
<Moon className="h-4 w-4" />
) : (
<Sun className="h-4 w-4" />
)}
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>
<Sun className="mr-2 h-4 w-4" />
<span>Light</span>
{theme === 'light' && <span className="ml-auto"></span>}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
<Moon className="mr-2 h-4 w-4" />
<span>Dark</span>
{theme === 'dark' && <span className="ml-auto"></span>}
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
<Monitor className="mr-2 h-4 w-4" />
<span>System</span>
{theme === 'system' && <span className="ml-auto"></span>}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@@ -74,24 +74,31 @@ export function ImageUpload({
const formData = new FormData();
formData.append('file', file);
// Get nonce from REST API settings
const nonce = (window as any).wpApiSettings?.nonce ||
(window as any).WooNooW?.nonce ||
document.querySelector('meta[name="wp-rest-nonce"]')?.getAttribute('content') || '';
// Upload to WordPress media library
const response = await fetch('/wp-json/wp/v2/media', {
method: 'POST',
headers: {
'X-WP-Nonce': (window as any).wpApiSettings?.nonce || '',
'X-WP-Nonce': nonce,
},
credentials: 'same-origin',
body: formData,
});
if (!response.ok) {
throw new Error('Upload failed');
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || 'Upload failed');
}
const data = await response.json();
onChange(data.source_url);
} catch (error) {
console.error('Upload error:', error);
alert('Failed to upload image');
alert(error instanceof Error ? error.message : 'Failed to upload image');
} finally {
setIsUploading(false);
}

View File

@@ -2,10 +2,15 @@ import React from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App';
import { ThemeProvider } from './components/ThemeProvider';
const el = document.getElementById('woonoow-admin-app');
if (el) {
createRoot(el).render(<App />);
createRoot(el).render(
<ThemeProvider>
<App />
</ThemeProvider>
);
} else {
console.warn('[WooNooW] Root element #woonoow-admin-app not found.');
}

View File

@@ -325,7 +325,7 @@ export default function StoreDetailsPage() {
title="Brand"
description="Logo, icon, and colors for your store"
>
<SettingsSection label="Store logo" description="Recommended: 200x60px PNG with transparent background">
<SettingsSection label="Store logo" description="Recommended size: 200x60px (or similar ratio). PNG with transparent background works best.">
<ImageUpload
value={settings.storeLogo}
onChange={(url) => updateSetting('storeLogo', url)}
@@ -334,7 +334,7 @@ export default function StoreDetailsPage() {
/>
</SettingsSection>
<SettingsSection label="Store icon" description="Favicon for browser tabs (32x32px)">
<SettingsSection label="Store icon" description="Favicon for browser tabs. Recommended: 32x32px or larger square image.">
<ImageUpload
value={settings.storeIcon}
onChange={(url) => updateSetting('storeIcon', url)}

View File

@@ -21,12 +21,8 @@ class Branding {
// Apply favicon
add_action('wp_head', [__CLASS__, 'inject_favicon']);
add_action('admin_head', [__CLASS__, 'inject_favicon']);
add_action('login_head', [__CLASS__, 'inject_favicon']);
// Customize login page
add_action('login_enqueue_scripts', [__CLASS__, 'customize_login_page']);
add_filter('login_headerurl', [__CLASS__, 'login_logo_url']);
add_filter('login_headertext', [__CLASS__, 'login_logo_title']);
// Note: Login page branding removed - WooNooW uses standalone SPA login
}
/**
@@ -62,113 +58,6 @@ class Branding {
}
}
/**
* Customize login page
*/
public static function customize_login_page() {
$logo = get_option('woonoow_store_logo', '');
$store_name = get_option('blogname', 'WooNooW');
$tagline = get_option('blogdescription', '');
$primary = get_option('woonoow_primary_color', '#3b82f6');
?>
<style type="text/css">
/* Brand colors */
:root {
--woonoow-primary: <?php echo esc_attr($primary); ?>;
}
/* Logo */
#login h1 a {
<?php if (!empty($logo)): ?>
background-image: url(<?php echo esc_url($logo); ?>);
background-size: contain;
background-position: center;
width: 100%;
height: 80px;
<?php else: ?>
/* Text logo fallback */
background: none !important;
width: auto !important;
height: auto !important;
font-size: 32px;
font-weight: 700;
color: var(--woonoow-primary);
text-indent: 0 !important;
<?php endif; ?>
}
<?php if (empty($logo)): ?>
#login h1 a::after {
content: '<?php echo esc_js($store_name); ?>';
display: block;
}
<?php endif; ?>
/* Tagline */
<?php if (!empty($tagline)): ?>
#login h1::after {
content: '<?php echo esc_js($tagline); ?>';
display: block;
text-align: center;
margin-top: 10px;
font-size: 14px;
color: #666;
font-weight: normal;
}
<?php endif; ?>
/* Primary button color */
.wp-core-ui .button-primary {
background: var(--woonoow-primary);
border-color: var(--woonoow-primary);
box-shadow: none;
text-shadow: none;
}
.wp-core-ui .button-primary:hover,
.wp-core-ui .button-primary:focus {
background: var(--woonoow-primary);
border-color: var(--woonoow-primary);
opacity: 0.9;
}
/* Link color */
#login a {
color: var(--woonoow-primary);
}
#login a:hover,
#login a:focus {
color: var(--woonoow-primary);
opacity: 0.8;
}
/* Input focus */
input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus {
border-color: var(--woonoow-primary);
box-shadow: 0 0 0 1px var(--woonoow-primary);
}
</style>
<?php
}
/**
* Change login logo URL
*/
public static function login_logo_url() {
return home_url();
}
/**
* Change login logo title
*/
public static function login_logo_title() {
return get_option('blogname', 'WooNooW');
}
/**
* Get logo URL or fallback to text
*