@promakeai/cli 0.8.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -8
- package/dist/index.js +196 -193
- package/dist/registry/auth-core.json +1 -1
- package/dist/registry/case-study-page.json +2 -2
- package/dist/registry/checkout-page.json +3 -3
- package/dist/registry/cookie-consent.json +1 -1
- package/dist/registry/forgot-password-page-split.json +3 -3
- package/dist/registry/forgot-password-page.json +3 -3
- package/dist/registry/header-ecommerce.json +2 -2
- package/dist/registry/order-card-compact.json +2 -2
- package/dist/registry/order-confirmation-page.json +2 -2
- package/dist/registry/post-detail-block.json +2 -2
- package/dist/registry/product-card-detailed.json +2 -2
- package/dist/registry/product-detail-section.json +2 -2
- package/dist/registry/product-quick-view.json +2 -2
- package/dist/registry/reset-password-page-split.json +1 -1
- package/dist/registry/reset-password-page.json +1 -1
- package/package.json +1 -1
- package/template/bun.lock +5 -5
- package/template/index.html +6 -2
- package/template/package.json +3 -3
- package/template/src/components/MetriaAnalytics.tsx +5 -1
- package/template/src/constants/constants.json +6 -2
- package/template/src/db/index.ts +1 -0
- package/template/src/db/provider.tsx +58 -19
- package/template/src/db/schema.json +20 -0
- package/template/src/hooks/use-page-title.ts +7 -1
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"path": "auth-core/use-auth.ts",
|
|
34
34
|
"type": "registry:hook",
|
|
35
35
|
"target": "$modules$/auth-core/use-auth.ts",
|
|
36
|
-
"content": "import { useCallback, useEffect, useRef } from \"react\";\
|
|
36
|
+
"content": "import { useCallback, useEffect, useRef } from \"react\";\nimport {\n useAuthStore,\n type User,\n type AuthTokens,\n} from \"./auth-store\";\nimport { customerClient } from \"@/modules/api\";\n\n// Refresh token 1 minute before expiry\nconst REFRESH_BUFFER_MS = 60 * 1000;\n\nexport function useAuth() {\n const {\n user,\n tokens,\n isAuthenticated,\n setAuth,\n updateTokens,\n clearAuth,\n isTokenExpired,\n getTimeUntilExpiry,\n } = useAuthStore();\n\n const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const isRefreshingRef = useRef(false);\n\n // Refresh token using the refresh token\n const refreshAccessToken = useCallback(async (): Promise<boolean> => {\n const currentTokens = useAuthStore.getState().tokens;\n\n // Don't refresh if no refresh token exists\n if (!currentTokens?.refreshToken || isRefreshingRef.current) {\n return false;\n }\n\n isRefreshingRef.current = true;\n\n try {\n // Use the typed refreshToken method\n const response = await customerClient.auth.refreshToken({\n refreshToken: currentTokens.refreshToken,\n });\n\n const { accessToken, refreshToken, expiresIn } = response;\n\n // Validate response has required data\n if (!accessToken) {\n return false;\n }\n\n const newTokens: AuthTokens = {\n accessToken,\n // Self-auth rotates refresh tokens — always use the new one\n refreshToken: refreshToken || currentTokens.refreshToken,\n idToken: currentTokens.idToken, // Preserve existing idToken\n encryptionKey: currentTokens.encryptionKey, // Preserve existing encryptionKey\n expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : undefined,\n };\n\n customerClient.setToken(accessToken);\n updateTokens(newTokens);\n\n return true;\n } catch (error) {\n // Self-auth rotates refresh tokens — if refresh fails, the old token\n // is already invalidated. Clear auth and force re-login.\n customerClient.setToken(null);\n clearAuth();\n return false;\n } finally {\n isRefreshingRef.current = false;\n }\n }, [updateTokens, clearAuth]);\n\n // Schedule automatic token refresh\n const scheduleTokenRefresh = useCallback(() => {\n // Clear any existing timeout\n if (refreshTimeoutRef.current) {\n clearTimeout(refreshTimeoutRef.current);\n refreshTimeoutRef.current = null;\n }\n\n const timeUntilExpiry = getTimeUntilExpiry();\n\n // Only schedule if we have an expiry time and a refresh token\n if (timeUntilExpiry === null || !tokens?.refreshToken) {\n return;\n }\n\n // Calculate when to refresh (REFRESH_BUFFER_MS before expiry)\n const refreshIn = Math.max(timeUntilExpiry - REFRESH_BUFFER_MS, 0);\n\n // Don't schedule if expiry is too far in the future (> 24 hours)\n if (refreshIn > 24 * 60 * 60 * 1000) {\n return;\n }\n\n refreshTimeoutRef.current = setTimeout(async () => {\n const success = await refreshAccessToken();\n if (success) {\n // Reschedule for the new token\n scheduleTokenRefresh();\n }\n }, refreshIn);\n }, [getTimeUntilExpiry, tokens?.refreshToken, refreshAccessToken]);\n\n // Sync token with API client and set up refresh on mount and token changes\n useEffect(() => {\n if (tokens?.accessToken) {\n customerClient.setToken(tokens.accessToken);\n\n // Only try to refresh if we have a refresh token AND token is expired\n if (isTokenExpired() && tokens.refreshToken) {\n refreshAccessToken().then((success) => {\n if (success) {\n scheduleTokenRefresh();\n }\n });\n } else if (tokens.refreshToken) {\n // Only schedule refresh if we have a refresh token\n scheduleTokenRefresh();\n }\n } else {\n customerClient.setToken(null);\n }\n\n // Cleanup timeout on unmount\n return () => {\n if (refreshTimeoutRef.current) {\n clearTimeout(refreshTimeoutRef.current);\n }\n };\n }, [\n tokens?.accessToken,\n tokens?.refreshToken,\n isTokenExpired,\n refreshAccessToken,\n scheduleTokenRefresh,\n ]);\n\n // Set up axios interceptor for 401 responses (token expired during request)\n useEffect(() => {\n const interceptorId = customerClient.axios.interceptors.response.use(\n (response) => response,\n async (error) => {\n const originalRequest = error.config;\n\n // Skip refresh for self-auth endpoints to prevent infinite loops\n const isAuthEndpoint = originalRequest?.url?.includes(\"/self-auth/\");\n\n // If we get a 401 and haven't retried yet, try to refresh\n if (\n error.response?.status === 401 &&\n !originalRequest._retry &&\n tokens?.refreshToken &&\n !isAuthEndpoint\n ) {\n originalRequest._retry = true;\n\n const success = await refreshAccessToken();\n if (success) {\n // Retry the original request with new token\n const newTokens = useAuthStore.getState().tokens;\n if (newTokens?.accessToken) {\n originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`;\n return customerClient.axios(originalRequest);\n }\n }\n }\n\n return Promise.reject(error);\n },\n );\n\n return () => {\n customerClient.axios.interceptors.response.eject(interceptorId);\n };\n }, [tokens?.refreshToken, refreshAccessToken]);\n\n const login = useCallback(async (username: string, password: string) => {\n const response = await customerClient.auth.login({ username, password });\n\n const newTokens: AuthTokens = {\n accessToken: response.accessToken,\n refreshToken: response.refreshToken,\n idToken: response.idToken,\n encryptionKey: response.encryptionKey,\n expiresAt: response.expiresIn\n ? Date.now() + response.expiresIn * 1000\n : undefined,\n };\n\n const newUser: User = {\n username,\n };\n\n customerClient.setToken(newTokens.accessToken);\n setAuth(newUser, newTokens);\n }, []);\n\n const register = useCallback(\n async (username: string, email: string, password: string) => {\n await customerClient.auth.register({ username, email, password });\n },\n [],\n );\n\n const me = useCallback(async () => {\n return customerClient.auth.me();\n }, []);\n\n // TODO: Backend self-auth altinda implement edilince aktif edilecek\n const confirmEmail = useCallback(async (username: string, code: string) => {\n await customerClient.auth.confirm({ username, code });\n }, []);\n\n const forgotPassword = useCallback(async (email: string) => {\n await customerClient.auth.forgotPassword({ email });\n }, []);\n\n const resetPassword = useCallback(\n async (email: string, code: string, newPassword: string) => {\n await customerClient.auth.resetPassword({ email, code, newPassword });\n },\n [],\n );\n\n // TODO: Backend self-auth altinda implement edilince aktif edilecek\n const resendCode = useCallback(async (username: string) => {\n return customerClient.auth.resendCode({ username });\n }, []);\n\n const logout = useCallback(async () => {\n // Clear any scheduled refresh\n if (refreshTimeoutRef.current) {\n clearTimeout(refreshTimeoutRef.current);\n refreshTimeoutRef.current = null;\n }\n\n // Self-auth logout requires refresh_token in body\n const currentTokens = useAuthStore.getState().tokens;\n try {\n if (currentTokens?.refreshToken) {\n await customerClient.auth.logout({\n refreshToken: currentTokens.refreshToken,\n });\n }\n } catch {\n // Proceed with client-side cleanup even if backend call fails\n }\n\n customerClient.setToken(null);\n clearAuth();\n }, [clearAuth]);\n\n return {\n user,\n token: tokens?.accessToken ?? null,\n tokens,\n isAuthenticated,\n api: customerClient,\n login,\n register,\n me,\n confirmEmail,\n forgotPassword,\n resetPassword,\n resendCode,\n logout,\n refreshAccessToken,\n };\n}\n"
|
|
37
37
|
}
|
|
38
38
|
],
|
|
39
39
|
"exports": {
|
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
"path": "case-study-page/lang/en.json",
|
|
30
30
|
"type": "registry:lang",
|
|
31
31
|
"target": "$modules$/case-study-page/lang/en.json",
|
|
32
|
-
"content": "{\r\n \"pageTitle\": \"Case Study\",\r\n \"title\": \"E-commerce Platform Redesign\",\r\n \"subtitle\": \"Let Promake tailor this case study to your actual project.\",\r\n \"sections\": {\r\n \"challenge\": {\r\n \"title\": \"The Challenge\",\r\n \"content\": \"The client needed a complete overhaul of their e-commerce platform to improve user experience, increase conversion rates, and modernize their brand presence. The existing platform was outdated, had poor mobile support, and suffered from slow load times that were affecting sales.\"\r\n },\r\n \"solution\": {\r\n \"title\": \"Our Solution\",\r\n \"content\": \"We designed and developed a modern, responsive e-commerce platform with a focus on performance and user experience. Key improvements included a streamlined checkout process, advanced product filtering, personalized recommendations, and a complete visual redesign that aligned with the client's evolved brand identity.\"\r\n },\r\n \"results\": {\r\n \"title\": \"The Results\",\r\n \"content\": \"After launch, the client saw a 45% increase in conversion rates, 60% improvement in page load times, and a 35% increase in mobile sales. Customer satisfaction scores improved by 28%, and the average order value increased by 22%.\"\r\n }\r\n },\r\n \"gallery\": \"Project Gallery\",\r\n \"projectDetails\": \"Project Details\",\r\n \"labels\": {\r\n \"client\": \"Client\",\r\n \"date\": \"Date\",\r\n \"category\": \"Category\",\r\n \"services\": \"Services\"\r\n },\r\n \"details\": {\r\n \"client\": \"Acme Corporation\",\r\n \"date\": \"January 2024\",\r\n \"category\": \"Web Design\",\r\n \"services\": [\"UX Design\", \"UI Design\", \"Development\"]\r\n },\r\n \"visitWebsite\": \"Visit Website\",\r\n \"share\": \"Share This Project\",\r\n \"navigation\": {\r\n \"prev\": \"Previous Project\",\r\n \"next\": \"Next Project\"\r\n },\r\n \"prev\": \"Prev\",\r\n \"next\": \"Next\",\r\n \"allProjects\": \"All Projects\"\r\n}\r\n"
|
|
32
|
+
"content": "{\r\n \"pageTitle\": \"Case Study\",\r\n \"title\": \"E-commerce Platform Redesign\",\r\n \"subtitle\": \"Let Promake tailor this case study to your actual project.\",\r\n \"sections\": {\r\n \"challenge\": {\r\n \"title\": \"The Challenge\",\r\n \"content\": \"The client needed a complete overhaul of their e-commerce platform to improve user experience, increase conversion rates, and modernize their brand presence. The existing platform was outdated, had poor mobile support, and suffered from slow load times that were affecting sales.\"\r\n },\r\n \"solution\": {\r\n \"title\": \"Our Solution\",\r\n \"content\": \"We designed and developed a modern, responsive e-commerce platform with a focus on performance and user experience. Key improvements included a streamlined checkout process, advanced product filtering, personalized recommendations, and a complete visual redesign that aligned with the client's evolved brand identity.\"\r\n },\r\n \"results\": {\r\n \"title\": \"The Results\",\r\n \"content\": \"After launch, the client saw a 45% increase in conversion rates, 60% improvement in page load times, and a 35% increase in mobile sales. Customer satisfaction scores improved by 28%, and the average order value increased by 22%.\"\r\n }\r\n },\r\n \"gallery\": \"Project Gallery\",\r\n \"projectDetails\": \"Project Details\",\r\n \"labels\": {\r\n \"client\": \"Client\",\r\n \"date\": \"Date\",\r\n \"category\": \"Category\",\r\n \"services\": \"Services\",\r\n \"website\": \"Website\"\r\n },\r\n \"details\": {\r\n \"client\": \"Acme Corporation\",\r\n \"date\": \"January 2024\",\r\n \"category\": \"Web Design\",\r\n \"services\": [\"UX Design\", \"UI Design\", \"Development\"]\r\n },\r\n \"visitWebsite\": \"Visit Website\",\r\n \"share\": \"Share This Project\",\r\n \"navigation\": {\r\n \"prev\": \"Previous Project\",\r\n \"next\": \"Next Project\"\r\n },\r\n \"prev\": \"Prev\",\r\n \"next\": \"Next\",\r\n \"allProjects\": \"All Projects\"\r\n}\r\n"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"path": "case-study-page/lang/tr.json",
|
|
36
36
|
"type": "registry:lang",
|
|
37
37
|
"target": "$modules$/case-study-page/lang/tr.json",
|
|
38
|
-
"content": "{\r\n \"pageTitle\": \"Vaka Çalışması\",\r\n \"title\": \"E-ticaret Platformu Yenileme\",\r\n \"subtitle\": \"Promake ile bu vaka çalışmasını gerçek projenize göre uyarlayın.\",\r\n \"sections\": {\r\n \"challenge\": {\r\n \"title\": \"Zorluk\",\r\n \"content\": \"Müşteri, kullanıcı deneyimini iyileştirmek, dönüşüm oranlarını artırmak ve marka varlığını modernize etmek için e-ticaret platformunun tamamen yenilenmesini istedi. Mevcut platform eskimişti, mobil desteği zayıftı ve satışları etkileyen yavaş yükleme süreleri yaşıyordu.\"\r\n },\r\n \"solution\": {\r\n \"title\": \"Çözümümüz\",\r\n \"content\": \"Performans ve kullanıcı deneyimine odaklanan modern, duyarlı bir e-ticaret platformu tasarladık ve geliştirdik. Temel iyileştirmeler arasında basitleştirilmiş ödeme süreci, gelişmiş ürün filtreleme, kişiselleştirilmiş öneriler ve müşterinin gelişen marka kimliğiyle uyumlu tam bir görsel yenileme yer aldı.\"\r\n },\r\n \"results\": {\r\n \"title\": \"Sonuçlar\",\r\n \"content\": \"Lansmandan sonra müşteri, dönüşüm oranlarında %45 artış, sayfa yükleme sürelerinde %60 iyileşme ve mobil satışlarda %35 artış gördü. Müşteri memnuniyeti puanları %28 iyileşti ve ortalama sipariş değeri %22 arttı.\"\r\n }\r\n },\r\n \"gallery\": \"Proje Galerisi\",\r\n \"projectDetails\": \"Proje Detayları\",\r\n \"labels\": {\r\n \"client\": \"Müşteri\",\r\n \"date\": \"Tarih\",\r\n \"category\": \"Kategori\",\r\n \"services\": \"Hizmetler\"\r\n },\r\n \"details\": {\r\n \"client\": \"Acme Corporation\",\r\n \"date\": \"Ocak 2024\",\r\n \"category\": \"Web Tasarımı\",\r\n \"services\": [\"UX Tasarımı\", \"UI Tasarımı\", \"Geliştirme\"]\r\n },\r\n \"visitWebsite\": \"Web Sitesini Ziyaret Et\",\r\n \"share\": \"Bu Projeyi Paylaş\",\r\n \"navigation\": {\r\n \"prev\": \"Önceki Proje\",\r\n \"next\": \"Sonraki Proje\"\r\n },\r\n \"prev\": \"Önceki\",\r\n \"next\": \"Sonraki\",\r\n \"allProjects\": \"Tüm Projeler\"\r\n}\r\n"
|
|
38
|
+
"content": "{\r\n \"pageTitle\": \"Vaka Çalışması\",\r\n \"title\": \"E-ticaret Platformu Yenileme\",\r\n \"subtitle\": \"Promake ile bu vaka çalışmasını gerçek projenize göre uyarlayın.\",\r\n \"sections\": {\r\n \"challenge\": {\r\n \"title\": \"Zorluk\",\r\n \"content\": \"Müşteri, kullanıcı deneyimini iyileştirmek, dönüşüm oranlarını artırmak ve marka varlığını modernize etmek için e-ticaret platformunun tamamen yenilenmesini istedi. Mevcut platform eskimişti, mobil desteği zayıftı ve satışları etkileyen yavaş yükleme süreleri yaşıyordu.\"\r\n },\r\n \"solution\": {\r\n \"title\": \"Çözümümüz\",\r\n \"content\": \"Performans ve kullanıcı deneyimine odaklanan modern, duyarlı bir e-ticaret platformu tasarladık ve geliştirdik. Temel iyileştirmeler arasında basitleştirilmiş ödeme süreci, gelişmiş ürün filtreleme, kişiselleştirilmiş öneriler ve müşterinin gelişen marka kimliğiyle uyumlu tam bir görsel yenileme yer aldı.\"\r\n },\r\n \"results\": {\r\n \"title\": \"Sonuçlar\",\r\n \"content\": \"Lansmandan sonra müşteri, dönüşüm oranlarında %45 artış, sayfa yükleme sürelerinde %60 iyileşme ve mobil satışlarda %35 artış gördü. Müşteri memnuniyeti puanları %28 iyileşti ve ortalama sipariş değeri %22 arttı.\"\r\n }\r\n },\r\n \"gallery\": \"Proje Galerisi\",\r\n \"projectDetails\": \"Proje Detayları\",\r\n \"labels\": {\r\n \"client\": \"Müşteri\",\r\n \"date\": \"Tarih\",\r\n \"category\": \"Kategori\",\r\n \"services\": \"Hizmetler\",\r\n \"website\": \"Web Sitesi\"\r\n },\r\n \"details\": {\r\n \"client\": \"Acme Corporation\",\r\n \"date\": \"Ocak 2024\",\r\n \"category\": \"Web Tasarımı\",\r\n \"services\": [\"UX Tasarımı\", \"UI Tasarımı\", \"Geliştirme\"]\r\n },\r\n \"visitWebsite\": \"Web Sitesini Ziyaret Et\",\r\n \"share\": \"Bu Projeyi Paylaş\",\r\n \"navigation\": {\r\n \"prev\": \"Önceki Proje\",\r\n \"next\": \"Sonraki Proje\"\r\n },\r\n \"prev\": \"Önceki\",\r\n \"next\": \"Sonraki\",\r\n \"allProjects\": \"Tüm Projeler\"\r\n}\r\n"
|
|
39
39
|
}
|
|
40
40
|
],
|
|
41
41
|
"exports": {
|
|
@@ -24,19 +24,19 @@
|
|
|
24
24
|
"path": "checkout-page/checkout-page.tsx",
|
|
25
25
|
"type": "registry:page",
|
|
26
26
|
"target": "$modules$/checkout-page/checkout-page.tsx",
|
|
27
|
-
"content": "import { useState, useEffect } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { ArrowLeft, CreditCard, Banknote, Truck, Check } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport { Skeleton } from \"@/components/ui/skeleton\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { toast } from \"sonner\";\r\nimport {\r\n useCart,\r\n formatPrice,\r\n type PaymentMethod,\r\n type OnlinePaymentProvider,\r\n getFilteredPaymentMethodConfigs,\r\n getOnlinePaymentProviders,\r\n ONLINE_PROVIDER_CONFIGS,\r\n} from \"@/modules/ecommerce-core\";\r\nimport { customerClient, getErrorMessage } from \"@/modules/api\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\ninterface Country {\r\n value: string;\r\n label: string;\r\n}\r\n\r\ninterface CheckoutFormData {\r\n firstName: string;\r\n lastName: string;\r\n email: string;\r\n phone: string;\r\n address: string;\r\n city: string;\r\n postalCode: string;\r\n country: string;\r\n notes: string;\r\n}\r\n\r\ninterface BankTransferInfo {\r\n bank_name: string;\r\n bank_account_name: string;\r\n iban: string;\r\n}\r\n\r\nconst DEFAULT_COUNTRIES: Country[] = [\r\n { value: \"US\", label: \"United States\" },\r\n { value: \"GB\", label: \"United Kingdom\" },\r\n { value: \"CA\", label: \"Canada\" },\r\n { value: \"AU\", label: \"Australia\" },\r\n { value: \"DE\", label: \"Germany\" },\r\n { value: \"FR\", label: \"France\" },\r\n { value: \"IT\", label: \"Italy\" },\r\n { value: \"ES\", label: \"Spain\" },\r\n { value: \"NL\", label: \"Netherlands\" },\r\n { value: \"TR\", label: \"Turkey\" },\r\n { value: \"JP\", label: \"Japan\" },\r\n];\r\n\r\nexport function CheckoutPage() {\r\n const { t } = useTranslation(\"checkout-page\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Checkout\") });\r\n const { state, clearCart } = useCart();\r\n const { items, total } = state;\r\n\r\n const currency = (constants as any).site?.currency || \"USD\";\r\n const taxRate = (constants as any).payments?.taxRate || 0;\r\n const freeShippingThreshold = (constants as any).payments?.freeShippingThreshold || 0;\r\n const shippingCost = (constants as any).shipping?.domesticShipping?.standard?.cost || 0;\r\n\r\n // Calculate shipping and tax\r\n const shipping = total >= freeShippingThreshold ? 0 : shippingCost;\r\n const tax = total * taxRate;\r\n\r\n const countries = DEFAULT_COUNTRIES;\r\n\r\n // Get available payment methods and providers from config\r\n const availablePaymentMethods = getFilteredPaymentMethodConfigs();\r\n const availableProviders = getOnlinePaymentProviders();\r\n\r\n const getProductPrice = (product: {\r\n price: number;\r\n sale_price?: number;\r\n on_sale?: boolean;\r\n }) => {\r\n return product.on_sale && product.sale_price\r\n ? product.sale_price\r\n : product.price;\r\n };\r\n\r\n const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>(\r\n availablePaymentMethods[0]?.id || \"card\"\r\n );\r\n const [selectedProvider, setSelectedProvider] = useState<OnlinePaymentProvider>(\r\n availableProviders[0] || \"stripe\"\r\n );\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [formData, setFormData] = useState<CheckoutFormData>({\r\n firstName: \"\",\r\n lastName: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n address: \"\",\r\n city: \"\",\r\n postalCode: \"\",\r\n country: \"\",\r\n notes: \"\",\r\n });\r\n const [agreedToTerms, setAgreedToTerms] = useState(false);\r\n\r\n // Bank transfer info state\r\n const [bankInfo, setBankInfo] = useState<BankTransferInfo | null>(null);\r\n const [isBankInfoLoading, setIsBankInfoLoading] = useState(false);\r\n const [bankInfoError, setBankInfoError] = useState<string | null>(null);\r\n\r\n const finalTotal = total + shipping + tax;\r\n\r\n // Fetch bank info when transfer is selected\r\n useEffect(() => {\r\n if (paymentMethod === \"transfer\") {\r\n const fetchBankInfo = async () => {\r\n setIsBankInfoLoading(true);\r\n setBankInfoError(null);\r\n try {\r\n const info = await customerClient.payment.getBankTransferInfo();\r\n setBankInfo(info);\r\n } catch (err: any) {\r\n setBankInfoError(\r\n err.message || t(\"bankInfoError\", \"Failed to load bank information\")\r\n );\r\n } finally {\r\n setIsBankInfoLoading(false);\r\n }\r\n };\r\n fetchBankInfo();\r\n }\r\n }, [paymentMethod, t]);\r\n\r\n const handleInputChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n const { name, value } = e.target;\r\n setFormData((prev) => ({ ...prev, [name]: value }));\r\n };\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n if (!agreedToTerms) {\r\n toast.error(t(\"agreeToTermsError\", \"Please agree to the terms and conditions\"));\r\n return;\r\n }\r\n\r\n setIsSubmitting(true);\r\n setError(null);\r\n\r\n try {\r\n // Determine payment type based on selection\r\n let paymentType: \"stripe\" | \"iyzico\" | \"bank_transfer\" | \"cash_on_delivery\";\r\n\r\n if (paymentMethod === \"card\") {\r\n paymentType = selectedProvider;\r\n } else if (paymentMethod === \"transfer\") {\r\n paymentType = \"bank_transfer\";\r\n } else {\r\n paymentType = \"cash_on_delivery\";\r\n }\r\n\r\n // Save checkout data to localStorage for success page\r\n const checkoutData = {\r\n items: items,\r\n total: finalTotal,\r\n customerInfo: formData,\r\n paymentMethod,\r\n paymentProvider: paymentType,\r\n };\r\n localStorage.setItem(\"pending_checkout\", JSON.stringify(checkoutData));\r\n\r\n // Build product data for checkout\r\n const productData = items.map((item) => {\r\n const price = getProductPrice(item.product);\r\n const qty = item.quantity || 1;\r\n\r\n return {\r\n quantity: qty,\r\n name: item.product.name || \"Product\",\r\n description: item.product.description || item.product.name || \"Product\",\r\n amount: Math.round(price * 100), // Convert to cents\r\n img: item.product.images?.[0] || \"/images/placeholder.png\",\r\n optionals: {\r\n productId: item.product.id,\r\n },\r\n };\r\n });\r\n\r\n // Tax amount in cents\r\n const taxAmountInCents = tax && !isNaN(tax) ? Math.round(tax * 100) : undefined;\r\n\r\n // Create checkout session\r\n const response = await customerClient.payment.createCheckout({\r\n currency: currency.toLowerCase(),\r\n taxAmount: taxAmountInCents,\r\n paymentType: paymentType,\r\n productData,\r\n contactData: {\r\n firstname: formData.firstName,\r\n lastname: formData.lastName,\r\n email: formData.email,\r\n phone: formData.phone,\r\n },\r\n shippingData: {\r\n address: formData.address,\r\n country: formData.country,\r\n city: formData.city,\r\n zip: formData.postalCode,\r\n },\r\n });\r\n\r\n // Clear cart and redirect to payment URL or confirmation page\r\n clearCart();\r\n if (response.url) {\r\n window.location.href = response.url;\r\n } else {\r\n window.location.href = `/order-confirmation?session_id=${response.sessionId}`;\r\n }\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(err, t(\"orderError\", \"Failed to place order. Please try again.\"));\r\n setError(errorMessage);\r\n toast.error(t(\"orderErrorTitle\", \"Order Failed\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n // Get icon component based on payment method\r\n const getPaymentIcon = (iconName: string) => {\r\n switch (iconName) {\r\n case \"CreditCard\":\r\n return CreditCard;\r\n case \"Banknote\":\r\n return Banknote;\r\n case \"Truck\":\r\n return Truck;\r\n default:\r\n return CreditCard;\r\n }\r\n };\r\n\r\n // Get icon color based on payment method\r\n const getIconColor = (methodId: string) => {\r\n switch (methodId) {\r\n case \"card\":\r\n return \"text-blue-600\";\r\n case \"transfer\":\r\n return \"text-primary\";\r\n case \"cash\":\r\n return \"text-green-600 dark:text-green-400\";\r\n default:\r\n return \"text-primary\";\r\n }\r\n };\r\n\r\n if (items.length === 0) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"max-w-2xl mx-auto text-center\">\r\n <h1 className=\"text-3xl font-bold mb-4\">\r\n {t(\"cartEmpty\", \"Your cart is empty\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-8\">\r\n {t(\r\n \"cartEmptyDescription\",\r\n \"Please add items to your cart before proceeding to checkout.\"\r\n )}\r\n </p>\r\n <Button asChild>\r\n <Link to=\"/products\">\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <FadeIn className=\"flex items-center gap-4 mb-8\">\r\n <Button variant=\"ghost\" size=\"icon\" asChild>\r\n <Link to=\"/cart\">\r\n <ArrowLeft className=\"h-4 w-4\" />\r\n </Link>\r\n </Button>\r\n <div>\r\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Checkout\")}</h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"completeOrder\", \"Complete your order\")}\r\n </p>\r\n </div>\r\n </FadeIn>\r\n\r\n <form onSubmit={handleSubmit}>\r\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\r\n <div className=\"lg:col-span-2 space-y-6\">\r\n {/* Contact Information */}\r\n <FadeIn delay={0.1}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>\r\n {t(\"contactInformation\", \"Contact Information\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n <FormField label={t(\"firstName\", \"First Name\")} htmlFor=\"firstName\" required>\r\n <Input\r\n id=\"firstName\"\r\n name=\"firstName\"\r\n value={formData.firstName}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n <FormField label={t(\"lastName\", \"Last Name\")} htmlFor=\"lastName\" required>\r\n <Input\r\n id=\"lastName\"\r\n name=\"lastName\"\r\n value={formData.lastName}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n </div>\r\n <FormField label={t(\"email\", \"Email Address\")} htmlFor=\"email\" required>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n <FormField label={t(\"phone\", \"Phone Number\")} htmlFor=\"phone\" required>\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n\r\n {/* Shipping Address */}\r\n <FadeIn delay={0.2}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>\r\n {t(\"shippingAddress\", \"Shipping Address\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"address\">{t(\"address\", \"Address\")} *</Label>\r\n <Textarea\r\n id=\"address\"\r\n name=\"address\"\r\n value={formData.address}\r\n onChange={handleInputChange}\r\n placeholder={t(\r\n \"addressPlaceholder\",\r\n \"Street address, apartment, suite, etc.\"\r\n )}\r\n required\r\n />\r\n </div>\r\n <FormField label={t(\"country\", \"Country\")} htmlFor=\"country\" required>\r\n <Select\r\n value={formData.country}\r\n onValueChange={(value) =>\r\n setFormData((prev) => ({ ...prev, country: value }))\r\n }\r\n required\r\n >\r\n <SelectTrigger id=\"country\">\r\n <SelectValue\r\n placeholder={t(\"selectCountry\", \"Select a country\")}\r\n />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {countries.map((country) => (\r\n <SelectItem key={country.value} value={country.value}>\r\n {country.label}\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n </FormField>\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n <FormField label={t(\"city\", \"City\")} htmlFor=\"city\" required>\r\n <Input\r\n id=\"city\"\r\n name=\"city\"\r\n value={formData.city}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n <FormField label={t(\"postalCode\", \"Postal Code\")} htmlFor=\"postalCode\" required>\r\n <Input\r\n id=\"postalCode\"\r\n name=\"postalCode\"\r\n value={formData.postalCode}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n\r\n {/* Payment Method */}\r\n <FadeIn delay={0.3}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"paymentMethod\", \"Payment Method\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <RadioGroup\r\n value={paymentMethod}\r\n onValueChange={(value) =>\r\n setPaymentMethod(value as PaymentMethod)\r\n }\r\n className=\"space-y-4\"\r\n >\r\n {availablePaymentMethods.map((method) => {\r\n const IconComponent = getPaymentIcon(method.icon);\r\n const iconColor = getIconColor(method.id);\r\n\r\n return (\r\n <div\r\n key={method.id}\r\n className=\"flex items-center space-x-2 p-4 border rounded-lg\"\r\n >\r\n <RadioGroupItem value={method.id} id={method.id} />\r\n <Label\r\n htmlFor={method.id}\r\n className=\"flex-1 cursor-pointer\"\r\n >\r\n <div className=\"flex items-center gap-3\">\r\n <IconComponent\r\n className={`h-5 w-5 ${iconColor}`}\r\n />\r\n <div>\r\n <div className=\"font-medium\">\r\n {t(method.id, method.label)}\r\n </div>\r\n <div className=\"text-sm text-muted-foreground\">\r\n {t(`${method.id}Description`, method.description)}\r\n </div>\r\n </div>\r\n </div>\r\n </Label>\r\n </div>\r\n );\r\n })}\r\n </RadioGroup>\r\n\r\n {/* Bank Transfer Details */}\r\n {paymentMethod === \"transfer\" && (\r\n <div className=\"mt-4 p-4 bg-primary/10 rounded-lg border border-primary/20\">\r\n <h4 className=\"font-medium mb-2\">\r\n {t(\"bankTransferDetailsTitle\", \"Bank Transfer Details\")}:\r\n </h4>\r\n {isBankInfoLoading ? (\r\n <div className=\"text-sm space-y-2\">\r\n <Skeleton className=\"h-4 w-full\" />\r\n <Skeleton className=\"h-4 w-3/4\" />\r\n <Skeleton className=\"h-4 w-full\" />\r\n </div>\r\n ) : bankInfoError ? (\r\n <p className=\"text-sm text-red-600\">{bankInfoError}</p>\r\n ) : bankInfo ? (\r\n <div className=\"text-sm space-y-1\">\r\n <p>\r\n <strong>{t(\"bank\", \"Bank\")}:</strong> {bankInfo.bank_name}\r\n </p>\r\n <p>\r\n <strong>{t(\"accountName\", \"Account Name\")}:</strong>{\" \"}\r\n {bankInfo.bank_account_name}\r\n </p>\r\n <p>\r\n <strong>IBAN:</strong> {bankInfo.iban}\r\n </p>\r\n </div>\r\n ) : (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"bankInfoNotAvailable\", \"Bank account information not available\")}\r\n </p>\r\n )}\r\n </div>\r\n )}\r\n\r\n {/* Card Payment - Provider Selection */}\r\n {paymentMethod === \"card\" && availableProviders.length > 1 && (\r\n <div className=\"mt-4 space-y-4\">\r\n <div className=\"p-4 bg-blue-50 dark:bg-blue-950/30 rounded-lg border border-blue-200 dark:border-blue-800\">\r\n <h4 className=\"font-medium text-blue-900 dark:text-blue-100 mb-3\">\r\n {t(\"selectPaymentProvider\", \"Select Payment Provider\")}\r\n </h4>\r\n <RadioGroup\r\n value={selectedProvider}\r\n onValueChange={(value) =>\r\n setSelectedProvider(value as OnlinePaymentProvider)\r\n }\r\n className=\"space-y-2\"\r\n >\r\n {availableProviders.map((provider) => (\r\n <div\r\n key={provider}\r\n className=\"flex items-center space-x-2 p-3 bg-background rounded border\"\r\n >\r\n <RadioGroupItem\r\n value={provider}\r\n id={`provider-${provider}`}\r\n />\r\n <Label\r\n htmlFor={`provider-${provider}`}\r\n className=\"flex-1 cursor-pointer\"\r\n >\r\n <div className=\"font-medium\">\r\n {t(`provider_${provider}_label`, ONLINE_PROVIDER_CONFIGS[provider].label)}\r\n </div>\r\n <div className=\"text-xs text-muted-foreground\">\r\n {t(`provider_${provider}_description`, ONLINE_PROVIDER_CONFIGS[provider].description)}\r\n </div>\r\n </Label>\r\n </div>\r\n ))}\r\n </RadioGroup>\r\n <p className=\"text-sm text-blue-700 dark:text-blue-300 mt-3\">\r\n {t(\r\n \"creditCardRedirectNote\",\r\n \"You will be redirected to the secure payment page to complete your purchase.\"\r\n )}\r\n </p>\r\n </div>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n\r\n {/* Order Notes */}\r\n <FadeIn delay={0.4}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>\r\n {t(\"orderNotesOptional\", \"Order Notes (Optional)\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <Textarea\r\n name=\"notes\"\r\n value={formData.notes}\r\n onChange={handleInputChange}\r\n placeholder={t(\r\n \"orderNotesPlaceholder\",\r\n \"Special instructions for your order...\"\r\n )}\r\n rows={3}\r\n />\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n </div>\r\n\r\n {/* Order Summary */}\r\n <FadeIn delay={0.2} className=\"lg:col-span-1\">\r\n <Card className=\"sticky top-24\">\r\n <CardHeader>\r\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"space-y-3\">\r\n {items.map((item) => (\r\n <div key={item.id} className=\"flex gap-3\">\r\n <img\r\n src={\r\n item.product.images?.[0] ||\r\n \"/images/placeholder.png\"\r\n }\r\n alt={item.product.name}\r\n className=\"w-12 h-12 object-cover rounded\"\r\n />\r\n <div className=\"flex-1 space-y-1\">\r\n <h4 className=\"text-sm font-medium leading-normal\">\r\n {item.product.name}\r\n </h4>\r\n <div className=\"flex justify-between text-sm\">\r\n <span className=\"text-muted-foreground\">\r\n {t(\"qty\", \"Qty\")}: {item.quantity}\r\n </span>\r\n <span>\r\n {formatPrice(\r\n getProductPrice(item.product) * item.quantity,\r\n currency\r\n )}\r\n </span>\r\n </div>\r\n </div>\r\n </div>\r\n ))}\r\n </div>\r\n\r\n <Separator />\r\n\r\n <div className=\"space-y-2\">\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"subtotal\", \"Subtotal\")}</span>\r\n <span>{formatPrice(total, currency)}</span>\r\n </div>\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"shipping\", \"Shipping\")}</span>\r\n <span>\r\n {shipping === 0\r\n ? t(\"free\", \"Free\")\r\n : formatPrice(shipping, currency)}\r\n </span>\r\n </div>\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"tax\", \"Tax\")}</span>\r\n <span>{formatPrice(tax, currency)}</span>\r\n </div>\r\n </div>\r\n\r\n <Separator />\r\n\r\n <div className=\"flex justify-between text-lg font-semibold\">\r\n <span>{t(\"total\", \"Total\")}</span>\r\n <span>{formatPrice(finalTotal, currency)}</span>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg\">\r\n <p className=\"text-red-800 dark:text-red-200 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <div className=\"flex items-center gap-2\">\r\n <Checkbox\r\n id=\"terms\"\r\n checked={agreedToTerms}\r\n onCheckedChange={(checked) =>\r\n setAgreedToTerms(checked as boolean)\r\n }\r\n />\r\n <span className=\"text-sm\">\r\n {t(\"agreeToTermsTextBefore\", \"I agree to the\")}{\" \"}\r\n <Link\r\n to=\"/terms\"\r\n className=\"text-primary hover:underline\"\r\n >\r\n {t(\"termsOfService\", \"Terms of Service\")}\r\n </Link>{\" \"}\r\n {t(\"and\", \"and\")}{\" \"}\r\n <Link\r\n to=\"/privacy\"\r\n className=\"text-primary hover:underline\"\r\n >\r\n {t(\"privacyPolicy\", \"Privacy Policy\")}\r\n </Link>\r\n </span>\r\n </div>\r\n\r\n <Button\r\n type=\"submit\"\r\n className=\"w-full\"\r\n size=\"lg\"\r\n disabled={!agreedToTerms || isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"processing\", \"Processing...\")}\r\n </>\r\n ) : (\r\n <>\r\n <Check className=\"w-4 h-4 mr-2\" />\r\n {paymentMethod === \"card\"\r\n ? t(\"proceedToPayment\", \"Proceed to Payment\")\r\n : t(\"placeOrder\", \"Place Order\")}\r\n </>\r\n )}\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n </div>\r\n </form>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default CheckoutPage;\r\n"
|
|
27
|
+
"content": "import { useState, useEffect } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { ArrowLeft, CreditCard, Banknote, Truck, Check } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport { Skeleton } from \"@/components/ui/skeleton\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { toast } from \"sonner\";\r\nimport {\r\n useCart,\r\n formatPrice,\r\n type PaymentMethod,\r\n type OnlinePaymentProvider,\r\n getFilteredPaymentMethodConfigs,\r\n getOnlinePaymentProviders,\r\n ONLINE_PROVIDER_CONFIGS,\r\n} from \"@/modules/ecommerce-core\";\r\nimport { customerClient, getErrorMessage } from \"@/modules/api\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\ninterface Country {\r\n value: string;\r\n label: string;\r\n}\r\n\r\ninterface CheckoutFormData {\r\n firstName: string;\r\n lastName: string;\r\n email: string;\r\n phone: string;\r\n address: string;\r\n city: string;\r\n postalCode: string;\r\n country: string;\r\n notes: string;\r\n}\r\n\r\ninterface BankTransferInfo {\r\n bank_name: string;\r\n bank_account_name: string;\r\n iban: string;\r\n}\r\n\r\nconst DEFAULT_COUNTRIES: Country[] = [\r\n { value: \"US\", label: \"United States\" },\r\n { value: \"GB\", label: \"United Kingdom\" },\r\n { value: \"CA\", label: \"Canada\" },\r\n { value: \"AU\", label: \"Australia\" },\r\n { value: \"DE\", label: \"Germany\" },\r\n { value: \"FR\", label: \"France\" },\r\n { value: \"IT\", label: \"Italy\" },\r\n { value: \"ES\", label: \"Spain\" },\r\n { value: \"NL\", label: \"Netherlands\" },\r\n { value: \"TR\", label: \"Turkey\" },\r\n { value: \"JP\", label: \"Japan\" },\r\n];\r\n\r\nexport function CheckoutPage() {\r\n const { t } = useTranslation(\"checkout-page\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Checkout\") });\r\n const { state, clearCart } = useCart();\r\n const { items, total } = state;\r\n\r\n const currency = (constants as any).site?.currency || \"USD\";\r\n const taxRate = (constants as any).payments?.taxRate || 0;\r\n const freeShippingThreshold = (constants as any).payments?.freeShippingThreshold || 0;\r\n const shippingCost = (constants as any).shipping?.domesticShipping?.standard?.cost || 0;\r\n\r\n // Calculate shipping and tax\r\n const shipping = total >= freeShippingThreshold ? 0 : shippingCost;\r\n const tax = total * taxRate;\r\n\r\n const countries = DEFAULT_COUNTRIES;\r\n\r\n // Get available payment methods and providers from config\r\n const availablePaymentMethods = getFilteredPaymentMethodConfigs();\r\n const availableProviders = getOnlinePaymentProviders();\r\n\r\n const getProductPrice = (product: {\r\n price: number;\r\n sale_price?: number;\r\n on_sale?: boolean;\r\n }) => {\r\n return product.on_sale && product.sale_price\r\n ? product.sale_price\r\n : product.price;\r\n };\r\n\r\n const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>(\r\n availablePaymentMethods[0]?.id || \"card\"\r\n );\r\n const [selectedProvider, setSelectedProvider] = useState<OnlinePaymentProvider>(\r\n availableProviders[0] || \"stripe\"\r\n );\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [formData, setFormData] = useState<CheckoutFormData>({\r\n firstName: \"\",\r\n lastName: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n address: \"\",\r\n city: \"\",\r\n postalCode: \"\",\r\n country: \"\",\r\n notes: \"\",\r\n });\r\n const [agreedToTerms, setAgreedToTerms] = useState(false);\r\n\r\n // Bank transfer info state\r\n const [bankInfo, setBankInfo] = useState<BankTransferInfo | null>(null);\r\n const [isBankInfoLoading, setIsBankInfoLoading] = useState(false);\r\n const [bankInfoError, setBankInfoError] = useState<string | null>(null);\r\n\r\n const finalTotal = total + shipping + tax;\r\n\r\n // Fetch bank info when transfer is selected\r\n useEffect(() => {\r\n if (paymentMethod === \"transfer\") {\r\n const fetchBankInfo = async () => {\r\n setIsBankInfoLoading(true);\r\n setBankInfoError(null);\r\n try {\r\n const info = await customerClient.payment.getBankTransferInfo();\r\n setBankInfo(info);\r\n } catch (err: any) {\r\n setBankInfoError(\r\n err.message || t(\"bankInfoError\", \"Failed to load bank information\")\r\n );\r\n } finally {\r\n setIsBankInfoLoading(false);\r\n }\r\n };\r\n fetchBankInfo();\r\n }\r\n }, [paymentMethod, t]);\r\n\r\n const handleInputChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n const { name, value } = e.target;\r\n setFormData((prev) => ({ ...prev, [name]: value }));\r\n };\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n if (!agreedToTerms) {\r\n toast.error(t(\"agreeToTermsError\", \"Please agree to the terms and conditions\"));\r\n return;\r\n }\r\n\r\n setIsSubmitting(true);\r\n setError(null);\r\n\r\n try {\r\n // Determine payment type based on selection\r\n let paymentType: \"stripe\" | \"iyzico\" | \"bank_transfer\" | \"cash_on_delivery\";\r\n\r\n if (paymentMethod === \"card\") {\r\n paymentType = selectedProvider;\r\n } else if (paymentMethod === \"transfer\") {\r\n paymentType = \"bank_transfer\";\r\n } else {\r\n paymentType = \"cash_on_delivery\";\r\n }\r\n\r\n // Save checkout data to localStorage for success page\r\n const checkoutData = {\r\n items: items,\r\n total: finalTotal,\r\n customerInfo: formData,\r\n paymentMethod,\r\n paymentProvider: paymentType,\r\n };\r\n localStorage.setItem(\"pending_checkout\", JSON.stringify(checkoutData));\r\n\r\n // Build product data for checkout\r\n const productData = items.map((item) => {\r\n const price = getProductPrice(item.product);\r\n const qty = item.quantity || 1;\r\n\r\n return {\r\n quantity: qty,\r\n name: item.product.name || \"Product\",\r\n description: item.product.description || item.product.name || \"Product\",\r\n amount: Math.round(price * 100), // Convert to cents\r\n img: item.product.images?.[0] || \"/images/placeholder.png\",\r\n optionals: {\r\n productId: item.product.id,\r\n },\r\n };\r\n });\r\n\r\n // Tax amount in cents\r\n const taxAmountInCents = tax && !isNaN(tax) ? Math.round(tax * 100) : undefined;\r\n\r\n // Create checkout session\r\n const response = await customerClient.payment.createCheckout({\r\n currency: currency.toLowerCase(),\r\n taxAmount: taxAmountInCents,\r\n paymentType: paymentType,\r\n db: \"local\",\r\n productData,\r\n contactData: {\r\n firstname: formData.firstName,\r\n lastname: formData.lastName,\r\n email: formData.email,\r\n phone: formData.phone,\r\n },\r\n shippingData: {\r\n address: formData.address,\r\n country: formData.country,\r\n city: formData.city,\r\n zip: formData.postalCode,\r\n },\r\n });\r\n\r\n // Clear cart and redirect to payment URL or confirmation page\r\n clearCart();\r\n if (response.url) {\r\n window.location.href = response.url;\r\n } else {\r\n window.location.href = `/order-confirmation?session_id=${response.sessionId}`;\r\n }\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(err, t(\"orderError\", \"Failed to place order. Please try again.\"));\r\n setError(errorMessage);\r\n toast.error(t(\"orderErrorTitle\", \"Order Failed\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n // Get icon component based on payment method\r\n const getPaymentIcon = (iconName: string) => {\r\n switch (iconName) {\r\n case \"CreditCard\":\r\n return CreditCard;\r\n case \"Banknote\":\r\n return Banknote;\r\n case \"Truck\":\r\n return Truck;\r\n default:\r\n return CreditCard;\r\n }\r\n };\r\n\r\n // Get icon color based on payment method\r\n const getIconColor = (methodId: string) => {\r\n switch (methodId) {\r\n case \"card\":\r\n return \"text-blue-600\";\r\n case \"transfer\":\r\n return \"text-primary\";\r\n case \"cash\":\r\n return \"text-green-600 dark:text-green-400\";\r\n default:\r\n return \"text-primary\";\r\n }\r\n };\r\n\r\n if (items.length === 0) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"max-w-2xl mx-auto text-center\">\r\n <h1 className=\"text-3xl font-bold mb-4\">\r\n {t(\"cartEmpty\", \"Your cart is empty\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-8\">\r\n {t(\r\n \"cartEmptyDescription\",\r\n \"Please add items to your cart before proceeding to checkout.\"\r\n )}\r\n </p>\r\n <Button asChild>\r\n <Link to=\"/products\">\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <FadeIn className=\"flex items-center gap-4 mb-8\">\r\n <Button variant=\"ghost\" size=\"icon\" asChild>\r\n <Link to=\"/cart\">\r\n <ArrowLeft className=\"h-4 w-4\" />\r\n </Link>\r\n </Button>\r\n <div>\r\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Checkout\")}</h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"completeOrder\", \"Complete your order\")}\r\n </p>\r\n </div>\r\n </FadeIn>\r\n\r\n <form onSubmit={handleSubmit}>\r\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\r\n <div className=\"lg:col-span-2 space-y-6\">\r\n {/* Contact Information */}\r\n <FadeIn delay={0.1}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>\r\n {t(\"contactInformation\", \"Contact Information\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n <FormField label={t(\"firstName\", \"First Name\")} htmlFor=\"firstName\" required>\r\n <Input\r\n id=\"firstName\"\r\n name=\"firstName\"\r\n value={formData.firstName}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n <FormField label={t(\"lastName\", \"Last Name\")} htmlFor=\"lastName\" required>\r\n <Input\r\n id=\"lastName\"\r\n name=\"lastName\"\r\n value={formData.lastName}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n </div>\r\n <FormField label={t(\"email\", \"Email Address\")} htmlFor=\"email\" required>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n <FormField label={t(\"phone\", \"Phone Number\")} htmlFor=\"phone\" required>\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n\r\n {/* Shipping Address */}\r\n <FadeIn delay={0.2}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>\r\n {t(\"shippingAddress\", \"Shipping Address\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"address\">{t(\"address\", \"Address\")} *</Label>\r\n <Textarea\r\n id=\"address\"\r\n name=\"address\"\r\n value={formData.address}\r\n onChange={handleInputChange}\r\n placeholder={t(\r\n \"addressPlaceholder\",\r\n \"Street address, apartment, suite, etc.\"\r\n )}\r\n required\r\n />\r\n </div>\r\n <FormField label={t(\"country\", \"Country\")} htmlFor=\"country\" required>\r\n <Select\r\n value={formData.country}\r\n onValueChange={(value) =>\r\n setFormData((prev) => ({ ...prev, country: value }))\r\n }\r\n required\r\n >\r\n <SelectTrigger id=\"country\">\r\n <SelectValue\r\n placeholder={t(\"selectCountry\", \"Select a country\")}\r\n />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {countries.map((country) => (\r\n <SelectItem key={country.value} value={country.value}>\r\n {country.label}\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n </FormField>\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n <FormField label={t(\"city\", \"City\")} htmlFor=\"city\" required>\r\n <Input\r\n id=\"city\"\r\n name=\"city\"\r\n value={formData.city}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n <FormField label={t(\"postalCode\", \"Postal Code\")} htmlFor=\"postalCode\" required>\r\n <Input\r\n id=\"postalCode\"\r\n name=\"postalCode\"\r\n value={formData.postalCode}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </FormField>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n\r\n {/* Payment Method */}\r\n <FadeIn delay={0.3}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"paymentMethod\", \"Payment Method\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <RadioGroup\r\n value={paymentMethod}\r\n onValueChange={(value) =>\r\n setPaymentMethod(value as PaymentMethod)\r\n }\r\n className=\"space-y-4\"\r\n >\r\n {availablePaymentMethods.map((method) => {\r\n const IconComponent = getPaymentIcon(method.icon);\r\n const iconColor = getIconColor(method.id);\r\n\r\n return (\r\n <div\r\n key={method.id}\r\n className=\"flex items-center space-x-2 p-4 border rounded-lg\"\r\n >\r\n <RadioGroupItem value={method.id} id={method.id} />\r\n <Label\r\n htmlFor={method.id}\r\n className=\"flex-1 cursor-pointer\"\r\n >\r\n <div className=\"flex items-center gap-3\">\r\n <IconComponent\r\n className={`h-5 w-5 ${iconColor}`}\r\n />\r\n <div>\r\n <div className=\"font-medium\">\r\n {t(method.id, method.label)}\r\n </div>\r\n <div className=\"text-sm text-muted-foreground\">\r\n {t(`${method.id}Description`, method.description)}\r\n </div>\r\n </div>\r\n </div>\r\n </Label>\r\n </div>\r\n );\r\n })}\r\n </RadioGroup>\r\n\r\n {/* Bank Transfer Details */}\r\n {paymentMethod === \"transfer\" && (\r\n <div className=\"mt-4 p-4 bg-primary/10 rounded-lg border border-primary/20\">\r\n <h4 className=\"font-medium mb-2\">\r\n {t(\"bankTransferDetailsTitle\", \"Bank Transfer Details\")}:\r\n </h4>\r\n {isBankInfoLoading ? (\r\n <div className=\"text-sm space-y-2\">\r\n <Skeleton className=\"h-4 w-full\" />\r\n <Skeleton className=\"h-4 w-3/4\" />\r\n <Skeleton className=\"h-4 w-full\" />\r\n </div>\r\n ) : bankInfoError ? (\r\n <p className=\"text-sm text-red-600\">{bankInfoError}</p>\r\n ) : bankInfo ? (\r\n <div className=\"text-sm space-y-1\">\r\n <p>\r\n <strong>{t(\"bank\", \"Bank\")}:</strong> {bankInfo.bank_name}\r\n </p>\r\n <p>\r\n <strong>{t(\"accountName\", \"Account Name\")}:</strong>{\" \"}\r\n {bankInfo.bank_account_name}\r\n </p>\r\n <p>\r\n <strong>IBAN:</strong> {bankInfo.iban}\r\n </p>\r\n </div>\r\n ) : (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"bankInfoNotAvailable\", \"Bank account information not available\")}\r\n </p>\r\n )}\r\n </div>\r\n )}\r\n\r\n {/* Card Payment - Provider Selection */}\r\n {paymentMethod === \"card\" && availableProviders.length > 1 && (\r\n <div className=\"mt-4 space-y-4\">\r\n <div className=\"p-4 bg-blue-50 dark:bg-blue-950/30 rounded-lg border border-blue-200 dark:border-blue-800\">\r\n <h4 className=\"font-medium text-blue-900 dark:text-blue-100 mb-3\">\r\n {t(\"selectPaymentProvider\", \"Select Payment Provider\")}\r\n </h4>\r\n <RadioGroup\r\n value={selectedProvider}\r\n onValueChange={(value) =>\r\n setSelectedProvider(value as OnlinePaymentProvider)\r\n }\r\n className=\"space-y-2\"\r\n >\r\n {availableProviders.map((provider) => (\r\n <div\r\n key={provider}\r\n className=\"flex items-center space-x-2 p-3 bg-background rounded border\"\r\n >\r\n <RadioGroupItem\r\n value={provider}\r\n id={`provider-${provider}`}\r\n />\r\n <Label\r\n htmlFor={`provider-${provider}`}\r\n className=\"flex-1 cursor-pointer\"\r\n >\r\n <div className=\"font-medium\">\r\n {t(`provider_${provider}_label`, ONLINE_PROVIDER_CONFIGS[provider].label)}\r\n </div>\r\n <div className=\"text-xs text-muted-foreground\">\r\n {t(`provider_${provider}_description`, ONLINE_PROVIDER_CONFIGS[provider].description)}\r\n </div>\r\n </Label>\r\n </div>\r\n ))}\r\n </RadioGroup>\r\n <p className=\"text-sm text-blue-700 dark:text-blue-300 mt-3\">\r\n {t(\r\n \"creditCardRedirectNote\",\r\n \"You will be redirected to the secure payment page to complete your purchase.\"\r\n )}\r\n </p>\r\n </div>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n\r\n {/* Order Notes */}\r\n <FadeIn delay={0.4}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>\r\n {t(\"orderNotesOptional\", \"Order Notes (Optional)\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <Textarea\r\n name=\"notes\"\r\n value={formData.notes}\r\n onChange={handleInputChange}\r\n placeholder={t(\r\n \"orderNotesPlaceholder\",\r\n \"Special instructions for your order...\"\r\n )}\r\n rows={3}\r\n />\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n </div>\r\n\r\n {/* Order Summary */}\r\n <FadeIn delay={0.2} className=\"lg:col-span-1\">\r\n <Card className=\"sticky top-24\">\r\n <CardHeader>\r\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"space-y-3\">\r\n {items.map((item) => (\r\n <div key={item.id} className=\"flex gap-3\">\r\n <img\r\n src={\r\n item.product.images?.[0] ||\r\n \"/images/placeholder.png\"\r\n }\r\n alt={item.product.name}\r\n className=\"w-12 h-12 object-cover rounded\"\r\n />\r\n <div className=\"flex-1 space-y-1\">\r\n <h4 className=\"text-sm font-medium leading-normal\">\r\n {item.product.name}\r\n </h4>\r\n <div className=\"flex justify-between text-sm\">\r\n <span className=\"text-muted-foreground\">\r\n {t(\"qty\", \"Qty\")}: {item.quantity}\r\n </span>\r\n <span>\r\n {formatPrice(\r\n getProductPrice(item.product) * item.quantity,\r\n currency\r\n )}\r\n </span>\r\n </div>\r\n </div>\r\n </div>\r\n ))}\r\n </div>\r\n\r\n <Separator />\r\n\r\n <div className=\"space-y-2\">\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"subtotal\", \"Subtotal\")}</span>\r\n <span>{formatPrice(total, currency)}</span>\r\n </div>\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"shipping\", \"Shipping\")}</span>\r\n <span>\r\n {shipping === 0\r\n ? t(\"free\", \"Free\")\r\n : formatPrice(shipping, currency)}\r\n </span>\r\n </div>\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"tax\", \"Tax\")}</span>\r\n <span>{formatPrice(tax, currency)}</span>\r\n </div>\r\n </div>\r\n\r\n <Separator />\r\n\r\n <div className=\"flex justify-between text-lg font-semibold\">\r\n <span>{t(\"total\", \"Total\")}</span>\r\n <span>{formatPrice(finalTotal, currency)}</span>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg\">\r\n <p className=\"text-red-800 dark:text-red-200 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <div className=\"flex items-center gap-2\">\r\n <Checkbox\r\n id=\"terms\"\r\n checked={agreedToTerms}\r\n onCheckedChange={(checked) =>\r\n setAgreedToTerms(checked as boolean)\r\n }\r\n />\r\n <span className=\"text-sm\">\r\n {t(\"agreeToTermsTextBefore\", \"I agree to the\")}{\" \"}\r\n <Link\r\n to=\"/terms\"\r\n className=\"text-primary hover:underline\"\r\n >\r\n {t(\"termsOfService\", \"Terms of Service\")}\r\n </Link>{\" \"}\r\n {t(\"and\", \"and\")}{\" \"}\r\n <Link\r\n to=\"/privacy\"\r\n className=\"text-primary hover:underline\"\r\n >\r\n {t(\"privacyPolicy\", \"Privacy Policy\")}\r\n </Link>\r\n </span>\r\n </div>\r\n\r\n <Button\r\n type=\"submit\"\r\n className=\"w-full\"\r\n size=\"lg\"\r\n disabled={!agreedToTerms || isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"processing\", \"Processing...\")}\r\n </>\r\n ) : (\r\n <>\r\n <Check className=\"w-4 h-4 mr-2\" />\r\n {paymentMethod === \"card\"\r\n ? t(\"proceedToPayment\", \"Proceed to Payment\")\r\n : t(\"placeOrder\", \"Place Order\")}\r\n </>\r\n )}\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n </div>\r\n </form>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default CheckoutPage;\r\n"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "checkout-page/lang/en.json",
|
|
31
31
|
"type": "registry:lang",
|
|
32
32
|
"target": "$modules$/checkout-page/lang/en.json",
|
|
33
|
-
"content": "{\r\n \"pageTitle\": \"Checkout\",\r\n \"title\": \"Checkout\",\r\n \"completeOrder\": \"Complete your order\",\r\n \"contactInformation\": \"Contact Information\",\r\n \"shippingAddress\": \"Shipping Address\",\r\n \"paymentMethod\": \"Payment Method\",\r\n \"orderSummary\": \"Order Summary\",\r\n \"placeOrder\": \"Place Order\",\r\n \"processing\": \"Processing...\",\r\n \"firstName\": \"First Name\",\r\n \"lastName\": \"Last Name\",\r\n \"email\": \"Email Address\",\r\n \"phone\": \"Phone Number\",\r\n \"address\": \"Address\",\r\n \"city\": \"City\",\r\n \"postalCode\": \"Postal Code\",\r\n \"country\": \"Country\",\r\n \"selectCountry\": \"Select a country\",\r\n \"qty\": \"Qty\",\r\n \"subtotal\": \"Subtotal\",\r\n \"shipping\": \"Shipping\",\r\n \"free\": \"Free\",\r\n \"tax\": \"Tax\",\r\n \"total\": \"Total\",\r\n \"and\": \"and\",\r\n \"termsOfService\": \"Terms of Service\",\r\n \"privacyPolicy\": \"Privacy Policy\",\r\n \"addressPlaceholder\": \"Street address, apartment, suite, etc.\",\r\n \"orderNotesOptional\": \"Order Notes (Optional)\",\r\n \"orderNotesPlaceholder\": \"Special instructions for your order...\",\r\n \"agreeToTermsTextBefore\": \"I agree to the\",\r\n \"agreeToTermsError\": \"Please agree to the terms and conditions\",\r\n \"card\": \"Credit/Debit Card\",\r\n \"transfer\": \"Bank Transfer\",\r\n \"cash\": \"Cash on Delivery\",\r\n \"cardDescription\": \"Pay securely with your credit or debit card\",\r\n \"transferDescription\": \"Transfer payment to our bank account\",\r\n \"cashDescription\": \"Pay when your order arrives at your doorstep\",\r\n \"proceedToPayment\": \"Proceed to Payment\",\r\n \"cartEmpty\": \"Your cart is empty\",\r\n \"cartEmptyDescription\": \"Please add items to your cart before proceeding to checkout.\",\r\n \"continueShopping\": \"Continue Shopping\"\r\n}\r\n"
|
|
33
|
+
"content": "{\r\n \"pageTitle\": \"Checkout\",\r\n \"title\": \"Checkout\",\r\n \"completeOrder\": \"Complete your order\",\r\n \"contactInformation\": \"Contact Information\",\r\n \"shippingAddress\": \"Shipping Address\",\r\n \"paymentMethod\": \"Payment Method\",\r\n \"orderSummary\": \"Order Summary\",\r\n \"placeOrder\": \"Place Order\",\r\n \"processing\": \"Processing...\",\r\n \"firstName\": \"First Name\",\r\n \"lastName\": \"Last Name\",\r\n \"email\": \"Email Address\",\r\n \"phone\": \"Phone Number\",\r\n \"address\": \"Address\",\r\n \"city\": \"City\",\r\n \"postalCode\": \"Postal Code\",\r\n \"country\": \"Country\",\r\n \"selectCountry\": \"Select a country\",\r\n \"qty\": \"Qty\",\r\n \"subtotal\": \"Subtotal\",\r\n \"shipping\": \"Shipping\",\r\n \"free\": \"Free\",\r\n \"tax\": \"Tax\",\r\n \"total\": \"Total\",\r\n \"and\": \"and\",\r\n \"termsOfService\": \"Terms of Service\",\r\n \"privacyPolicy\": \"Privacy Policy\",\r\n \"addressPlaceholder\": \"Street address, apartment, suite, etc.\",\r\n \"orderNotesOptional\": \"Order Notes (Optional)\",\r\n \"orderNotesPlaceholder\": \"Special instructions for your order...\",\r\n \"agreeToTermsTextBefore\": \"I agree to the\",\r\n \"agreeToTermsError\": \"Please agree to the terms and conditions\",\r\n \"card\": \"Credit/Debit Card\",\r\n \"transfer\": \"Bank Transfer\",\r\n \"cash\": \"Cash on Delivery\",\r\n \"cardDescription\": \"Pay securely with your credit or debit card\",\r\n \"transferDescription\": \"Transfer payment to our bank account\",\r\n \"cashDescription\": \"Pay when your order arrives at your doorstep\",\r\n \"proceedToPayment\": \"Proceed to Payment\",\r\n \"cartEmpty\": \"Your cart is empty\",\r\n \"cartEmptyDescription\": \"Please add items to your cart before proceeding to checkout.\",\r\n \"continueShopping\": \"Continue Shopping\",\r\n \"bankInfoError\": \"Failed to load bank information\",\r\n \"orderError\": \"Failed to place order. Please try again.\",\r\n \"orderErrorTitle\": \"Order Failed\",\r\n \"bankTransferDetailsTitle\": \"Bank Transfer Details\",\r\n \"bank\": \"Bank\",\r\n \"accountName\": \"Account Name\",\r\n \"bankInfoNotAvailable\": \"Bank account information not available\",\r\n \"selectPaymentProvider\": \"Select Payment Provider\",\r\n \"creditCardRedirectNote\": \"You will be redirected to the secure payment page to complete your purchase.\"\r\n}\r\n"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"path": "checkout-page/lang/tr.json",
|
|
37
37
|
"type": "registry:lang",
|
|
38
38
|
"target": "$modules$/checkout-page/lang/tr.json",
|
|
39
|
-
"content": "{\r\n \"pageTitle\": \"Ödeme\",\r\n \"title\": \"Ödeme\",\r\n \"completeOrder\": \"Siparişinizi tamamlayın\",\r\n \"contactInformation\": \"İletişim Bilgileri\",\r\n \"shippingAddress\": \"Teslimat Adresi\",\r\n \"paymentMethod\": \"Ödeme Yöntemi\",\r\n \"orderSummary\": \"Sipariş Özeti\",\r\n \"placeOrder\": \"Siparişi Tamamla\",\r\n \"processing\": \"İşleniyor...\",\r\n \"firstName\": \"Ad\",\r\n \"lastName\": \"Soyad\",\r\n \"email\": \"E-posta Adresi\",\r\n \"phone\": \"Telefon Numarası\",\r\n \"address\": \"Adres\",\r\n \"city\": \"Şehir\",\r\n \"postalCode\": \"Posta Kodu\",\r\n \"country\": \"Ülke\",\r\n \"selectCountry\": \"Ülke seçin\",\r\n \"qty\": \"Adet\",\r\n \"subtotal\": \"Ara Toplam\",\r\n \"shipping\": \"Kargo\",\r\n \"free\": \"Ücretsiz\",\r\n \"tax\": \"Vergi\",\r\n \"total\": \"Toplam\",\r\n \"and\": \"ve\",\r\n \"termsOfService\": \"Hizmet Koşulları\",\r\n \"privacyPolicy\": \"Gizlilik Politikası\",\r\n \"addressPlaceholder\": \"Sokak adresi, daire, suit, vb.\",\r\n \"orderNotesOptional\": \"Sipariş Notları (Opsiyonel)\",\r\n \"orderNotesPlaceholder\": \"Siparişiniz için özel talimatlar...\",\r\n \"agreeToTermsTextBefore\": \"Kabul ediyorum:\",\r\n \"agreeToTermsError\": \"Lütfen hizmet koşullarını kabul edin\",\r\n \"card\": \"Kredi/Banka Kartı\",\r\n \"transfer\": \"Banka Havalesi\",\r\n \"cash\": \"Kapıda Ödeme\",\r\n \"cardDescription\": \"Kredi veya banka kartınızla güvenli ödeme\",\r\n \"transferDescription\": \"Banka hesabımıza ödeme transferi\",\r\n \"cashDescription\": \"Siparişiniz kapınıza geldiğinde ödeme\",\r\n \"proceedToPayment\": \"Ödemeye Geç\",\r\n \"cartEmpty\": \"Sepetiniz boş\",\r\n \"cartEmptyDescription\": \"Ödemeye geçmeden önce lütfen sepetinize ürün ekleyin.\",\r\n \"continueShopping\": \"Alışverişe Devam Et\"\r\n}\r\n"
|
|
39
|
+
"content": "{\r\n \"pageTitle\": \"Ödeme\",\r\n \"title\": \"Ödeme\",\r\n \"completeOrder\": \"Siparişinizi tamamlayın\",\r\n \"contactInformation\": \"İletişim Bilgileri\",\r\n \"shippingAddress\": \"Teslimat Adresi\",\r\n \"paymentMethod\": \"Ödeme Yöntemi\",\r\n \"orderSummary\": \"Sipariş Özeti\",\r\n \"placeOrder\": \"Siparişi Tamamla\",\r\n \"processing\": \"İşleniyor...\",\r\n \"firstName\": \"Ad\",\r\n \"lastName\": \"Soyad\",\r\n \"email\": \"E-posta Adresi\",\r\n \"phone\": \"Telefon Numarası\",\r\n \"address\": \"Adres\",\r\n \"city\": \"Şehir\",\r\n \"postalCode\": \"Posta Kodu\",\r\n \"country\": \"Ülke\",\r\n \"selectCountry\": \"Ülke seçin\",\r\n \"qty\": \"Adet\",\r\n \"subtotal\": \"Ara Toplam\",\r\n \"shipping\": \"Kargo\",\r\n \"free\": \"Ücretsiz\",\r\n \"tax\": \"Vergi\",\r\n \"total\": \"Toplam\",\r\n \"and\": \"ve\",\r\n \"termsOfService\": \"Hizmet Koşulları\",\r\n \"privacyPolicy\": \"Gizlilik Politikası\",\r\n \"addressPlaceholder\": \"Sokak adresi, daire, suit, vb.\",\r\n \"orderNotesOptional\": \"Sipariş Notları (Opsiyonel)\",\r\n \"orderNotesPlaceholder\": \"Siparişiniz için özel talimatlar...\",\r\n \"agreeToTermsTextBefore\": \"Kabul ediyorum:\",\r\n \"agreeToTermsError\": \"Lütfen hizmet koşullarını kabul edin\",\r\n \"card\": \"Kredi/Banka Kartı\",\r\n \"transfer\": \"Banka Havalesi\",\r\n \"cash\": \"Kapıda Ödeme\",\r\n \"cardDescription\": \"Kredi veya banka kartınızla güvenli ödeme\",\r\n \"transferDescription\": \"Banka hesabımıza ödeme transferi\",\r\n \"cashDescription\": \"Siparişiniz kapınıza geldiğinde ödeme\",\r\n \"proceedToPayment\": \"Ödemeye Geç\",\r\n \"cartEmpty\": \"Sepetiniz boş\",\r\n \"cartEmptyDescription\": \"Ödemeye geçmeden önce lütfen sepetinize ürün ekleyin.\",\r\n \"continueShopping\": \"Alışverişe Devam Et\",\r\n \"bankInfoError\": \"Banka bilgileri yüklenemedi\",\r\n \"orderError\": \"Sipariş verilemedi. Lütfen tekrar deneyin.\",\r\n \"orderErrorTitle\": \"Sipariş Başarısız\",\r\n \"bankTransferDetailsTitle\": \"Banka Havale Bilgileri\",\r\n \"bank\": \"Banka\",\r\n \"accountName\": \"Hesap Adı\",\r\n \"bankInfoNotAvailable\": \"Banka hesap bilgileri mevcut değil\",\r\n \"selectPaymentProvider\": \"Ödeme Sağlayıcısı Seçin\",\r\n \"creditCardRedirectNote\": \"Satın alma işleminizi tamamlamak için güvenli ödeme sayfasına yönlendirileceksiniz.\"\r\n}\r\n"
|
|
40
40
|
}
|
|
41
41
|
],
|
|
42
42
|
"exports": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"path": "cookie-consent/lang/en.json",
|
|
26
26
|
"type": "registry:lang",
|
|
27
27
|
"target": "$modules$/cookie-consent/lang/en.json",
|
|
28
|
-
"content": "{\r\n \"title\": \"Cookie Notice\",\r\n \"description\": \"We use cookies to improve your experience.\",\r\n \"privacyPolicy\": \"Privacy\",\r\n \"cookiePolicy\": \"Cookies\",\r\n \"accept\": \"Accept\",\r\n \"decline\": \"Decline\",\r\n \"close\": \"Close\"\r\n}\r\n"
|
|
28
|
+
"content": "{\r\n \"title\": \"Cookie Notice\",\r\n \"description\": \"We use cookies to improve your experience.\",\r\n \"privacyPolicy\": \"Privacy\",\r\n \"cookiePolicy\": \"Cookies\",\r\n \"accept\": \"Accept\",\r\n \"decline\": \"Decline\",\r\n \"customize\": \"Customize\",\r\n \"close\": \"Close\"\r\n}\r\n"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "cookie-consent/lang/tr.json",
|
|
@@ -23,19 +23,19 @@
|
|
|
23
23
|
"path": "forgot-password-page-split/forgot-password-page-split.tsx",
|
|
24
24
|
"type": "registry:page",
|
|
25
25
|
"target": "$modules$/forgot-password-page-split/forgot-password-page-split.tsx",
|
|
26
|
-
"content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { Mail, ArrowLeft, CheckCircle } from \"lucide-react\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\ninterface ForgotPasswordPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function ForgotPasswordPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: ForgotPasswordPageSplitProps) {\r\n const { t } = useTranslation(\"forgot-password-page-split\");\r\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\r\n const { forgotPassword } = useAuth();\r\n\r\n const [
|
|
26
|
+
"content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { Mail, ArrowLeft, CheckCircle } from \"lucide-react\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\ninterface ForgotPasswordPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function ForgotPasswordPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: ForgotPasswordPageSplitProps) {\r\n const { t } = useTranslation(\"forgot-password-page-split\");\r\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\r\n const { forgotPassword } = useAuth();\r\n\r\n const [email, setEmail] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [isEmailSent, setIsEmailSent] = useState(false);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n setIsLoading(true);\r\n\r\n try {\r\n await forgotPassword(email);\r\n\r\n setIsEmailSent(true);\r\n toast.success(t(\"emailSent\", \"Reset link sent!\"), {\r\n description: t(\"checkInbox\", \"Please check your email inbox.\"),\r\n });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"forgotPasswordError\", \"Failed to send reset link. Please try again.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n // Success state - email sent\r\n if (isEmailSent) {\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6 text-center\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center\">\r\n <CheckCircle className=\"w-8 h-8 text-green-600 dark:text-green-400\" />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"checkYourEmail\", \"Check your email\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-2\">\r\n {t(\"emailSentTo\", \"We've sent a password reset link to\")}\r\n </p>\r\n <p className=\"text-sm font-medium mt-1\">{email}</p>\r\n </div>\r\n\r\n <div className=\"text-sm text-muted-foreground\">\r\n <p>{t(\"didntReceive\", \"Didn't receive the email?\")}</p>\r\n <Button\r\n variant=\"link\"\r\n className=\"p-0 h-auto\"\r\n onClick={() => {\r\n setIsEmailSent(false);\r\n setError(null);\r\n }}\r\n >\r\n {t(\"tryAgain\", \"Click here to try again\")}\r\n </Button>\r\n </div>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Forgot password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n }\r\n\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center\">\r\n <Mail className=\"w-6 h-6 text-primary\" />\r\n </div>\r\n </div>\r\n\r\n <div className=\"text-center\">\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Forgot Password\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"subtitle\", \"Enter your email to reset your password\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <form onSubmit={handleSubmit} className=\"grid gap-4\">\r\n <div className=\"grid gap-2\">\r\n <FormField label={t(\"email\", \"Email\")} htmlFor=\"email\" required>\r\n <Input\r\n required\r\n id=\"email\"\r\n type=\"email\"\r\n autoComplete=\"email\"\r\n placeholder={t(\"emailPlaceholder\", \"Enter your email\")}\r\n value={email}\r\n onChange={(e) => setEmail(e.target.value)}\r\n disabled={isLoading}\r\n />\r\n </FormField>\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\", \"Sending...\")}\r\n </>\r\n ) : (\r\n t(\"sendLink\", \"Send Reset Link\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Forgot password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default ForgotPasswordPageSplit;\r\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "forgot-password-page-split/lang/en.json",
|
|
30
30
|
"type": "registry:lang",
|
|
31
31
|
"target": "$modules$/forgot-password-page-split/lang/en.json",
|
|
32
|
-
"content": "{\r\n \"title\": \"Forgot Password\",\r\n \"subtitle\": \"Enter your
|
|
32
|
+
"content": "{\r\n \"title\": \"Forgot Password\",\r\n \"subtitle\": \"Enter your email to reset your password\",\r\n \"email\": \"Email\",\r\n \"emailPlaceholder\": \"you@example.com\",\r\n \"sendLink\": \"Send Reset Link\",\r\n \"sending\": \"Sending...\",\r\n \"emailSent\": \"Reset link sent!\",\r\n \"checkInbox\": \"Please check your email inbox.\",\r\n \"forgotPasswordError\": \"Failed to send reset link. Please try again.\",\r\n \"checkYourEmail\": \"Check your email\",\r\n \"emailSentTo\": \"We've sent a password reset link to\",\r\n \"didntReceive\": \"Didn't receive the email?\",\r\n \"tryAgain\": \"Click here to try again\",\r\n \"backToLogin\": \"Back to login\",\r\n \"copyright\": \"All rights reserved.\",\r\n \"imageAlt\": \"Forgot password background\"\r\n}\r\n"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"path": "forgot-password-page-split/lang/tr.json",
|
|
36
36
|
"type": "registry:lang",
|
|
37
37
|
"target": "$modules$/forgot-password-page-split/lang/tr.json",
|
|
38
|
-
"content": "{\r\n \"title\": \"Şifremi Unuttum\",\r\n \"subtitle\": \"Şifrenizi sıfırlamak için
|
|
38
|
+
"content": "{\r\n \"title\": \"Şifremi Unuttum\",\r\n \"subtitle\": \"Şifrenizi sıfırlamak için e-posta adresinizi girin\",\r\n \"email\": \"E-posta\",\r\n \"emailPlaceholder\": \"ornek@email.com\",\r\n \"sendLink\": \"Sıfırlama Linki Gönder\",\r\n \"sending\": \"Gönderiliyor...\",\r\n \"emailSent\": \"Sıfırlama linki gönderildi!\",\r\n \"checkInbox\": \"Lütfen e-posta gelen kutunuzu kontrol edin.\",\r\n \"forgotPasswordError\": \"Sıfırlama linki gönderilemedi. Lütfen tekrar deneyin.\",\r\n \"checkYourEmail\": \"E-postanızı kontrol edin\",\r\n \"emailSentTo\": \"Şifre sıfırlama linki gönderildi:\",\r\n \"didntReceive\": \"E-posta almadınız mı?\",\r\n \"tryAgain\": \"Tekrar denemek için tıklayın\",\r\n \"backToLogin\": \"Girişe dön\",\r\n \"copyright\": \"Tüm hakları saklıdır.\",\r\n \"imageAlt\": \"Şifre sıfırlama arka planı\"\r\n}\r\n"
|
|
39
39
|
}
|
|
40
40
|
],
|
|
41
41
|
"exports": {
|
|
@@ -23,19 +23,19 @@
|
|
|
23
23
|
"path": "forgot-password-page/forgot-password-page.tsx",
|
|
24
24
|
"type": "registry:page",
|
|
25
25
|
"target": "$modules$/forgot-password-page/forgot-password-page.tsx",
|
|
26
|
-
"content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardHeader,\r\n CardTitle,\r\n CardDescription,\r\n} from \"@/components/ui/card\";\r\nimport { KeyRound, ArrowLeft, CheckCircle2 } from \"lucide-react\";\r\nimport { FormField } from \"@/components/FormField\";\r\nimport { PasswordInput } from \"@/components/PasswordInput\";\r\n\r\ntype Step = \"request\" | \"reset\" | \"success\";\r\n\r\nexport function ForgotPasswordPage() {\r\n const { t } = useTranslation(\"forgot-password-page\");\r\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\r\n\r\n const { forgotPassword, resetPassword } = useAuth();\r\n\r\n const [step, setStep] = useState<Step>(\"request\");\r\n const [username, setUsername] = useState(\"\");\r\n const [code, setCode] = useState(\"\");\r\n const [newPassword, setNewPassword] = useState(\"\");\r\n const [confirmPassword, setConfirmPassword] = useState(\"\");\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const handleRequestCode = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setError(null);\r\n\r\n try {\r\n await forgotPassword(username);\r\n toast.success(t(\"codeSentTitle\", \"Code Sent!\"), {\r\n description: t(\r\n \"codeSentDesc\",\r\n \"A password reset code has been sent to your email.\",\r\n ),\r\n });\r\n setStep(\"reset\");\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"errorGeneric\", \"Failed to send reset code. Please try again.\"),\r\n );\r\n setError(errorMessage);\r\n toast.error(t(\"errorTitle\", \"Error\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleResetPassword = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n\r\n // Validate passwords match\r\n if (newPassword !== confirmPassword) {\r\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\r\n return;\r\n }\r\n\r\n setIsSubmitting(true);\r\n\r\n try {\r\n await resetPassword(username, code, newPassword);\r\n toast.success(t(\"resetSuccessTitle\", \"Password Reset!\"), {\r\n description: t(\r\n \"resetSuccessDesc\",\r\n \"Your password has been successfully reset.\",\r\n ),\r\n });\r\n setStep(\"success\");\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"errorResetGeneric\", \"Failed to reset password. Please try again.\"),\r\n );\r\n setError(errorMessage);\r\n toast.error(t(\"errorTitle\", \"Error\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n // Success step\r\n if (step === \"success\") {\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardContent className=\"pt-8 pb-8 text-center\">\r\n <CheckCircle2 className=\"w-16 h-16 text-green-500 mx-auto mb-4\" />\r\n <h1 className=\"text-2xl font-bold mb-2\">\r\n {t(\"successTitle\", \"Password Reset Successfully!\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-6\">\r\n {t(\r\n \"successDescription\",\r\n \"Your password has been changed. You can now login with your new password.\",\r\n )}\r\n </p>\r\n <Button asChild className=\"w-full\">\r\n <Link to=\"/login\">{t(\"goToLogin\", \"Go to Login\")}</Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Hero Section */}\r\n <div className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\r\n {t(\"title\", \"Forgot Password\")}\r\n </h1>\r\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\r\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\r\n {step === \"request\"\r\n ? t(\r\n \"descriptionRequest\",\r\n \"Enter your username and we'll send you a code to reset your password.\",\r\n )\r\n : t(\r\n \"descriptionReset\",\r\n \"Enter the code sent to your email and your new password.\",\r\n )}\r\n </p>\r\n </div>\r\n\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <KeyRound className=\"w-5 h-5 text-primary\" />\r\n {step === \"request\"\r\n ? t(\"cardTitleRequest\", \"Request Reset Code\")\r\n : t(\"cardTitleReset\", \"Reset Password\")}\r\n </CardTitle>\r\n <CardDescription>\r\n {step === \"request\"\r\n ? t(\"cardDescRequest\", \"Step 1 of 2: Request a reset code\")\r\n : t(\r\n \"cardDescReset\",\r\n \"Step 2 of 2: Enter code and new password\",\r\n )}\r\n </CardDescription>\r\n </CardHeader>\r\n <CardContent>\r\n {step === \"request\" ? (\r\n // Step 1: Request Code\r\n <form onSubmit={handleRequestCode} className=\"space-y-6\">\r\n <FormField label={t(\"username\", \"Username\")} htmlFor=\"username\" required>\r\n <Input\r\n id=\"username\"\r\n type=\"text\"\r\n value={username}\r\n onChange={(e) => setUsername(e.target.value)}\r\n placeholder={t(\"usernamePlaceholder\", \"Enter your username\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"username\"\r\n />\r\n </FormField>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\r\n <p className=\"text-red-800 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\", \"Sending...\")}\r\n </>\r\n ) : (\r\n t(\"sendCode\", \"Send Reset Code\")\r\n )}\r\n </Button>\r\n\r\n <div className=\"text-center\">\r\n <Link\r\n to=\"/login\"\r\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to Login\")}\r\n </Link>\r\n </div>\r\n </form>\r\n ) : (\r\n // Step 2: Reset Password\r\n <form onSubmit={handleResetPassword} className=\"space-y-6\">\r\n <div className=\"p-3 bg-muted rounded-lg text-sm\">\r\n <span className=\"text-muted-foreground\">\r\n {t(\"codeFor\", \"Reset code for:\")}{\" \"}\r\n </span>\r\n <span className=\"font-medium\">{username}</span>\r\n </div>\r\n <FormField label={t(\"code\", \"Reset Code\")} htmlFor=\"code\" required>\r\n <Input\r\n id=\"code\"\r\n type=\"text\"\r\n value={code}\r\n onChange={(e) => setCode(e.target.value)}\r\n placeholder={t(\"codePlaceholder\", \"Enter 6-digit code\")}\r\n required\r\n className=\"mt-1\"\r\n maxLength={6}\r\n />\r\n </FormField>\r\n <FormField label={t(\"newPassword\", \"New Password\")} htmlFor=\"new-password\" required>\r\n <PasswordInput\r\n id=\"new-password\"\r\n name=\"new-password\"\r\n value={newPassword}\r\n onChange={(e) => setNewPassword(e.target.value)}\r\n placeholder={t(\"newPasswordPlaceholder\", \"Enter new password\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"new-password\"\r\n />\r\n </FormField>\r\n <div>\r\n <p className=\"text-xs text-muted-foreground mt-1\">\r\n {t(\r\n \"passwordRequirements\",\r\n \"At least 8 characters, 1 letter and 1 number\",\r\n )}\r\n </p>\r\n </div>\r\n\r\n <FormField label={t(\"confirmPassword\", \"Confirm Password\")} htmlFor=\"confirm-password\" required>\r\n <PasswordInput\r\n id=\"confirm-password\"\r\n value={confirmPassword}\r\n name=\"confirm-password\"\r\n onChange={(e) => setConfirmPassword(e.target.value)}\r\n placeholder={t(\"confirmPasswordPlaceholder\", \"Confirm new password\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"new-password\"\r\n />\r\n </FormField>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\r\n <p className=\"text-red-800 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"resetting\", \"Resetting...\")}\r\n </>\r\n ) : (\r\n t(\"resetPassword\", \"Reset Password\")\r\n )}\r\n </Button>\r\n\r\n <div className=\"flex justify-between\">\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n setStep(\"request\");\r\n setCode(\"\");\r\n setNewPassword(\"\");\r\n setConfirmPassword(\"\");\r\n setError(null);\r\n }}\r\n className=\"text-sm text-muted-foreground hover:text-primary\"\r\n >\r\n {t(\"changeUsername\", \"Change username\")}\r\n </button>\r\n <button\r\n type=\"button\"\r\n onClick={() =>\r\n handleRequestCode({\r\n preventDefault: () => { },\r\n } as React.FormEvent)\r\n }\r\n className=\"text-sm text-primary hover:underline\"\r\n disabled={isSubmitting}\r\n >\r\n {t(\"resendCode\", \"Resend code\")}\r\n </button>\r\n </div>\r\n </form>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ForgotPasswordPage;\r\n"
|
|
26
|
+
"content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardHeader,\r\n CardTitle,\r\n CardDescription,\r\n} from \"@/components/ui/card\";\r\nimport { KeyRound, ArrowLeft, CheckCircle2 } from \"lucide-react\";\r\nimport { FormField } from \"@/components/FormField\";\r\nimport { PasswordInput } from \"@/components/PasswordInput\";\r\n\r\ntype Step = \"request\" | \"reset\" | \"success\";\r\n\r\nexport function ForgotPasswordPage() {\r\n const { t } = useTranslation(\"forgot-password-page\");\r\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\r\n\r\n const { forgotPassword, resetPassword } = useAuth();\r\n\r\n const [step, setStep] = useState<Step>(\"request\");\r\n const [email, setEmail] = useState(\"\");\r\n const [code, setCode] = useState(\"\");\r\n const [newPassword, setNewPassword] = useState(\"\");\r\n const [confirmPassword, setConfirmPassword] = useState(\"\");\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const handleRequestCode = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setError(null);\r\n\r\n try {\r\n await forgotPassword(email);\r\n toast.success(t(\"codeSentTitle\", \"Code Sent!\"), {\r\n description: t(\r\n \"codeSentDesc\",\r\n \"A password reset code has been sent to your email.\",\r\n ),\r\n });\r\n setStep(\"reset\");\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"errorGeneric\", \"Failed to send reset code. Please try again.\"),\r\n );\r\n setError(errorMessage);\r\n toast.error(t(\"errorTitle\", \"Error\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleResetPassword = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n\r\n // Validate passwords match\r\n if (newPassword !== confirmPassword) {\r\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\r\n return;\r\n }\r\n\r\n setIsSubmitting(true);\r\n\r\n try {\r\n await resetPassword(email, code, newPassword);\r\n toast.success(t(\"resetSuccessTitle\", \"Password Reset!\"), {\r\n description: t(\r\n \"resetSuccessDesc\",\r\n \"Your password has been successfully reset.\",\r\n ),\r\n });\r\n setStep(\"success\");\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"errorResetGeneric\", \"Failed to reset password. Please try again.\"),\r\n );\r\n setError(errorMessage);\r\n toast.error(t(\"errorTitle\", \"Error\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n // Success step\r\n if (step === \"success\") {\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardContent className=\"pt-8 pb-8 text-center\">\r\n <CheckCircle2 className=\"w-16 h-16 text-green-500 mx-auto mb-4\" />\r\n <h1 className=\"text-2xl font-bold mb-2\">\r\n {t(\"successTitle\", \"Password Reset Successfully!\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-6\">\r\n {t(\r\n \"successDescription\",\r\n \"Your password has been changed. You can now login with your new password.\",\r\n )}\r\n </p>\r\n <Button asChild className=\"w-full\">\r\n <Link to=\"/login\">{t(\"goToLogin\", \"Go to Login\")}</Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Hero Section */}\r\n <div className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\r\n {t(\"title\", \"Forgot Password\")}\r\n </h1>\r\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\r\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\r\n {step === \"request\"\r\n ? t(\r\n \"descriptionRequest\",\r\n \"Enter your email and we'll send you a code to reset your password.\",\r\n )\r\n : t(\r\n \"descriptionReset\",\r\n \"Enter the code sent to your email and your new password.\",\r\n )}\r\n </p>\r\n </div>\r\n\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <KeyRound className=\"w-5 h-5 text-primary\" />\r\n {step === \"request\"\r\n ? t(\"cardTitleRequest\", \"Request Reset Code\")\r\n : t(\"cardTitleReset\", \"Reset Password\")}\r\n </CardTitle>\r\n <CardDescription>\r\n {step === \"request\"\r\n ? t(\"cardDescRequest\", \"Step 1 of 2: Request a reset code\")\r\n : t(\r\n \"cardDescReset\",\r\n \"Step 2 of 2: Enter code and new password\",\r\n )}\r\n </CardDescription>\r\n </CardHeader>\r\n <CardContent>\r\n {step === \"request\" ? (\r\n // Step 1: Request Code\r\n <form onSubmit={handleRequestCode} className=\"space-y-6\">\r\n <FormField label={t(\"email\", \"Email\")} htmlFor=\"email\" required>\r\n <Input\r\n id=\"email\"\r\n type=\"email\"\r\n value={email}\r\n onChange={(e) => setEmail(e.target.value)}\r\n placeholder={t(\"emailPlaceholder\", \"Enter your email\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"email\"\r\n />\r\n </FormField>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\r\n <p className=\"text-red-800 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\", \"Sending...\")}\r\n </>\r\n ) : (\r\n t(\"sendCode\", \"Send Reset Code\")\r\n )}\r\n </Button>\r\n\r\n <div className=\"text-center\">\r\n <Link\r\n to=\"/login\"\r\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to Login\")}\r\n </Link>\r\n </div>\r\n </form>\r\n ) : (\r\n // Step 2: Reset Password\r\n <form onSubmit={handleResetPassword} className=\"space-y-6\">\r\n <div className=\"p-3 bg-muted rounded-lg text-sm\">\r\n <span className=\"text-muted-foreground\">\r\n {t(\"codeFor\", \"Reset code for:\")}{\" \"}\r\n </span>\r\n <span className=\"font-medium\">{email}</span>\r\n </div>\r\n <FormField label={t(\"code\", \"Reset Code\")} htmlFor=\"code\" required>\r\n <Input\r\n id=\"code\"\r\n type=\"text\"\r\n value={code}\r\n onChange={(e) => setCode(e.target.value)}\r\n placeholder={t(\"codePlaceholder\", \"Enter 6-digit code\")}\r\n required\r\n className=\"mt-1\"\r\n maxLength={6}\r\n />\r\n </FormField>\r\n <FormField label={t(\"newPassword\", \"New Password\")} htmlFor=\"new-password\" required>\r\n <PasswordInput\r\n id=\"new-password\"\r\n name=\"new-password\"\r\n value={newPassword}\r\n onChange={(e) => setNewPassword(e.target.value)}\r\n placeholder={t(\"newPasswordPlaceholder\", \"Enter new password\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"new-password\"\r\n />\r\n </FormField>\r\n <div>\r\n <p className=\"text-xs text-muted-foreground mt-1\">\r\n {t(\r\n \"passwordRequirements\",\r\n \"At least 8 characters, 1 letter and 1 number\",\r\n )}\r\n </p>\r\n </div>\r\n\r\n <FormField label={t(\"confirmPassword\", \"Confirm Password\")} htmlFor=\"confirm-password\" required>\r\n <PasswordInput\r\n id=\"confirm-password\"\r\n value={confirmPassword}\r\n name=\"confirm-password\"\r\n onChange={(e) => setConfirmPassword(e.target.value)}\r\n placeholder={t(\"confirmPasswordPlaceholder\", \"Confirm new password\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"new-password\"\r\n />\r\n </FormField>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\r\n <p className=\"text-red-800 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"resetting\", \"Resetting...\")}\r\n </>\r\n ) : (\r\n t(\"resetPassword\", \"Reset Password\")\r\n )}\r\n </Button>\r\n\r\n <div className=\"flex justify-between\">\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n setStep(\"request\");\r\n setCode(\"\");\r\n setNewPassword(\"\");\r\n setConfirmPassword(\"\");\r\n setError(null);\r\n }}\r\n className=\"text-sm text-muted-foreground hover:text-primary\"\r\n >\r\n {t(\"changeEmail\", \"Change email\")}\r\n </button>\r\n <button\r\n type=\"button\"\r\n onClick={() =>\r\n handleRequestCode({\r\n preventDefault: () => { },\r\n } as React.FormEvent)\r\n }\r\n className=\"text-sm text-primary hover:underline\"\r\n disabled={isSubmitting}\r\n >\r\n {t(\"resendCode\", \"Resend code\")}\r\n </button>\r\n </div>\r\n </form>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ForgotPasswordPage;\r\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "forgot-password-page/lang/en.json",
|
|
30
30
|
"type": "registry:lang",
|
|
31
31
|
"target": "$modules$/forgot-password-page/lang/en.json",
|
|
32
|
-
"content": "{\r\n \"title\": \"Forgot Password\",\r\n \"descriptionRequest\": \"Enter your
|
|
32
|
+
"content": "{\r\n \"title\": \"Forgot Password\",\r\n \"descriptionRequest\": \"Enter your email and we'll send you a code to reset your password.\",\r\n \"descriptionReset\": \"Enter the code sent to your email and your new password.\",\r\n \"cardTitleRequest\": \"Request Reset Code\",\r\n \"cardTitleReset\": \"Reset Password\",\r\n \"cardDescRequest\": \"Step 1 of 2: Request a reset code\",\r\n \"cardDescReset\": \"Step 2 of 2: Enter code and new password\",\r\n \"email\": \"Email\",\r\n \"emailPlaceholder\": \"Enter your email\",\r\n \"code\": \"Reset Code\",\r\n \"codePlaceholder\": \"Enter 6-digit code\",\r\n \"codeFor\": \"Reset code for:\",\r\n \"newPassword\": \"New Password\",\r\n \"newPasswordPlaceholder\": \"Enter new password\",\r\n \"confirmPassword\": \"Confirm Password\",\r\n \"confirmPasswordPlaceholder\": \"Confirm new password\",\r\n \"passwordRequirements\": \"At least 8 characters, 1 letter and 1 number\",\r\n \"passwordMismatch\": \"Passwords do not match\",\r\n \"sendCode\": \"Send Reset Code\",\r\n \"sending\": \"Sending...\",\r\n \"resetPassword\": \"Reset Password\",\r\n \"resetting\": \"Resetting...\",\r\n \"backToLogin\": \"Back to Login\",\r\n \"changeEmail\": \"Change email\",\r\n \"resendCode\": \"Resend code\",\r\n \"codeSentTitle\": \"Code Sent!\",\r\n \"codeSentDesc\": \"A password reset code has been sent to your email.\",\r\n \"errorTitle\": \"Error\",\r\n \"errorGeneric\": \"Failed to send reset code. Please try again.\",\r\n \"errorResetGeneric\": \"Failed to reset password. Please try again.\",\r\n \"resetSuccessTitle\": \"Password Reset!\",\r\n \"resetSuccessDesc\": \"Your password has been successfully reset.\",\r\n \"successTitle\": \"Password Reset Successfully!\",\r\n \"successDescription\": \"Your password has been changed. You can now login with your new password.\",\r\n \"goToLogin\": \"Go to Login\"\r\n}\r\n"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"path": "forgot-password-page/lang/tr.json",
|
|
36
36
|
"type": "registry:lang",
|
|
37
37
|
"target": "$modules$/forgot-password-page/lang/tr.json",
|
|
38
|
-
"content": "{\r\n \"title\": \"Şifremi Unuttum\",\r\n \"descriptionRequest\": \"
|
|
38
|
+
"content": "{\r\n \"title\": \"Şifremi Unuttum\",\r\n \"descriptionRequest\": \"E-posta adresinizi girin, size şifre sıfırlama kodu göndereceğiz.\",\r\n \"descriptionReset\": \"E-postanıza gönderilen kodu ve yeni şifrenizi girin.\",\r\n \"cardTitleRequest\": \"Sıfırlama Kodu İste\",\r\n \"cardTitleReset\": \"Şifre Sıfırla\",\r\n \"cardDescRequest\": \"Adım 1/2: Sıfırlama kodu isteyin\",\r\n \"cardDescReset\": \"Adım 2/2: Kodu ve yeni şifreyi girin\",\r\n \"email\": \"E-posta\",\r\n \"emailPlaceholder\": \"E-posta adresinizi girin\",\r\n \"code\": \"Sıfırlama Kodu\",\r\n \"codePlaceholder\": \"6 haneli kodu girin\",\r\n \"codeFor\": \"Sıfırlama kodu:\",\r\n \"newPassword\": \"Yeni Şifre\",\r\n \"newPasswordPlaceholder\": \"Yeni şifre girin\",\r\n \"confirmPassword\": \"Şifre Onayı\",\r\n \"confirmPasswordPlaceholder\": \"Yeni şifreyi onaylayın\",\r\n \"passwordRequirements\": \"En az 8 karakter, 1 harf ve 1 rakam\",\r\n \"passwordMismatch\": \"Şifreler eşleşmiyor\",\r\n \"sendCode\": \"Sıfırlama Kodu Gönder\",\r\n \"sending\": \"Gönderiliyor...\",\r\n \"resetPassword\": \"Şifreyi Sıfırla\",\r\n \"resetting\": \"Sıfırlanıyor...\",\r\n \"backToLogin\": \"Girişe Dön\",\r\n \"changeEmail\": \"E-posta adresini değiştir\",\r\n \"resendCode\": \"Kodu tekrar gönder\",\r\n \"codeSentTitle\": \"Kod Gönderildi!\",\r\n \"codeSentDesc\": \"E-postanıza şifre sıfırlama kodu gönderildi.\",\r\n \"errorTitle\": \"Hata\",\r\n \"errorGeneric\": \"Sıfırlama kodu gönderilemedi. Lütfen tekrar deneyin.\",\r\n \"errorResetGeneric\": \"Şifre sıfırlanamadı. Lütfen tekrar deneyin.\",\r\n \"resetSuccessTitle\": \"Şifre Sıfırlandı!\",\r\n \"resetSuccessDesc\": \"Şifreniz başarıyla sıfırlandı.\",\r\n \"successTitle\": \"Şifre Başarıyla Sıfırlandı!\",\r\n \"successDescription\": \"Şifreniz değiştirildi. Artık yeni şifrenizle giriş yapabilirsiniz.\",\r\n \"goToLogin\": \"Girişe Git\"\r\n}\r\n"
|
|
39
39
|
}
|
|
40
40
|
],
|
|
41
41
|
"exports": {
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"path": "header-ecommerce/lang/en.json",
|
|
27
27
|
"type": "registry:lang",
|
|
28
28
|
"target": "$modules$/header-ecommerce/lang/en.json",
|
|
29
|
-
"content": "{\r\n \"menu\": \"Menu\",\r\n \"home\": \"Home\",\r\n \"products\": \"Products\",\r\n \"about\": \"About\",\r\n \"contact\": \"Contact\",\r\n \"cart\": \"Cart\",\r\n \"favorites\": \"Favorites\",\r\n \"searchProducts\": \"Search Products\",\r\n \"searchPlaceholder\": \"Search for products...\",\r\n \"searchButton\": \"Search\",\r\n \"result\": \"result\",\r\n \"results\": \"results\",\r\n \"noResults\": \"No products found\",\r\n \"tryDifferent\": \"Try different keywords\",\r\n \"viewAllResults\": \"View all results\"\r\n}\r\n"
|
|
29
|
+
"content": "{\r\n \"menu\": \"Menu\",\r\n \"home\": \"Home\",\r\n \"products\": \"Products\",\r\n \"about\": \"About\",\r\n \"contact\": \"Contact\",\r\n \"cart\": \"Cart\",\r\n \"favorites\": \"Favorites\",\r\n \"searchProducts\": \"Search Products\",\r\n \"searchPlaceholder\": \"Search for products...\",\r\n \"searchButton\": \"Search\",\r\n \"result\": \"result\",\r\n \"results\": \"results\",\r\n \"noResults\": \"No products found\",\r\n \"tryDifferent\": \"Try different keywords\",\r\n \"viewAllResults\": \"View all results\",\r\n \"logoutToastTitle\": \"Goodbye!\",\r\n \"logoutToastDesc\": \"You have been logged out successfully.\",\r\n \"tryDifferentKeywords\": \"Try different keywords\",\r\n \"cancel\": \"Cancel\",\r\n \"myOrders\": \"My Orders\",\r\n \"logout\": \"Logout\",\r\n \"login\": \"Login\"\r\n}\r\n"
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
"path": "header-ecommerce/lang/tr.json",
|
|
33
33
|
"type": "registry:lang",
|
|
34
34
|
"target": "$modules$/header-ecommerce/lang/tr.json",
|
|
35
|
-
"content": "{\r\n \"menu\": \"Menü\",\r\n \"home\": \"Ana Sayfa\",\r\n \"products\": \"Ürünler\",\r\n \"about\": \"Hakkımızda\",\r\n \"contact\": \"İletişim\",\r\n \"cart\": \"Sepet\",\r\n \"favorites\": \"Favoriler\",\r\n \"searchProducts\": \"Ürün Ara\",\r\n \"searchPlaceholder\": \"Ürün ara...\",\r\n \"searchButton\": \"Ara\",\r\n \"result\": \"sonuç\",\r\n \"results\": \"sonuç\",\r\n \"noResults\": \"Ürün bulunamadı\",\r\n \"tryDifferent\": \"Farklı anahtar kelimeler deneyin\",\r\n \"viewAllResults\": \"Tüm sonuçları gör\"\r\n}\r\n"
|
|
35
|
+
"content": "{\r\n \"menu\": \"Menü\",\r\n \"home\": \"Ana Sayfa\",\r\n \"products\": \"Ürünler\",\r\n \"about\": \"Hakkımızda\",\r\n \"contact\": \"İletişim\",\r\n \"cart\": \"Sepet\",\r\n \"favorites\": \"Favoriler\",\r\n \"searchProducts\": \"Ürün Ara\",\r\n \"searchPlaceholder\": \"Ürün ara...\",\r\n \"searchButton\": \"Ara\",\r\n \"result\": \"sonuç\",\r\n \"results\": \"sonuç\",\r\n \"noResults\": \"Ürün bulunamadı\",\r\n \"tryDifferent\": \"Farklı anahtar kelimeler deneyin\",\r\n \"viewAllResults\": \"Tüm sonuçları gör\",\r\n \"logoutToastTitle\": \"Hoşça kal!\",\r\n \"logoutToastDesc\": \"Başarıyla çıkış yaptınız.\",\r\n \"tryDifferentKeywords\": \"Farklı anahtar kelimeler deneyin\",\r\n \"cancel\": \"İptal\",\r\n \"myOrders\": \"Siparişlerim\",\r\n \"logout\": \"Çıkış Yap\",\r\n \"login\": \"Giriş Yap\"\r\n}\r\n"
|
|
36
36
|
}
|
|
37
37
|
],
|
|
38
38
|
"exports": {
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"path": "order-card-compact/lang/en.json",
|
|
25
25
|
"type": "registry:lang",
|
|
26
26
|
"target": "$modules$/order-card-compact/lang/en.json",
|
|
27
|
-
"content": "{\r\n \"orderNumber\": \"Order\",\r\n \"total\": \"Total\",\r\n \"items\": \"Items\",\r\n \"viewDetails\": \"View Details\"\r\n}\r\n"
|
|
27
|
+
"content": "{\r\n \"orderNumber\": \"Order\",\r\n \"total\": \"Total\",\r\n \"items\": \"Items\",\r\n \"viewDetails\": \"View Details\",\r\n \"item\": \"Item\",\r\n \"qty\": \"Qty\"\r\n}\r\n"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "order-card-compact/lang/tr.json",
|
|
31
31
|
"type": "registry:lang",
|
|
32
32
|
"target": "$modules$/order-card-compact/lang/tr.json",
|
|
33
|
-
"content": "{\r\n \"orderNumber\": \"Sipariş\",\r\n \"total\": \"Toplam\",\r\n \"items\": \"Ürünler\",\r\n \"viewDetails\": \"Detayları Gör\"\r\n}\r\n"
|
|
33
|
+
"content": "{\r\n \"orderNumber\": \"Sipariş\",\r\n \"total\": \"Toplam\",\r\n \"items\": \"Ürünler\",\r\n \"viewDetails\": \"Detayları Gör\",\r\n \"item\": \"Ürün\",\r\n \"qty\": \"Adet\"\r\n}\r\n"
|
|
34
34
|
}
|
|
35
35
|
],
|
|
36
36
|
"exports": {
|