@promakeai/cli 0.5.4 → 0.5.6
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/dist/index.js +249 -249
- package/dist/registry/cart-drawer.json +1 -1
- package/dist/registry/cart-page.json +1 -1
- package/dist/registry/checkout-page.json +1 -1
- package/dist/registry/contact-page-centered.json +1 -1
- package/dist/registry/contact-page-map-overlay.json +1 -1
- package/dist/registry/contact-page-map-split.json +1 -1
- package/dist/registry/contact-page-split.json +1 -1
- package/dist/registry/contact-page.json +1 -1
- package/dist/registry/forgot-password-page-split.json +1 -1
- package/dist/registry/forgot-password-page.json +1 -1
- package/dist/registry/login-page-split.json +1 -1
- package/dist/registry/login-page.json +1 -1
- package/dist/registry/newsletter-section.json +1 -1
- package/dist/registry/register-page-split.json +1 -1
- package/dist/registry/register-page.json +1 -1
- package/dist/registry/reset-password-page-split.json +1 -1
- package/package.json +1 -1
- package/template/eslint.config.js +41 -41
- package/template/src/components/FormField.tsx +11 -5
- package/template/src/components/PasswordInput.tsx +61 -0
- package/template/src/lang/index.ts +90 -90
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"path": "cart-drawer/cart-drawer.tsx",
|
|
21
21
|
"type": "registry:component",
|
|
22
22
|
"target": "$modules$/cart-drawer/cart-drawer.tsx",
|
|
23
|
-
"content": "import { useMemo } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { Minus, Plus } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Sheet,\r\n SheetContent,\r\n SheetHeader,\r\n SheetTitle,\r\n} from \"@/components/ui/sheet\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\r\nimport type { Category } from \"@/modules/ecommerce-core/types\";\r\nimport { useDbList } from \"@/db\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\ninterface CartDrawerProps {\r\n checkoutHref?: string;\r\n className?: string;\r\n showTrigger?: boolean;\r\n}\r\n\r\nexport function CartDrawer({\r\n checkoutHref = \"/checkout\",\r\n className,\r\n showTrigger = true,\r\n}: CartDrawerProps) {\r\n const { t } = useTranslation(\"cart-drawer\");\r\n const {\r\n state,\r\n removeItem,\r\n updateQuantity,\r\n isDrawerOpen,\r\n setDrawerOpen,\r\n } = useCart();\r\n const { items, total } = state;\r\n const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\r\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\r\n const currency = (constants.site as any).currency || \"USD\";\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 handleQuantityChange = (id: string | number, newQuantity: number) => {\r\n if (newQuantity <= 0) {\r\n removeItem(id);\r\n } else {\r\n updateQuantity(id, newQuantity);\r\n }\r\n };\r\n\r\n return (\r\n <Sheet open={isDrawerOpen} onOpenChange={setDrawerOpen}>\r\n <SheetContent className=\"w-full sm:max-w-md flex flex-col px-6 pb-8\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"title\", \"Shopping cart\")}</SheetTitle>\r\n </SheetHeader>\r\n\r\n <div className=\"flex-1 overflow-y-auto mt-8\">\r\n {items.length === 0 ? (\r\n <p className=\"text-center text-muted-foreground py-8\">\r\n {t(\"empty\", \"Your cart is empty\")}\r\n </p>\r\n ) : (\r\n <ul className=\"-my-6 divide-y divide-border\">\r\n {items.map((item) => (\r\n <li key={item.id} className=\"flex py-6\">\r\n <div className=\"size-24 shrink-0 overflow-hidden rounded-md border border-border\">\r\n <img\r\n alt={item.product.name}\r\n src={item.product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"}\r\n className=\"size-full object-cover\"\r\n />\r\n </div>\r\n\r\n <div className=\"ml-4 flex flex-1 flex-col\">\r\n <div>\r\n <div className=\"flex justify-between text-base font-medium\">\r\n <h3>\r\n <Link\r\n to={`/products/${item.product.slug}`}\r\n onClick={() => setDrawerOpen(false)}\r\n >\r\n {item.product.name}\r\n </Link>\r\n </h3>\r\n <p className=\"ml-4\">\r\n {formatPrice(getProductPrice(item.product), currency)}\r\n </p>\r\n </div>\r\n {categoryMap.get(item.product.categories?.[0] as number)?.name && (\r\n <p className=\"mt-1 text-sm text-muted-foreground\">\r\n {categoryMap.get(item.product.categories?.[0] as number)?.name}\r\n </p>\r\n )}\r\n </div>\r\n <div className=\"flex flex-1 items-end justify-between text-sm\">\r\n <div className=\"flex items-center gap-1\">\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-6 w-6\"\r\n onClick={() =>\r\n handleQuantityChange(item.id, item.quantity - 1)\r\n }\r\n >\r\n <Minus className=\"h-3 w-3\" />\r\n </Button>\r\n <span className=\"w-8 text-center text-sm\">\r\n {item.quantity}\r\n </span>\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-6 w-6\"\r\n onClick={() =>\r\n handleQuantityChange(item.id, item.quantity + 1)\r\n }\r\n >\r\n <Plus className=\"h-3 w-3\" />\r\n </Button>\r\n </div>\r\n\r\n <button\r\n type=\"button\"\r\n onClick={() => removeItem(item.id)}\r\n className=\"font-medium text-primary hover:text-primary/80\"\r\n >\r\n {t(\"remove\", \"Remove\")}\r\n </button>\r\n </div>\r\n </div>\r\n </li>\r\n ))}\r\n </ul>\r\n )}\r\n </div>\r\n\r\n <div className=\"border-t border-border pt-6 mt-6\">\r\n <div className=\"flex justify-between text-base font-medium\">\r\n <p>{t(\"subtotal\", \"Subtotal\")}</p>\r\n <p>{formatPrice(total, currency)}</p>\r\n </div>\r\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\r\n {t(\"shippingNote\", \"Shipping and taxes calculated at checkout.\")}\r\n </p>\r\n <div className=\"mt-6\">\r\n <Button asChild className=\"w-full\" disabled={items.length === 0}>\r\n <Link to={checkoutHref} onClick={() => setDrawerOpen(false)}>\r\n {t(\"checkout\", \"Checkout\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n );\r\n}\r\n"
|
|
23
|
+
"content": "import { useMemo } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { Minus, Plus } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Sheet,\r\n SheetContent,\r\n SheetHeader,\r\n SheetTitle,\r\n} from \"@/components/ui/sheet\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\r\nimport type { Category } from \"@/modules/ecommerce-core/types\";\r\nimport { useDbList } from \"@/db\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\ninterface CartDrawerProps {\r\n checkoutHref?: string;\r\n className?: string;\r\n showTrigger?: boolean;\r\n}\r\n\r\nexport function CartDrawer({\r\n checkoutHref = \"/checkout\",\r\n className,\r\n showTrigger = true,\r\n}: CartDrawerProps) {\r\n const { t } = useTranslation(\"cart-drawer\");\r\n const {\r\n state,\r\n removeItem,\r\n updateQuantity,\r\n isDrawerOpen,\r\n setDrawerOpen,\r\n } = useCart();\r\n const { items, total } = state;\r\n const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\r\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\r\n const currency = (constants.site as any).currency || \"USD\";\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 handleQuantityChange = (id: string | number, newQuantity: number) => {\r\n if (newQuantity <= 0) {\r\n removeItem(id);\r\n } else {\r\n updateQuantity(id, newQuantity);\r\n }\r\n };\r\n\r\n return (\r\n <Sheet open={isDrawerOpen} onOpenChange={setDrawerOpen}>\r\n <SheetContent className=\"w-full sm:max-w-md flex flex-col px-6 pb-8\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"title\", \"Shopping cart\")}</SheetTitle>\r\n </SheetHeader>\r\n\r\n <div className=\"flex-1 overflow-y-auto mt-8\">\r\n {items.length === 0 ? (\r\n <p className=\"text-center text-muted-foreground py-8\">\r\n {t(\"empty\", \"Your cart is empty\")}\r\n </p>\r\n ) : (\r\n <ul className=\"-my-6 divide-y divide-border\">\r\n {items.map((item) => (\r\n <li key={item.id} className=\"flex py-6\">\r\n <div className=\"size-24 shrink-0 overflow-hidden rounded-md border border-border\">\r\n <img\r\n alt={item.product.name}\r\n src={item.product.images?.length ? item.product.images?.[0] : \"/images/placeholder.png\"}\r\n className=\"size-full object-cover\"\r\n />\r\n </div>\r\n\r\n <div className=\"ml-4 flex flex-1 flex-col\">\r\n <div>\r\n <div className=\"flex justify-between text-base font-medium\">\r\n <h3>\r\n <Link\r\n to={`/products/${item.product.slug}`}\r\n onClick={() => setDrawerOpen(false)}\r\n >\r\n {item.product.name}\r\n </Link>\r\n </h3>\r\n <p className=\"ml-4\">\r\n {formatPrice(getProductPrice(item.product), currency)}\r\n </p>\r\n </div>\r\n {categoryMap.get(item.product.categories?.[0] as number)?.name && (\r\n <p className=\"mt-1 text-sm text-muted-foreground\">\r\n {categoryMap.get(item.product.categories?.[0] as number)?.name}\r\n </p>\r\n )}\r\n </div>\r\n <div className=\"flex flex-1 items-end justify-between text-sm\">\r\n <div className=\"flex items-center gap-1\">\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-6 w-6\"\r\n onClick={() =>\r\n handleQuantityChange(item.id, item.quantity - 1)\r\n }\r\n >\r\n <Minus className=\"h-3 w-3\" />\r\n </Button>\r\n <span className=\"w-8 text-center text-sm\">\r\n {item.quantity}\r\n </span>\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-6 w-6\"\r\n onClick={() =>\r\n handleQuantityChange(item.id, item.quantity + 1)\r\n }\r\n >\r\n <Plus className=\"h-3 w-3\" />\r\n </Button>\r\n </div>\r\n\r\n <button\r\n type=\"button\"\r\n onClick={() => removeItem(item.id)}\r\n className=\"font-medium text-primary hover:text-primary/80\"\r\n >\r\n {t(\"remove\", \"Remove\")}\r\n </button>\r\n </div>\r\n </div>\r\n </li>\r\n ))}\r\n </ul>\r\n )}\r\n </div>\r\n\r\n <div className=\"border-t border-border pt-6 mt-6\">\r\n <div className=\"flex justify-between text-base font-medium\">\r\n <p>{t(\"subtotal\", \"Subtotal\")}</p>\r\n <p>{formatPrice(total, currency)}</p>\r\n </div>\r\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\r\n {t(\"shippingNote\", \"Shipping and taxes calculated at checkout.\")}\r\n </p>\r\n <div className=\"mt-6\">\r\n <Button asChild className=\"w-full\" disabled={items.length === 0}>\r\n <Link to={checkoutHref} onClick={() => setDrawerOpen(false)}>\r\n {t(\"checkout\", \"Checkout\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n );\r\n}\r\n"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "cart-drawer/lang/en.json",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"path": "cart-page/cart-page.tsx",
|
|
24
24
|
"type": "registry:page",
|
|
25
25
|
"target": "$modules$/cart-page/cart-page.tsx",
|
|
26
|
-
"content": "import { useMemo } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { Trash2, Plus, Minus, ArrowLeft, ShoppingBag } 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 { Separator } from \"@/components/ui/separator\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\r\nimport type { Category } from \"@/modules/ecommerce-core/types\";\r\nimport { useDbList } from \"@/db\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\n\r\nexport function CartPage() {\r\n const { t } = useTranslation(\"cart-page\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Shopping Cart\") });\r\n const { state, removeItem, updateQuantity } = useCart();\r\n const { items, total } = state;\r\n const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\r\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\r\n\r\n const currency = constants.site.currency || \"USD\";\r\n const shipping = 0;\r\n const tax = 0;\r\n const freeShippingThreshold = 100;\r\n\r\n const getProductPrice = (product: { price: number; sale_price?: number; on_sale?: boolean }) => {\r\n return product.on_sale && product.sale_price ? product.sale_price : product.price;\r\n };\r\n\r\n const handleQuantityChange = (productId: number | string, newQuantity: number) => {\r\n if (newQuantity <= 0) {\r\n removeItem(productId);\r\n } else {\r\n updateQuantity(productId, newQuantity);\r\n }\r\n };\r\n\r\n const handleQuantityInputChange = (productId: number | string, value: string) => {\r\n const quantity = parseInt(value) || 1;\r\n handleQuantityChange(productId, quantity);\r\n };\r\n\r\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\r\n const finalTotal = total + shipping + tax;\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 py-16\">\r\n <div className=\"mb-8\">\r\n <ShoppingBag className=\"h-24 w-24 mx-auto text-muted-foreground mb-4\" />\r\n <h1 className=\"text-3xl font-bold mb-4\">\r\n {t(\"empty\", \"Your Cart is Empty\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-8\">\r\n {t(\"emptyDescription\", \"Looks like you haven't added any items to your cart yet.\")}\r\n </p>\r\n <Button asChild size=\"lg\">\r\n <Link to=\"/products\">\r\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\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=\"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=\"/products\">\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\", \"Shopping Cart\")}</h1>\r\n <p className=\"text-muted-foreground\">\r\n {itemCount} {t(\"itemsInCart\", \"items in your cart\")}\r\n </p>\r\n </div>\r\n </FadeIn>\r\n\r\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\r\n <div className=\"lg:col-span-2 space-y-4\">\r\n {items.map((item) => (\r\n <Card key={item.id}>\r\n <CardContent className=\"p-6\">\r\n <div className=\"flex gap-4\">\r\n <div className=\"w-24 h-24 flex-shrink-0\">\r\n <img\r\n src={item.product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"}\r\n alt={item.product.name}\r\n className=\"w-full h-full object-cover rounded-lg\"\r\n />\r\n </div>\r\n\r\n <div className=\"flex-1 space-y-2\">\r\n <div className=\"flex items-start justify-between\">\r\n <div>\r\n <h3 className=\"font-semibold\">{item.product.name}</h3>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {categoryMap.get(item.product.categories?.[0] as number)?.name}\r\n </p>\r\n </div>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n onClick={() => removeItem(item.product.id)}\r\n className=\"text-destructive hover:text-destructive\"\r\n >\r\n <Trash2 className=\"h-4 w-4\" />\r\n </Button>\r\n </div>\r\n\r\n <div className=\"flex items-center justify-between\">\r\n <div className=\"flex items-center gap-2\">\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-8 w-8\"\r\n onClick={() => handleQuantityChange(item.product.id, item.quantity - 1)}\r\n >\r\n <Minus className=\"h-3 w-3\" />\r\n </Button>\r\n <Input\r\n type=\"number\"\r\n value={item.quantity}\r\n onChange={(e) => handleQuantityInputChange(item.product.id, e.target.value)}\r\n className=\"w-16 text-center\"\r\n min=\"1\"\r\n />\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-8 w-8\"\r\n onClick={() => handleQuantityChange(item.product.id, item.quantity + 1)}\r\n >\r\n <Plus className=\"h-3 w-3\" />\r\n </Button>\r\n </div>\r\n\r\n <div className=\"text-right\">\r\n <p className=\"font-semibold\">\r\n {formatPrice(getProductPrice(item.product) * item.quantity, currency)}\r\n </p>\r\n {item.quantity > 1 && (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {formatPrice(getProductPrice(item.product), currency)} {t(\"each\", \"each\")}\r\n </p>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n ))}\r\n </div>\r\n\r\n <div className=\"space-y-6\">\r\n <Card>\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=\"flex justify-between\">\r\n <span>\r\n {t(\"subtotal\", \"Subtotal\")} ({itemCount} {t(\"items\", \"items\")})\r\n </span>\r\n <span>{formatPrice(total, currency)}</span>\r\n </div>\r\n\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"shipping\", \"Shipping\")}</span>\r\n <span>\r\n {shipping === 0 ? t(\"free\", \"Free\") : formatPrice(shipping, currency)}\r\n </span>\r\n </div>\r\n\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\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 {shipping > 0 && freeShippingThreshold && freeShippingThreshold > total && (\r\n <div className=\"text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg\">\r\n {t(\"freeShippingMessage\", \"Add {{amount}} more for free shipping!\").replace(\r\n \"{{amount}}\",\r\n formatPrice(freeShippingThreshold - total, currency)\r\n )}\r\n </div>\r\n )}\r\n\r\n <Button asChild className=\"w-full\" size=\"lg\">\r\n <Link to=\"/checkout\">{t(\"proceedToCheckout\", \"Proceed to Checkout\")}</Link>\r\n </Button>\r\n\r\n <Button variant=\"outline\" asChild className=\"w-full\">\r\n <Link to=\"/products\">{t(\"continueShopping\", \"Continue Shopping\")}</Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n\r\n <Card>\r\n <CardContent className=\"p-4\">\r\n <div className=\"text-center space-y-2\">\r\n <div className=\"text-sm text-muted-foreground\">\r\n {t(\"secureCheckout\", \"Secure Checkout\")}\r\n </div>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {t(\"secureCheckoutDescription\", \"Your payment information is encrypted and secure\")}\r\n </p>\r\n </div>\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 CartPage;\r\n"
|
|
26
|
+
"content": "import { useMemo } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { Trash2, Plus, Minus, ArrowLeft, ShoppingBag } 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 { Separator } from \"@/components/ui/separator\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\r\nimport type { Category } from \"@/modules/ecommerce-core/types\";\r\nimport { useDbList } from \"@/db\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\n\r\nexport function CartPage() {\r\n const { t } = useTranslation(\"cart-page\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Shopping Cart\") });\r\n const { state, removeItem, updateQuantity } = useCart();\r\n const { items, total } = state;\r\n const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\r\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\r\n\r\n const currency = constants.site.currency || \"USD\";\r\n const shipping = 0;\r\n const tax = 0;\r\n const freeShippingThreshold = 100;\r\n\r\n const getProductPrice = (product: { price: number; sale_price?: number; on_sale?: boolean }) => {\r\n return product.on_sale && product.sale_price ? product.sale_price : product.price;\r\n };\r\n\r\n const handleQuantityChange = (productId: number | string, newQuantity: number) => {\r\n if (newQuantity <= 0) {\r\n removeItem(productId);\r\n } else {\r\n updateQuantity(productId, newQuantity);\r\n }\r\n };\r\n\r\n const handleQuantityInputChange = (productId: number | string, value: string) => {\r\n const quantity = parseInt(value) || 1;\r\n handleQuantityChange(productId, quantity);\r\n };\r\n\r\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\r\n const finalTotal = total + shipping + tax;\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 py-16\">\r\n <div className=\"mb-8\">\r\n <ShoppingBag className=\"h-24 w-24 mx-auto text-muted-foreground mb-4\" />\r\n <h1 className=\"text-3xl font-bold mb-4\">\r\n {t(\"empty\", \"Your Cart is Empty\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-8\">\r\n {t(\"emptyDescription\", \"Looks like you haven't added any items to your cart yet.\")}\r\n </p>\r\n <Button asChild size=\"lg\">\r\n <Link to=\"/products\">\r\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\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=\"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=\"/products\">\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\", \"Shopping Cart\")}</h1>\r\n <p className=\"text-muted-foreground\">\r\n {itemCount} {t(\"itemsInCart\", \"items in your cart\")}\r\n </p>\r\n </div>\r\n </FadeIn>\r\n\r\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\r\n <div className=\"lg:col-span-2 space-y-4\">\r\n {items.map((item) => (\r\n <Card key={item.id}>\r\n <CardContent className=\"p-6\">\r\n <div className=\"flex gap-4\">\r\n <div className=\"w-24 h-24 flex-shrink-0\">\r\n <img\r\n src={item.product.images?.length ? item.product.images?.[0] : \"/images/placeholder.png\"}\r\n alt={item.product.name}\r\n className=\"w-full h-full object-cover rounded-lg\"\r\n />\r\n </div>\r\n\r\n <div className=\"flex-1 space-y-2\">\r\n <div className=\"flex items-start justify-between\">\r\n <div>\r\n <h3 className=\"font-semibold\">{item.product.name}</h3>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {categoryMap.get(item.product.categories?.[0] as number)?.name}\r\n </p>\r\n </div>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n onClick={() => removeItem(item.id)}\r\n className=\"text-destructive hover:text-destructive\"\r\n >\r\n <Trash2 className=\"h-4 w-4\" />\r\n </Button>\r\n </div>\r\n\r\n <div className=\"flex items-center justify-between\">\r\n <div className=\"flex items-center gap-2\">\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-8 w-8\"\r\n onClick={() => handleQuantityChange(item.id, item.quantity - 1)}\r\n >\r\n <Minus className=\"h-3 w-3\" />\r\n </Button>\r\n <Input\r\n type=\"number\"\r\n value={item.quantity}\r\n onChange={(e) => handleQuantityInputChange(item.id, e.target.value)}\r\n className=\"w-16 text-center\"\r\n min=\"1\"\r\n />\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-8 w-8\"\r\n onClick={() => handleQuantityChange(item.id, item.quantity + 1)}\r\n >\r\n <Plus className=\"h-3 w-3\" />\r\n </Button>\r\n </div>\r\n\r\n <div className=\"text-right\">\r\n <p className=\"font-semibold\">\r\n {formatPrice(getProductPrice(item.product) * item.quantity, currency)}\r\n </p>\r\n {item.quantity > 1 && (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {formatPrice(getProductPrice(item.product), currency)} {t(\"each\", \"each\")}\r\n </p>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n ))}\r\n </div>\r\n\r\n <div className=\"space-y-6\">\r\n <Card>\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=\"flex justify-between\">\r\n <span>\r\n {t(\"subtotal\", \"Subtotal\")} ({itemCount} {t(\"items\", \"items\")})\r\n </span>\r\n <span>{formatPrice(total, currency)}</span>\r\n </div>\r\n\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"shipping\", \"Shipping\")}</span>\r\n <span>\r\n {shipping === 0 ? t(\"free\", \"Free\") : formatPrice(shipping, currency)}\r\n </span>\r\n </div>\r\n\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\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 {shipping > 0 && freeShippingThreshold && freeShippingThreshold > total && (\r\n <div className=\"text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg\">\r\n {t(\"freeShippingMessage\", \"Add {{amount}} more for free shipping!\").replace(\r\n \"{{amount}}\",\r\n formatPrice(freeShippingThreshold - total, currency)\r\n )}\r\n </div>\r\n )}\r\n\r\n <Button asChild className=\"w-full\" size=\"lg\">\r\n <Link to=\"/checkout\">{t(\"proceedToCheckout\", \"Proceed to Checkout\")}</Link>\r\n </Button>\r\n\r\n <Button variant=\"outline\" asChild className=\"w-full\">\r\n <Link to=\"/products\">{t(\"continueShopping\", \"Continue Shopping\")}</Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n\r\n <Card>\r\n <CardContent className=\"p-4\">\r\n <div className=\"text-center space-y-2\">\r\n <div className=\"text-sm text-muted-foreground\">\r\n {t(\"secureCheckout\", \"Secure Checkout\")}\r\n </div>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {t(\"secureCheckoutDescription\", \"Your payment information is encrypted and secure\")}\r\n </p>\r\n </div>\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 CartPage;\r\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "cart-page/lang/en.json",
|
|
@@ -24,7 +24,7 @@
|
|
|
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\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 <div className=\"space-y-2\">\r\n <Label htmlFor=\"firstName\">\r\n {t(\"firstName\", \"First Name\")} *\r\n </Label>\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 </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"lastName\">\r\n {t(\"lastName\", \"Last Name\")} *\r\n </Label>\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 </div>\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"email\">\r\n {t(\"email\", \"Email Address\")} *\r\n </Label>\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 </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"phone\">\r\n {t(\"phone\", \"Phone Number\")} *\r\n </Label>\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 </div>\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 <div className=\"space-y-2\">\r\n <Label htmlFor=\"country\">{t(\"country\", \"Country\")} *</Label>\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 </div>\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"city\">{t(\"city\", \"City\")} *</Label>\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 </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"postalCode\">\r\n {t(\"postalCode\", \"Postal Code\")} *\r\n </Label>\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 </div>\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 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",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"path": "contact-page-centered/contact-page-centered.tsx",
|
|
26
26
|
"type": "registry:component",
|
|
27
27
|
"target": "$modules$/contact-page-centered/contact-page-centered.tsx",
|
|
28
|
-
"content": "import React, {
|
|
28
|
+
"content": "import React, { useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Mail, Phone, MapPin } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\ninterface ContactPageCenteredProps {\r\n className?: string;\r\n}\r\n\r\nexport function ContactPageCentered({ className }: ContactPageCenteredProps) {\r\n const { t } = useTranslation(\"contact-page-centered\");\r\n usePageTitle({ title: t(\"title\", \"Contact Us\") });\r\n const apiService = useApiService();\r\n const fileMaxFiles = constants.file?.maxFiles || 5;\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\"idle\" | \"success\" | \"error\">(\"idle\");\r\n\r\n const contactCards = [\r\n {\r\n icon: Mail,\r\n title: t(\"emailTitle\", \"Email\"),\r\n value: constants.email || \"hello@example.com\",\r\n href: `mailto:${constants.email || \"hello@example.com\"}`,\r\n },\r\n {\r\n icon: Phone,\r\n title: t(\"phoneTitle\", \"Phone\"),\r\n value: constants.phone || \"+1 234 567 890\",\r\n href: `tel:${constants.phone || \"+1234567890\"}`,\r\n },\r\n {\r\n icon: MapPin,\r\n title: t(\"addressTitle\", \"Address\"),\r\n value: constants.address?.city || \"New York, USA\",\r\n href: \"#\",\r\n },\r\n ];\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n\r\n const remainingSlots = fileMaxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: fileMaxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n\r\n try {\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n constants.site.defaultLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({ name: \"\", email: \"\", message: \"\", attachments: [] });\r\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n } catch {\r\n setSubmitStatus(\"error\");\r\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {\r\n setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));\r\n };\r\n\r\n return (\r\n <Layout>\r\n <div className={cn(\"min-h-screen bg-muted/30 py-16 md:py-24\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 max-w-4xl\">\r\n {/* Header */}\r\n <div className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold mb-4\">{t(\"title\", \"Contact Us\")}</h1>\r\n <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto\">\r\n {t(\"subtitle\", \"We'd love to hear from you. Send us a message and we'll respond as soon as possible.\")}\r\n </p>\r\n </div>\r\n\r\n {/* Contact Cards */}\r\n <div className=\"grid sm:grid-cols-3 gap-4 mb-12\">\r\n {contactCards.map((card, index) => (\r\n <Card key={index} className=\"text-center\">\r\n <CardContent className=\"pt-6\">\r\n <div className=\"mx-auto w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-4\">\r\n <card.icon className=\"h-6 w-6 text-primary\" />\r\n </div>\r\n <h3 className=\"font-semibold mb-1\">{card.title}</h3>\r\n <a\r\n href={card.href}\r\n className=\"text-sm text-muted-foreground hover:text-primary transition-colors\"\r\n >\r\n {card.value}\r\n </a>\r\n </CardContent>\r\n </Card>\r\n ))}\r\n </div>\r\n\r\n {/* Form */}\r\n <Card>\r\n <CardContent className=\"pt-6\">\r\n <form onSubmit={handleSubmit} className=\"space-y-6\">\r\n <div className=\"grid sm:grid-cols-2 gap-4\">\r\n <FormField label={t(\"nameLabel\", \"Name\")} htmlFor=\"name\" required>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"namePlaceholder\", \"Your name\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </FormField>\r\n <FormField label={t(\"emailLabel\", \"Email\")} 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={handleChange}\r\n placeholder={t(\"emailPlaceholder\", \"your@email.com\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </FormField>\r\n </div>\r\n <FormField label={t(\"messageLabel\", \"Message\")} htmlFor=\"message\" required>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\", \"How can we help you?\")}\r\n required\r\n rows={6}\r\n className=\"mt-1 resize-none\"\r\n />\r\n </FormField>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-4 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm font-medium\">\r\n {t(\"success\", \"Message sent successfully! We'll get back to you soon.\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-4 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm font-medium\">\r\n {t(\"error\", \"Something went wrong. Please try again.\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button type=\"submit\" size=\"lg\" className=\"w-full\" disabled={isSubmitting}>\r\n {isSubmitting ? t(\"sending\", \"Sending...\") : t(\"submit\", \"Send Message\")}\r\n </Button>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageCentered;\r\n"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "contact-page-centered/lang/en.json",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"path": "contact-page-map-overlay/contact-page-map-overlay.tsx",
|
|
31
31
|
"type": "registry:component",
|
|
32
32
|
"target": "$modules$/contact-page-map-overlay/contact-page-map-overlay.tsx",
|
|
33
|
-
"content": "import React, { useState, useMemo, useEffect, useRef } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport {\r\n Mail,\r\n Phone,\r\n MapPin,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Send,\r\n ExternalLink,\r\n Upload,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { GoogleMap } from \"@/modules/google-map\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n};\r\n\r\nexport function ContactPageMapOverlay() {\r\n const { t } = useTranslation(\"contact-page-map-overlay\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n const fileInputRef = useRef<HTMLInputElement>(null);\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const maxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = maxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n // Auto-reset status after 5 seconds with proper cleanup\r\n useEffect(() => {\r\n if (submitStatus === \"idle\") return;\r\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n return () => clearTimeout(timer);\r\n }, [submitStatus]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: []\r\n });\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n // Default coordinates (can be customized via constants)\r\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\r\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\r\n\r\n return (\r\n <Layout>\r\n <div className=\"relative min-h-[calc(100vh-4rem)]\">\r\n {/* Full-screen Map Background */}\r\n <div className=\"absolute inset-0\">\r\n <GoogleMap\r\n latitude={mapLatitude}\r\n longitude={mapLongitude}\r\n zoom={14}\r\n height=\"100%\"\r\n className=\"rounded-none border-0 h-full\"\r\n title={t(\"mapTitle\")}\r\n />\r\n {/* Dark overlay for better readability */}\r\n <div className=\"absolute inset-0 bg-black/30 pointer-events-none\" />\r\n </div>\r\n\r\n {/* Content Overlay */}\r\n <div className=\"relative z-10 min-h-[calc(100vh-4rem)] flex items-center py-12 px-4\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto\">\r\n <div className=\"grid lg:grid-cols-2 gap-8 items-stretch\">\r\n {/* Form Card - Glassmorphism */}\r\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\r\n <CardHeader>\r\n <CardTitle className=\"text-2xl lg:text-3xl\">\r\n {t(\"title\")}\r\n </CardTitle>\r\n <p className=\"text-muted-foreground mt-2\">\r\n {t(\"description\")}\r\n </p>\r\n </CardHeader>\r\n <CardContent>\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\r\n <div>\r\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\r\n {t(\"fullName\")} *\r\n </Label>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1.5 bg-background/50\"\r\n />\r\n </div>\r\n <div>\r\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\r\n {t(\"emailAddress\")} *\r\n </Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1.5 bg-background/50\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\r\n {t(\"message\")} *\r\n </Label>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={4}\r\n className=\"mt-1.5 resize-none bg-background/50\"\r\n />\r\n </div>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm\">{t(\"error\")}</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-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n <>\r\n <Send className=\"w-4 h-4 mr-2\" />\r\n {t(\"submit\")}\r\n </>\r\n )}\r\n </Button>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Contact Info Card - Glassmorphism */}\r\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\r\n <CardHeader>\r\n <CardTitle>{t(\"contactInfo\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-5\">\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <Mail className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"email\")}</p>\r\n <a\r\n href={`mailto:${constants.email}`}\r\n className=\"font-medium hover:text-primary transition-colors\"\r\n >\r\n {constants.email}\r\n </a>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <Phone className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"phone\")}</p>\r\n <a\r\n href={`tel:${constants.phone}`}\r\n className=\"font-medium hover:text-primary transition-colors\"\r\n >\r\n {constants.phone}\r\n </a>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"address\")}</p>\r\n <p className=\"font-medium\">\r\n {constants.address.line1}\r\n <br />\r\n {constants.address.city}, {constants.address.state} {constants.address.postalCode}\r\n </p>\r\n </div>\r\n </div>\r\n\r\n {/* Open in Maps Link */}\r\n <a\r\n href={`https://www.google.com/maps?q=${mapLatitude},${mapLongitude}`}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"inline-flex items-center gap-2 text-sm text-primary hover:underline mt-2\"\r\n >\r\n <ExternalLink className=\"w-4 h-4\" />\r\n {t(\"openInMaps\")}\r\n </a>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"pt-4 border-t\">\r\n <p className=\"text-sm text-muted-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-4 w-4\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageMapOverlay;\r\n"
|
|
33
|
+
"content": "import React, { useState, useMemo, useEffect } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport {\r\n Mail,\r\n Phone,\r\n MapPin,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Send,\r\n ExternalLink,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { GoogleMap } from \"@/modules/google-map\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n};\r\n\r\nexport function ContactPageMapOverlay() {\r\n const { t } = useTranslation(\"contact-page-map-overlay\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const maxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = maxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n // Auto-reset status after 5 seconds with proper cleanup\r\n useEffect(() => {\r\n if (submitStatus === \"idle\") return;\r\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n return () => clearTimeout(timer);\r\n }, [submitStatus]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: []\r\n });\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n // Default coordinates (can be customized via constants)\r\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\r\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\r\n\r\n return (\r\n <Layout>\r\n <div className=\"relative min-h-[calc(100vh-4rem)]\">\r\n {/* Full-screen Map Background */}\r\n <div className=\"absolute inset-0\">\r\n <GoogleMap\r\n latitude={mapLatitude}\r\n longitude={mapLongitude}\r\n zoom={14}\r\n height=\"100%\"\r\n className=\"rounded-none border-0 h-full\"\r\n title={t(\"mapTitle\")}\r\n />\r\n {/* Dark overlay for better readability */}\r\n <div className=\"absolute inset-0 bg-black/30 pointer-events-none\" />\r\n </div>\r\n\r\n {/* Content Overlay */}\r\n <div className=\"relative z-10 min-h-[calc(100vh-4rem)] flex items-center py-12 px-4\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto\">\r\n <div className=\"grid lg:grid-cols-2 gap-8 items-stretch\">\r\n {/* Form Card - Glassmorphism */}\r\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\r\n <CardHeader>\r\n <CardTitle className=\"text-2xl lg:text-3xl\">\r\n {t(\"title\")}\r\n </CardTitle>\r\n <p className=\"text-muted-foreground mt-2\">\r\n {t(\"description\")}\r\n </p>\r\n </CardHeader>\r\n <CardContent>\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\r\n <FormField label={t(\"fullName\")} htmlFor=\"name\" required>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1.5 bg-background/50\"\r\n />\r\n </FormField>\r\n <FormField label={t(\"emailAddress\")} 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={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1.5 bg-background/50\"\r\n />\r\n </FormField>\r\n </div>\r\n\r\n <FormField label={t(\"message\")} htmlFor=\"message\" required>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={4}\r\n className=\"mt-1.5 resize-none bg-background/50\"\r\n />\r\n </FormField>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm\">{t(\"error\")}</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-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n <>\r\n <Send className=\"w-4 h-4 mr-2\" />\r\n {t(\"submit\")}\r\n </>\r\n )}\r\n </Button>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Contact Info Card - Glassmorphism */}\r\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\r\n <CardHeader>\r\n <CardTitle>{t(\"contactInfo\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-5\">\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <Mail className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"email\")}</p>\r\n <a\r\n href={`mailto:${constants.email}`}\r\n className=\"font-medium hover:text-primary transition-colors\"\r\n >\r\n {constants.email}\r\n </a>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <Phone className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"phone\")}</p>\r\n <a\r\n href={`tel:${constants.phone}`}\r\n className=\"font-medium hover:text-primary transition-colors\"\r\n >\r\n {constants.phone}\r\n </a>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"address\")}</p>\r\n <p className=\"font-medium\">\r\n {constants.address.line1}\r\n <br />\r\n {constants.address.city}, {constants.address.state} {constants.address.postalCode}\r\n </p>\r\n </div>\r\n </div>\r\n\r\n {/* Open in Maps Link */}\r\n <a\r\n href={`https://www.google.com/maps?q=${mapLatitude},${mapLongitude}`}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"inline-flex items-center gap-2 text-sm text-primary hover:underline mt-2\"\r\n >\r\n <ExternalLink className=\"w-4 h-4\" />\r\n {t(\"openInMaps\")}\r\n </a>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"pt-4 border-t\">\r\n <p className=\"text-sm text-muted-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-4 w-4\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageMapOverlay;\r\n"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"path": "contact-page-map-overlay/lang/en.json",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"path": "contact-page-map-split/contact-page-map-split.tsx",
|
|
31
31
|
"type": "registry:component",
|
|
32
32
|
"target": "$modules$/contact-page-map-split/contact-page-map-split.tsx",
|
|
33
|
-
"content": "import React, { useState, useMemo, useEffect, useRef } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport {\r\n Mail,\r\n Phone,\r\n MapPin,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Send,\r\n Upload,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { GoogleMap } from \"@/modules/google-map\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n};\r\n\r\nexport function ContactPageMapSplit() {\r\n const { t } = useTranslation(\"contact-page-map-split\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n const fileInputRef = useRef<HTMLInputElement>(null);\r\n\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const maxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = maxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n // Auto-reset status after 5 seconds with proper cleanup\r\n useEffect(() => {\r\n if (submitStatus === \"idle\") return;\r\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n return () => clearTimeout(timer);\r\n }, [submitStatus]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"phone\", required: false },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: []\r\n });\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n // Default coordinates (can be customized via constants)\r\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\r\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\r\n\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen flex flex-col lg:flex-row\">\r\n {/* Left Side - Form & Info */}\r\n <div className=\"w-full lg:w-1/2 bg-background py-12 lg:py-16 px-6 lg:px-12 flex flex-col justify-center\">\r\n <div className=\"max-w-lg mx-auto w-full\">\r\n {/* Header */}\r\n <div className=\"mb-10\">\r\n <h1 className=\"text-3xl lg:text-4xl font-bold text-foreground mb-3\">\r\n {t(\"title\")}\r\n </h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"description\")}\r\n </p>\r\n </div>\r\n\r\n {/* Contact Info Cards */}\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4 mb-10\">\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <Mail className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"email\")}</p>\r\n <p className=\"text-sm font-medium truncate\">{constants.email}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <Phone className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"phone\")}</p>\r\n <p className=\"text-sm font-medium truncate\">{constants.phone}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50 sm:col-span-2\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"address\")}</p>\r\n <p className=\"text-sm font-medium\">\r\n {constants.address.line1}, {constants.address.city}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Contact Form */}\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\r\n <div>\r\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\r\n {t(\"fullName\")} *\r\n </Label>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1.5\"\r\n />\r\n </div>\r\n <div>\r\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\r\n {t(\"emailAddress\")} *\r\n </Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1.5\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"phone\" className=\"text-sm font-medium\">\r\n {t(\"phoneNumber\")}\r\n </Label>\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleChange}\r\n placeholder={t(\"phonePlaceholder\")}\r\n className=\"mt-1.5\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\r\n {t(\"message\")} *\r\n </Label>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={4}\r\n className=\"mt-1.5 resize-none\"\r\n />\r\n </div>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm\">{t(\"error\")}</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-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n <>\r\n <Send className=\"w-4 h-4 mr-2\" />\r\n {t(\"submit\")}\r\n </>\r\n )}\r\n </Button>\r\n </form>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"mt-10 pt-6 border-t\">\r\n <p className=\"text-sm text-muted-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-4 w-4\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Right Side - Map */}\r\n <div className=\"w-full lg:w-1/2 h-[400px] lg:h-[calc(100vh-4rem)] relative\">\r\n <GoogleMap\r\n latitude={mapLatitude}\r\n longitude={mapLongitude}\r\n zoom={14}\r\n height=\"100%\"\r\n className=\"rounded-none border-0 h-full\"\r\n title={t(\"mapTitle\")}\r\n />\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageMapSplit;\r\n"
|
|
33
|
+
"content": "import React, { useState, useMemo, useEffect } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { \r\n Mail,\r\n Phone,\r\n MapPin,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Send,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { GoogleMap } from \"@/modules/google-map\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n};\r\n\r\nexport function ContactPageMapSplit() {\r\n const { t } = useTranslation(\"contact-page-map-split\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n\r\n\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const maxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = maxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n // Auto-reset status after 5 seconds with proper cleanup\r\n useEffect(() => {\r\n if (submitStatus === \"idle\") return;\r\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n return () => clearTimeout(timer);\r\n }, [submitStatus]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"phone\", required: false },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: []\r\n });\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n // Default coordinates (can be customized via constants)\r\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\r\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\r\n\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen flex flex-col lg:flex-row\">\r\n {/* Left Side - Form & Info */}\r\n <div className=\"w-full lg:w-1/2 bg-background py-12 lg:py-16 px-6 lg:px-12 flex flex-col justify-center\">\r\n <div className=\"max-w-lg mx-auto w-full\">\r\n {/* Header */}\r\n <div className=\"mb-10\">\r\n <h1 className=\"text-3xl lg:text-4xl font-bold text-foreground mb-3\">\r\n {t(\"title\")}\r\n </h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"description\")}\r\n </p>\r\n </div>\r\n\r\n {/* Contact Info Cards */}\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4 mb-10\">\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <Mail className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"email\")}</p>\r\n <p className=\"text-sm font-medium truncate\">{constants.email}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <Phone className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"phone\")}</p>\r\n <p className=\"text-sm font-medium truncate\">{constants.phone}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50 sm:col-span-2\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"address\")}</p>\r\n <p className=\"text-sm font-medium\">\r\n {constants.address.line1}, {constants.address.city}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Contact Form */}\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\r\n <FormField label={t(\"fullName\")} htmlFor=\"name\" required>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1.5\"\r\n />\r\n </FormField>\r\n <FormField label={t(\"emailAddress\")} 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={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1.5\"\r\n />\r\n </FormField>\r\n </div>\r\n\r\n <FormField label={t(\"phoneNumber\")} htmlFor=\"phone\">\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleChange}\r\n placeholder={t(\"phonePlaceholder\")}\r\n className=\"mt-1.5\"\r\n />\r\n </FormField>\r\n <FormField label={t(\"message\")} htmlFor=\"message\" required>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={4}\r\n className=\"mt-1.5 resize-none\"\r\n />\r\n </FormField>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm\">{t(\"error\")}</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-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n <>\r\n <Send className=\"w-4 h-4 mr-2\" />\r\n {t(\"submit\")}\r\n </>\r\n )}\r\n </Button>\r\n </form>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"mt-10 pt-6 border-t\">\r\n <p className=\"text-sm text-muted-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-4 w-4\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Right Side - Map */}\r\n <div className=\"w-full lg:w-1/2 h-[400px] lg:h-[calc(100vh-4rem)] relative\">\r\n <GoogleMap\r\n latitude={mapLatitude}\r\n longitude={mapLongitude}\r\n zoom={14}\r\n height=\"100%\"\r\n className=\"rounded-none border-0 h-full\"\r\n title={t(\"mapTitle\")}\r\n />\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageMapSplit;\r\n"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"path": "contact-page-map-split/lang/en.json",
|