Hintergründe zum EventInvitePage Layout Customizer hinzugefügt. Badge und CTA entfernt, Textfelder zu Textareas gemacht. Geschenkgutscheine verbessert, E-Mail-Versand ergänzt + Resend + Confirmationseite mit Code-Copy und Link zur Package-Seite, die den Code als URL-Parameter enthält.

This commit is contained in:
Codex Agent
2025-12-08 16:20:04 +01:00
parent 046e2fe3ec
commit 4784c23e70
35 changed files with 1503 additions and 136 deletions

View File

@@ -9,6 +9,7 @@ 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 { useRateLimitHelper } from '@/hooks/useRateLimitHelper';
import toast from 'react-hot-toast';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
@@ -154,6 +155,9 @@ export const PaymentStep: React.FC = () => {
const [withdrawalTitle, setWithdrawalTitle] = useState<string | null>(null);
const [withdrawalLoading, setWithdrawalLoading] = useState(false);
const [withdrawalError, setWithdrawalError] = useState<string | null>(null);
const RateLimitHelper = useRateLimitHelper('coupon');
const [voucherExpiry, setVoucherExpiry] = useState<string | null>(null);
const [isGiftVoucher, setIsGiftVoucher] = useState(false);
const paddleLocale = useMemo(() => {
const sourceLocale = i18n.language || (typeof document !== 'undefined' ? document.documentElement.lang : null);
@@ -177,6 +181,11 @@ export const PaymentStep: React.FC = () => {
return;
}
if (RateLimitHelper.isLimited(trimmed)) {
setCouponError(t('coupon.errors.too_many_attempts'));
return;
}
setCouponLoading(true);
setCouponError(null);
setCouponNotice(null);
@@ -190,6 +199,8 @@ export const PaymentStep: React.FC = () => {
amount: preview.pricing.formatted.discount,
})
);
setVoucherExpiry(preview.coupon.expires_at ?? null);
setIsGiftVoucher(preview.coupon.code?.toUpperCase().startsWith('GIFT-') ?? false);
if (typeof window !== 'undefined') {
localStorage.setItem('preferred_coupon_code', preview.coupon.code);
}
@@ -197,6 +208,7 @@ export const PaymentStep: React.FC = () => {
setCouponPreview(null);
setCouponNotice(null);
setCouponError(error instanceof Error ? error.message : t('coupon.errors.generic'));
RateLimitHelper.bump(trimmed);
} finally {
setCouponLoading(false);
}
@@ -742,9 +754,26 @@ export const PaymentStep: React.FC = () => {
<span>{t('coupon.fields.total')}</span>
<span>{couponPreview.pricing.formatted.total}</span>
</div>
{voucherExpiry && (
<div className="flex justify-between text-xs text-muted-foreground">
<span>{t('coupon.fields.expires')}</span>
<span>{new Date(voucherExpiry).toLocaleDateString(i18n.language)}</span>
</div>
)}
</div>
</div>
)}
{isGiftVoucher && (
<div className="rounded-md border border-primary/30 bg-primary/5 p-3 text-xs text-muted-foreground">
<span>{t('coupon.legal_note')}{' '}</span>
<a
href={i18n.language === 'de' ? '/de/widerrufsbelehrung' : '/en/withdrawal'}
className="text-primary underline"
>
{t('coupon.legal_link')}
</a>
</div>
)}
</div>
{!inlineActive && (