Files
smoothschedule/frontend/src/components/ui/lumina.tsx
poduck 4a66246708 Add booking flow, business hours, and dark mode support
Features:
- Complete multi-step booking flow with service selection, date/time picker,
  auth (login/signup with email verification), payment, and confirmation
- Business hours settings page for defining when business is open
- TimeBlock purpose field (BUSINESS_HOURS, CLOSURE, UNAVAILABLE)
- Service resource assignment with prep/takedown time buffers
- Availability checking respects business hours and service buffers
- Customer registration via email verification code

UI/UX:
- Full dark mode support for all booking components
- Separate first/last name fields in signup form
- Back buttons on each wizard step
- Removed auto-redirect from confirmation page

API:
- Public endpoints for services, availability, business hours
- Customer verification and registration endpoints
- Tenant lookup from X-Business-Subdomain header

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-11 20:20:18 -05:00

311 lines
8.5 KiB
TypeScript

/**
* Lumina Design System - Reusable UI Components
* Modern, premium design aesthetic with smooth animations and clean styling
*/
import React from 'react';
import { LucideIcon } from 'lucide-react';
// ============================================================================
// Button Components
// ============================================================================
interface LuminaButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
icon?: LucideIcon;
iconPosition?: 'left' | 'right';
loading?: boolean;
children: React.ReactNode;
}
export const LuminaButton: React.FC<LuminaButtonProps> = ({
variant = 'primary',
size = 'md',
icon: Icon,
iconPosition = 'right',
loading = false,
children,
className = '',
disabled,
...props
}) => {
const baseClasses = 'inline-flex items-center justify-center font-medium transition-all focus:outline-none focus:ring-2 focus:ring-offset-2';
const variantClasses = {
primary: 'bg-indigo-600 text-white hover:bg-indigo-700 focus:ring-indigo-500 shadow-sm',
secondary: 'bg-white text-gray-900 border border-gray-300 hover:bg-gray-50 focus:ring-indigo-500',
ghost: 'text-indigo-600 hover:bg-indigo-50 focus:ring-indigo-500',
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm rounded-lg',
md: 'px-4 py-2.5 text-sm rounded-lg',
lg: 'px-6 py-3 text-base rounded-lg',
};
const disabledClasses = 'disabled:opacity-70 disabled:cursor-not-allowed';
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${disabledClasses} ${className}`}
disabled={disabled || loading}
{...props}
>
{loading ? (
<span className="animate-pulse">Processing...</span>
) : (
<>
{Icon && iconPosition === 'left' && <Icon className="w-4 h-4 mr-2" />}
{children}
{Icon && iconPosition === 'right' && <Icon className="w-4 h-4 ml-2" />}
</>
)}
</button>
);
};
// ============================================================================
// Input Components
// ============================================================================
interface LuminaInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
hint?: string;
icon?: LucideIcon;
}
export const LuminaInput: React.FC<LuminaInputProps> = ({
label,
error,
hint,
icon: Icon,
className = '',
...props
}) => {
return (
<div className="w-full">
{label && (
<label className="block text-sm font-medium text-gray-700 mb-1">
{label}
{props.required && <span className="text-red-500 ml-1">*</span>}
</label>
)}
<div className="relative">
{Icon && (
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon className="h-5 w-5 text-gray-400" />
</div>
)}
<input
className={`block w-full ${Icon ? 'pl-10' : 'pl-3'} pr-3 py-2.5 border ${
error ? 'border-red-300 focus:ring-red-500 focus:border-red-500' : 'border-gray-300 focus:ring-indigo-500 focus:border-indigo-500'
} rounded-lg transition-colors ${className}`}
{...props}
/>
</div>
{error && <p className="text-sm text-red-600 mt-1">{error}</p>}
{hint && !error && <p className="text-sm text-gray-500 mt-1">{hint}</p>}
</div>
);
};
// ============================================================================
// Card Components
// ============================================================================
interface LuminaCardProps {
children: React.ReactNode;
className?: string;
padding?: 'none' | 'sm' | 'md' | 'lg';
hover?: boolean;
}
export const LuminaCard: React.FC<LuminaCardProps> = ({
children,
className = '',
padding = 'md',
hover = false,
}) => {
const paddingClasses = {
none: '',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
};
const hoverClasses = hover ? 'hover:shadow-lg hover:-translate-y-0.5 transition-all' : '';
return (
<div className={`bg-white rounded-2xl shadow-sm border border-gray-100 ${paddingClasses[padding]} ${hoverClasses} ${className}`}>
{children}
</div>
);
};
// ============================================================================
// Badge Components
// ============================================================================
interface LuminaBadgeProps {
children: React.ReactNode;
variant?: 'default' | 'success' | 'warning' | 'error' | 'info';
size?: 'sm' | 'md';
}
export const LuminaBadge: React.FC<LuminaBadgeProps> = ({
children,
variant = 'default',
size = 'md',
}) => {
const variantClasses = {
default: 'bg-gray-100 text-gray-800',
success: 'bg-green-100 text-green-800',
warning: 'bg-amber-100 text-amber-800',
error: 'bg-red-100 text-red-800',
info: 'bg-blue-100 text-blue-800',
};
const sizeClasses = {
sm: 'text-xs px-2 py-0.5',
md: 'text-sm px-2.5 py-1',
};
return (
<span className={`inline-flex items-center font-medium rounded-full ${variantClasses[variant]} ${sizeClasses[size]}`}>
{children}
</span>
);
};
// ============================================================================
// Section Container
// ============================================================================
interface LuminaSectionProps {
children: React.ReactNode;
title?: string;
subtitle?: string;
className?: string;
}
export const LuminaSection: React.FC<LuminaSectionProps> = ({
children,
title,
subtitle,
className = '',
}) => {
return (
<section className={`py-16 px-4 sm:px-6 lg:px-8 ${className}`}>
<div className="max-w-7xl mx-auto">
{(title || subtitle) && (
<div className="text-center mb-12">
{title && <h2 className="text-3xl font-bold text-gray-900 mb-3">{title}</h2>}
{subtitle && <p className="text-lg text-gray-600 max-w-2xl mx-auto">{subtitle}</p>}
</div>
)}
{children}
</div>
</section>
);
};
// ============================================================================
// Icon Box Component
// ============================================================================
interface LuminaIconBoxProps {
icon: LucideIcon;
color?: 'indigo' | 'green' | 'amber' | 'red' | 'blue';
size?: 'sm' | 'md' | 'lg';
}
export const LuminaIconBox: React.FC<LuminaIconBoxProps> = ({
icon: Icon,
color = 'indigo',
size = 'md',
}) => {
const colorClasses = {
indigo: 'bg-indigo-100 text-indigo-600',
green: 'bg-green-100 text-green-600',
amber: 'bg-amber-100 text-amber-600',
red: 'bg-red-100 text-red-600',
blue: 'bg-blue-100 text-blue-600',
};
const sizeClasses = {
sm: 'w-10 h-10',
md: 'w-12 h-12',
lg: 'w-16 h-16',
};
const iconSizeClasses = {
sm: 'w-5 h-5',
md: 'w-6 h-6',
lg: 'w-8 h-8',
};
return (
<div className={`${sizeClasses[size]} ${colorClasses[color]} rounded-xl flex items-center justify-center`}>
<Icon className={iconSizeClasses[size]} />
</div>
);
};
// ============================================================================
// Feature Card Component
// ============================================================================
interface LuminaFeatureCardProps {
icon: LucideIcon;
title: string;
description: string;
onClick?: () => void;
}
export const LuminaFeatureCard: React.FC<LuminaFeatureCardProps> = ({
icon,
title,
description,
onClick,
}) => {
return (
<LuminaCard
hover={!!onClick}
className={onClick ? 'cursor-pointer' : ''}
onClick={onClick}
>
<div className="flex flex-col items-center text-center">
<LuminaIconBox icon={icon} size="lg" />
<h3 className="mt-4 text-lg font-semibold text-gray-900">{title}</h3>
<p className="mt-2 text-gray-600">{description}</p>
</div>
</LuminaCard>
);
};
// ============================================================================
// Loading Spinner
// ============================================================================
interface LuminaSpinnerProps {
size?: 'sm' | 'md' | 'lg';
className?: string;
}
export const LuminaSpinner: React.FC<LuminaSpinnerProps> = ({
size = 'md',
className = '',
}) => {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-8 h-8',
lg: 'w-12 h-12',
};
return (
<div className={`animate-spin rounded-full border-2 border-gray-200 border-t-indigo-600 ${sizeClasses[size]} ${className}`} />
);
};