checkout: buttons verbessert, paddle zahlungsschritt schicker gemacht, schritt 4 optimiert+schick gemacht. Dashboard: translations ergänzt. Startseite vom Event Admin optimiert.
This commit is contained in:
@@ -2,12 +2,14 @@ import React, { FormEvent, useCallback, useEffect, useMemo, useRef, useState } f
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LoaderCircle, CheckCircle2, XCircle } from 'lucide-react';
|
||||
import { LoaderCircle, CheckCircle2, XCircle, ShieldCheck, Receipt, Headphones } from 'lucide-react';
|
||||
import { useCheckoutWizard } from '../WizardContext';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { previewCoupon as requestCouponPreview } from '@/lib/coupons';
|
||||
import type { CouponPreviewResponse } from '@/types/coupon';
|
||||
import { cn } from '@/lib/utils';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
type PaymentStatus = 'idle' | 'processing' | 'ready' | 'error';
|
||||
|
||||
@@ -27,6 +29,7 @@ declare global {
|
||||
|
||||
const PADDLE_SCRIPT_URL = 'https://cdn.paddle.com/paddle/v2/paddle.js';
|
||||
const PADDLE_SUPPORTED_LOCALES = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pt', 'sv', 'da', 'fi', 'no'];
|
||||
const PRIMARY_CTA_STYLES = 'min-w-[200px] disabled:bg-muted disabled:text-muted-foreground disabled:hover:bg-muted disabled:hover:text-muted-foreground';
|
||||
|
||||
export function resolvePaddleLocale(rawLocale?: string | null): string {
|
||||
if (!rawLocale) {
|
||||
@@ -94,11 +97,16 @@ async function loadPaddle(environment: PaddleEnvironment): Promise<typeof window
|
||||
return configurePaddle(paddle, environment);
|
||||
}
|
||||
|
||||
const PaddleCta: React.FC<{ onCheckout: () => Promise<void>; disabled: boolean; isProcessing: boolean }> = ({ onCheckout, disabled, isProcessing }) => {
|
||||
const PaddleCta: React.FC<{ onCheckout: () => Promise<void>; disabled: boolean; isProcessing: boolean; className?: string }> = ({ onCheckout, disabled, isProcessing, className }) => {
|
||||
const { t } = useTranslation('marketing');
|
||||
|
||||
return (
|
||||
<Button size="lg" className="w-full sm:w-auto" disabled={disabled} onClick={onCheckout}>
|
||||
<Button
|
||||
size="lg"
|
||||
className={cn('w-full sm:w-auto', className)}
|
||||
disabled={disabled}
|
||||
onClick={onCheckout}
|
||||
>
|
||||
{isProcessing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{t('checkout.payment_step.pay_with_paddle')}
|
||||
</Button>
|
||||
@@ -130,6 +138,7 @@ export const PaymentStep: React.FC = () => {
|
||||
const [couponNotice, setCouponNotice] = useState<string | null>(null);
|
||||
const [couponLoading, setCouponLoading] = useState(false);
|
||||
const paddleRef = useRef<typeof window.Paddle | null>(null);
|
||||
const checkoutContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const eventCallbackRef = useRef<(event: any) => void>();
|
||||
const hasAutoAppliedCoupon = useRef(false);
|
||||
const checkoutContainerClass = 'paddle-checkout-container';
|
||||
@@ -297,6 +306,17 @@ export const PaymentStep: React.FC = () => {
|
||||
setInlineActive(true);
|
||||
setStatus('ready');
|
||||
setMessage(t('checkout.payment_step.paddle_overlay_ready'));
|
||||
if (typeof window !== 'undefined' && checkoutContainerRef.current) {
|
||||
window.requestAnimationFrame(() => {
|
||||
const rect = checkoutContainerRef.current?.getBoundingClientRect();
|
||||
if (!rect) {
|
||||
return;
|
||||
}
|
||||
const offset = 120;
|
||||
const target = Math.max(window.scrollY + rect.top - offset, 0);
|
||||
window.scrollTo({ top: target, behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -380,6 +400,7 @@ export const PaymentStep: React.FC = () => {
|
||||
setMessage(t('checkout.payment_step.paddle_overlay_ready'));
|
||||
setInlineActive(false);
|
||||
setPaymentCompleted(true);
|
||||
toast.success(t('checkout.payment_step.toast_success'));
|
||||
}
|
||||
|
||||
if (event.name === 'checkout.closed') {
|
||||
@@ -494,10 +515,60 @@ export const PaymentStep: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const TrustPill = ({ icon: Icon, label }: { icon: React.ElementType; label: string }) => (
|
||||
<div className="flex items-center gap-2 rounded-full bg-white/10 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm">
|
||||
<Icon className="h-4 w-4 text-white/80" />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const PaddleLogo = () => (
|
||||
<div className="flex items-center gap-3 rounded-full bg-white/15 px-4 py-2 text-white shadow-inner backdrop-blur-sm">
|
||||
<img
|
||||
src="/paddle.logo.svg"
|
||||
alt="Paddle"
|
||||
className="h-6 w-auto brightness-0 invert"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<span className="text-xs font-semibold">{t('checkout.payment_step.paddle_partner')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border bg-card p-6 shadow-sm">
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-2xl border bg-card p-6 shadow-sm">
|
||||
<div className="space-y-6">
|
||||
{!inlineActive && (
|
||||
<div className="overflow-hidden rounded-2xl border bg-gradient-to-br from-[#001835] via-[#002b55] to-[#00407c] p-6 text-white shadow-md">
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="space-y-4">
|
||||
<PaddleLogo />
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-2xl font-semibold">{t('checkout.payment_step.guided_title')}</h3>
|
||||
<p className="text-sm text-white/80">{t('checkout.payment_step.guided_body')}</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<TrustPill icon={ShieldCheck} label={t('checkout.payment_step.trust_secure')} />
|
||||
<TrustPill icon={Receipt} label={t('checkout.payment_step.trust_tax')} />
|
||||
<TrustPill icon={Headphones} label={t('checkout.payment_step.trust_support')} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-stretch gap-3 w-full max-w-sm">
|
||||
<PaddleCta
|
||||
onCheckout={startPaddleCheckout}
|
||||
disabled={status === 'processing'}
|
||||
isProcessing={status === 'processing'}
|
||||
className={cn('bg-white text-[#001835] hover:bg-white/90', PRIMARY_CTA_STYLES)}
|
||||
/>
|
||||
<p className="text-xs text-white/70 text-center">
|
||||
{t('checkout.payment_step.guided_cta_hint')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
<form className="flex flex-col gap-2 sm:flex-row" onSubmit={handleCouponSubmit}>
|
||||
<Input
|
||||
@@ -564,6 +635,7 @@ export const PaymentStep: React.FC = () => {
|
||||
onCheckout={startPaddleCheckout}
|
||||
disabled={status === 'processing'}
|
||||
isProcessing={status === 'processing'}
|
||||
className={PRIMARY_CTA_STYLES}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -586,7 +658,7 @@ export const PaymentStep: React.FC = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className={`${checkoutContainerClass} min-h-[360px]`} />
|
||||
<div ref={checkoutContainerRef} className={`${checkoutContainerClass} min-h-[360px]`} />
|
||||
|
||||
<p className={`text-xs text-muted-foreground ${inlineActive ? 'text-center' : 'sm:text-right'}`}>
|
||||
{t('checkout.payment_step.paddle_disclaimer')}
|
||||
|
||||
Reference in New Issue
Block a user