Files
fotospiel-app/resources/js/pages/marketing/checkout/steps/PackageStep.tsx

165 lines
5.9 KiB
TypeScript

import React, { useMemo, useState } from "react";
import { useTranslation } from 'react-i18next';
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 }) {
const isFree = pkg.price === 0;
return (
<Card className={`shadow-sm ${isFree ? "opacity-75" : ""}`}>
<CardHeader className="space-y-1">
<CardTitle className={`flex items-center gap-3 text-2xl ${isFree ? "text-muted-foreground" : ""}`}>
<PackageIcon className={`h-6 w-6 ${isFree ? "text-muted-foreground" : "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 ${isFree ? "text-muted-foreground" : ""}`}>
{pkg.price === 0 ? "Kostenlos" : currencyFormatter.format(pkg.price)}
</span>
<Badge variant={isFree ? "outline" : "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 ${isFree ? "text-muted-foreground" : "text-primary"}`} />
<span>{feature}</span>
</li>
))}
</ul>
)}
</CardContent>
</Card>
);
}
function PackageOption({ pkg, isActive, onSelect }: { pkg: CheckoutPackage; isActive: boolean; onSelect: () => void }) {
const isFree = pkg.price === 0;
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"
: isFree
? "border-border hover:border-primary/40 opacity-75 hover:opacity-100"
: "border-border hover:border-primary/40"
}`}
>
<div className="flex items-center justify-between text-sm font-medium">
<span className={isFree ? "text-muted-foreground" : ""}>{pkg.name}</span>
<span className={isFree ? "text-muted-foreground font-normal" : "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 { t } = useTranslation('marketing');
const { selectedPackage, packageOptions, setSelectedPackage, resetPaymentState, nextStep } = useCheckoutWizard();
const [isLoading, setIsLoading] = useState(false);
// Early return if no package is selected
if (!selectedPackage) {
return (
<div className="text-center py-8">
<p className="text-muted-foreground">{t('checkout.package_step.no_package_selected')}</p>
</div>
);
}
const comparablePackages = useMemo(() => {
// Filter by type and sort: free packages first, then by price ascending
return packageOptions
.filter((pkg: CheckoutPackage) => pkg.type === selectedPackage.type)
.sort((a: CheckoutPackage, b: CheckoutPackage) => {
// Free packages first
if (a.price === 0 && b.price > 0) return -1;
if (a.price > 0 && b.price === 0) return 1;
// Then sort by price ascending
return a.price - b.price;
});
}, [packageOptions, selectedPackage]);
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" />
{t('checkout.package_step.loading')}
</>
) : (
t('checkout.package_step.next_to_account')
)}
</Button>
</div>
</div>
<aside className="space-y-4">
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
{t('checkout.package_step.alternatives_title')}
</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">
{t('checkout.package_step.no_alternatives')}
</p>
)}
</div>
</aside>
</div>
);
};