discraded old booking mechanism, added Try again button in Cancel Page and refactored success page

This commit is contained in:
Rajpreet0 2026-05-25 21:57:18 +02:00
parent 3713f6ef68
commit e7521facc2
6 changed files with 118 additions and 238 deletions

View file

@ -203,7 +203,6 @@ async function handleCheckoutCompleted(session: Stripe.Checkout.Session) {
}
revalidatePath("/courses");
revalidatePath(`/courses/book/${courseId}`);
revalidatePath("/courses/myCourses");
}

View file

@ -1,5 +1,6 @@
import Link from "next/link";
import { Button } from "@/src/components/ui/button";
import { getCourseSummaryForConfirmation } from "@/src/modules/courses/actions/booking-actions";
import { XCircle } from "lucide-react";
interface CancelPageProps {
@ -8,6 +9,8 @@ interface CancelPageProps {
export default async function CourseBookingCancelPage({ params }: CancelPageProps) {
const { courseId } = await params;
const result = await getCourseSummaryForConfirmation(courseId);
const businessSlug = result.success ? (result.course.business?.slug ?? null) : null;
return (
<div className="max-w-2xl mx-auto p-6">
@ -24,9 +27,11 @@ export default async function CourseBookingCancelPage({ params }: CancelPageProp
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Link href={`/courses/book/${courseId}`}>
<Button className="w-full sm:w-auto">Erneut versuchen</Button>
</Link>
{businessSlug && (
<Link href={`/business/${businessSlug}`}>
<Button className="w-full sm:w-auto">Erneut versuchen</Button>
</Link>
)}
<Link href="/">
<Button variant="outline" className="w-full sm:w-auto">
Zur Startseite

View file

@ -1,6 +1,5 @@
import { getCourseForBooking } from "@/src/modules/courses/actions/booking-actions";
import CourseBookingView from "@/src/modules/courses/ui/views/course-booking-view";
import { notFound } from "next/navigation";
import { getCourseSummaryForConfirmation } from "@/src/modules/courses/actions/booking-actions";
import { notFound, redirect } from "next/navigation";
interface CourseBookingPageProps {
params: Promise<{
@ -8,13 +7,18 @@ interface CourseBookingPageProps {
}>;
}
/**
* Die alte direkte Buchungs-View ist abgelöst – gebucht wird nur noch über die
* öffentliche Business-Seite. Alte (geteilte) Links werden hierher geleitet und
* auf die öffentliche Seite des zugehörigen Business weitergeleitet.
*/
export default async function CourseBookingPage({ params }: CourseBookingPageProps) {
const { courseId } = await params;
const result = await getCourseForBooking(courseId);
const result = await getCourseSummaryForConfirmation(courseId);
if (!result.success || !result.course) {
if (!result.success || !result.course.business?.slug) {
notFound();
}
return <CourseBookingView course={result.course} />;
redirect(`/business/${result.course.business.slug}`);
}

View file

@ -1,6 +1,7 @@
import Link from "next/link";
import { Button } from "@/src/components/ui/button";
import { CheckCircle2 } from "lucide-react";
import { getCourseSummaryForConfirmation } from "@/src/modules/courses/actions/booking-actions";
import { CheckCircle2, Calendar, Clock, MapPin } from "lucide-react";
interface SuccessPageProps {
params: Promise<{ courseId: string }>;
@ -9,6 +10,23 @@ interface SuccessPageProps {
export default async function CourseBookingSuccessPage({ params }: SuccessPageProps) {
const { courseId } = await params;
const result = await getCourseSummaryForConfirmation(courseId);
const course = result.success ? result.course : null;
const businessSlug = course?.business?.slug ?? null;
const formattedDate = course
? new Date(course.date).toLocaleDateString("de-DE", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})
: null;
const formattedPrice = course
? new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(course.price)
: null;
return (
<div className="max-w-2xl mx-auto p-6">
@ -24,6 +42,39 @@ export default async function CourseBookingSuccessPage({ params }: SuccessPagePr
Vielen Dank für Ihre Buchung. Sie erhalten in Kürze eine Bestätigungs-E-Mail.
</p>
{course && (
<div className="bg-gray-50 rounded-lg p-5 text-left mb-6">
<h2 className="text-lg font-bold text-gray-900 mb-1">{course.name}</h2>
{course.business?.name && (
<p className="text-sm text-gray-500 mb-4">{course.business.name}</p>
)}
<div className="space-y-2 text-sm text-gray-700">
<div className="flex items-center gap-2.5">
<Calendar size={15} className="text-gray-400 shrink-0" />
<span>{formattedDate}</span>
</div>
<div className="flex items-center gap-2.5">
<Clock size={15} className="text-gray-400 shrink-0" />
<span>
{course.timeFrom} – {course.timeTo}
</span>
</div>
{course.business?.address && (
<div className="flex items-center gap-2.5">
<MapPin size={15} className="text-gray-400 shrink-0" />
<span>{course.business.address}</span>
</div>
)}
</div>
<div className="border-t border-gray-200 mt-4 pt-3 flex items-center justify-between">
<span className="text-sm text-gray-500">Bezahlt</span>
<span className="text-lg font-bold text-gray-900">{formattedPrice}</span>
</div>
</div>
)}
<p className="text-sm text-gray-500 mb-8">
Ihre Buchung wird gerade verarbeitet. Falls Sie den Kurs nicht in Ihrer Übersicht sehen,
warten Sie bitte einen Moment und laden die Seite neu.
@ -33,11 +84,13 @@ export default async function CourseBookingSuccessPage({ params }: SuccessPagePr
<Link href="/courses/myCourses">
<Button className="w-full sm:w-auto">Meine Kurse ansehen</Button>
</Link>
<Link href={`/courses/book/${courseId}`}>
<Button variant="outline" className="w-full sm:w-auto">
Zurück zum Kurs
</Button>
</Link>
{businessSlug && (
<Link href={`/business/${businessSlug}`}>
<Button variant="outline" className="w-full sm:w-auto">
Weitere Kurse ansehen
</Button>
</Link>
)}
</div>
</div>
</div>

View file

@ -10,56 +10,6 @@
import { prisma } from "@/src/lib/prisma";
import { getUserData } from "@/src/lib/auth/getUser";
/**
* Ruft einen Kurs mit Buchungsinformationen ab (öffentlich)
*
* Diese Funktion gibt Kursinformationen inkl. aktueller Teilnehmerzahl zurück.
* Sie ist öffentlich zugänglich für die Buchungsseite.
*
* @param courseId - Die eindeutige ID des Kurses
* @returns Ein Objekt mit success-Flag und Kursdaten
*/
export async function getCourseForBooking(courseId: string) {
try {
const course = await prisma.course.findUnique({
where: { id: courseId },
include: {
_count: {
select: { bookings: { where: { paymentStatus: "paid" } } },
},
},
});
if (!course) {
return {
success: false,
error: "Kurs nicht gefunden",
};
}
if (course.status !== "published") {
return {
success: false,
error: "Dieser Kurs ist nicht verfügbar",
};
}
return {
success: true,
course: {
...course,
currentParticipants: course._count.bookings,
},
};
} catch (error) {
console.error("Error fetching course for booking:", error);
return {
success: false,
error: "Fehler beim Laden des Kurses",
};
}
}
/**
* Überprüft, ob ein User bereits für einen Kurs gebucht hat
*
@ -100,6 +50,47 @@ export async function checkUserBookingStatus(courseId: string) {
}
}
/**
* Lädt eine kompakte Kurs-Zusammenfassung inkl. Business-Slug.
* Wird für die Success-Page (Kursdetails anzeigen) und für den Redirect
* der alten Buchungs-View auf die öffentliche Business-Seite genutzt.
*
* @param courseId - Die eindeutige ID des Kurses
*/
export async function getCourseSummaryForConfirmation(courseId: string) {
try {
const course = await prisma.course.findUnique({
where: { id: courseId },
select: {
id: true,
name: true,
sport: true,
date: true,
timeFrom: true,
timeTo: true,
price: true,
room: true,
business: {
select: {
name: true,
slug: true,
address: true,
},
},
},
});
if (!course) {
return { success: false as const, error: "Kurs nicht gefunden" };
}
return { success: true as const, course };
} catch (error) {
console.error("Error fetching course summary:", error);
return { success: false as const, error: "Fehler beim Laden des Kurses" };
}
}
export async function getBusinessBySlug(slug: string) {
try {
const business = await prisma.business.findUnique({

View file

@ -1,172 +0,0 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/src/components/ui/button";
import { checkUserBookingStatus } from "../../actions/booking-actions";
import { toast } from "sonner";
interface CourseBookingViewProps {
course: {
id: string;
name: string;
sport: string[];
date: Date;
timeFrom: string;
timeTo: string;
description: string;
maxParticipants: number;
currentParticipants: number;
room: string;
price: number;
};
}
const CourseBookingView = ({ course }: CourseBookingViewProps) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [isAlreadyBooked, setIsAlreadyBooked] = useState(false);
const [isCheckingStatus, setIsCheckingStatus] = useState(true);
const spotsAvailable = course.maxParticipants - course.currentParticipants;
const isFull = spotsAvailable <= 0;
// Format date for display
const formattedDate = new Date(course.date).toLocaleDateString("de-DE", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
});
const formattedPrice = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
}).format(course.price);
// Check if user already booked this course
useEffect(() => {
const checkStatus = async () => {
const result = await checkUserBookingStatus(course.id);
if (result.success) {
setIsAlreadyBooked(result.isBooked);
}
setIsCheckingStatus(false);
};
checkStatus();
}, [course.id]);
const handleBooking = async () => {
setIsSubmitting(true);
try {
const response = await fetch("/api/stripe/checkout/course", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ courseId: course.id }),
});
if (response.status === 401) {
window.location.assign("/login");
return;
}
let data: { url?: string; error?: string } = {};
try {
data = (await response.json()) as { url?: string; error?: string };
} catch {
// fallback error message below
}
if (!response.ok || !data.url) {
toast.error(data.error || "Fehler bei der Buchung");
setIsSubmitting(false);
return;
}
window.location.assign(data.url);
} catch (error) {
toast.error("Ein unerwarteter Fehler ist aufgetreten");
console.error(error);
setIsSubmitting(false);
}
};
return (
<div className="max-w-2xl mx-auto p-6">
<div className="bg-white rounded-lg shadow-md p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold mb-2">{course.name}</h1>
<p className="text-lg text-gray-600">{course.sport.join(", ")}</p>
</div>
<div className="space-y-4 mb-6">
<div>
<h2 className="font-semibold text-gray-700">Datum</h2>
<p className="text-gray-900">{formattedDate}</p>
</div>
<div>
<h2 className="font-semibold text-gray-700">Uhrzeit</h2>
<p className="text-gray-900">
{course.timeFrom} - {course.timeTo}
</p>
</div>
<div>
<h2 className="font-semibold text-gray-700">Raum</h2>
<p className="text-gray-900">{course.room}</p>
</div>
<div>
<h2 className="font-semibold text-gray-700">Verfügbare Plätze</h2>
<p className="text-gray-900">
{course.currentParticipants} / {course.maxParticipants} Teilnehmer
{isFull && <span className="text-red-500 ml-2">(Ausgebucht)</span>}
{!isFull && (
<span className="text-green-600 ml-2">({spotsAvailable} Plätze frei)</span>
)}
</p>
</div>
<div>
<h2 className="font-semibold text-gray-700">Preis</h2>
<p className="text-2xl font-bold text-gray-900">{formattedPrice}</p>
</div>
<div>
<h2 className="font-semibold text-gray-700 mb-2">Was Sie mitbringen sollten</h2>
<div
className="prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: course.description }}
/>
</div>
</div>
<div className="flex gap-4">
{isCheckingStatus ? (
<Button disabled className="flex-1">
Lade Status...
</Button>
) : isAlreadyBooked ? (
<Button disabled className="flex-1">
Bereits angemeldet
</Button>
) : (
<Button onClick={handleBooking} disabled={isSubmitting || isFull} className="flex-1">
{isSubmitting
? "Wird weitergeleitet..."
: isFull
? "Ausgebucht"
: `Jetzt buchen - ${formattedPrice}`}
</Button>
)}
</div>
{isAlreadyBooked && (
<p className="text-green-600 text-center mt-4">✓ Sie sind für diesen Kurs angemeldet</p>
)}
</div>
</div>
);
};
export default CourseBookingView;