136 lines
4.6 KiB
TypeScript
136 lines
4.6 KiB
TypeScript
import React, { useMemo, useState } from "react";
|
|
import { Check, Package as PackageIcon, Loader2 } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { useCheckoutWizard } from "../WizardContext";
|
|
import type { CheckoutPackage } from "../types";
|
|
|
|
const currencyFormatter = new Intl.NumberFormat("de-DE", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
minimumFractionDigits: 2,
|
|
});
|
|
|
|
function PackageSummary({ pkg }: { pkg: CheckoutPackage }) {
|
|
return (
|
|
<Card className="shadow-sm">
|
|
<CardHeader className="space-y-1">
|
|
<CardTitle className="flex items-center gap-3 text-2xl">
|
|
<PackageIcon className="h-6 w-6 text-primary" />
|
|
{pkg.name}
|
|
</CardTitle>
|
|
<CardDescription className="text-base text-muted-foreground">
|
|
{pkg.description}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="flex items-baseline gap-2">
|
|
<span className="text-3xl font-semibold">
|
|
{pkg.price === 0 ? "Kostenlos" : currencyFormatter.format(pkg.price)}
|
|
</span>
|
|
<Badge variant="secondary" className="uppercase tracking-wider text-xs">
|
|
{pkg.type === "reseller" ? "Reseller" : "Endkunde"}
|
|
</Badge>
|
|
</div>
|
|
{Array.isArray(pkg.features) && pkg.features.length > 0 && (
|
|
<ul className="space-y-3">
|
|
{pkg.features.map((feature, index) => (
|
|
<li key={index} className="flex items-start gap-3 text-sm text-muted-foreground">
|
|
<Check className="mt-0.5 h-4 w-4 text-primary" />
|
|
<span>{feature}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function PackageOption({ pkg, isActive, onSelect }: { pkg: CheckoutPackage; isActive: boolean; onSelect: () => void }) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onSelect}
|
|
className={`w-full rounded-md border bg-background p-4 text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 ${
|
|
isActive ? "border-primary shadow-sm" : "border-border hover:border-primary/40"
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between text-sm font-medium">
|
|
<span>{pkg.name}</span>
|
|
<span className="text-muted-foreground">
|
|
{pkg.price === 0 ? "Kostenlos" : currencyFormatter.format(pkg.price)}
|
|
</span>
|
|
</div>
|
|
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">{pkg.description}</p>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
export const PackageStep: React.FC = () => {
|
|
const { selectedPackage, packageOptions, setSelectedPackage, resetPaymentState, nextStep } = useCheckoutWizard();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const comparablePackages = useMemo(() => {
|
|
return packageOptions.filter((pkg) => pkg.type === selectedPackage.type);
|
|
}, [packageOptions, selectedPackage.type]);
|
|
|
|
const handlePackageChange = (pkg: CheckoutPackage) => {
|
|
if (pkg.id === selectedPackage.id) {
|
|
return;
|
|
}
|
|
setSelectedPackage(pkg);
|
|
resetPaymentState();
|
|
};
|
|
|
|
const handleNextStep = async () => {
|
|
setIsLoading(true);
|
|
// Kleine Verzögerung für bessere UX
|
|
setTimeout(() => {
|
|
nextStep();
|
|
setIsLoading(false);
|
|
}, 300);
|
|
};
|
|
|
|
return (
|
|
<div className="grid gap-8 lg:grid-cols-[2fr_1fr]">
|
|
<div className="space-y-6">
|
|
<PackageSummary pkg={selectedPackage} />
|
|
<div className="flex justify-end">
|
|
<Button size="lg" onClick={handleNextStep} disabled={isLoading}>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Wird geladen...
|
|
</>
|
|
) : (
|
|
"Weiter zum Konto"
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<aside className="space-y-4">
|
|
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
|
|
Alternative Pakete
|
|
</h3>
|
|
<div className="space-y-3">
|
|
{comparablePackages.map((pkg) => (
|
|
<PackageOption
|
|
key={pkg.id}
|
|
pkg={pkg}
|
|
isActive={pkg.id === selectedPackage.id}
|
|
onSelect={() => handlePackageChange(pkg)}
|
|
/>
|
|
))}
|
|
{comparablePackages.length === 0 && (
|
|
<p className="text-xs text-muted-foreground">
|
|
Keine weiteren Pakete in dieser Kategorie verfuegbar.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
);
|
|
};
|