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>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { BlockedDate, BlockType } from '../../types';
|
||||
import { BlockedDate, BlockType, BlockPurpose } from '../../types';
|
||||
|
||||
interface TimeBlockCalendarOverlayProps {
|
||||
blockedDates: BlockedDate[];
|
||||
@@ -126,61 +126,46 @@ const TimeBlockCalendarOverlay: React.FC<TimeBlockCalendarOverlayProps> = ({
|
||||
return overlays;
|
||||
}, [relevantBlocks, days, dayWidth, pixelsPerMinute, zoomLevel, startHour]);
|
||||
|
||||
const getBlockStyle = (blockType: BlockType, isBusinessLevel: boolean): React.CSSProperties => {
|
||||
const getBlockStyle = (blockType: BlockType, purpose: BlockPurpose, isBusinessLevel: boolean): React.CSSProperties => {
|
||||
const baseStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
height: '100%',
|
||||
pointerEvents: 'auto',
|
||||
cursor: 'default',
|
||||
zIndex: 5, // Ensure overlays are visible above grid lines
|
||||
};
|
||||
|
||||
// Business-level blocks (including business hours): Simple gray background
|
||||
// No fancy styling - just indicates "not available for booking"
|
||||
if (isBusinessLevel) {
|
||||
// Business blocks: Red (hard) / Amber (soft)
|
||||
if (blockType === 'HARD') {
|
||||
return {
|
||||
...baseStyle,
|
||||
background: `repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgba(239, 68, 68, 0.3),
|
||||
rgba(239, 68, 68, 0.3) 5px,
|
||||
rgba(239, 68, 68, 0.5) 5px,
|
||||
rgba(239, 68, 68, 0.5) 10px
|
||||
)`,
|
||||
borderTop: '2px solid rgba(239, 68, 68, 0.7)',
|
||||
borderBottom: '2px solid rgba(239, 68, 68, 0.7)',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...baseStyle,
|
||||
background: 'rgba(251, 191, 36, 0.2)',
|
||||
borderTop: '2px dashed rgba(251, 191, 36, 0.8)',
|
||||
borderBottom: '2px dashed rgba(251, 191, 36, 0.8)',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...baseStyle,
|
||||
background: 'rgba(107, 114, 128, 0.25)', // Gray-500 at 25% opacity (more visible)
|
||||
};
|
||||
}
|
||||
|
||||
// Resource-level blocks: Purple (hard) / Cyan (soft)
|
||||
if (blockType === 'HARD') {
|
||||
return {
|
||||
...baseStyle,
|
||||
background: `repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgba(147, 51, 234, 0.25),
|
||||
rgba(147, 51, 234, 0.25) 5px,
|
||||
rgba(147, 51, 234, 0.4) 5px,
|
||||
rgba(147, 51, 234, 0.4) 10px
|
||||
)`,
|
||||
borderTop: '2px solid rgba(147, 51, 234, 0.7)',
|
||||
borderBottom: '2px solid rgba(147, 51, 234, 0.7)',
|
||||
};
|
||||
} else {
|
||||
// Resource blocks: Purple (hard) / Cyan (soft)
|
||||
if (blockType === 'HARD') {
|
||||
return {
|
||||
...baseStyle,
|
||||
background: `repeating-linear-gradient(
|
||||
-45deg,
|
||||
rgba(147, 51, 234, 0.25),
|
||||
rgba(147, 51, 234, 0.25) 5px,
|
||||
rgba(147, 51, 234, 0.4) 5px,
|
||||
rgba(147, 51, 234, 0.4) 10px
|
||||
)`,
|
||||
borderTop: '2px solid rgba(147, 51, 234, 0.7)',
|
||||
borderBottom: '2px solid rgba(147, 51, 234, 0.7)',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...baseStyle,
|
||||
background: 'rgba(6, 182, 212, 0.15)',
|
||||
borderTop: '2px dashed rgba(6, 182, 212, 0.7)',
|
||||
borderBottom: '2px dashed rgba(6, 182, 212, 0.7)',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...baseStyle,
|
||||
background: 'rgba(6, 182, 212, 0.15)',
|
||||
borderTop: '2px dashed rgba(6, 182, 212, 0.7)',
|
||||
borderBottom: '2px dashed rgba(6, 182, 212, 0.7)',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -208,7 +193,7 @@ const TimeBlockCalendarOverlay: React.FC<TimeBlockCalendarOverlayProps> = ({
|
||||
<>
|
||||
{blockOverlays.map((overlay, index) => {
|
||||
const isBusinessLevel = overlay.block.resource_id === null;
|
||||
const style = getBlockStyle(overlay.block.block_type, isBusinessLevel);
|
||||
const style = getBlockStyle(overlay.block.block_type, overlay.block.purpose, isBusinessLevel);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -224,14 +209,12 @@ const TimeBlockCalendarOverlay: React.FC<TimeBlockCalendarOverlayProps> = ({
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={() => onDayClick?.(days[overlay.dayIndex])}
|
||||
>
|
||||
{/* Block level indicator */}
|
||||
<div className={`absolute top-1 left-1 px-1.5 py-0.5 text-white text-[10px] font-bold rounded shadow-sm uppercase tracking-wide ${
|
||||
isBusinessLevel
|
||||
? 'bg-red-600'
|
||||
: 'bg-purple-600'
|
||||
}`}>
|
||||
{isBusinessLevel ? 'B' : 'R'}
|
||||
</div>
|
||||
{/* Only show badge for resource-level blocks */}
|
||||
{!isBusinessLevel && (
|
||||
<div className="absolute top-1 left-1 px-1.5 py-0.5 text-white text-[10px] font-bold rounded shadow-sm uppercase tracking-wide bg-purple-600">
|
||||
R
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user