Integrate LiveKit SFU and harden booking workflow
This commit is contained in:
@@ -4,15 +4,10 @@ import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { BookingStatusBadge, resolveDisplayStatus } from "./BookingStatusBadge";
|
||||
import type { BookingRequest, Event, Offer } from "@/payload-types";
|
||||
import { formatBerlinDate, formatBerlinTime } from "@/lib/calendar/timezone";
|
||||
import type { MeetingStatus } from "@/lib/meeting/status";
|
||||
|
||||
function fmtDate(iso: string) {
|
||||
return new Date(iso).toLocaleDateString("de-DE", { weekday: "long", day: "2-digit", month: "long", year: "numeric" });
|
||||
}
|
||||
function fmtTime(iso: string) {
|
||||
return new Date(iso).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
export function BookingCard({ booking }: { booking: BookingRequest }) {
|
||||
export function BookingCard({ booking, meetingStatus }: { booking: BookingRequest; meetingStatus?: MeetingStatus | null }) {
|
||||
const router = useRouter();
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -46,7 +41,7 @@ export function BookingCard({ booking }: { booking: BookingRequest }) {
|
||||
<div>
|
||||
<h3 className="font-serif text-xl font-medium text-anouma-plum">{offer?.title ?? "Termin"}</h3>
|
||||
<p className="mt-1 text-sm text-anouma-plum/80">
|
||||
{fmtDate(booking.date)} · {fmtTime(booking.startTime)} – {fmtTime(booking.endTime)}
|
||||
{formatBerlinDate(new Date(booking.date), { weekday: "long", day: "2-digit", month: "long", year: "numeric" })} · {formatBerlinTime(new Date(booking.startTime), { hour: "2-digit", minute: "2-digit" })} – {formatBerlinTime(new Date(booking.endTime), { hour: "2-digit", minute: "2-digit" })}
|
||||
</p>
|
||||
</div>
|
||||
<BookingStatusBadge status={displayStatus} />
|
||||
@@ -62,8 +57,8 @@ export function BookingCard({ booking }: { booking: BookingRequest }) {
|
||||
<div className="mt-4 rounded-2xl bg-anouma-cream-light p-4">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-anouma-plum/70">Alternativer Termin</p>
|
||||
<p className="mt-1 text-base text-anouma-plum">
|
||||
{fmtDate(booking.proposedAlternative.date!)} · {fmtTime(booking.proposedAlternative.startTime!)} –{" "}
|
||||
{fmtTime(booking.proposedAlternative.endTime!)}
|
||||
{formatBerlinDate(new Date(booking.proposedAlternative.date!), { weekday: "long", day: "2-digit", month: "long", year: "numeric" })} · {formatBerlinTime(new Date(booking.proposedAlternative.startTime!), { hour: "2-digit", minute: "2-digit" })} –{" "}
|
||||
{formatBerlinTime(new Date(booking.proposedAlternative.endTime!), { hour: "2-digit", minute: "2-digit" })}
|
||||
</p>
|
||||
<div className="mt-3 flex flex-wrap gap-3">
|
||||
<button
|
||||
@@ -88,14 +83,17 @@ export function BookingCard({ booking }: { booking: BookingRequest }) {
|
||||
|
||||
{booking.status === "confirmed" && (
|
||||
<div className="mt-4 space-y-3">
|
||||
{booking.appointmentType === "online" && event && (
|
||||
<a
|
||||
href={`/termine/${event.slug}/beitreten`}
|
||||
className="inline-flex rounded-full bg-anouma-mauve-dark px-5 py-2.5 text-sm font-medium text-white hover:bg-anouma-plum"
|
||||
>
|
||||
{booking.appointmentType === "online" && event && meetingStatus !== "ended" && meetingStatus !== "scheduled" && (
|
||||
<a href={`/termine/${event.slug}/beitreten`} className="inline-flex rounded-full bg-anouma-mauve-dark px-5 py-2.5 text-sm font-medium text-white hover:bg-anouma-plum">
|
||||
Video-Call betreten
|
||||
</a>
|
||||
)}
|
||||
{booking.appointmentType === "online" && event && meetingStatus === "scheduled" && (
|
||||
<p className="text-sm text-anouma-plum/75">Der Video-Call wird 10 Minuten vor Beginn freigeschaltet.</p>
|
||||
)}
|
||||
{booking.appointmentType === "online" && event && meetingStatus === "ended" && (
|
||||
<p className="text-sm text-anouma-plum/75">Dieser Video-Call ist bereits beendet.</p>
|
||||
)}
|
||||
{booking.appointmentType === "onsite" && (
|
||||
<p className="text-sm text-anouma-plum/80">Vor Ort – Details siehe Bestätigungs-E-Mail.</p>
|
||||
)}
|
||||
|
||||
@@ -5,19 +5,16 @@ import { MonthCalendar, type CalendarMarker } from "./MonthCalendar";
|
||||
import { ResendVerificationButton } from "@/components/auth/ResendVerificationButton";
|
||||
import { TextAreaField, FormError } from "@/components/FormField";
|
||||
import { track } from "@/lib/analytics/client";
|
||||
import { berlinDateKey, formatBerlinDate, formatBerlinTime } from "@/lib/calendar/timezone";
|
||||
|
||||
type SlotsResponse = { durationMinutes: number; slots: Record<string, { start: string; end: string }[]> };
|
||||
|
||||
function dateKey(d: Date) {
|
||||
return d.toISOString().slice(0, 10);
|
||||
return berlinDateKey(d);
|
||||
}
|
||||
|
||||
function fmtTime(iso: string) {
|
||||
return new Date(iso).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
function fmtDate(iso: string) {
|
||||
return new Date(iso).toLocaleDateString("de-DE", { weekday: "long", day: "2-digit", month: "long" });
|
||||
}
|
||||
const fmtTime = (iso: string) => formatBerlinTime(new Date(iso), { hour: "2-digit", minute: "2-digit" });
|
||||
const fmtDate = (iso: string) => formatBerlinDate(new Date(iso), { weekday: "long", day: "2-digit", month: "long" });
|
||||
|
||||
export function BookingWidget({
|
||||
offerSlug,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { berlinDateKey, formatBerlinDate } from "@/lib/calendar/timezone";
|
||||
|
||||
export type CalendarMarker = {
|
||||
date: Date;
|
||||
@@ -23,7 +24,7 @@ function startOfMonth(d: Date) {
|
||||
return new Date(d.getFullYear(), d.getMonth(), 1);
|
||||
}
|
||||
function dateKey(d: Date) {
|
||||
return d.toISOString().slice(0, 10);
|
||||
return berlinDateKey(d);
|
||||
}
|
||||
|
||||
export function MonthCalendar({
|
||||
@@ -62,7 +63,7 @@ export function MonthCalendar({
|
||||
return result;
|
||||
}, [month]);
|
||||
|
||||
const monthLabel = month.toLocaleDateString("de-DE", { month: "long", year: "numeric" });
|
||||
const monthLabel = formatBerlinDate(month, { month: "long", year: "numeric" });
|
||||
const today = dateKey(new Date());
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,13 +4,11 @@ import { useState } from "react";
|
||||
import { MonthCalendar, type CalendarMarker } from "./MonthCalendar";
|
||||
import { BookingCard } from "./BookingCard";
|
||||
import { resolveDisplayStatus } from "./BookingStatusBadge";
|
||||
import type { BookingRequest } from "@/payload-types";
|
||||
import type { BookingRequest, MeetingSetting } from "@/payload-types";
|
||||
import { berlinDateKey } from "@/lib/calendar/timezone";
|
||||
import { getBookingMeetingStatus } from "@/lib/booking/meetingAccess";
|
||||
|
||||
function dateKey(d: Date) {
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export function PersonalCalendar({ bookings }: { bookings: BookingRequest[] }) {
|
||||
export function PersonalCalendar({ bookings, meetingSettings }: { bookings: BookingRequest[]; meetingSettings: MeetingSetting }) {
|
||||
const [selectedDay, setSelectedDay] = useState<Date | null>(null);
|
||||
|
||||
const markers: CalendarMarker[] = bookings.map((b) => ({
|
||||
@@ -19,7 +17,7 @@ export function PersonalCalendar({ bookings }: { bookings: BookingRequest[] }) {
|
||||
}));
|
||||
|
||||
const selectedBookings = selectedDay
|
||||
? bookings.filter((b) => dateKey(new Date(b.date)) === dateKey(selectedDay))
|
||||
? bookings.filter((b) => berlinDateKey(new Date(b.date)) === berlinDateKey(selectedDay))
|
||||
: [];
|
||||
|
||||
return (
|
||||
@@ -30,7 +28,7 @@ export function PersonalCalendar({ bookings }: { bookings: BookingRequest[] }) {
|
||||
selectedBookings.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{selectedBookings.map((b) => (
|
||||
<BookingCard key={b.id} booking={b} />
|
||||
<BookingCard key={b.id} booking={b} meetingStatus={getBookingMeetingStatus(b, meetingSettings)} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user