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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user