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:
poduck
2025-12-03 01:35:59 -05:00
parent ef58e9fc94
commit 5cef01ad0d
25 changed files with 2220 additions and 330 deletions

View File

@@ -2,13 +2,17 @@
* Branding Settings Page
*
* Logo uploads, colors, and display preferences.
* Features live preview of color changes that revert on navigation/reload if not saved.
*/
import React, { useState } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useOutletContext } from 'react-router-dom';
import { Palette, Save, Check, Upload, X, Image as ImageIcon } from 'lucide-react';
import { Business, User } from '../../types';
import { applyBrandColors } from '../../utils/colorUtils';
import { UpgradePrompt } from '../../components/UpgradePrompt';
import { FeatureKey } from '../../hooks/usePlanFeatures';
// Color palette options
const colorPalettes = [
@@ -26,10 +30,12 @@ const colorPalettes = [
const BrandingSettings: React.FC = () => {
const { t } = useTranslation();
const { business, updateBusiness, user } = useOutletContext<{
const { business, updateBusiness, user, isFeatureLocked, lockedFeature } = useOutletContext<{
business: Business;
updateBusiness: (updates: Partial<Business>) => void;
user: User;
isFeatureLocked?: boolean;
lockedFeature?: FeatureKey;
}>();
const [formState, setFormState] = useState({
@@ -41,8 +47,37 @@ const BrandingSettings: React.FC = () => {
});
const [showToast, setShowToast] = useState(false);
// Store the original saved colors to restore on unmount/navigation
const savedColorsRef = useRef({
primary: business.primaryColor,
secondary: business.secondaryColor || business.primaryColor,
});
// Live preview: Update CSS variables as user cycles through palettes
useEffect(() => {
applyBrandColors(formState.primaryColor, formState.secondaryColor);
// Cleanup: Restore saved colors when component unmounts (navigation away)
return () => {
applyBrandColors(savedColorsRef.current.primary, savedColorsRef.current.secondary);
};
}, [formState.primaryColor, formState.secondaryColor]);
// Update savedColorsRef when business data changes (after successful save)
useEffect(() => {
savedColorsRef.current = {
primary: business.primaryColor,
secondary: business.secondaryColor || business.primaryColor,
};
}, [business.primaryColor, business.secondaryColor]);
const handleSave = async () => {
await updateBusiness(formState);
// Update the saved reference so cleanup doesn't revert
savedColorsRef.current = {
primary: formState.primaryColor,
secondary: formState.secondaryColor,
};
setShowToast(true);
setTimeout(() => setShowToast(false), 3000);
};
@@ -63,6 +98,11 @@ const BrandingSettings: React.FC = () => {
);
}
// Show upgrade prompt if feature is locked
if (isFeatureLocked && lockedFeature) {
return <UpgradePrompt feature={lockedFeature} />;
}
return (
<div className="space-y-6">
{/* Header */}