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:
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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 }} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user