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:
145
frontend/src/pages/settings/BookingSettings.tsx
Normal file
145
frontend/src/pages/settings/BookingSettings.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Booking Settings Page
|
||||
*
|
||||
* Manage booking URLs and customer redirect settings.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import {
|
||||
Calendar, Link2, Copy, ExternalLink, Save, CheckCircle
|
||||
} from 'lucide-react';
|
||||
import { Business, User } from '../../types';
|
||||
|
||||
const BookingSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { business, user, updateBusiness } = useOutletContext<{
|
||||
business: Business;
|
||||
user: User;
|
||||
updateBusiness: (updates: Partial<Business>) => void;
|
||||
}>();
|
||||
|
||||
// Local state
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
const [returnUrl, setReturnUrl] = useState(business.bookingReturnUrl || '');
|
||||
const [returnUrlSaving, setReturnUrlSaving] = useState(false);
|
||||
|
||||
const isOwner = user.role === 'owner';
|
||||
|
||||
const handleSaveReturnUrl = async () => {
|
||||
setReturnUrlSaving(true);
|
||||
try {
|
||||
await updateBusiness({ bookingReturnUrl: returnUrl });
|
||||
setShowToast(true);
|
||||
setTimeout(() => setShowToast(false), 3000);
|
||||
} catch (error) {
|
||||
alert('Failed to save return URL');
|
||||
} finally {
|
||||
setReturnUrlSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOwner) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-gray-500 dark:text-gray-400">
|
||||
Only the business owner can access these settings.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
|
||||
<Calendar className="text-brand-500" />
|
||||
{t('settings.booking.title', 'Booking')}
|
||||
</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400 mt-1">
|
||||
Configure your booking page URL and customer redirect settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Booking URL */}
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Link2 size={20} className="text-brand-500" /> Your Booking URL
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
Share this URL with your customers so they can book appointments with you.
|
||||
</p>
|
||||
<div className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
|
||||
<code className="flex-1 text-sm font-mono text-gray-900 dark:text-white">
|
||||
{business.subdomain}.smoothschedule.com
|
||||
</code>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`https://${business.subdomain}.smoothschedule.com`);
|
||||
setShowToast(true);
|
||||
setTimeout(() => setShowToast(false), 2000);
|
||||
}}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
<a
|
||||
href={`https://${business.subdomain}.smoothschedule.com`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-2 text-brand-500 hover:text-brand-600 dark:hover:text-brand-400 transition-colors"
|
||||
title="Open booking page"
|
||||
>
|
||||
<ExternalLink size={16} />
|
||||
</a>
|
||||
</div>
|
||||
<p className="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
Want to use your own domain? Set up a <a href="/settings/custom-domains" className="text-brand-500 hover:underline">custom domain</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Return URL - Where to redirect customers after booking */}
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
|
||||
<ExternalLink size={20} className="text-green-500" /> Return URL
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
|
||||
After a customer completes a booking, redirect them to this URL (e.g., a thank you page on your website).
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="url"
|
||||
value={returnUrl}
|
||||
onChange={(e) => setReturnUrl(e.target.value)}
|
||||
placeholder="https://yourbusiness.com/thank-you"
|
||||
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-brand-500 text-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSaveReturnUrl}
|
||||
disabled={returnUrlSaving}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium text-sm"
|
||||
>
|
||||
<Save size={16} />
|
||||
{returnUrlSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Leave empty to keep customers on the booking confirmation page.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Toast */}
|
||||
{showToast && (
|
||||
<div className="fixed bottom-4 right-4 flex items-center gap-2 px-4 py-3 bg-green-500 text-white rounded-lg shadow-lg">
|
||||
<CheckCircle size={18} />
|
||||
Copied to clipboard
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookingSettings;
|
||||
@@ -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 */}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,15 @@
|
||||
/**
|
||||
* Domains Settings Page
|
||||
* Custom Domains Settings Page
|
||||
*
|
||||
* Manage custom domains and booking URLs for the business.
|
||||
* Manage custom domains - BYOD and domain purchase.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { useOutletContext, Link } from 'react-router-dom';
|
||||
import {
|
||||
Globe, Link2, Copy, Star, Trash2, RefreshCw, CheckCircle, AlertCircle,
|
||||
ShoppingCart, Crown
|
||||
Globe, Copy, Star, Trash2, RefreshCw, CheckCircle, AlertCircle,
|
||||
ShoppingCart, Lock, ArrowUpRight
|
||||
} from 'lucide-react';
|
||||
import { Business, User, CustomDomain } from '../../types';
|
||||
import {
|
||||
@@ -21,9 +21,8 @@ import {
|
||||
} from '../../hooks/useCustomDomains';
|
||||
import DomainPurchase from '../../components/DomainPurchase';
|
||||
import { usePlanFeatures } from '../../hooks/usePlanFeatures';
|
||||
import { LockedSection } from '../../components/UpgradePrompt';
|
||||
|
||||
const DomainsSettings: React.FC = () => {
|
||||
const CustomDomainsSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { business, user } = useOutletContext<{
|
||||
business: Business;
|
||||
@@ -115,51 +114,45 @@ const DomainsSettings: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const isCustomDomainLocked = !canUse('custom_domain');
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
|
||||
<Globe className="text-indigo-500" />
|
||||
{t('settings.domains.title', 'Custom Domains')}
|
||||
{t('settings.customDomains.title', 'Custom Domains')}
|
||||
</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400 mt-1">
|
||||
Configure custom domains for your booking pages.
|
||||
Use your own domains for your booking pages.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<LockedSection feature="custom_domain" isLocked={!canUse('custom_domain')}>
|
||||
{/* Quick Domain Setup - Booking URL */}
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Link2 size={20} className="text-brand-500" /> Your Booking URL
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-900/50 rounded-lg">
|
||||
<code className="flex-1 text-sm font-mono text-gray-900 dark:text-white">
|
||||
{business.subdomain}.smoothschedule.com
|
||||
</code>
|
||||
<button
|
||||
onClick={() => navigator.clipboard.writeText(`${business.subdomain}.smoothschedule.com`)}
|
||||
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Custom Domains Management */}
|
||||
{business.plan !== 'Free' ? (
|
||||
<>
|
||||
{/* Custom Domains Management - with overlay when locked */}
|
||||
<div className="relative">
|
||||
{isCustomDomainLocked && (
|
||||
<div className="absolute inset-0 z-10 bg-white/70 dark:bg-gray-900/70 backdrop-blur-[2px] rounded-xl flex items-center justify-center">
|
||||
<Link
|
||||
to="/settings/billing"
|
||||
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-amber-500 to-orange-500 text-white font-semibold rounded-lg shadow-lg hover:from-amber-600 hover:to-orange-600 transition-all"
|
||||
>
|
||||
<Lock size={18} />
|
||||
Upgrade to Enable Custom Domains
|
||||
<ArrowUpRight size={18} />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className={isCustomDomainLocked ? 'pointer-events-none select-none' : ''}>
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<Globe size={20} className="text-indigo-500" />
|
||||
Custom Domains
|
||||
Bring Your Own Domain
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Use your own domains for your booking pages
|
||||
Connect a domain you already own
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -288,7 +281,7 @@ const DomainsSettings: React.FC = () => {
|
||||
</section>
|
||||
|
||||
{/* Domain Purchase */}
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm mt-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
@@ -302,26 +295,8 @@ const DomainsSettings: React.FC = () => {
|
||||
</div>
|
||||
<DomainPurchase />
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
/* Upgrade prompt for free plans */
|
||||
<section className="bg-gradient-to-br from-amber-50 to-orange-50 dark:from-amber-900/20 dark:to-orange-900/20 p-6 rounded-xl border border-amber-200 dark:border-amber-800">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="p-3 bg-amber-100 dark:bg-amber-900/40 rounded-lg">
|
||||
<Crown size={24} className="text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">Unlock Custom Domains</h4>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
||||
Upgrade to use your own domain (e.g., <span className="font-mono">book.yourbusiness.com</span>) or purchase a new one.
|
||||
</p>
|
||||
<button className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-gradient-to-r from-indigo-500 to-purple-500 rounded-lg hover:from-indigo-600 hover:to-purple-600 transition-all">
|
||||
<Crown size={16} /> View Plans
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toast */}
|
||||
{showToast && (
|
||||
@@ -330,9 +305,8 @@ const DomainsSettings: React.FC = () => {
|
||||
Changes saved successfully
|
||||
</div>
|
||||
)}
|
||||
</LockedSection>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DomainsSettings;
|
||||
export default CustomDomainsSettings;
|
||||
@@ -7,7 +7,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useOutletContext } from 'react-router-dom';
|
||||
import { Building2, Save, Check } from 'lucide-react';
|
||||
import { Building2, Save, Check, Globe } from 'lucide-react';
|
||||
import { Business, User } from '../../types';
|
||||
|
||||
const GeneralSettings: React.FC = () => {
|
||||
@@ -23,14 +23,40 @@ const GeneralSettings: React.FC = () => {
|
||||
subdomain: business.subdomain,
|
||||
contactEmail: business.contactEmail || '',
|
||||
phone: business.phone || '',
|
||||
timezone: business.timezone || 'America/New_York',
|
||||
timezoneDisplayMode: business.timezoneDisplayMode || 'business',
|
||||
});
|
||||
const [showToast, setShowToast] = useState(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormState(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
// Common timezones grouped by region
|
||||
const commonTimezones = [
|
||||
{ value: 'America/New_York', label: 'Eastern Time (New York)' },
|
||||
{ value: 'America/Chicago', label: 'Central Time (Chicago)' },
|
||||
{ value: 'America/Denver', label: 'Mountain Time (Denver)' },
|
||||
{ value: 'America/Los_Angeles', label: 'Pacific Time (Los Angeles)' },
|
||||
{ value: 'America/Anchorage', label: 'Alaska Time' },
|
||||
{ value: 'Pacific/Honolulu', label: 'Hawaii Time' },
|
||||
{ value: 'America/Phoenix', label: 'Arizona (no DST)' },
|
||||
{ value: 'America/Toronto', label: 'Eastern Time (Toronto)' },
|
||||
{ value: 'America/Vancouver', label: 'Pacific Time (Vancouver)' },
|
||||
{ value: 'Europe/London', label: 'London (GMT/BST)' },
|
||||
{ value: 'Europe/Paris', label: 'Central European Time' },
|
||||
{ value: 'Europe/Berlin', label: 'Berlin' },
|
||||
{ value: 'Asia/Tokyo', label: 'Japan Time' },
|
||||
{ value: 'Asia/Shanghai', label: 'China Time' },
|
||||
{ value: 'Asia/Singapore', label: 'Singapore Time' },
|
||||
{ value: 'Asia/Dubai', label: 'Dubai (GST)' },
|
||||
{ value: 'Australia/Sydney', label: 'Sydney (AEST)' },
|
||||
{ value: 'Australia/Melbourne', label: 'Melbourne (AEST)' },
|
||||
{ value: 'Pacific/Auckland', label: 'New Zealand Time' },
|
||||
{ value: 'UTC', label: 'UTC' },
|
||||
];
|
||||
|
||||
const handleSave = async () => {
|
||||
await updateBusiness(formState);
|
||||
setShowToast(true);
|
||||
@@ -103,6 +129,59 @@ const GeneralSettings: React.FC = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Timezone Settings */}
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Globe size={20} className="text-brand-500" />
|
||||
{t('settings.timezone.title', 'Timezone Settings')}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('settings.timezone.businessTimezone', 'Business Timezone')}
|
||||
</label>
|
||||
<select
|
||||
name="timezone"
|
||||
value={formState.timezone}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
>
|
||||
{commonTimezones.map(tz => (
|
||||
<option key={tz.value} value={tz.value}>
|
||||
{tz.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{t('settings.timezone.businessTimezoneHint', 'The timezone where your business operates.')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{t('settings.timezone.displayMode', 'Time Display Mode')}
|
||||
</label>
|
||||
<select
|
||||
name="timezoneDisplayMode"
|
||||
value={formState.timezoneDisplayMode}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg focus:ring-2 focus:ring-brand-500"
|
||||
>
|
||||
<option value="business">
|
||||
{t('settings.timezone.businessMode', 'Business Timezone')}
|
||||
</option>
|
||||
<option value="viewer">
|
||||
{t('settings.timezone.viewerMode', "Viewer's Local Timezone")}
|
||||
</option>
|
||||
</select>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{formState.timezoneDisplayMode === 'business'
|
||||
? t('settings.timezone.businessModeHint', 'All appointment times are displayed in your business timezone.')
|
||||
: t('settings.timezone.viewerModeHint', 'Appointment times adapt to each viewer\'s local timezone.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Information */}
|
||||
<section className="bg-white dark:bg-gray-800 p-6 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
|
||||
Reference in New Issue
Block a user