@fayz-ai/plugin-agenda 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/bridges/create-financial-bridge.d.ts +1 -1
- package/dist/bridges/create-financial-bridge.d.ts.map +1 -1
- package/dist/bridges/financial-bridge.d.ts.map +1 -0
- package/dist/{chunk-Q2PAWBPR.js → chunk-76ZOHRBM.js} +2 -2
- package/dist/{chunk-Q2PAWBPR.js.map → chunk-76ZOHRBM.js.map} +1 -1
- package/dist/components/PaymentStatusBadge.d.ts +1 -1
- package/dist/components/PaymentStatusBadge.d.ts.map +1 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/{AgendaContext.d.ts → context.d.ts} +1 -1
- package/dist/context.d.ts.map +1 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +170 -7
- package/dist/index.js.map +1 -1
- package/dist/lib/message-events.d.ts +12 -0
- package/dist/lib/message-events.d.ts.map +1 -0
- package/dist/public/{data.d.ts → data/index.d.ts} +4 -4
- package/dist/public/data/index.d.ts.map +1 -0
- package/dist/public/{data.supabase.d.ts → data/supabase.d.ts} +2 -2
- package/dist/public/data/supabase.d.ts.map +1 -0
- package/dist/public/{hooks.d.ts → hooks/index.d.ts} +4 -4
- package/dist/public/hooks/index.d.ts.map +1 -0
- package/dist/public/index.d.ts +4 -4
- package/dist/public/index.d.ts.map +1 -1
- package/dist/public/index.js +3 -3
- package/dist/public/index.js.map +1 -1
- package/dist/public/{format.d.ts → lib/format.d.ts} +1 -1
- package/dist/public/lib/format.d.ts.map +1 -0
- package/dist/public/views/BookingWidget.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/ContactStep.d.ts +1 -1
- package/dist/public/views/steps/ContactStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/DateTimeStep.d.ts +3 -3
- package/dist/public/views/steps/DateTimeStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/OverviewStep.d.ts +1 -1
- package/dist/public/views/steps/OverviewStep.d.ts.map +1 -0
- package/dist/public/{steps → views/steps}/PaymentStep.d.ts +1 -1
- package/dist/public/views/steps/PaymentStep.d.ts.map +1 -0
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/styles/calendar-styles.d.ts.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/{AgendaPage.d.ts → views/AgendaPage.d.ts} +3 -3
- package/dist/views/AgendaPage.d.ts.map +1 -0
- package/package.json +6 -9
- package/dist/AgendaContext.d.ts.map +0 -1
- package/dist/AgendaPage.d.ts.map +0 -1
- package/dist/calendar-styles.d.ts.map +0 -1
- package/dist/financial-bridge.d.ts.map +0 -1
- package/dist/public/BookingWidget.d.ts.map +0 -1
- package/dist/public/data.d.ts.map +0 -1
- package/dist/public/data.supabase.d.ts.map +0 -1
- package/dist/public/format.d.ts.map +0 -1
- package/dist/public/hooks.d.ts.map +0 -1
- package/dist/public/steps/ContactStep.d.ts.map +0 -1
- package/dist/public/steps/DateTimeStep.d.ts.map +0 -1
- package/dist/public/steps/OverviewStep.d.ts.map +0 -1
- package/dist/public/steps/PaymentStep.d.ts.map +0 -1
- package/dist/registries.d.ts.map +0 -1
- package/src/AgendaContext.tsx +0 -11
- package/src/AgendaPage.tsx +0 -33
- package/src/bridges/create-financial-bridge.ts +0 -296
- package/src/calendar-styles.css +0 -245
- package/src/calendar-styles.ts +0 -285
- package/src/components/AgendaGeneralSettings.tsx +0 -86
- package/src/components/AgendaMobileList.tsx +0 -103
- package/src/components/AppointmentModal.tsx +0 -1050
- package/src/components/AppointmentPopover.tsx +0 -231
- package/src/components/BookingPaymentPanel.tsx +0 -394
- package/src/components/CalendarSidebar.tsx +0 -119
- package/src/components/CalendarToolbar.tsx +0 -86
- package/src/components/EventContextMenu.tsx +0 -125
- package/src/components/FloatingPanel.tsx +0 -123
- package/src/components/MiniCalendar.tsx +0 -120
- package/src/components/PaymentStatusBadge.tsx +0 -62
- package/src/components/StatusBadge.tsx +0 -23
- package/src/config.ts +0 -368
- package/src/data/index.ts +0 -4
- package/src/data/mock.ts +0 -344
- package/src/data/supabase.ts +0 -636
- package/src/data/types.ts +0 -37
- package/src/financial-bridge.ts +0 -96
- package/src/hooks/useAgendaSettings.ts +0 -17
- package/src/index.ts +0 -297
- package/src/integrations/google-calendar/GoogleCalendarSettings.tsx +0 -136
- package/src/integrations/google-calendar/components/CalendarColorDot.tsx +0 -23
- package/src/integrations/google-calendar/components/ChannelMappingList.tsx +0 -329
- package/src/integrations/google-calendar/components/ImportEventsWizard.tsx +0 -215
- package/src/integrations/google-calendar/components/useAgendaLookups.ts +0 -100
- package/src/integrations/google-calendar/connector.ts +0 -20
- package/src/integrations/google-calendar/connectorDef.tsx +0 -185
- package/src/integrations/google-calendar/data/supabase.ts +0 -170
- package/src/integrations/google-calendar/data/tables.ts +0 -12
- package/src/integrations/google-calendar/index.ts +0 -45
- package/src/integrations/google-calendar/integration.test.ts +0 -199
- package/src/integrations/google-calendar/mapping.ts +0 -183
- package/src/integrations/google-calendar/migrations/001_google_calendar.sql +0 -97
- package/src/integrations/google-calendar/migrations/002_smart_sync.sql +0 -517
- package/src/integrations/google-calendar/migrations/003_plg_rename.sql +0 -273
- package/src/integrations/google-calendar/types.ts +0 -142
- package/src/lib/tenant.ts +0 -3
- package/src/locales/en.ts +0 -220
- package/src/locales/index.ts +0 -7
- package/src/locales/pt-BR.ts +0 -220
- package/src/migrations/000b_v_bookings_rename.sql +0 -21
- package/src/migrations/000c_dedupe_customer_phones.sql +0 -43
- package/src/migrations/001_public_booking.sql +0 -296
- package/src/migrations/002_v_bookings_compat.sql +0 -14
- package/src/migrations/003_public_client_kind.sql +0 -175
- package/src/migrations/004_public_booking_assignee.sql +0 -176
- package/src/migrations/005_agent_rpcs.sql +0 -168
- package/src/migrations/006_agent_rpc_localtime.sql +0 -181
- package/src/migrations/007_schedule_exception_precedence.sql +0 -290
- package/src/migrations/008_agent_rpc_exception_precedence.sql +0 -172
- package/src/migrations/009_public_booking_location.sql +0 -162
- package/src/migrations/010_public_service_curation.sql +0 -31
- package/src/migrations/011_default_assignee.sql +0 -167
- package/src/migrations/012_drop_legacy_booking_overload.sql +0 -29
- package/src/migrations/013_normalize_v_appointments.sql +0 -199
- package/src/migrations/014_agent_rpc_update_appointment.sql +0 -199
- package/src/migrations/index.ts +0 -2377
- package/src/public/BookingWidget.tsx +0 -324
- package/src/public/components/BrandHeader.tsx +0 -27
- package/src/public/components/ServiceCard.tsx +0 -39
- package/src/public/components/SidebarExtras.tsx +0 -30
- package/src/public/components/StepRailItem.tsx +0 -27
- package/src/public/components/SuccessPanel.tsx +0 -27
- package/src/public/components/SummaryRail.tsx +0 -90
- package/src/public/components/defaults.ts +0 -14
- package/src/public/context.tsx +0 -103
- package/src/public/createPublicBookingPlugin.ts +0 -281
- package/src/public/data.supabase.ts +0 -97
- package/src/public/data.ts +0 -105
- package/src/public/format.ts +0 -96
- package/src/public/hooks.ts +0 -110
- package/src/public/index.ts +0 -69
- package/src/public/steps/ContactStep.tsx +0 -184
- package/src/public/steps/DateTimeStep.tsx +0 -138
- package/src/public/steps/OverviewStep.tsx +0 -81
- package/src/public/steps/PaymentStep.tsx +0 -180
- package/src/public/types.ts +0 -250
- package/src/registries.ts +0 -35
- package/src/store.ts +0 -311
- package/src/types.ts +0 -273
- package/src/views/CalendarView.tsx +0 -1112
- package/src/views/ConfirmationsView.tsx +0 -155
- /package/dist/{financial-bridge.d.ts → bridges/financial-bridge.d.ts} +0 -0
- /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
- /package/dist/public/{BookingWidget.d.ts → views/BookingWidget.d.ts} +0 -0
- /package/dist/{calendar-styles.d.ts → styles/calendar-styles.d.ts} +0 -0
package/src/public/hooks.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react'
|
|
2
|
-
import { useBooking } from './context'
|
|
3
|
-
import type { PublicService } from './types'
|
|
4
|
-
import type { PublicCreateBookingInput, PublicBookingResult } from './data'
|
|
5
|
-
import type { TimeSlot } from '../types'
|
|
6
|
-
|
|
7
|
-
/** The resolved bookable services (static option or fetched from the provider). */
|
|
8
|
-
export function useServices(): PublicService[] {
|
|
9
|
-
return useBooking().services
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface UsePublicServicesResult {
|
|
13
|
-
services: PublicService[]
|
|
14
|
-
loading: boolean
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Like useServices, but exposes the fetch state for skeletons. */
|
|
18
|
-
export function usePublicServices(): UsePublicServicesResult {
|
|
19
|
-
const { services, servicesLoading } = useBooking()
|
|
20
|
-
return { services, loading: servicesLoading }
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface UseAvailableSlotsResult {
|
|
24
|
-
slots: TimeSlot[]
|
|
25
|
-
loading: boolean
|
|
26
|
-
error: Error | null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Available time slots for a service on a given date. Delegates to
|
|
31
|
-
* provider.getAvailableSlots — mock now, Supabase RPC when configured, same call.
|
|
32
|
-
*/
|
|
33
|
-
export function useAvailableSlots(params: { serviceId: string | null; date: string | null }): UseAvailableSlotsResult {
|
|
34
|
-
const { provider, professionalId, services, window } = useBooking()
|
|
35
|
-
const [slots, setSlots] = useState<TimeSlot[]>([])
|
|
36
|
-
const [loading, setLoading] = useState(false)
|
|
37
|
-
const [error, setError] = useState<Error | null>(null)
|
|
38
|
-
|
|
39
|
-
const { serviceId, date } = params
|
|
40
|
-
const service = services.find((s) => s.id === serviceId)
|
|
41
|
-
const durationMinutes = service?.durationMinutes ?? 0
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
let active = true
|
|
45
|
-
if (!serviceId || !date || !durationMinutes) {
|
|
46
|
-
setSlots([])
|
|
47
|
-
return
|
|
48
|
-
}
|
|
49
|
-
setLoading(true)
|
|
50
|
-
provider
|
|
51
|
-
.getAvailableSlots({ assigneeId: professionalId, date, durationMinutes, slotInterval: window.slotInterval })
|
|
52
|
-
.then((result) => {
|
|
53
|
-
if (!active) return
|
|
54
|
-
setSlots(result)
|
|
55
|
-
setError(null)
|
|
56
|
-
})
|
|
57
|
-
.catch((err) => {
|
|
58
|
-
if (!active) return
|
|
59
|
-
setError(err instanceof Error ? err : new Error(String(err)))
|
|
60
|
-
})
|
|
61
|
-
.finally(() => {
|
|
62
|
-
if (active) setLoading(false)
|
|
63
|
-
})
|
|
64
|
-
return () => {
|
|
65
|
-
active = false
|
|
66
|
-
}
|
|
67
|
-
}, [provider, professionalId, serviceId, date, durationMinutes, window.slotInterval])
|
|
68
|
-
|
|
69
|
-
return { slots, loading, error }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export type CreatePublicBookingInput = PublicCreateBookingInput
|
|
73
|
-
|
|
74
|
-
export interface UseCreateBookingResult {
|
|
75
|
-
submit: (input: CreatePublicBookingInput) => Promise<PublicBookingResult>
|
|
76
|
-
submitting: boolean
|
|
77
|
-
error: Error | null
|
|
78
|
-
done: boolean
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** Create a booking via the provider (mock append / Supabase RPC). */
|
|
82
|
-
export function useCreateBooking(): UseCreateBookingResult {
|
|
83
|
-
const { provider, professionalId } = useBooking()
|
|
84
|
-
const [submitting, setSubmitting] = useState(false)
|
|
85
|
-
const [error, setError] = useState<Error | null>(null)
|
|
86
|
-
const [done, setDone] = useState(false)
|
|
87
|
-
|
|
88
|
-
async function submit(input: CreatePublicBookingInput): Promise<PublicBookingResult> {
|
|
89
|
-
setSubmitting(true)
|
|
90
|
-
setError(null)
|
|
91
|
-
try {
|
|
92
|
-
// The professional the visitor saw availability for must be the one
|
|
93
|
-
// booked — thread it unless the caller already pinned another.
|
|
94
|
-
const result = await provider.createBooking({
|
|
95
|
-
...input,
|
|
96
|
-
assigneeId: input.assigneeId ?? professionalId ?? null,
|
|
97
|
-
})
|
|
98
|
-
setDone(true)
|
|
99
|
-
return result
|
|
100
|
-
} catch (err) {
|
|
101
|
-
const e = err instanceof Error ? err : new Error(String(err))
|
|
102
|
-
setError(e)
|
|
103
|
-
throw e
|
|
104
|
-
} finally {
|
|
105
|
-
setSubmitting(false)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return { submit, submitting, error, done }
|
|
110
|
-
}
|
package/src/public/index.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// @fayz-ai/plugin-agenda/public — customer-facing booking surface.
|
|
3
|
-
//
|
|
4
|
-
// Separate entry from the admin plugin so a website host imports only the lean
|
|
5
|
-
// booking widget + data seam (no @fayz-ai/saas / calendar admin graph).
|
|
6
|
-
//
|
|
7
|
-
// Customization levers (cheapest first): `labels` → `brand` → `components`
|
|
8
|
-
// (swap subcomponents; defaults exported below for wrapping/composition).
|
|
9
|
-
// Data: PublicBookingDataProvider — mock seed by default, Supabase RPCs when a
|
|
10
|
-
// global client is configured (requires `tenantId`).
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
export { createPublicBookingPlugin } from './createPublicBookingPlugin'
|
|
14
|
-
export type { PublicBookingOptions, PublicBookingPlugin } from './createPublicBookingPlugin'
|
|
15
|
-
export { BookingWidget } from './BookingWidget'
|
|
16
|
-
export { BookingProvider, useBooking } from './context'
|
|
17
|
-
export type { PublicBookingContextValue, OnIdentityVerified } from './context'
|
|
18
|
-
export { useServices, usePublicServices, useAvailableSlots, useCreateBooking } from './hooks'
|
|
19
|
-
export type {
|
|
20
|
-
UsePublicServicesResult,
|
|
21
|
-
UseAvailableSlotsResult,
|
|
22
|
-
UseCreateBookingResult,
|
|
23
|
-
CreatePublicBookingInput,
|
|
24
|
-
} from './hooks'
|
|
25
|
-
|
|
26
|
-
// Data seam
|
|
27
|
-
export type {
|
|
28
|
-
PublicBookingDataProvider,
|
|
29
|
-
PublicSlotQuery,
|
|
30
|
-
PublicCreateBookingInput,
|
|
31
|
-
PublicBookingResult,
|
|
32
|
-
MockPublicBookingOptions,
|
|
33
|
-
} from './data'
|
|
34
|
-
export { createMockPublicBookingProvider } from './data'
|
|
35
|
-
export { createSupabasePublicBookingProvider } from './data.supabase'
|
|
36
|
-
export type { SupabasePublicBookingOptions } from './data.supabase'
|
|
37
|
-
|
|
38
|
-
// Customization seam — default components (wrap or replace via `components`)
|
|
39
|
-
export { DEFAULT_BOOKING_COMPONENTS } from './components/defaults'
|
|
40
|
-
export { DefaultBrandHeader } from './components/BrandHeader'
|
|
41
|
-
export { DefaultServiceCard } from './components/ServiceCard'
|
|
42
|
-
export { DefaultSidebarExtras } from './components/SidebarExtras'
|
|
43
|
-
export { DefaultStepRailItem } from './components/StepRailItem'
|
|
44
|
-
export { DefaultSuccessPanel } from './components/SuccessPanel'
|
|
45
|
-
|
|
46
|
-
// Formatting helpers (useful inside custom components)
|
|
47
|
-
export { useFormatters, formatSlotRange, DAY_NAMES, WEEK_ORDER } from './format'
|
|
48
|
-
|
|
49
|
-
export type {
|
|
50
|
-
PublicService,
|
|
51
|
-
ContactInfo,
|
|
52
|
-
BookingWindow,
|
|
53
|
-
PublicBookingLabels,
|
|
54
|
-
WorkingHours,
|
|
55
|
-
PaymentConfig,
|
|
56
|
-
ResolvedPayment,
|
|
57
|
-
PhoneVerificationMode,
|
|
58
|
-
BookingBrand,
|
|
59
|
-
ResolvedBookingBrand,
|
|
60
|
-
BookingHighlight,
|
|
61
|
-
BookingFormatters,
|
|
62
|
-
BookingComponents,
|
|
63
|
-
BrandHeaderProps,
|
|
64
|
-
ServiceCardProps,
|
|
65
|
-
SidebarExtrasProps,
|
|
66
|
-
SuccessPanelProps,
|
|
67
|
-
StepRailItemProps,
|
|
68
|
-
StepState,
|
|
69
|
-
} from './types'
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import { Check, ChevronDown, CreditCard, Loader2, ShieldCheck } from 'lucide-react'
|
|
2
|
-
import { COUNTRIES, maskPhone, unmaskPhone, type CountryDef } from '@fayz-ai/core'
|
|
3
|
-
import type { PhoneVerificationMode, PublicBookingLabels, ResolvedPayment } from '../types'
|
|
4
|
-
|
|
5
|
-
export interface ContactDraft {
|
|
6
|
-
name: string
|
|
7
|
-
phone: string
|
|
8
|
-
email: string
|
|
9
|
-
notes: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface ContactStepProps {
|
|
13
|
-
labels: PublicBookingLabels
|
|
14
|
-
contact: ContactDraft
|
|
15
|
-
onContactChange: (contact: ContactDraft) => void
|
|
16
|
-
contactStep: 'phone' | 'code' | 'details'
|
|
17
|
-
/** 'none' → no code is sent, so the copy drops every OTP promise. */
|
|
18
|
-
phoneVerification: PhoneVerificationMode
|
|
19
|
-
country: CountryDef
|
|
20
|
-
onCountryChange: (iso2: string) => void
|
|
21
|
-
code: string
|
|
22
|
-
onCodeChange: (code: string) => void
|
|
23
|
-
codeError: boolean
|
|
24
|
-
onSendCode: () => void
|
|
25
|
-
onConfirmCode: () => void
|
|
26
|
-
onBackToPhone: () => void
|
|
27
|
-
onSubmitDetails: () => void
|
|
28
|
-
payment: ResolvedPayment | null
|
|
29
|
-
submitting: boolean
|
|
30
|
-
error: Error | null
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Contact step — phone verification first (Quaddro-style), then details. */
|
|
34
|
-
export function ContactStep({
|
|
35
|
-
labels, contact, onContactChange, contactStep, phoneVerification, country, onCountryChange,
|
|
36
|
-
code, onCodeChange, codeError, onSendCode, onConfirmCode, onBackToPhone,
|
|
37
|
-
onSubmitDetails, payment, submitting, error,
|
|
38
|
-
}: ContactStepProps) {
|
|
39
|
-
return (
|
|
40
|
-
<div>
|
|
41
|
-
<h3 className="font-heading text-xl font-bold text-foreground">{labels.contactStep}</h3>
|
|
42
|
-
|
|
43
|
-
{/* Sub-step: phone */}
|
|
44
|
-
{contactStep === 'phone' ? (
|
|
45
|
-
<form onSubmit={(e) => { e.preventDefault(); onSendCode() }}>
|
|
46
|
-
<p className="mt-1 text-sm text-muted-foreground">{labels.contactIntro}</p>
|
|
47
|
-
<div className="mt-5">
|
|
48
|
-
<label htmlFor="bk-phone" className="mb-1 block text-sm font-medium text-foreground">{labels.phoneLabel}</label>
|
|
49
|
-
<div className="flex">
|
|
50
|
-
<div className="relative">
|
|
51
|
-
<select
|
|
52
|
-
aria-label="País"
|
|
53
|
-
value={country.iso2}
|
|
54
|
-
onChange={(e) => onCountryChange(e.target.value)}
|
|
55
|
-
className="h-full appearance-none rounded-l-xl border border-r-0 border-border bg-muted py-3 pl-3 pr-8 text-sm font-medium text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
56
|
-
>
|
|
57
|
-
{COUNTRIES.map((c) => (
|
|
58
|
-
<option key={c.iso2} value={c.iso2}>{c.flag} {c.dial}</option>
|
|
59
|
-
))}
|
|
60
|
-
</select>
|
|
61
|
-
<ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
62
|
-
</div>
|
|
63
|
-
<input
|
|
64
|
-
id="bk-phone" type="tel" autoComplete="tel" inputMode="tel" value={contact.phone}
|
|
65
|
-
onChange={(e) => onContactChange({ ...contact, phone: maskPhone(e.target.value, country.mask) })}
|
|
66
|
-
className="w-full rounded-r-xl border border-border bg-background px-4 py-3 text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring"
|
|
67
|
-
placeholder={`Ex.: ${maskPhone('11987654321', country.mask)}`}
|
|
68
|
-
/>
|
|
69
|
-
</div>
|
|
70
|
-
<p className="mt-2 text-xs text-muted-foreground">
|
|
71
|
-
{phoneVerification === 'none' ? labels.phoneHintNoVerification : labels.phoneHint}
|
|
72
|
-
</p>
|
|
73
|
-
</div>
|
|
74
|
-
<button
|
|
75
|
-
type="submit"
|
|
76
|
-
disabled={unmaskPhone(contact.phone).length < 8}
|
|
77
|
-
className="mt-5 w-full rounded-xl bg-primary py-3 font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
78
|
-
>
|
|
79
|
-
{phoneVerification === 'none' ? labels.phoneContinueCta : labels.sendCodeCta}
|
|
80
|
-
</button>
|
|
81
|
-
</form>
|
|
82
|
-
) : null}
|
|
83
|
-
|
|
84
|
-
{/* Sub-step: details (after verification) */}
|
|
85
|
-
{contactStep === 'details' ? (
|
|
86
|
-
<form onSubmit={(e) => { e.preventDefault(); onSubmitDetails() }}>
|
|
87
|
-
<p className="mt-1 text-sm text-muted-foreground">{labels.contactIntro}</p>
|
|
88
|
-
<div className="mt-4 flex items-center justify-between rounded-xl border border-border bg-muted/40 px-4 py-3">
|
|
89
|
-
<span className="flex items-center gap-2 text-sm text-foreground">
|
|
90
|
-
<ShieldCheck className="h-4 w-4 text-primary" />{' '}
|
|
91
|
-
{phoneVerification === 'none' ? labels.phoneLabel : labels.phoneVerified} · {country.flag} {country.dial} {contact.phone}
|
|
92
|
-
</span>
|
|
93
|
-
<button
|
|
94
|
-
type="button"
|
|
95
|
-
onClick={onBackToPhone}
|
|
96
|
-
className="rounded text-sm font-medium text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
97
|
-
>
|
|
98
|
-
{labels.changePhone}
|
|
99
|
-
</button>
|
|
100
|
-
</div>
|
|
101
|
-
<div className="mt-4 space-y-3">
|
|
102
|
-
<div>
|
|
103
|
-
<label htmlFor="bk-name" className="mb-1 block text-sm font-medium text-foreground">{labels.nameLabel}</label>
|
|
104
|
-
<input
|
|
105
|
-
id="bk-name" type="text" autoComplete="name" value={contact.name}
|
|
106
|
-
onChange={(e) => onContactChange({ ...contact, name: e.target.value })}
|
|
107
|
-
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring"
|
|
108
|
-
placeholder={labels.namePlaceholder}
|
|
109
|
-
/>
|
|
110
|
-
</div>
|
|
111
|
-
<div>
|
|
112
|
-
<label htmlFor="bk-email" className="mb-1 block text-sm font-medium text-foreground">{labels.emailLabel}</label>
|
|
113
|
-
<input
|
|
114
|
-
id="bk-email" type="email" autoComplete="email" inputMode="email" value={contact.email}
|
|
115
|
-
onChange={(e) => onContactChange({ ...contact, email: e.target.value })}
|
|
116
|
-
className="w-full rounded-xl border border-border bg-background px-4 py-3 text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring"
|
|
117
|
-
placeholder={labels.emailPlaceholder}
|
|
118
|
-
/>
|
|
119
|
-
<p className="mt-1 text-xs text-muted-foreground">{labels.emailHint}</p>
|
|
120
|
-
</div>
|
|
121
|
-
<div>
|
|
122
|
-
<label htmlFor="bk-notes" className="mb-1 block text-sm font-medium text-foreground">{labels.notesLabel}</label>
|
|
123
|
-
<textarea
|
|
124
|
-
id="bk-notes" rows={3} value={contact.notes}
|
|
125
|
-
onChange={(e) => onContactChange({ ...contact, notes: e.target.value })}
|
|
126
|
-
className="w-full resize-none rounded-xl border border-border bg-background px-4 py-3 text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring"
|
|
127
|
-
placeholder={labels.notesPlaceholder}
|
|
128
|
-
/>
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
{error ? (
|
|
132
|
-
<p role="alert" className="mt-3 text-sm text-destructive">{labels.submitError}</p>
|
|
133
|
-
) : null}
|
|
134
|
-
<button
|
|
135
|
-
type="submit"
|
|
136
|
-
disabled={!contact.name.trim() || submitting}
|
|
137
|
-
className="mt-5 flex w-full items-center justify-center gap-2 rounded-xl bg-primary py-3 font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
138
|
-
>
|
|
139
|
-
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : <Check className="h-4 w-4" />}
|
|
140
|
-
{payment ? labels.saveAndContinueCta : labels.confirmCta}
|
|
141
|
-
</button>
|
|
142
|
-
{!payment ? (
|
|
143
|
-
<p className="mt-3 flex items-center justify-center gap-1.5 text-xs text-muted-foreground">
|
|
144
|
-
<CreditCard className="h-3.5 w-3.5" /> {labels.noChargeNote}
|
|
145
|
-
</p>
|
|
146
|
-
) : null}
|
|
147
|
-
</form>
|
|
148
|
-
) : null}
|
|
149
|
-
|
|
150
|
-
{/* Code verification modal */}
|
|
151
|
-
{contactStep === 'code' ? (
|
|
152
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-foreground/40 p-4" role="dialog" aria-modal="true" aria-label={labels.codeTitle}>
|
|
153
|
-
<div className="w-full max-w-md rounded-2xl border border-border bg-card p-6 shadow-xl">
|
|
154
|
-
<h4 className="font-heading text-lg font-bold text-foreground">{labels.codeTitle}</h4>
|
|
155
|
-
<p className="mt-1 text-sm text-muted-foreground">{labels.codeBody}</p>
|
|
156
|
-
<form onSubmit={(e) => { e.preventDefault(); onConfirmCode() }}>
|
|
157
|
-
<input
|
|
158
|
-
type="text" inputMode="numeric" value={code} autoFocus
|
|
159
|
-
onChange={(e) => onCodeChange(e.target.value)}
|
|
160
|
-
className="mt-4 w-full rounded-xl border border-border bg-background px-4 py-3 text-center text-lg tracking-[0.4em] text-foreground outline-none transition-colors focus:border-primary focus-visible:ring-2 focus-visible:ring-ring placeholder:text-sm placeholder:tracking-normal"
|
|
161
|
-
placeholder="Digite o código"
|
|
162
|
-
/>
|
|
163
|
-
{codeError ? (
|
|
164
|
-
<p role="alert" className="mt-2 text-sm text-destructive">{labels.codeInvalid}</p>
|
|
165
|
-
) : null}
|
|
166
|
-
<button
|
|
167
|
-
type="submit"
|
|
168
|
-
className="mt-4 w-full rounded-xl bg-primary py-3 font-semibold text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
169
|
-
>
|
|
170
|
-
{labels.codeConfirmCta}
|
|
171
|
-
</button>
|
|
172
|
-
</form>
|
|
173
|
-
<div className="mt-3 flex items-center justify-between text-xs text-muted-foreground">
|
|
174
|
-
<span>{labels.codeResendHint}</span>
|
|
175
|
-
<button type="button" onClick={onBackToPhone} className="font-medium text-primary hover:underline">
|
|
176
|
-
{labels.back}
|
|
177
|
-
</button>
|
|
178
|
-
</div>
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
) : null}
|
|
182
|
-
</div>
|
|
183
|
-
)
|
|
184
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { ChevronLeft, ChevronRight, Clock } from 'lucide-react'
|
|
2
|
-
import type { TimeSlot } from '../../types'
|
|
3
|
-
import type { DayCell } from '../format'
|
|
4
|
-
import type { BookingFormatters, PublicBookingLabels, WorkingHours } from '../types'
|
|
5
|
-
|
|
6
|
-
interface DateTimeStepProps {
|
|
7
|
-
labels: PublicBookingLabels
|
|
8
|
-
fmt: BookingFormatters
|
|
9
|
-
week: DayCell[]
|
|
10
|
-
weekOffset: number
|
|
11
|
-
maxWeekOffset: number
|
|
12
|
-
onWeekOffset: (updater: (w: number) => number) => void
|
|
13
|
-
businessHours: WorkingHours
|
|
14
|
-
date: string | null
|
|
15
|
-
onSelectDate: (iso: string) => void
|
|
16
|
-
slots: TimeSlot[]
|
|
17
|
-
slotsLoading: boolean
|
|
18
|
-
slotStart: string | null
|
|
19
|
-
onSelectSlot: (startIso: string) => void
|
|
20
|
-
onContinue: () => void
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Week strip + available-slot grid. */
|
|
24
|
-
export function DateTimeStep({
|
|
25
|
-
labels, fmt, week, weekOffset, maxWeekOffset, onWeekOffset, businessHours,
|
|
26
|
-
date, onSelectDate, slots, slotsLoading, slotStart, onSelectSlot, onContinue,
|
|
27
|
-
}: DateTimeStepProps) {
|
|
28
|
-
const openDays = new Set(businessHours.daysOfWeek)
|
|
29
|
-
return (
|
|
30
|
-
<div>
|
|
31
|
-
<div className="flex flex-wrap items-baseline justify-between gap-2">
|
|
32
|
-
<h3 className="font-heading text-xl font-bold text-foreground">{labels.dateStep}</h3>
|
|
33
|
-
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
|
34
|
-
<Clock className="h-3.5 w-3.5" /> {labels.timezoneNote}
|
|
35
|
-
</span>
|
|
36
|
-
</div>
|
|
37
|
-
|
|
38
|
-
<div className="mt-5 flex items-center justify-between">
|
|
39
|
-
<span className="text-sm font-semibold capitalize text-foreground">{fmt.month.format(week[0].date)}</span>
|
|
40
|
-
<div className="flex items-center gap-1">
|
|
41
|
-
<button
|
|
42
|
-
type="button"
|
|
43
|
-
disabled={weekOffset === 0}
|
|
44
|
-
onClick={() => onWeekOffset((w) => Math.max(0, w - 1))}
|
|
45
|
-
className="flex h-9 w-9 items-center justify-center rounded-lg border border-border text-foreground transition-colors hover:bg-accent disabled:opacity-40 disabled:hover:bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
46
|
-
aria-label={labels.prevWeek}
|
|
47
|
-
>
|
|
48
|
-
<ChevronLeft className="h-4 w-4" />
|
|
49
|
-
</button>
|
|
50
|
-
<button
|
|
51
|
-
type="button"
|
|
52
|
-
disabled={weekOffset >= maxWeekOffset}
|
|
53
|
-
onClick={() => onWeekOffset((w) => Math.min(maxWeekOffset, w + 1))}
|
|
54
|
-
className="flex h-9 w-9 items-center justify-center rounded-lg border border-border text-foreground transition-colors hover:bg-accent disabled:opacity-40 disabled:hover:bg-transparent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
55
|
-
aria-label={labels.nextWeek}
|
|
56
|
-
>
|
|
57
|
-
<ChevronRight className="h-4 w-4" />
|
|
58
|
-
</button>
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<div className="mt-3 grid grid-cols-7 gap-1.5">
|
|
63
|
-
{week.map((d) => {
|
|
64
|
-
const selected = date === d.iso
|
|
65
|
-
const closed = !openDays.has(d.date.getDay())
|
|
66
|
-
return (
|
|
67
|
-
<button
|
|
68
|
-
key={d.iso}
|
|
69
|
-
type="button"
|
|
70
|
-
disabled={closed}
|
|
71
|
-
onClick={() => onSelectDate(d.iso)}
|
|
72
|
-
className={[
|
|
73
|
-
'flex flex-col items-center gap-0.5 rounded-xl border py-2.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
74
|
-
closed
|
|
75
|
-
? 'cursor-not-allowed border-transparent text-muted-foreground/40'
|
|
76
|
-
: selected
|
|
77
|
-
? 'border-primary bg-primary text-primary-foreground'
|
|
78
|
-
: 'border-border text-foreground hover:border-primary/60 hover:bg-accent/40',
|
|
79
|
-
].join(' ')}
|
|
80
|
-
>
|
|
81
|
-
<span className="text-[11px] font-medium capitalize opacity-80">
|
|
82
|
-
{fmt.weekday.format(d.date).replace('.', '')}
|
|
83
|
-
</span>
|
|
84
|
-
<span className="text-sm font-bold tabular-nums">{fmt.dayNum.format(d.date)}</span>
|
|
85
|
-
</button>
|
|
86
|
-
)
|
|
87
|
-
})}
|
|
88
|
-
</div>
|
|
89
|
-
|
|
90
|
-
{date ? (
|
|
91
|
-
<div className="mt-6">
|
|
92
|
-
<p className="mb-2 text-sm font-semibold text-foreground">{labels.slotsHeading}</p>
|
|
93
|
-
{slotsLoading ? (
|
|
94
|
-
<div className="grid grid-cols-3 gap-2 sm:grid-cols-4">
|
|
95
|
-
{Array.from({ length: 8 }).map((_, i) => (
|
|
96
|
-
<div key={i} className="h-10 animate-pulse rounded-lg bg-muted" />
|
|
97
|
-
))}
|
|
98
|
-
</div>
|
|
99
|
-
) : slots.length === 0 ? (
|
|
100
|
-
<div className="rounded-xl border border-dashed border-border p-6 text-center text-sm text-muted-foreground">
|
|
101
|
-
{labels.noSlots}
|
|
102
|
-
</div>
|
|
103
|
-
) : (
|
|
104
|
-
<div className="grid grid-cols-3 gap-2 sm:grid-cols-4">
|
|
105
|
-
{slots.map((s) => {
|
|
106
|
-
const selected = slotStart === s.start
|
|
107
|
-
return (
|
|
108
|
-
<button
|
|
109
|
-
key={s.start}
|
|
110
|
-
type="button"
|
|
111
|
-
onClick={() => onSelectSlot(s.start)}
|
|
112
|
-
className={[
|
|
113
|
-
'rounded-lg border py-2.5 text-sm font-semibold tabular-nums transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
114
|
-
selected
|
|
115
|
-
? 'border-primary bg-primary text-primary-foreground'
|
|
116
|
-
: 'border-border text-foreground hover:border-primary/60 hover:bg-accent/40',
|
|
117
|
-
].join(' ')}
|
|
118
|
-
>
|
|
119
|
-
{fmt.time.format(new Date(s.start))}
|
|
120
|
-
</button>
|
|
121
|
-
)
|
|
122
|
-
})}
|
|
123
|
-
</div>
|
|
124
|
-
)}
|
|
125
|
-
</div>
|
|
126
|
-
) : null}
|
|
127
|
-
|
|
128
|
-
<button
|
|
129
|
-
type="button"
|
|
130
|
-
disabled={!slotStart}
|
|
131
|
-
onClick={onContinue}
|
|
132
|
-
className="mt-6 w-full rounded-xl bg-primary py-3 font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
133
|
-
>
|
|
134
|
-
{labels.continueCta}
|
|
135
|
-
</button>
|
|
136
|
-
</div>
|
|
137
|
-
)
|
|
138
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { Clock } from 'lucide-react'
|
|
2
|
-
import { DAY_NAMES, WEEK_ORDER } from '../format'
|
|
3
|
-
import type { BookingComponents, BookingFormatters, PublicBookingLabels, PublicService, ResolvedBookingBrand, WorkingHours } from '../types'
|
|
4
|
-
|
|
5
|
-
interface OverviewStepProps {
|
|
6
|
-
services: PublicService[]
|
|
7
|
-
servicesLoading: boolean
|
|
8
|
-
businessHours: WorkingHours
|
|
9
|
-
professionalName: string
|
|
10
|
-
labels: PublicBookingLabels
|
|
11
|
-
brand: ResolvedBookingBrand
|
|
12
|
-
fmt: BookingFormatters
|
|
13
|
-
components: BookingComponents
|
|
14
|
-
onStartBooking: (serviceId?: string) => void
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Default view: services + opening hours + sidebar CTA (Quaddro-style). */
|
|
18
|
-
export function OverviewStep({
|
|
19
|
-
services, servicesLoading, businessHours, professionalName, labels, brand, fmt, components: C, onStartBooking,
|
|
20
|
-
}: OverviewStepProps) {
|
|
21
|
-
const openDays = new Set(businessHours.daysOfWeek)
|
|
22
|
-
return (
|
|
23
|
-
<div className="grid gap-6 lg:grid-cols-[1fr_18rem]">
|
|
24
|
-
<div className="space-y-6">
|
|
25
|
-
{/* Serviços */}
|
|
26
|
-
<section className="rounded-2xl border border-border bg-card p-6 shadow-sm">
|
|
27
|
-
<h3 className="mb-4 font-heading text-lg font-bold text-foreground">{labels.servicesHeading}</h3>
|
|
28
|
-
<div className="space-y-3">
|
|
29
|
-
{servicesLoading ? (
|
|
30
|
-
Array.from({ length: 2 }).map((_, i) => (
|
|
31
|
-
<div key={i} className="h-24 animate-pulse rounded-2xl bg-muted" />
|
|
32
|
-
))
|
|
33
|
-
) : (
|
|
34
|
-
services.map((s, i) => (
|
|
35
|
-
<C.ServiceCard
|
|
36
|
-
key={s.id}
|
|
37
|
-
service={s}
|
|
38
|
-
featured={i === 0}
|
|
39
|
-
onSelect={() => onStartBooking(s.id)}
|
|
40
|
-
fmt={fmt}
|
|
41
|
-
brand={brand}
|
|
42
|
-
/>
|
|
43
|
-
))
|
|
44
|
-
)}
|
|
45
|
-
</div>
|
|
46
|
-
</section>
|
|
47
|
-
|
|
48
|
-
{/* Sobre — horário de funcionamento */}
|
|
49
|
-
<section className="rounded-2xl border border-border bg-card p-6 shadow-sm">
|
|
50
|
-
<h3 className="font-heading text-lg font-bold text-foreground">{labels.aboutHeading}</h3>
|
|
51
|
-
<p className="mt-3 mb-2 flex items-center gap-1.5 text-sm font-medium text-muted-foreground">
|
|
52
|
-
<Clock className="h-4 w-4" /> {labels.openingHoursHeading}
|
|
53
|
-
</p>
|
|
54
|
-
<ul className="divide-y divide-border border-l-2 border-primary/20 pl-4">
|
|
55
|
-
{WEEK_ORDER.map((day) => {
|
|
56
|
-
const open = openDays.has(day)
|
|
57
|
-
return (
|
|
58
|
-
<li key={day} className="flex items-center justify-between py-2 text-sm">
|
|
59
|
-
<span className="text-foreground">{DAY_NAMES[day]}</span>
|
|
60
|
-
<span className={open ? 'tabular-nums text-foreground' : 'text-muted-foreground'}>
|
|
61
|
-
{open ? `${businessHours.start} – ${businessHours.end}` : labels.closedLabel}
|
|
62
|
-
</span>
|
|
63
|
-
</li>
|
|
64
|
-
)
|
|
65
|
-
})}
|
|
66
|
-
</ul>
|
|
67
|
-
</section>
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
{/* Sidebar CTA + highlights */}
|
|
71
|
-
<aside className="lg:sticky lg:top-24 lg:self-start">
|
|
72
|
-
<C.SidebarExtras
|
|
73
|
-
professionalName={professionalName}
|
|
74
|
-
brand={brand}
|
|
75
|
-
labels={labels}
|
|
76
|
-
onStartBooking={() => onStartBooking()}
|
|
77
|
-
/>
|
|
78
|
-
</aside>
|
|
79
|
-
</div>
|
|
80
|
-
)
|
|
81
|
-
}
|