Enhance tenant management UI by adding branding customization options in CreateTenantWizard and TenantDetails components. Implement file upload functionality for logo and favicon, and allow users to set primary, secondary, and accent colors for tenant branding. Update settings schema to accommodate new branding fields.
This commit is contained in:
parent
f07db4040e
commit
4226f67923
@ -9,7 +9,7 @@ import { FormField, FormSelect, PrimaryButton, SecondaryButton, MultiselectPagin
|
||||
import { tenantService } from '@/services/tenant-service';
|
||||
import { moduleService } from '@/services/module-service';
|
||||
import { showToast } from '@/utils/toast';
|
||||
import { ChevronRight, ChevronLeft } from 'lucide-react';
|
||||
import { ChevronRight, ChevronLeft, Image as ImageIcon } from 'lucide-react';
|
||||
|
||||
// Step 1: Tenant Details Schema - matches NewTenantModal
|
||||
const tenantDetailsSchema = z.object({
|
||||
@ -61,6 +61,9 @@ const contactDetailsSchema = z
|
||||
const settingsSchema = z.object({
|
||||
enable_sso: z.boolean(),
|
||||
enable_2fa: z.boolean(),
|
||||
primary_color: z.string().optional().nullable(),
|
||||
secondary_color: z.string().optional().nullable(),
|
||||
accent_color: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
type TenantDetailsForm = z.infer<typeof tenantDetailsSchema>;
|
||||
@ -144,9 +147,16 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
defaultValues: {
|
||||
enable_sso: false,
|
||||
enable_2fa: false,
|
||||
primary_color: '#112868',
|
||||
secondary_color: '#23DCE1',
|
||||
accent_color: '#084CC8',
|
||||
},
|
||||
});
|
||||
|
||||
// File upload state for branding
|
||||
const [logoFile, setLogoFile] = useState<File | null>(null);
|
||||
const [faviconFile, setFaviconFile] = useState<File | null>(null);
|
||||
|
||||
// Auto-generate slug and domain from name
|
||||
const nameValue = tenantDetailsForm.watch('name');
|
||||
const baseUrlWithoutProtocol = getBaseUrlWithoutProtocol();
|
||||
@ -265,12 +275,25 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
// Extract confirmPassword from contactDetails (not needed in API call)
|
||||
const { confirmPassword, ...contactData } = contactDetails;
|
||||
|
||||
// Extract branding colors from settings
|
||||
const { enable_sso, enable_2fa, primary_color, secondary_color, accent_color } = settings;
|
||||
|
||||
const tenantData = {
|
||||
...restTenantDetails,
|
||||
module_ids: selectedModules.length > 0 ? selectedModules : undefined,
|
||||
settings: {
|
||||
...settings,
|
||||
enable_sso,
|
||||
enable_2fa,
|
||||
contact: contactData, // Include first_name, last_name, email, password
|
||||
branding: {
|
||||
primary_color: primary_color || undefined,
|
||||
secondary_color: secondary_color || undefined,
|
||||
accent_color: accent_color || undefined,
|
||||
// Note: logo and favicon files would need to be uploaded separately via FormData
|
||||
// For now, we're just storing the file references
|
||||
logo_file: logoFile ? logoFile.name : undefined,
|
||||
favicon_file: faviconFile ? faviconFile.name : undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -663,13 +686,209 @@ const CreateTenantWizard = (): ReactElement => {
|
||||
|
||||
{/* Step 3: Settings */}
|
||||
{currentStep === 3 && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
<div className="pb-4 border-b border-[rgba(0,0,0,0.08)]">
|
||||
<h2 className="text-lg font-semibold text-[#0f1724]">Configuration & Limits</h2>
|
||||
<p className="text-sm text-[#6b7280] mt-1">
|
||||
Set resource limits and security preferences for this tenant.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Branding Section */}
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6 flex flex-col gap-4">
|
||||
{/* Section Header */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold text-[#0f1724]">Branding</h3>
|
||||
<p className="text-sm font-normal text-[#9ca3af]">
|
||||
Customize logo, favicon, and colors for this tenant experience.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Logo and Favicon Upload */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{/* Company Logo */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">Company Logo</label>
|
||||
<label
|
||||
htmlFor="logo-upload-wizard"
|
||||
className="bg-[#f5f7fa] border border-dashed border-[#d1d5db] rounded-md p-4 flex gap-4 items-center cursor-pointer hover:bg-[#e5e7eb] transition-colors"
|
||||
>
|
||||
<div className="bg-white border border-[#d1d5db] rounded-lg size-12 flex items-center justify-center shrink-0">
|
||||
<ImageIcon className="w-5 h-5 text-[#6b7280]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 flex-1 min-w-0">
|
||||
<span className="text-sm font-medium text-[#0f1724]">Upload Logo</span>
|
||||
<span className="text-xs font-normal text-[#9ca3af]">PNG, SVG, JPG up to 2MB.</span>
|
||||
</div>
|
||||
<input
|
||||
id="logo-upload-wizard"
|
||||
type="file"
|
||||
accept="image/png,image/svg+xml,image/jpeg,image/jpg"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// Validate file size (2MB max)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
showToast.error('Logo file size must be less than 2MB');
|
||||
return;
|
||||
}
|
||||
// Validate file type
|
||||
const validTypes = ['image/png', 'image/svg+xml', 'image/jpeg', 'image/jpg'];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
showToast.error('Logo must be PNG, SVG, or JPG format');
|
||||
return;
|
||||
}
|
||||
setLogoFile(file);
|
||||
}
|
||||
}}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
{logoFile && (
|
||||
<div className="text-xs text-[#6b7280] mt-1">
|
||||
Selected: {logoFile.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Favicon */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">Favicon</label>
|
||||
<label
|
||||
htmlFor="favicon-upload-wizard"
|
||||
className="bg-[#f5f7fa] border border-dashed border-[#d1d5db] rounded-md p-4 flex gap-4 items-center cursor-pointer hover:bg-[#e5e7eb] transition-colors"
|
||||
>
|
||||
<div className="bg-white border border-[#d1d5db] rounded-lg size-12 flex items-center justify-center shrink-0">
|
||||
<ImageIcon className="w-5 h-5 text-[#6b7280]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 flex-1 min-w-0">
|
||||
<span className="text-sm font-medium text-[#0f1724]">Upload Favicon</span>
|
||||
<span className="text-xs font-normal text-[#9ca3af]">ICO or PNG up to 500KB.</span>
|
||||
</div>
|
||||
<input
|
||||
id="favicon-upload-wizard"
|
||||
type="file"
|
||||
accept="image/x-icon,image/png,image/vnd.microsoft.icon"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// Validate file size (500KB max)
|
||||
if (file.size > 500 * 1024) {
|
||||
showToast.error('Favicon file size must be less than 500KB');
|
||||
return;
|
||||
}
|
||||
// Validate file type
|
||||
const validTypes = ['image/x-icon', 'image/png', 'image/vnd.microsoft.icon'];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
showToast.error('Favicon must be ICO or PNG format');
|
||||
return;
|
||||
}
|
||||
setFaviconFile(file);
|
||||
}
|
||||
}}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
{faviconFile && (
|
||||
<div className="text-xs text-[#6b7280] mt-1">
|
||||
Selected: {faviconFile.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Primary Color */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">Primary Color</label>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div
|
||||
className="border border-[#d1d5db] rounded-md size-10 shrink-0"
|
||||
style={{ backgroundColor: settingsForm.watch('primary_color') || '#112868' }}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={settingsForm.watch('primary_color') || '#112868'}
|
||||
onChange={(e) => settingsForm.setValue('primary_color', e.target.value)}
|
||||
className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent"
|
||||
placeholder="#112868"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="color"
|
||||
value={settingsForm.watch('primary_color') || '#112868'}
|
||||
onChange={(e) => settingsForm.setValue('primary_color', e.target.value)}
|
||||
className="size-10 rounded-md border border-[#d1d5db] cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for navigation, headers, and key actions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Secondary and Accent Colors */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{/* Secondary Color */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">Secondary Color</label>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div
|
||||
className="border border-[#d1d5db] rounded-md size-10 shrink-0"
|
||||
style={{ backgroundColor: settingsForm.watch('secondary_color') || '#23DCE1' }}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={settingsForm.watch('secondary_color') || '#23DCE1'}
|
||||
onChange={(e) => settingsForm.setValue('secondary_color', e.target.value)}
|
||||
className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent"
|
||||
placeholder="#23DCE1"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="color"
|
||||
value={settingsForm.watch('secondary_color') || '#23DCE1'}
|
||||
onChange={(e) => settingsForm.setValue('secondary_color', e.target.value)}
|
||||
className="size-10 rounded-md border border-[#d1d5db] cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for highlights and supporting elements.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Accent Color */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">Accent Color</label>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div
|
||||
className="border border-[#d1d5db] rounded-md size-10 shrink-0"
|
||||
style={{ backgroundColor: settingsForm.watch('accent_color') || '#084CC8' }}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={settingsForm.watch('accent_color') || '#084CC8'}
|
||||
onChange={(e) => settingsForm.setValue('accent_color', e.target.value)}
|
||||
className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent"
|
||||
placeholder="#084CC8"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="color"
|
||||
value={settingsForm.watch('accent_color') || '#084CC8'}
|
||||
onChange={(e) => settingsForm.setValue('accent_color', e.target.value)}
|
||||
className="size-10 rounded-md border border-[#d1d5db] cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for alerts and special notices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security Settings */}
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-md p-4 flex items-center justify-between">
|
||||
<div>
|
||||
|
||||
@ -12,6 +12,8 @@ import {
|
||||
Edit,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Settings,
|
||||
Image as ImageIcon,
|
||||
} from 'lucide-react';
|
||||
import { Layout } from '@/components/layout/Layout';
|
||||
import {
|
||||
@ -28,13 +30,14 @@ import type { Tenant, AssignedModule } from '@/types/tenant';
|
||||
import type { AuditLog } from '@/types/audit-log';
|
||||
import { formatDate } from '@/utils/format-date';
|
||||
|
||||
type TabType = 'overview' | 'users' | 'roles' | 'modules' | 'license' | 'audit-logs' | 'billing';
|
||||
type TabType = 'overview' | 'users' | 'roles' | 'modules' | 'settings' | 'license' | 'audit-logs' | 'billing';
|
||||
|
||||
const tabs: Array<{ id: TabType; label: string; icon: ReactElement }> = [
|
||||
{ id: 'overview', label: 'Overview', icon: <FileText className="w-4 h-4" /> },
|
||||
{ id: 'users', label: 'Users', icon: <Users className="w-4 h-4" /> },
|
||||
{ id: 'roles', label: 'Roles', icon: <FileText className="w-4 h-4" /> },
|
||||
{ id: 'modules', label: 'Modules', icon: <Package className="w-4 h-4" /> },
|
||||
{ id: 'settings', label: 'Settings', icon: <Settings className="w-4 h-4" /> },
|
||||
{ id: 'license', label: 'License', icon: <FileText className="w-4 h-4" /> },
|
||||
{ id: 'audit-logs', label: 'Audit Logs', icon: <History className="w-4 h-4" /> },
|
||||
{ id: 'billing', label: 'Billing', icon: <CreditCard className="w-4 h-4" /> },
|
||||
@ -276,6 +279,9 @@ const TenantDetails = (): ReactElement => {
|
||||
modules={tenant.assignedModules || []}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'settings' && tenant && (
|
||||
<SettingsTab tenant={tenant} />
|
||||
)}
|
||||
{activeTab === 'license' && <LicenseTab tenant={tenant} />}
|
||||
{activeTab === 'audit-logs' && (
|
||||
<AuditLogsTab
|
||||
@ -606,6 +612,221 @@ const AuditLogsTab = ({
|
||||
);
|
||||
};
|
||||
|
||||
// Settings Tab Component
|
||||
interface SettingsTabProps {
|
||||
tenant: Tenant;
|
||||
}
|
||||
|
||||
const SettingsTab = ({ tenant: _tenant }: SettingsTabProps): ReactElement => {
|
||||
const [logoFile, setLogoFile] = useState<File | null>(null);
|
||||
const [faviconFile, setFaviconFile] = useState<File | null>(null);
|
||||
const [primaryColor, setPrimaryColor] = useState<string>('#112868');
|
||||
const [secondaryColor, setSecondaryColor] = useState<string>('#23DCE1');
|
||||
const [accentColor, setAccentColor] = useState<string>('#084CC8');
|
||||
|
||||
const handleLogoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// Validate file size (2MB max)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
alert('Logo file size must be less than 2MB');
|
||||
return;
|
||||
}
|
||||
// Validate file type
|
||||
const validTypes = ['image/png', 'image/svg+xml', 'image/jpeg', 'image/jpg'];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
alert('Logo must be PNG, SVG, or JPG format');
|
||||
return;
|
||||
}
|
||||
setLogoFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFaviconChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
// Validate file size (500KB max)
|
||||
if (file.size > 500 * 1024) {
|
||||
alert('Favicon file size must be less than 500KB');
|
||||
return;
|
||||
}
|
||||
// Validate file type
|
||||
const validTypes = ['image/x-icon', 'image/png', 'image/vnd.microsoft.icon'];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
alert('Favicon must be ICO or PNG format');
|
||||
return;
|
||||
}
|
||||
setFaviconFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Branding Section */}
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg p-4 md:p-6 flex flex-col gap-4">
|
||||
{/* Section Header */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-base font-semibold text-[#0f1724]">Branding</h3>
|
||||
<p className="text-sm font-normal text-[#9ca3af]">
|
||||
Customize logo, favicon, and colors for this tenant experience.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Logo and Favicon Upload */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{/* Company Logo */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">Company Logo</label>
|
||||
<label
|
||||
htmlFor="logo-upload"
|
||||
className="bg-[#f5f7fa] border border-dashed border-[#d1d5db] rounded-md p-4 flex gap-4 items-center cursor-pointer hover:bg-[#e5e7eb] transition-colors"
|
||||
>
|
||||
<div className="bg-white border border-[#d1d5db] rounded-lg size-12 flex items-center justify-center shrink-0">
|
||||
<ImageIcon className="w-5 h-5 text-[#6b7280]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 flex-1 min-w-0">
|
||||
<span className="text-sm font-medium text-[#0f1724]">Upload Logo</span>
|
||||
<span className="text-xs font-normal text-[#9ca3af]">PNG, SVG, JPG up to 2MB.</span>
|
||||
</div>
|
||||
<input
|
||||
id="logo-upload"
|
||||
type="file"
|
||||
accept="image/png,image/svg+xml,image/jpeg,image/jpg"
|
||||
onChange={handleLogoChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
{logoFile && (
|
||||
<div className="text-xs text-[#6b7280] mt-1">
|
||||
Selected: {logoFile.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Favicon */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">Favicon</label>
|
||||
<label
|
||||
htmlFor="favicon-upload"
|
||||
className="bg-[#f5f7fa] border border-dashed border-[#d1d5db] rounded-md p-4 flex gap-4 items-center cursor-pointer hover:bg-[#e5e7eb] transition-colors"
|
||||
>
|
||||
<div className="bg-white border border-[#d1d5db] rounded-lg size-12 flex items-center justify-center shrink-0">
|
||||
<ImageIcon className="w-5 h-5 text-[#6b7280]" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 flex-1 min-w-0">
|
||||
<span className="text-sm font-medium text-[#0f1724]">Upload Favicon</span>
|
||||
<span className="text-xs font-normal text-[#9ca3af]">ICO or PNG up to 500KB.</span>
|
||||
</div>
|
||||
<input
|
||||
id="favicon-upload"
|
||||
type="file"
|
||||
accept="image/x-icon,image/png,image/vnd.microsoft.icon"
|
||||
onChange={handleFaviconChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
{faviconFile && (
|
||||
<div className="text-xs text-[#6b7280] mt-1">
|
||||
Selected: {faviconFile.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Primary Color */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">Primary Color</label>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div
|
||||
className="border border-[#d1d5db] rounded-md size-10 shrink-0"
|
||||
style={{ backgroundColor: primaryColor }}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={primaryColor}
|
||||
onChange={(e) => setPrimaryColor(e.target.value)}
|
||||
className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent"
|
||||
placeholder="#112868"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="color"
|
||||
value={primaryColor}
|
||||
onChange={(e) => setPrimaryColor(e.target.value)}
|
||||
className="size-10 rounded-md border border-[#d1d5db] cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for navigation, headers, and key actions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Secondary and Accent Colors */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{/* Secondary Color */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">Secondary Color</label>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div
|
||||
className="border border-[#d1d5db] rounded-md size-10 shrink-0"
|
||||
style={{ backgroundColor: secondaryColor }}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={secondaryColor}
|
||||
onChange={(e) => setSecondaryColor(e.target.value)}
|
||||
className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent"
|
||||
placeholder="#23DCE1"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="color"
|
||||
value={secondaryColor}
|
||||
onChange={(e) => setSecondaryColor(e.target.value)}
|
||||
className="size-10 rounded-md border border-[#d1d5db] cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for highlights and supporting elements.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Accent Color */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-[#0f1724]">Accent Color</label>
|
||||
<div className="flex gap-3 items-center">
|
||||
<div
|
||||
className="border border-[#d1d5db] rounded-md size-10 shrink-0"
|
||||
style={{ backgroundColor: accentColor }}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={accentColor}
|
||||
onChange={(e) => setAccentColor(e.target.value)}
|
||||
className="bg-[#f5f7fa] border border-[#d1d5db] h-10 px-3 py-1 rounded-md text-sm text-[#0f1724] w-full focus:outline-none focus:ring-2 focus:ring-[#112868] focus:border-transparent"
|
||||
placeholder="#084CC8"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="color"
|
||||
value={accentColor}
|
||||
onChange={(e) => setAccentColor(e.target.value)}
|
||||
className="size-10 rounded-md border border-[#d1d5db] cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs font-normal text-[#9ca3af]">
|
||||
Used for alerts and special notices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Billing Tab Component
|
||||
interface BillingTabProps {
|
||||
tenant: Tenant;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user