feat: Reorganize settings sidebar and add plan-based feature locking
- Add locked state to Plugins sidebar item with plan feature check - Create Branding section in settings with Appearance, Email Templates, Custom Domains - Split Domains page into Booking (URLs, redirects) and Custom Domains (BYOD, purchase) - Add booking_return_url field to Tenant model for customer redirects - Update SidebarItem component to support locked prop with lock icon - Move Email Templates from main sidebar to Settings > Branding - Add communication credits hooks and payment form updates - Add timezone fields migration and various UI improvements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,88 +15,7 @@ import { useTicket } from '../hooks/useTickets';
|
||||
import { MasqueradeStackEntry } from '../api/auth';
|
||||
import { useScrollToTop } from '../hooks/useScrollToTop';
|
||||
import { SandboxProvider, useSandbox } from '../contexts/SandboxContext';
|
||||
|
||||
/**
|
||||
* Convert a hex color to HSL values
|
||||
*/
|
||||
function hexToHSL(hex: string): { h: number; s: number; l: number } {
|
||||
// Remove # if present
|
||||
hex = hex.replace(/^#/, '');
|
||||
|
||||
// Parse hex values
|
||||
const r = parseInt(hex.substring(0, 2), 16) / 255;
|
||||
const g = parseInt(hex.substring(2, 4), 16) / 255;
|
||||
const b = parseInt(hex.substring(4, 6), 16) / 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0;
|
||||
let s = 0;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case r:
|
||||
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
||||
break;
|
||||
case g:
|
||||
h = ((b - r) / d + 2) / 6;
|
||||
break;
|
||||
case b:
|
||||
h = ((r - g) / d + 4) / 6;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { h: h * 360, s: s * 100, l: l * 100 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HSL values to hex color
|
||||
*/
|
||||
function hslToHex(h: number, s: number, l: number): string {
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
|
||||
const c = (1 - Math.abs(2 * l - 1)) * s;
|
||||
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
||||
const m = l - c / 2;
|
||||
|
||||
let r = 0, g = 0, b = 0;
|
||||
|
||||
if (h < 60) { r = c; g = x; b = 0; }
|
||||
else if (h < 120) { r = x; g = c; b = 0; }
|
||||
else if (h < 180) { r = 0; g = c; b = x; }
|
||||
else if (h < 240) { r = 0; g = x; b = c; }
|
||||
else if (h < 300) { r = x; g = 0; b = c; }
|
||||
else { r = c; g = 0; b = x; }
|
||||
|
||||
const toHex = (n: number) => Math.round((n + m) * 255).toString(16).padStart(2, '0');
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a color palette from a base color
|
||||
*/
|
||||
function generateColorPalette(baseColor: string): Record<string, string> {
|
||||
const { h, s } = hexToHSL(baseColor);
|
||||
|
||||
return {
|
||||
50: hslToHex(h, Math.min(s, 30), 97),
|
||||
100: hslToHex(h, Math.min(s, 40), 94),
|
||||
200: hslToHex(h, Math.min(s, 50), 86),
|
||||
300: hslToHex(h, Math.min(s, 60), 74),
|
||||
400: hslToHex(h, Math.min(s, 70), 60),
|
||||
500: hslToHex(h, s, 50),
|
||||
600: baseColor, // Use the exact primary color for 600
|
||||
700: hslToHex(h, s, 40),
|
||||
800: hslToHex(h, s, 32),
|
||||
900: hslToHex(h, s, 24),
|
||||
};
|
||||
}
|
||||
import { applyColorPalette, applyBrandColors, defaultColorPalette } from '../utils/colorUtils';
|
||||
|
||||
interface BusinessLayoutProps {
|
||||
business: Business;
|
||||
@@ -145,37 +64,19 @@ const BusinessLayoutContent: React.FC<BusinessLayoutProps> = ({ business, user,
|
||||
setTicketModalId(null);
|
||||
};
|
||||
|
||||
// Generate brand color palette from business primary color
|
||||
const brandPalette = useMemo(() => {
|
||||
return generateColorPalette(business.primaryColor || '#2563eb');
|
||||
}, [business.primaryColor]);
|
||||
|
||||
// Set CSS custom properties for brand colors
|
||||
// Set CSS custom properties for brand colors (primary palette + secondary color)
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
Object.entries(brandPalette).forEach(([shade, color]) => {
|
||||
root.style.setProperty(`--color-brand-${shade}`, color);
|
||||
});
|
||||
applyBrandColors(
|
||||
business.primaryColor || '#2563eb',
|
||||
business.secondaryColor || business.primaryColor || '#2563eb'
|
||||
);
|
||||
|
||||
// Cleanup: reset to defaults when component unmounts
|
||||
return () => {
|
||||
const defaultColors: Record<string, string> = {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
};
|
||||
Object.entries(defaultColors).forEach(([shade, color]) => {
|
||||
root.style.setProperty(`--color-brand-${shade}`, color);
|
||||
});
|
||||
applyColorPalette(defaultColorPalette);
|
||||
document.documentElement.style.setProperty('--color-brand-secondary', '#0ea5e9');
|
||||
};
|
||||
}, [brandPalette]);
|
||||
}, [business.primaryColor, business.secondaryColor]);
|
||||
|
||||
// Check for trial expiration and redirect
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user