Consolidate white_label to remove_branding and add embed widget

- Rename white_label feature to remove_branding across frontend/backend
- Update billing catalog, plan features, and permission checks
- Add dark mode support to Recharts tooltips with useDarkMode hook
- Create embeddable booking widget with EmbedBooking page
- Add EmbedWidgetSettings for generating embed code
- Fix Appearance settings page permission check
- Update test files for new feature naming
- Add notes field to User model

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-16 21:20:17 -05:00
parent 73d2bee01a
commit 6a6ad63e7b
31 changed files with 2115 additions and 181 deletions

View File

@@ -68,7 +68,7 @@ describe('UpgradePrompt', () => {
});
it('should render for any feature in inline mode', () => {
const features: FeatureKey[] = ['plugins', 'custom_domain', 'white_label'];
const features: FeatureKey[] = ['plugins', 'custom_domain', 'remove_branding'];
features.forEach((feature) => {
const { unmount } = renderWithRouter(
@@ -140,7 +140,7 @@ describe('UpgradePrompt', () => {
'webhooks',
'api_access',
'custom_domain',
'white_label',
'remove_branding',
'plugins',
];
@@ -243,7 +243,7 @@ describe('UpgradePrompt', () => {
it('should make children non-interactive', () => {
renderWithRouter(
<UpgradePrompt feature="white_label" variant="overlay">
<UpgradePrompt feature="remove_branding" variant="overlay">
<button data-testid="locked-button">Click Me</button>
</UpgradePrompt>
);
@@ -374,7 +374,7 @@ describe('LockedSection', () => {
describe('Different Features', () => {
it('should work with different feature keys', () => {
const features: FeatureKey[] = [
'white_label',
'remove_branding',
'custom_oauth',
'can_create_plugins',
'tasks',
@@ -470,7 +470,7 @@ describe('LockedButton', () => {
const handleClick = vi.fn();
renderWithRouter(
<LockedButton
feature="white_label"
feature="remove_branding"
isLocked={true}
onClick={handleClick}
>

View File

@@ -11,6 +11,7 @@ import {
Line,
} from 'recharts';
import { GripVertical, X } from 'lucide-react';
import { useDarkMode, getChartTooltipStyles } from '../../hooks/useDarkMode';
interface ChartData {
name: string;
@@ -36,6 +37,8 @@ const ChartWidget: React.FC<ChartWidgetProps> = ({
isEditing,
onRemove,
}) => {
const isDark = useDarkMode();
const tooltipStyles = getChartTooltipStyles(isDark);
const formatValue = (value: number) => `${valuePrefix}${value}`;
return (
@@ -58,8 +61,8 @@ const ChartWidget: React.FC<ChartWidgetProps> = ({
{title}
</h3>
<div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%" minWidth={100} minHeight={100}>
<div className="flex-1 min-h-[200px]">
<ResponsiveContainer width="100%" height={200}>
{type === 'bar' ? (
<BarChart data={data}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#374151" strokeOpacity={0.2} />
@@ -67,13 +70,7 @@ const ChartWidget: React.FC<ChartWidgetProps> = ({
<YAxis axisLine={false} tickLine={false} tickFormatter={formatValue} tick={{ fill: '#9CA3AF', fontSize: 12 }} />
<Tooltip
cursor={{ fill: 'rgba(107, 114, 128, 0.1)' }}
contentStyle={{
borderRadius: '8px',
border: 'none',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
backgroundColor: '#1F2937',
color: '#F3F4F6',
}}
contentStyle={tooltipStyles.contentStyle}
formatter={(value: number) => [formatValue(value), title]}
/>
<Bar dataKey="value" fill={color} radius={[4, 4, 0, 0]} />
@@ -84,13 +81,7 @@ const ChartWidget: React.FC<ChartWidgetProps> = ({
<XAxis dataKey="name" axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF', fontSize: 12 }} />
<YAxis axisLine={false} tickLine={false} tick={{ fill: '#9CA3AF', fontSize: 12 }} />
<Tooltip
contentStyle={{
borderRadius: '8px',
border: 'none',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
backgroundColor: '#1F2937',
color: '#F3F4F6',
}}
contentStyle={tooltipStyles.contentStyle}
formatter={(value: number) => [value, title]}
/>
<Line type="monotone" dataKey="value" stroke={color} strokeWidth={3} dot={{ r: 4, fill: color }} />

View File

@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { GripVertical, X, Users, UserPlus, UserCheck } from 'lucide-react';
import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts';
import { Customer } from '../../types';
import { useDarkMode, getChartTooltipStyles } from '../../hooks/useDarkMode';
interface CustomerBreakdownWidgetProps {
customers: Customer[];
@@ -16,6 +17,8 @@ const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
onRemove,
}) => {
const { t } = useTranslation();
const isDark = useDarkMode();
const tooltipStyles = getChartTooltipStyles(isDark);
const breakdownData = useMemo(() => {
// Customers with lastVisit are returning, without are new
const returning = customers.filter((c) => c.lastVisit !== null).length;
@@ -57,8 +60,8 @@ const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
<div className="flex-1 flex items-center gap-3 min-h-0">
{/* Pie Chart */}
<div className="w-20 h-20 flex-shrink-0">
<ResponsiveContainer width="100%" height="100%" minWidth={60} minHeight={60}>
<div className="flex-shrink-0">
<ResponsiveContainer width={80} height={80}>
<PieChart>
<Pie
data={breakdownData.chartData}
@@ -73,15 +76,7 @@ const CustomerBreakdownWidget: React.FC<CustomerBreakdownWidgetProps> = ({
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip
contentStyle={{
borderRadius: '8px',
border: 'none',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
backgroundColor: '#1F2937',
color: '#F3F4F6',
}}
/>
<Tooltip contentStyle={tooltipStyles.contentStyle} />
</PieChart>
</ResponsiveContainer>
</div>

View File

@@ -53,8 +53,7 @@ const FEATURE_CATEGORIES = [
features: [
{ code: 'custom_domain', label: 'Custom domain' },
{ code: 'custom_branding', label: 'Custom branding' },
{ code: 'remove_branding', label: 'Remove "Powered by"' },
{ code: 'white_label', label: 'White label' },
{ code: 'remove_branding', label: 'Remove branding' },
],
},
{

View File

@@ -117,11 +117,11 @@ export const PERMISSION_DEFINITIONS: PermissionDefinition[] = [
category: 'customization',
},
{
key: 'white_label',
key: 'remove_branding',
planKey: 'can_white_label',
businessKey: 'can_white_label',
label: 'White Labelling',
description: 'Remove SmoothSchedule branding',
label: 'Remove Branding',
description: 'Remove SmoothSchedule branding from customer-facing pages',
category: 'customization',
},