@promakeai/cli 0.5.8 → 0.6.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 (135) hide show
  1. package/README.md +71 -71
  2. package/dist/index.js +259 -254
  3. package/dist/registry/about-page.json +3 -3
  4. package/dist/registry/about-section.json +4 -4
  5. package/dist/registry/animations.json +2 -2
  6. package/dist/registry/announcement-bar.json +4 -4
  7. package/dist/registry/api.json +1 -1
  8. package/dist/registry/auth-core.json +3 -3
  9. package/dist/registry/bento-grid-section.json +4 -4
  10. package/dist/registry/blog-core.json +5 -5
  11. package/dist/registry/blog-list-page.json +4 -4
  12. package/dist/registry/blog-section.json +4 -4
  13. package/dist/registry/cards-carousel-section.json +4 -4
  14. package/dist/registry/cart-drawer.json +4 -4
  15. package/dist/registry/cart-page.json +4 -4
  16. package/dist/registry/case-study-page.json +3 -3
  17. package/dist/registry/category-section.json +4 -4
  18. package/dist/registry/checkout-page.json +4 -4
  19. package/dist/registry/coming-soon-page-minimal.json +4 -4
  20. package/dist/registry/coming-soon-page.json +4 -4
  21. package/dist/registry/contact-info-grid.json +4 -4
  22. package/dist/registry/contact-page-centered.json +4 -4
  23. package/dist/registry/contact-page-map-overlay.json +3 -3
  24. package/dist/registry/contact-page-map-split.json +3 -3
  25. package/dist/registry/contact-page-split.json +4 -4
  26. package/dist/registry/contact-page.json +4 -4
  27. package/dist/registry/content-section.json +4 -4
  28. package/dist/registry/cookie-consent.json +4 -4
  29. package/dist/registry/cookies-page.json +3 -3
  30. package/dist/registry/cta-section.json +3 -3
  31. package/dist/registry/ecommerce-core.json +8 -8
  32. package/dist/registry/empty-page.json +3 -3
  33. package/dist/registry/faq-categorized.json +4 -4
  34. package/dist/registry/faq-simple.json +4 -4
  35. package/dist/registry/favorites-blog-block.json +1 -1
  36. package/dist/registry/favorites-blog-page.json +4 -4
  37. package/dist/registry/favorites-ecommerce-block.json +1 -1
  38. package/dist/registry/favorites-ecommerce-page.json +4 -4
  39. package/dist/registry/feature-section.json +3 -3
  40. package/dist/registry/featured-products.json +4 -4
  41. package/dist/registry/footer-detailed.json +4 -4
  42. package/dist/registry/footer-minimal.json +3 -3
  43. package/dist/registry/footer.json +3 -3
  44. package/dist/registry/forgot-password-page-split.json +4 -4
  45. package/dist/registry/forgot-password-page.json +4 -4
  46. package/dist/registry/google-adsense.json +4 -4
  47. package/dist/registry/google-map.json +2 -2
  48. package/dist/registry/header-centered-pill.json +4 -4
  49. package/dist/registry/header-ecommerce.json +4 -4
  50. package/dist/registry/header-mega.json +4 -4
  51. package/dist/registry/header-minimal.json +4 -4
  52. package/dist/registry/header-simple.json +3 -3
  53. package/dist/registry/hero-carousel.json +3 -3
  54. package/dist/registry/hero-cta.json +4 -4
  55. package/dist/registry/hero-gradient.json +4 -4
  56. package/dist/registry/hero-grid.json +4 -4
  57. package/dist/registry/hero-profile.json +3 -3
  58. package/dist/registry/hero.json +3 -3
  59. package/dist/registry/index.json +103 -103
  60. package/dist/registry/landing-page-app.json +3 -3
  61. package/dist/registry/landing-page-saas.json +3 -3
  62. package/dist/registry/login-page-split.json +4 -4
  63. package/dist/registry/login-page.json +4 -4
  64. package/dist/registry/logo-cloud.json +4 -4
  65. package/dist/registry/masonry-grid.json +3 -3
  66. package/dist/registry/my-orders-page.json +4 -4
  67. package/dist/registry/newsletter-section.json +4 -4
  68. package/dist/registry/order-card-compact.json +1 -1
  69. package/dist/registry/order-confirmation-page.json +4 -4
  70. package/dist/registry/order-detail-block.json +1 -1
  71. package/dist/registry/orders-list-block.json +1 -1
  72. package/dist/registry/payment-success-block.json +2 -2
  73. package/dist/registry/portfolio-page.json +4 -4
  74. package/dist/registry/post-card.json +4 -4
  75. package/dist/registry/post-detail-block.json +2 -2
  76. package/dist/registry/post-detail-page.json +4 -4
  77. package/dist/registry/pricing-card.json +3 -3
  78. package/dist/registry/pricing-page.json +4 -4
  79. package/dist/registry/pricing-section.json +4 -4
  80. package/dist/registry/privacy-page.json +3 -3
  81. package/dist/registry/product-card-detailed.json +4 -4
  82. package/dist/registry/product-card-hover.json +4 -4
  83. package/dist/registry/product-card.json +4 -4
  84. package/dist/registry/product-detail-block.json +2 -2
  85. package/dist/registry/product-detail-page.json +4 -4
  86. package/dist/registry/product-detail-section.json +4 -4
  87. package/dist/registry/product-quick-view.json +4 -4
  88. package/dist/registry/products-page.json +4 -4
  89. package/dist/registry/reading-progress.json +4 -4
  90. package/dist/registry/register-page-split.json +4 -4
  91. package/dist/registry/register-page.json +4 -4
  92. package/dist/registry/related-posts-block.json +1 -1
  93. package/dist/registry/related-products-block.json +2 -2
  94. package/dist/registry/reset-password-page-split.json +4 -4
  95. package/dist/registry/reset-password-page.json +4 -4
  96. package/dist/registry/service-card.json +1 -1
  97. package/dist/registry/share-buttons.json +4 -4
  98. package/dist/registry/skill-card.json +1 -1
  99. package/dist/registry/team-page.json +4 -4
  100. package/dist/registry/terms-page.json +3 -3
  101. package/dist/registry/testimonials-carousel.json +4 -4
  102. package/dist/registry/testimonials-grid.json +4 -4
  103. package/dist/registry/timeline-section.json +4 -4
  104. package/dist/registry/video-hero.json +4 -4
  105. package/dist/registry/youtube-embed.json +4 -4
  106. package/package.json +59 -56
  107. package/template/.env +6 -6
  108. package/template/README.md +54 -54
  109. package/template/eslint.config.js +41 -41
  110. package/template/package.json +95 -95
  111. package/template/public/_redirects +1 -1
  112. package/template/public/robots.txt +14 -14
  113. package/template/scripts/init-db.ts +18 -18
  114. package/template/src/App.tsx +19 -19
  115. package/template/src/components/FormField.tsx +48 -48
  116. package/template/src/components/FormFileInput.tsx +75 -75
  117. package/template/src/components/GoogleAnalytics.tsx +34 -34
  118. package/template/src/components/LanguageSwitcher.tsx +53 -53
  119. package/template/src/components/PasswordInput.tsx +60 -60
  120. package/template/src/components/ScriptInjector.tsx +62 -62
  121. package/template/src/components/Stack.tsx +39 -39
  122. package/template/src/constants/constants.json +67 -67
  123. package/template/src/db/index.ts +20 -20
  124. package/template/src/db/provider.tsx +78 -78
  125. package/template/src/db/schema.json +258 -258
  126. package/template/src/db/types.ts +195 -195
  127. package/template/src/hooks/use-debounced-value.ts +12 -12
  128. package/template/src/lang/index.ts +90 -90
  129. package/template/src/lib/api.ts +345 -345
  130. package/template/src/lib/env.ts +19 -19
  131. package/template/src/router.tsx +14 -14
  132. package/template/src/vite-env.d.ts +1 -1
  133. package/template/vite.config.ts +68 -64
  134. package/template/public/data/database.db-shm +0 -0
  135. package/template/public/data/database.db-wal +0 -0
@@ -12,25 +12,25 @@
12
12
  "path": "product-card/index.ts",
13
13
  "type": "registry:index",
14
14
  "target": "$modules$/product-card/index.ts",
15
- "content": "export * from './product-card';\n"
15
+ "content": "export * from './product-card';\r\n"
16
16
  },
17
17
  {
18
18
  "path": "product-card/product-card.tsx",
19
19
  "type": "registry:component",
20
20
  "target": "$modules$/product-card/product-card.tsx",
21
- "content": "import React from \"react\";\nimport { Link } from \"react-router\";\nimport { Heart, ShoppingCart } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\nimport { useCart, useFavorites, formatPrice } from \"@/modules/ecommerce-core\";\nimport { useTranslation } from \"react-i18next\";\nimport constants from \"@/constants/constants.json\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ProductCardProps {\n product: Product;\n variant?: \"grid\" | \"list\" | \"featured\" | \"compact\";\n className?: string;\n}\n\nexport const ProductCard: React.FC<ProductCardProps> = ({\n product,\n variant = \"grid\",\n className,\n}) => {\n const { addItem } = useCart();\n const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();\n const { t } = useTranslation(\"product-card\");\n\n const currentPrice = product.on_sale\n ? product.sale_price || product.price\n : product.price;\n const isProductFavorite = isFavorite(product.id);\n const isOutOfStock = product.stock <= 0;\n\n const handleAddToCart = (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n if (isOutOfStock) {\n return; // Don't add to cart if out of stock\n }\n\n addItem(product);\n };\n\n const handleToggleFavorite = (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n if (isProductFavorite) {\n removeFromFavorites(product.id);\n } else {\n addToFavorites(product);\n }\n };\n\n if (variant === \"list\") {\n return (\n <Card\n className={cn(\n \"overflow-hidden p-0 hover:shadow-lg transition-all duration-200\",\n className\n )}\n >\n <Link\n to={`/products/${product.slug}`}\n className=\"flex flex-col sm:flex-row\"\n >\n <div className=\"w-full sm:w-48 md:w-56 h-48 sm:h-56 flex-shrink-0 relative\">\n <img\n src={product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"}\n alt={product.name}\n className=\"w-full h-full object-cover\"\n />\n {/* Badges on image */}\n <div className=\"absolute top-3 left-3 flex flex-wrap gap-1.5 max-w-[calc(100%-1.5rem)]\">\n {isOutOfStock && (\n <Badge\n variant=\"secondary\"\n className=\"text-xs font-semibold shadow-md bg-muted text-muted-foreground border-0\"\n >\n {t(\"outOfStock\", \"Out of Stock\")}\n </Badge>\n )}\n {!isOutOfStock && product.on_sale && (\n <Badge\n variant=\"destructive\"\n className=\"text-xs font-semibold shadow-md bg-red-600 hover:bg-red-700 text-white border-0\"\n >\n {t(\"sale\", \"Sale\")}\n </Badge>\n )}\n {!isOutOfStock && product.is_new && (\n <Badge\n variant=\"secondary\"\n className=\"text-xs font-semibold shadow-md bg-blue-600 hover:bg-blue-700 text-white border-0\"\n >\n {t(\"new\", \"New\")}\n </Badge>\n )}\n {!isOutOfStock && product.featured && (\n <Badge\n variant=\"default\"\n className=\"text-xs font-semibold shadow-md bg-green-600 hover:bg-green-700 text-white border-0\"\n >\n {t(\"featured\", \"Featured\")}\n </Badge>\n )}\n </div>\n </div>\n <CardContent className=\"flex-1 p-4 sm:p-6\">\n <div className=\"flex flex-col sm:flex-row sm:justify-between sm:items-start h-full gap-4\">\n <div className=\"flex-1 flex flex-col justify-between\">\n <div>\n <h3 className=\"text-lg sm:text-xl font-semibold text-foreground mb-2 sm:mb-3 line-clamp-2 leading-normal\">\n {product.name}\n </h3>\n <p className=\"text-sm sm:text-base text-muted-foreground mb-3 sm:mb-4 line-clamp-2 sm:line-clamp-3 leading-relaxed\">\n {product.description}\n </p>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"flex flex-col gap-0.5\">\n {product.on_sale && (\n <span className=\"text-sm sm:text-base text-muted-foreground line-through\">\n {formatPrice(product.price, constants.site.currency)}\n </span>\n )}\n <span className=\"text-lg sm:text-xl font-bold text-foreground\">\n {formatPrice(currentPrice, constants.site.currency)}\n </span>\n {product.on_sale && (\n <span className=\"text-xs sm:text-sm text-green-600 dark:text-green-400 font-medium\">\n {t(\"save\", \"Save\")}{\" \"}\n {formatPrice(\n product.price - currentPrice,\n constants.site.currency\n )}\n </span>\n )}\n </div>\n </div>\n </div>\n <div className=\"flex sm:flex-col gap-3 sm:ml-6 justify-end sm:justify-start\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={handleToggleFavorite}\n className={cn(\n \"w-10 h-10 sm:w-12 sm:h-12 p-0 shadow-sm hover:shadow-md transition-all duration-200\",\n isProductFavorite\n ? \"text-red-600 dark:text-red-400 border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-950 hover:bg-red-100 dark:hover:bg-red-900 hover:border-red-400\"\n : \"text-muted-foreground border-border bg-card hover:bg-muted hover:text-red-500 hover:border-red-300\"\n )}\n >\n <Heart\n className={cn(\n \"w-4 h-4 sm:w-5 sm:h-5\",\n isProductFavorite && \"fill-current\"\n )}\n />\n </Button>\n <Button\n size=\"sm\"\n onClick={handleAddToCart}\n disabled={isOutOfStock}\n className={cn(\n \"w-10 h-10 sm:w-12 sm:h-12 p-0 shadow-sm hover:shadow-md transition-all duration-200\",\n isOutOfStock && \"opacity-50 cursor-not-allowed\"\n )}\n >\n <ShoppingCart className=\"w-4 h-4 sm:w-5 sm:h-5\" />\n </Button>\n </div>\n </div>\n </CardContent>\n </Link>\n </Card>\n );\n }\n\n // Grid, Featured, and Compact variants\n return (\n <Card\n className={cn(\n \"group overflow-hidden border-0 p-0 shadow-lg hover:shadow-2xl hover:-translate-y-2 transition-all duration-500 bg-card rounded-2xl h-full flex flex-col\",\n variant === \"compact\" && \"max-w-xs\",\n className\n )}\n >\n <Link to={`/products/${product.slug}`} className=\"flex flex-col h-full\">\n <div className=\"relative\">\n <div\n className={cn(\n \"w-full bg-muted overflow-hidden\",\n variant === \"featured\"\n ? \"aspect-square\"\n : variant === \"compact\"\n ? \"aspect-square\"\n : \"aspect-[4/3]\"\n )}\n >\n <img\n src={product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"}\n alt={product.name}\n className=\"absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-300\"\n />\n </div>\n\n {/* Badges */}\n <div className=\"absolute top-3 left-3 flex flex-col gap-1.5\">\n {isOutOfStock && (\n <Badge\n variant=\"secondary\"\n className=\"text-xs font-semibold shadow-lg bg-muted text-muted-foreground border-0\"\n >\n {t(\"outOfStock\", \"Out of Stock\")}\n </Badge>\n )}\n {!isOutOfStock && product.on_sale && (\n <Badge\n variant=\"destructive\"\n className=\"text-xs font-semibold shadow-lg bg-red-600 hover:bg-red-700 text-white border-0\"\n >\n {t(\"sale\", \"Sale\")}\n </Badge>\n )}\n {!isOutOfStock && product.is_new && (\n <Badge\n variant=\"secondary\"\n className=\"text-xs font-semibold shadow-lg bg-blue-600 hover:bg-blue-700 text-white border-0\"\n >\n {t(\"new\", \"New\")}\n </Badge>\n )}\n {!isOutOfStock && product.featured && (\n <Badge\n variant=\"default\"\n className=\"text-xs font-semibold shadow-lg bg-green-600 hover:bg-green-700 text-white border-0\"\n >\n {t(\"featured\", \"Featured\")}\n </Badge>\n )}\n </div>\n\n {/* Favorite button */}\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={handleToggleFavorite}\n className={cn(\n \"absolute top-3 right-3 w-10 h-10 p-0 shadow-lg backdrop-blur-sm border-2 transition-all duration-200\",\n isProductFavorite\n ? \"text-red-600 dark:text-red-400 border-red-300 dark:border-red-700 bg-card/95 hover:bg-red-50 dark:hover:bg-red-950 hover:border-red-400\"\n : \"text-muted-foreground border-border bg-card/90 hover:bg-card hover:text-red-500 hover:border-red-300\"\n )}\n >\n <Heart\n className={cn(\"w-5 h-5\", isProductFavorite && \"fill-current\")}\n />\n </Button>\n </div>\n\n <CardContent className=\"p-6 flex flex-col flex-1\">\n <h3\n className={cn(\n \"font-semibold text-foreground mb-2 line-clamp-2 leading-normal\",\n variant === \"compact\" ? \"text-sm\" : \"text-base\"\n )}\n >\n {product.name}\n </h3>\n\n {variant !== \"compact\" && (\n <p className=\"text-sm text-muted-foreground mb-4 line-clamp-2 leading-relaxed\">\n {product.description}\n </p>\n )}\n\n <div className=\"flex items-end justify-between gap-3 mt-auto\">\n <div className=\"flex flex-col gap-0.5\">\n {product.on_sale && (\n <span className=\"text-sm text-muted-foreground line-through\">\n {formatPrice(product.price, constants.site.currency)}\n </span>\n )}\n <span\n className={cn(\n \"font-bold text-foreground\",\n variant === \"compact\" ? \"text-base\" : \"text-lg\"\n )}\n >\n {formatPrice(currentPrice, constants.site.currency)}\n </span>\n {product.on_sale && (\n <span className=\"text-xs text-green-600 dark:text-green-400 font-medium\">\n {t(\"save\", \"Save\")}{\" \"}\n {formatPrice(\n product.price - currentPrice,\n constants.site.currency\n )}\n </span>\n )}\n </div>\n\n <Button\n size={variant === \"compact\" ? \"sm\" : \"default\"}\n onClick={handleAddToCart}\n disabled={isOutOfStock}\n className={cn(\n \"flex-shrink-0 shadow-sm hover:shadow-md transition-all duration-200\",\n variant === \"compact\"\n ? \"h-9 w-9 p-0\"\n : \"h-10 px-4 text-sm whitespace-nowrap\",\n isOutOfStock && \"opacity-50 cursor-not-allowed\"\n )}\n >\n <ShoppingCart\n className={cn(\n variant === \"compact\" ? \"w-4 h-4\" : \"w-4 h-4 mr-2\"\n )}\n />\n {variant !== \"compact\" &&\n (isOutOfStock\n ? t(\"outOfStock\", \"Out of Stock\")\n : t(\"addToCart\", \"Add to Cart\"))}\n </Button>\n </div>\n </CardContent>\n </Link>\n </Card>\n );\n};\n"
21
+ "content": "import React from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { Heart, ShoppingCart } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport type { Product } from \"@/modules/ecommerce-core/types\";\r\nimport { useCart, useFavorites, formatPrice } from \"@/modules/ecommerce-core\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface ProductCardProps {\r\n product: Product;\r\n variant?: \"grid\" | \"list\" | \"featured\" | \"compact\";\r\n className?: string;\r\n}\r\n\r\nexport const ProductCard: React.FC<ProductCardProps> = ({\r\n product,\r\n variant = \"grid\",\r\n className,\r\n}) => {\r\n const { addItem } = useCart();\r\n const { addToFavorites, removeFromFavorites, isFavorite } = useFavorites();\r\n const { t } = useTranslation(\"product-card\");\r\n\r\n const currentPrice = product.on_sale\r\n ? product.sale_price || product.price\r\n : product.price;\r\n const isProductFavorite = isFavorite(product.id);\r\n const isOutOfStock = product.stock <= 0;\r\n\r\n const handleAddToCart = (e: React.MouseEvent) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n if (isOutOfStock) {\r\n return; // Don't add to cart if out of stock\r\n }\r\n\r\n addItem(product);\r\n };\r\n\r\n const handleToggleFavorite = (e: React.MouseEvent) => {\r\n e.preventDefault();\r\n e.stopPropagation();\r\n\r\n if (isProductFavorite) {\r\n removeFromFavorites(product.id);\r\n } else {\r\n addToFavorites(product);\r\n }\r\n };\r\n\r\n if (variant === \"list\") {\r\n return (\r\n <Card\r\n className={cn(\r\n \"overflow-hidden p-0 hover:shadow-lg transition-all duration-200\",\r\n className\r\n )}\r\n >\r\n <Link\r\n to={`/products/${product.slug}`}\r\n className=\"flex flex-col sm:flex-row\"\r\n >\r\n <div className=\"w-full sm:w-48 md:w-56 h-48 sm:h-56 flex-shrink-0 relative\">\r\n <img\r\n src={product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"}\r\n alt={product.name}\r\n className=\"w-full h-full object-cover\"\r\n />\r\n {/* Badges on image */}\r\n <div className=\"absolute top-3 left-3 flex flex-wrap gap-1.5 max-w-[calc(100%-1.5rem)]\">\r\n {isOutOfStock && (\r\n <Badge\r\n variant=\"secondary\"\r\n className=\"text-xs font-semibold shadow-md bg-muted text-muted-foreground border-0\"\r\n >\r\n {t(\"outOfStock\", \"Out of Stock\")}\r\n </Badge>\r\n )}\r\n {!isOutOfStock && product.on_sale && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"text-xs font-semibold shadow-md bg-red-600 hover:bg-red-700 text-white border-0\"\r\n >\r\n {t(\"sale\", \"Sale\")}\r\n </Badge>\r\n )}\r\n {!isOutOfStock && product.is_new && (\r\n <Badge\r\n variant=\"secondary\"\r\n className=\"text-xs font-semibold shadow-md bg-blue-600 hover:bg-blue-700 text-white border-0\"\r\n >\r\n {t(\"new\", \"New\")}\r\n </Badge>\r\n )}\r\n {!isOutOfStock && product.featured && (\r\n <Badge\r\n variant=\"default\"\r\n className=\"text-xs font-semibold shadow-md bg-green-600 hover:bg-green-700 text-white border-0\"\r\n >\r\n {t(\"featured\", \"Featured\")}\r\n </Badge>\r\n )}\r\n </div>\r\n </div>\r\n <CardContent className=\"flex-1 p-4 sm:p-6\">\r\n <div className=\"flex flex-col sm:flex-row sm:justify-between sm:items-start h-full gap-4\">\r\n <div className=\"flex-1 flex flex-col justify-between\">\r\n <div>\r\n <h3 className=\"text-lg sm:text-xl font-semibold text-foreground mb-2 sm:mb-3 line-clamp-2 leading-normal\">\r\n {product.name}\r\n </h3>\r\n <p className=\"text-sm sm:text-base text-muted-foreground mb-3 sm:mb-4 line-clamp-2 sm:line-clamp-3 leading-relaxed\">\r\n {product.description}\r\n </p>\r\n </div>\r\n <div className=\"flex items-center gap-3\">\r\n <div className=\"flex flex-col gap-0.5\">\r\n {product.on_sale && (\r\n <span className=\"text-sm sm:text-base text-muted-foreground line-through\">\r\n {formatPrice(product.price, constants.site.currency)}\r\n </span>\r\n )}\r\n <span className=\"text-lg sm:text-xl font-bold text-foreground\">\r\n {formatPrice(currentPrice, constants.site.currency)}\r\n </span>\r\n {product.on_sale && (\r\n <span className=\"text-xs sm:text-sm text-green-600 dark:text-green-400 font-medium\">\r\n {t(\"save\", \"Save\")}{\" \"}\r\n {formatPrice(\r\n product.price - currentPrice,\r\n constants.site.currency\r\n )}\r\n </span>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n <div className=\"flex sm:flex-col gap-3 sm:ml-6 justify-end sm:justify-start\">\r\n <Button\r\n size=\"sm\"\r\n variant=\"outline\"\r\n onClick={handleToggleFavorite}\r\n className={cn(\r\n \"w-10 h-10 sm:w-12 sm:h-12 p-0 shadow-sm hover:shadow-md transition-all duration-200\",\r\n isProductFavorite\r\n ? \"text-red-600 dark:text-red-400 border-red-300 dark:border-red-700 bg-red-50 dark:bg-red-950 hover:bg-red-100 dark:hover:bg-red-900 hover:border-red-400\"\r\n : \"text-muted-foreground border-border bg-card hover:bg-muted hover:text-red-500 hover:border-red-300\"\r\n )}\r\n >\r\n <Heart\r\n className={cn(\r\n \"w-4 h-4 sm:w-5 sm:h-5\",\r\n isProductFavorite && \"fill-current\"\r\n )}\r\n />\r\n </Button>\r\n <Button\r\n size=\"sm\"\r\n onClick={handleAddToCart}\r\n disabled={isOutOfStock}\r\n className={cn(\r\n \"w-10 h-10 sm:w-12 sm:h-12 p-0 shadow-sm hover:shadow-md transition-all duration-200\",\r\n isOutOfStock && \"opacity-50 cursor-not-allowed\"\r\n )}\r\n >\r\n <ShoppingCart className=\"w-4 h-4 sm:w-5 sm:h-5\" />\r\n </Button>\r\n </div>\r\n </div>\r\n </CardContent>\r\n </Link>\r\n </Card>\r\n );\r\n }\r\n\r\n // Grid, Featured, and Compact variants\r\n return (\r\n <Card\r\n className={cn(\r\n \"group overflow-hidden border-0 p-0 shadow-lg hover:shadow-2xl hover:-translate-y-2 transition-all duration-500 bg-card rounded-2xl h-full flex flex-col\",\r\n variant === \"compact\" && \"max-w-xs\",\r\n className\r\n )}\r\n >\r\n <Link to={`/products/${product.slug}`} className=\"flex flex-col h-full\">\r\n <div className=\"relative\">\r\n <div\r\n className={cn(\r\n \"w-full bg-muted overflow-hidden\",\r\n variant === \"featured\"\r\n ? \"aspect-square\"\r\n : variant === \"compact\"\r\n ? \"aspect-square\"\r\n : \"aspect-[4/3]\"\r\n )}\r\n >\r\n <img\r\n src={product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"}\r\n alt={product.name}\r\n className=\"absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-300\"\r\n />\r\n </div>\r\n\r\n {/* Badges */}\r\n <div className=\"absolute top-3 left-3 flex flex-col gap-1.5\">\r\n {isOutOfStock && (\r\n <Badge\r\n variant=\"secondary\"\r\n className=\"text-xs font-semibold shadow-lg bg-muted text-muted-foreground border-0\"\r\n >\r\n {t(\"outOfStock\", \"Out of Stock\")}\r\n </Badge>\r\n )}\r\n {!isOutOfStock && product.on_sale && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"text-xs font-semibold shadow-lg bg-red-600 hover:bg-red-700 text-white border-0\"\r\n >\r\n {t(\"sale\", \"Sale\")}\r\n </Badge>\r\n )}\r\n {!isOutOfStock && product.is_new && (\r\n <Badge\r\n variant=\"secondary\"\r\n className=\"text-xs font-semibold shadow-lg bg-blue-600 hover:bg-blue-700 text-white border-0\"\r\n >\r\n {t(\"new\", \"New\")}\r\n </Badge>\r\n )}\r\n {!isOutOfStock && product.featured && (\r\n <Badge\r\n variant=\"default\"\r\n className=\"text-xs font-semibold shadow-lg bg-green-600 hover:bg-green-700 text-white border-0\"\r\n >\r\n {t(\"featured\", \"Featured\")}\r\n </Badge>\r\n )}\r\n </div>\r\n\r\n {/* Favorite button */}\r\n <Button\r\n size=\"sm\"\r\n variant=\"outline\"\r\n onClick={handleToggleFavorite}\r\n className={cn(\r\n \"absolute top-3 right-3 w-10 h-10 p-0 shadow-lg backdrop-blur-sm border-2 transition-all duration-200\",\r\n isProductFavorite\r\n ? \"text-red-600 dark:text-red-400 border-red-300 dark:border-red-700 bg-card/95 hover:bg-red-50 dark:hover:bg-red-950 hover:border-red-400\"\r\n : \"text-muted-foreground border-border bg-card/90 hover:bg-card hover:text-red-500 hover:border-red-300\"\r\n )}\r\n >\r\n <Heart\r\n className={cn(\"w-5 h-5\", isProductFavorite && \"fill-current\")}\r\n />\r\n </Button>\r\n </div>\r\n\r\n <CardContent className=\"p-6 flex flex-col flex-1\">\r\n <h3\r\n className={cn(\r\n \"font-semibold text-foreground mb-2 line-clamp-2 leading-normal\",\r\n variant === \"compact\" ? \"text-sm\" : \"text-base\"\r\n )}\r\n >\r\n {product.name}\r\n </h3>\r\n\r\n {variant !== \"compact\" && (\r\n <p className=\"text-sm text-muted-foreground mb-4 line-clamp-2 leading-relaxed\">\r\n {product.description}\r\n </p>\r\n )}\r\n\r\n <div className=\"flex items-end justify-between gap-3 mt-auto\">\r\n <div className=\"flex flex-col gap-0.5\">\r\n {product.on_sale && (\r\n <span className=\"text-sm text-muted-foreground line-through\">\r\n {formatPrice(product.price, constants.site.currency)}\r\n </span>\r\n )}\r\n <span\r\n className={cn(\r\n \"font-bold text-foreground\",\r\n variant === \"compact\" ? \"text-base\" : \"text-lg\"\r\n )}\r\n >\r\n {formatPrice(currentPrice, constants.site.currency)}\r\n </span>\r\n {product.on_sale && (\r\n <span className=\"text-xs text-green-600 dark:text-green-400 font-medium\">\r\n {t(\"save\", \"Save\")}{\" \"}\r\n {formatPrice(\r\n product.price - currentPrice,\r\n constants.site.currency\r\n )}\r\n </span>\r\n )}\r\n </div>\r\n\r\n <Button\r\n size={variant === \"compact\" ? \"sm\" : \"default\"}\r\n onClick={handleAddToCart}\r\n disabled={isOutOfStock}\r\n className={cn(\r\n \"flex-shrink-0 shadow-sm hover:shadow-md transition-all duration-200\",\r\n variant === \"compact\"\r\n ? \"h-9 w-9 p-0\"\r\n : \"h-10 px-4 text-sm whitespace-nowrap\",\r\n isOutOfStock && \"opacity-50 cursor-not-allowed\"\r\n )}\r\n >\r\n <ShoppingCart\r\n className={cn(\r\n variant === \"compact\" ? \"w-4 h-4\" : \"w-4 h-4 mr-2\"\r\n )}\r\n />\r\n {variant !== \"compact\" &&\r\n (isOutOfStock\r\n ? t(\"outOfStock\", \"Out of Stock\")\r\n : t(\"addToCart\", \"Add to Cart\"))}\r\n </Button>\r\n </div>\r\n </CardContent>\r\n </Link>\r\n </Card>\r\n );\r\n};\r\n"
22
22
  },
23
23
  {
24
24
  "path": "product-card/lang/en.json",
25
25
  "type": "registry:lang",
26
26
  "target": "$modules$/product-card/lang/en.json",
27
- "content": "{\n \"outOfStock\": \"Out of Stock\",\n \"sale\": \"Sale\",\n \"new\": \"New\",\n \"featured\": \"Featured\",\n \"addToCart\": \"Add to Cart\",\n \"addToCartShort\": \"Add\",\n \"save\": \"Save\"\n}\n"
27
+ "content": "{\r\n \"outOfStock\": \"Out of Stock\",\r\n \"sale\": \"Sale\",\r\n \"new\": \"New\",\r\n \"featured\": \"Featured\",\r\n \"addToCart\": \"Add to Cart\",\r\n \"addToCartShort\": \"Add\",\r\n \"save\": \"Save\"\r\n}\r\n"
28
28
  },
29
29
  {
30
30
  "path": "product-card/lang/tr.json",
31
31
  "type": "registry:lang",
32
32
  "target": "$modules$/product-card/lang/tr.json",
33
- "content": "{\n \"outOfStock\": \"Stokta Yok\",\n \"sale\": \"İndirim\",\n \"new\": \"Yeni\",\n \"featured\": \"Öne Çıkan\",\n \"addToCart\": \"Sepete Ekle\",\n \"addToCartShort\": \"Ekle\",\n \"save\": \"Tasarruf\"\n}\n"
33
+ "content": "{\r\n \"outOfStock\": \"Stokta Yok\",\r\n \"sale\": \"İndirim\",\r\n \"new\": \"Yeni\",\r\n \"featured\": \"Öne Çıkan\",\r\n \"addToCart\": \"Sepete Ekle\",\r\n \"addToCartShort\": \"Ekle\",\r\n \"save\": \"Tasarruf\"\r\n}\r\n"
34
34
  }
35
35
  ],
36
36
  "exports": {
@@ -12,13 +12,13 @@
12
12
  "path": "product-detail-block/index.ts",
13
13
  "type": "registry:index",
14
14
  "target": "$modules$/product-detail-block/index.ts",
15
- "content": "export * from './product-detail-block';\n"
15
+ "content": "export * from './product-detail-block';\r\n"
16
16
  },
17
17
  {
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, useMemo } 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, Category } from \"@/modules/ecommerce-core/types\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDbList } from \"@/db\";\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 const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\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?.length ? 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 {categoryMap.get(product.categories?.[0] as number)?.name}\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) => (\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 {String(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?.length ? 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"
21
+ "content": "import { useState, useMemo } from \"react\";\r\nimport {\r\n Star,\r\n Heart,\r\n Share2,\r\n Truck,\r\n RotateCcw,\r\n Shield,\r\n Plus,\r\n Minus,\r\n ChevronLeft,\r\n ChevronRight,\r\n X,\r\n ZoomIn,\r\n} from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\r\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\r\nimport type { Product, Category } from \"@/modules/ecommerce-core/types\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useDbList } from \"@/db\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\ninterface ProductDetailBlockProps {\r\n product: Product;\r\n}\r\n\r\nexport function ProductDetailBlock({ product }: ProductDetailBlockProps) {\r\n const { t } = useTranslation(\"product-detail-block\");\r\n const { addItem } = useCart();\r\n const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\r\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\r\n\r\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\r\n const [quantity, setQuantity] = useState(1);\r\n const [selectedVariant, setSelectedVariant] = useState<string>(\"\");\r\n const [isAdding, setIsAdding] = useState(false);\r\n const [isImageModalOpen, setIsImageModalOpen] = useState(false);\r\n const [zoomLevel, setZoomLevel] = useState(1);\r\n const [position, setPosition] = useState({ x: 0, y: 0 });\r\n const [isDragging, setIsDragging] = useState(false);\r\n const [dragStart, setDragStart] = useState({ x: 0, y: 0 });\r\n\r\n const handleAddToCart = async () => {\r\n if (product) {\r\n setIsAdding(true);\r\n // Add multiple items by calling addItem quantity times\r\n for (let i = 0; i < quantity; i++) {\r\n addItem(product);\r\n }\r\n\r\n // Show success feedback\r\n setTimeout(() => {\r\n setIsAdding(false);\r\n }, 1000);\r\n }\r\n };\r\n\r\n const handleQuantityChange = (change: number) => {\r\n const newQuantity = quantity + change;\r\n if (newQuantity >= 1 && newQuantity <= product.stock) {\r\n setQuantity(newQuantity);\r\n }\r\n };\r\n\r\n const features = [\r\n {\r\n icon: Truck,\r\n title: t(\"freeShipping\", \"Free Shipping\"),\r\n description: t(\"freeShippingDesc\", \"On orders over 50\"),\r\n },\r\n {\r\n icon: RotateCcw,\r\n title: t(\"easyReturns\", \"Easy Returns\"),\r\n description: t(\"easyReturnsDesc\", \"30-day return policy\"),\r\n },\r\n {\r\n icon: Shield,\r\n title: t(\"secureCheckout\", \"Secure Checkout\"),\r\n description: t(\"secureCheckoutDesc\", \"SSL encrypted payment\"),\r\n },\r\n ];\r\n\r\n return (\r\n <>\r\n <div className=\"grid lg:grid-cols-2 gap-12\">\r\n {/* Product Images */}\r\n <div className=\"space-y-4\">\r\n {/* Main Image */}\r\n <div className=\"aspect-square relative overflow-hidden rounded-lg bg-muted group\">\r\n <img\r\n src={product.images?.length ? product.images?.[selectedImageIndex] : \"/images/placeholder.png\"}\r\n alt={product.name}\r\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 cursor-zoom-in\"\r\n onClick={() => setIsImageModalOpen(true)}\r\n />\r\n\r\n {/* Zoom Indicator */}\r\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\">\r\n <ZoomIn className=\"w-5 h-5\" />\r\n </div>\r\n\r\n {/* Navigation Arrows */}\r\n {product?.images?.length > 1 && (\r\n <>\r\n <Button\r\n variant=\"secondary\"\r\n size=\"icon\"\r\n className=\"absolute left-4 top-1/2 transform -translate-y-1/2\"\r\n onClick={() =>\r\n setSelectedImageIndex(\r\n selectedImageIndex === 0\r\n ? product?.images?.length - 1\r\n : selectedImageIndex - 1\r\n )\r\n }\r\n >\r\n <ChevronLeft className=\"h-4 w-4\" />\r\n </Button>\r\n <Button\r\n variant=\"secondary\"\r\n size=\"icon\"\r\n className=\"absolute right-4 top-1/2 transform -translate-y-1/2\"\r\n onClick={() =>\r\n setSelectedImageIndex(\r\n selectedImageIndex === product?.images?.length - 1\r\n ? 0\r\n : selectedImageIndex + 1,\r\n )\r\n }\r\n >\r\n <ChevronRight className=\"h-4 w-4\" />\r\n </Button>\r\n </>\r\n )}\r\n\r\n {/* Badges */}\r\n <div className=\"absolute top-4 left-4 flex flex-col gap-2\">\r\n {product.on_sale && (\r\n <Badge variant=\"destructive\">{t(\"sale\", \"Sale\")}</Badge>\r\n )}\r\n {product.is_new && (\r\n <Badge variant=\"secondary\">{t(\"new\", \"New\")}</Badge>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Thumbnail Images */}\r\n {product?.images?.length > 1 && (\r\n <div className=\"flex gap-3 overflow-x-auto\">\r\n {product.images.map((image, index) => (\r\n <button\r\n key={index}\r\n className={`flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden border-2 transition-colors ${\r\n selectedImageIndex === index\r\n ? \"border-primary\"\r\n : \"border-transparent hover:border-muted-foreground\"\r\n }`}\r\n onClick={() => setSelectedImageIndex(index)}\r\n >\r\n <img\r\n src={image}\r\n alt={`${product.name} ${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=\"space-y-6\">\r\n {/* Header */}\r\n <div>\r\n <div className=\"flex items-center gap-2 mb-2\">\r\n <Badge variant=\"outline\">\r\n {categoryMap.get(product.categories?.[0] as number)?.name}\r\n </Badge>\r\n {product.featured && (\r\n <Badge variant=\"secondary\">{t(\"featured\", \"Featured\")}</Badge>\r\n )}\r\n </div>\r\n <h1 className=\"text-3xl font-bold mb-4\">{product.name}</h1>\r\n\r\n {/* Rating */}\r\n <div className=\"flex items-center gap-4 mb-4\">\r\n <div className=\"flex items-center gap-1\">\r\n {[...Array(5)].map((_, i) => (\r\n <Star\r\n key={i}\r\n className={`h-4 w-4 ${\r\n i < Math.floor(product.rating)\r\n ? \"fill-current text-yellow-400\"\r\n : \"text-muted-foreground\"\r\n }`}\r\n />\r\n ))}\r\n <span className=\"text-sm font-medium ml-1\">\r\n {product.rating}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n {/* Price */}\r\n <div className=\"flex items-center gap-3 mb-6\">\r\n <span className=\"text-3xl font-bold\">\r\n {formatPrice(\r\n product.on_sale && product.sale_price\r\n ? product.sale_price\r\n : product.price,\r\n constants.site.currency\r\n )}\r\n </span>\r\n {product.on_sale && product.sale_price && (\r\n <span className=\"text-xl text-muted-foreground line-through\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </span>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Description */}\r\n <div>\r\n <p className=\"text-muted-foreground leading-relaxed\">\r\n {product.description}\r\n </p>\r\n </div>\r\n\r\n {/* Variants */}\r\n {product.variants && product.variants.length > 0 && (\r\n <div>\r\n <label className=\"text-sm font-medium mb-2 block\">\r\n {t(\"size\", \"Size\")}\r\n </label>\r\n <Select\r\n value={selectedVariant}\r\n onValueChange={setSelectedVariant}\r\n >\r\n <SelectTrigger className=\"w-full\">\r\n <SelectValue placeholder={t(\"selectSize\", \"Select size\")} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {product.variants.map((variant) => (\r\n <SelectItem\r\n key={variant.id}\r\n value={variant.id}\r\n disabled={variant.stockQuantity === 0}\r\n >\r\n {variant.value}{\" \"}\r\n {variant.stockQuantity === 0 &&\r\n `(${t(\"outOfStock\", \"Out of Stock\")})`}\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n </div>\r\n )}\r\n\r\n {/* Quantity */}\r\n <div>\r\n <label className=\"text-sm font-medium mb-2 block\">\r\n {t(\"quantity\", \"Quantity\")}\r\n </label>\r\n <div className=\"flex items-center gap-3\">\r\n <div className=\"flex items-center border rounded-lg\">\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n onClick={() => handleQuantityChange(-1)}\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\">{quantity}</span>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n onClick={() => handleQuantityChange(1)}\r\n disabled={quantity >= product.stock}\r\n >\r\n <Plus className=\"h-4 w-4\" />\r\n </Button>\r\n </div>\r\n <span className=\"text-sm text-muted-foreground\">\r\n {product.stock} {t(\"available\", \"available\")}\r\n </span>\r\n </div>\r\n </div>\r\n\r\n {/* Actions */}\r\n <div className=\"space-y-3\">\r\n <div className=\"flex gap-3\">\r\n <Button\r\n size=\"lg\"\r\n className=\"flex-1\"\r\n disabled={product.stock <= 0 || isAdding}\r\n variant=\"default\"\r\n onClick={handleAddToCart}\r\n >\r\n {isAdding ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"adding\", \"Adding...\")}\r\n </>\r\n ) : (\r\n t(\"addToCart\", \"Add to Cart\")\r\n )}\r\n </Button>\r\n <Button variant=\"outline\" size=\"lg\">\r\n <Heart className=\"h-4 w-4\" />\r\n </Button>\r\n <Button variant=\"outline\" size=\"lg\">\r\n <Share2 className=\"h-4 w-4\" />\r\n </Button>\r\n </div>\r\n\r\n {product.stock > 0 ? (\r\n <p className=\"text-sm text-green-600 dark:text-green-400 font-medium\">\r\n ✓ {t(\"inStockReady\", \"In stock and ready to ship\")} (\r\n {product.stock} {t(\"available\", \"available\")})\r\n </p>\r\n ) : (\r\n <p className=\"text-sm text-red-600 dark:text-red-400 font-medium\">\r\n ✗ {t(\"currentlyOutOfStock\", \"Currently out of stock\")}\r\n </p>\r\n )}\r\n </div>\r\n\r\n {/* Features */}\r\n <div className=\"grid grid-cols-3 gap-4 pt-6 border-t\">\r\n {features.map((feature, index) => {\r\n const Icon = feature.icon;\r\n return (\r\n <div key={index} className=\"text-center\">\r\n <Icon className=\"h-6 w-6 mx-auto mb-2 text-primary\" />\r\n <h4 className=\"text-sm font-medium\">{feature.title}</h4>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {feature.description}\r\n </p>\r\n </div>\r\n );\r\n })}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Product Details Tabs */}\r\n <Tabs defaultValue=\"description\" className=\"mt-16\">\r\n <TabsList className=\"grid w-full grid-cols-2\">\r\n <TabsTrigger value=\"description\">\r\n {t(\"description\", \"Description\")}\r\n </TabsTrigger>\r\n <TabsTrigger value=\"specifications\">\r\n {t(\"specifications\", \"Specifications\")}\r\n </TabsTrigger>\r\n </TabsList>\r\n\r\n <TabsContent value=\"description\" className=\"mt-6\">\r\n <Card>\r\n <CardContent className=\"pt-6\">\r\n <div className=\"prose max-w-none\">\r\n <p className=\"text-muted-foreground leading-relaxed\">\r\n {product.description}\r\n </p>\r\n <Separator className=\"my-6\" />\r\n <h3 className=\"text-lg font-semibold mb-3\">\r\n {t(\"keyFeatures\", \"Key Features\")}\r\n </h3>\r\n <ul className=\"space-y-2\">\r\n {Array.isArray(product.tags) && product.tags.map((tag, index) => (\r\n <li key={index} className=\"flex items-center gap-2\">\r\n <div className=\"w-1.5 h-1.5 bg-primary rounded-full\"></div>\r\n <span className=\"capitalize\">{tag}</span>\r\n </li>\r\n ))}\r\n </ul>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </TabsContent>\r\n\r\n <TabsContent value=\"specifications\" className=\"mt-6\">\r\n <Card>\r\n <CardContent className=\"pt-6\">\r\n {product.specifications ? (\r\n <div className=\"grid gap-4\">\r\n {Object.entries(product.specifications).map(\r\n ([key, value]) => (\r\n <div\r\n key={key}\r\n className=\"bg-muted/30 rounded-lg p-4 flex justify-between items-center\"\r\n >\r\n <span className=\"font-medium text-foreground capitalize\">\r\n {key\r\n .replace(/_/g, \" \")\r\n .replace(/([A-Z])/g, \" $1\")\r\n .trim()}\r\n </span>\r\n <span className=\"text-muted-foreground font-mono bg-background px-3 py-1 rounded-md border\">\r\n {String(value)}\r\n </span>\r\n </div>\r\n )\r\n )}\r\n </div>\r\n ) : (\r\n <p className=\"text-muted-foreground\">\r\n {t(\"noSpecifications\", \"No specifications available.\")}\r\n </p>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </TabsContent>\r\n </Tabs>\r\n\r\n {/* Image Zoom Modal */}\r\n <Dialog\r\n open={isImageModalOpen}\r\n onOpenChange={(open) => {\r\n setIsImageModalOpen(open);\r\n if (!open) {\r\n setZoomLevel(1);\r\n setPosition({ x: 0, y: 0 });\r\n }\r\n }}\r\n >\r\n <DialogContent className=\"!max-w-[95vw] !w-[95vw] !h-[95vh] p-0 overflow-hidden bg-black/95 border-none [&>button]:hidden\">\r\n {/* Close Button */}\r\n <button\r\n onClick={() => setIsImageModalOpen(false)}\r\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\"\r\n >\r\n <X className=\"h-6 w-6 text-white\" />\r\n </button>\r\n\r\n {/* Image Counter */}\r\n {product?.images?.length > 1 && (\r\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\">\r\n <span className=\"text-white text-sm font-semibold\">\r\n {selectedImageIndex + 1} / {product?.images?.length}\r\n </span>\r\n </div>\r\n )}\r\n\r\n {/* Navigation Buttons */}\r\n {product?.images?.length > 1 && (\r\n <>\r\n <button\r\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\"\r\n onClick={() => {\r\n setSelectedImageIndex(\r\n selectedImageIndex === 0\r\n ? product?.images?.length - 1\r\n : selectedImageIndex - 1\r\n );\r\n setZoomLevel(1);\r\n setPosition({ x: 0, y: 0 });\r\n }}\r\n >\r\n <ChevronLeft className=\"h-6 w-6\" />\r\n </button>\r\n <button\r\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\"\r\n onClick={() => {\r\n setSelectedImageIndex(\r\n selectedImageIndex === product?.images?.length - 1\r\n ? 0\r\n : selectedImageIndex + 1\r\n );\r\n setZoomLevel(1);\r\n setPosition({ x: 0, y: 0 });\r\n }}\r\n >\r\n <ChevronRight className=\"h-6 w-6\" />\r\n </button>\r\n </>\r\n )}\r\n\r\n {/* Zoom Controls */}\r\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\">\r\n <button\r\n className=\"text-white hover:bg-white/20 h-8 w-8 rounded-full flex items-center justify-center disabled:opacity-50\"\r\n onClick={() => {\r\n const newZoom = Math.max(1, zoomLevel - 0.3);\r\n setZoomLevel(newZoom);\r\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\r\n }}\r\n disabled={zoomLevel <= 1}\r\n >\r\n <Minus className=\"h-4 w-4\" />\r\n </button>\r\n <span className=\"text-white text-sm font-semibold min-w-[50px] text-center\">\r\n {Math.round(zoomLevel * 100)}%\r\n </span>\r\n <button\r\n className=\"text-white hover:bg-white/20 h-8 w-8 rounded-full flex items-center justify-center disabled:opacity-50\"\r\n onClick={() => setZoomLevel(Math.min(4, zoomLevel + 0.3))}\r\n disabled={zoomLevel >= 4}\r\n >\r\n <Plus className=\"h-4 w-4\" />\r\n </button>\r\n <div className=\"w-px h-5 bg-white/20 mx-1\" />\r\n <button\r\n className=\"text-white hover:bg-white/20 text-xs px-3 h-8 rounded-full disabled:opacity-50\"\r\n onClick={() => {\r\n setZoomLevel(1);\r\n setPosition({ x: 0, y: 0 });\r\n }}\r\n disabled={zoomLevel === 1}\r\n >\r\n Reset\r\n </button>\r\n </div>\r\n\r\n {/* Zoomable Image Container */}\r\n <div\r\n className=\"w-full h-full flex items-center justify-center overflow-hidden select-none\"\r\n style={{\r\n cursor: zoomLevel > 1 ? (isDragging ? \"grabbing\" : \"grab\") : \"zoom-in\",\r\n }}\r\n onWheel={(e) => {\r\n e.preventDefault();\r\n const delta = e.deltaY > 0 ? -0.15 : 0.15;\r\n const newZoom = Math.max(1, Math.min(4, zoomLevel + delta));\r\n setZoomLevel(newZoom);\r\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\r\n }}\r\n onMouseDown={(e) => {\r\n if (zoomLevel > 1) {\r\n setIsDragging(true);\r\n setDragStart({\r\n x: e.clientX - position.x,\r\n y: e.clientY - position.y,\r\n });\r\n } else if (e.detail === 2) {\r\n setZoomLevel(2.5);\r\n }\r\n }}\r\n onMouseMove={(e) => {\r\n if (isDragging && zoomLevel > 1) {\r\n setPosition({\r\n x: e.clientX - dragStart.x,\r\n y: e.clientY - dragStart.y,\r\n });\r\n }\r\n }}\r\n onMouseUp={() => setIsDragging(false)}\r\n onMouseLeave={() => setIsDragging(false)}\r\n onClick={(e) => {\r\n if (zoomLevel === 1 && e.detail === 1) {\r\n setZoomLevel(2.5);\r\n }\r\n }}\r\n >\r\n <img\r\n src={product.images?.length ? product.images?.[selectedImageIndex] : \"/images/placeholder.png\"}\r\n alt={product.name}\r\n className=\"max-w-[85vw] max-h-[85vh] object-contain pointer-events-none transition-transform duration-200\"\r\n style={{\r\n transform: `scale(${zoomLevel}) translate(${position.x / zoomLevel}px, ${position.y / zoomLevel}px)`,\r\n }}\r\n draggable={false}\r\n />\r\n </div>\r\n </DialogContent>\r\n </Dialog>\r\n </>\r\n );\r\n}\r\n"
22
22
  },
23
23
  {
24
24
  "path": "product-detail-block/lang/en.json",
@@ -17,25 +17,25 @@
17
17
  "path": "product-detail-page/index.ts",
18
18
  "type": "registry:index",
19
19
  "target": "$modules$/product-detail-page/index.ts",
20
- "content": "export * from './product-detail-page';\nexport { ProductDetailPage as default } from './product-detail-page';\n"
20
+ "content": "export * from './product-detail-page';\r\nexport { ProductDetailPage as default } from './product-detail-page';\r\n"
21
21
  },
22
22
  {
23
23
  "path": "product-detail-page/product-detail-page.tsx",
24
24
  "type": "registry:page",
25
25
  "target": "$modules$/product-detail-page/product-detail-page.tsx",
26
- "content": "import { useParams } from \"react-router\";\nimport { useDbGet } from \"@/db\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\nimport { ProductDetailBlock } from \"@/modules/product-detail-block\";\nimport { Layout } from \"@/components/Layout\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\n\nexport function ProductDetailPage() {\n const { t } = useTranslation(\"product-detail-page\");\n const { slug } = useParams<{ slug: string }>();\n const { data: product, isLoading: loading, error } = useDbGet<Product>(\"products\", {\n where: { slug: slug || \"\" },\n enabled: !!slug,\n });\n\n usePageTitle({ title: product?.name || t(\"loading\", \"Loading...\") });\n\n if (loading) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"animate-pulse\">\n <div className=\"grid lg:grid-cols-2 gap-12\">\n <div className=\"aspect-square bg-muted rounded-lg\"></div>\n <div className=\"space-y-4\">\n <div className=\"h-6 bg-muted rounded w-1/4\"></div>\n <div className=\"h-10 bg-muted rounded w-3/4\"></div>\n <div className=\"h-4 bg-muted rounded w-1/3\"></div>\n <div className=\"h-8 bg-muted rounded w-1/4\"></div>\n <div className=\"space-y-2\">\n <div className=\"h-4 bg-muted rounded\"></div>\n <div className=\"h-4 bg-muted rounded\"></div>\n <div className=\"h-4 bg-muted rounded w-2/3\"></div>\n </div>\n <div className=\"h-12 bg-muted rounded w-1/2\"></div>\n </div>\n </div>\n </div>\n </div>\n </Layout>\n );\n }\n\n if (error || !product) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8 text-center\">\n <h1 className=\"text-2xl font-bold mb-4\">{t(\"notFound\", \"Product Not Found\")}</h1>\n <p className=\"text-muted-foreground\">{t(\"notFoundDescription\", \"The product you're looking for doesn't exist or has been removed.\")}</p>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <ProductDetailBlock product={product} />\n </div>\n </Layout>\n );\n}\n\nexport default ProductDetailPage;\n"
26
+ "content": "import { useParams } from \"react-router\";\r\nimport { useDbGet } from \"@/db\";\r\nimport type { Product } from \"@/modules/ecommerce-core/types\";\r\nimport { ProductDetailBlock } from \"@/modules/product-detail-block\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\n\r\nexport function ProductDetailPage() {\r\n const { t } = useTranslation(\"product-detail-page\");\r\n const { slug } = useParams<{ slug: string }>();\r\n const { data: product, isLoading: loading, error } = useDbGet<Product>(\"products\", {\r\n where: { slug: slug || \"\" },\r\n enabled: !!slug,\r\n });\r\n\r\n usePageTitle({ title: product?.name || t(\"loading\", \"Loading...\") });\r\n\r\n if (loading) {\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=\"animate-pulse\">\r\n <div className=\"grid lg:grid-cols-2 gap-12\">\r\n <div className=\"aspect-square bg-muted rounded-lg\"></div>\r\n <div className=\"space-y-4\">\r\n <div className=\"h-6 bg-muted rounded w-1/4\"></div>\r\n <div className=\"h-10 bg-muted rounded w-3/4\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/3\"></div>\r\n <div className=\"h-8 bg-muted rounded w-1/4\"></div>\r\n <div className=\"space-y-2\">\r\n <div className=\"h-4 bg-muted rounded\"></div>\r\n <div className=\"h-4 bg-muted rounded\"></div>\r\n <div className=\"h-4 bg-muted rounded w-2/3\"></div>\r\n </div>\r\n <div className=\"h-12 bg-muted rounded w-1/2\"></div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n if (error || !product) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8 text-center\">\r\n <h1 className=\"text-2xl font-bold mb-4\">{t(\"notFound\", \"Product Not Found\")}</h1>\r\n <p className=\"text-muted-foreground\">{t(\"notFoundDescription\", \"The product you're looking for doesn't exist or has been removed.\")}</p>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <ProductDetailBlock product={product} />\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ProductDetailPage;\r\n"
27
27
  },
28
28
  {
29
29
  "path": "product-detail-page/lang/en.json",
30
30
  "type": "registry:lang",
31
31
  "target": "$modules$/product-detail-page/lang/en.json",
32
- "content": "{\n \"loading\": \"Loading...\",\n \"notFound\": \"Product Not Found\",\n \"notFoundDescription\": \"The product you're looking for doesn't exist or has been removed.\"\n}\n"
32
+ "content": "{\r\n \"loading\": \"Loading...\",\r\n \"notFound\": \"Product Not Found\",\r\n \"notFoundDescription\": \"The product you're looking for doesn't exist or has been removed.\"\r\n}\r\n"
33
33
  },
34
34
  {
35
35
  "path": "product-detail-page/lang/tr.json",
36
36
  "type": "registry:lang",
37
37
  "target": "$modules$/product-detail-page/lang/tr.json",
38
- "content": "{\n \"loading\": \"Yükleniyor...\",\n \"notFound\": \"Ürün Bulunamadı\",\n \"notFoundDescription\": \"Aradığınız ürün mevcut değil veya kaldırılmış.\"\n}\n"
38
+ "content": "{\r\n \"loading\": \"Yükleniyor...\",\r\n \"notFound\": \"Ürün Bulunamadı\",\r\n \"notFoundDescription\": \"Aradığınız ürün mevcut değil veya kaldırılmış.\"\r\n}\r\n"
39
39
  }
40
40
  ],
41
41
  "exports": {
@@ -17,25 +17,25 @@
17
17
  "path": "product-detail-section/index.ts",
18
18
  "type": "registry:index",
19
19
  "target": "$modules$/product-detail-section/index.ts",
20
- "content": "export * from './product-detail-section';\n"
20
+ "content": "export * from './product-detail-section';\r\n"
21
21
  },
22
22
  {
23
23
  "path": "product-detail-section/product-detail-section.tsx",
24
24
  "type": "registry:component",
25
25
  "target": "$modules$/product-detail-section/product-detail-section.tsx",
26
- "content": "import { useMemo } from \"react\";\nimport { Star, Heart, Facebook, Twitter, MessageCircle } from \"lucide-react\";\nimport { toast } from \"sonner\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { useTranslation } from \"react-i18next\";\nimport {\n useCart,\n useFavorites,\n formatPrice,\n type Product,\n} from \"@/modules/ecommerce-core\";\nimport type { Category } from \"@/modules/ecommerce-core/types\";\nimport { useDbList } from \"@/db\";\nimport constants from \"@/constants/constants.json\";\n\ninterface ProductDetailSectionProps {\n product: Product;\n className?: string;\n}\n\nexport function ProductDetailSection({\n product,\n className,\n}: ProductDetailSectionProps) {\n const { t } = useTranslation(\"product-detail-section\");\n const { addItem } = useCart();\n const { isFavorite, addToFavorites, removeFromFavorites } = useFavorites();\n const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\n const currency = (constants.site as any).currency || \"USD\";\n\n if (!product) {\n return null;\n }\n\n const price = product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n\n const handleAddToCart = () => {\n addItem(product);\n toast.success(t(\"addedToCart\", \"Added to cart!\"));\n };\n\n const handleToggleFavorite = () => {\n if (isFavorite(product.id)) {\n removeFromFavorites(product.id);\n } else {\n addToFavorites(product);\n }\n };\n\n const renderStars = (rating: number) => {\n return Array.from({ length: 5 }, (_, i) => (\n <Star\n key={i}\n className={cn(\n \"w-4 h-4\",\n i < Math.floor(rating) ? \"fill-primary text-primary\" : \"text-primary\"\n )}\n />\n ));\n };\n\n return (\n <section className={cn(\"py-24\", className)}>\n <div className=\"container px-5 mx-auto\">\n <div className=\"lg:w-4/5 mx-auto flex flex-wrap\">\n <img\n alt={product.name}\n className=\"lg:w-1/2 w-full lg:h-auto h-64 object-cover object-center rounded\"\n src={product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"}\n />\n <div className=\"lg:w-1/2 w-full lg:pl-10 lg:py-6 mt-6 lg:mt-0\">\n {product.brand && (\n <h2 className=\"text-sm text-muted-foreground tracking-widest uppercase\">\n {product.brand}\n </h2>\n )}\n <h1 className=\"text-3xl font-medium mb-1\">{product.name}</h1>\n\n <div className=\"flex mb-4\">\n <span className=\"flex items-center\">\n {renderStars(product.rating)}\n <span className=\"text-muted-foreground ml-3\">\n {product.review_count} {t(\"reviews\", \"Reviews\")}\n </span>\n </span>\n <span className=\"flex ml-3 pl-3 py-2 border-l-2 border-border space-x-2\">\n <a\n href=\"#\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Facebook className=\"w-5 h-5\" />\n </a>\n <a\n href=\"#\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <Twitter className=\"w-5 h-5\" />\n </a>\n <a\n href=\"#\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <MessageCircle className=\"w-5 h-5\" />\n </a>\n </span>\n </div>\n\n <p className=\"leading-relaxed text-muted-foreground\">\n {product.description}\n </p>\n\n {categoryMap.get(product.categories?.[0] as number)?.name && (\n <p className=\"mt-4 text-sm text-muted-foreground\">\n {t(\"category\", \"Category\")}: {categoryMap.get(product.categories?.[0] as number)?.name}\n </p>\n )}\n\n <div className=\"flex mt-6 items-center pb-5 border-b-2 border-border mb-5\">\n {product.stock > 0 ? (\n <span className=\"text-sm text-green-600 dark:text-green-400\">\n {t(\"inStock\", \"In Stock\")} ({product.stock})\n </span>\n ) : (\n <span className=\"text-sm text-red-600 dark:text-red-400\">\n {t(\"outOfStock\", \"Out of Stock\")}\n </span>\n )}\n </div>\n\n <div className=\"flex items-center\">\n <span className=\"font-medium text-2xl\">\n {formatPrice(price, currency)}\n {product.on_sale && product.sale_price && (\n <span className=\"text-lg text-muted-foreground line-through ml-2\">\n {formatPrice(product.price, currency)}\n </span>\n )}\n </span>\n <Button\n onClick={handleAddToCart}\n className=\"ml-auto\"\n disabled={product.stock === 0}\n >\n {t(\"addToCart\", \"Add to Cart\")}\n </Button>\n <Button\n variant=\"outline\"\n size=\"icon\"\n onClick={handleToggleFavorite}\n className=\"rounded-full ml-4\"\n >\n <Heart\n className={cn(\n \"w-5 h-5\",\n isFavorite(product.id) && \"fill-current text-red-500\"\n )}\n />\n </Button>\n </div>\n </div>\n </div>\n </div>\n </section>\n );\n}\n"
26
+ "content": "import { useMemo } from \"react\";\r\nimport { Star, Heart, Facebook, Twitter, MessageCircle } from \"lucide-react\";\r\nimport { toast } from \"sonner\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport {\r\n useCart,\r\n useFavorites,\r\n formatPrice,\r\n type Product,\r\n} from \"@/modules/ecommerce-core\";\r\nimport type { Category } from \"@/modules/ecommerce-core/types\";\r\nimport { useDbList } from \"@/db\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\ninterface ProductDetailSectionProps {\r\n product: Product;\r\n className?: string;\r\n}\r\n\r\nexport function ProductDetailSection({\r\n product,\r\n className,\r\n}: ProductDetailSectionProps) {\r\n const { t } = useTranslation(\"product-detail-section\");\r\n const { addItem } = useCart();\r\n const { isFavorite, addToFavorites, removeFromFavorites } = useFavorites();\r\n const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\r\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\r\n const currency = (constants.site as any).currency || \"USD\";\r\n\r\n if (!product) {\r\n return null;\r\n }\r\n\r\n const price = product.on_sale && product.sale_price\r\n ? product.sale_price\r\n : product.price;\r\n\r\n const handleAddToCart = () => {\r\n addItem(product);\r\n toast.success(t(\"addedToCart\", \"Added to cart!\"));\r\n };\r\n\r\n const handleToggleFavorite = () => {\r\n if (isFavorite(product.id)) {\r\n removeFromFavorites(product.id);\r\n } else {\r\n addToFavorites(product);\r\n }\r\n };\r\n\r\n const renderStars = (rating: number) => {\r\n return Array.from({ length: 5 }, (_, i) => (\r\n <Star\r\n key={i}\r\n className={cn(\r\n \"w-4 h-4\",\r\n i < Math.floor(rating) ? \"fill-primary text-primary\" : \"text-primary\"\r\n )}\r\n />\r\n ));\r\n };\r\n\r\n return (\r\n <section className={cn(\"py-24\", className)}>\r\n <div className=\"container px-5 mx-auto\">\r\n <div className=\"lg:w-4/5 mx-auto flex flex-wrap\">\r\n <img\r\n alt={product.name}\r\n className=\"lg:w-1/2 w-full lg:h-auto h-64 object-cover object-center rounded\"\r\n src={product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"}\r\n />\r\n <div className=\"lg:w-1/2 w-full lg:pl-10 lg:py-6 mt-6 lg:mt-0\">\r\n {product.brand && (\r\n <h2 className=\"text-sm text-muted-foreground tracking-widest uppercase\">\r\n {product.brand}\r\n </h2>\r\n )}\r\n <h1 className=\"text-3xl font-medium mb-1\">{product.name}</h1>\r\n\r\n <div className=\"flex mb-4\">\r\n <span className=\"flex items-center\">\r\n {renderStars(product.rating)}\r\n <span className=\"text-muted-foreground ml-3\">\r\n {product.review_count} {t(\"reviews\", \"Reviews\")}\r\n </span>\r\n </span>\r\n <span className=\"flex ml-3 pl-3 py-2 border-l-2 border-border space-x-2\">\r\n <a\r\n href=\"#\"\r\n className=\"text-muted-foreground hover:text-foreground\"\r\n >\r\n <Facebook className=\"w-5 h-5\" />\r\n </a>\r\n <a\r\n href=\"#\"\r\n className=\"text-muted-foreground hover:text-foreground\"\r\n >\r\n <Twitter className=\"w-5 h-5\" />\r\n </a>\r\n <a\r\n href=\"#\"\r\n className=\"text-muted-foreground hover:text-foreground\"\r\n >\r\n <MessageCircle className=\"w-5 h-5\" />\r\n </a>\r\n </span>\r\n </div>\r\n\r\n <p className=\"leading-relaxed text-muted-foreground\">\r\n {product.description}\r\n </p>\r\n\r\n {categoryMap.get(product.categories?.[0] as number)?.name && (\r\n <p className=\"mt-4 text-sm text-muted-foreground\">\r\n {t(\"category\", \"Category\")}: {categoryMap.get(product.categories?.[0] as number)?.name}\r\n </p>\r\n )}\r\n\r\n <div className=\"flex mt-6 items-center pb-5 border-b-2 border-border mb-5\">\r\n {product.stock > 0 ? (\r\n <span className=\"text-sm text-green-600 dark:text-green-400\">\r\n {t(\"inStock\", \"In Stock\")} ({product.stock})\r\n </span>\r\n ) : (\r\n <span className=\"text-sm text-red-600 dark:text-red-400\">\r\n {t(\"outOfStock\", \"Out of Stock\")}\r\n </span>\r\n )}\r\n </div>\r\n\r\n <div className=\"flex items-center\">\r\n <span className=\"font-medium text-2xl\">\r\n {formatPrice(price, currency)}\r\n {product.on_sale && product.sale_price && (\r\n <span className=\"text-lg text-muted-foreground line-through ml-2\">\r\n {formatPrice(product.price, currency)}\r\n </span>\r\n )}\r\n </span>\r\n <Button\r\n onClick={handleAddToCart}\r\n className=\"ml-auto\"\r\n disabled={product.stock === 0}\r\n >\r\n {t(\"addToCart\", \"Add to Cart\")}\r\n </Button>\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n onClick={handleToggleFavorite}\r\n className=\"rounded-full ml-4\"\r\n >\r\n <Heart\r\n className={cn(\r\n \"w-5 h-5\",\r\n isFavorite(product.id) && \"fill-current text-red-500\"\r\n )}\r\n />\r\n </Button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
27
27
  },
28
28
  {
29
29
  "path": "product-detail-section/lang/en.json",
30
30
  "type": "registry:lang",
31
31
  "target": "$modules$/product-detail-section/lang/en.json",
32
- "content": "{\n \"reviews\": \"Reviews\",\n \"color\": \"Color\",\n \"size\": \"Size\",\n \"addToCart\": \"Add to Cart\"\n}\n"
32
+ "content": "{\r\n \"reviews\": \"Reviews\",\r\n \"color\": \"Color\",\r\n \"size\": \"Size\",\r\n \"addToCart\": \"Add to Cart\"\r\n}\r\n"
33
33
  },
34
34
  {
35
35
  "path": "product-detail-section/lang/tr.json",
36
36
  "type": "registry:lang",
37
37
  "target": "$modules$/product-detail-section/lang/tr.json",
38
- "content": "{\n \"reviews\": \"Değerlendirme\",\n \"color\": \"Renk\",\n \"size\": \"Beden\",\n \"addToCart\": \"Sepete Ekle\"\n}\n"
38
+ "content": "{\r\n \"reviews\": \"Değerlendirme\",\r\n \"color\": \"Renk\",\r\n \"size\": \"Beden\",\r\n \"addToCart\": \"Sepete Ekle\"\r\n}\r\n"
39
39
  }
40
40
  ],
41
41
  "exports": {
@@ -17,25 +17,25 @@
17
17
  "path": "product-quick-view/product-quick-view.tsx",
18
18
  "type": "registry:component",
19
19
  "target": "$modules$/product-quick-view/product-quick-view.tsx",
20
- "content": "\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router\";\nimport { toast } from \"sonner\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Dialog,\n DialogContent,\n DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { X, Minus, Plus, ShoppingCart, Heart } from \"lucide-react\";\nimport {\n useCart,\n useFavorites,\n formatPrice,\n type Product,\n} from \"@/modules/ecommerce-core\";\nimport type { Category } from \"@/modules/ecommerce-core/types\";\nimport { useDbList } from \"@/db\";\nimport constants from \"@/constants/constants.json\";\n\ninterface ProductQuickViewProps {\n product: Product;\n open: boolean;\n onOpenChange: (open: boolean) => void;\n className?: string;\n}\n\nexport function ProductQuickView({\n product,\n open,\n onOpenChange,\n className,\n}: ProductQuickViewProps) {\n const { t } = useTranslation(\"product-quick-view\");\n const { addItem } = useCart();\n const { isFavorite, addToFavorites, removeFromFavorites } = useFavorites();\n const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\n const currency = (constants.site as any).currency || \"USD\";\n\n const [selectedImage, setSelectedImage] = useState(0);\n const [quantity, setQuantity] = useState(1);\n\n if (!product) {\n return null;\n }\n\n const price = product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n\n const discount = product.on_sale && product.sale_price\n ? Math.round(((product.price - product.sale_price) / product.price) * 100)\n : 0;\n\n const handleAddToCart = () => {\n for (let i = 0; i < quantity; i++) {\n addItem(product);\n }\n toast.success(t(\"addedToCart\", \"Added to cart!\"));\n onOpenChange(false);\n };\n\n const handleToggleFavorite = () => {\n if (isFavorite(product.id)) {\n removeFromFavorites(product.id);\n toast.success(t(\"removedFromFavorites\", \"Removed from favorites\"));\n } else {\n addToFavorites(product);\n toast.success(t(\"addedToFavorites\", \"Added to favorites!\"));\n }\n };\n\n const incrementQuantity = () => setQuantity((q) => q + 1);\n const decrementQuantity = () => setQuantity((q) => Math.max(1, q - 1));\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className={cn(\"sm:max-w-4xl p-0 gap-0 overflow-hidden\", className)}>\n <DialogTitle className=\"sr-only\">{product.name}</DialogTitle>\n\n <button\n onClick={() => onOpenChange(false)}\n className=\"absolute right-4 top-4 z-10 rounded-full bg-background/80 backdrop-blur-sm p-2 hover:bg-background transition-colors\"\n >\n <X className=\"h-4 w-4\" />\n <span className=\"sr-only\">Close</span>\n </button>\n\n <div className=\"grid md:grid-cols-2\">\n {/* Image Gallery */}\n <div className=\"relative bg-muted aspect-square md:aspect-auto md:h-full\">\n <img\n src={product.images[selectedImage] || \"/images/placeholder.png\"}\n alt={product.name}\n className=\"w-full h-full object-cover\"\n />\n\n {discount > 0 && (\n <span className=\"absolute top-4 left-4 bg-destructive text-destructive-foreground text-sm font-semibold px-3 py-1 rounded-full\">\n -{discount}%\n </span>\n )}\n\n {product?.images?.length > 1 && (\n <div className=\"absolute bottom-4 left-4 flex flex-col gap-2\">\n {product.images.map((image, index) => (\n <button\n key={index}\n onClick={() => setSelectedImage(index)}\n className={cn(\n \"w-14 h-14 rounded-lg overflow-hidden border-2 transition-all bg-background/80 backdrop-blur-sm\",\n selectedImage === index\n ? \"border-primary ring-2 ring-primary/20\"\n : \"border-transparent opacity-70 hover:opacity-100\"\n )}\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=\"p-6 md:p-8 flex flex-col\">\n <div className=\"flex-1\">\n {product.brand && (\n <p className=\"text-sm text-muted-foreground uppercase tracking-wide mb-1\">\n {product.brand}\n </p>\n )}\n <h2 className=\"text-2xl md:text-3xl font-bold mb-2\">\n {product.name}\n </h2>\n\n {/* Price */}\n <div className=\"flex items-center gap-3 mb-4\">\n <span className=\"text-2xl font-bold text-primary\">\n {formatPrice(price, currency)}\n </span>\n {product.on_sale && product.sale_price && (\n <span className=\"text-lg text-muted-foreground line-through\">\n {formatPrice(product.price, currency)}\n </span>\n )}\n </div>\n\n {/* Description */}\n <p className=\"text-muted-foreground mb-6 line-clamp-4\">\n {product.description}\n </p>\n\n {/* Category */}\n {categoryMap.get(product.categories?.[0] as number)?.name && (\n <p className=\"text-sm text-muted-foreground mb-4\">\n {t(\"category\", \"Category\")}: {categoryMap.get(product.categories?.[0] as number)?.name}\n </p>\n )}\n\n {/* Quantity */}\n <div className=\"mb-6\">\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 border-border rounded-lg\">\n <button\n onClick={decrementQuantity}\n className=\"p-3 hover:bg-muted transition-colors\"\n disabled={quantity <= 1}\n >\n <Minus className=\"h-4 w-4\" />\n </button>\n <span className=\"w-12 text-center font-medium\">\n {quantity}\n </span>\n <button\n onClick={incrementQuantity}\n className=\"p-3 hover:bg-muted transition-colors\"\n >\n <Plus className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"space-y-3 pt-4 border-t border-border\">\n <div className=\"flex gap-2\">\n <Button\n onClick={handleAddToCart}\n className=\"flex-1 gap-2\"\n size=\"lg\"\n >\n <ShoppingCart className=\"h-5 w-5\" />\n {t(\"addToCart\", \"Add to Cart\")}\n </Button>\n <Button\n variant=\"outline\"\n size=\"lg\"\n onClick={handleToggleFavorite}\n className=\"px-4\"\n >\n <Heart\n className={cn(\n \"h-5 w-5\",\n isFavorite(product.id) && \"fill-current text-red-500\"\n )}\n />\n </Button>\n </div>\n\n <Link to={`/products/${product.slug}`} className=\"block\">\n <Button\n variant=\"outline\"\n className=\"w-full\"\n size=\"lg\"\n onClick={() => onOpenChange(false)}\n >\n {t(\"viewDetails\", \"View Full Details\")}\n </Button>\n </Link>\n </div>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n"
20
+ "content": "\"use client\";\r\n\r\nimport { useState, useMemo } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Link } from \"react-router\";\r\nimport { toast } from \"sonner\";\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, Heart } from \"lucide-react\";\r\nimport {\r\n useCart,\r\n useFavorites,\r\n formatPrice,\r\n type Product,\r\n} from \"@/modules/ecommerce-core\";\r\nimport type { Category } from \"@/modules/ecommerce-core/types\";\r\nimport { useDbList } from \"@/db\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\ninterface ProductQuickViewProps {\r\n product: Product;\r\n open: boolean;\r\n onOpenChange: (open: boolean) => void;\r\n className?: string;\r\n}\r\n\r\nexport function ProductQuickView({\r\n product,\r\n open,\r\n onOpenChange,\r\n className,\r\n}: ProductQuickViewProps) {\r\n const { t } = useTranslation(\"product-quick-view\");\r\n const { addItem } = useCart();\r\n const { isFavorite, addToFavorites, removeFromFavorites } = useFavorites();\r\n const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\r\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\r\n const currency = (constants.site as any).currency || \"USD\";\r\n\r\n const [selectedImage, setSelectedImage] = useState(0);\r\n const [quantity, setQuantity] = useState(1);\r\n\r\n if (!product) {\r\n return null;\r\n }\r\n\r\n const price = product.on_sale && product.sale_price\r\n ? product.sale_price\r\n : product.price;\r\n\r\n const discount = product.on_sale && product.sale_price\r\n ? Math.round(((product.price - product.sale_price) / product.price) * 100)\r\n : 0;\r\n\r\n const handleAddToCart = () => {\r\n for (let i = 0; i < quantity; i++) {\r\n addItem(product);\r\n }\r\n toast.success(t(\"addedToCart\", \"Added to cart!\"));\r\n onOpenChange(false);\r\n };\r\n\r\n const handleToggleFavorite = () => {\r\n if (isFavorite(product.id)) {\r\n removeFromFavorites(product.id);\r\n toast.success(t(\"removedFromFavorites\", \"Removed from favorites\"));\r\n } else {\r\n addToFavorites(product);\r\n toast.success(t(\"addedToFavorites\", \"Added to favorites!\"));\r\n }\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 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.name}</DialogTitle>\r\n\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 <img\r\n src={product.images[selectedImage] || \"/images/placeholder.png\"}\r\n alt={product.name}\r\n className=\"w-full h-full object-cover\"\r\n />\r\n\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 {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.name} ${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 {product.brand && (\r\n <p className=\"text-sm text-muted-foreground uppercase tracking-wide mb-1\">\r\n {product.brand}\r\n </p>\r\n )}\r\n <h2 className=\"text-2xl md:text-3xl font-bold mb-2\">\r\n {product.name}\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(price, currency)}\r\n </span>\r\n {product.on_sale && product.sale_price && (\r\n <span className=\"text-lg text-muted-foreground line-through\">\r\n {formatPrice(product.price, currency)}\r\n </span>\r\n )}\r\n </div>\r\n\r\n {/* Description */}\r\n <p className=\"text-muted-foreground mb-6 line-clamp-4\">\r\n {product.description}\r\n </p>\r\n\r\n {/* Category */}\r\n {categoryMap.get(product.categories?.[0] as number)?.name && (\r\n <p className=\"text-sm text-muted-foreground mb-4\">\r\n {t(\"category\", \"Category\")}: {categoryMap.get(product.categories?.[0] as number)?.name}\r\n </p>\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\", \"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 <div className=\"flex gap-2\">\r\n <Button\r\n onClick={handleAddToCart}\r\n className=\"flex-1 gap-2\"\r\n size=\"lg\"\r\n >\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {t(\"addToCart\", \"Add to Cart\")}\r\n </Button>\r\n <Button\r\n variant=\"outline\"\r\n size=\"lg\"\r\n onClick={handleToggleFavorite}\r\n className=\"px-4\"\r\n >\r\n <Heart\r\n className={cn(\r\n \"h-5 w-5\",\r\n isFavorite(product.id) && \"fill-current text-red-500\"\r\n )}\r\n />\r\n </Button>\r\n </div>\r\n\r\n <Link to={`/products/${product.slug}`} className=\"block\">\r\n <Button\r\n variant=\"outline\"\r\n className=\"w-full\"\r\n size=\"lg\"\r\n onClick={() => onOpenChange(false)}\r\n >\r\n {t(\"viewDetails\", \"View Full Details\")}\r\n </Button>\r\n </Link>\r\n </div>\r\n </div>\r\n </div>\r\n </DialogContent>\r\n </Dialog>\r\n );\r\n}\r\n"
21
21
  },
22
22
  {
23
23
  "path": "product-quick-view/index.ts",
24
24
  "type": "registry:index",
25
25
  "target": "$modules$/product-quick-view/index.ts",
26
- "content": "export * from \"./product-quick-view\";\n"
26
+ "content": "export * from \"./product-quick-view\";\r\n"
27
27
  },
28
28
  {
29
29
  "path": "product-quick-view/lang/en.json",
30
30
  "type": "registry:lang",
31
31
  "target": "$modules$/product-quick-view/lang/en.json",
32
- "content": "{\n \"size\": \"Size\",\n \"color\": \"Color\",\n \"quantity\": \"Quantity\",\n \"addToCart\": \"Add to Cart\",\n \"viewDetails\": \"View Full Details\"\n}\n"
32
+ "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"
33
33
  },
34
34
  {
35
35
  "path": "product-quick-view/lang/tr.json",
36
36
  "type": "registry:lang",
37
37
  "target": "$modules$/product-quick-view/lang/tr.json",
38
- "content": "{\n \"size\": \"Beden\",\n \"color\": \"Renk\",\n \"quantity\": \"Adet\",\n \"addToCart\": \"Sepete Ekle\",\n \"viewDetails\": \"Tüm Detayları Gör\"\n}\n"
38
+ "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"
39
39
  }
40
40
  ],
41
41
  "exports": {
@@ -18,25 +18,25 @@
18
18
  "path": "products-page/index.ts",
19
19
  "type": "registry:index",
20
20
  "target": "$modules$/products-page/index.ts",
21
- "content": "export * from './products-page';\nexport { ProductsPage as default } from './products-page';\n"
21
+ "content": "export * from './products-page';\r\nexport { ProductsPage as default } from './products-page';\r\n"
22
22
  },
23
23
  {
24
24
  "path": "products-page/products-page.tsx",
25
25
  "type": "registry:page",
26
26
  "target": "$modules$/products-page/products-page.tsx",
27
- "content": "import { useState, useRef, useCallback, useMemo } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Filter, Grid, List } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { FadeIn } from \"@/modules/animations\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/components/ui/sheet\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useDbList } from \"@/db\";\nimport type { Product, Category } from \"@/modules/ecommerce-core/types\";\n\ninterface FilterSidebarProps {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n t: any;\n categories: Category[];\n selectedCategories: string[];\n handleCategoryChange: (category: string, checked: boolean) => void;\n selectedFeatures: string[];\n handleFeatureChange: (feature: string, checked: boolean) => void;\n minPriceRef: React.RefObject<HTMLInputElement | null>;\n maxPriceRef: React.RefObject<HTMLInputElement | null>;\n searchParams: URLSearchParams;\n handlePriceFilter: () => void;\n}\n\nfunction FilterSidebar({\n t,\n categories,\n selectedCategories,\n handleCategoryChange,\n selectedFeatures,\n handleFeatureChange,\n minPriceRef,\n maxPriceRef,\n searchParams,\n handlePriceFilter,\n}: FilterSidebarProps) {\n return (\n <div className=\"space-y-6\">\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"categories\", \"Categories\")}\n </h3>\n <div className=\"space-y-3\">\n {categories.map((category) => (\n <div\n key={category.id}\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\n data-db-table=\"product_categories\"\n data-db-id={category.id}\n >\n <Checkbox\n id={`category-${category.id}`}\n checked={selectedCategories.includes(category.slug)}\n onCheckedChange={(checked) =>\n handleCategoryChange(category.slug, checked as boolean)\n }\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\n />\n <label\n htmlFor={`category-${category.id}`}\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\n >\n {category.name}\n </label>\n </div>\n ))}\n </div>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"priceRange\", \"Price Range\")}\n </h3>\n <div className=\"space-y-3 p-3 bg-muted/30 rounded-lg\">\n <div className=\"grid grid-cols-2 gap-3\">\n <input\n ref={minPriceRef}\n type=\"number\"\n placeholder={t(\"minPrice\", \"Min\")}\n defaultValue={searchParams.get(\"minPrice\") || \"\"}\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\n />\n <input\n ref={maxPriceRef}\n type=\"number\"\n placeholder={t(\"maxPrice\", \"Max\")}\n defaultValue={searchParams.get(\"maxPrice\") || \"\"}\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\n />\n </div>\n </div>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"features\", \"Features\")}\n </h3>\n <div className=\"space-y-3\">\n {[\n { key: \"on_sale\", label: t(\"onSale\", \"On Sale\") },\n { key: \"is_new\", label: t(\"newArrivals\", \"New Arrivals\") },\n { key: \"featured\", label: t(\"featuredLabel\", \"Featured\") },\n { key: \"in_stock\", label: t(\"inStock\", \"In Stock\") },\n ].map((feature) => (\n <div\n key={feature.key}\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\n >\n <Checkbox\n id={feature.key}\n checked={selectedFeatures.includes(feature.key)}\n onCheckedChange={(checked) =>\n handleFeatureChange(feature.key, checked as boolean)\n }\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\n />\n <label\n htmlFor={feature.key}\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\n >\n {feature.label}\n </label>\n </div>\n ))}\n </div>\n </div>\n </div>\n );\n}\n\nexport function ProductsPage() {\n const { t } = useTranslation(\"products-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Products\") });\n const { data: products = [], isLoading: productsLoading } = useDbList<Product>(\"products\", {\n where: { published: 1 },\n });\n const { data: categories = [], isLoading: categoriesLoading } = useDbList<Category>(\"product_categories\");\n const loading = productsLoading || categoriesLoading;\n\n const [searchParams, setSearchParams] = useSearchParams();\n const [viewMode, setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\n const [sortBy, setSortBy] = useState(\"featured\");\n const [selectedCategories, setSelectedCategories] = useState<string[]>(() => {\n const categorySlug = searchParams.get(\"category\");\n return categorySlug ? [categorySlug] : [];\n });\n const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);\n const searchQuery = searchParams.get(\"search\") || \"\";\n const minPriceRef = useRef<HTMLInputElement>(null);\n const maxPriceRef = useRef<HTMLInputElement>(null);\n\n const selectedCategoryIds = useMemo(() => {\n if (selectedCategories.length === 0) return new Set<number>();\n return new Set(\n categories.filter(c => selectedCategories.includes(c.slug)).map(c => c.id)\n );\n }, [selectedCategories, categories]);\n\n const filteredProducts = useMemo(() => {\n const minPrice = parseFloat(searchParams.get(\"minPrice\") || \"0\") || 0;\n const maxPrice =\n parseFloat(searchParams.get(\"maxPrice\") || \"999999\") || 999999;\n\n let filtered = products.filter((product) => {\n const currentPrice =\n product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n return currentPrice >= minPrice && currentPrice <= maxPrice;\n });\n\n if (selectedCategories.length > 0) {\n filtered = filtered.filter((product) =>\n product.categories?.some((id) => selectedCategoryIds.has(id))\n );\n }\n\n if (selectedFeatures.length > 0) {\n filtered = filtered.filter((product) => {\n return selectedFeatures.every((feature) => {\n switch (feature) {\n case \"on_sale\":\n return product.on_sale;\n case \"is_new\":\n return product.is_new;\n case \"featured\":\n return product.featured;\n case \"in_stock\":\n return product.stock > 0;\n default:\n return true;\n }\n });\n });\n }\n\n // Apply sorting\n return [...filtered].sort((a, b) => {\n switch (sortBy) {\n case \"price-low\":\n return (\n (a.on_sale ? a.sale_price || a.price : a.price) -\n (b.on_sale ? b.sale_price || b.price : b.price)\n );\n case \"price-high\":\n return (\n (b.on_sale ? b.sale_price || b.price : b.price) -\n (a.on_sale ? a.sale_price || a.price : a.price)\n );\n case \"newest\":\n return (\n new Date(b.created_at || 0).getTime() -\n new Date(a.created_at || 0).getTime()\n );\n case \"featured\":\n default:\n return (b.featured ? 1 : 0) - (a.featured ? 1 : 0);\n }\n });\n }, [products, searchParams, selectedFeatures, selectedCategories, selectedCategoryIds, sortBy]);\n\n const handlePriceFilter = useCallback(() => {\n const minPrice = minPriceRef.current?.value || \"\";\n const maxPrice = maxPriceRef.current?.value || \"\";\n const params = new URLSearchParams(searchParams);\n if (minPrice) params.set(\"minPrice\", minPrice);\n else params.delete(\"minPrice\");\n if (maxPrice) params.set(\"maxPrice\", maxPrice);\n else params.delete(\"maxPrice\");\n setSearchParams(params);\n }, [searchParams, setSearchParams]);\n\n const handleCategoryChange = useCallback(\n (category: string, checked: boolean) => {\n if (checked) {\n setSelectedCategories((prev) => [...prev, category]);\n } else {\n setSelectedCategories((prev) => prev.filter((c) => c !== category));\n }\n },\n []\n );\n\n const handleFeatureChange = useCallback(\n (feature: string, checked: boolean) => {\n if (checked) {\n setSelectedFeatures((prev) => [...prev, feature]);\n } else {\n setSelectedFeatures((prev) => prev.filter((f) => f !== feature));\n }\n },\n []\n );\n\n const sortOptions = [\n { value: \"featured\", label: t(\"featured\", \"Featured\") },\n { value: \"price-low\", label: t(\"sortPriceLow\", \"Price: Low to High\") },\n { value: \"price-high\", label: t(\"sortPriceHigh\", \"Price: High to Low\") },\n { value: \"newest\", label: t(\"sortNewest\", \"Newest\") },\n ];\n\n const filterSidebarProps: FilterSidebarProps = {\n t,\n categories,\n selectedCategories,\n handleCategoryChange,\n selectedFeatures,\n handleFeatureChange,\n minPriceRef,\n maxPriceRef,\n searchParams,\n handlePriceFilter,\n };\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"mb-8\">\n <div className=\"flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6\">\n <div className=\"space-y-1\">\n <h1 className=\"text-2xl lg:text-3xl font-bold\">\n {searchQuery\n ? t(\"searchResultsFor\", `Search Results for \"${searchQuery}\"`)\n : t(\"allProducts\", \"All Products\")}\n </h1>\n <p className=\"text-sm lg:text-base text-muted-foreground\">\n {t(\"showing\", \"Showing\")} {filteredProducts.length}{\" \"}\n {t(\"of\", \"of\")} {products.length} {t(\"products\", \"products\")}\n </p>\n </div>\n {searchQuery && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setSearchParams({})}\n className=\"w-fit\"\n >\n {t(\"clearSearch\", \"Clear Search\")}\n </Button>\n )}\n </div>\n\n <div className=\"flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between\">\n <Sheet>\n <SheetTrigger asChild>\n <Button\n variant=\"outline\"\n className=\"lg:hidden w-full sm:w-auto\"\n >\n <Filter className=\"h-4 w-4 mr-2\" />\n {t(\"filters\", \"Filters\")}\n </Button>\n </SheetTrigger>\n <SheetContent side=\"left\" className=\"w-[300px]\">\n <SheetHeader>\n <SheetTitle>{t(\"filters\", \"Filters\")}</SheetTitle>\n <SheetDescription>\n {t(\"refineSearch\", \"Refine your product search\")}\n </SheetDescription>\n </SheetHeader>\n <div className=\"mt-6\">\n <FilterSidebar {...filterSidebarProps} />\n </div>\n </SheetContent>\n </Sheet>\n\n <div className=\"flex flex-col sm:flex-row items-stretch sm:items-center gap-3\">\n <Select value={sortBy} onValueChange={setSortBy}>\n <SelectTrigger className=\"w-full sm:w-[160px]\">\n <SelectValue placeholder={t(\"sortBy\", \"Sort by\")} />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n\n <div className=\"flex border rounded-lg p-1 w-full sm:w-auto\">\n <Button\n variant={viewMode === \"grid\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => setViewMode(\"grid\")}\n className=\"flex-1 sm:flex-none\"\n >\n <Grid className=\"h-4 w-4\" />\n </Button>\n <Button\n variant={viewMode === \"list\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => setViewMode(\"list\")}\n className=\"flex-1 sm:flex-none\"\n >\n <List className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n </div>\n </FadeIn>\n\n <div className=\"flex gap-8\">\n <aside className=\"hidden lg:block w-64 flex-shrink-0\">\n <div className=\"sticky top-24\">\n <FilterSidebar {...filterSidebarProps} />\n </div>\n </aside>\n\n <div className=\"flex-1\">\n {loading ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\n {[...Array(6)].map((_, i) => (\n <div\n key={i}\n className=\"animate-pulse bg-card rounded-lg shadow-md overflow-hidden\"\n >\n <div className=\"aspect-square bg-muted mb-4\"></div>\n <div className=\"p-4\">\n <div className=\"h-4 bg-muted rounded w-3/4 mb-2\"></div>\n <div className=\"h-3 bg-muted rounded w-1/2 mb-3\"></div>\n <div className=\"h-4 bg-muted rounded w-1/3\"></div>\n </div>\n </div>\n ))}\n </div>\n ) : viewMode === \"grid\" ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\n {filteredProducts.map((product) => (\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\n <ProductCard\n product={product}\n variant=\"grid\"\n />\n </div>\n ))}\n </div>\n ) : (\n <div className=\"space-y-6\">\n {filteredProducts.map((product) => (\n <div\n key={product.id}\n className=\"w-full\"\n data-db-table=\"products\"\n data-db-id={product.id}\n >\n <ProductCard\n product={product}\n variant=\"list\"\n />\n </div>\n ))}\n </div>\n )}\n\n {!loading && filteredProducts.length === 0 && (\n <div className=\"text-center py-12\">\n <p className=\"text-muted-foreground\">\n {t(\n \"noProductsFound\",\n \"No products found matching your criteria.\"\n )}\n </p>\n </div>\n )}\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ProductsPage;\n"
27
+ "content": "import { useState, useRef, useCallback, useMemo } from \"react\";\r\nimport { useSearchParams } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Filter, Grid, List } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport {\r\n Sheet,\r\n SheetContent,\r\n SheetDescription,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport { ProductCard } from \"@/modules/product-card/product-card\";\r\nimport { useDbList } from \"@/db\";\r\nimport type { Product, Category } from \"@/modules/ecommerce-core/types\";\r\n\r\ninterface FilterSidebarProps {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n t: any;\r\n categories: Category[];\r\n selectedCategories: string[];\r\n handleCategoryChange: (category: string, checked: boolean) => void;\r\n selectedFeatures: string[];\r\n handleFeatureChange: (feature: string, checked: boolean) => void;\r\n minPriceRef: React.RefObject<HTMLInputElement | null>;\r\n maxPriceRef: React.RefObject<HTMLInputElement | null>;\r\n searchParams: URLSearchParams;\r\n handlePriceFilter: () => void;\r\n}\r\n\r\nfunction FilterSidebar({\r\n t,\r\n categories,\r\n selectedCategories,\r\n handleCategoryChange,\r\n selectedFeatures,\r\n handleFeatureChange,\r\n minPriceRef,\r\n maxPriceRef,\r\n searchParams,\r\n handlePriceFilter,\r\n}: FilterSidebarProps) {\r\n return (\r\n <div className=\"space-y-6\">\r\n <div>\r\n <h3 className=\"font-semibold mb-4 text-base\">\r\n {t(\"categories\", \"Categories\")}\r\n </h3>\r\n <div className=\"space-y-3\">\r\n {categories.map((category) => (\r\n <div\r\n key={category.id}\r\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\r\n data-db-table=\"product_categories\"\r\n data-db-id={category.id}\r\n >\r\n <Checkbox\r\n id={`category-${category.id}`}\r\n checked={selectedCategories.includes(category.slug)}\r\n onCheckedChange={(checked) =>\r\n handleCategoryChange(category.slug, checked as boolean)\r\n }\r\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\r\n />\r\n <label\r\n htmlFor={`category-${category.id}`}\r\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\r\n >\r\n {category.name}\r\n </label>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h3 className=\"font-semibold mb-4 text-base\">\r\n {t(\"priceRange\", \"Price Range\")}\r\n </h3>\r\n <div className=\"space-y-3 p-3 bg-muted/30 rounded-lg\">\r\n <div className=\"grid grid-cols-2 gap-3\">\r\n <input\r\n ref={minPriceRef}\r\n type=\"number\"\r\n placeholder={t(\"minPrice\", \"Min\")}\r\n defaultValue={searchParams.get(\"minPrice\") || \"\"}\r\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\r\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\r\n />\r\n <input\r\n ref={maxPriceRef}\r\n type=\"number\"\r\n placeholder={t(\"maxPrice\", \"Max\")}\r\n defaultValue={searchParams.get(\"maxPrice\") || \"\"}\r\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\r\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\r\n />\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h3 className=\"font-semibold mb-4 text-base\">\r\n {t(\"features\", \"Features\")}\r\n </h3>\r\n <div className=\"space-y-3\">\r\n {[\r\n { key: \"on_sale\", label: t(\"onSale\", \"On Sale\") },\r\n { key: \"is_new\", label: t(\"newArrivals\", \"New Arrivals\") },\r\n { key: \"featured\", label: t(\"featuredLabel\", \"Featured\") },\r\n { key: \"in_stock\", label: t(\"inStock\", \"In Stock\") },\r\n ].map((feature) => (\r\n <div\r\n key={feature.key}\r\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\r\n >\r\n <Checkbox\r\n id={feature.key}\r\n checked={selectedFeatures.includes(feature.key)}\r\n onCheckedChange={(checked) =>\r\n handleFeatureChange(feature.key, checked as boolean)\r\n }\r\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\r\n />\r\n <label\r\n htmlFor={feature.key}\r\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\r\n >\r\n {feature.label}\r\n </label>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\nexport function ProductsPage() {\r\n const { t } = useTranslation(\"products-page\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Products\") });\r\n const { data: products = [], isLoading: productsLoading } = useDbList<Product>(\"products\", {\r\n where: { published: 1 },\r\n });\r\n const { data: categories = [], isLoading: categoriesLoading } = useDbList<Category>(\"product_categories\");\r\n const loading = productsLoading || categoriesLoading;\r\n\r\n const [searchParams, setSearchParams] = useSearchParams();\r\n const [viewMode, setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\r\n const [sortBy, setSortBy] = useState(\"featured\");\r\n const [selectedCategories, setSelectedCategories] = useState<string[]>(() => {\r\n const categorySlug = searchParams.get(\"category\");\r\n return categorySlug ? [categorySlug] : [];\r\n });\r\n const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);\r\n const searchQuery = searchParams.get(\"search\") || \"\";\r\n const minPriceRef = useRef<HTMLInputElement>(null);\r\n const maxPriceRef = useRef<HTMLInputElement>(null);\r\n\r\n const selectedCategoryIds = useMemo(() => {\r\n if (selectedCategories.length === 0) return new Set<number>();\r\n return new Set(\r\n categories.filter(c => selectedCategories.includes(c.slug)).map(c => c.id)\r\n );\r\n }, [selectedCategories, categories]);\r\n\r\n const filteredProducts = useMemo(() => {\r\n const minPrice = parseFloat(searchParams.get(\"minPrice\") || \"0\") || 0;\r\n const maxPrice =\r\n parseFloat(searchParams.get(\"maxPrice\") || \"999999\") || 999999;\r\n\r\n let filtered = products.filter((product) => {\r\n const currentPrice =\r\n product.on_sale && product.sale_price\r\n ? product.sale_price\r\n : product.price;\r\n return currentPrice >= minPrice && currentPrice <= maxPrice;\r\n });\r\n\r\n if (selectedCategories.length > 0) {\r\n filtered = filtered.filter((product) =>\r\n product.categories?.some((id) => selectedCategoryIds.has(id))\r\n );\r\n }\r\n\r\n if (selectedFeatures.length > 0) {\r\n filtered = filtered.filter((product) => {\r\n return selectedFeatures.every((feature) => {\r\n switch (feature) {\r\n case \"on_sale\":\r\n return product.on_sale;\r\n case \"is_new\":\r\n return product.is_new;\r\n case \"featured\":\r\n return product.featured;\r\n case \"in_stock\":\r\n return product.stock > 0;\r\n default:\r\n return true;\r\n }\r\n });\r\n });\r\n }\r\n\r\n // Apply sorting\r\n return [...filtered].sort((a, b) => {\r\n switch (sortBy) {\r\n case \"price-low\":\r\n return (\r\n (a.on_sale ? a.sale_price || a.price : a.price) -\r\n (b.on_sale ? b.sale_price || b.price : b.price)\r\n );\r\n case \"price-high\":\r\n return (\r\n (b.on_sale ? b.sale_price || b.price : b.price) -\r\n (a.on_sale ? a.sale_price || a.price : a.price)\r\n );\r\n case \"newest\":\r\n return (\r\n new Date(b.created_at || 0).getTime() -\r\n new Date(a.created_at || 0).getTime()\r\n );\r\n case \"featured\":\r\n default:\r\n return (b.featured ? 1 : 0) - (a.featured ? 1 : 0);\r\n }\r\n });\r\n }, [products, searchParams, selectedFeatures, selectedCategories, selectedCategoryIds, sortBy]);\r\n\r\n const handlePriceFilter = useCallback(() => {\r\n const minPrice = minPriceRef.current?.value || \"\";\r\n const maxPrice = maxPriceRef.current?.value || \"\";\r\n const params = new URLSearchParams(searchParams);\r\n if (minPrice) params.set(\"minPrice\", minPrice);\r\n else params.delete(\"minPrice\");\r\n if (maxPrice) params.set(\"maxPrice\", maxPrice);\r\n else params.delete(\"maxPrice\");\r\n setSearchParams(params);\r\n }, [searchParams, setSearchParams]);\r\n\r\n const handleCategoryChange = useCallback(\r\n (category: string, checked: boolean) => {\r\n if (checked) {\r\n setSelectedCategories((prev) => [...prev, category]);\r\n } else {\r\n setSelectedCategories((prev) => prev.filter((c) => c !== category));\r\n }\r\n },\r\n []\r\n );\r\n\r\n const handleFeatureChange = useCallback(\r\n (feature: string, checked: boolean) => {\r\n if (checked) {\r\n setSelectedFeatures((prev) => [...prev, feature]);\r\n } else {\r\n setSelectedFeatures((prev) => prev.filter((f) => f !== feature));\r\n }\r\n },\r\n []\r\n );\r\n\r\n const sortOptions = [\r\n { value: \"featured\", label: t(\"featured\", \"Featured\") },\r\n { value: \"price-low\", label: t(\"sortPriceLow\", \"Price: Low to High\") },\r\n { value: \"price-high\", label: t(\"sortPriceHigh\", \"Price: High to Low\") },\r\n { value: \"newest\", label: t(\"sortNewest\", \"Newest\") },\r\n ];\r\n\r\n const filterSidebarProps: FilterSidebarProps = {\r\n t,\r\n categories,\r\n selectedCategories,\r\n handleCategoryChange,\r\n selectedFeatures,\r\n handleFeatureChange,\r\n minPriceRef,\r\n maxPriceRef,\r\n searchParams,\r\n handlePriceFilter,\r\n };\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <FadeIn className=\"mb-8\">\r\n <div className=\"flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6\">\r\n <div className=\"space-y-1\">\r\n <h1 className=\"text-2xl lg:text-3xl font-bold\">\r\n {searchQuery\r\n ? t(\"searchResultsFor\", `Search Results for \"${searchQuery}\"`)\r\n : t(\"allProducts\", \"All Products\")}\r\n </h1>\r\n <p className=\"text-sm lg:text-base text-muted-foreground\">\r\n {t(\"showing\", \"Showing\")} {filteredProducts.length}{\" \"}\r\n {t(\"of\", \"of\")} {products.length} {t(\"products\", \"products\")}\r\n </p>\r\n </div>\r\n {searchQuery && (\r\n <Button\r\n variant=\"outline\"\r\n size=\"sm\"\r\n onClick={() => setSearchParams({})}\r\n className=\"w-fit\"\r\n >\r\n {t(\"clearSearch\", \"Clear Search\")}\r\n </Button>\r\n )}\r\n </div>\r\n\r\n <div className=\"flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between\">\r\n <Sheet>\r\n <SheetTrigger asChild>\r\n <Button\r\n variant=\"outline\"\r\n className=\"lg:hidden w-full sm:w-auto\"\r\n >\r\n <Filter className=\"h-4 w-4 mr-2\" />\r\n {t(\"filters\", \"Filters\")}\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent side=\"left\" className=\"w-[300px]\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"filters\", \"Filters\")}</SheetTitle>\r\n <SheetDescription>\r\n {t(\"refineSearch\", \"Refine your product search\")}\r\n </SheetDescription>\r\n </SheetHeader>\r\n <div className=\"mt-6\">\r\n <FilterSidebar {...filterSidebarProps} />\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n\r\n <div className=\"flex flex-col sm:flex-row items-stretch sm:items-center gap-3\">\r\n <Select value={sortBy} onValueChange={setSortBy}>\r\n <SelectTrigger className=\"w-full sm:w-[160px]\">\r\n <SelectValue placeholder={t(\"sortBy\", \"Sort by\")} />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {sortOptions.map((option) => (\r\n <SelectItem key={option.value} value={option.value}>\r\n {option.label}\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n\r\n <div className=\"flex border rounded-lg p-1 w-full sm:w-auto\">\r\n <Button\r\n variant={viewMode === \"grid\" ? \"default\" : \"ghost\"}\r\n size=\"sm\"\r\n onClick={() => setViewMode(\"grid\")}\r\n className=\"flex-1 sm:flex-none\"\r\n >\r\n <Grid className=\"h-4 w-4\" />\r\n </Button>\r\n <Button\r\n variant={viewMode === \"list\" ? \"default\" : \"ghost\"}\r\n size=\"sm\"\r\n onClick={() => setViewMode(\"list\")}\r\n className=\"flex-1 sm:flex-none\"\r\n >\r\n <List className=\"h-4 w-4\" />\r\n </Button>\r\n </div>\r\n </div>\r\n </div>\r\n </FadeIn>\r\n\r\n <div className=\"flex gap-8\">\r\n <aside className=\"hidden lg:block w-64 flex-shrink-0\">\r\n <div className=\"sticky top-24\">\r\n <FilterSidebar {...filterSidebarProps} />\r\n </div>\r\n </aside>\r\n\r\n <div className=\"flex-1\">\r\n {loading ? (\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\r\n {[...Array(6)].map((_, i) => (\r\n <div\r\n key={i}\r\n className=\"animate-pulse bg-card rounded-lg shadow-md overflow-hidden\"\r\n >\r\n <div className=\"aspect-square bg-muted mb-4\"></div>\r\n <div className=\"p-4\">\r\n <div className=\"h-4 bg-muted rounded w-3/4 mb-2\"></div>\r\n <div className=\"h-3 bg-muted rounded w-1/2 mb-3\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/3\"></div>\r\n </div>\r\n </div>\r\n ))}\r\n </div>\r\n ) : viewMode === \"grid\" ? (\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\r\n {filteredProducts.map((product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <ProductCard\r\n product={product}\r\n variant=\"grid\"\r\n />\r\n </div>\r\n ))}\r\n </div>\r\n ) : (\r\n <div className=\"space-y-6\">\r\n {filteredProducts.map((product) => (\r\n <div\r\n key={product.id}\r\n className=\"w-full\"\r\n data-db-table=\"products\"\r\n data-db-id={product.id}\r\n >\r\n <ProductCard\r\n product={product}\r\n variant=\"list\"\r\n />\r\n </div>\r\n ))}\r\n </div>\r\n )}\r\n\r\n {!loading && filteredProducts.length === 0 && (\r\n <div className=\"text-center py-12\">\r\n <p className=\"text-muted-foreground\">\r\n {t(\r\n \"noProductsFound\",\r\n \"No products found matching your criteria.\"\r\n )}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ProductsPage;\r\n"
28
28
  },
29
29
  {
30
30
  "path": "products-page/lang/en.json",
31
31
  "type": "registry:lang",
32
32
  "target": "$modules$/products-page/lang/en.json",
33
- "content": "{\n \"pageTitle\": \"Products\",\n \"title\": \"Products\",\n \"allProducts\": \"All Products\",\n \"searchResultsFor\": \"Search Results\",\n \"showing\": \"Showing\",\n \"of\": \"of\",\n \"products\": \"products\",\n \"clearSearch\": \"Clear Search\",\n \"filters\": \"Filters\",\n \"refineSearch\": \"Refine your product search\",\n \"categories\": \"Categories\",\n \"priceRange\": \"Price Range\",\n \"minPrice\": \"Min\",\n \"maxPrice\": \"Max\",\n \"features\": \"Features\",\n \"onSale\": \"On Sale\",\n \"newArrivals\": \"New Arrivals\",\n \"featuredLabel\": \"Featured\",\n \"inStock\": \"In Stock\",\n \"sortBy\": \"Sort by\",\n \"featured\": \"Featured\",\n \"sortPriceLow\": \"Price: Low to High\",\n \"sortPriceHigh\": \"Price: High to Low\",\n \"sortNewest\": \"Newest\",\n \"noProductsFound\": \"No products found matching your criteria.\"\n}\n"
33
+ "content": "{\r\n \"pageTitle\": \"Products\",\r\n \"title\": \"Products\",\r\n \"allProducts\": \"All Products\",\r\n \"searchResultsFor\": \"Search Results\",\r\n \"showing\": \"Showing\",\r\n \"of\": \"of\",\r\n \"products\": \"products\",\r\n \"clearSearch\": \"Clear Search\",\r\n \"filters\": \"Filters\",\r\n \"refineSearch\": \"Refine your product search\",\r\n \"categories\": \"Categories\",\r\n \"priceRange\": \"Price Range\",\r\n \"minPrice\": \"Min\",\r\n \"maxPrice\": \"Max\",\r\n \"features\": \"Features\",\r\n \"onSale\": \"On Sale\",\r\n \"newArrivals\": \"New Arrivals\",\r\n \"featuredLabel\": \"Featured\",\r\n \"inStock\": \"In Stock\",\r\n \"sortBy\": \"Sort by\",\r\n \"featured\": \"Featured\",\r\n \"sortPriceLow\": \"Price: Low to High\",\r\n \"sortPriceHigh\": \"Price: High to Low\",\r\n \"sortNewest\": \"Newest\",\r\n \"noProductsFound\": \"No products found matching your criteria.\"\r\n}\r\n"
34
34
  },
35
35
  {
36
36
  "path": "products-page/lang/tr.json",
37
37
  "type": "registry:lang",
38
38
  "target": "$modules$/products-page/lang/tr.json",
39
- "content": "{\n \"pageTitle\": \"Ürünler\",\n \"title\": \"Ürünler\",\n \"allProducts\": \"Tüm Ürünler\",\n \"searchResultsFor\": \"Arama Sonuçları\",\n \"showing\": \"Gösterilen\",\n \"of\": \"/\",\n \"products\": \"ürün\",\n \"clearSearch\": \"Aramayı Temizle\",\n \"filters\": \"Filtreler\",\n \"refineSearch\": \"Ürün aramanızı daraltın\",\n \"categories\": \"Kategoriler\",\n \"priceRange\": \"Fiyat Aralığı\",\n \"minPrice\": \"Min\",\n \"maxPrice\": \"Max\",\n \"features\": \"Özellikler\",\n \"onSale\": \"İndirimde\",\n \"newArrivals\": \"Yeni Gelenler\",\n \"featuredLabel\": \"Öne Çıkan\",\n \"inStock\": \"Stokta\",\n \"sortBy\": \"Sırala\",\n \"featured\": \"Öne Çıkan\",\n \"sortPriceLow\": \"Fiyat: Düşükten Yükseğe\",\n \"sortPriceHigh\": \"Fiyat: Yüksekten Düşüğe\",\n \"sortNewest\": \"En Yeni\",\n \"noProductsFound\": \"Kriterlerinize uygun ürün bulunamadı.\"\n}\n"
39
+ "content": "{\r\n \"pageTitle\": \"Ürünler\",\r\n \"title\": \"Ürünler\",\r\n \"allProducts\": \"Tüm Ürünler\",\r\n \"searchResultsFor\": \"Arama Sonuçları\",\r\n \"showing\": \"Gösterilen\",\r\n \"of\": \"/\",\r\n \"products\": \"ürün\",\r\n \"clearSearch\": \"Aramayı Temizle\",\r\n \"filters\": \"Filtreler\",\r\n \"refineSearch\": \"Ürün aramanızı daraltın\",\r\n \"categories\": \"Kategoriler\",\r\n \"priceRange\": \"Fiyat Aralığı\",\r\n \"minPrice\": \"Min\",\r\n \"maxPrice\": \"Max\",\r\n \"features\": \"Özellikler\",\r\n \"onSale\": \"İndirimde\",\r\n \"newArrivals\": \"Yeni Gelenler\",\r\n \"featuredLabel\": \"Öne Çıkan\",\r\n \"inStock\": \"Stokta\",\r\n \"sortBy\": \"Sırala\",\r\n \"featured\": \"Öne Çıkan\",\r\n \"sortPriceLow\": \"Fiyat: Düşükten Yükseğe\",\r\n \"sortPriceHigh\": \"Fiyat: Yüksekten Düşüğe\",\r\n \"sortNewest\": \"En Yeni\",\r\n \"noProductsFound\": \"Kriterlerinize uygun ürün bulunamadı.\"\r\n}\r\n"
40
40
  }
41
41
  ],
42
42
  "exports": {