Blookie
Get Started

  1. Blocks
  2. calenders
  3. 1
import { Avatar, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { Card, CardContent } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { cn } from '@/lib/utils';
import { addDays, format, startOfWeek } from 'date-fns';
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';
import { useState } from 'react';
 
export interface Service {
    id: string;
    name: string;
    price: number;
    duration: number;
}
 
export interface Staff {
    id: string;
    name: string;
    title: string;
    avatar: string;
}
 
export interface TimeSlot {
    date: string;
    slots: string[];
}
 
const services: Service[] = [
    { id: 'male-cut', name: 'Male Haircut', price: 45, duration: 60 },
    { id: 'female-cut', name: 'Female Haircut', price: 45, duration: 60 },
    { id: 'massage', name: 'Back Massage', price: 700, duration: 60 },
];
 
const staffList: Staff[] = [
    { id: 'random', name: 'No Preference', title: 'Best Availability', avatar: '' },
    { id: 'alice', name: 'Alice Johnson', title: 'Senior Stylist', avatar: 'https://blookie.io/stock/person1.webp' },
    { id: 'john', name: 'John Smith', title: 'Barber', avatar: 'https://blookie.io/stock/person3.webp' },
];
 
const today = new Date();
const weekStart = startOfWeek(today, { weekStartsOn: 1 });
const dummySlots: TimeSlot[] = [];
for (let week = 0; week < 2; week++) {
    for (let day = 0; day < 7; day++) {
        const date = addDays(weekStart, week * 7 + day);
        const iso = format(date, 'yyyy-MM-dd');
        const allSlots = ['09:00', '10:00', '11:00', '12:00', '13:00'];
        const slots = allSlots.filter(() => Math.random() > 0.3);
        dummySlots.push({ date: iso, slots });
    }
}
 
function ServiceSidebar({
    selectedService,
    selectedStaff,
    onSelectStaff,
}: {
    selectedService: Service;
    selectedStaff: string;
    onSelectStaff: (id: string) => void;
}) {
    return (
        <div className="space-y-6">
            <div>
                <p className="text-base font-medium">{selectedService.name}</p>
                <p className="text-muted-foreground mt-2 flex items-center gap-2 text-sm">
                    <span>{selectedService.duration} min</span>
                    <span className="text-foreground">${selectedService.price}</span>
                    <a href="#" className="text-primary ml-auto underline">
                        Change
                    </a>
                </p>
            </div>
            <div>
                <h3 className="font-medium">Choose Your Provider</h3>
                <RadioGroup value={selectedStaff} onValueChange={onSelectStaff} className="mt-4 grid gap-3">
                    {staffList.map((staff) => {
                        const isSelected = selectedStaff === staff.id;
                        return (
                            <Label
                                key={staff.id}
                                htmlFor={staff.id}
                                className={cn(`relative flex items-center gap-4 rounded-lg border p-4`, isSelected && 'border-primary')}
                            >
                                <RadioGroupItem value={staff.id} id={staff.id} className="absolute top-2 right-2 h-4 w-4" />
                                <Avatar className="bg-muted">
                                    <AvatarImage src={staff.avatar} alt={staff.name} />
                                </Avatar>
                                <div className="grid gap-1">
                                    <div className="text-sm/none font-medium">{staff.name}</div>
                                    <div className="text-muted-foreground text-xs">{staff.title}</div>
                                </div>
                            </Label>
                        );
                    })}
                </RadioGroup>
            </div>
        </div>
    );
}
 
export default function BookingSection() {
    const [currentWeekStartDate, setCurrentWeekStartDate] = useState<Date>(weekStart);
    const [selectedService] = useState<Service>(services[0]);
    const [selectedStaff, setSelectedStaff] = useState<string>(staffList[0].id);
    const [popoverOpen, setPopoverOpen] = useState(false);
 
    const days = Array.from({ length: 7 }).map((_, i) => addDays(currentWeekStartDate, i));
 
    function handleDateSelect(date: Date) {
        const newStart = startOfWeek(date, { weekStartsOn: 1 });
        setCurrentWeekStartDate(newStart);
        setPopoverOpen(false);
    }
 
    return (
        <section className="py-16 lg:py-32">
            <div className="mx-auto w-full max-w-2xl px-6 lg:max-w-7xl">
                <div className="relative flex flex-col gap-8 lg:flex-row">
                    <div className="flex flex-1 flex-col">
                        <div className="flex items-center justify-between">
                            <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
                                <PopoverTrigger asChild>
                                    <Button size="sm" variant="ghost">
                                        {format(days[0], 'MMM d')}–{format(days[6], 'd, yyyy')} <ChevronDown />
                                    </Button>
                                </PopoverTrigger>
                                <PopoverContent align="start">
                                    <Calendar
                                        mode="single"
                                        required
                                        selected={currentWeekStartDate}
                                        onSelect={(date) => date && handleDateSelect(date)}
                                    />
                                </PopoverContent>
                            </Popover>
 
                            <div className="flex items-center gap-2">
                                <Button variant="ghost" size="sm" onClick={() => setCurrentWeekStartDate((prev) => addDays(prev, -7))}>
                                    <ChevronLeft />
                                </Button>
                                <Button variant="ghost" size="sm" onClick={() => setCurrentWeekStartDate((prev) => addDays(prev, 7))}>
                                    <ChevronRight />
                                </Button>
                                <Popover>
                                    <PopoverTrigger asChild>
                                        <Button size="sm" variant="outline" className="lg:hidden">
                                            Details
                                        </Button>
                                    </PopoverTrigger>
                                    <PopoverContent align="end">
                                        <ServiceSidebar
                                            selectedService={selectedService}
                                            selectedStaff={selectedStaff}
                                            onSelectStaff={setSelectedStaff}
                                        />
                                    </PopoverContent>
                                </Popover>
                            </div>
                        </div>
 
                        <div className="mt-4 grid flex-1 grid-cols-7 overflow-hidden rounded-xl border">
                            {days.map((date) => {
                                const dayStr = format(date, 'EEE');
                                const dateIso = format(date, 'yyyy-MM-dd');
                                const times = dummySlots.find((s) => s.date === dateIso)?.slots || [];
                                return (
                                    <div key={dateIso} className="bg-muted/50 [&:not(:last-child)]:border-r">
                                        <div className="flex flex-col items-center gap-1 p-4 text-sm">
                                            <div>{dayStr}</div>
                                            <div>{format(date, 'd')}</div>
                                        </div>
                                        <div className="grid gap-2 p-0.5 sm:p-2 [&:not(:last-child)]:border-r">
                                            {times.map((time) => (
                                                <Button key={time} variant="outline" className="px-0 text-xs sm:text-sm">
                                                    {time}
                                                </Button>
                                            ))}
                                        </div>
                                    </div>
                                );
                            })}
                        </div>
                    </div>
 
                    <Card className="hidden w-84 shadow-none lg:block">
                        <CardContent>
                            <ServiceSidebar selectedService={selectedService} selectedStaff={selectedStaff} onSelectStaff={setSelectedStaff} />
                        </CardContent>
                    </Card>
                </div>
            </div>
        </section>
    );
}