@promakeai/cli 0.0.6 → 0.1.0

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.
Files changed (114) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/registry/about-page.json +4 -2
  3. package/dist/registry/about-section.json +2 -2
  4. package/dist/registry/announcement-bar.json +43 -0
  5. package/dist/registry/auth-core.json +43 -0
  6. package/dist/registry/auth.json +1 -1
  7. package/dist/registry/blog-list-page.json +3 -2
  8. package/dist/registry/blog-section.json +1 -1
  9. package/dist/registry/cart-page.json +3 -3
  10. package/dist/registry/case-study-page.json +48 -0
  11. package/dist/registry/checkout-page.json +6 -5
  12. package/dist/registry/coming-soon-page-minimal.json +45 -0
  13. package/dist/registry/coming-soon-page.json +47 -0
  14. package/dist/registry/contact-info-grid.json +1 -1
  15. package/dist/registry/contact-page-centered.json +2 -2
  16. package/dist/registry/contact-page-map-overlay.json +4 -3
  17. package/dist/registry/contact-page-map-split.json +4 -3
  18. package/dist/registry/contact-page-split.json +3 -3
  19. package/dist/registry/contact-page.json +5 -3
  20. package/dist/registry/cookie-consent.json +43 -0
  21. package/dist/registry/cookies-page.json +3 -1
  22. package/dist/registry/cta-section.json +1 -1
  23. package/dist/registry/db.json +1 -1
  24. package/dist/registry/docs/about-page.md +5 -0
  25. package/dist/registry/docs/announcement-bar.md +38 -0
  26. package/dist/registry/docs/auth-core.md +64 -0
  27. package/dist/registry/docs/blog-list-page.md +1 -0
  28. package/dist/registry/docs/case-study-page.md +39 -0
  29. package/dist/registry/docs/checkout-page.md +2 -1
  30. package/dist/registry/docs/coming-soon-page-minimal.md +32 -0
  31. package/dist/registry/docs/coming-soon-page.md +37 -0
  32. package/dist/registry/docs/contact-page-map-overlay.md +2 -2
  33. package/dist/registry/docs/contact-page-map-split.md +2 -2
  34. package/dist/registry/docs/contact-page.md +5 -0
  35. package/dist/registry/docs/cookie-consent.md +37 -0
  36. package/dist/registry/docs/cookies-page.md +5 -0
  37. package/dist/registry/docs/ecommerce-core.md +4 -3
  38. package/dist/registry/docs/forgot-password-page-split.md +45 -0
  39. package/dist/registry/docs/forgot-password-page.md +14 -5
  40. package/dist/registry/docs/header-ecommerce.md +1 -0
  41. package/dist/registry/docs/hero-carousel.md +37 -0
  42. package/dist/registry/docs/landing-page-app.md +43 -0
  43. package/dist/registry/docs/landing-page-saas.md +41 -0
  44. package/dist/registry/docs/login-page-split.md +13 -4
  45. package/dist/registry/docs/login-page.md +17 -4
  46. package/dist/registry/docs/logo-cloud.md +33 -0
  47. package/dist/registry/docs/masonry-grid.md +37 -0
  48. package/dist/registry/docs/my-orders-page.md +44 -0
  49. package/dist/registry/docs/order-confirmation-page.md +41 -0
  50. package/dist/registry/docs/portfolio-page.md +38 -0
  51. package/dist/registry/docs/pricing-page.md +38 -0
  52. package/dist/registry/docs/privacy-page.md +5 -0
  53. package/dist/registry/docs/product-quick-view.md +37 -0
  54. package/dist/registry/docs/reading-progress.md +31 -0
  55. package/dist/registry/docs/register-page-split.md +45 -0
  56. package/dist/registry/docs/register-page.md +14 -7
  57. package/dist/registry/docs/reset-password-page-split.md +45 -0
  58. package/dist/registry/docs/reset-password-page.md +36 -0
  59. package/dist/registry/docs/share-buttons.md +37 -0
  60. package/dist/registry/docs/team-page.md +38 -0
  61. package/dist/registry/docs/terms-page.md +5 -0
  62. package/dist/registry/docs/timeline-section.md +37 -0
  63. package/dist/registry/docs/video-hero.md +41 -0
  64. package/dist/registry/ecommerce-core.json +17 -1
  65. package/dist/registry/empty-page.json +1 -1
  66. package/dist/registry/faq-categorized.json +1 -1
  67. package/dist/registry/faq-simple.json +1 -1
  68. package/dist/registry/favorites-ecommerce-block.json +1 -1
  69. package/dist/registry/feature-section.json +2 -2
  70. package/dist/registry/footer.json +1 -1
  71. package/dist/registry/forgot-password-page-split.json +50 -0
  72. package/dist/registry/forgot-password-page.json +9 -7
  73. package/dist/registry/header-ecommerce.json +3 -2
  74. package/dist/registry/header-mega.json +1 -1
  75. package/dist/registry/hero-carousel.json +45 -0
  76. package/dist/registry/hero-cta.json +2 -2
  77. package/dist/registry/hero-gradient.json +1 -1
  78. package/dist/registry/hero.json +1 -1
  79. package/dist/registry/index.json +22 -2
  80. package/dist/registry/landing-page-app.json +47 -0
  81. package/dist/registry/landing-page-saas.json +47 -0
  82. package/dist/registry/login-page-split.json +11 -7
  83. package/dist/registry/login-page.json +4 -4
  84. package/dist/registry/logo-cloud.json +41 -0
  85. package/dist/registry/masonry-grid.json +43 -0
  86. package/dist/registry/my-orders-page.json +52 -0
  87. package/dist/registry/order-confirmation-page.json +49 -0
  88. package/dist/registry/portfolio-page.json +45 -0
  89. package/dist/registry/pricing-page.json +47 -0
  90. package/dist/registry/pricing-section.json +1 -1
  91. package/dist/registry/privacy-page.json +3 -1
  92. package/dist/registry/product-detail-block.json +1 -1
  93. package/dist/registry/product-quick-view.json +46 -0
  94. package/dist/registry/products-page.json +3 -3
  95. package/dist/registry/reading-progress.json +43 -0
  96. package/dist/registry/register-page-split.json +50 -0
  97. package/dist/registry/register-page.json +9 -7
  98. package/dist/registry/reset-password-page-split.json +50 -0
  99. package/dist/registry/reset-password-page.json +39 -0
  100. package/dist/registry/share-buttons.json +46 -0
  101. package/dist/registry/team-page.json +47 -0
  102. package/dist/registry/terms-page.json +3 -1
  103. package/dist/registry/testimonials-carousel.json +1 -1
  104. package/dist/registry/testimonials-grid.json +1 -1
  105. package/dist/registry/timeline-section.json +43 -0
  106. package/dist/registry/video-hero.json +42 -0
  107. package/package.json +1 -1
  108. package/template/index.html +5 -5
  109. package/template/src/App.tsx +4 -0
  110. package/template/src/components/GoogleAnalytics.tsx +34 -0
  111. package/template/src/components/Layout.tsx +1 -1
  112. package/template/src/components/ScriptInjector.tsx +62 -0
  113. package/template/src/constants/constants.json +8 -2
  114. package/template/src/pages/Index.tsx +5 -1
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "case-study-page",
3
+ "type": "registry:component",
4
+ "title": "Case Study Page",
5
+ "description": "Detailed project case study with challenge/solution/results sections. Includes hero image, gallery, project details sidebar, and share buttons.",
6
+ "dependencies": [
7
+ "lucide-react"
8
+ ],
9
+ "registryDependencies": [
10
+ "button",
11
+ "animations",
12
+ "share-buttons"
13
+ ],
14
+ "usage": "import { CaseStudyPage } from '@/modules/case-study-page';\n\n<CaseStudyPage />\n\n• Installed at: src/modules/case-study-page/\n• Customize content: lang/en/case-study-page.json\n• Props: title, heroImage, gallery[], challenge, solution, results, details",
15
+ "files": [
16
+ {
17
+ "path": "case-study-page/case-study-page.tsx",
18
+ "type": "registry:component",
19
+ "target": "$modules$/case-study-page/case-study-page.tsx",
20
+ "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router\";\nimport { cn } from \"@/lib/utils\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { ShareButtons } from \"@/modules/share-buttons\";\nimport { FadeIn } from \"@/modules/animations\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\n\ninterface ProjectDetails {\n client?: string;\n date?: string;\n category?: string;\n services?: string[];\n website?: string;\n}\n\ninterface CaseStudySection {\n title: string;\n content: string;\n}\n\ninterface CaseStudyPageProps {\n title?: string;\n subtitle?: string;\n heroImage?: string;\n gallery?: string[];\n challenge?: CaseStudySection;\n solution?: CaseStudySection;\n results?: CaseStudySection;\n details?: ProjectDetails;\n prevProject?: { title: string; link: string };\n nextProject?: { title: string; link: string };\n className?: string;\n}\n\nexport function CaseStudyPage({\n title,\n subtitle,\n heroImage,\n gallery,\n challenge,\n solution,\n results,\n details,\n prevProject,\n nextProject,\n className,\n}: CaseStudyPageProps) {\n const { t } = useTranslation(\"case-study-page\");\n usePageTitle({ title: t(\"pageTitle\") });\n const [selectedImage, setSelectedImage] = useState<string | null>(null);\n\n const displayTitle = title ?? t(\"title\");\n const displaySubtitle = subtitle ?? t(\"subtitle\");\n const displayHeroImage = heroImage ?? \"/images/placeholder.png\";\n const displayGallery = gallery ?? [\n \"/images/placeholder.png\",\n \"/images/placeholder.png\",\n \"/images/placeholder.png\",\n \"/images/placeholder.png\",\n ];\n\n const displayChallenge = challenge ?? {\n title: t(\"sections.challenge.title\"),\n content: t(\"sections.challenge.content\"),\n };\n\n const displaySolution = solution ?? {\n title: t(\"sections.solution.title\"),\n content: t(\"sections.solution.content\"),\n };\n\n const displayResults = results ?? {\n title: t(\"sections.results.title\"),\n content: t(\"sections.results.content\"),\n };\n\n const displayDetails = details ?? {\n client: t(\"details.client\"),\n date: t(\"details.date\"),\n category: t(\"details.category\"),\n services: [t(\"details.services.0\"), t(\"details.services.1\"), t(\"details.services.2\")],\n website: \"https://example.com\",\n };\n\n const displayPrevProject = prevProject ?? {\n title: t(\"navigation.prev\"),\n link: \"/portfolio\",\n };\n\n const displayNextProject = nextProject ?? {\n title: t(\"navigation.next\"),\n link: \"/portfolio\",\n };\n\n return (\n <Layout>\n <div className={cn(\"min-h-screen\", className)}>\n {/* Hero Section */}\n <div className=\"relative h-[50vh] md:h-[70vh] overflow-hidden\">\n <img\n src={displayHeroImage}\n alt={displayTitle}\n className=\"w-full h-full object-cover\"\n />\n <div className=\"absolute inset-0 bg-gradient-to-t from-background via-background/50 to-transparent\" />\n <div className=\"absolute bottom-0 left-0 right-0 p-8 md:p-16\">\n <div className=\"container max-w-[var(--container-max-width)] mx-auto\">\n <FadeIn>\n <h1 className=\"text-4xl md:text-5xl lg:text-6xl font-bold mb-4\">\n {displayTitle}\n </h1>\n <p className=\"text-lg md:text-xl text-muted-foreground max-w-2xl\">\n {displaySubtitle}\n </p>\n </FadeIn>\n </div>\n </div>\n </div>\n\n {/* Content */}\n <div className=\"container max-w-[var(--container-max-width)] mx-auto px-4 py-12 md:py-16\">\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-12\">\n {/* Main Content */}\n <div className=\"lg:col-span-2 space-y-12\">\n {/* Challenge */}\n <FadeIn>\n <section>\n <h2 className=\"text-2xl md:text-3xl font-bold mb-4\">\n {displayChallenge.title}\n </h2>\n <p className=\"text-muted-foreground leading-relaxed\">\n {displayChallenge.content}\n </p>\n </section>\n </FadeIn>\n\n {/* Solution */}\n <FadeIn delay={0.1}>\n <section>\n <h2 className=\"text-2xl md:text-3xl font-bold mb-4\">\n {displaySolution.title}\n </h2>\n <p className=\"text-muted-foreground leading-relaxed\">\n {displaySolution.content}\n </p>\n </section>\n </FadeIn>\n\n {/* Gallery */}\n <FadeIn delay={0.2}>\n <section>\n <h2 className=\"text-2xl md:text-3xl font-bold mb-6\">\n {t(\"gallery\")}\n </h2>\n <div className=\"grid grid-cols-2 gap-4\">\n {displayGallery.map((image, index) => (\n <button\n key={index}\n onClick={() => setSelectedImage(image)}\n className=\"aspect-[4/3] overflow-hidden rounded-xl group\"\n >\n <img\n src={image}\n alt={`Gallery ${index + 1}`}\n className=\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\"\n />\n </button>\n ))}\n </div>\n </section>\n </FadeIn>\n\n {/* Results */}\n <FadeIn delay={0.3}>\n <section>\n <h2 className=\"text-2xl md:text-3xl font-bold mb-4\">\n {displayResults.title}\n </h2>\n <p className=\"text-muted-foreground leading-relaxed\">\n {displayResults.content}\n </p>\n </section>\n </FadeIn>\n </div>\n\n {/* Sidebar */}\n <div className=\"lg:col-span-1\">\n <FadeIn delay={0.2}>\n <div className=\"sticky top-24 space-y-8\">\n {/* Project Details */}\n <div className=\"bg-muted/50 rounded-xl p-6\">\n <h3 className=\"text-lg font-semibold mb-4\">{t(\"projectDetails\")}</h3>\n <dl className=\"space-y-4\">\n {displayDetails.client && (\n <div>\n <dt className=\"text-sm text-muted-foreground\">{t(\"labels.client\")}</dt>\n <dd className=\"font-medium\">{displayDetails.client}</dd>\n </div>\n )}\n {displayDetails.date && (\n <div>\n <dt className=\"text-sm text-muted-foreground\">{t(\"labels.date\")}</dt>\n <dd className=\"font-medium\">{displayDetails.date}</dd>\n </div>\n )}\n {displayDetails.category && (\n <div>\n <dt className=\"text-sm text-muted-foreground\">{t(\"labels.category\")}</dt>\n <dd className=\"font-medium\">{displayDetails.category}</dd>\n </div>\n )}\n {displayDetails.services && displayDetails.services.length > 0 && (\n <div>\n <dt className=\"text-sm text-muted-foreground\">{t(\"labels.services\")}</dt>\n <dd className=\"font-medium\">\n {displayDetails.services.join(\", \")}\n </dd>\n </div>\n )}\n {displayDetails.website && (\n <div>\n <dt className=\"text-sm text-muted-foreground\">{t(\"labels.website\")}</dt>\n <dd>\n <a\n href={displayDetails.website}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"font-medium text-primary hover:underline\"\n >\n {t(\"visitWebsite\")}\n </a>\n </dd>\n </div>\n )}\n </dl>\n </div>\n\n {/* Share */}\n <div>\n <h3 className=\"text-lg font-semibold mb-4\">{t(\"share\")}</h3>\n <ShareButtons />\n </div>\n </div>\n </FadeIn>\n </div>\n </div>\n\n {/* Navigation */}\n <FadeIn delay={0.4}>\n <div className=\"mt-16 pt-8 border-t border-border\">\n <div className=\"flex justify-between items-center\">\n <Link to={displayPrevProject.link}>\n <Button variant=\"ghost\" className=\"gap-2\">\n <ChevronLeft className=\"w-4 h-4\" />\n <span className=\"hidden sm:inline\">{displayPrevProject.title}</span>\n <span className=\"sm:hidden\">{t(\"prev\")}</span>\n </Button>\n </Link>\n <Link to=\"/portfolio\">\n <Button variant=\"outline\">{t(\"allProjects\")}</Button>\n </Link>\n <Link to={displayNextProject.link}>\n <Button variant=\"ghost\" className=\"gap-2\">\n <span className=\"hidden sm:inline\">{displayNextProject.title}</span>\n <span className=\"sm:hidden\">{t(\"next\")}</span>\n <ChevronRight className=\"w-4 h-4\" />\n </Button>\n </Link>\n </div>\n </div>\n </FadeIn>\n </div>\n\n {/* Lightbox */}\n {selectedImage && (\n <div\n className=\"fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4\"\n onClick={() => setSelectedImage(null)}\n >\n <button\n className=\"absolute top-4 right-4 text-white/80 hover:text-white\"\n onClick={() => setSelectedImage(null)}\n >\n <svg\n className=\"w-8 h-8\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M6 18L18 6M6 6l12 12\"\n />\n </svg>\n </button>\n <img\n src={selectedImage}\n alt=\"Gallery\"\n className=\"max-w-full max-h-[90vh] object-contain rounded-lg\"\n onClick={(e) => e.stopPropagation()}\n />\n </div>\n )}\n </div>\n </Layout>\n );\n}\n\nexport default CaseStudyPage;\n"
21
+ },
22
+ {
23
+ "path": "case-study-page/index.ts",
24
+ "type": "registry:index",
25
+ "target": "$modules$/case-study-page/index.ts",
26
+ "content": "export * from \"./case-study-page\";\r\nexport { CaseStudyPage as default } from \"./case-study-page\";\r\n"
27
+ },
28
+ {
29
+ "path": "case-study-page/lang/en.json",
30
+ "type": "registry:lang",
31
+ "target": "$modules$/case-study-page/lang/en.json",
32
+ "content": "{\r\n \"pageTitle\": \"Case Study\",\r\n \"title\": \"E-commerce Platform Redesign\",\r\n \"subtitle\": \"Ask Promake to customize this case study based on your actual project.\",\r\n \"sections\": {\r\n \"challenge\": {\r\n \"title\": \"The Challenge\",\r\n \"content\": \"The client needed a complete overhaul of their e-commerce platform to improve user experience, increase conversion rates, and modernize their brand presence. The existing platform was outdated, had poor mobile support, and suffered from slow load times that were affecting sales.\"\r\n },\r\n \"solution\": {\r\n \"title\": \"Our Solution\",\r\n \"content\": \"We designed and developed a modern, responsive e-commerce platform with a focus on performance and user experience. Key improvements included a streamlined checkout process, advanced product filtering, personalized recommendations, and a complete visual redesign that aligned with the client's evolved brand identity.\"\r\n },\r\n \"results\": {\r\n \"title\": \"The Results\",\r\n \"content\": \"After launch, the client saw a 45% increase in conversion rates, 60% improvement in page load times, and a 35% increase in mobile sales. Customer satisfaction scores improved by 28%, and the average order value increased by 22%.\"\r\n }\r\n },\r\n \"gallery\": \"Project Gallery\",\r\n \"projectDetails\": \"Project Details\",\r\n \"labels\": {\r\n \"client\": \"Client\",\r\n \"date\": \"Date\",\r\n \"category\": \"Category\",\r\n \"services\": \"Services\"\r\n },\r\n \"details\": {\r\n \"client\": \"Acme Corporation\",\r\n \"date\": \"January 2024\",\r\n \"category\": \"Web Design\",\r\n \"services\": [\"UX Design\", \"UI Design\", \"Development\"]\r\n },\r\n \"visitWebsite\": \"Visit Website\",\r\n \"share\": \"Share This Project\",\r\n \"navigation\": {\r\n \"prev\": \"Previous Project\",\r\n \"next\": \"Next Project\"\r\n },\r\n \"prev\": \"Prev\",\r\n \"next\": \"Next\",\r\n \"allProjects\": \"All Projects\"\r\n}\r\n"
33
+ },
34
+ {
35
+ "path": "case-study-page/lang/tr.json",
36
+ "type": "registry:lang",
37
+ "target": "$modules$/case-study-page/lang/tr.json",
38
+ "content": "{\r\n \"pageTitle\": \"Vaka Çalışması\",\r\n \"title\": \"E-ticaret Platformu Yenileme\",\r\n \"subtitle\": \"Bu vaka çalışmasını gerçek projenize göre özelleştirmek için Promake'e sorun.\",\r\n \"sections\": {\r\n \"challenge\": {\r\n \"title\": \"Zorluk\",\r\n \"content\": \"Müşteri, kullanıcı deneyimini iyileştirmek, dönüşüm oranlarını artırmak ve marka varlığını modernize etmek için e-ticaret platformunun tamamen yenilenmesini istedi. Mevcut platform eskimişti, mobil desteği zayıftı ve satışları etkileyen yavaş yükleme süreleri yaşıyordu.\"\r\n },\r\n \"solution\": {\r\n \"title\": \"Çözümümüz\",\r\n \"content\": \"Performans ve kullanıcı deneyimine odaklanan modern, duyarlı bir e-ticaret platformu tasarladık ve geliştirdik. Temel iyileştirmeler arasında basitleştirilmiş ödeme süreci, gelişmiş ürün filtreleme, kişiselleştirilmiş öneriler ve müşterinin gelişen marka kimliğiyle uyumlu tam bir görsel yenileme yer aldı.\"\r\n },\r\n \"results\": {\r\n \"title\": \"Sonuçlar\",\r\n \"content\": \"Lansmandan sonra müşteri, dönüşüm oranlarında %45 artış, sayfa yükleme sürelerinde %60 iyileşme ve mobil satışlarda %35 artış gördü. Müşteri memnuniyeti puanları %28 iyileşti ve ortalama sipariş değeri %22 arttı.\"\r\n }\r\n },\r\n \"gallery\": \"Proje Galerisi\",\r\n \"projectDetails\": \"Proje Detayları\",\r\n \"labels\": {\r\n \"client\": \"Müşteri\",\r\n \"date\": \"Tarih\",\r\n \"category\": \"Kategori\",\r\n \"services\": \"Hizmetler\"\r\n },\r\n \"details\": {\r\n \"client\": \"Acme Corporation\",\r\n \"date\": \"Ocak 2024\",\r\n \"category\": \"Web Tasarımı\",\r\n \"services\": [\"UX Tasarımı\", \"UI Tasarımı\", \"Geliştirme\"]\r\n },\r\n \"visitWebsite\": \"Web Sitesini Ziyaret Et\",\r\n \"share\": \"Bu Projeyi Paylaş\",\r\n \"navigation\": {\r\n \"prev\": \"Önceki Proje\",\r\n \"next\": \"Sonraki Proje\"\r\n },\r\n \"prev\": \"Önceki\",\r\n \"next\": \"Sonraki\",\r\n \"allProjects\": \"Tüm Projeler\"\r\n}\r\n"
39
+ }
40
+ ],
41
+ "exports": {
42
+ "types": [],
43
+ "variables": [
44
+ "CaseStudyPage",
45
+ "default"
46
+ ]
47
+ }
48
+ }
@@ -2,10 +2,11 @@
2
2
  "name": "checkout-page",
3
3
  "type": "registry:page",
4
4
  "title": "Checkout Page",
5
- "description": "Multi-step checkout page with customer information form (name, email, phone, address), shipping method selection, payment method options (credit card, PayPal, bank transfer), and order review. Features form validation, order summary sidebar, shipping cost calculation, and place order button. Includes progress indicator for checkout steps.",
5
+ "description": "Full-featured checkout page with customer information form, payment method selection (card, bank transfer, cash on delivery), and online payment provider selection (Stripe, iyzico). Integrates with payment API for creating checkout sessions and fetching bank transfer info. Features form validation, order summary sidebar, and skeleton loading states.",
6
6
  "registryDependencies": [
7
7
  "ecommerce-core",
8
- "animations"
8
+ "animations",
9
+ "api"
9
10
  ],
10
11
  "route": {
11
12
  "path": "/checkout",
@@ -23,19 +24,19 @@
23
24
  "path": "checkout-page/checkout-page.tsx",
24
25
  "type": "registry:page",
25
26
  "target": "$modules$/checkout-page/checkout-page.tsx",
26
- "content": "import { useState } from \"react\";\nimport { Link, useNavigate } from \"react-router\";\nimport { ArrowLeft, CreditCard, Banknote, Truck, Check } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { useTranslation } from \"react-i18next\";\nimport { toast } from \"sonner\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport { customerClient } from \"@/modules/api\";\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn } from \"@/modules/animations\";\n\ninterface Country {\n value: string;\n label: string;\n}\n\ntype PaymentMethod = \"card\" | \"cash\" | \"transfer\";\n\ninterface CheckoutFormData {\n firstName: string;\n lastName: string;\n email: string;\n phone: string;\n address: string;\n city: string;\n postalCode: string;\n country: string;\n notes: string;\n}\n\nconst DEFAULT_COUNTRIES: Country[] = [\n { value: \"US\", label: \"United States\" },\n { value: \"GB\", label: \"United Kingdom\" },\n { value: \"CA\", label: \"Canada\" },\n { value: \"AU\", label: \"Australia\" },\n { value: \"DE\", label: \"Germany\" },\n { value: \"FR\", label: \"France\" },\n { value: \"TR\", label: \"Turkey\" },\n { value: \"JP\", label: \"Japan\" },\n];\n\nexport function CheckoutPage() {\n const { t } = useTranslation(\"checkout-page\");\n const navigate = useNavigate();\n const { state, clearCart } = useCart();\n const { items, total } = state;\n\n const currency = constants.site.currency || \"USD\";\n const shipping = 0;\n const tax = 0;\n const countries = DEFAULT_COUNTRIES;\n\n const getProductPrice = (product: {\n price: number;\n sale_price?: number;\n on_sale?: boolean;\n }) => {\n return product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n };\n\n const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>(\"card\");\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [formData, setFormData] = useState<CheckoutFormData>({\n firstName: \"\",\n lastName: \"\",\n email: \"\",\n phone: \"\",\n address: \"\",\n city: \"\",\n postalCode: \"\",\n country: \"\",\n notes: \"\",\n });\n const [agreedToTerms, setAgreedToTerms] = useState(false);\n\n const finalTotal = total + shipping + tax;\n\n const handleInputChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n const { name, value } = e.target;\n setFormData((prev) => ({ ...prev, [name]: value }));\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!agreedToTerms) return;\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n const orderData = {\n items: items.map((item) => ({\n product_id: item.product.id,\n quantity: item.quantity,\n price: getProductPrice(item.product),\n })),\n shipping_address: {\n first_name: formData.firstName,\n last_name: formData.lastName,\n email: formData.email,\n phone: formData.phone,\n address: formData.address,\n city: formData.city,\n postal_code: formData.postalCode,\n country: formData.country,\n },\n payment_method: paymentMethod,\n notes: formData.notes,\n total: finalTotal,\n };\n\n const response = await customerClient.orders.create(orderData);\n\n toast.success(t(\"orderSuccess\", \"Order placed successfully!\"), {\n description: t(\"orderSuccessDesc\", \"Thank you for your purchase.\"),\n });\n\n clearCart();\n navigate(`/order-success/${response.id}`);\n } catch (err) {\n const errorMessage = getErrorMessage(err, t(\"orderError\", \"Failed to place order. Please try again.\"));\n setError(errorMessage);\n toast.error(t(\"orderErrorTitle\", \"Order Failed\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const paymentMethods = [\n {\n id: \"card\" as PaymentMethod,\n label: t(\"card\", \"Credit/Debit Card\"),\n description: t(\n \"cardDescription\",\n \"Pay securely with your credit or debit card\"\n ),\n icon: CreditCard,\n iconColor: \"text-blue-600\",\n },\n {\n id: \"transfer\" as PaymentMethod,\n label: t(\"transfer\", \"Bank Transfer\"),\n description: t(\n \"transferDescription\",\n \"Transfer payment to our bank account\"\n ),\n icon: Banknote,\n iconColor: \"text-primary\",\n },\n {\n id: \"cash\" as PaymentMethod,\n label: t(\"cash\", \"Cash on Delivery\"),\n description: t(\n \"cashDescription\",\n \"Pay when your order arrives at your doorstep\"\n ),\n icon: Truck,\n iconColor: \"text-green-600 dark:text-green-400\",\n },\n ];\n\n if (items.length === 0) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"max-w-2xl mx-auto text-center\">\n <h1 className=\"text-3xl font-bold mb-4\">\n {t(\"cartEmpty\", \"Your cart is empty\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\n \"cartEmptyDescription\",\n \"Please add items to your cart before proceeding to checkout.\"\n )}\n </p>\n <Button asChild>\n <Link to=\"/products\">\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"flex items-center gap-4 mb-8\">\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <Link to=\"/cart\">\n <ArrowLeft className=\"h-4 w-4\" />\n </Link>\n </Button>\n <div>\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Checkout\")}</h1>\n <p className=\"text-muted-foreground\">\n {t(\"completeOrder\", \"Complete your order\")}\n </p>\n </div>\n </FadeIn>\n\n <form onSubmit={handleSubmit}>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Contact Information */}\n <FadeIn delay={0.1}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"contactInformation\", \"Contact Information\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div>\n <Label htmlFor=\"firstName\">\n {t(\"firstName\", \"First Name\")} *\n </Label>\n <Input\n id=\"firstName\"\n name=\"firstName\"\n value={formData.firstName}\n onChange={handleInputChange}\n required\n />\n </div>\n <div>\n <Label htmlFor=\"lastName\">\n {t(\"lastName\", \"Last Name\")} *\n </Label>\n <Input\n id=\"lastName\"\n name=\"lastName\"\n value={formData.lastName}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n <div>\n <Label htmlFor=\"email\">\n {t(\"email\", \"Email Address\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleInputChange}\n required\n />\n </div>\n <div>\n <Label htmlFor=\"phone\">\n {t(\"phone\", \"Phone Number\")} *\n </Label>\n <Input\n id=\"phone\"\n name=\"phone\"\n type=\"tel\"\n value={formData.phone}\n onChange={handleInputChange}\n required\n />\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Shipping Address */}\n <FadeIn delay={0.2}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"shippingAddress\", \"Shipping Address\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div>\n <Label htmlFor=\"address\">{t(\"address\", \"Address\")} *</Label>\n <Textarea\n id=\"address\"\n name=\"address\"\n value={formData.address}\n onChange={handleInputChange}\n placeholder={t(\n \"addressPlaceholder\",\n \"Street address, apartment, suite, etc.\"\n )}\n required\n />\n </div>\n <div>\n <Label htmlFor=\"country\">{t(\"country\", \"Country\")} *</Label>\n <Select\n value={formData.country}\n onValueChange={(value) =>\n setFormData((prev) => ({ ...prev, country: value }))\n }\n required\n >\n <SelectTrigger id=\"country\">\n <SelectValue\n placeholder={t(\"selectCountry\", \"Select a country\")}\n />\n </SelectTrigger>\n <SelectContent>\n {countries.map((country) => (\n <SelectItem key={country.value} value={country.value}>\n {country.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div>\n <Label htmlFor=\"city\">{t(\"city\", \"City\")} *</Label>\n <Input\n id=\"city\"\n name=\"city\"\n value={formData.city}\n onChange={handleInputChange}\n required\n />\n </div>\n <div>\n <Label htmlFor=\"postalCode\">\n {t(\"postalCode\", \"Postal Code\")} *\n </Label>\n <Input\n id=\"postalCode\"\n name=\"postalCode\"\n value={formData.postalCode}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Payment Method */}\n <FadeIn delay={0.3}>\n <Card>\n <CardHeader>\n <CardTitle>{t(\"paymentMethod\", \"Payment Method\")}</CardTitle>\n </CardHeader>\n <CardContent>\n <RadioGroup\n value={paymentMethod}\n onValueChange={(value) =>\n setPaymentMethod(value as PaymentMethod)\n }\n className=\"space-y-4\"\n >\n {paymentMethods.map((method) => (\n <div\n key={method.id}\n className=\"flex items-center space-x-2 p-4 border rounded-lg\"\n >\n <RadioGroupItem value={method.id} id={method.id} />\n <Label\n htmlFor={method.id}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"flex items-center gap-3\">\n <method.icon\n className={`h-5 w-5 ${method.iconColor}`}\n />\n <div>\n <div className=\"font-medium\">{method.label}</div>\n <div className=\"text-sm text-muted-foreground\">\n {method.description}\n </div>\n </div>\n </div>\n </Label>\n </div>\n ))}\n </RadioGroup>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Order Notes */}\n <FadeIn delay={0.4}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"orderNotesOptional\", \"Order Notes (Optional)\")}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Textarea\n name=\"notes\"\n value={formData.notes}\n onChange={handleInputChange}\n placeholder={t(\n \"orderNotesPlaceholder\",\n \"Special instructions for your order...\"\n )}\n rows={3}\n />\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n\n {/* Order Summary */}\n <FadeIn delay={0.2} className=\"lg:col-span-1\">\n <Card className=\"sticky top-24\">\n <CardHeader>\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-3\">\n {items.map((item) => (\n <div key={item.id} className=\"flex gap-3\">\n <img\n src={\n item.product.images?.[0] ||\n \"/images/placeholder.png\"\n }\n alt={item.product.name}\n className=\"w-12 h-12 object-cover rounded\"\n />\n <div className=\"flex-1 space-y-1\">\n <h4 className=\"text-sm font-medium leading-normal\">\n {item.product.name}\n </h4>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-muted-foreground\">\n {t(\"qty\", \"Qty\")}: {item.quantity}\n </span>\n <span>\n {formatPrice(\n getProductPrice(item.product) * item.quantity,\n currency\n )}\n </span>\n </div>\n </div>\n </div>\n ))}\n </div>\n\n <Separator />\n\n <div className=\"space-y-2\">\n <div className=\"flex justify-between\">\n <span>{t(\"subtotal\", \"Subtotal\")}</span>\n <span>{formatPrice(total, currency)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"shipping\", \"Shipping\")}</span>\n <span>\n {shipping === 0\n ? t(\"free\", \"Free\")\n : formatPrice(shipping, currency)}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"tax\", \"Tax\")}</span>\n <span>{formatPrice(tax, currency)}</span>\n </div>\n </div>\n\n <Separator />\n\n <div className=\"flex justify-between text-lg font-semibold\">\n <span>{t(\"total\", \"Total\")}</span>\n <span>{formatPrice(finalTotal, currency)}</span>\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg\">\n <p className=\"text-red-800 dark:text-red-200 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <div className=\"flex items-start space-x-2\">\n <Checkbox\n id=\"terms\"\n checked={agreedToTerms}\n onCheckedChange={(checked) =>\n setAgreedToTerms(checked as boolean)\n }\n />\n <Label htmlFor=\"terms\" className=\"text-sm leading-relaxed\">\n {t(\"agreeToTermsTextBefore\", \"I agree to the\")}{\" \"}\n <Link\n to=\"/terms\"\n className=\"text-primary hover:underline\"\n >\n {t(\"termsOfService\", \"Terms of Service\")}\n </Link>{\" \"}\n {t(\"and\", \"and\")}{\" \"}\n <Link\n to=\"/privacy\"\n className=\"text-primary hover:underline\"\n >\n {t(\"privacyPolicy\", \"Privacy Policy\")}\n </Link>\n </Label>\n </div>\n\n <Button\n type=\"submit\"\n className=\"w-full\"\n size=\"lg\"\n disabled={!agreedToTerms || isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"processing\", \"Processing...\")}\n </>\n ) : (\n <>\n <Check className=\"w-4 h-4 mr-2\" />\n {paymentMethod === \"card\"\n ? t(\"proceedToPayment\", \"Proceed to Payment\")\n : t(\"placeOrder\", \"Place Order\")}\n </>\n )}\n </Button>\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n </form>\n </div>\n </Layout>\n );\n}\n"
27
+ "content": "import { useState, useEffect } from \"react\";\nimport { Link } from \"react-router\";\nimport { ArrowLeft, CreditCard, Banknote, Truck, Check } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { toast } from \"sonner\";\nimport {\n useCart,\n formatPrice,\n type PaymentMethod,\n type OnlinePaymentProvider,\n getFilteredPaymentMethodConfigs,\n getOnlinePaymentProviders,\n ONLINE_PROVIDER_CONFIGS,\n} from \"@/modules/ecommerce-core\";\nimport { customerClient } from \"@/modules/api\";\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn } from \"@/modules/animations\";\n\ninterface Country {\n value: string;\n label: string;\n}\n\ninterface CheckoutFormData {\n firstName: string;\n lastName: string;\n email: string;\n phone: string;\n address: string;\n city: string;\n postalCode: string;\n country: string;\n notes: string;\n}\n\ninterface BankTransferInfo {\n bank_name: string;\n bank_account_name: string;\n iban: string;\n}\n\nconst DEFAULT_COUNTRIES: Country[] = [\n { value: \"US\", label: \"United States\" },\n { value: \"GB\", label: \"United Kingdom\" },\n { value: \"CA\", label: \"Canada\" },\n { value: \"AU\", label: \"Australia\" },\n { value: \"DE\", label: \"Germany\" },\n { value: \"FR\", label: \"France\" },\n { value: \"IT\", label: \"Italy\" },\n { value: \"ES\", label: \"Spain\" },\n { value: \"NL\", label: \"Netherlands\" },\n { value: \"TR\", label: \"Turkey\" },\n { value: \"JP\", label: \"Japan\" },\n];\n\nexport function CheckoutPage() {\n const { t } = useTranslation(\"checkout-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Checkout\") });\n const { state, clearCart } = useCart();\n const { items, total } = state;\n\n const currency = (constants as any).site?.currency || \"USD\";\n const taxRate = (constants as any).payments?.taxRate || 0;\n const freeShippingThreshold = (constants as any).payments?.freeShippingThreshold || 0;\n const shippingCost = (constants as any).shipping?.domesticShipping?.standard?.cost || 0;\n\n // Calculate shipping and tax\n const shipping = total >= freeShippingThreshold ? 0 : shippingCost;\n const tax = total * taxRate;\n\n const countries = DEFAULT_COUNTRIES;\n\n // Get available payment methods and providers from config\n const availablePaymentMethods = getFilteredPaymentMethodConfigs();\n const availableProviders = getOnlinePaymentProviders();\n\n const getProductPrice = (product: {\n price: number;\n sale_price?: number;\n on_sale?: boolean;\n }) => {\n return product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n };\n\n const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>(\n availablePaymentMethods[0]?.id || \"card\"\n );\n const [selectedProvider, setSelectedProvider] = useState<OnlinePaymentProvider>(\n availableProviders[0] || \"stripe\"\n );\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [formData, setFormData] = useState<CheckoutFormData>({\n firstName: \"\",\n lastName: \"\",\n email: \"\",\n phone: \"\",\n address: \"\",\n city: \"\",\n postalCode: \"\",\n country: \"\",\n notes: \"\",\n });\n const [agreedToTerms, setAgreedToTerms] = useState(false);\n\n // Bank transfer info state\n const [bankInfo, setBankInfo] = useState<BankTransferInfo | null>(null);\n const [isBankInfoLoading, setIsBankInfoLoading] = useState(false);\n const [bankInfoError, setBankInfoError] = useState<string | null>(null);\n\n const finalTotal = total + shipping + tax;\n\n // Fetch bank info when transfer is selected\n useEffect(() => {\n if (paymentMethod === \"transfer\") {\n const fetchBankInfo = async () => {\n setIsBankInfoLoading(true);\n setBankInfoError(null);\n try {\n const info = await (customerClient as any).payment.getBankTransferInfo();\n setBankInfo(info);\n } catch (err: any) {\n setBankInfoError(\n err.message || t(\"bankInfoError\", \"Failed to load bank information\")\n );\n } finally {\n setIsBankInfoLoading(false);\n }\n };\n fetchBankInfo();\n }\n }, [paymentMethod, t]);\n\n const handleInputChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n const { name, value } = e.target;\n setFormData((prev) => ({ ...prev, [name]: value }));\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!agreedToTerms) {\n toast.error(t(\"agreeToTermsError\", \"Please agree to the terms and conditions\"));\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n // Determine payment type based on selection\n let paymentType: \"stripe\" | \"iyzico\" | \"bank_transfer\" | \"cash_on_delivery\";\n\n if (paymentMethod === \"card\") {\n paymentType = selectedProvider;\n } else if (paymentMethod === \"transfer\") {\n paymentType = \"bank_transfer\";\n } else {\n paymentType = \"cash_on_delivery\";\n }\n\n // Save checkout data to localStorage for success page\n const checkoutData = {\n items: items,\n total: finalTotal,\n customerInfo: formData,\n paymentMethod,\n paymentProvider: paymentType,\n };\n localStorage.setItem(\"pending_checkout\", JSON.stringify(checkoutData));\n\n // Build product data for checkout\n const productData = items.map((item) => {\n const price = getProductPrice(item.product);\n const qty = item.quantity || 1;\n\n return {\n quantity: qty,\n name: item.product.name || \"Product\",\n description: item.product.description || item.product.name || \"Product\",\n amount: Math.round(price * 100), // Convert to cents\n img: item.product.images?.[0] || \"/images/placeholder.png\",\n optionals: {\n productId: item.product.id,\n },\n };\n });\n\n // Tax amount in cents\n const taxAmountInCents = tax && !isNaN(tax) ? Math.round(tax * 100) : undefined;\n\n // Create checkout session\n const response = await (customerClient as any).payment.createCheckout({\n currency: currency.toLowerCase(),\n taxAmount: taxAmountInCents,\n paymentType: paymentType,\n productData,\n contactData: {\n firstname: formData.firstName,\n lastname: formData.lastName,\n email: formData.email,\n phone: formData.phone,\n },\n shippingData: {\n address: formData.address,\n country: formData.country,\n city: formData.city,\n zip: formData.postalCode,\n },\n });\n\n // Clear cart and redirect to payment URL\n clearCart();\n window.location.href = response.url;\n } catch (err) {\n const errorMessage = getErrorMessage(err, t(\"orderError\", \"Failed to place order. Please try again.\"));\n setError(errorMessage);\n toast.error(t(\"orderErrorTitle\", \"Order Failed\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // Get icon component based on payment method\n const getPaymentIcon = (iconName: string) => {\n switch (iconName) {\n case \"CreditCard\":\n return CreditCard;\n case \"Banknote\":\n return Banknote;\n case \"Truck\":\n return Truck;\n default:\n return CreditCard;\n }\n };\n\n // Get icon color based on payment method\n const getIconColor = (methodId: string) => {\n switch (methodId) {\n case \"card\":\n return \"text-blue-600\";\n case \"transfer\":\n return \"text-primary\";\n case \"cash\":\n return \"text-green-600 dark:text-green-400\";\n default:\n return \"text-primary\";\n }\n };\n\n if (items.length === 0) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"max-w-2xl mx-auto text-center\">\n <h1 className=\"text-3xl font-bold mb-4\">\n {t(\"cartEmpty\", \"Your cart is empty\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\n \"cartEmptyDescription\",\n \"Please add items to your cart before proceeding to checkout.\"\n )}\n </p>\n <Button asChild>\n <Link to=\"/products\">\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"flex items-center gap-4 mb-8\">\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <Link to=\"/cart\">\n <ArrowLeft className=\"h-4 w-4\" />\n </Link>\n </Button>\n <div>\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Checkout\")}</h1>\n <p className=\"text-muted-foreground\">\n {t(\"completeOrder\", \"Complete your order\")}\n </p>\n </div>\n </FadeIn>\n\n <form onSubmit={handleSubmit}>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Contact Information */}\n <FadeIn delay={0.1}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"contactInformation\", \"Contact Information\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"firstName\">\n {t(\"firstName\", \"First Name\")} *\n </Label>\n <Input\n id=\"firstName\"\n name=\"firstName\"\n value={formData.firstName}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"lastName\">\n {t(\"lastName\", \"Last Name\")} *\n </Label>\n <Input\n id=\"lastName\"\n name=\"lastName\"\n value={formData.lastName}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">\n {t(\"email\", \"Email Address\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"phone\">\n {t(\"phone\", \"Phone Number\")} *\n </Label>\n <Input\n id=\"phone\"\n name=\"phone\"\n type=\"tel\"\n value={formData.phone}\n onChange={handleInputChange}\n required\n />\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Shipping Address */}\n <FadeIn delay={0.2}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"shippingAddress\", \"Shipping Address\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"address\">{t(\"address\", \"Address\")} *</Label>\n <Textarea\n id=\"address\"\n name=\"address\"\n value={formData.address}\n onChange={handleInputChange}\n placeholder={t(\n \"addressPlaceholder\",\n \"Street address, apartment, suite, etc.\"\n )}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"country\">{t(\"country\", \"Country\")} *</Label>\n <Select\n value={formData.country}\n onValueChange={(value) =>\n setFormData((prev) => ({ ...prev, country: value }))\n }\n required\n >\n <SelectTrigger id=\"country\">\n <SelectValue\n placeholder={t(\"selectCountry\", \"Select a country\")}\n />\n </SelectTrigger>\n <SelectContent>\n {countries.map((country) => (\n <SelectItem key={country.value} value={country.value}>\n {country.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"city\">{t(\"city\", \"City\")} *</Label>\n <Input\n id=\"city\"\n name=\"city\"\n value={formData.city}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"postalCode\">\n {t(\"postalCode\", \"Postal Code\")} *\n </Label>\n <Input\n id=\"postalCode\"\n name=\"postalCode\"\n value={formData.postalCode}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Payment Method */}\n <FadeIn delay={0.3}>\n <Card>\n <CardHeader>\n <CardTitle>{t(\"paymentMethod\", \"Payment Method\")}</CardTitle>\n </CardHeader>\n <CardContent>\n <RadioGroup\n value={paymentMethod}\n onValueChange={(value) =>\n setPaymentMethod(value as PaymentMethod)\n }\n className=\"space-y-4\"\n >\n {availablePaymentMethods.map((method) => {\n const IconComponent = getPaymentIcon(method.icon);\n const iconColor = getIconColor(method.id);\n\n return (\n <div\n key={method.id}\n className=\"flex items-center space-x-2 p-4 border rounded-lg\"\n >\n <RadioGroupItem value={method.id} id={method.id} />\n <Label\n htmlFor={method.id}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"flex items-center gap-3\">\n <IconComponent\n className={`h-5 w-5 ${iconColor}`}\n />\n <div>\n <div className=\"font-medium\">\n {t(method.id, method.label)}\n </div>\n <div className=\"text-sm text-muted-foreground\">\n {t(`${method.id}Description`, method.description)}\n </div>\n </div>\n </div>\n </Label>\n </div>\n );\n })}\n </RadioGroup>\n\n {/* Bank Transfer Details */}\n {paymentMethod === \"transfer\" && (\n <div className=\"mt-4 p-4 bg-primary/10 rounded-lg border border-primary/20\">\n <h4 className=\"font-medium mb-2\">\n {t(\"bankTransferDetailsTitle\", \"Bank Transfer Details\")}:\n </h4>\n {isBankInfoLoading ? (\n <div className=\"text-sm space-y-2\">\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-full\" />\n </div>\n ) : bankInfoError ? (\n <p className=\"text-sm text-red-600\">{bankInfoError}</p>\n ) : bankInfo ? (\n <div className=\"text-sm space-y-1\">\n <p>\n <strong>{t(\"bank\", \"Bank\")}:</strong> {bankInfo.bank_name}\n </p>\n <p>\n <strong>{t(\"accountName\", \"Account Name\")}:</strong>{\" \"}\n {bankInfo.bank_account_name}\n </p>\n <p>\n <strong>IBAN:</strong> {bankInfo.iban}\n </p>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t(\"bankInfoNotAvailable\", \"Bank account information not available\")}\n </p>\n )}\n </div>\n )}\n\n {/* Card Payment - Provider Selection */}\n {paymentMethod === \"card\" && availableProviders.length > 1 && (\n <div className=\"mt-4 space-y-4\">\n <div className=\"p-4 bg-blue-50 dark:bg-blue-950/30 rounded-lg border border-blue-200 dark:border-blue-800\">\n <h4 className=\"font-medium text-blue-900 dark:text-blue-100 mb-3\">\n {t(\"selectPaymentProvider\", \"Select Payment Provider\")}\n </h4>\n <RadioGroup\n value={selectedProvider}\n onValueChange={(value) =>\n setSelectedProvider(value as OnlinePaymentProvider)\n }\n className=\"space-y-2\"\n >\n {availableProviders.map((provider) => (\n <div\n key={provider}\n className=\"flex items-center space-x-2 p-3 bg-background rounded border\"\n >\n <RadioGroupItem\n value={provider}\n id={`provider-${provider}`}\n />\n <Label\n htmlFor={`provider-${provider}`}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"font-medium\">\n {t(`provider_${provider}_label`, ONLINE_PROVIDER_CONFIGS[provider].label)}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {t(`provider_${provider}_description`, ONLINE_PROVIDER_CONFIGS[provider].description)}\n </div>\n </Label>\n </div>\n ))}\n </RadioGroup>\n <p className=\"text-sm text-blue-700 dark:text-blue-300 mt-3\">\n {t(\n \"creditCardRedirectNote\",\n \"You will be redirected to the secure payment page to complete your purchase.\"\n )}\n </p>\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Order Notes */}\n <FadeIn delay={0.4}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"orderNotesOptional\", \"Order Notes (Optional)\")}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Textarea\n name=\"notes\"\n value={formData.notes}\n onChange={handleInputChange}\n placeholder={t(\n \"orderNotesPlaceholder\",\n \"Special instructions for your order...\"\n )}\n rows={3}\n />\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n\n {/* Order Summary */}\n <FadeIn delay={0.2} className=\"lg:col-span-1\">\n <Card className=\"sticky top-24\">\n <CardHeader>\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-3\">\n {items.map((item) => (\n <div key={item.id} className=\"flex gap-3\">\n <img\n src={\n item.product.images?.[0] ||\n \"/images/placeholder.png\"\n }\n alt={item.product.name}\n className=\"w-12 h-12 object-cover rounded\"\n />\n <div className=\"flex-1 space-y-1\">\n <h4 className=\"text-sm font-medium leading-normal\">\n {item.product.name}\n </h4>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-muted-foreground\">\n {t(\"qty\", \"Qty\")}: {item.quantity}\n </span>\n <span>\n {formatPrice(\n getProductPrice(item.product) * item.quantity,\n currency\n )}\n </span>\n </div>\n </div>\n </div>\n ))}\n </div>\n\n <Separator />\n\n <div className=\"space-y-2\">\n <div className=\"flex justify-between\">\n <span>{t(\"subtotal\", \"Subtotal\")}</span>\n <span>{formatPrice(total, currency)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"shipping\", \"Shipping\")}</span>\n <span>\n {shipping === 0\n ? t(\"free\", \"Free\")\n : formatPrice(shipping, currency)}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"tax\", \"Tax\")}</span>\n <span>{formatPrice(tax, currency)}</span>\n </div>\n </div>\n\n <Separator />\n\n <div className=\"flex justify-between text-lg font-semibold\">\n <span>{t(\"total\", \"Total\")}</span>\n <span>{formatPrice(finalTotal, currency)}</span>\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg\">\n <p className=\"text-red-800 dark:text-red-200 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"terms\"\n checked={agreedToTerms}\n onCheckedChange={(checked) =>\n setAgreedToTerms(checked as boolean)\n }\n />\n <span className=\"text-sm\">\n {t(\"agreeToTermsTextBefore\", \"I agree to the\")}{\" \"}\n <Link\n to=\"/terms\"\n className=\"text-primary hover:underline\"\n >\n {t(\"termsOfService\", \"Terms of Service\")}\n </Link>{\" \"}\n {t(\"and\", \"and\")}{\" \"}\n <Link\n to=\"/privacy\"\n className=\"text-primary hover:underline\"\n >\n {t(\"privacyPolicy\", \"Privacy Policy\")}\n </Link>\n </span>\n </div>\n\n <Button\n type=\"submit\"\n className=\"w-full\"\n size=\"lg\"\n disabled={!agreedToTerms || isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"processing\", \"Processing...\")}\n </>\n ) : (\n <>\n <Check className=\"w-4 h-4 mr-2\" />\n {paymentMethod === \"card\"\n ? t(\"proceedToPayment\", \"Proceed to Payment\")\n : t(\"placeOrder\", \"Place Order\")}\n </>\n )}\n </Button>\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n </form>\n </div>\n </Layout>\n );\n}\n\nexport default CheckoutPage;\n"
27
28
  },
28
29
  {
29
30
  "path": "checkout-page/lang/en.json",
30
31
  "type": "registry:lang",
31
32
  "target": "$modules$/checkout-page/lang/en.json",
32
- "content": "{\r\n \"title\": \"Checkout\",\r\n \"completeOrder\": \"Complete your order\",\r\n \"contactInformation\": \"Contact Information\",\r\n \"shippingAddress\": \"Shipping Address\",\r\n \"paymentMethod\": \"Payment Method\",\r\n \"orderSummary\": \"Order Summary\",\r\n \"placeOrder\": \"Place Order\",\r\n \"processing\": \"Processing...\",\r\n \"firstName\": \"First Name\",\r\n \"lastName\": \"Last Name\",\r\n \"email\": \"Email Address\",\r\n \"phone\": \"Phone Number\",\r\n \"address\": \"Address\",\r\n \"city\": \"City\",\r\n \"postalCode\": \"Postal Code\",\r\n \"country\": \"Country\",\r\n \"selectCountry\": \"Select a country\",\r\n \"qty\": \"Qty\",\r\n \"subtotal\": \"Subtotal\",\r\n \"shipping\": \"Shipping\",\r\n \"free\": \"Free\",\r\n \"tax\": \"Tax\",\r\n \"total\": \"Total\",\r\n \"and\": \"and\",\r\n \"termsOfService\": \"Terms of Service\",\r\n \"privacyPolicy\": \"Privacy Policy\",\r\n \"addressPlaceholder\": \"Street address, apartment, suite, etc.\",\r\n \"orderNotesOptional\": \"Order Notes (Optional)\",\r\n \"orderNotesPlaceholder\": \"Special instructions for your order...\",\r\n \"agreeToTermsTextBefore\": \"I agree to the\",\r\n \"agreeToTermsError\": \"Please agree to the terms and conditions\",\r\n \"card\": \"Credit/Debit Card\",\r\n \"transfer\": \"Bank Transfer\",\r\n \"cash\": \"Cash on Delivery\",\r\n \"cardDescription\": \"Pay securely with your credit or debit card\",\r\n \"transferDescription\": \"Transfer payment to our bank account\",\r\n \"cashDescription\": \"Pay when your order arrives at your doorstep\",\r\n \"proceedToPayment\": \"Proceed to Payment\",\r\n \"cartEmpty\": \"Your cart is empty\",\r\n \"cartEmptyDescription\": \"Please add items to your cart before proceeding to checkout.\",\r\n \"continueShopping\": \"Continue Shopping\"\r\n}\r\n"
33
+ "content": "{\r\n \"pageTitle\": \"Checkout\",\r\n \"title\": \"Checkout\",\r\n \"completeOrder\": \"Complete your order\",\r\n \"contactInformation\": \"Contact Information\",\r\n \"shippingAddress\": \"Shipping Address\",\r\n \"paymentMethod\": \"Payment Method\",\r\n \"orderSummary\": \"Order Summary\",\r\n \"placeOrder\": \"Place Order\",\r\n \"processing\": \"Processing...\",\r\n \"firstName\": \"First Name\",\r\n \"lastName\": \"Last Name\",\r\n \"email\": \"Email Address\",\r\n \"phone\": \"Phone Number\",\r\n \"address\": \"Address\",\r\n \"city\": \"City\",\r\n \"postalCode\": \"Postal Code\",\r\n \"country\": \"Country\",\r\n \"selectCountry\": \"Select a country\",\r\n \"qty\": \"Qty\",\r\n \"subtotal\": \"Subtotal\",\r\n \"shipping\": \"Shipping\",\r\n \"free\": \"Free\",\r\n \"tax\": \"Tax\",\r\n \"total\": \"Total\",\r\n \"and\": \"and\",\r\n \"termsOfService\": \"Terms of Service\",\r\n \"privacyPolicy\": \"Privacy Policy\",\r\n \"addressPlaceholder\": \"Street address, apartment, suite, etc.\",\r\n \"orderNotesOptional\": \"Order Notes (Optional)\",\r\n \"orderNotesPlaceholder\": \"Special instructions for your order...\",\r\n \"agreeToTermsTextBefore\": \"I agree to the\",\r\n \"agreeToTermsError\": \"Please agree to the terms and conditions\",\r\n \"card\": \"Credit/Debit Card\",\r\n \"transfer\": \"Bank Transfer\",\r\n \"cash\": \"Cash on Delivery\",\r\n \"cardDescription\": \"Pay securely with your credit or debit card\",\r\n \"transferDescription\": \"Transfer payment to our bank account\",\r\n \"cashDescription\": \"Pay when your order arrives at your doorstep\",\r\n \"proceedToPayment\": \"Proceed to Payment\",\r\n \"cartEmpty\": \"Your cart is empty\",\r\n \"cartEmptyDescription\": \"Please add items to your cart before proceeding to checkout.\",\r\n \"continueShopping\": \"Continue Shopping\"\r\n}\r\n"
33
34
  },
34
35
  {
35
36
  "path": "checkout-page/lang/tr.json",
36
37
  "type": "registry:lang",
37
38
  "target": "$modules$/checkout-page/lang/tr.json",
38
- "content": "{\r\n \"title\": \"Ödeme\",\r\n \"completeOrder\": \"Siparişinizi tamamlayın\",\r\n \"contactInformation\": \"İletişim Bilgileri\",\r\n \"shippingAddress\": \"Teslimat Adresi\",\r\n \"paymentMethod\": \"Ödeme Yöntemi\",\r\n \"orderSummary\": \"Sipariş Özeti\",\r\n \"placeOrder\": \"Siparişi Tamamla\",\r\n \"processing\": \"İşleniyor...\",\r\n \"firstName\": \"Ad\",\r\n \"lastName\": \"Soyad\",\r\n \"email\": \"E-posta Adresi\",\r\n \"phone\": \"Telefon Numarası\",\r\n \"address\": \"Adres\",\r\n \"city\": \"Şehir\",\r\n \"postalCode\": \"Posta Kodu\",\r\n \"country\": \"Ülke\",\r\n \"selectCountry\": \"Ülke seçin\",\r\n \"qty\": \"Adet\",\r\n \"subtotal\": \"Ara Toplam\",\r\n \"shipping\": \"Kargo\",\r\n \"free\": \"Ücretsiz\",\r\n \"tax\": \"Vergi\",\r\n \"total\": \"Toplam\",\r\n \"and\": \"ve\",\r\n \"termsOfService\": \"Hizmet Koşulları\",\r\n \"privacyPolicy\": \"Gizlilik Politikası\",\r\n \"addressPlaceholder\": \"Sokak adresi, daire, suit, vb.\",\r\n \"orderNotesOptional\": \"Sipariş Notları (Opsiyonel)\",\r\n \"orderNotesPlaceholder\": \"Siparişiniz için özel talimatlar...\",\r\n \"agreeToTermsTextBefore\": \"Kabul ediyorum:\",\r\n \"agreeToTermsError\": \"Lütfen hizmet koşullarını kabul edin\",\r\n \"card\": \"Kredi/Banka Kartı\",\r\n \"transfer\": \"Banka Havalesi\",\r\n \"cash\": \"Kapıda Ödeme\",\r\n \"cardDescription\": \"Kredi veya banka kartınızla güvenli ödeme\",\r\n \"transferDescription\": \"Banka hesabımıza ödeme transferi\",\r\n \"cashDescription\": \"Siparişiniz kapınıza geldiğinde ödeme\",\r\n \"proceedToPayment\": \"Ödemeye Geç\",\r\n \"cartEmpty\": \"Sepetiniz boş\",\r\n \"cartEmptyDescription\": \"Ödemeye geçmeden önce lütfen sepetinize ürün ekleyin.\",\r\n \"continueShopping\": \"Alışverişe Devam Et\"\r\n}\r\n"
39
+ "content": "{\r\n \"pageTitle\": \"Ödeme\",\r\n \"title\": \"Ödeme\",\r\n \"completeOrder\": \"Siparişinizi tamamlayın\",\r\n \"contactInformation\": \"İletişim Bilgileri\",\r\n \"shippingAddress\": \"Teslimat Adresi\",\r\n \"paymentMethod\": \"Ödeme Yöntemi\",\r\n \"orderSummary\": \"Sipariş Özeti\",\r\n \"placeOrder\": \"Siparişi Tamamla\",\r\n \"processing\": \"İşleniyor...\",\r\n \"firstName\": \"Ad\",\r\n \"lastName\": \"Soyad\",\r\n \"email\": \"E-posta Adresi\",\r\n \"phone\": \"Telefon Numarası\",\r\n \"address\": \"Adres\",\r\n \"city\": \"Şehir\",\r\n \"postalCode\": \"Posta Kodu\",\r\n \"country\": \"Ülke\",\r\n \"selectCountry\": \"Ülke seçin\",\r\n \"qty\": \"Adet\",\r\n \"subtotal\": \"Ara Toplam\",\r\n \"shipping\": \"Kargo\",\r\n \"free\": \"Ücretsiz\",\r\n \"tax\": \"Vergi\",\r\n \"total\": \"Toplam\",\r\n \"and\": \"ve\",\r\n \"termsOfService\": \"Hizmet Koşulları\",\r\n \"privacyPolicy\": \"Gizlilik Politikası\",\r\n \"addressPlaceholder\": \"Sokak adresi, daire, suit, vb.\",\r\n \"orderNotesOptional\": \"Sipariş Notları (Opsiyonel)\",\r\n \"orderNotesPlaceholder\": \"Siparişiniz için özel talimatlar...\",\r\n \"agreeToTermsTextBefore\": \"Kabul ediyorum:\",\r\n \"agreeToTermsError\": \"Lütfen hizmet koşullarını kabul edin\",\r\n \"card\": \"Kredi/Banka Kartı\",\r\n \"transfer\": \"Banka Havalesi\",\r\n \"cash\": \"Kapıda Ödeme\",\r\n \"cardDescription\": \"Kredi veya banka kartınızla güvenli ödeme\",\r\n \"transferDescription\": \"Banka hesabımıza ödeme transferi\",\r\n \"cashDescription\": \"Siparişiniz kapınıza geldiğinde ödeme\",\r\n \"proceedToPayment\": \"Ödemeye Geç\",\r\n \"cartEmpty\": \"Sepetiniz boş\",\r\n \"cartEmptyDescription\": \"Ödemeye geçmeden önce lütfen sepetinize ürün ekleyin.\",\r\n \"continueShopping\": \"Alışverişe Devam Et\"\r\n}\r\n"
39
40
  }
40
41
  ],
41
42
  "exports": {
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "coming-soon-page-minimal",
3
+ "type": "registry:page",
4
+ "title": "Coming Soon Page (Minimal)",
5
+ "description": "Minimalist coming soon page with large typography, days countdown, and simple email input. Clean design with focus on the launch date number.",
6
+ "registryDependencies": [],
7
+ "route": {
8
+ "path": "/coming-soon-minimal",
9
+ "componentName": "ComingSoonPageMinimal"
10
+ },
11
+ "usage": "import { ComingSoonPageMinimal } from '@/modules/coming-soon-page-minimal';\n\n<Route path=\"/coming-soon\" element={<ComingSoonPageMinimal />} />\n\n• Large countdown number display\n• Minimal email signup\n• Customizable launch date",
12
+ "files": [
13
+ {
14
+ "path": "coming-soon-page-minimal/index.ts",
15
+ "type": "registry:index",
16
+ "target": "$modules$/coming-soon-page-minimal/index.ts",
17
+ "content": "export * from \"./coming-soon-page-minimal\";\r\nexport { default } from \"./coming-soon-page-minimal\";\r\n"
18
+ },
19
+ {
20
+ "path": "coming-soon-page-minimal/coming-soon-page-minimal.tsx",
21
+ "type": "registry:page",
22
+ "target": "$modules$/coming-soon-page-minimal/coming-soon-page-minimal.tsx",
23
+ "content": "import { useState, useEffect, useMemo } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { ArrowRight } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface ComingSoonPageMinimalProps {\r\n launchDate?: Date;\r\n className?: string;\r\n}\r\n\r\n// Default launch date: 30 days from initial load\r\nconst DEFAULT_LAUNCH_DATE = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);\r\n\r\nexport function ComingSoonPageMinimal({\r\n launchDate: launchDateProp,\r\n className\r\n}: ComingSoonPageMinimalProps) {\r\n const { t } = useTranslation(\"coming-soon-page-minimal\");\r\n usePageTitle({ title: t(\"title\", \"Coming Soon\") });\r\n\r\n const launchDate = useMemo(() => launchDateProp ?? DEFAULT_LAUNCH_DATE, [launchDateProp]);\r\n\r\n const [email, setEmail] = useState(\"\");\r\n const [isSubmitted, setIsSubmitted] = useState(false);\r\n const [days, setDays] = useState(0);\r\n\r\n useEffect(() => {\r\n const calculateDays = () => {\r\n const difference = launchDate.getTime() - new Date().getTime();\r\n if (difference > 0) {\r\n setDays(Math.floor(difference / (1000 * 60 * 60 * 24)));\r\n }\r\n };\r\n calculateDays();\r\n const timer = setInterval(calculateDays, 60000);\r\n return () => clearInterval(timer);\r\n }, [launchDate]);\r\n\r\n const handleSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitted(true);\r\n setEmail(\"\");\r\n };\r\n\r\n return (\r\n <div className={cn(\r\n \"min-h-screen flex flex-col items-center justify-center bg-background px-4\",\r\n className\r\n )}>\r\n <div className=\"max-w-3xl mx-auto text-center\">\r\n {/* Large countdown number */}\r\n <div className=\"mb-8\">\r\n <span className=\"text-[12rem] md:text-[16rem] lg:text-[20rem] font-bold leading-none text-primary/10\">\r\n {days}\r\n </span>\r\n </div>\r\n\r\n {/* Heading overlaid */}\r\n <div className=\"-mt-32 md:-mt-48 lg:-mt-64 relative z-10 mb-12\">\r\n <p className=\"text-sm uppercase tracking-[0.3em] text-muted-foreground mb-4\">\r\n {t(\"daysLeft\", \"days until launch\")}\r\n </p>\r\n <h1 className=\"text-4xl md:text-6xl lg:text-7xl font-bold text-foreground mb-6\">\r\n {t(\"heading\", \"Coming Soon\")}\r\n </h1>\r\n <p className=\"text-lg text-muted-foreground max-w-lg mx-auto\">\r\n {t(\"description\", \"We're crafting something beautiful. Leave your email to get notified.\")}\r\n </p>\r\n </div>\r\n\r\n {/* Email signup */}\r\n <div className=\"max-w-sm mx-auto\">\r\n {!isSubmitted ? (\r\n <form onSubmit={handleSubmit} className=\"relative\">\r\n <Input\r\n type=\"email\"\r\n value={email}\r\n onChange={(e) => setEmail(e.target.value)}\r\n placeholder={t(\"emailPlaceholder\", \"your@email.com\")}\r\n required\r\n className=\"h-14 pr-14 text-base\"\r\n />\r\n <Button\r\n type=\"submit\"\r\n size=\"icon\"\r\n className=\"absolute right-2 top-1/2 -translate-y-1/2 h-10 w-10 rounded-full\"\r\n >\r\n <ArrowRight className=\"h-5 w-5\" />\r\n </Button>\r\n </form>\r\n ) : (\r\n <p className=\"text-primary font-medium py-4\">\r\n {t(\"success\", \"We'll be in touch!\")}\r\n </p>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Bottom decoration line */}\r\n <div className=\"absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-transparent via-primary/50 to-transparent\" />\r\n </div>\r\n );\r\n}\r\n\r\nexport default ComingSoonPageMinimal;\r\n"
24
+ },
25
+ {
26
+ "path": "coming-soon-page-minimal/lang/en.json",
27
+ "type": "registry:lang",
28
+ "target": "$modules$/coming-soon-page-minimal/lang/en.json",
29
+ "content": "{\r\n \"title\": \"Coming Soon\",\r\n \"daysLeft\": \"days until launch\",\r\n \"heading\": \"Coming Soon\",\r\n \"description\": \"Ask Promake to customize this description. We're crafting something beautiful.\",\r\n \"emailPlaceholder\": \"your@email.com\",\r\n \"success\": \"We'll be in touch!\"\r\n}\r\n"
30
+ },
31
+ {
32
+ "path": "coming-soon-page-minimal/lang/tr.json",
33
+ "type": "registry:lang",
34
+ "target": "$modules$/coming-soon-page-minimal/lang/tr.json",
35
+ "content": "{\r\n \"title\": \"Yakında\",\r\n \"daysLeft\": \"gün kaldı\",\r\n \"heading\": \"Yakında\",\r\n \"description\": \"Güzel bir şey hazırlıyoruz. Haberdar olmak için e-postanızı bırakın.\",\r\n \"emailPlaceholder\": \"ornek@email.com\",\r\n \"success\": \"Sizinle iletişime geçeceğiz!\"\r\n}\r\n"
36
+ }
37
+ ],
38
+ "exports": {
39
+ "types": [],
40
+ "variables": [
41
+ "ComingSoonPageMinimal",
42
+ "default"
43
+ ]
44
+ }
45
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "coming-soon-page",
3
+ "type": "registry:page",
4
+ "title": "Coming Soon Page",
5
+ "description": "Full-featured coming soon page with countdown timer, email signup form, social links, and gradient background. Includes FadeIn and ScaleUp animations. Perfect for product launches and pre-release marketing.",
6
+ "registryDependencies": [
7
+ "animations"
8
+ ],
9
+ "route": {
10
+ "path": "/coming-soon",
11
+ "componentName": "ComingSoonPage"
12
+ },
13
+ "usage": "import { ComingSoonPage } from '@/modules/coming-soon-page';\n\n<Route path=\"/coming-soon\" element={<ComingSoonPage />} />\n\n• Customizable launch date via launchDate prop\n• Email signup with success state\n• Social links (Twitter, Instagram, GitHub, LinkedIn)",
14
+ "files": [
15
+ {
16
+ "path": "coming-soon-page/index.ts",
17
+ "type": "registry:index",
18
+ "target": "$modules$/coming-soon-page/index.ts",
19
+ "content": "export * from \"./coming-soon-page\";\r\nexport { default } from \"./coming-soon-page\";\r\n"
20
+ },
21
+ {
22
+ "path": "coming-soon-page/coming-soon-page.tsx",
23
+ "type": "registry:page",
24
+ "target": "$modules$/coming-soon-page/coming-soon-page.tsx",
25
+ "content": "import { useState, useEffect, useMemo } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Mail, Twitter, Instagram, Github, Linkedin } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { FadeIn, ScaleUp } from \"@/modules/animations\";\r\n\r\ninterface ComingSoonPageProps {\r\n launchDate?: Date;\r\n className?: string;\r\n}\r\n\r\ninterface TimeLeft {\r\n days: number;\r\n hours: number;\r\n minutes: number;\r\n seconds: number;\r\n}\r\n\r\n// Default launch date: 30 days from initial load\r\nconst DEFAULT_LAUNCH_DATE = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);\r\n\r\nexport function ComingSoonPage({\r\n launchDate: launchDateProp,\r\n className\r\n}: ComingSoonPageProps) {\r\n const { t } = useTranslation(\"coming-soon-page\");\r\n usePageTitle({ title: t(\"title\", \"Coming Soon\") });\r\n\r\n const launchDate = useMemo(() => launchDateProp ?? DEFAULT_LAUNCH_DATE, [launchDateProp]);\r\n\r\n const [email, setEmail] = useState(\"\");\r\n const [isSubmitted, setIsSubmitted] = useState(false);\r\n const [timeLeft, setTimeLeft] = useState<TimeLeft>({ days: 0, hours: 0, minutes: 0, seconds: 0 });\r\n\r\n useEffect(() => {\r\n const calculateTimeLeft = () => {\r\n const difference = launchDate.getTime() - new Date().getTime();\r\n\r\n if (difference > 0) {\r\n setTimeLeft({\r\n days: Math.floor(difference / (1000 * 60 * 60 * 24)),\r\n hours: Math.floor((difference / (1000 * 60 * 60)) % 24),\r\n minutes: Math.floor((difference / 1000 / 60) % 60),\r\n seconds: Math.floor((difference / 1000) % 60),\r\n });\r\n }\r\n };\r\n\r\n calculateTimeLeft();\r\n const timer = setInterval(calculateTimeLeft, 1000);\r\n return () => clearInterval(timer);\r\n }, [launchDate]);\r\n\r\n const handleSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n // Handle email submission\r\n setIsSubmitted(true);\r\n setEmail(\"\");\r\n };\r\n\r\n const socialLinks = [\r\n { icon: Twitter, href: \"#\", label: \"Twitter\" },\r\n { icon: Instagram, href: \"#\", label: \"Instagram\" },\r\n { icon: Github, href: \"#\", label: \"GitHub\" },\r\n { icon: Linkedin, href: \"#\", label: \"LinkedIn\" },\r\n ];\r\n\r\n const timeUnits = [\r\n { value: timeLeft.days, label: t(\"days\", \"Days\") },\r\n { value: timeLeft.hours, label: t(\"hours\", \"Hours\") },\r\n { value: timeLeft.minutes, label: t(\"minutes\", \"Minutes\") },\r\n { value: timeLeft.seconds, label: t(\"seconds\", \"Seconds\") },\r\n ];\r\n\r\n return (\r\n <div className={cn(\r\n \"min-h-screen flex flex-col items-center justify-center relative overflow-hidden\",\r\n \"bg-gradient-to-br from-background via-muted/50 to-background\",\r\n className\r\n )}>\r\n {/* Background decoration */}\r\n <div className=\"absolute inset-0 overflow-hidden pointer-events-none\">\r\n <div className=\"absolute -top-1/2 -left-1/2 w-full h-full bg-primary/5 rounded-full blur-3xl\" />\r\n <div className=\"absolute -bottom-1/2 -right-1/2 w-full h-full bg-primary/10 rounded-full blur-3xl\" />\r\n </div>\r\n\r\n <div className=\"relative z-10 w-full max-w-[var(--container-max-width)] mx-auto px-4 py-12 text-center\">\r\n {/* Heading */}\r\n <FadeIn className=\"mb-6\">\r\n <h1 className=\"text-4xl md:text-5xl lg:text-6xl font-bold text-foreground mb-4\">\r\n {t(\"heading\", \"Something Amazing is Coming\")}\r\n </h1>\r\n <p className=\"text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto\">\r\n {t(\"description\", \"We're working hard to bring you something special. Stay tuned and be the first to know when we launch.\")}\r\n </p>\r\n </FadeIn>\r\n\r\n {/* Countdown */}\r\n <ScaleUp delay={0.1} className=\"mb-12\">\r\n <div className=\"flex justify-center gap-4 md:gap-8\">\r\n {timeUnits.map((unit, index) => (\r\n <div key={index} className=\"flex flex-col items-center\">\r\n <div className=\"w-16 h-16 md:w-24 md:h-24 bg-card border border-border rounded-2xl flex items-center justify-center shadow-lg\">\r\n <span className=\"text-2xl md:text-4xl font-bold text-foreground\">\r\n {String(unit.value).padStart(2, \"0\")}\r\n </span>\r\n </div>\r\n <span className=\"mt-2 text-xs md:text-sm text-muted-foreground uppercase tracking-wide\">\r\n {unit.label}\r\n </span>\r\n </div>\r\n ))}\r\n </div>\r\n </ScaleUp>\r\n\r\n {/* Email signup */}\r\n <FadeIn delay={0.2} className=\"mb-12\">\r\n {!isSubmitted ? (\r\n <form onSubmit={handleSubmit} className=\"flex flex-col sm:flex-row gap-3 max-w-md mx-auto\">\r\n <div className=\"relative flex-1\">\r\n <Mail className=\"absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground\" />\r\n <Input\r\n type=\"email\"\r\n value={email}\r\n onChange={(e) => setEmail(e.target.value)}\r\n placeholder={t(\"emailPlaceholder\", \"Enter your email\")}\r\n required\r\n className=\"pl-10 h-12\"\r\n />\r\n </div>\r\n <Button type=\"submit\" size=\"lg\" className=\"h-12\">\r\n {t(\"notify\", \"Notify Me\")}\r\n </Button>\r\n </form>\r\n ) : (\r\n <div className=\"bg-primary/10 border border-primary/20 rounded-lg p-4 max-w-md mx-auto\">\r\n <p className=\"text-primary font-medium\">\r\n {t(\"success\", \"Thank you! We'll notify you when we launch.\")}\r\n </p>\r\n </div>\r\n )}\r\n </FadeIn>\r\n\r\n {/* Social links */}\r\n <FadeIn delay={0.3}>\r\n <p className=\"text-sm text-muted-foreground mb-4\">\r\n {t(\"followUs\", \"Follow us for updates\")}\r\n </p>\r\n <div className=\"flex justify-center gap-4\">\r\n {socialLinks.map((social, index) => (\r\n <a\r\n key={index}\r\n href={social.href}\r\n aria-label={social.label}\r\n className=\"w-10 h-10 rounded-full bg-muted hover:bg-primary/10 flex items-center justify-center transition-colors\"\r\n >\r\n <social.icon className=\"h-5 w-5 text-muted-foreground hover:text-primary transition-colors\" />\r\n </a>\r\n ))}\r\n </div>\r\n </FadeIn>\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\nexport default ComingSoonPage;\r\n"
26
+ },
27
+ {
28
+ "path": "coming-soon-page/lang/en.json",
29
+ "type": "registry:lang",
30
+ "target": "$modules$/coming-soon-page/lang/en.json",
31
+ "content": "{\r\n \"title\": \"Coming Soon\",\r\n \"heading\": \"Something Amazing is Coming\",\r\n \"description\": \"Ask Promake to customize this description based on your upcoming launch. Lorem ipsum dolor sit amet.\",\r\n \"days\": \"Days\",\r\n \"hours\": \"Hours\",\r\n \"minutes\": \"Minutes\",\r\n \"seconds\": \"Seconds\",\r\n \"emailPlaceholder\": \"Enter your email\",\r\n \"notify\": \"Notify Me\",\r\n \"success\": \"Thank you! We'll notify you when we launch.\",\r\n \"followUs\": \"Follow us for updates\"\r\n}\r\n"
32
+ },
33
+ {
34
+ "path": "coming-soon-page/lang/tr.json",
35
+ "type": "registry:lang",
36
+ "target": "$modules$/coming-soon-page/lang/tr.json",
37
+ "content": "{\r\n \"title\": \"Yakında\",\r\n \"heading\": \"Harika Bir Şey Geliyor\",\r\n \"description\": \"Size özel bir şey sunmak için çalışıyoruz. Lansman yapıldığında ilk öğrenen siz olun.\",\r\n \"days\": \"Gün\",\r\n \"hours\": \"Saat\",\r\n \"minutes\": \"Dakika\",\r\n \"seconds\": \"Saniye\",\r\n \"emailPlaceholder\": \"E-posta adresiniz\",\r\n \"notify\": \"Beni Bilgilendir\",\r\n \"success\": \"Teşekkürler! Lansman yapıldığında sizi bilgilendireceğiz.\",\r\n \"followUs\": \"Güncellemeler için bizi takip edin\"\r\n}\r\n"
38
+ }
39
+ ],
40
+ "exports": {
41
+ "types": [],
42
+ "variables": [
43
+ "ComingSoonPage",
44
+ "default"
45
+ ]
46
+ }
47
+ }
@@ -22,7 +22,7 @@
22
22
  "path": "contact-info-grid/lang/en.json",
23
23
  "type": "registry:lang",
24
24
  "target": "$modules$/contact-info-grid/lang/en.json",
25
- "content": "{\r\n \"title\": \"Get in Touch\",\r\n \"subtitle\": \"AI will customize this contact subtitle based on your site. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\",\r\n \"emailLabel\": \"Email\",\r\n \"emailDesc\": \"We respond to all emails within 24 hours.\",\r\n \"officeLabel\": \"Office\",\r\n \"officeDesc\": \"Drop by our office for a chat.\",\r\n \"phoneLabel\": \"Phone\",\r\n \"phoneDesc\": \"We're available Mon-Fri, 9am-5pm.\",\r\n \"chatLabel\": \"Live Chat\",\r\n \"chatDesc\": \"Get instant help from our support team.\",\r\n \"startChat\": \"Start Chat\"\r\n}\r\n"
25
+ "content": "{\r\n \"title\": \"Get in Touch\",\r\n \"subtitle\": \"Ask Promake to customize this contact subtitle based on your site. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\",\r\n \"emailLabel\": \"Email\",\r\n \"emailDesc\": \"We respond to all emails within 24 hours.\",\r\n \"officeLabel\": \"Office\",\r\n \"officeDesc\": \"Drop by our office for a chat.\",\r\n \"phoneLabel\": \"Phone\",\r\n \"phoneDesc\": \"We're available Mon-Fri, 9am-5pm.\",\r\n \"chatLabel\": \"Live Chat\",\r\n \"chatDesc\": \"Get instant help from our support team.\",\r\n \"startChat\": \"Start Chat\"\r\n}\r\n"
26
26
  },
27
27
  {
28
28
  "path": "contact-info-grid/lang/tr.json",
@@ -25,13 +25,13 @@
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, { useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\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 { Label } from \"@/components/ui/label\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport constants from \"@/constants/constants.json\";\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 const apiService = useApiService();\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\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\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.submitForm(\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 ],\r\n },\r\n constants.site.defaultLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({ name: \"\", email: \"\", message: \"\" });\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 <div>\r\n <Label htmlFor=\"name\">{t(\"nameLabel\", \"Name\")} *</Label>\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 </div>\r\n <div>\r\n <Label htmlFor=\"email\">{t(\"emailLabel\", \"Email\")} *</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\", \"your@email.com\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"message\">{t(\"messageLabel\", \"Message\")} *</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\", \"How can we help you?\")}\r\n required\r\n rows={6}\r\n className=\"mt-1 resize-none\"\r\n />\r\n </div>\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"
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 { Label } from \"@/components/ui/label\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport constants from \"@/constants/constants.json\";\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\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\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\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.submitForm(\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 ],\r\n },\r\n constants.site.defaultLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({ name: \"\", email: \"\", message: \"\" });\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 <div>\r\n <Label htmlFor=\"name\">{t(\"nameLabel\", \"Name\")} *</Label>\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 </div>\r\n <div>\r\n <Label htmlFor=\"email\">{t(\"emailLabel\", \"Email\")} *</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\", \"your@email.com\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"message\">{t(\"messageLabel\", \"Message\")} *</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\", \"How can we help you?\")}\r\n required\r\n rows={6}\r\n className=\"mt-1 resize-none\"\r\n />\r\n </div>\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",
32
32
  "type": "registry:lang",
33
33
  "target": "$modules$/contact-page-centered/lang/en.json",
34
- "content": "{\r\n \"title\": \"Contact Us\",\r\n \"subtitle\": \"AI will customize this contact subtitle. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor.\",\r\n \"emailTitle\": \"Email\",\r\n \"phoneTitle\": \"Phone\",\r\n \"addressTitle\": \"Address\",\r\n \"nameLabel\": \"Name\",\r\n \"namePlaceholder\": \"Your name\",\r\n \"emailLabel\": \"Email\",\r\n \"emailPlaceholder\": \"your@email.com\",\r\n \"messageLabel\": \"Message\",\r\n \"messagePlaceholder\": \"How can we help you?\",\r\n \"submit\": \"Send Message\",\r\n \"sending\": \"Sending...\",\r\n \"success\": \"Message sent successfully! We'll get back to you soon.\",\r\n \"error\": \"Something went wrong. Please try again.\"\r\n}\r\n"
34
+ "content": "{\r\n \"title\": \"Contact Us\",\r\n \"subtitle\": \"Ask Promake to customize this contact subtitle. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor.\",\r\n \"emailTitle\": \"Email\",\r\n \"phoneTitle\": \"Phone\",\r\n \"addressTitle\": \"Address\",\r\n \"nameLabel\": \"Name\",\r\n \"namePlaceholder\": \"Your name\",\r\n \"emailLabel\": \"Email\",\r\n \"emailPlaceholder\": \"your@email.com\",\r\n \"messageLabel\": \"Message\",\r\n \"messagePlaceholder\": \"How can we help you?\",\r\n \"submit\": \"Send Message\",\r\n \"sending\": \"Sending...\",\r\n \"success\": \"Message sent successfully! We'll get back to you soon.\",\r\n \"error\": \"Something went wrong. Please try again.\"\r\n}\r\n"
35
35
  },
36
36
  {
37
37
  "path": "contact-page-centered/lang/tr.json",
@@ -24,13 +24,13 @@
24
24
  "path": "contact-page-map-overlay/index.ts",
25
25
  "type": "registry:index",
26
26
  "target": "$modules$/contact-page-map-overlay/index.ts",
27
- "content": "export * from \"./contact-page-map-overlay\";\n"
27
+ "content": "export * from \"./contact-page-map-overlay\";\nexport { default } from \"./contact-page-map-overlay\";\n"
28
28
  },
29
29
  {
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 } from \"react\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { Layout } from \"@/components/Layout\";\nimport { useApiService } from \"@/lib/api\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Label } from \"@/components/ui/label\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Mail,\n Phone,\n MapPin,\n Facebook,\n Twitter,\n Instagram,\n Linkedin,\n Send,\n ExternalLink,\n} from \"lucide-react\";\nimport constants from \"@/constants/constants.json\";\nimport { GoogleMap } from \"@/modules/google-map\";\n\nconst socialIcons: Record<string, React.ElementType> = {\n facebook: Facebook,\n twitter: Twitter,\n instagram: Instagram,\n linkedin: Linkedin,\n};\n\nexport function ContactPageMapOverlay() {\n const { t } = useTranslation(\"contact-page-map-overlay\");\n usePageTitle({ title: t(\"title\") });\n\n const apiService = useApiService();\n\n const socialLinks = useMemo(() => {\n const socialMedia = constants.socialMedia as\n | Record<string, string>\n | undefined;\n if (!socialMedia) return [];\n return Object.entries(socialMedia)\n .filter(([platform, url]) => url && socialIcons[platform])\n .map(([platform, url]) => ({\n platform,\n url,\n Icon: socialIcons[platform],\n }));\n }, []);\n\n const [formData, setFormData] = useState({\n name: \"\",\n email: \"\",\n message: \"\",\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [submitStatus, setSubmitStatus] = useState<\n \"idle\" | \"success\" | \"error\"\n >(\"idle\");\n\n // Auto-reset status after 5 seconds with proper cleanup\n useEffect(() => {\n if (submitStatus === \"idle\") return;\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\n return () => clearTimeout(timer);\n }, [submitStatus]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setSubmitStatus(\"idle\");\n\n try {\n const currentLanguage = constants.site.defaultLanguage;\n\n await apiService.submitForm(\n formData,\n {\n email_subject1: \"Thank you for contacting us\",\n email_subject2: \"New Contact Form Submission\",\n fields: [\n { name: \"name\", required: true },\n { name: \"email\", required: true },\n { name: \"message\", required: true },\n ],\n },\n currentLanguage\n );\n\n setSubmitStatus(\"success\");\n setFormData({\n name: \"\",\n email: \"\",\n message: \"\",\n });\n } catch (error: unknown) {\n console.error(\"Form submission failed:\", error);\n setSubmitStatus(\"error\");\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n // Default coordinates (can be customized via constants)\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\n\n return (\n <Layout>\n <div className=\"relative min-h-screen\">\n {/* Full-screen Map Background */}\n <div className=\"absolute inset-0\">\n <GoogleMap\n latitude={mapLatitude}\n longitude={mapLongitude}\n zoom={14}\n height=\"100%\"\n className=\"rounded-none border-0 h-full\"\n title={t(\"mapTitle\")}\n />\n {/* Dark overlay for better readability */}\n <div className=\"absolute inset-0 bg-black/30 pointer-events-none\" />\n </div>\n\n {/* Content Overlay */}\n <div className=\"relative z-10 min-h-screen flex items-center py-12 px-4\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto max-w-6xl\">\n <div className=\"grid lg:grid-cols-5 gap-8 items-center\">\n {/* Form Card - Glassmorphism */}\n <Card className=\"lg:col-span-3 backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\n <CardHeader>\n <CardTitle className=\"text-2xl lg:text-3xl\">\n {t(\"title\")}\n </CardTitle>\n <p className=\"text-muted-foreground mt-2\">\n {t(\"description\")}\n </p>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\n {t(\"fullName\")} *\n </Label>\n <Input\n id=\"name\"\n name=\"name\"\n type=\"text\"\n value={formData.name}\n onChange={handleChange}\n placeholder={t(\"fullNamePlaceholder\")}\n required\n className=\"mt-1.5 bg-background/50\"\n />\n </div>\n <div>\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\n {t(\"emailAddress\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1.5 bg-background/50\"\n />\n </div>\n </div>\n\n <div>\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\n {t(\"message\")} *\n </Label>\n <Textarea\n id=\"message\"\n name=\"message\"\n value={formData.message}\n onChange={handleChange}\n placeholder={t(\"messagePlaceholder\")}\n required\n rows={4}\n className=\"mt-1.5 resize-none bg-background/50\"\n />\n </div>\n\n {submitStatus === \"success\" && (\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\n {t(\"success\")}\n </p>\n </div>\n )}\n\n {submitStatus === \"error\" && (\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"sending\")}\n </>\n ) : (\n <>\n <Send className=\"w-4 h-4 mr-2\" />\n {t(\"submit\")}\n </>\n )}\n </Button>\n </form>\n </CardContent>\n </Card>\n\n {/* Contact Info Card - Glassmorphism */}\n <Card className=\"lg:col-span-2 backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\n <CardHeader>\n <CardTitle>{t(\"contactInfo\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <Mail className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"email\")}</p>\n <a\n href={`mailto:${constants.email}`}\n className=\"font-medium hover:text-primary transition-colors\"\n >\n {constants.email}\n </a>\n </div>\n </div>\n\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <Phone className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"phone\")}</p>\n <a\n href={`tel:${constants.phone}`}\n className=\"font-medium hover:text-primary transition-colors\"\n >\n {constants.phone}\n </a>\n </div>\n </div>\n\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <MapPin className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"address\")}</p>\n <p className=\"font-medium\">\n {constants.address.line1}\n <br />\n {constants.address.city}, {constants.address.state} {constants.address.postalCode}\n </p>\n </div>\n </div>\n\n {/* Open in Maps Link */}\n <a\n href={`https://www.google.com/maps?q=${mapLatitude},${mapLongitude}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-2 text-sm text-primary hover:underline mt-2\"\n >\n <ExternalLink className=\"w-4 h-4\" />\n {t(\"openInMaps\")}\n </a>\n\n {/* Social Links */}\n {socialLinks.length > 0 && (\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-muted-foreground mb-3\">\n {t(\"followUs\")}\n </p>\n <div className=\"flex gap-2\">\n {socialLinks.map(({ platform, url, Icon }) => (\n <a\n key={platform}\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\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\"\n >\n <Icon className=\"h-4 w-4\" />\n </a>\n ))}\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n"
33
+ "content": "import React, { useState, useMemo, useEffect } from \"react\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { Layout } from \"@/components/Layout\";\nimport { useApiService } from \"@/lib/api\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Label } from \"@/components/ui/label\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Mail,\n Phone,\n MapPin,\n Facebook,\n Twitter,\n Instagram,\n Linkedin,\n Send,\n ExternalLink,\n} from \"lucide-react\";\nimport constants from \"@/constants/constants.json\";\nimport { GoogleMap } from \"@/modules/google-map\";\n\nconst socialIcons: Record<string, React.ElementType> = {\n facebook: Facebook,\n twitter: Twitter,\n instagram: Instagram,\n linkedin: Linkedin,\n};\n\nexport function ContactPageMapOverlay() {\n const { t } = useTranslation(\"contact-page-map-overlay\");\n usePageTitle({ title: t(\"title\") });\n\n const apiService = useApiService();\n\n const socialLinks = useMemo(() => {\n const socialMedia = constants.socialMedia as\n | Record<string, string>\n | undefined;\n if (!socialMedia) return [];\n return Object.entries(socialMedia)\n .filter(([platform, url]) => url && socialIcons[platform])\n .map(([platform, url]) => ({\n platform,\n url,\n Icon: socialIcons[platform],\n }));\n }, []);\n\n const [formData, setFormData] = useState({\n name: \"\",\n email: \"\",\n message: \"\",\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [submitStatus, setSubmitStatus] = useState<\n \"idle\" | \"success\" | \"error\"\n >(\"idle\");\n\n // Auto-reset status after 5 seconds with proper cleanup\n useEffect(() => {\n if (submitStatus === \"idle\") return;\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\n return () => clearTimeout(timer);\n }, [submitStatus]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setSubmitStatus(\"idle\");\n\n try {\n const currentLanguage = constants.site.defaultLanguage;\n\n await apiService.submitForm(\n formData,\n {\n email_subject1: \"Thank you for contacting us\",\n email_subject2: \"New Contact Form Submission\",\n fields: [\n { name: \"name\", required: true },\n { name: \"email\", required: true },\n { name: \"message\", required: true },\n ],\n },\n currentLanguage\n );\n\n setSubmitStatus(\"success\");\n setFormData({\n name: \"\",\n email: \"\",\n message: \"\",\n });\n } catch (error: unknown) {\n console.error(\"Form submission failed:\", error);\n setSubmitStatus(\"error\");\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n // Default coordinates (can be customized via constants)\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\n\n return (\n <Layout>\n <div className=\"relative min-h-[calc(100vh-4rem)]\">\n {/* Full-screen Map Background */}\n <div className=\"absolute inset-0\">\n <GoogleMap\n latitude={mapLatitude}\n longitude={mapLongitude}\n zoom={14}\n height=\"100%\"\n className=\"rounded-none border-0 h-full\"\n title={t(\"mapTitle\")}\n />\n {/* Dark overlay for better readability */}\n <div className=\"absolute inset-0 bg-black/30 pointer-events-none\" />\n </div>\n\n {/* Content Overlay */}\n <div className=\"relative z-10 min-h-[calc(100vh-4rem)] flex items-center py-12 px-4\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto\">\n <div className=\"grid lg:grid-cols-2 gap-8 items-stretch\">\n {/* Form Card - Glassmorphism */}\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\n <CardHeader>\n <CardTitle className=\"text-2xl lg:text-3xl\">\n {t(\"title\")}\n </CardTitle>\n <p className=\"text-muted-foreground mt-2\">\n {t(\"description\")}\n </p>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\n {t(\"fullName\")} *\n </Label>\n <Input\n id=\"name\"\n name=\"name\"\n type=\"text\"\n value={formData.name}\n onChange={handleChange}\n placeholder={t(\"fullNamePlaceholder\")}\n required\n className=\"mt-1.5 bg-background/50\"\n />\n </div>\n <div>\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\n {t(\"emailAddress\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1.5 bg-background/50\"\n />\n </div>\n </div>\n\n <div>\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\n {t(\"message\")} *\n </Label>\n <Textarea\n id=\"message\"\n name=\"message\"\n value={formData.message}\n onChange={handleChange}\n placeholder={t(\"messagePlaceholder\")}\n required\n rows={4}\n className=\"mt-1.5 resize-none bg-background/50\"\n />\n </div>\n\n {submitStatus === \"success\" && (\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\n {t(\"success\")}\n </p>\n </div>\n )}\n\n {submitStatus === \"error\" && (\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"sending\")}\n </>\n ) : (\n <>\n <Send className=\"w-4 h-4 mr-2\" />\n {t(\"submit\")}\n </>\n )}\n </Button>\n </form>\n </CardContent>\n </Card>\n\n {/* Contact Info Card - Glassmorphism */}\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\n <CardHeader>\n <CardTitle>{t(\"contactInfo\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <Mail className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"email\")}</p>\n <a\n href={`mailto:${constants.email}`}\n className=\"font-medium hover:text-primary transition-colors\"\n >\n {constants.email}\n </a>\n </div>\n </div>\n\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <Phone className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"phone\")}</p>\n <a\n href={`tel:${constants.phone}`}\n className=\"font-medium hover:text-primary transition-colors\"\n >\n {constants.phone}\n </a>\n </div>\n </div>\n\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <MapPin className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"address\")}</p>\n <p className=\"font-medium\">\n {constants.address.line1}\n <br />\n {constants.address.city}, {constants.address.state} {constants.address.postalCode}\n </p>\n </div>\n </div>\n\n {/* Open in Maps Link */}\n <a\n href={`https://www.google.com/maps?q=${mapLatitude},${mapLongitude}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-2 text-sm text-primary hover:underline mt-2\"\n >\n <ExternalLink className=\"w-4 h-4\" />\n {t(\"openInMaps\")}\n </a>\n\n {/* Social Links */}\n {socialLinks.length > 0 && (\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-muted-foreground mb-3\">\n {t(\"followUs\")}\n </p>\n <div className=\"flex gap-2\">\n {socialLinks.map(({ platform, url, Icon }) => (\n <a\n key={platform}\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\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\"\n >\n <Icon className=\"h-4 w-4\" />\n </a>\n ))}\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ContactPageMapOverlay;\n"
34
34
  },
35
35
  {
36
36
  "path": "contact-page-map-overlay/lang/en.json",
@@ -48,7 +48,8 @@
48
48
  "exports": {
49
49
  "types": [],
50
50
  "variables": [
51
- "ContactPageMapOverlay"
51
+ "ContactPageMapOverlay",
52
+ "default"
52
53
  ]
53
54
  }
54
55
  }