From 4bdb93c17145713d2ed15ec92bee8287110354d0 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Mon, 8 Dec 2025 20:18:01 +0100 Subject: [PATCH] gift voucher language fixes + move into user menu --- public/lang/de/marketing.json | 21 +++- public/lang/en/marketing.json | 21 +++- resources/js/i18n.ts | 3 +- resources/js/layouts/mainWebsite.tsx | 10 ++ resources/js/pages/marketing/GiftVoucher.tsx | 88 +-------------- .../js/pages/marketing/GiftVoucherStatus.tsx | 103 ++++++++++++++++++ resources/lang/de/marketing.php | 19 ++++ resources/lang/en/marketing.php | 19 ++++ routes/web.php | 4 + 9 files changed, 199 insertions(+), 89 deletions(-) create mode 100644 resources/js/pages/marketing/GiftVoucherStatus.tsx diff --git a/public/lang/de/marketing.json b/public/lang/de/marketing.json index 64a87aa..f5bf272 100644 --- a/public/lang/de/marketing.json +++ b/public/lang/de/marketing.json @@ -980,7 +980,24 @@ "error_purchaser_email": "Bitte gib eine gültige E-Mail ein.", "error_recipient_email": "Bitte gib eine gültige Empfänger-E-Mail ein.", "error_checkout": "Checkout konnte nicht gestartet werden. Bitte versuche es erneut.", - "error": "Etwas ist schiefgelaufen. Bitte versuche es erneut." + "error": "Etwas ist schiefgelaufen. Bitte versuche es erneut.", + "lookup_title": "Gutscheinstatus prüfen", + "lookup_subtitle": "Du hast schon einen Code? Prüfe Wert, Gültigkeit und Status.", + "lookup_label": "Gutscheincode", + "lookup_cta": "Code prüfen", + "lookup_not_found": "Gutschein nicht gefunden oder nicht mehr gültig.", + "lookup_result_code": "Code: :code", + "lookup_result_value": "Wert: :amount :currency", + "lookup_result_expires": "Gültig bis :date", + "lookup_status": { + "issued": "Status: Ausgestellt (einlösbar)", + "redeemed": "Status: Eingelöst", + "refunded": "Status: Erstattet", + "expired": "Status: Abgelaufen", + "reminder": "Erinnerung geplant", + "expiry": "Ablauf-Hinweis geplant" + }, + "too_many_attempts": "Zu viele Versuche. Bitte kurz warten und erneut probieren." }, "not_found": { "title": "Seite nicht gefunden", @@ -1002,4 +1019,4 @@ "privacy": "Datenschutz", "terms": "AGB" } -} \ No newline at end of file +} diff --git a/public/lang/en/marketing.json b/public/lang/en/marketing.json index 2905200..a2ce3e6 100644 --- a/public/lang/en/marketing.json +++ b/public/lang/en/marketing.json @@ -973,7 +973,24 @@ "error_purchaser_email": "Please enter a valid email.", "error_recipient_email": "Please enter a valid recipient email.", "error_checkout": "Unable to start the checkout. Please try again.", - "error": "Something went wrong. Please try again." + "error": "Something went wrong. Please try again.", + "lookup_title": "Check voucher status", + "lookup_subtitle": "Already have a code? See value, validity, and status.", + "lookup_label": "Voucher code", + "lookup_cta": "Check code", + "lookup_not_found": "Voucher not found or no longer valid.", + "lookup_result_code": "Code: :code", + "lookup_result_value": "Value: :amount :currency", + "lookup_result_expires": "Valid until :date", + "lookup_status": { + "issued": "Status: Issued (ready to redeem)", + "redeemed": "Status: Redeemed", + "refunded": "Status: Refunded", + "expired": "Status: Expired", + "reminder": "Reminder scheduled", + "expiry": "Expiry reminder scheduled" + }, + "too_many_attempts": "Too many attempts. Please wait a moment and try again." }, "not_found": { "title": "Page not found", @@ -995,4 +1012,4 @@ "privacy": "Privacy", "terms": "Terms & Conditions" } -} \ No newline at end of file +} diff --git a/resources/js/i18n.ts b/resources/js/i18n.ts index 2d0c25c..821b404 100644 --- a/resources/js/i18n.ts +++ b/resources/js/i18n.ts @@ -28,7 +28,8 @@ i18n escapeValue: false, }, backend: { - loadPath: '/lang/{{lng}}/{{ns}}.json', + // Cache-bust to ensure fresh translations when files change. + loadPath: '/lang/{{lng}}/{{ns}}.json?v=20250204', }, react: { useSuspense: false, diff --git a/resources/js/layouts/mainWebsite.tsx b/resources/js/layouts/mainWebsite.tsx index 6749a51..4aed90f 100644 --- a/resources/js/layouts/mainWebsite.tsx +++ b/resources/js/layouts/mainWebsite.tsx @@ -287,6 +287,16 @@ const MarketingLayout: React.FC = ({ children, title }) => {user.name ?? user.email} + { + event.preventDefault(); + router.visit(localizedPath('/voucher-status')); + }} + className="flex items-center gap-2 font-sans-marketing dark:text-gray-100" + > + + {t('gift.lookup_title', 'Gutscheinstatus')} + { event.preventDefault(); diff --git a/resources/js/pages/marketing/GiftVoucher.tsx b/resources/js/pages/marketing/GiftVoucher.tsx index 3290ed9..9dbb68a 100644 --- a/resources/js/pages/marketing/GiftVoucher.tsx +++ b/resources/js/pages/marketing/GiftVoucher.tsx @@ -10,34 +10,30 @@ import { Textarea } from '@/components/ui/textarea'; import { fetchGiftVoucherTiers, createGiftVoucherCheckout, - fetchGiftVoucherByCode, type GiftVoucherTier, - type GiftVoucherLookupResponse, } from '@/lib/giftVouchers'; import { useLocalizedRoutes } from '@/hooks/useLocalizedRoutes'; import { cn } from '@/lib/utils'; -import { useRateLimitHelper } from '@/hooks/useRateLimitHelper'; function useGiftVoucherTiers(initial: GiftVoucherTier[] = []) { const [tiers, setTiers] = React.useState(initial); const [loading, setLoading] = React.useState(initial.length === 0); const [error, setError] = React.useState(null); - const { locale } = useLocalizedRoutes(); React.useEffect(() => { if (initial.length > 0) { + setTiers(initial.filter((tier) => tier.currency === 'EUR')); setLoading(false); return; } fetchGiftVoucherTiers() .then((data) => { - const preferredCurrency = locale === 'en' ? 'USD' : 'EUR'; - const preferred = data.filter((tier) => tier.currency === preferredCurrency && tier.can_checkout); - setTiers(preferred.length > 0 ? preferred : data); + const eurOnly = data.filter((tier) => tier.currency === 'EUR'); + setTiers(eurOnly.length > 0 ? eurOnly : data); }) .catch((err) => setError(err?.message || 'Failed to load tiers')) .finally(() => setLoading(false)); - }, [initial, locale]); + }, [initial]); return { tiers, loading, error }; } @@ -57,11 +53,6 @@ function GiftVoucherPage({ tiers: initialTiers = [] }: { tiers: GiftVoucherTier[ accept_terms: false, }); const [errors, setErrors] = React.useState>({}); - const [lookupCode, setLookupCode] = React.useState(''); - const [lookupResult, setLookupResult] = React.useState(null); - const [lookupError, setLookupError] = React.useState(null); - const [lookupLoading, setLookupLoading] = React.useState(false); - const rateLimit = useRateLimitHelper('voucher'); const selectedTierKey = form.tier_key; @@ -129,31 +120,6 @@ function GiftVoucherPage({ tiers: initialTiers = [] }: { tiers: GiftVoucherTier[ } }; - const onLookup = async () => { - if (rateLimit.isLimited(lookupCode)) { - setLookupError(t('gift.too_many_attempts')); - return; - } - - setLookupLoading(true); - setLookupError(null); - setLookupResult(null); - try { - const result = await fetchGiftVoucherByCode(lookupCode); - if (result) { - setLookupResult(result); - rateLimit.clear(lookupCode); - } else { - setLookupError(t('gift.lookup_not_found')); - rateLimit.bump(lookupCode); - } - } catch (error: any) { - setLookupError(error?.message || t('gift.lookup_not_found')); - rateLimit.bump(lookupCode); - } finally { - setLookupLoading(false); - } - }; return ( @@ -294,52 +260,6 @@ function GiftVoucherPage({ tiers: initialTiers = [] }: { tiers: GiftVoucherTier[ - - - {t('gift.lookup_title')} - {t('gift.lookup_subtitle')} - - -
- - setLookupCode(e.target.value)} - /> -
-
- -
- {lookupError &&

{lookupError}

} - {lookupResult && ( -
-

- {t('gift.lookup_result_code', { code: lookupResult.code })} -

-

- {t('gift.lookup_result_value', { - amount: lookupResult.amount.toFixed(2), - currency: lookupResult.currency, - })} -

- {lookupResult.expires_at && ( -

- {t('gift.lookup_result_expires', { - date: new Date(lookupResult.expires_at).toLocaleDateString(locale || undefined), - })} -

- )} -

- {t(`gift.lookup_status.${lookupResult.status}`, lookupResult.status)} -

-
- )} -
-
diff --git a/resources/js/pages/marketing/GiftVoucherStatus.tsx b/resources/js/pages/marketing/GiftVoucherStatus.tsx new file mode 100644 index 0000000..2b69f20 --- /dev/null +++ b/resources/js/pages/marketing/GiftVoucherStatus.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import MarketingLayout from '@/layouts/mainWebsite'; +import { useTranslation } from 'react-i18next'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { fetchGiftVoucherByCode, type GiftVoucherLookupResponse } from '@/lib/giftVouchers'; +import { useRateLimitHelper } from '@/hooks/useRateLimitHelper'; + +const GiftVoucherStatus: React.FC = () => { + const { t, i18n } = useTranslation('marketing'); + const [code, setCode] = React.useState(''); + const [result, setResult] = React.useState(null); + const [error, setError] = React.useState(null); + const [loading, setLoading] = React.useState(false); + const rateLimit = useRateLimitHelper('voucher'); + + const onLookup = async () => { + if (rateLimit.isLimited(code)) { + setError(t('gift.too_many_attempts')); + return; + } + + setLoading(true); + setError(null); + setResult(null); + try { + const data = await fetchGiftVoucherByCode(code); + if (data) { + setResult(data); + rateLimit.clear(code); + } else { + setError(t('gift.lookup_not_found')); + rateLimit.bump(code); + } + } catch (e: any) { + setError(e?.message || t('gift.lookup_not_found')); + rateLimit.bump(code); + } finally { + setLoading(false); + } + }; + + return ( + +
+
+
+

{t('gift.lookup_title')}

+

{t('gift.lookup_subtitle')}

+
+ + + {t('gift.lookup_label')} + {t('gift.lookup_subtitle')} + + +
+ + setCode(e.target.value)} + /> +
+ + {error &&

{error}

} + {result && ( +
+

{t('gift.lookup_result_code', { code: result.code })}

+

+ {t('gift.lookup_result_value', { + amount: result.amount.toFixed(2), + currency: result.currency, + })} +

+ {result.expires_at && ( +

+ {t('gift.lookup_result_expires', { + date: new Date(result.expires_at).toLocaleDateString(i18n.language), + })} +

+ )} +

+ {t(`gift.lookup_status.${result.status}`, result.status)} +

+
+ )} +
+
+
+
+
+ ); +}; + +GiftVoucherStatus.layout = (page: React.ReactNode) => page; + +export default GiftVoucherStatus; diff --git a/resources/lang/de/marketing.php b/resources/lang/de/marketing.php index 1f1f74a..e74be33 100644 --- a/resources/lang/de/marketing.php +++ b/resources/lang/de/marketing.php @@ -281,4 +281,23 @@ return [ 'legal_note' => 'Geschenkgutscheine: 14 Tage Widerrufsrecht bis zur Einlösung; siehe Widerrufsbelehrung.', 'legal_link' => 'Widerrufsbelehrung öffnen', ], + 'gift' => [ + 'lookup_title' => 'Gutscheinstatus prüfen', + 'lookup_subtitle' => 'Du hast schon einen Code? Prüfe Wert, Gültigkeit und Status.', + 'lookup_label' => 'Gutscheincode', + 'lookup_cta' => 'Code prüfen', + 'lookup_not_found' => 'Gutschein nicht gefunden oder nicht mehr gültig.', + 'lookup_result_code' => 'Code: :code', + 'lookup_result_value' => 'Wert: :amount :currency', + 'lookup_result_expires' => 'Gültig bis :date', + 'lookup_status' => [ + 'issued' => 'Status: Ausgestellt (einlösbar)', + 'redeemed' => 'Status: Eingelöst', + 'refunded' => 'Status: Erstattet', + 'expired' => 'Status: Abgelaufen', + 'reminder' => 'Erinnerung geplant', + 'expiry' => 'Ablauf-Hinweis geplant', + ], + 'too_many_attempts' => 'Zu viele Versuche. Bitte kurz warten und erneut probieren.', + ], ]; diff --git a/resources/lang/en/marketing.php b/resources/lang/en/marketing.php index 77647f9..6e0dcd5 100644 --- a/resources/lang/en/marketing.php +++ b/resources/lang/en/marketing.php @@ -281,4 +281,23 @@ return [ 'legal_note' => 'Gift vouchers: 14-day withdrawal right until redemption; see withdrawal policy.', 'legal_link' => 'Open withdrawal policy', ], + 'gift' => [ + 'lookup_title' => 'Check voucher status', + 'lookup_subtitle' => 'Already have a code? See value, validity, and status.', + 'lookup_label' => 'Voucher code', + 'lookup_cta' => 'Check code', + 'lookup_not_found' => 'Voucher not found or no longer valid.', + 'lookup_result_code' => 'Code: :code', + 'lookup_result_value' => 'Value: :amount :currency', + 'lookup_result_expires' => 'Valid until :date', + 'lookup_status' => [ + 'issued' => 'Status: Issued (ready to redeem)', + 'redeemed' => 'Status: Redeemed', + 'refunded' => 'Status: Refunded', + 'expired' => 'Status: Expired', + 'reminder' => 'Reminder scheduled', + 'expiry' => 'Expiry reminder scheduled', + ], + 'too_many_attempts' => 'Too many attempts. Please wait a moment and try again.', + ], ]; diff --git a/routes/web.php b/routes/web.php index c14197e..34733b4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -116,6 +116,10 @@ Route::prefix('{locale}') Route::get('/gift-vouchers/{voucher}/print', GiftVoucherPrintController::class) ->middleware('signed') ->name('marketing.gift-vouchers.print'); + Route::middleware('auth')->group(function () { + Route::get('/voucher-status', fn () => Inertia::render('marketing/GiftVoucherStatus')) + ->name('marketing.gift-voucher.status'); + }); Route::get('/impressum', [LegalPageController::class, 'show']) ->defaults('slug', 'impressum')