Fix time picker, WIP date picker
This commit is contained in:
@@ -85,7 +85,6 @@
|
|||||||
let defaultHours = $state<WorkingHourRow[]>([]);
|
let defaultHours = $state<WorkingHourRow[]>([]);
|
||||||
let defaultHoursIsLoading = $state(true);
|
let defaultHoursIsLoading = $state(true);
|
||||||
|
|
||||||
/** Format time from HH:MM:SS to 12-hour format */
|
|
||||||
/** Format time from HH:MM:SS to 12-hour format, with "Noon" for 12:00 PM */
|
/** Format time from HH:MM:SS to 12-hour format, with "Noon" for 12:00 PM */
|
||||||
function formatTime(time: string): string {
|
function formatTime(time: string): string {
|
||||||
const [hours, minutes] = time.split(':').map(Number);
|
const [hours, minutes] = time.split(':').map(Number);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// src/routes/api/[...path]/+server.ts
|
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
@@ -6,7 +5,11 @@ const BACKEND_URL =
|
|||||||
import.meta.env.VITE_BACKEND_URL || 'http://localhost:8080';
|
import.meta.env.VITE_BACKEND_URL || 'http://localhost:8080';
|
||||||
|
|
||||||
async function proxyRequest(request: Request, path: string) {
|
async function proxyRequest(request: Request, path: string) {
|
||||||
const url = `${BACKEND_URL}/api/${path}`;
|
const incomingUrl = new URL(request.url);
|
||||||
|
|
||||||
|
const queryString = incomingUrl.search;
|
||||||
|
|
||||||
|
const url = `${BACKEND_URL}/api/${path}${queryString}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const headers = new Headers(request.headers);
|
const headers = new Headers(request.headers);
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
import { Textarea } from '$lib/components/ui/textarea/index.js';
|
import { Textarea } from '$lib/components/ui/textarea/index.js';
|
||||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||||
import Calendar from '$lib/components/ui/calendar/calendar.svelte';
|
import Calendar from '$lib/components/ui/calendar/calendar.svelte';
|
||||||
import { CalendarDate, getLocalTimeZone } from '@internationalized/date';
|
import { CalendarDate, getLocalTimeZone, type DateValue } from '@internationalized/date';
|
||||||
|
|
||||||
// Booking state
|
// Booking state
|
||||||
let currentStep = $state<number>(1);
|
let currentStep = $state<number>(1);
|
||||||
let selectedServices = $state<any[]>([]);
|
let selectedServices = $state<any[]>([]);
|
||||||
let selectedDate = $state<CalendarDate | undefined>(new CalendarDate(2025, 6, 12));
|
let selectedDate = $state<CalendarDate | undefined>(undefined);
|
||||||
let selectedTime = $state<string | null>(null);
|
let selectedTime = $state<string | null>(null);
|
||||||
let customerInfo = $state({
|
let customerInfo = $state({
|
||||||
firstName: '',
|
firstName: '',
|
||||||
@@ -81,33 +81,246 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Mock unavailable dates
|
// Working hours state
|
||||||
const bookedDates = Array.from({ length: 5 }, (_, i) => new CalendarDate(2025, 6, 17 + i));
|
let workingHours = $state<Record<
|
||||||
|
string,
|
||||||
|
{ isOpen: boolean; startTime: string; endTime: string }
|
||||||
|
> | null>(null);
|
||||||
|
let loadingWorkingHours = $state<boolean>(false);
|
||||||
|
const workingHoursCache = new Map<
|
||||||
|
string,
|
||||||
|
Record<string, { isOpen: boolean; startTime: string; endTime: string }>
|
||||||
|
>();
|
||||||
|
|
||||||
function generateTimeSlots(duration: number) {
|
// Initialize date boundaries
|
||||||
const slots = [];
|
const today = new Date();
|
||||||
const startHour = 9;
|
const maxDate = new Date();
|
||||||
const endHour = 17;
|
maxDate.setMonth(today.getMonth() + 6);
|
||||||
|
|
||||||
for (let hour = startHour; hour < endHour; hour++) {
|
// Create CalendarDate objects directly without toCalendar conversion
|
||||||
for (let minute = 0; minute < 60; minute += 15) {
|
const minDate = new CalendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
||||||
// Changed to 15-minute intervals
|
const maxCalendarDate = new CalendarDate(
|
||||||
const timeStr = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
maxDate.getFullYear(),
|
||||||
|
maxDate.getMonth() + 1,
|
||||||
|
maxDate.getDate()
|
||||||
|
);
|
||||||
|
let placeholder = $state<CalendarDate>(minDate);
|
||||||
|
$effect(() => {
|
||||||
|
const monthKey = `${placeholder.year}-${String(placeholder.month).padStart(2, '0')}`;
|
||||||
|
if (!workingHoursCache.has(monthKey)) {
|
||||||
|
fetchWorkingHoursForMonth(placeholder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Check if there's enough time before closing
|
// Fetch working hours for a given month
|
||||||
const endTime = new Date();
|
async function fetchWorkingHoursForMonth(date: CalendarDate) {
|
||||||
endTime.setHours(hour, minute + duration, 0, 0);
|
const monthKey = `${date.year}-${String(date.month).padStart(2, '0')}`;
|
||||||
if (endTime.getHours() <= endHour) {
|
if (workingHoursCache.has(monthKey)) {
|
||||||
slots.push(timeStr);
|
workingHours = workingHoursCache.get(monthKey)!;
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingWorkingHours = true;
|
||||||
|
try {
|
||||||
|
// Calculate start and end of month
|
||||||
|
const startOfMonth = new CalendarDate(date.year, date.month, 1);
|
||||||
|
const endOfMonth = new CalendarDate(
|
||||||
|
date.year,
|
||||||
|
date.month,
|
||||||
|
date.calendar.getDaysInMonth(date)
|
||||||
|
);
|
||||||
|
|
||||||
|
const startStr = startOfMonth.toString();
|
||||||
|
const endStr = endOfMonth.toString();
|
||||||
|
|
||||||
|
const response = await fetch(`/api/scheduling/working-hours?start=${startStr}&end=${endStr}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkingHoursDay = {
|
||||||
|
date: string;
|
||||||
|
weekday: number;
|
||||||
|
startTime: string;
|
||||||
|
endTime: string;
|
||||||
|
isOpen: boolean;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const data: Array<WorkingHoursDay> = await response.json();
|
||||||
|
|
||||||
|
const hoursMap: Record<string, { isOpen: boolean; startTime: string; endTime: string }> = {};
|
||||||
|
data.forEach((day) => {
|
||||||
|
hoursMap[day.date] = {
|
||||||
|
isOpen: day.isOpen,
|
||||||
|
startTime: day.startTime,
|
||||||
|
endTime: day.endTime
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
workingHoursCache.set(monthKey, hoursMap);
|
||||||
|
workingHours = hoursMap;
|
||||||
|
|
||||||
|
// Set default selected date if not set
|
||||||
|
if (!selectedDate) {
|
||||||
|
setDefaultSelectedDate(hoursMap);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch working hours:', error);
|
||||||
|
// Fallback to current date if API fails
|
||||||
|
if (!selectedDate) {
|
||||||
|
selectedDate = minDate;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loadingWorkingHours = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default selected date to next available working day
|
||||||
|
function setDefaultSelectedDate(
|
||||||
|
hoursMap: Record<string, { isOpen: boolean; startTime: string; endTime: string }>
|
||||||
|
) {
|
||||||
|
const currentDate = new Date();
|
||||||
|
let nextDate = new Date(currentDate);
|
||||||
|
|
||||||
|
// Check next 30 days for available working day
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
nextDate.setDate(currentDate.getDate() + i);
|
||||||
|
const dateStr = nextDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
if (hoursMap[dateStr]?.isOpen) {
|
||||||
|
selectedDate = new CalendarDate(
|
||||||
|
nextDate.getFullYear(),
|
||||||
|
nextDate.getMonth() + 1,
|
||||||
|
nextDate.getDate()
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback to today if no working day found
|
||||||
|
if (!selectedDate) {
|
||||||
|
selectedDate = minDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the end time based on a 24-hour start time and a duration in minutes.
|
||||||
|
* @param startTime - Time string in "HH:MM:SS" format (e.g., "09:00:00").
|
||||||
|
* @param durationMinutes - The total duration of the service in minutes.
|
||||||
|
* @returns Time string in "HH:MM" 24-hour format (e.g., "10:30").
|
||||||
|
*/
|
||||||
|
function calculateEndTime(startTime: string, durationMinutes: number): string {
|
||||||
|
const [hours, minutes] = startTime.split(':').map(Number);
|
||||||
|
|
||||||
|
// Create a fixed date object to manipulate the time
|
||||||
|
const date = new Date();
|
||||||
|
// Set time on the fixed date
|
||||||
|
date.setHours(hours, minutes, 0, 0);
|
||||||
|
|
||||||
|
// Add duration in minutes
|
||||||
|
date.setMinutes(date.getMinutes() + durationMinutes);
|
||||||
|
|
||||||
|
// Format back to HH:MM (24-hour format)
|
||||||
|
const endHours = date.getHours().toString().padStart(2, '0');
|
||||||
|
const endMinutes = date.getMinutes().toString().padStart(2, '0');
|
||||||
|
|
||||||
|
return `${endHours}:${endMinutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format time from HH:MM:SS (or HH:MM) to human-readable 12-hour format, with "Noon" for 12:00 PM */
|
||||||
|
function formatTime(time: string): string {
|
||||||
|
// Handle cases where seconds might be present (HH:MM:SS) or not (HH:MM)
|
||||||
|
const parts = time.split(':').map(Number);
|
||||||
|
const hours = parts[0];
|
||||||
|
const minutes = parts.length > 1 ? parts[1] : 0;
|
||||||
|
|
||||||
|
// Special case for 12:00
|
||||||
|
if (hours === 12 && minutes === 0) {
|
||||||
|
return 'Noon';
|
||||||
|
} else if (hours === 0 && minutes === 0) {
|
||||||
|
return 'Midnight';
|
||||||
|
}
|
||||||
|
|
||||||
|
const period = hours >= 12 ? 'PM' : 'AM';
|
||||||
|
const displayHours = hours % 12 || 12;
|
||||||
|
return `${displayHours}:${minutes.toString().padStart(2, '0')} ${period}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize with current month
|
||||||
|
fetchWorkingHoursForMonth(minDate);
|
||||||
|
|
||||||
|
// Handle calendar month change
|
||||||
|
function handleMonthChange(newMonth: CalendarDate) {
|
||||||
|
// Only fetch if within 6 months range
|
||||||
|
if (newMonth.compare(maxCalendarDate) <= 0) {
|
||||||
|
fetchWorkingHoursForMonth(newMonth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if date is unavailable - updated to handle DateValue type
|
||||||
|
function isDateUnavailable(date: DateValue): boolean {
|
||||||
|
// Only CalendarDate has compare method
|
||||||
|
if (!(date instanceof CalendarDate)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if date is outside allowed range
|
||||||
|
if (date.compare(minDate) < 0 || date.compare(maxCalendarDate) > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have working hours data
|
||||||
|
if (!workingHours) return false;
|
||||||
|
|
||||||
|
const dateStr = date.toString();
|
||||||
|
const dayHours = workingHours[dateStr];
|
||||||
|
|
||||||
|
// If no data for this date, assume unavailable
|
||||||
|
if (!dayHours) return true;
|
||||||
|
|
||||||
|
return !dayHours.isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTimeSlots(duration: number, date: CalendarDate | undefined) {
|
||||||
|
if (!date || !workingHours) return [];
|
||||||
|
|
||||||
|
const dateStr = date.toString();
|
||||||
|
const dayHours = workingHours[dateStr];
|
||||||
|
|
||||||
|
// If no working hours data or closed, return empty
|
||||||
|
if (!dayHours || !dayHours.isOpen) return [];
|
||||||
|
|
||||||
|
const slots = [];
|
||||||
|
const [startHour, startMinute] = dayHours.startTime.split(':').map(Number);
|
||||||
|
const [endHour, endMinute] = dayHours.endTime.split(':').map(Number);
|
||||||
|
|
||||||
|
// Convert to minutes for easier calculation
|
||||||
|
const startTotalMinutes = startHour * 60 + startMinute;
|
||||||
|
const endTotalMinutes = endHour * 60 + endMinute;
|
||||||
|
|
||||||
|
// Generate 15-minute intervals
|
||||||
|
for (let minutes = startTotalMinutes; minutes < endTotalMinutes; minutes += 15) {
|
||||||
|
const slotEndMinutes = minutes + duration;
|
||||||
|
|
||||||
|
// Skip if slot would end after closing time
|
||||||
|
if (slotEndMinutes > endTotalMinutes) continue;
|
||||||
|
|
||||||
|
const hour = Math.floor(minutes / 60);
|
||||||
|
const minute = minutes % 60;
|
||||||
|
const timeStr = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
slots.push(timeStr);
|
||||||
|
}
|
||||||
|
|
||||||
return slots;
|
return slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate time slots based on selected services (use longest duration)
|
// Generate time slots based on selected services and date
|
||||||
const timeSlots = $derived(
|
const timeSlots = $derived(
|
||||||
selectedServices.length > 0 ? generateTimeSlots(getTotalDuration()) : []
|
selectedServices.length > 0 && selectedDate
|
||||||
|
? generateTimeSlots(getTotalDuration(), selectedDate)
|
||||||
|
: []
|
||||||
);
|
);
|
||||||
|
|
||||||
function getTotalDuration() {
|
function getTotalDuration() {
|
||||||
@@ -125,6 +338,8 @@
|
|||||||
} else {
|
} else {
|
||||||
selectedServices = [...selectedServices, service];
|
selectedServices = [...selectedServices, service];
|
||||||
}
|
}
|
||||||
|
// Reset time selection when services change
|
||||||
|
selectedTime = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isServiceSelected(service: any) {
|
function isServiceSelected(service: any) {
|
||||||
@@ -157,7 +372,6 @@
|
|||||||
function nextStep() {
|
function nextStep() {
|
||||||
if (currentStep < 4) {
|
if (currentStep < 4) {
|
||||||
currentStep++;
|
currentStep++;
|
||||||
// Use setTimeout to ensure DOM is updated before scrolling
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}, 50);
|
}, 50);
|
||||||
@@ -167,7 +381,6 @@
|
|||||||
function prevStep() {
|
function prevStep() {
|
||||||
if (currentStep > 1) {
|
if (currentStep > 1) {
|
||||||
currentStep--;
|
currentStep--;
|
||||||
// Use setTimeout to ensure DOM is updated before scrolling
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
}, 50);
|
}, 50);
|
||||||
@@ -175,7 +388,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleBooking() {
|
function handleBooking() {
|
||||||
// This would connect to your backend
|
|
||||||
alert('Booking submitted! (This is just a prototype - no backend connected yet)');
|
alert('Booking submitted! (This is just a prototype - no backend connected yet)');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +450,7 @@
|
|||||||
class="focus:ring-primary cursor-pointer rounded-lg p-4 text-left shadow-sm transition-colors hover:bg-fuchsia-50 {isServiceSelected(
|
class="focus:ring-primary cursor-pointer rounded-lg p-4 text-left shadow-sm transition-colors hover:bg-fuchsia-50 {isServiceSelected(
|
||||||
service
|
service
|
||||||
)
|
)
|
||||||
? 'bg-fuchsia-100'
|
? 'bg-fuchsia-200'
|
||||||
: 'border-ring'}"
|
: 'border-ring'}"
|
||||||
onclick={() => toggleService(service)}
|
onclick={() => toggleService(service)}
|
||||||
>
|
>
|
||||||
@@ -314,46 +526,48 @@
|
|||||||
</Card.Header>
|
</Card.Header>
|
||||||
<Card.Content class="p-0">
|
<Card.Content class="p-0">
|
||||||
<Card.Root class="gap-0 border-0 p-0">
|
<Card.Root class="gap-0 border-0 p-0">
|
||||||
<Card.Content class="relative p-0 md:pr-48">
|
<Card.Content class="relative p-0 md:pr-56">
|
||||||
<div class="flex items-center justify-center p-6">
|
<div class="flex items-center justify-center p-6">
|
||||||
<Calendar
|
<Calendar
|
||||||
type="single"
|
type="single"
|
||||||
bind:value={selectedDate}
|
bind:value={selectedDate}
|
||||||
isDateUnavailable={(date) => bookedDates.some((d) => d.compare(date) === 0)}
|
bind:placeholder
|
||||||
|
{isDateUnavailable}
|
||||||
class="data-unavailable:line-through data-unavailable:opacity-100 bg-transparent p-0 [--cell-size:--spacing(10)] md:[--cell-size:--spacing(12)] [&_[data-outside-month]]:hidden"
|
class="data-unavailable:line-through data-unavailable:opacity-100 bg-transparent p-0 [--cell-size:--spacing(10)] md:[--cell-size:--spacing(12)] [&_[data-outside-month]]:hidden"
|
||||||
weekdayFormat="short"
|
weekdayFormat="short"
|
||||||
|
minValue={minDate}
|
||||||
|
maxValue={maxCalendarDate}
|
||||||
|
weekStartsOn={1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="no-scrollbar inset-y-0 right-0 flex max-h-48 w-full scroll-pb-6 flex-col gap-4 overflow-y-auto border-t p-6 md:absolute md:max-h-none md:w-48 md:border-l md:border-t-0"
|
class="no-scrollbar inset-y-0 right-0 flex max-h-48 w-full scroll-pb-6 flex-col gap-4 overflow-y-auto border-t p-6 md:absolute md:max-h-none md:w-56 md:border-l md:border-t-0"
|
||||||
>
|
>
|
||||||
<div class="grid gap-2">
|
{#if loadingWorkingHours && selectedDate}
|
||||||
{#if timeSlots.length > 0}
|
<div class="text-center text-sm text-gray-500">Loading available times...</div>
|
||||||
|
{:else if timeSlots.length > 0}
|
||||||
|
{@const duration = getTotalDuration()}
|
||||||
|
<div class="grid gap-2">
|
||||||
{#each timeSlots as time (time)}
|
{#each timeSlots as time (time)}
|
||||||
{@const timeStatus = isTimeInDuration(time, selectedTime, getTotalDuration())}
|
{@const endTime = calculateEndTime(time, duration)}
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onclick={() => (selectedTime = time)}
|
onclick={() => (selectedTime = time)}
|
||||||
class={`w-full hover:bg-fuchsia-50 ${
|
class={`w-full hover:bg-fuchsia-50 ${
|
||||||
timeStatus === 'start'
|
time === selectedTime ? ' bg-fuchsia-200' : ''
|
||||||
? ' text-primary bg-fuchsia-200'
|
|
||||||
: timeStatus === 'duration'
|
|
||||||
? ' text-primary bg-fuchsia-100'
|
|
||||||
: ''
|
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{time}
|
{formatTime(time)} <span class="text-gray-500">- {formatTime(endTime)}</span>
|
||||||
{#if timeStatus === 'duration'}
|
|
||||||
<span class="ml-1 text-xs opacity-70">(selected)</span>
|
|
||||||
{/if}
|
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
{:else if selectedServices.length === 0}
|
</div>
|
||||||
<p class="text-center text-sm text-gray-500">Select services first</p>
|
{:else if selectedServices.length === 0}
|
||||||
{:else}
|
<p class="text-center text-sm text-gray-500">Select services first</p>
|
||||||
<p class="text-center text-sm text-gray-500">No available slots</p>
|
{:else if !selectedDate}
|
||||||
{/if}
|
<p class="text-center text-sm text-gray-500">Select a date first</p>
|
||||||
</div>
|
{:else}
|
||||||
|
<p class="text-center text-sm text-gray-500">No available slots</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
@@ -370,7 +584,7 @@
|
|||||||
month: 'short'
|
month: 'short'
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
at <span class="font-medium">{selectedTime}</span>
|
<br />at <span class="font-medium">{formatTime(selectedTime)}</span>
|
||||||
{:else}
|
{:else}
|
||||||
Select a date and time
|
Select a date and time
|
||||||
{/if}
|
{/if}
|
||||||
@@ -390,7 +604,7 @@
|
|||||||
month: 'short'
|
month: 'short'
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
at <span class="font-medium">{selectedTime}</span>
|
at <span class="font-medium">{formatTime(selectedTime)}</span>
|
||||||
{:else}
|
{:else}
|
||||||
Select a date and time
|
Select a date and time
|
||||||
{/if}
|
{/if}
|
||||||
@@ -511,7 +725,7 @@
|
|||||||
<Button
|
<Button
|
||||||
disabled={!canProceedStep3}
|
disabled={!canProceedStep3}
|
||||||
onclick={nextStep}
|
onclick={nextStep}
|
||||||
class="bg-primary text-primary-foreground hover:bg-primary/90"
|
class="bg-primary text-primary-foreground"
|
||||||
>
|
>
|
||||||
Next: Payment
|
Next: Payment
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user