@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,49 @@
1
+ {
2
+ "name": "order-confirmation-page",
3
+ "type": "registry:page",
4
+ "title": "Order Confirmation Page",
5
+ "description": "Post-checkout order confirmation page. Displays order summary, payment status for Stripe payments, and order details. Reads pending checkout from localStorage and clears cart on success.",
6
+ "registryDependencies": [
7
+ "button",
8
+ "api",
9
+ "ecommerce-core"
10
+ ],
11
+ "usage": "import OrderConfirmationPage from '@/modules/order-confirmation-page';\n\n<OrderConfirmationPage />\n\n• Installed at: src/modules/order-confirmation-page/\n• Customize text: src/modules/order-confirmation-page/lang/*.json\n• Reads 'pending_checkout' from localStorage\n• Checks payment status for Stripe payments\n• Add to your router as a page component",
12
+ "route": {
13
+ "path": "/order-confirmation",
14
+ "componentName": "OrderConfirmationPage"
15
+ },
16
+ "files": [
17
+ {
18
+ "path": "order-confirmation-page/index.ts",
19
+ "type": "registry:index",
20
+ "target": "$modules$/order-confirmation-page/index.ts",
21
+ "content": "export * from \"./order-confirmation-page\";\r\nexport { default } from \"./order-confirmation-page\";\r\n"
22
+ },
23
+ {
24
+ "path": "order-confirmation-page/order-confirmation-page.tsx",
25
+ "type": "registry:page",
26
+ "target": "$modules$/order-confirmation-page/order-confirmation-page.tsx",
27
+ "content": "import { useEffect, useState } from \"react\";\r\nimport { Link, useParams, useSearchParams } from \"react-router\";\r\nimport {\r\n CheckCircle,\r\n Package,\r\n Truck,\r\n CreditCard,\r\n ArrowLeft,\r\n Clock,\r\n AlertCircle,\r\n Loader2,\r\n ShoppingBag,\r\n} from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { formatPrice } from \"@/modules/ecommerce-core\";\r\nimport { customerClient } from \"@/modules/api/customer-client\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\ninterface CheckoutData {\r\n items: any[];\r\n total: number;\r\n customerInfo: {\r\n firstName: string;\r\n lastName: string;\r\n email: string;\r\n phone: string;\r\n address: string;\r\n city: string;\r\n postalCode: string;\r\n country: string;\r\n notes?: string;\r\n };\r\n paymentMethod: string;\r\n paymentProvider: string;\r\n}\r\n\r\nexport function OrderConfirmationPage() {\r\n const { t } = useTranslation(\"order-confirmation-page\");\r\n usePageTitle({ title: t(\"title\", \"Order Confirmation\") });\r\n\r\n const { orderId } = useParams<{ orderId: string }>();\r\n const [searchParams] = useSearchParams();\r\n const sessionId = searchParams.get(\"session_id\");\r\n\r\n const currency = (constants as any).site?.currency || \"USD\";\r\n\r\n const [checkoutData, setCheckoutData] = useState<CheckoutData | null>(null);\r\n const [paymentStatus, setPaymentStatus] = useState<string | null>(null);\r\n const [isCheckingStatus, setIsCheckingStatus] = useState(false);\r\n const [statusError, setStatusError] = useState<string | null>(null);\r\n\r\n // Check if this is a Stripe payment\r\n const isStripePayment = orderId?.startsWith(\"stripe-\") || !!sessionId;\r\n\r\n useEffect(() => {\r\n window.scrollTo(0, 0);\r\n\r\n // Load checkout data from localStorage\r\n const savedData = localStorage.getItem(\"pending_checkout\");\r\n if (savedData) {\r\n try {\r\n setCheckoutData(JSON.parse(savedData));\r\n // Clear after loading\r\n localStorage.removeItem(\"pending_checkout\");\r\n } catch (e) {\r\n console.error(\"Failed to parse checkout data:\", e);\r\n }\r\n }\r\n\r\n // If Stripe payment, check status\r\n if (isStripePayment && (sessionId || orderId)) {\r\n checkStripePaymentStatus();\r\n }\r\n }, []);\r\n\r\n const checkStripePaymentStatus = async () => {\r\n const sid = sessionId || orderId?.replace(\"stripe-\", \"\");\r\n if (!sid) return;\r\n\r\n setIsCheckingStatus(true);\r\n setStatusError(null);\r\n\r\n try {\r\n const response = await (customerClient as any).payment.status({ sessionId: sid });\r\n setPaymentStatus(response.status);\r\n } catch (error: any) {\r\n console.error(\"Payment status check error:\", error);\r\n setStatusError(error.message || \"An error occurred\");\r\n } finally {\r\n setIsCheckingStatus(false);\r\n }\r\n };\r\n\r\n // No checkout data found\r\n if (!checkoutData) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\r\n <div className=\"max-w-md mx-auto text-center\">\r\n <Card>\r\n <CardContent className=\"pt-8 pb-8\">\r\n <Package className=\"w-16 h-16 text-muted-foreground mx-auto mb-4\" />\r\n <h1 className=\"text-2xl font-bold mb-2\">\r\n {t(\"orderNotFound\", \"Order Not Found\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-6\">\r\n {t(\r\n \"orderNotFoundDescription\",\r\n \"We couldn't find the order details. Please check your email for confirmation.\"\r\n )}\r\n </p>\r\n <Button asChild>\r\n <Link to=\"/products\">\r\n <ShoppingBag className=\"w-4 h-4 mr-2\" />\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n const { items, total, customerInfo, paymentMethod, paymentProvider } = checkoutData;\r\n\r\n const getPaymentMethodDisplay = () => {\r\n if (paymentProvider === \"cash_on_delivery\" || paymentMethod === \"cash\") {\r\n return {\r\n icon: <Truck className=\"w-4 h-4 text-green-600\" />,\r\n label: t(\"cashOnDelivery\", \"Cash on Delivery\"),\r\n };\r\n }\r\n if (paymentProvider === \"bank_transfer\" || paymentMethod === \"transfer\") {\r\n return {\r\n icon: <CreditCard className=\"w-4 h-4 text-primary\" />,\r\n label: t(\"bankTransfer\", \"Bank Transfer\"),\r\n };\r\n }\r\n return {\r\n icon: <CreditCard className=\"w-4 h-4 text-blue-600\" />,\r\n label: t(\"creditCard\", \"Credit Card\"),\r\n };\r\n };\r\n\r\n const paymentDisplay = getPaymentMethodDisplay();\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"max-w-4xl mx-auto\">\r\n {/* Success Header */}\r\n <div className=\"text-center mb-8\">\r\n <div className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-4\">\r\n <CheckCircle className=\"w-8 h-8 text-green-600 dark:text-green-400\" />\r\n </div>\r\n <h1 className=\"text-3xl font-bold text-green-600 dark:text-green-400 mb-2\">\r\n {t(\"title\", \"Order Confirmed!\")}\r\n </h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\r\n \"thankYou\",\r\n \"Thank you for your order! We've received your order and will process it shortly.\"\r\n )}\r\n </p>\r\n </div>\r\n\r\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\r\n {/* Order Details */}\r\n <div className=\"lg:col-span-2 space-y-6\">\r\n {/* Order Info */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <Package className=\"w-5 h-5\" />\r\n {t(\"orderInformation\", \"Order Information\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n {orderId && (\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"orderId\", \"Order ID\")}\r\n </p>\r\n <p className=\"font-semibold font-mono\">#{orderId.slice(0, 12)}</p>\r\n </div>\r\n )}\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"orderDate\", \"Order Date\")}\r\n </p>\r\n <p className=\"font-semibold\">\r\n {new Date().toLocaleDateString()}\r\n </p>\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"totalAmount\", \"Total Amount\")}\r\n </p>\r\n <p className=\"font-semibold text-lg\">\r\n {formatPrice(total, currency)}\r\n </p>\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"paymentMethod\", \"Payment Method\")}\r\n </p>\r\n <div className=\"flex items-center gap-2\">\r\n {paymentDisplay.icon}\r\n <span className=\"font-semibold\">{paymentDisplay.label}</span>\r\n </div>\r\n </div>\r\n {isStripePayment && (\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"paymentStatus\", \"Payment Status\")}\r\n </p>\r\n <div className=\"flex items-center gap-2\">\r\n {isCheckingStatus ? (\r\n <div className=\"flex items-center gap-2 text-muted-foreground\">\r\n <Loader2 className=\"w-4 h-4 animate-spin\" />\r\n <span className=\"text-sm\">{t(\"checking\", \"Checking...\")}</span>\r\n </div>\r\n ) : statusError ? (\r\n <Badge variant=\"destructive\" className=\"flex items-center gap-1\">\r\n <AlertCircle className=\"w-3 h-3\" />\r\n {t(\"statusError\", \"Error\")}\r\n </Badge>\r\n ) : paymentStatus === \"succeeded\" ? (\r\n <Badge className=\"bg-green-100 text-green-800 border-green-200 flex items-center gap-1\">\r\n <CheckCircle className=\"w-3 h-3\" />\r\n {t(\"paymentSucceeded\", \"Paid\")}\r\n </Badge>\r\n ) : paymentStatus === \"processing\" ? (\r\n <Badge className=\"bg-blue-100 text-blue-800 border-blue-200 flex items-center gap-1\">\r\n <Loader2 className=\"w-3 h-3 animate-spin\" />\r\n {t(\"processing\", \"Processing\")}\r\n </Badge>\r\n ) : paymentStatus === \"requires_payment_method\" ? (\r\n <Badge className=\"bg-yellow-100 text-yellow-800 border-yellow-200 flex items-center gap-1\">\r\n <AlertCircle className=\"w-3 h-3\" />\r\n {t(\"requiresPayment\", \"Requires Payment\")}\r\n </Badge>\r\n ) : paymentStatus ? (\r\n <Badge variant=\"secondary\" className=\"flex items-center gap-1\">\r\n <Clock className=\"w-3 h-3\" />\r\n {paymentStatus}\r\n </Badge>\r\n ) : null}\r\n </div>\r\n </div>\r\n )}\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"orderStatus\", \"Order Status\")}\r\n </p>\r\n <Badge className=\"bg-yellow-100 text-yellow-800 border-yellow-200\">\r\n <Clock className=\"w-3 h-3 mr-1\" />\r\n {t(\"pending\", \"Pending\")}\r\n </Badge>\r\n </div>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Delivery Info */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"deliveryInformation\", \"Delivery Information\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"space-y-2\">\r\n <p className=\"font-semibold\">\r\n {customerInfo.firstName} {customerInfo.lastName}\r\n </p>\r\n <p className=\"text-sm text-muted-foreground\">{customerInfo.email}</p>\r\n <p className=\"text-sm text-muted-foreground\">{customerInfo.phone}</p>\r\n <div className=\"mt-3\">\r\n <p className=\"text-sm font-medium text-muted-foreground\">\r\n {t(\"address\", \"Address\")}:\r\n </p>\r\n <p className=\"text-sm\">{customerInfo.address}</p>\r\n <p className=\"text-sm\">\r\n {customerInfo.city} {customerInfo.postalCode}\r\n </p>\r\n </div>\r\n {customerInfo.notes && (\r\n <div className=\"mt-3\">\r\n <p className=\"text-sm font-medium text-muted-foreground\">\r\n {t(\"notes\", \"Notes\")}:\r\n </p>\r\n <p className=\"text-sm\">{customerInfo.notes}</p>\r\n </div>\r\n )}\r\n </div>\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Order Items */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"orderItems\", \"Order Items\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"space-y-4\">\r\n {items.map((item: any, index: number) => (\r\n <div key={item.id || index} className=\"flex gap-4\">\r\n <img\r\n src={item.product?.images?.[0] || \"/images/placeholder.png\"}\r\n alt={item.product?.name || \"Product\"}\r\n className=\"w-16 h-16 object-cover rounded border\"\r\n />\r\n <div className=\"flex-1\">\r\n <h4 className=\"font-medium\">{item.product?.name}</h4>\r\n <div className=\"flex justify-between items-center mt-2\">\r\n <span className=\"text-sm\">\r\n {t(\"qty\", \"Qty\")}: {item.quantity}\r\n </span>\r\n <span className=\"font-semibold\">\r\n {formatPrice(\r\n (item.product?.on_sale\r\n ? item.product?.sale_price\r\n : item.product?.price) * item.quantity,\r\n currency\r\n )}\r\n </span>\r\n </div>\r\n </div>\r\n </div>\r\n ))}\r\n </div>\r\n\r\n <Separator className=\"my-4\" />\r\n\r\n <div className=\"flex justify-between font-semibold text-lg\">\r\n <span>{t(\"total\", \"Total\")}</span>\r\n <span>{formatPrice(total, currency)}</span>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n\r\n {/* Sidebar */}\r\n <div className=\"space-y-6\">\r\n {/* Bank Transfer Instructions */}\r\n {(paymentProvider === \"bank_transfer\" || paymentMethod === \"transfer\") && (\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"text-lg\">\r\n {t(\"paymentInstructions\", \"Payment Instructions\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"p-4 bg-primary/10 border border-primary/20 rounded-lg\">\r\n <p className=\"text-sm font-medium text-primary mb-2\">\r\n {t(\"bankTransferDetails\", \"Bank Transfer Details\")}\r\n </p>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\r\n \"bankDetailsEmail\",\r\n \"Bank details will be sent via email. Please complete the transfer within 48 hours.\"\r\n )}\r\n </p>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n )}\r\n\r\n {/* Cash on Delivery Info */}\r\n {(paymentProvider === \"cash_on_delivery\" || paymentMethod === \"cash\") && (\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"text-lg\">\r\n {t(\"deliveryPayment\", \"Payment on Delivery\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg\">\r\n <p className=\"text-sm font-medium text-green-800 dark:text-green-200 mb-2\">\r\n {t(\"cashOnDeliveryInfo\", \"Cash on Delivery\")}\r\n </p>\r\n <p className=\"text-sm text-green-700 dark:text-green-300\">\r\n {t(\r\n \"codInstructions\",\r\n \"Pay when your order arrives. Our delivery team will contact you before delivery.\"\r\n )}\r\n </p>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n )}\r\n\r\n {/* Actions */}\r\n <Card>\r\n <CardContent className=\"pt-6 space-y-4\">\r\n <Button asChild className=\"w-full\">\r\n <Link to=\"/products\">\r\n <ShoppingBag className=\"w-4 h-4 mr-2\" />\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\r\n\r\n <Button variant=\"outline\" asChild className=\"w-full\">\r\n <Link to=\"/\">\r\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\r\n {t(\"backToHome\", \"Back to Home\")}\r\n </Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default OrderConfirmationPage;\r\n"
28
+ },
29
+ {
30
+ "path": "order-confirmation-page/lang/en.json",
31
+ "type": "registry:lang",
32
+ "target": "$modules$/order-confirmation-page/lang/en.json",
33
+ "content": "{\r\n \"title\": \"Order Confirmed!\",\r\n \"orderNotFound\": \"Order Not Found\",\r\n \"orderNotFoundDescription\": \"We couldn't find the order details. Please check your email for confirmation.\",\r\n \"continueShopping\": \"Continue Shopping\",\r\n \"cashOnDelivery\": \"Cash on Delivery\",\r\n \"bankTransfer\": \"Bank Transfer\",\r\n \"creditCard\": \"Credit Card\",\r\n \"thankYou\": \"Thank you for your order! We've received your order and will process it shortly.\",\r\n \"orderInformation\": \"Order Information\",\r\n \"orderId\": \"Order ID\",\r\n \"orderDate\": \"Order Date\",\r\n \"totalAmount\": \"Total Amount\",\r\n \"paymentMethod\": \"Payment Method\",\r\n \"paymentStatus\": \"Payment Status\",\r\n \"checking\": \"Checking...\",\r\n \"statusError\": \"Error\",\r\n \"paymentSucceeded\": \"Paid\",\r\n \"processing\": \"Processing\",\r\n \"requiresPayment\": \"Requires Payment\",\r\n \"orderStatus\": \"Order Status\",\r\n \"pending\": \"Pending\",\r\n \"deliveryInformation\": \"Delivery Information\",\r\n \"address\": \"Address\",\r\n \"notes\": \"Notes\",\r\n \"orderItems\": \"Order Items\",\r\n \"qty\": \"Qty\",\r\n \"total\": \"Total\",\r\n \"paymentInstructions\": \"Payment Instructions\",\r\n \"bankTransferDetails\": \"Bank Transfer Details\",\r\n \"bankDetailsEmail\": \"Bank details will be sent via email. Please complete the transfer within 48 hours.\",\r\n \"deliveryPayment\": \"Payment on Delivery\",\r\n \"cashOnDeliveryInfo\": \"Cash on Delivery\",\r\n \"codInstructions\": \"Pay when your order arrives. Our delivery team will contact you before delivery.\",\r\n \"backToHome\": \"Back to Home\"\r\n}\r\n"
34
+ },
35
+ {
36
+ "path": "order-confirmation-page/lang/tr.json",
37
+ "type": "registry:lang",
38
+ "target": "$modules$/order-confirmation-page/lang/tr.json",
39
+ "content": "{\r\n \"title\": \"Sipariş Onaylandı!\",\r\n \"orderNotFound\": \"Sipariş Bulunamadı\",\r\n \"orderNotFoundDescription\": \"Sipariş detaylarını bulamadık. Lütfen onay için e-postanızı kontrol edin.\",\r\n \"continueShopping\": \"Alışverişe Devam Et\",\r\n \"cashOnDelivery\": \"Kapıda Ödeme\",\r\n \"bankTransfer\": \"Havale/EFT\",\r\n \"creditCard\": \"Kredi Kartı\",\r\n \"thankYou\": \"Siparişiniz için teşekkürler! Siparişinizi aldık ve en kısa sürede işleme alacağız.\",\r\n \"orderInformation\": \"Sipariş Bilgileri\",\r\n \"orderId\": \"Sipariş No\",\r\n \"orderDate\": \"Sipariş Tarihi\",\r\n \"totalAmount\": \"Toplam Tutar\",\r\n \"paymentMethod\": \"Ödeme Yöntemi\",\r\n \"paymentStatus\": \"Ödeme Durumu\",\r\n \"checking\": \"Kontrol ediliyor...\",\r\n \"statusError\": \"Hata\",\r\n \"paymentSucceeded\": \"Ödendi\",\r\n \"processing\": \"İşleniyor\",\r\n \"requiresPayment\": \"Ödeme Gerekli\",\r\n \"orderStatus\": \"Sipariş Durumu\",\r\n \"pending\": \"Beklemede\",\r\n \"deliveryInformation\": \"Teslimat Bilgileri\",\r\n \"address\": \"Adres\",\r\n \"notes\": \"Notlar\",\r\n \"orderItems\": \"Sipariş Ürünleri\",\r\n \"qty\": \"Adet\",\r\n \"total\": \"Toplam\",\r\n \"paymentInstructions\": \"Ödeme Talimatları\",\r\n \"bankTransferDetails\": \"Havale/EFT Bilgileri\",\r\n \"bankDetailsEmail\": \"Banka bilgileri e-posta ile gönderilecektir. Lütfen transferi 48 saat içinde tamamlayın.\",\r\n \"deliveryPayment\": \"Teslimatta Ödeme\",\r\n \"cashOnDeliveryInfo\": \"Kapıda Ödeme\",\r\n \"codInstructions\": \"Siparişiniz geldiğinde ödeme yapın. Teslimat ekibimiz teslimat öncesi sizinle iletişime geçecektir.\",\r\n \"backToHome\": \"Ana Sayfaya Dön\"\r\n}\r\n"
40
+ }
41
+ ],
42
+ "exports": {
43
+ "types": [],
44
+ "variables": [
45
+ "OrderConfirmationPage",
46
+ "default"
47
+ ]
48
+ }
49
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "portfolio-page",
3
+ "type": "registry:component",
4
+ "title": "Portfolio Page",
5
+ "description": "Filterable portfolio grid with project cards. Features category filtering, hover overlays, and smooth animations.",
6
+ "dependencies": [],
7
+ "registryDependencies": [
8
+ "button",
9
+ "animations"
10
+ ],
11
+ "usage": "import { PortfolioPage } from '@/modules/portfolio-page';\n\n<PortfolioPage />\n\n• Installed at: src/modules/portfolio-page/\n• Customize content: lang/en/portfolio-page.json\n• Props: items[], categories[], title, subtitle",
12
+ "files": [
13
+ {
14
+ "path": "portfolio-page/portfolio-page.tsx",
15
+ "type": "registry:component",
16
+ "target": "$modules$/portfolio-page/portfolio-page.tsx",
17
+ "content": "\"use client\";\r\n\r\nimport { useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Link } from \"react-router\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\n\r\ninterface PortfolioItem {\r\n id: string;\r\n title: string;\r\n category: string;\r\n image: string;\r\n description?: string;\r\n link?: string;\r\n}\r\n\r\ninterface PortfolioPageProps {\r\n items?: PortfolioItem[];\r\n categories?: string[];\r\n title?: string;\r\n subtitle?: string;\r\n className?: string;\r\n}\r\n\r\nconst defaultItems: PortfolioItem[] = [\r\n {\r\n id: \"1\",\r\n title: \"E-commerce Platform Redesign\",\r\n category: \"Web Design\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Complete redesign of a major e-commerce platform\",\r\n link: \"/portfolio/1\",\r\n },\r\n {\r\n id: \"2\",\r\n title: \"Mobile Banking App\",\r\n category: \"Mobile App\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Secure and intuitive mobile banking experience\",\r\n link: \"/portfolio/2\",\r\n },\r\n {\r\n id: \"3\",\r\n title: \"Brand Identity System\",\r\n category: \"Branding\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Complete brand identity for a tech startup\",\r\n link: \"/portfolio/3\",\r\n },\r\n {\r\n id: \"4\",\r\n title: \"Healthcare Dashboard\",\r\n category: \"Web Design\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Analytics dashboard for healthcare providers\",\r\n link: \"/portfolio/4\",\r\n },\r\n {\r\n id: \"5\",\r\n title: \"Fitness Tracking App\",\r\n category: \"Mobile App\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Comprehensive fitness and wellness tracking\",\r\n link: \"/portfolio/5\",\r\n },\r\n {\r\n id: \"6\",\r\n title: \"Restaurant Chain Rebrand\",\r\n category: \"Branding\",\r\n image: \"/images/placeholder.png\",\r\n description: \"Modern rebrand for a national restaurant chain\",\r\n link: \"/portfolio/6\",\r\n },\r\n];\r\n\r\nconst defaultCategories = [\"All\", \"Web Design\", \"Mobile App\", \"Branding\"];\r\n\r\nexport function PortfolioPage({\r\n items = defaultItems,\r\n categories = defaultCategories,\r\n title,\r\n subtitle,\r\n className,\r\n}: PortfolioPageProps) {\r\n const { t } = useTranslation(\"portfolio-page\");\r\n usePageTitle({ title: t(\"pageTitle\") });\r\n const [activeCategory, setActiveCategory] = useState(\"All\");\r\n\r\n const displayTitle = title ?? t(\"title\");\r\n const displaySubtitle = subtitle ?? t(\"subtitle\");\r\n\r\n const filteredItems =\r\n activeCategory === \"All\"\r\n ? items\r\n : items.filter((item) => item.category === activeCategory);\r\n\r\n return (\r\n <Layout>\r\n <div className={cn(\"min-h-screen py-12 md:py-16\", className)}>\r\n <div className=\"container max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Header */}\r\n <FadeIn className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl md:text-5xl font-bold mb-4\">{displayTitle}</h1>\r\n <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto\">\r\n {displaySubtitle}\r\n </p>\r\n </FadeIn>\r\n\r\n {/* Category Filter */}\r\n <FadeIn delay={0.1} className=\"flex flex-wrap justify-center gap-2 mb-12\">\r\n {categories.map((category) => (\r\n <Button\r\n key={category}\r\n variant={activeCategory === category ? \"default\" : \"outline\"}\r\n size=\"sm\"\r\n onClick={() => setActiveCategory(category)}\r\n className=\"rounded-full\"\r\n >\r\n {category}\r\n </Button>\r\n ))}\r\n </FadeIn>\r\n\r\n {/* Portfolio Grid */}\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\r\n {filteredItems.map((item, index) => (\r\n <FadeIn key={item.id} delay={index * 0.05}>\r\n <Link\r\n to={item.link || `/portfolio/${item.id}`}\r\n className=\"group block\"\r\n >\r\n <div className=\"relative overflow-hidden rounded-xl bg-muted aspect-[4/3]\">\r\n <img\r\n src={item.image}\r\n alt={item.title}\r\n className=\"w-full h-full object-cover transition-transform duration-500 group-hover:scale-110\"\r\n />\r\n {/* Overlay */}\r\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300\">\r\n <div className=\"absolute bottom-0 left-0 right-0 p-6 translate-y-4 group-hover:translate-y-0 transition-transform duration-300\">\r\n <span className=\"inline-block text-sm text-primary-foreground/80 bg-primary/80 px-3 py-1 rounded-full mb-2\">\r\n {item.category}\r\n </span>\r\n <h3 className=\"text-xl font-bold text-white mb-1\">\r\n {item.title}\r\n </h3>\r\n {item.description && (\r\n <p className=\"text-white/80 text-sm line-clamp-2\">\r\n {item.description}\r\n </p>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </Link>\r\n </FadeIn>\r\n ))}\r\n </div>\r\n\r\n {/* Empty State */}\r\n {filteredItems.length === 0 && (\r\n <div className=\"text-center py-12\">\r\n <p className=\"text-muted-foreground\">{t(\"noItems\")}</p>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default PortfolioPage;\r\n"
18
+ },
19
+ {
20
+ "path": "portfolio-page/index.ts",
21
+ "type": "registry:index",
22
+ "target": "$modules$/portfolio-page/index.ts",
23
+ "content": "export * from \"./portfolio-page\";\r\nexport { PortfolioPage as default } from \"./portfolio-page\";\r\n"
24
+ },
25
+ {
26
+ "path": "portfolio-page/lang/en.json",
27
+ "type": "registry:lang",
28
+ "target": "$modules$/portfolio-page/lang/en.json",
29
+ "content": "{\r\n \"pageTitle\": \"Portfolio\",\r\n \"title\": \"Our Portfolio\",\r\n \"subtitle\": \"Ask Promake to customize these projects based on your work and case studies.\",\r\n \"noItems\": \"No projects found in this category.\"\r\n}\r\n"
30
+ },
31
+ {
32
+ "path": "portfolio-page/lang/tr.json",
33
+ "type": "registry:lang",
34
+ "target": "$modules$/portfolio-page/lang/tr.json",
35
+ "content": "{\r\n \"pageTitle\": \"Portfolyo\",\r\n \"title\": \"Portfolyomuz\",\r\n \"subtitle\": \"Bu projeleri kendi çalışmalarınıza ve vaka çalışmalarınıza göre özelleştirmek için Promake'e sorun.\",\r\n \"noItems\": \"Bu kategoride proje bulunamadı.\"\r\n}\r\n"
36
+ }
37
+ ],
38
+ "exports": {
39
+ "types": [],
40
+ "variables": [
41
+ "PortfolioPage",
42
+ "default"
43
+ ]
44
+ }
45
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "pricing-page",
3
+ "type": "registry:page",
4
+ "title": "Pricing Page",
5
+ "description": "Full pricing page with 3-tier pricing cards (Free, Pro, Enterprise), monthly/annual toggle with 20% savings, feature comparison table, and FAQ teaser. Includes popular badge and staggered animations.",
6
+ "registryDependencies": [
7
+ "animations"
8
+ ],
9
+ "route": {
10
+ "path": "/pricing",
11
+ "componentName": "PricingPage"
12
+ },
13
+ "usage": "import { PricingPage } from '@/modules/pricing-page';\n\n<Route path=\"/pricing\" element={<PricingPage />} />\n\n• Monthly/Annual billing toggle\n• Feature comparison table\n• Popular plan highlighting\n• FAQ and contact CTA",
14
+ "files": [
15
+ {
16
+ "path": "pricing-page/index.ts",
17
+ "type": "registry:index",
18
+ "target": "$modules$/pricing-page/index.ts",
19
+ "content": "export * from \"./pricing-page\";\r\nexport { default } from \"./pricing-page\";\r\n"
20
+ },
21
+ {
22
+ "path": "pricing-page/pricing-page.tsx",
23
+ "type": "registry:page",
24
+ "target": "$modules$/pricing-page/pricing-page.tsx",
25
+ "content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Card, CardContent, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Switch } from \"@/components/ui/switch\";\r\nimport { Check, X, HelpCircle } from \"lucide-react\";\r\nimport { FadeIn, StaggerContainer, StaggerItem } from \"@/modules/animations\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipProvider,\r\n TooltipTrigger,\r\n} from \"@/components/ui/tooltip\";\r\n\r\ninterface PricingPageProps {\r\n className?: string;\r\n}\r\n\r\nexport function PricingPage({ className }: PricingPageProps) {\r\n const { t } = useTranslation(\"pricing-page\");\r\n usePageTitle({ title: t(\"title\") });\r\n const [isAnnual, setIsAnnual] = useState(true);\r\n\r\n const plans = [\r\n {\r\n id: \"free\",\r\n name: t(\"freeName\"),\r\n description: t(\"freeDesc\"),\r\n monthlyPrice: 0,\r\n annualPrice: 0,\r\n features: [\r\n { name: t(\"freeFeature1\"), included: true },\r\n { name: t(\"freeFeature2\"), included: true },\r\n { name: t(\"freeFeature3\"), included: true },\r\n { name: t(\"freeFeature4\"), included: true },\r\n { name: t(\"freeFeature5\"), included: false },\r\n { name: t(\"freeFeature6\"), included: false },\r\n { name: t(\"freeFeature7\"), included: false },\r\n ],\r\n popular: false,\r\n cta: t(\"freeCta\"),\r\n },\r\n {\r\n id: \"pro\",\r\n name: t(\"proName\"),\r\n description: t(\"proDesc\"),\r\n monthlyPrice: 29,\r\n annualPrice: 24,\r\n features: [\r\n { name: t(\"proFeature1\"), included: true },\r\n { name: t(\"proFeature2\"), included: true },\r\n { name: t(\"proFeature3\"), included: true },\r\n { name: t(\"proFeature4\"), included: true },\r\n { name: t(\"proFeature5\"), included: true },\r\n { name: t(\"proFeature6\"), included: true },\r\n { name: t(\"proFeature7\"), included: true },\r\n ],\r\n popular: true,\r\n cta: t(\"proCta\"),\r\n },\r\n {\r\n id: \"enterprise\",\r\n name: t(\"enterpriseName\"),\r\n description: t(\"enterpriseDesc\"),\r\n monthlyPrice: 99,\r\n annualPrice: 79,\r\n features: [\r\n { name: t(\"enterpriseFeature1\"), included: true },\r\n { name: t(\"enterpriseFeature2\"), included: true },\r\n { name: t(\"enterpriseFeature3\"), included: true },\r\n { name: t(\"enterpriseFeature4\"), included: true },\r\n { name: t(\"enterpriseFeature5\"), included: true },\r\n { name: t(\"enterpriseFeature6\"), included: true },\r\n { name: t(\"enterpriseFeature7\"), included: true },\r\n ],\r\n popular: false,\r\n cta: t(\"enterpriseCta\"),\r\n },\r\n ];\r\n\r\n const comparisonFeatures = [\r\n { name: t(\"compProjects\"), free: \"3\", pro: t(\"unlimited\"), enterprise: t(\"unlimited\") },\r\n { name: t(\"compStorage\"), free: \"1GB\", pro: \"25GB\", enterprise: t(\"unlimited\") },\r\n { name: t(\"compUsers\"), free: \"1\", pro: \"10\", enterprise: t(\"unlimited\") },\r\n { name: t(\"compApi\"), free: \"1,000\", pro: \"100,000\", enterprise: t(\"unlimited\") },\r\n { name: t(\"compSupport\"), free: t(\"community\"), pro: t(\"email\"), enterprise: t(\"dedicated\") },\r\n ];\r\n\r\n return (\r\n <Layout>\r\n <div className={cn(\"min-h-screen bg-muted/30 py-12 md:py-16\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Header */}\r\n <FadeIn className=\"text-center mb-12\">\r\n <p className=\"text-sm font-medium text-primary mb-2 uppercase tracking-wide\">\r\n {t(\"label\")}\r\n </p>\r\n <h1 className=\"text-4xl md:text-5xl font-bold text-foreground mb-4\">\r\n {t(\"heading\")}\r\n </h1>\r\n <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto mb-8\">\r\n {t(\"description\")}\r\n </p>\r\n\r\n {/* Billing Toggle */}\r\n <div className=\"flex items-center justify-center gap-4\">\r\n <span className={cn(\"text-sm\", !isAnnual && \"text-foreground font-medium\")}>\r\n {t(\"monthly\")}\r\n </span>\r\n <Switch checked={isAnnual} onCheckedChange={setIsAnnual} />\r\n <span className={cn(\"text-sm\", isAnnual && \"text-foreground font-medium\")}>\r\n {t(\"annual\")}\r\n <Badge variant=\"secondary\" className=\"ml-2\">\r\n {t(\"save\")}\r\n </Badge>\r\n </span>\r\n </div>\r\n </FadeIn>\r\n\r\n {/* Pricing Cards */}\r\n <StaggerContainer className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-8 mb-20\">\r\n {plans.map((plan) => (\r\n <StaggerItem key={plan.id}>\r\n <Card className={cn(\r\n \"relative h-full flex flex-col\",\r\n plan.popular && \"border-primary shadow-lg ring-2 ring-primary/20\"\r\n )}>\r\n {plan.popular && (\r\n <div className=\"absolute -top-3 left-1/2 -translate-x-1/2\">\r\n <Badge className=\"bg-primary text-primary-foreground\">\r\n {t(\"mostPopular\")}\r\n </Badge>\r\n </div>\r\n )}\r\n\r\n <CardHeader className=\"text-center pt-8\">\r\n <CardTitle className=\"text-xl font-bold\">{plan.name}</CardTitle>\r\n <p className=\"text-sm text-muted-foreground\">{plan.description}</p>\r\n </CardHeader>\r\n\r\n <CardContent className=\"flex-1\">\r\n <div className=\"text-center mb-8\">\r\n <div className=\"flex items-baseline justify-center gap-1\">\r\n <span className=\"text-4xl md:text-5xl font-bold\">\r\n ${isAnnual ? plan.annualPrice : plan.monthlyPrice}\r\n </span>\r\n <span className=\"text-muted-foreground\">/{t(\"month\")}</span>\r\n </div>\r\n {isAnnual && plan.monthlyPrice > 0 && (\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"billedAnnually\")} (${plan.annualPrice * 12}/yr)\r\n </p>\r\n )}\r\n </div>\r\n\r\n <ul className=\"space-y-3\">\r\n {plan.features.map((feature, index) => (\r\n <li key={index} className=\"flex items-center gap-3\">\r\n {feature.included ? (\r\n <Check className=\"h-5 w-5 text-primary flex-shrink-0\" />\r\n ) : (\r\n <X className=\"h-5 w-5 text-muted-foreground/50 flex-shrink-0\" />\r\n )}\r\n <span className={cn(\r\n \"text-sm\",\r\n feature.included ? \"text-foreground\" : \"text-muted-foreground/50\"\r\n )}>\r\n {feature.name}\r\n </span>\r\n </li>\r\n ))}\r\n </ul>\r\n </CardContent>\r\n\r\n <CardFooter className=\"pt-4\">\r\n <Button\r\n asChild\r\n className=\"w-full\"\r\n variant={plan.popular ? \"default\" : \"outline\"}\r\n size=\"lg\"\r\n >\r\n <Link to={plan.id === \"enterprise\" ? \"/contact\" : \"/register\"}>\r\n {plan.cta}\r\n </Link>\r\n </Button>\r\n </CardFooter>\r\n </Card>\r\n </StaggerItem>\r\n ))}\r\n </StaggerContainer>\r\n\r\n {/* Comparison Table */}\r\n <FadeIn className=\"mb-16\">\r\n <h2 className=\"text-2xl md:text-3xl font-bold text-center mb-8\">\r\n {t(\"compareTitle\")}\r\n </h2>\r\n <div className=\"overflow-x-auto\">\r\n <table className=\"w-full border-collapse\">\r\n <thead>\r\n <tr className=\"border-b border-border\">\r\n <th className=\"text-left py-4 px-4 font-medium text-muted-foreground\">\r\n {t(\"feature\")}\r\n </th>\r\n {plans.map((plan) => (\r\n <th key={plan.id} className=\"text-center py-4 px-4 font-medium\">\r\n {plan.name}\r\n </th>\r\n ))}\r\n </tr>\r\n </thead>\r\n <tbody>\r\n {comparisonFeatures.map((feature, index) => (\r\n <tr key={index} className=\"border-b border-border\">\r\n <td className=\"py-4 px-4 text-sm\">{feature.name}</td>\r\n <td className=\"py-4 px-4 text-center text-sm text-muted-foreground\">{feature.free}</td>\r\n <td className=\"py-4 px-4 text-center text-sm font-medium\">{feature.pro}</td>\r\n <td className=\"py-4 px-4 text-center text-sm\">{feature.enterprise}</td>\r\n </tr>\r\n ))}\r\n </tbody>\r\n </table>\r\n </div>\r\n </FadeIn>\r\n\r\n {/* FAQ Teaser */}\r\n <FadeIn className=\"text-center\">\r\n <Card className=\"bg-card border\">\r\n <CardContent className=\"p-8\">\r\n <TooltipProvider>\r\n <Tooltip>\r\n <TooltipTrigger asChild>\r\n <HelpCircle className=\"h-8 w-8 text-primary mx-auto mb-4 cursor-help\" />\r\n </TooltipTrigger>\r\n <TooltipContent>\r\n <p>{t(\"faqTooltip\")}</p>\r\n </TooltipContent>\r\n </Tooltip>\r\n </TooltipProvider>\r\n <h3 className=\"text-xl font-semibold mb-2\">\r\n {t(\"faqTitle\")}\r\n </h3>\r\n <p className=\"text-muted-foreground mb-4\">\r\n {t(\"faqDesc\")}\r\n </p>\r\n <div className=\"flex flex-col sm:flex-row gap-3 justify-center\">\r\n <Button asChild variant=\"outline\">\r\n <Link to=\"/faq\">{t(\"viewFaq\")}</Link>\r\n </Button>\r\n <Button asChild>\r\n <Link to=\"/contact\">{t(\"contactSales\")}</Link>\r\n </Button>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default PricingPage;\r\n"
26
+ },
27
+ {
28
+ "path": "pricing-page/lang/en.json",
29
+ "type": "registry:lang",
30
+ "target": "$modules$/pricing-page/lang/en.json",
31
+ "content": "{\r\n \"title\": \"Pricing\",\r\n \"label\": \"Pricing\",\r\n \"heading\": \"Simple, Transparent Pricing\",\r\n \"description\": \"Ask Promake to customize this pricing description based on your plans and offers.\",\r\n \"monthly\": \"Monthly\",\r\n \"annual\": \"Annual\",\r\n \"save\": \"Save 20%\",\r\n \"month\": \"mo\",\r\n \"billedAnnually\": \"Billed annually\",\r\n \"mostPopular\": \"Most Popular\",\r\n \"freeName\": \"Free\",\r\n \"freeDesc\": \"Perfect for getting started\",\r\n \"freeFeature1\": \"Feature included\",\r\n \"freeFeature2\": \"Feature included\",\r\n \"freeFeature3\": \"Feature included\",\r\n \"freeFeature4\": \"Feature included\",\r\n \"freeFeature5\": \"Feature not included\",\r\n \"freeFeature6\": \"Feature not included\",\r\n \"freeFeature7\": \"Feature not included\",\r\n \"freeCta\": \"Get Started\",\r\n \"proName\": \"Professional\",\r\n \"proDesc\": \"Best for growing businesses\",\r\n \"proFeature1\": \"All Free features\",\r\n \"proFeature2\": \"Feature included\",\r\n \"proFeature3\": \"Feature included\",\r\n \"proFeature4\": \"Feature included\",\r\n \"proFeature5\": \"Feature included\",\r\n \"proFeature6\": \"Feature included\",\r\n \"proFeature7\": \"Feature included\",\r\n \"proCta\": \"Start Free Trial\",\r\n \"enterpriseName\": \"Enterprise\",\r\n \"enterpriseDesc\": \"For large organizations\",\r\n \"enterpriseFeature1\": \"All Pro features\",\r\n \"enterpriseFeature2\": \"Feature included\",\r\n \"enterpriseFeature3\": \"Feature included\",\r\n \"enterpriseFeature4\": \"Feature included\",\r\n \"enterpriseFeature5\": \"Feature included\",\r\n \"enterpriseFeature6\": \"Feature included\",\r\n \"enterpriseFeature7\": \"Feature included\",\r\n \"enterpriseCta\": \"Contact Sales\",\r\n \"compareTitle\": \"Compare Plans\",\r\n \"feature\": \"Feature\",\r\n \"compProjects\": \"Projects\",\r\n \"compStorage\": \"Storage\",\r\n \"compUsers\": \"Team members\",\r\n \"compApi\": \"API requests\",\r\n \"compSupport\": \"Support\",\r\n \"unlimited\": \"Unlimited\",\r\n \"community\": \"Community\",\r\n \"email\": \"Email\",\r\n \"dedicated\": \"Dedicated\",\r\n \"faqTooltip\": \"Click to see frequently asked questions\",\r\n \"faqTitle\": \"Have questions?\",\r\n \"faqDesc\": \"Check out our FAQ or contact our sales team for more information.\",\r\n \"viewFaq\": \"View FAQ\",\r\n \"contactSales\": \"Contact Sales\"\r\n}\r\n"
32
+ },
33
+ {
34
+ "path": "pricing-page/lang/tr.json",
35
+ "type": "registry:lang",
36
+ "target": "$modules$/pricing-page/lang/tr.json",
37
+ "content": "{\r\n \"title\": \"Fiyatlandırma\",\r\n \"label\": \"Fiyatlandırma\",\r\n \"heading\": \"Basit, Şeffaf Fiyatlandırma\",\r\n \"description\": \"Size uygun planı seçin. Tüm planlar 14 günlük ücretsiz deneme içerir.\",\r\n \"monthly\": \"Aylık\",\r\n \"annual\": \"Yıllık\",\r\n \"save\": \"%20 Tasarruf\",\r\n \"month\": \"ay\",\r\n \"billedAnnually\": \"Yıllık faturalandırılır\",\r\n \"mostPopular\": \"En Popüler\",\r\n \"freeName\": \"Ücretsiz\",\r\n \"freeDesc\": \"Platformumuzu denemek için mükemmel\",\r\n \"freeFeature1\": \"3 projeye kadar\",\r\n \"freeFeature2\": \"Temel analitik\",\r\n \"freeFeature3\": \"Topluluk desteği\",\r\n \"freeFeature4\": \"1GB depolama\",\r\n \"freeFeature5\": \"API erişimi\",\r\n \"freeFeature6\": \"Özel alan adı\",\r\n \"freeFeature7\": \"Öncelikli destek\",\r\n \"freeCta\": \"Başlayın\",\r\n \"proName\": \"Profesyonel\",\r\n \"proDesc\": \"Büyüyen işletmeler için en iyisi\",\r\n \"proFeature1\": \"Sınırsız proje\",\r\n \"proFeature2\": \"Gelişmiş analitik\",\r\n \"proFeature3\": \"Öncelikli e-posta desteği\",\r\n \"proFeature4\": \"25GB depolama\",\r\n \"proFeature5\": \"API erişimi\",\r\n \"proFeature6\": \"Özel alan adı\",\r\n \"proFeature7\": \"Takım işbirliği\",\r\n \"proCta\": \"Ücretsiz Deneyin\",\r\n \"enterpriseName\": \"Kurumsal\",\r\n \"enterpriseDesc\": \"Büyük ölçekli organizasyonlar için\",\r\n \"enterpriseFeature1\": \"Pro'daki her şey\",\r\n \"enterpriseFeature2\": \"Sınırsız depolama\",\r\n \"enterpriseFeature3\": \"7/24 telefon desteği\",\r\n \"enterpriseFeature4\": \"Özel entegrasyonlar\",\r\n \"enterpriseFeature5\": \"Özel müdür\",\r\n \"enterpriseFeature6\": \"SLA garantisi\",\r\n \"enterpriseFeature7\": \"Şirket içi seçeneği\",\r\n \"enterpriseCta\": \"Satışa Başvurun\",\r\n \"compareTitle\": \"Planları Karşılaştırın\",\r\n \"feature\": \"Özellik\",\r\n \"compProjects\": \"Projeler\",\r\n \"compStorage\": \"Depolama\",\r\n \"compUsers\": \"Takım üyeleri\",\r\n \"compApi\": \"Aylık API istekleri\",\r\n \"compSupport\": \"Destek\",\r\n \"unlimited\": \"Sınırsız\",\r\n \"community\": \"Topluluk\",\r\n \"email\": \"E-posta\",\r\n \"dedicated\": \"7/24 Özel\",\r\n \"faqTooltip\": \"Sık sorulan soruları görmek için tıklayın\",\r\n \"faqTitle\": \"Sorularınız mı var?\",\r\n \"faqDesc\": \"SSS'imize göz atın veya daha fazla bilgi için satış ekibimizle iletişime geçin.\",\r\n \"viewFaq\": \"SSS'yi Görüntüle\",\r\n \"contactSales\": \"Satışa Başvurun\"\r\n}\r\n"
38
+ }
39
+ ],
40
+ "exports": {
41
+ "types": [],
42
+ "variables": [
43
+ "PricingPage",
44
+ "default"
45
+ ]
46
+ }
47
+ }
@@ -25,7 +25,7 @@
25
25
  "path": "pricing-section/lang/en.json",
26
26
  "type": "registry:lang",
27
27
  "target": "$modules$/pricing-section/lang/en.json",
28
- "content": "{\r\n \"label\": \"Pricing\",\r\n \"title\": \"AI will customize this pricing title based on your site needs\",\r\n \"subtitle\": \"This subtitle will be replaced with pricing information relevant to your service offerings.\",\r\n \"perMonth\": \"/month\",\r\n \"popular\": \"Most Popular\",\r\n \"cta\": \"Get Started\",\r\n \"guarantee\": \"Lorem ipsum dolor sit amet. AI will customize guarantee text based on your policies.\",\r\n \"starterName\": \"Starter\",\r\n \"starterDesc\": \"AI will customize this plan description based on your pricing tiers\",\r\n \"starterPrice\": \"$9\",\r\n \"starterFeature1\": \"Replace with your starter plan feature\",\r\n \"starterFeature2\": \"This feature will be customized by AI\",\r\n \"starterFeature3\": \"Placeholder feature text\",\r\n \"starterFeature4\": \"AI will replace this with relevant feature\",\r\n \"proName\": \"Pro\",\r\n \"proDesc\": \"This description will be customized based on your mid-tier offerings\",\r\n \"proPrice\": \"$29\",\r\n \"proFeature1\": \"Customize this plan feature\",\r\n \"proFeature2\": \"AI will generate appropriate feature description\",\r\n \"proFeature3\": \"Placeholder text for plan features\",\r\n \"proFeature4\": \"Replace with actual plan feature\",\r\n \"proFeature5\": \"AI will customize this feature based on your plan\",\r\n \"enterpriseName\": \"Advanced\",\r\n \"enterpriseDesc\": \"AI will customize this for your advanced offering\",\r\n \"enterprisePrice\": \"$99\",\r\n \"enterpriseFeature1\": \"Advanced plan feature placeholder\",\r\n \"enterpriseFeature2\": \"This will be replaced by AI with advanced plan features\",\r\n \"enterpriseFeature3\": \"Customize advanced plan feature description\",\r\n \"enterpriseFeature4\": \"AI will generate appropriate advanced plan feature\",\r\n \"enterpriseFeature5\": \"Placeholder for advanced plan feature\",\r\n \"enterpriseFeature6\": \"Replace with an actual advanced plan feature\"\r\n}\r\n"
28
+ "content": "{\r\n \"label\": \"Pricing\",\r\n \"title\": \"Ask Promake to customize this pricing title based on your site needs\",\r\n \"subtitle\": \"This subtitle will be replaced with pricing information relevant to your service offerings.\",\r\n \"perMonth\": \"/month\",\r\n \"popular\": \"Most Popular\",\r\n \"cta\": \"Get Started\",\r\n \"guarantee\": \"Lorem ipsum dolor sit amet. Ask Promake to customize guarantee text based on your policies.\",\r\n \"starterName\": \"Starter\",\r\n \"starterDesc\": \"Ask Promake to customize this plan description based on your pricing tiers\",\r\n \"starterPrice\": \"$9\",\r\n \"starterFeature1\": \"Replace with your starter plan feature\",\r\n \"starterFeature2\": \"This feature will be customized by Promake\",\r\n \"starterFeature3\": \"Placeholder feature text\",\r\n \"starterFeature4\": \"Ask Promake to replace this with relevant feature\",\r\n \"proName\": \"Pro\",\r\n \"proDesc\": \"This description will be customized based on your mid-tier offerings\",\r\n \"proPrice\": \"$29\",\r\n \"proFeature1\": \"Customize this plan feature\",\r\n \"proFeature2\": \"Ask Promake to generate appropriate feature description\",\r\n \"proFeature3\": \"Placeholder text for plan features\",\r\n \"proFeature4\": \"Replace with actual plan feature\",\r\n \"proFeature5\": \"Ask Promake to customize this feature based on your plan\",\r\n \"enterpriseName\": \"Advanced\",\r\n \"enterpriseDesc\": \"Ask Promake to customize this for your advanced offering\",\r\n \"enterprisePrice\": \"$99\",\r\n \"enterpriseFeature1\": \"Advanced plan feature placeholder\",\r\n \"enterpriseFeature2\": \"This will be replaced by Promake with advanced plan features\",\r\n \"enterpriseFeature3\": \"Customize advanced plan feature description\",\r\n \"enterpriseFeature4\": \"Ask Promake to generate appropriate advanced plan feature\",\r\n \"enterpriseFeature5\": \"Placeholder for advanced plan feature\",\r\n \"enterpriseFeature6\": \"Replace with an actual advanced plan feature\"\r\n}\r\n"
29
29
  },
30
30
  {
31
31
  "path": "pricing-section/lang/tr.json",
@@ -3,7 +3,9 @@
3
3
  "type": "registry:page",
4
4
  "title": "Privacy Policy Page",
5
5
  "description": "GDPR/CCPA-friendly privacy policy page with table of contents, collapsible sections, and legal text formatting. Covers data collection, usage, cookies, third-party services, user rights, and contact information. Last updated date and version tracking included. Professionally structured for compliance.",
6
- "registryDependencies": [],
6
+ "registryDependencies": [
7
+ "animations"
8
+ ],
7
9
  "usage": "import { PrivacyPage } from '@/modules/privacy-page';\n\n<Route path=\"/privacy\" element={<PrivacyPage />} />\n\n• GDPR/CCPA compliant structure\n• Sections: data collection, cookies, rights, contact\n• Edit content in lang/en.json",
8
10
  "route": {
9
11
  "path": "/privacy",
@@ -18,7 +18,7 @@
18
18
  "path": "product-detail-block/product-detail-block.tsx",
19
19
  "type": "registry:block",
20
20
  "target": "$modules$/product-detail-block/product-detail-block.tsx",
21
- "content": "import { useState } from \"react\";\nimport {\n Star,\n Heart,\n Share2,\n Truck,\n RotateCcw,\n Shield,\n Plus,\n Minus,\n ChevronLeft,\n ChevronRight,\n X,\n ZoomIn,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { Dialog, DialogContent, DialogClose } from \"@/components/ui/dialog\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\nimport { useTranslation } from \"react-i18next\";\nimport constants from \"@/constants/constants.json\";\n\ninterface ProductDetailBlockProps {\n product: Product;\n}\n\nexport function ProductDetailBlock({ product }: ProductDetailBlockProps) {\n const { t } = useTranslation(\"product-detail-block\");\n const { addItem } = useCart();\n\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [quantity, setQuantity] = useState(1);\n const [selectedVariant, setSelectedVariant] = useState<string>(\"\");\n const [isAdding, setIsAdding] = useState(false);\n const [isImageModalOpen, setIsImageModalOpen] = useState(false);\n const [zoomLevel, setZoomLevel] = useState(1);\n const [position, setPosition] = useState({ x: 0, y: 0 });\n const [isDragging, setIsDragging] = useState(false);\n const [dragStart, setDragStart] = useState({ x: 0, y: 0 });\n\n const handleAddToCart = async () => {\n if (product) {\n setIsAdding(true);\n // Add multiple items by calling addItem quantity times\n for (let i = 0; i < quantity; i++) {\n addItem(product);\n }\n\n // Show success feedback\n setTimeout(() => {\n setIsAdding(false);\n }, 1000);\n }\n };\n\n const handleQuantityChange = (change: number) => {\n const newQuantity = quantity + change;\n if (newQuantity >= 1 && newQuantity <= product.stock) {\n setQuantity(newQuantity);\n }\n };\n\n const features = [\n {\n icon: Truck,\n title: t(\"freeShipping\", \"Free Shipping\"),\n description: t(\"freeShippingDesc\", \"On orders over 50\"),\n },\n {\n icon: RotateCcw,\n title: t(\"easyReturns\", \"Easy Returns\"),\n description: t(\"easyReturnsDesc\", \"30-day return policy\"),\n },\n {\n icon: Shield,\n title: t(\"secureCheckout\", \"Secure Checkout\"),\n description: t(\"secureCheckoutDesc\", \"SSL encrypted payment\"),\n },\n ];\n\n return (\n <>\n <div className=\"grid lg:grid-cols-2 gap-12\">\n {/* Product Images */}\n <div className=\"space-y-4\">\n {/* Main Image */}\n <div className=\"aspect-square relative overflow-hidden rounded-lg bg-muted group\">\n <img\n src={product.images[selectedImageIndex] || \"/images/placeholder.png\"}\n alt={product.name}\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 cursor-zoom-in\"\n onClick={() => setIsImageModalOpen(true)}\n />\n\n {/* Zoom Indicator */}\n <div className=\"absolute bottom-4 right-4 bg-black/50 text-white p-2 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity\">\n <ZoomIn className=\"w-5 h-5\" />\n </div>\n\n {/* Navigation Arrows */}\n {product.images.length > 1 && (\n <>\n <Button\n variant=\"secondary\"\n size=\"icon\"\n className=\"absolute left-4 top-1/2 transform -translate-y-1/2\"\n onClick={() =>\n setSelectedImageIndex(\n selectedImageIndex === 0\n ? product.images.length - 1\n : selectedImageIndex - 1\n )\n }\n >\n <ChevronLeft className=\"h-4 w-4\" />\n </Button>\n <Button\n variant=\"secondary\"\n size=\"icon\"\n className=\"absolute right-4 top-1/2 transform -translate-y-1/2\"\n onClick={() =>\n setSelectedImageIndex(\n selectedImageIndex === product.images.length - 1\n ? 0\n : selectedImageIndex + 1\n )\n }\n >\n <ChevronRight className=\"h-4 w-4\" />\n </Button>\n </>\n )}\n\n {/* Badges */}\n <div className=\"absolute top-4 left-4 flex flex-col gap-2\">\n {product.on_sale && (\n <Badge variant=\"destructive\">{t(\"sale\", \"Sale\")}</Badge>\n )}\n {product.is_new && (\n <Badge variant=\"secondary\">{t(\"new\", \"New\")}</Badge>\n )}\n </div>\n </div>\n\n {/* Thumbnail Images */}\n {product.images.length > 1 && (\n <div className=\"flex gap-3 overflow-x-auto\">\n {product.images.map((image, index) => (\n <button\n key={index}\n className={`flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden border-2 transition-colors ${\n selectedImageIndex === index\n ? \"border-primary\"\n : \"border-transparent hover:border-muted-foreground\"\n }`}\n onClick={() => setSelectedImageIndex(index)}\n >\n <img\n src={image}\n alt={`${product.name} ${index + 1}`}\n className=\"w-full h-full object-cover\"\n />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"space-y-6\">\n {/* Header */}\n <div>\n <div className=\"flex items-center gap-2 mb-2\">\n <Badge variant=\"outline\">\n {product.category_name ||\n product.categories?.[0]?.name ||\n product.category}\n </Badge>\n {product.featured && (\n <Badge variant=\"secondary\">{t(\"featured\", \"Featured\")}</Badge>\n )}\n </div>\n <h1 className=\"text-3xl font-bold mb-4\">{product.name}</h1>\n\n {/* Rating */}\n <div className=\"flex items-center gap-4 mb-4\">\n <div className=\"flex items-center gap-1\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={`h-4 w-4 ${\n i < Math.floor(product.rating)\n ? \"fill-current text-yellow-400\"\n : \"text-muted-foreground\"\n }`}\n />\n ))}\n <span className=\"text-sm font-medium ml-1\">\n {product.rating}\n </span>\n </div>\n </div>\n\n {/* Price */}\n <div className=\"flex items-center gap-3 mb-6\">\n <span className=\"text-3xl font-bold\">\n {formatPrice(\n product.on_sale && product.sale_price\n ? product.sale_price\n : product.price,\n constants.site.currency\n )}\n </span>\n {product.on_sale && product.sale_price && (\n <span className=\"text-xl text-muted-foreground line-through\">\n {formatPrice(\n product.price,\n constants.site.currency\n )}\n </span>\n )}\n </div>\n </div>\n\n {/* Description */}\n <div>\n <p className=\"text-muted-foreground leading-relaxed\">\n {product.description}\n </p>\n </div>\n\n {/* Variants */}\n {product.variants && product.variants.length > 0 && (\n <div>\n <label className=\"text-sm font-medium mb-2 block\">\n {t(\"size\", \"Size\")}\n </label>\n <Select\n value={selectedVariant}\n onValueChange={setSelectedVariant}\n >\n <SelectTrigger className=\"w-full\">\n <SelectValue placeholder={t(\"selectSize\", \"Select size\")} />\n </SelectTrigger>\n <SelectContent>\n {product.variants.map((variant: any) => (\n <SelectItem\n key={variant.id}\n value={variant.id}\n disabled={variant.stockQuantity === 0}\n >\n {variant.value}{\" \"}\n {variant.stockQuantity === 0 &&\n `(${t(\"outOfStock\", \"Out of Stock\")})`}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n\n {/* Quantity */}\n <div>\n <label className=\"text-sm font-medium mb-2 block\">\n {t(\"quantity\", \"Quantity\")}\n </label>\n <div className=\"flex items-center gap-3\">\n <div className=\"flex items-center border rounded-lg\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleQuantityChange(-1)}\n disabled={quantity <= 1}\n >\n <Minus className=\"h-4 w-4\" />\n </Button>\n <span className=\"w-12 text-center font-medium\">{quantity}</span>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleQuantityChange(1)}\n disabled={quantity >= product.stock}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n <span className=\"text-sm text-muted-foreground\">\n {product.stock} {t(\"available\", \"available\")}\n </span>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"space-y-3\">\n <div className=\"flex gap-3\">\n <Button\n size=\"lg\"\n className=\"flex-1\"\n disabled={product.stock <= 0 || isAdding}\n variant=\"default\"\n onClick={handleAddToCart}\n >\n {isAdding ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"adding\", \"Adding...\")}\n </>\n ) : (\n t(\"addToCart\", \"Add to Cart\")\n )}\n </Button>\n <Button variant=\"outline\" size=\"lg\">\n <Heart className=\"h-4 w-4\" />\n </Button>\n <Button variant=\"outline\" size=\"lg\">\n <Share2 className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {product.stock > 0 ? (\n <p className=\"text-sm text-green-600 dark:text-green-400 font-medium\">\n ✓ {t(\"inStockReady\", \"In stock and ready to ship\")} (\n {product.stock} {t(\"available\", \"available\")})\n </p>\n ) : (\n <p className=\"text-sm text-red-600 dark:text-red-400 font-medium\">\n ✗ {t(\"currentlyOutOfStock\", \"Currently out of stock\")}\n </p>\n )}\n </div>\n\n {/* Features */}\n <div className=\"grid grid-cols-3 gap-4 pt-6 border-t\">\n {features.map((feature, index) => {\n const Icon = feature.icon;\n return (\n <div key={index} className=\"text-center\">\n <Icon className=\"h-6 w-6 mx-auto mb-2 text-primary\" />\n <h4 className=\"text-sm font-medium\">{feature.title}</h4>\n <p className=\"text-xs text-muted-foreground\">\n {feature.description}\n </p>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n {/* Product Details Tabs */}\n <Tabs defaultValue=\"description\" className=\"mt-16\">\n <TabsList className=\"grid w-full grid-cols-2\">\n <TabsTrigger value=\"description\">\n {t(\"description\", \"Description\")}\n </TabsTrigger>\n <TabsTrigger value=\"specifications\">\n {t(\"specifications\", \"Specifications\")}\n </TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"description\" className=\"mt-6\">\n <Card>\n <CardContent className=\"pt-6\">\n <div className=\"prose max-w-none\">\n <p className=\"text-muted-foreground leading-relaxed\">\n {product.description}\n </p>\n <Separator className=\"my-6\" />\n <h3 className=\"text-lg font-semibold mb-3\">\n {t(\"keyFeatures\", \"Key Features\")}\n </h3>\n <ul className=\"space-y-2\">\n {product.tags.map((tag, index) => (\n <li key={index} className=\"flex items-center gap-2\">\n <div className=\"w-1.5 h-1.5 bg-primary rounded-full\"></div>\n <span className=\"capitalize\">{tag}</span>\n </li>\n ))}\n </ul>\n </div>\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"specifications\" className=\"mt-6\">\n <Card>\n <CardContent className=\"pt-6\">\n {product.specifications ? (\n <div className=\"grid gap-4\">\n {Object.entries(product.specifications).map(\n ([key, value]) => (\n <div\n key={key}\n className=\"bg-muted/30 rounded-lg p-4 flex justify-between items-center\"\n >\n <span className=\"font-medium text-foreground capitalize\">\n {key\n .replace(/_/g, \" \")\n .replace(/([A-Z])/g, \" $1\")\n .trim()}\n </span>\n <span className=\"text-muted-foreground font-mono bg-background px-3 py-1 rounded-md border\">\n {value}\n </span>\n </div>\n )\n )}\n </div>\n ) : (\n <p className=\"text-muted-foreground\">\n {t(\"noSpecifications\", \"No specifications available.\")}\n </p>\n )}\n </CardContent>\n </Card>\n </TabsContent>\n </Tabs>\n\n {/* Image Zoom Modal */}\n <Dialog\n open={isImageModalOpen}\n onOpenChange={(open) => {\n setIsImageModalOpen(open);\n if (!open) {\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }\n }}\n >\n <DialogContent className=\"max-w-[98vw] w-[98vw] h-[98vh] p-0 overflow-hidden bg-black/95\">\n <DialogClose className=\"absolute top-4 right-4 z-50 rounded-full bg-black/60 hover:bg-black/80 p-2 transition-all border border-white/10 backdrop-blur-md\">\n <X className=\"h-6 w-6 text-white\" />\n </DialogClose>\n\n <div className=\"relative w-full h-full flex items-center justify-center\">\n {/* Image Navigation */}\n {product.images.length > 1 && (\n <>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute left-4 z-40 bg-black/60 hover:bg-black/80 text-white border border-white/10 backdrop-blur-md\"\n onClick={() => {\n setSelectedImageIndex(\n selectedImageIndex === 0\n ? product.images.length - 1\n : selectedImageIndex - 1\n );\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n >\n <ChevronLeft className=\"h-8 w-8\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"absolute right-4 z-40 bg-black/60 hover:bg-black/80 text-white border border-white/10 backdrop-blur-md\"\n onClick={() => {\n setSelectedImageIndex(\n selectedImageIndex === product.images.length - 1\n ? 0\n : selectedImageIndex + 1\n );\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n >\n <ChevronRight className=\"h-8 w-8\" />\n </Button>\n </>\n )}\n\n {/* Zoom Controls */}\n <div className=\"absolute bottom-4 left-1/2 transform -translate-x-1/2 z-40 flex items-center gap-2 bg-black/60 backdrop-blur-md rounded-full px-4 py-3 border border-white/10\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-white hover:bg-white/20 h-8 w-8 p-0 rounded-full\"\n onClick={() => {\n const newZoom = Math.max(1, zoomLevel - 0.3);\n setZoomLevel(newZoom);\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\n }}\n disabled={zoomLevel <= 1}\n >\n <Minus className=\"h-4 w-4\" />\n </Button>\n <span className=\"text-white text-sm font-semibold min-w-[60px] text-center\">\n {Math.round(zoomLevel * 100)}%\n </span>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-white hover:bg-white/20 h-8 w-8 p-0 rounded-full\"\n onClick={() => setZoomLevel(Math.min(4, zoomLevel + 0.3))}\n disabled={zoomLevel >= 4}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n <div className=\"w-px h-6 bg-white/20 mx-2\" />\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-white hover:bg-white/20 text-xs px-3 h-8 rounded-full\"\n onClick={() => {\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n disabled={zoomLevel === 1}\n >\n Reset\n </Button>\n </div>\n\n {/* Zoomable Image */}\n <div\n className=\"relative w-full h-full flex items-center justify-center overflow-hidden select-none\"\n style={{\n cursor:\n zoomLevel > 1\n ? isDragging\n ? \"grabbing\"\n : \"grab\"\n : \"zoom-in\",\n }}\n onWheel={(e) => {\n e.preventDefault();\n const delta = e.deltaY > 0 ? -0.15 : 0.15;\n const newZoom = Math.max(1, Math.min(4, zoomLevel + delta));\n setZoomLevel(newZoom);\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\n }}\n onMouseDown={(e) => {\n if (zoomLevel > 1) {\n setIsDragging(true);\n setDragStart({\n x: e.clientX - position.x,\n y: e.clientY - position.y,\n });\n } else {\n // Double click to zoom\n if (e.detail === 2) {\n setZoomLevel(2.5);\n }\n }\n }}\n onMouseMove={(e) => {\n if (isDragging && zoomLevel > 1) {\n setPosition({\n x: e.clientX - dragStart.x,\n y: e.clientY - dragStart.y,\n });\n }\n }}\n onMouseUp={() => setIsDragging(false)}\n onMouseLeave={() => setIsDragging(false)}\n onClick={(e) => {\n if (zoomLevel === 1 && e.detail === 1) {\n // Single click to zoom in\n setZoomLevel(2.5);\n }\n }}\n >\n <div\n className=\"transition-transform duration-300 ease-out\"\n style={{\n transform: `scale(${zoomLevel}) translate(${\n position.x / zoomLevel\n }px, ${position.y / zoomLevel}px)`,\n transformOrigin: \"center center\",\n }}\n >\n <img\n src={product.images[selectedImageIndex] || \"/images/placeholder.png\"}\n alt={product.name}\n className=\"max-w-[90vw] max-h-[90vh] object-contain pointer-events-none\"\n draggable={false}\n />\n </div>\n </div>\n\n {/* Image Counter & Info */}\n <div className=\"absolute top-4 left-1/2 transform -translate-x-1/2 z-40 bg-black/60 backdrop-blur-md rounded-full px-4 py-2 border border-white/10\">\n {product.images.length > 1 ? (\n <span className=\"text-white text-sm font-semibold\">\n {selectedImageIndex + 1} / {product.images.length}\n </span>\n ) : (\n <span className=\"text-white text-xs opacity-70\">\n {t(\"zoomHint\", \"Scroll to zoom • Click & drag to pan\")}\n </span>\n )}\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </>\n );\n}\n"
21
+ "content": "import { useState } from \"react\";\nimport {\n Star,\n Heart,\n Share2,\n Truck,\n RotateCcw,\n Shield,\n Plus,\n Minus,\n ChevronLeft,\n ChevronRight,\n X,\n ZoomIn,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\nimport { useTranslation } from \"react-i18next\";\nimport constants from \"@/constants/constants.json\";\n\ninterface ProductDetailBlockProps {\n product: Product;\n}\n\nexport function ProductDetailBlock({ product }: ProductDetailBlockProps) {\n const { t } = useTranslation(\"product-detail-block\");\n const { addItem } = useCart();\n\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [quantity, setQuantity] = useState(1);\n const [selectedVariant, setSelectedVariant] = useState<string>(\"\");\n const [isAdding, setIsAdding] = useState(false);\n const [isImageModalOpen, setIsImageModalOpen] = useState(false);\n const [zoomLevel, setZoomLevel] = useState(1);\n const [position, setPosition] = useState({ x: 0, y: 0 });\n const [isDragging, setIsDragging] = useState(false);\n const [dragStart, setDragStart] = useState({ x: 0, y: 0 });\n\n const handleAddToCart = async () => {\n if (product) {\n setIsAdding(true);\n // Add multiple items by calling addItem quantity times\n for (let i = 0; i < quantity; i++) {\n addItem(product);\n }\n\n // Show success feedback\n setTimeout(() => {\n setIsAdding(false);\n }, 1000);\n }\n };\n\n const handleQuantityChange = (change: number) => {\n const newQuantity = quantity + change;\n if (newQuantity >= 1 && newQuantity <= product.stock) {\n setQuantity(newQuantity);\n }\n };\n\n const features = [\n {\n icon: Truck,\n title: t(\"freeShipping\", \"Free Shipping\"),\n description: t(\"freeShippingDesc\", \"On orders over 50\"),\n },\n {\n icon: RotateCcw,\n title: t(\"easyReturns\", \"Easy Returns\"),\n description: t(\"easyReturnsDesc\", \"30-day return policy\"),\n },\n {\n icon: Shield,\n title: t(\"secureCheckout\", \"Secure Checkout\"),\n description: t(\"secureCheckoutDesc\", \"SSL encrypted payment\"),\n },\n ];\n\n return (\n <>\n <div className=\"grid lg:grid-cols-2 gap-12\">\n {/* Product Images */}\n <div className=\"space-y-4\">\n {/* Main Image */}\n <div className=\"aspect-square relative overflow-hidden rounded-lg bg-muted group\">\n <img\n src={product.images[selectedImageIndex] || \"/images/placeholder.png\"}\n alt={product.name}\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 cursor-zoom-in\"\n onClick={() => setIsImageModalOpen(true)}\n />\n\n {/* Zoom Indicator */}\n <div className=\"absolute bottom-4 right-4 bg-black/50 text-white p-2 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity\">\n <ZoomIn className=\"w-5 h-5\" />\n </div>\n\n {/* Navigation Arrows */}\n {product.images.length > 1 && (\n <>\n <Button\n variant=\"secondary\"\n size=\"icon\"\n className=\"absolute left-4 top-1/2 transform -translate-y-1/2\"\n onClick={() =>\n setSelectedImageIndex(\n selectedImageIndex === 0\n ? product.images.length - 1\n : selectedImageIndex - 1\n )\n }\n >\n <ChevronLeft className=\"h-4 w-4\" />\n </Button>\n <Button\n variant=\"secondary\"\n size=\"icon\"\n className=\"absolute right-4 top-1/2 transform -translate-y-1/2\"\n onClick={() =>\n setSelectedImageIndex(\n selectedImageIndex === product.images.length - 1\n ? 0\n : selectedImageIndex + 1\n )\n }\n >\n <ChevronRight className=\"h-4 w-4\" />\n </Button>\n </>\n )}\n\n {/* Badges */}\n <div className=\"absolute top-4 left-4 flex flex-col gap-2\">\n {product.on_sale && (\n <Badge variant=\"destructive\">{t(\"sale\", \"Sale\")}</Badge>\n )}\n {product.is_new && (\n <Badge variant=\"secondary\">{t(\"new\", \"New\")}</Badge>\n )}\n </div>\n </div>\n\n {/* Thumbnail Images */}\n {product.images.length > 1 && (\n <div className=\"flex gap-3 overflow-x-auto\">\n {product.images.map((image, index) => (\n <button\n key={index}\n className={`flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden border-2 transition-colors ${\n selectedImageIndex === index\n ? \"border-primary\"\n : \"border-transparent hover:border-muted-foreground\"\n }`}\n onClick={() => setSelectedImageIndex(index)}\n >\n <img\n src={image}\n alt={`${product.name} ${index + 1}`}\n className=\"w-full h-full object-cover\"\n />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"space-y-6\">\n {/* Header */}\n <div>\n <div className=\"flex items-center gap-2 mb-2\">\n <Badge variant=\"outline\">\n {product.category_name ||\n product.categories?.[0]?.name ||\n product.category}\n </Badge>\n {product.featured && (\n <Badge variant=\"secondary\">{t(\"featured\", \"Featured\")}</Badge>\n )}\n </div>\n <h1 className=\"text-3xl font-bold mb-4\">{product.name}</h1>\n\n {/* Rating */}\n <div className=\"flex items-center gap-4 mb-4\">\n <div className=\"flex items-center gap-1\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={`h-4 w-4 ${\n i < Math.floor(product.rating)\n ? \"fill-current text-yellow-400\"\n : \"text-muted-foreground\"\n }`}\n />\n ))}\n <span className=\"text-sm font-medium ml-1\">\n {product.rating}\n </span>\n </div>\n </div>\n\n {/* Price */}\n <div className=\"flex items-center gap-3 mb-6\">\n <span className=\"text-3xl font-bold\">\n {formatPrice(\n product.on_sale && product.sale_price\n ? product.sale_price\n : product.price,\n constants.site.currency\n )}\n </span>\n {product.on_sale && product.sale_price && (\n <span className=\"text-xl text-muted-foreground line-through\">\n {formatPrice(\n product.price,\n constants.site.currency\n )}\n </span>\n )}\n </div>\n </div>\n\n {/* Description */}\n <div>\n <p className=\"text-muted-foreground leading-relaxed\">\n {product.description}\n </p>\n </div>\n\n {/* Variants */}\n {product.variants && product.variants.length > 0 && (\n <div>\n <label className=\"text-sm font-medium mb-2 block\">\n {t(\"size\", \"Size\")}\n </label>\n <Select\n value={selectedVariant}\n onValueChange={setSelectedVariant}\n >\n <SelectTrigger className=\"w-full\">\n <SelectValue placeholder={t(\"selectSize\", \"Select size\")} />\n </SelectTrigger>\n <SelectContent>\n {product.variants.map((variant: any) => (\n <SelectItem\n key={variant.id}\n value={variant.id}\n disabled={variant.stockQuantity === 0}\n >\n {variant.value}{\" \"}\n {variant.stockQuantity === 0 &&\n `(${t(\"outOfStock\", \"Out of Stock\")})`}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n\n {/* Quantity */}\n <div>\n <label className=\"text-sm font-medium mb-2 block\">\n {t(\"quantity\", \"Quantity\")}\n </label>\n <div className=\"flex items-center gap-3\">\n <div className=\"flex items-center border rounded-lg\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleQuantityChange(-1)}\n disabled={quantity <= 1}\n >\n <Minus className=\"h-4 w-4\" />\n </Button>\n <span className=\"w-12 text-center font-medium\">{quantity}</span>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleQuantityChange(1)}\n disabled={quantity >= product.stock}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n <span className=\"text-sm text-muted-foreground\">\n {product.stock} {t(\"available\", \"available\")}\n </span>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"space-y-3\">\n <div className=\"flex gap-3\">\n <Button\n size=\"lg\"\n className=\"flex-1\"\n disabled={product.stock <= 0 || isAdding}\n variant=\"default\"\n onClick={handleAddToCart}\n >\n {isAdding ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"adding\", \"Adding...\")}\n </>\n ) : (\n t(\"addToCart\", \"Add to Cart\")\n )}\n </Button>\n <Button variant=\"outline\" size=\"lg\">\n <Heart className=\"h-4 w-4\" />\n </Button>\n <Button variant=\"outline\" size=\"lg\">\n <Share2 className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {product.stock > 0 ? (\n <p className=\"text-sm text-green-600 dark:text-green-400 font-medium\">\n ✓ {t(\"inStockReady\", \"In stock and ready to ship\")} (\n {product.stock} {t(\"available\", \"available\")})\n </p>\n ) : (\n <p className=\"text-sm text-red-600 dark:text-red-400 font-medium\">\n ✗ {t(\"currentlyOutOfStock\", \"Currently out of stock\")}\n </p>\n )}\n </div>\n\n {/* Features */}\n <div className=\"grid grid-cols-3 gap-4 pt-6 border-t\">\n {features.map((feature, index) => {\n const Icon = feature.icon;\n return (\n <div key={index} className=\"text-center\">\n <Icon className=\"h-6 w-6 mx-auto mb-2 text-primary\" />\n <h4 className=\"text-sm font-medium\">{feature.title}</h4>\n <p className=\"text-xs text-muted-foreground\">\n {feature.description}\n </p>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n {/* Product Details Tabs */}\n <Tabs defaultValue=\"description\" className=\"mt-16\">\n <TabsList className=\"grid w-full grid-cols-2\">\n <TabsTrigger value=\"description\">\n {t(\"description\", \"Description\")}\n </TabsTrigger>\n <TabsTrigger value=\"specifications\">\n {t(\"specifications\", \"Specifications\")}\n </TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"description\" className=\"mt-6\">\n <Card>\n <CardContent className=\"pt-6\">\n <div className=\"prose max-w-none\">\n <p className=\"text-muted-foreground leading-relaxed\">\n {product.description}\n </p>\n <Separator className=\"my-6\" />\n <h3 className=\"text-lg font-semibold mb-3\">\n {t(\"keyFeatures\", \"Key Features\")}\n </h3>\n <ul className=\"space-y-2\">\n {Array.isArray(product.tags) && product.tags.map((tag, index) => (\n <li key={index} className=\"flex items-center gap-2\">\n <div className=\"w-1.5 h-1.5 bg-primary rounded-full\"></div>\n <span className=\"capitalize\">{tag}</span>\n </li>\n ))}\n </ul>\n </div>\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"specifications\" className=\"mt-6\">\n <Card>\n <CardContent className=\"pt-6\">\n {product.specifications ? (\n <div className=\"grid gap-4\">\n {Object.entries(product.specifications).map(\n ([key, value]) => (\n <div\n key={key}\n className=\"bg-muted/30 rounded-lg p-4 flex justify-between items-center\"\n >\n <span className=\"font-medium text-foreground capitalize\">\n {key\n .replace(/_/g, \" \")\n .replace(/([A-Z])/g, \" $1\")\n .trim()}\n </span>\n <span className=\"text-muted-foreground font-mono bg-background px-3 py-1 rounded-md border\">\n {value}\n </span>\n </div>\n )\n )}\n </div>\n ) : (\n <p className=\"text-muted-foreground\">\n {t(\"noSpecifications\", \"No specifications available.\")}\n </p>\n )}\n </CardContent>\n </Card>\n </TabsContent>\n </Tabs>\n\n {/* Image Zoom Modal */}\n <Dialog\n open={isImageModalOpen}\n onOpenChange={(open) => {\n setIsImageModalOpen(open);\n if (!open) {\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }\n }}\n >\n <DialogContent className=\"!max-w-[95vw] !w-[95vw] !h-[95vh] p-0 overflow-hidden bg-black/95 border-none [&>button]:hidden\">\n {/* Close Button */}\n <button\n onClick={() => setIsImageModalOpen(false)}\n className=\"absolute top-4 right-4 z-50 rounded-full bg-black/60 hover:bg-black/80 p-2 transition-all border border-white/10 backdrop-blur-md\"\n >\n <X className=\"h-6 w-6 text-white\" />\n </button>\n\n {/* Image Counter */}\n {product.images.length > 1 && (\n <div className=\"absolute top-4 left-1/2 -translate-x-1/2 z-50 bg-black/60 backdrop-blur-md rounded-full px-4 py-2 border border-white/10\">\n <span className=\"text-white text-sm font-semibold\">\n {selectedImageIndex + 1} / {product.images.length}\n </span>\n </div>\n )}\n\n {/* Navigation Buttons */}\n {product.images.length > 1 && (\n <>\n <button\n className=\"absolute left-4 top-1/2 -translate-y-1/2 z-50 bg-black/60 hover:bg-black/80 text-white border border-white/10 backdrop-blur-md rounded-full p-3 transition-all\"\n onClick={() => {\n setSelectedImageIndex(\n selectedImageIndex === 0\n ? product.images.length - 1\n : selectedImageIndex - 1\n );\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n >\n <ChevronLeft className=\"h-6 w-6\" />\n </button>\n <button\n className=\"absolute right-4 top-1/2 -translate-y-1/2 z-50 bg-black/60 hover:bg-black/80 text-white border border-white/10 backdrop-blur-md rounded-full p-3 transition-all\"\n onClick={() => {\n setSelectedImageIndex(\n selectedImageIndex === product.images.length - 1\n ? 0\n : selectedImageIndex + 1\n );\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n >\n <ChevronRight className=\"h-6 w-6\" />\n </button>\n </>\n )}\n\n {/* Zoom Controls */}\n <div className=\"absolute bottom-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-2 bg-black/60 backdrop-blur-md rounded-full px-4 py-2 border border-white/10\">\n <button\n className=\"text-white hover:bg-white/20 h-8 w-8 rounded-full flex items-center justify-center disabled:opacity-50\"\n onClick={() => {\n const newZoom = Math.max(1, zoomLevel - 0.3);\n setZoomLevel(newZoom);\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\n }}\n disabled={zoomLevel <= 1}\n >\n <Minus className=\"h-4 w-4\" />\n </button>\n <span className=\"text-white text-sm font-semibold min-w-[50px] text-center\">\n {Math.round(zoomLevel * 100)}%\n </span>\n <button\n className=\"text-white hover:bg-white/20 h-8 w-8 rounded-full flex items-center justify-center disabled:opacity-50\"\n onClick={() => setZoomLevel(Math.min(4, zoomLevel + 0.3))}\n disabled={zoomLevel >= 4}\n >\n <Plus className=\"h-4 w-4\" />\n </button>\n <div className=\"w-px h-5 bg-white/20 mx-1\" />\n <button\n className=\"text-white hover:bg-white/20 text-xs px-3 h-8 rounded-full disabled:opacity-50\"\n onClick={() => {\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n disabled={zoomLevel === 1}\n >\n Reset\n </button>\n </div>\n\n {/* Zoomable Image Container */}\n <div\n className=\"w-full h-full flex items-center justify-center overflow-hidden select-none\"\n style={{\n cursor: zoomLevel > 1 ? (isDragging ? \"grabbing\" : \"grab\") : \"zoom-in\",\n }}\n onWheel={(e) => {\n e.preventDefault();\n const delta = e.deltaY > 0 ? -0.15 : 0.15;\n const newZoom = Math.max(1, Math.min(4, zoomLevel + delta));\n setZoomLevel(newZoom);\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\n }}\n onMouseDown={(e) => {\n if (zoomLevel > 1) {\n setIsDragging(true);\n setDragStart({\n x: e.clientX - position.x,\n y: e.clientY - position.y,\n });\n } else if (e.detail === 2) {\n setZoomLevel(2.5);\n }\n }}\n onMouseMove={(e) => {\n if (isDragging && zoomLevel > 1) {\n setPosition({\n x: e.clientX - dragStart.x,\n y: e.clientY - dragStart.y,\n });\n }\n }}\n onMouseUp={() => setIsDragging(false)}\n onMouseLeave={() => setIsDragging(false)}\n onClick={(e) => {\n if (zoomLevel === 1 && e.detail === 1) {\n setZoomLevel(2.5);\n }\n }}\n >\n <img\n src={product.images[selectedImageIndex] || \"/images/placeholder.png\"}\n alt={product.name}\n className=\"max-w-[85vw] max-h-[85vh] object-contain pointer-events-none transition-transform duration-200\"\n style={{\n transform: `scale(${zoomLevel}) translate(${position.x / zoomLevel}px, ${position.y / zoomLevel}px)`,\n }}\n draggable={false}\n />\n </div>\n </DialogContent>\n </Dialog>\n </>\n );\n}\n"
22
22
  },
23
23
  {
24
24
  "path": "product-detail-block/lang/en.json",
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "product-quick-view",
3
+ "type": "registry:component",
4
+ "title": "Product Quick View",
5
+ "description": "Modal overlay for quick product preview with variant selection. Features image gallery, size/color selectors, and add to cart functionality.",
6
+ "dependencies": [
7
+ "lucide-react"
8
+ ],
9
+ "registryDependencies": [
10
+ "button",
11
+ "dialog"
12
+ ],
13
+ "usage": "import { ProductQuickView } from '@/modules/product-quick-view';\n\n<ProductQuickView product={product} open={open} onOpenChange={setOpen} />\n\n• Installed at: src/modules/product-quick-view/\n• Props: product, open, onOpenChange, onAddToCart",
14
+ "files": [
15
+ {
16
+ "path": "product-quick-view/product-quick-view.tsx",
17
+ "type": "registry:component",
18
+ "target": "$modules$/product-quick-view/product-quick-view.tsx",
19
+ "content": "\"use client\";\r\n\r\nimport { useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Link } from \"react-router\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Dialog,\r\n DialogContent,\r\n DialogTitle,\r\n} from \"@/components/ui/dialog\";\r\nimport { X, Minus, Plus, ShoppingCart } from \"lucide-react\";\r\n\r\ninterface ProductVariant {\r\n id: string;\r\n name: string;\r\n available?: boolean;\r\n}\r\n\r\ninterface Product {\r\n id: string;\r\n title: string;\r\n price: number;\r\n originalPrice?: number;\r\n description: string;\r\n images: string[];\r\n sizes?: ProductVariant[];\r\n colors?: ProductVariant[];\r\n link?: string;\r\n}\r\n\r\ninterface ProductQuickViewProps {\r\n product: Product;\r\n open: boolean;\r\n onOpenChange: (open: boolean) => void;\r\n onAddToCart?: (product: Product, quantity: number, selectedSize?: string, selectedColor?: string) => void;\r\n className?: string;\r\n}\r\n\r\nexport function ProductQuickView({\r\n product,\r\n open,\r\n onOpenChange,\r\n onAddToCart,\r\n className,\r\n}: ProductQuickViewProps) {\r\n const { t } = useTranslation(\"product-quick-view\");\r\n const [selectedImage, setSelectedImage] = useState(0);\r\n const [quantity, setQuantity] = useState(1);\r\n const [selectedSize, setSelectedSize] = useState<string | undefined>(\r\n product.sizes?.[0]?.id\r\n );\r\n const [selectedColor, setSelectedColor] = useState<string | undefined>(\r\n product.colors?.[0]?.id\r\n );\r\n\r\n const formatPrice = (price: number) => {\r\n return new Intl.NumberFormat(\"en-US\", {\r\n style: \"currency\",\r\n currency: \"USD\",\r\n }).format(price);\r\n };\r\n\r\n const handleAddToCart = () => {\r\n onAddToCart?.(product, quantity, selectedSize, selectedColor);\r\n onOpenChange(false);\r\n };\r\n\r\n const incrementQuantity = () => setQuantity((q) => q + 1);\r\n const decrementQuantity = () => setQuantity((q) => Math.max(1, q - 1));\r\n\r\n const discount = product.originalPrice\r\n ? Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)\r\n : 0;\r\n\r\n return (\r\n <Dialog open={open} onOpenChange={onOpenChange}>\r\n <DialogContent className={cn(\"sm:max-w-4xl p-0 gap-0 overflow-hidden\", className)}>\r\n <DialogTitle className=\"sr-only\">{product.title}</DialogTitle>\r\n\r\n {/* Close button */}\r\n <button\r\n onClick={() => onOpenChange(false)}\r\n className=\"absolute right-4 top-4 z-10 rounded-full bg-background/80 backdrop-blur-sm p-2 hover:bg-background transition-colors\"\r\n >\r\n <X className=\"h-4 w-4\" />\r\n <span className=\"sr-only\">Close</span>\r\n </button>\r\n\r\n <div className=\"grid md:grid-cols-2\">\r\n {/* Image Gallery */}\r\n <div className=\"relative bg-muted aspect-square md:aspect-auto md:h-full\">\r\n {/* Main Image */}\r\n <img\r\n src={product.images[selectedImage]}\r\n alt={product.title}\r\n className=\"w-full h-full object-cover\"\r\n />\r\n\r\n {/* Discount Badge */}\r\n {discount > 0 && (\r\n <span className=\"absolute top-4 left-4 bg-destructive text-destructive-foreground text-sm font-semibold px-3 py-1 rounded-full\">\r\n -{discount}%\r\n </span>\r\n )}\r\n\r\n {/* Thumbnails */}\r\n {product.images.length > 1 && (\r\n <div className=\"absolute bottom-4 left-4 flex flex-col gap-2\">\r\n {product.images.map((image, index) => (\r\n <button\r\n key={index}\r\n onClick={() => setSelectedImage(index)}\r\n className={cn(\r\n \"w-14 h-14 rounded-lg overflow-hidden border-2 transition-all bg-background/80 backdrop-blur-sm\",\r\n selectedImage === index\r\n ? \"border-primary ring-2 ring-primary/20\"\r\n : \"border-transparent opacity-70 hover:opacity-100\"\r\n )}\r\n >\r\n <img\r\n src={image}\r\n alt={`${product.title} ${index + 1}`}\r\n className=\"w-full h-full object-cover\"\r\n />\r\n </button>\r\n ))}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Product Info */}\r\n <div className=\"p-6 md:p-8 flex flex-col\">\r\n <div className=\"flex-1\">\r\n <h2 className=\"text-2xl md:text-3xl font-bold mb-2\">\r\n {product.title}\r\n </h2>\r\n\r\n {/* Price */}\r\n <div className=\"flex items-center gap-3 mb-4\">\r\n <span className=\"text-2xl font-bold text-primary\">\r\n {formatPrice(product.price)}\r\n </span>\r\n {product.originalPrice && (\r\n <span className=\"text-lg text-muted-foreground line-through\">\r\n {formatPrice(product.originalPrice)}\r\n </span>\r\n )}\r\n </div>\r\n\r\n {/* Description */}\r\n <p className=\"text-muted-foreground mb-6\">\r\n {product.description}\r\n </p>\r\n\r\n {/* Size Selector */}\r\n {product.sizes && product.sizes.length > 0 && (\r\n <div className=\"mb-6\">\r\n <label className=\"text-sm font-medium mb-2 block\">\r\n {t(\"size\")}\r\n </label>\r\n <div className=\"flex flex-wrap gap-2\">\r\n {product.sizes.map((size) => (\r\n <button\r\n key={size.id}\r\n onClick={() => setSelectedSize(size.id)}\r\n disabled={size.available === false}\r\n className={cn(\r\n \"px-4 py-2 rounded-lg border text-sm font-medium transition-all\",\r\n selectedSize === size.id\r\n ? \"border-primary bg-primary text-primary-foreground\"\r\n : \"border-border hover:border-primary\",\r\n size.available === false && \"opacity-50 cursor-not-allowed line-through\"\r\n )}\r\n >\r\n {size.name}\r\n </button>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Color Selector */}\r\n {product.colors && product.colors.length > 0 && (\r\n <div className=\"mb-6\">\r\n <label className=\"text-sm font-medium mb-2 block\">\r\n {t(\"color\")}\r\n </label>\r\n <div className=\"flex flex-wrap gap-2\">\r\n {product.colors.map((color) => (\r\n <button\r\n key={color.id}\r\n onClick={() => setSelectedColor(color.id)}\r\n disabled={color.available === false}\r\n className={cn(\r\n \"px-4 py-2 rounded-lg border text-sm font-medium transition-all\",\r\n selectedColor === color.id\r\n ? \"border-primary bg-primary text-primary-foreground\"\r\n : \"border-border hover:border-primary\",\r\n color.available === false && \"opacity-50 cursor-not-allowed line-through\"\r\n )}\r\n >\r\n {color.name}\r\n </button>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Quantity */}\r\n <div className=\"mb-6\">\r\n <label className=\"text-sm font-medium mb-2 block\">\r\n {t(\"quantity\")}\r\n </label>\r\n <div className=\"flex items-center gap-3\">\r\n <div className=\"flex items-center border border-border rounded-lg\">\r\n <button\r\n onClick={decrementQuantity}\r\n className=\"p-3 hover:bg-muted transition-colors\"\r\n disabled={quantity <= 1}\r\n >\r\n <Minus className=\"h-4 w-4\" />\r\n </button>\r\n <span className=\"w-12 text-center font-medium\">\r\n {quantity}\r\n </span>\r\n <button\r\n onClick={incrementQuantity}\r\n className=\"p-3 hover:bg-muted transition-colors\"\r\n >\r\n <Plus className=\"h-4 w-4\" />\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Actions */}\r\n <div className=\"space-y-3 pt-4 border-t border-border\">\r\n <Button\r\n onClick={handleAddToCart}\r\n className=\"w-full gap-2\"\r\n size=\"lg\"\r\n >\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {t(\"addToCart\")}\r\n </Button>\r\n\r\n {product.link && (\r\n <Link to={product.link} className=\"block\">\r\n <Button variant=\"outline\" className=\"w-full\" size=\"lg\">\r\n {t(\"viewDetails\")}\r\n </Button>\r\n </Link>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </DialogContent>\r\n </Dialog>\r\n );\r\n}\r\n"
20
+ },
21
+ {
22
+ "path": "product-quick-view/index.ts",
23
+ "type": "registry:index",
24
+ "target": "$modules$/product-quick-view/index.ts",
25
+ "content": "export * from \"./product-quick-view\";\r\n"
26
+ },
27
+ {
28
+ "path": "product-quick-view/lang/en.json",
29
+ "type": "registry:lang",
30
+ "target": "$modules$/product-quick-view/lang/en.json",
31
+ "content": "{\r\n \"size\": \"Size\",\r\n \"color\": \"Color\",\r\n \"quantity\": \"Quantity\",\r\n \"addToCart\": \"Add to Cart\",\r\n \"viewDetails\": \"View Full Details\"\r\n}\r\n"
32
+ },
33
+ {
34
+ "path": "product-quick-view/lang/tr.json",
35
+ "type": "registry:lang",
36
+ "target": "$modules$/product-quick-view/lang/tr.json",
37
+ "content": "{\r\n \"size\": \"Beden\",\r\n \"color\": \"Renk\",\r\n \"quantity\": \"Adet\",\r\n \"addToCart\": \"Sepete Ekle\",\r\n \"viewDetails\": \"Tüm Detayları Gör\"\r\n}\r\n"
38
+ }
39
+ ],
40
+ "exports": {
41
+ "types": [],
42
+ "variables": [
43
+ "ProductQuickView"
44
+ ]
45
+ }
46
+ }