@promakeai/cli 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,104 +1,104 @@
1
- [
2
- "about-page",
3
- "about-section",
4
- "announcement-bar",
5
- "api",
6
- "auth-core",
7
- "animations",
8
- "bento-grid-section",
9
- "blog-core",
10
- "blog-list-page",
11
- "blog-section",
12
- "cards-carousel-section",
13
- "cart-drawer",
14
- "cart-page",
15
- "case-study-page",
16
- "category-section",
17
- "checkout-page",
18
- "coming-soon-page",
19
- "coming-soon-page-minimal",
20
- "contact-info-grid",
21
- "contact-page",
22
- "contact-page-centered",
23
- "contact-page-map-overlay",
24
- "contact-page-map-split",
25
- "contact-page-split",
26
- "content-section",
27
- "cookie-consent",
28
- "cookies-page",
29
- "cta-section",
30
- "db",
31
- "ecommerce-core",
32
- "empty-page",
33
- "faq-categorized",
34
- "faq-simple",
35
- "favorites-blog-block",
36
- "favorites-blog-page",
37
- "favorites-ecommerce-block",
38
- "favorites-ecommerce-page",
39
- "feature-section",
40
- "featured-products",
41
- "footer",
42
- "footer-detailed",
43
- "footer-minimal",
44
- "forgot-password-page",
45
- "forgot-password-page-split",
46
- "google-adsense",
47
- "google-map",
48
- "header-centered-pill",
49
- "header-ecommerce",
50
- "header-mega",
51
- "header-minimal",
52
- "header-simple",
53
- "hero",
54
- "hero-carousel",
55
- "hero-cta",
56
- "hero-gradient",
57
- "hero-grid",
58
- "hero-profile",
59
- "landing-page-app",
60
- "landing-page-saas",
61
- "login-page",
62
- "login-page-split",
63
- "logo-cloud",
64
- "masonry-grid",
65
- "my-orders-page",
66
- "newsletter-section",
67
- "order-card-compact",
68
- "order-confirmation-page",
69
- "order-detail-block",
70
- "orders-list-block",
71
- "payment-success-block",
72
- "portfolio-page",
73
- "post-card",
74
- "post-detail-block",
75
- "post-detail-page",
76
- "pricing-card",
77
- "pricing-page",
78
- "pricing-section",
79
- "privacy-page",
80
- "product-card",
81
- "product-card-detailed",
82
- "product-card-hover",
83
- "product-detail-block",
84
- "product-detail-page",
85
- "product-detail-section",
86
- "product-quick-view",
87
- "products-page",
88
- "reading-progress",
89
- "register-page",
90
- "register-page-split",
91
- "related-posts-block",
92
- "related-products-block",
93
- "reset-password-page-split",
94
- "service-card",
95
- "share-buttons",
96
- "skill-card",
97
- "team-page",
98
- "terms-page",
99
- "testimonials-carousel",
100
- "testimonials-grid",
101
- "timeline-section",
102
- "video-hero",
103
- "youtube-embed"
104
- ]
1
+ [
2
+ "about-page",
3
+ "about-section",
4
+ "announcement-bar",
5
+ "api",
6
+ "auth-core",
7
+ "animations",
8
+ "bento-grid-section",
9
+ "blog-core",
10
+ "blog-list-page",
11
+ "blog-section",
12
+ "cards-carousel-section",
13
+ "cart-drawer",
14
+ "cart-page",
15
+ "case-study-page",
16
+ "category-section",
17
+ "checkout-page",
18
+ "coming-soon-page",
19
+ "coming-soon-page-minimal",
20
+ "contact-info-grid",
21
+ "contact-page",
22
+ "contact-page-centered",
23
+ "contact-page-map-overlay",
24
+ "contact-page-map-split",
25
+ "contact-page-split",
26
+ "content-section",
27
+ "cookie-consent",
28
+ "cookies-page",
29
+ "cta-section",
30
+ "db",
31
+ "ecommerce-core",
32
+ "empty-page",
33
+ "faq-categorized",
34
+ "faq-simple",
35
+ "favorites-blog-block",
36
+ "favorites-blog-page",
37
+ "favorites-ecommerce-block",
38
+ "favorites-ecommerce-page",
39
+ "feature-section",
40
+ "featured-products",
41
+ "footer",
42
+ "footer-detailed",
43
+ "footer-minimal",
44
+ "forgot-password-page",
45
+ "forgot-password-page-split",
46
+ "google-adsense",
47
+ "google-map",
48
+ "header-centered-pill",
49
+ "header-ecommerce",
50
+ "header-mega",
51
+ "header-minimal",
52
+ "header-simple",
53
+ "hero",
54
+ "hero-carousel",
55
+ "hero-cta",
56
+ "hero-gradient",
57
+ "hero-grid",
58
+ "hero-profile",
59
+ "landing-page-app",
60
+ "landing-page-saas",
61
+ "login-page",
62
+ "login-page-split",
63
+ "logo-cloud",
64
+ "masonry-grid",
65
+ "my-orders-page",
66
+ "newsletter-section",
67
+ "order-card-compact",
68
+ "order-confirmation-page",
69
+ "order-detail-block",
70
+ "orders-list-block",
71
+ "payment-success-block",
72
+ "portfolio-page",
73
+ "post-card",
74
+ "post-detail-block",
75
+ "post-detail-page",
76
+ "pricing-card",
77
+ "pricing-page",
78
+ "pricing-section",
79
+ "privacy-page",
80
+ "product-card",
81
+ "product-card-detailed",
82
+ "product-card-hover",
83
+ "product-detail-block",
84
+ "product-detail-page",
85
+ "product-detail-section",
86
+ "product-quick-view",
87
+ "products-page",
88
+ "reading-progress",
89
+ "register-page",
90
+ "register-page-split",
91
+ "related-posts-block",
92
+ "related-products-block",
93
+ "reset-password-page-split",
94
+ "service-card",
95
+ "share-buttons",
96
+ "skill-card",
97
+ "team-page",
98
+ "terms-page",
99
+ "testimonials-carousel",
100
+ "testimonials-grid",
101
+ "timeline-section",
102
+ "video-hero",
103
+ "youtube-embed"
104
+ ]
@@ -20,7 +20,7 @@
20
20
  "path": "payment-success-block/payment-success-block.tsx",
21
21
  "type": "registry:block",
22
22
  "target": "$modules$/payment-success-block/payment-success-block.tsx",
23
- "content": "import { Link } from \"react-router\";\nimport {\n CheckCircle,\n XCircle,\n Loader2,\n ShoppingBag,\n Package,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { useTranslation } from \"react-i18next\";\nimport { formatPrice } from \"@/modules/ecommerce-core\";\n\ntype PaymentStatus = \"loading\" | \"success\" | \"failed\";\n\ninterface OrderDetails {\n id?: string;\n totalAmount?: number;\n currency?: string;\n paymentMethod?: string;\n paymentStatus?: string;\n status?: string;\n}\n\ninterface PaymentSuccessBlockProps {\n status: PaymentStatus;\n orderDetails?: OrderDetails | null;\n errorMessage?: string;\n onRetry?: () => void;\n}\n\nexport function PaymentSuccessBlock({\n status,\n orderDetails,\n errorMessage,\n onRetry,\n}: PaymentSuccessBlockProps) {\n const { t } = useTranslation(\"payment-success-block\");\n\n // Loading State\n if (status === \"loading\") {\n return (\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\n <div className=\"max-w-md mx-auto text-center\">\n <Card>\n <CardContent className=\"pt-8 pb-8\">\n <Loader2 className=\"w-16 h-16 text-primary mx-auto mb-4 animate-spin\" />\n <h1 className=\"text-2xl font-bold mb-2\">\n {t(\"verifyingPayment\", \"Verifying Payment\")}\n </h1>\n <p className=\"text-muted-foreground\">\n {t(\n \"pleaseWait\",\n \"Please wait while we verify your payment...\"\n )}\n </p>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n }\n\n // Failed State\n if (status === \"failed\") {\n return (\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\n <div className=\"max-w-md mx-auto text-center\">\n <Card>\n <CardContent className=\"pt-8 pb-8\">\n <XCircle className=\"w-16 h-16 text-destructive mx-auto mb-4\" />\n <h1 className=\"text-2xl font-bold mb-2\">\n {t(\"paymentFailed\", \"Payment Failed\")}\n </h1>\n\n {/* Error Message */}\n {errorMessage && (\n <div className=\"bg-destructive/10 border border-destructive/30 rounded-lg p-4 mb-4\">\n <p className=\"text-sm text-destructive\">{errorMessage}</p>\n </div>\n )}\n\n <p className=\"text-muted-foreground mb-6\">\n {t(\n \"paymentFailedDescription\",\n \"We couldn't verify your payment. Please try again or contact support.\"\n )}\n </p>\n\n <div className=\"flex flex-col gap-3\">\n {onRetry && (\n <Button onClick={onRetry}>\n {t(\"tryAgain\", \"Try Again\")}\n </Button>\n )}\n <Button variant=\"outline\" asChild>\n <Link to=\"/contact\">\n {t(\"contactSupport\", \"Contact Support\")}\n </Link>\n </Button>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n }\n\n // Success State\n return (\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\n <div className=\"max-w-lg mx-auto text-center\">\n <Card>\n <CardContent className=\"pt-8 pb-8\">\n <CheckCircle className=\"w-20 h-20 text-green-600 dark:text-green-400 mx-auto mb-6\" />\n <h1 className=\"text-3xl font-bold mb-2\">\n {t(\"thankYou\", \"Thank You!\")}\n </h1>\n <p className=\"text-xl text-muted-foreground mb-6\">\n {t(\"orderConfirmed\", \"Your order has been confirmed.\")}\n </p>\n\n {orderDetails && (\n <div className=\"bg-muted/50 rounded-lg p-4 mb-6 text-left\">\n <h3 className=\"font-semibold mb-2\">\n {t(\"orderDetails\", \"Order Details\")}\n </h3>\n <div className=\"text-sm space-y-1\">\n {orderDetails.id && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"orderId\", \"Order ID\")}:\n </span>{\" \"}\n <span className=\"font-medium\">{orderDetails.id}</span>\n </p>\n )}\n {orderDetails.totalAmount !== undefined && orderDetails.currency && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"total\", \"Total\")}:\n </span>{\" \"}\n <span className=\"font-medium\">\n {formatPrice(orderDetails.totalAmount, orderDetails.currency)}\n </span>\n </p>\n )}\n {orderDetails.paymentMethod && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"paymentMethod\", \"Payment Method\")}:\n </span>{\" \"}\n <span className=\"font-medium capitalize\">\n {orderDetails.paymentMethod}\n </span>\n </p>\n )}\n {orderDetails.paymentStatus && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"paymentStatus\", \"Payment Status\")}:\n </span>{\" \"}\n <span className=\"font-medium capitalize text-green-600 dark:text-green-400\">\n {orderDetails.paymentStatus}\n </span>\n </p>\n )}\n {orderDetails.status && (\n <p>\n <span className=\"text-muted-foreground\">\n {t(\"orderStatus\", \"Order Status\")}:\n </span>{\" \"}\n <span className=\"font-medium capitalize\">\n {orderDetails.status}\n </span>\n </p>\n )}\n </div>\n </div>\n )}\n\n <p className=\"text-sm text-muted-foreground mb-6\">\n {t(\n \"confirmationEmailSent\",\n \"A confirmation email has been sent to your email address.\"\n )}\n </p>\n\n <div className=\"flex flex-col gap-3\">\n <Button asChild size=\"lg\">\n <Link to=\"/my-orders\">\n <Package className=\"w-4 h-4 mr-2\" />\n {t(\"viewMyOrders\", \"View My Orders\")}\n </Link>\n </Button>\n <Button variant=\"outline\" asChild>\n <Link to=\"/products\">\n <ShoppingBag className=\"w-4 h-4 mr-2\" />\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n );\n}\n"
23
+ "content": "import { Link } from \"react-router\";\r\nimport {\r\n CheckCircle,\r\n XCircle,\r\n Loader2,\r\n ShoppingBag,\r\n Package,\r\n} from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { formatPrice } from \"@/modules/ecommerce-core\";\r\n\r\ntype PaymentStatus = \"loading\" | \"success\" | \"failed\";\r\n\r\ninterface OrderDetails {\r\n id?: string;\r\n totalAmount?: number;\r\n currency?: string;\r\n paymentMethod?: string;\r\n paymentStatus?: string;\r\n status?: string;\r\n}\r\n\r\ninterface PaymentSuccessBlockProps {\r\n status: PaymentStatus;\r\n orderDetails?: OrderDetails | null;\r\n errorMessage?: string;\r\n onRetry?: () => void;\r\n}\r\n\r\nexport function PaymentSuccessBlock({\r\n status,\r\n orderDetails,\r\n errorMessage,\r\n onRetry,\r\n}: PaymentSuccessBlockProps) {\r\n const { t } = useTranslation(\"payment-success-block\");\r\n\r\n // Loading State\r\n if (status === \"loading\") {\r\n return (\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\r\n <div className=\"max-w-md mx-auto text-center\">\r\n <Card>\r\n <CardContent className=\"pt-8 pb-8\">\r\n <Loader2 className=\"w-16 h-16 text-primary mx-auto mb-4 animate-spin\" />\r\n <h1 className=\"text-2xl font-bold mb-2\">\r\n {t(\"verifyingPayment\", \"Verifying Payment\")}\r\n </h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\r\n \"pleaseWait\",\r\n \"Please wait while we verify your payment...\"\r\n )}\r\n </p>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n );\r\n }\r\n\r\n // Failed State\r\n if (status === \"failed\") {\r\n return (\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\r\n <div className=\"max-w-md mx-auto text-center\">\r\n <Card>\r\n <CardContent className=\"pt-8 pb-8\">\r\n <XCircle className=\"w-16 h-16 text-destructive mx-auto mb-4\" />\r\n <h1 className=\"text-2xl font-bold mb-2\">\r\n {t(\"paymentFailed\", \"Payment Failed\")}\r\n </h1>\r\n\r\n {/* Error Message */}\r\n {errorMessage && (\r\n <div className=\"bg-destructive/10 border border-destructive/30 rounded-lg p-4 mb-4\">\r\n <p className=\"text-sm text-destructive\">{errorMessage}</p>\r\n </div>\r\n )}\r\n\r\n <p className=\"text-muted-foreground mb-6\">\r\n {t(\r\n \"paymentFailedDescription\",\r\n \"We couldn't verify your payment. Please try again or contact support.\"\r\n )}\r\n </p>\r\n\r\n <div className=\"flex flex-col gap-3\">\r\n {onRetry && (\r\n <Button onClick={onRetry}>\r\n {t(\"tryAgain\", \"Try Again\")}\r\n </Button>\r\n )}\r\n <Button variant=\"outline\" asChild>\r\n <Link to=\"/contact\">\r\n {t(\"contactSupport\", \"Contact Support\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n );\r\n }\r\n\r\n // Success State\r\n return (\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\r\n <div className=\"max-w-lg mx-auto text-center\">\r\n <Card>\r\n <CardContent className=\"pt-8 pb-8\">\r\n <CheckCircle className=\"w-20 h-20 text-green-600 dark:text-green-400 mx-auto mb-6\" />\r\n <h1 className=\"text-3xl font-bold mb-2\">\r\n {t(\"thankYou\", \"Thank You!\")}\r\n </h1>\r\n <p className=\"text-xl text-muted-foreground mb-6\">\r\n {t(\"orderConfirmed\", \"Your order has been confirmed.\")}\r\n </p>\r\n\r\n {orderDetails && (\r\n <div className=\"bg-muted/50 rounded-lg p-4 mb-6 text-left\">\r\n <h3 className=\"font-semibold mb-2\">\r\n {t(\"orderDetails\", \"Order Details\")}\r\n </h3>\r\n <div className=\"text-sm space-y-1\">\r\n {orderDetails.id && (\r\n <p>\r\n <span className=\"text-muted-foreground\">\r\n {t(\"orderId\", \"Order ID\")}:\r\n </span>{\" \"}\r\n <span className=\"font-medium\">{orderDetails.id}</span>\r\n </p>\r\n )}\r\n {orderDetails.totalAmount !== undefined && orderDetails.currency && (\r\n <p>\r\n <span className=\"text-muted-foreground\">\r\n {t(\"total\", \"Total\")}:\r\n </span>{\" \"}\r\n <span className=\"font-medium\">\r\n {formatPrice(orderDetails.totalAmount, orderDetails.currency)}\r\n </span>\r\n </p>\r\n )}\r\n {orderDetails.paymentMethod && (\r\n <p>\r\n <span className=\"text-muted-foreground\">\r\n {t(\"paymentMethod\", \"Payment Method\")}:\r\n </span>{\" \"}\r\n <span className=\"font-medium capitalize\">\r\n {orderDetails.paymentMethod}\r\n </span>\r\n </p>\r\n )}\r\n {orderDetails.paymentStatus && (\r\n <p>\r\n <span className=\"text-muted-foreground\">\r\n {t(\"paymentStatus\", \"Payment Status\")}:\r\n </span>{\" \"}\r\n <span className=\"font-medium capitalize text-green-600 dark:text-green-400\">\r\n {orderDetails.paymentStatus}\r\n </span>\r\n </p>\r\n )}\r\n {orderDetails.status && (\r\n <p>\r\n <span className=\"text-muted-foreground\">\r\n {t(\"orderStatus\", \"Order Status\")}:\r\n </span>{\" \"}\r\n <span className=\"font-medium capitalize\">\r\n {orderDetails.status}\r\n </span>\r\n </p>\r\n )}\r\n </div>\r\n </div>\r\n )}\r\n\r\n <p className=\"text-sm text-muted-foreground mb-6\">\r\n {t(\r\n \"confirmationEmailSent\",\r\n \"A confirmation email has been sent to your email address.\"\r\n )}\r\n </p>\r\n\r\n <div className=\"flex flex-col gap-3\">\r\n <Button asChild size=\"lg\">\r\n <Link to=\"/my-orders\">\r\n <Package className=\"w-4 h-4 mr-2\" />\r\n {t(\"viewMyOrders\", \"View My Orders\")}\r\n </Link>\r\n </Button>\r\n <Button variant=\"outline\" asChild>\r\n <Link to=\"/products\">\r\n <ShoppingBag className=\"w-4 h-4 mr-2\" />\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
24
24
  },
25
25
  {
26
26
  "path": "payment-success-block/lang/en.json",
@@ -18,7 +18,7 @@
18
18
  "path": "post-detail-block/post-detail-block.tsx",
19
19
  "type": "registry:block",
20
20
  "target": "$modules$/post-detail-block/post-detail-block.tsx",
21
- "content": "import { Link } from \"react-router\";\nimport {\n Calendar,\n User,\n Eye,\n Clock,\n ArrowLeft,\n Share2,\n Heart,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { useTranslation } from \"react-i18next\";\nimport type { Post } from \"@/modules/blog-core/types\";\n\ninterface PostDetailBlockProps {\n post: Post;\n onShare?: () => void;\n onAddToFavorites?: () => void;\n isFavorite?: boolean;\n}\n\nexport function PostDetailBlock({\n post,\n onShare,\n onAddToFavorites,\n isFavorite = false,\n}: PostDetailBlockProps) {\n const { t } = useTranslation(\"post-detail-block\");\n\n const formatDate = (dateString?: string) => {\n if (!dateString) return \"\";\n return new Date(dateString).toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n });\n };\n\n return (\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"max-w-4xl mx-auto\">\n {/* Back Button */}\n <div className=\"mb-6\">\n <Button variant=\"ghost\" asChild>\n <Link to=\"/blog\">\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n {t(\"backToBlog\", \"Back to Blog\")}\n </Link>\n </Button>\n </div>\n\n {/* Post Header */}\n <div className=\"mb-8\">\n {/* Category Badge */}\n {post.category && (\n <Badge variant=\"secondary\" className=\"mb-4\">\n {post.category}\n </Badge>\n )}\n\n {/* Title */}\n <h1 className=\"text-4xl md:text-5xl font-bold mb-6\">{post.title}</h1>\n\n {/* Meta Information */}\n <div className=\"flex flex-wrap items-center gap-4 text-muted-foreground mb-6\">\n {post.author && (\n <div className=\"flex items-center gap-2\">\n {post.author_avatar ? (\n <Avatar className=\"w-8 h-8\">\n <AvatarImage src={post.author_avatar} alt={post.author} />\n <AvatarFallback>\n {post.author\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n ) : (\n <User className=\"w-4 h-4\" />\n )}\n <span className=\"font-medium\">{post.author}</span>\n </div>\n )}\n\n {post.published_at && (\n <div className=\"flex items-center gap-1\">\n <Calendar className=\"w-4 h-4\" />\n <span>{formatDate(post.published_at)}</span>\n </div>\n )}\n\n {post.read_time && (\n <div className=\"flex items-center gap-1\">\n <Clock className=\"w-4 h-4\" />\n <span>\n {post.read_time} {t(\"minRead\", \"min read\")}\n </span>\n </div>\n )}\n\n {post.view_count !== undefined && (\n <div className=\"flex items-center gap-1\">\n <Eye className=\"w-4 h-4\" />\n <span>\n {post.view_count} {t(\"views\", \"views\")}\n </span>\n </div>\n )}\n </div>\n\n {/* Actions */}\n <div className=\"flex gap-2\">\n {onShare && (\n <Button variant=\"outline\" size=\"sm\" onClick={onShare}>\n <Share2 className=\"w-4 h-4 mr-2\" />\n {t(\"share\", \"Share\")}\n </Button>\n )}\n {onAddToFavorites && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={onAddToFavorites}\n className={isFavorite ? \"text-red-500\" : \"\"}\n >\n <Heart\n className={`w-4 h-4 mr-2 ${isFavorite ? \"fill-current\" : \"\"}`}\n />\n {isFavorite\n ? t(\"removeFromFavorites\", \"Remove\")\n : t(\"addToFavorites\", \"Add to Favorites\")}\n </Button>\n )}\n </div>\n </div>\n\n {/* Featured Image */}\n {post.featured_image && (\n <div className=\"mb-8\">\n <img\n src={post.featured_image}\n alt={post.title}\n className=\"w-full h-auto rounded-lg\"\n />\n </div>\n )}\n\n <Separator className=\"my-8\" />\n\n {/* Post Content */}\n <article className=\"prose prose-lg dark:prose-invert max-w-none\">\n <div\n dangerouslySetInnerHTML={{ __html: post.content }}\n className=\"leading-relaxed\"\n />\n </article>\n\n {/* Tags */}\n {post.tags && post.tags.length > 0 && (\n <div className=\"mt-8\">\n <Separator className=\"mb-4\" />\n <div className=\"flex items-center gap-2 flex-wrap\">\n <span className=\"text-sm font-medium text-muted-foreground\">\n {t(\"tags\", \"Tags\")}:\n </span>\n {post.tags.map((tag, index) => (\n <Badge key={index} variant=\"outline\">\n {tag}\n </Badge>\n ))}\n </div>\n </div>\n )}\n\n {/* Author Box */}\n {post.author && (\n <div className=\"mt-12 p-6 bg-muted/30 rounded-lg\">\n <div className=\"flex items-start gap-4\">\n {post.author_avatar && (\n <Avatar className=\"w-16 h-16\">\n <AvatarImage src={post.author_avatar} alt={post.author} />\n <AvatarFallback>\n {post.author\n .split(\" \")\n .map((n) => n[0])\n .join(\"\")}\n </AvatarFallback>\n </Avatar>\n )}\n <div>\n <h3 className=\"font-semibold text-lg mb-1\">{post.author}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {t(\"authorDescription\", \"Author of this post\")}\n </p>\n </div>\n </div>\n </div>\n )}\n </div>\n </div>\n );\n}\n"
21
+ "content": "import { Link } from \"react-router\";\r\nimport {\r\n Calendar,\r\n User,\r\n Eye,\r\n Clock,\r\n ArrowLeft,\r\n Share2,\r\n Heart,\r\n} from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport type { Post } from \"@/modules/blog-core/types\";\r\n\r\ninterface PostDetailBlockProps {\r\n post: Post;\r\n onShare?: () => void;\r\n onAddToFavorites?: () => void;\r\n isFavorite?: boolean;\r\n}\r\n\r\nexport function PostDetailBlock({\r\n post,\r\n onShare,\r\n onAddToFavorites,\r\n isFavorite = false,\r\n}: PostDetailBlockProps) {\r\n const { t } = useTranslation(\"post-detail-block\");\r\n\r\n const formatDate = (dateString?: string) => {\r\n if (!dateString) return \"\";\r\n return new Date(dateString).toLocaleDateString(\"en-US\", {\r\n year: \"numeric\",\r\n month: \"long\",\r\n day: \"numeric\",\r\n });\r\n };\r\n\r\n return (\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"max-w-4xl mx-auto\">\r\n {/* Back Button */}\r\n <div className=\"mb-6\">\r\n <Button variant=\"ghost\" asChild>\r\n <Link to=\"/blog\">\r\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\r\n {t(\"backToBlog\", \"Back to Blog\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n\r\n {/* Post Header */}\r\n <div className=\"mb-8\">\r\n {/* Category Badge */}\r\n {post.category && (\r\n <Badge variant=\"secondary\" className=\"mb-4\">\r\n {post.category}\r\n </Badge>\r\n )}\r\n\r\n {/* Title */}\r\n <h1 className=\"text-4xl md:text-5xl font-bold mb-6\">{post.title}</h1>\r\n\r\n {/* Meta Information */}\r\n <div className=\"flex flex-wrap items-center gap-4 text-muted-foreground mb-6\">\r\n {post.author && (\r\n <div className=\"flex items-center gap-2\">\r\n {post.author_avatar ? (\r\n <Avatar className=\"w-8 h-8\">\r\n <AvatarImage src={post.author_avatar} alt={post.author} />\r\n <AvatarFallback>\r\n {post.author\r\n .split(\" \")\r\n .map((n) => n[0])\r\n .join(\"\")}\r\n </AvatarFallback>\r\n </Avatar>\r\n ) : (\r\n <User className=\"w-4 h-4\" />\r\n )}\r\n <span className=\"font-medium\">{post.author}</span>\r\n </div>\r\n )}\r\n\r\n {post.published_at && (\r\n <div className=\"flex items-center gap-1\">\r\n <Calendar className=\"w-4 h-4\" />\r\n <span>{formatDate(post.published_at)}</span>\r\n </div>\r\n )}\r\n\r\n {post.read_time && (\r\n <div className=\"flex items-center gap-1\">\r\n <Clock className=\"w-4 h-4\" />\r\n <span>\r\n {post.read_time} {t(\"minRead\", \"min read\")}\r\n </span>\r\n </div>\r\n )}\r\n\r\n {post.view_count !== undefined && (\r\n <div className=\"flex items-center gap-1\">\r\n <Eye className=\"w-4 h-4\" />\r\n <span>\r\n {post.view_count} {t(\"views\", \"views\")}\r\n </span>\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Actions */}\r\n <div className=\"flex gap-2\">\r\n {onShare && (\r\n <Button variant=\"outline\" size=\"sm\" onClick={onShare}>\r\n <Share2 className=\"w-4 h-4 mr-2\" />\r\n {t(\"share\", \"Share\")}\r\n </Button>\r\n )}\r\n {onAddToFavorites && (\r\n <Button\r\n variant=\"outline\"\r\n size=\"sm\"\r\n onClick={onAddToFavorites}\r\n className={isFavorite ? \"text-red-500\" : \"\"}\r\n >\r\n <Heart\r\n className={`w-4 h-4 mr-2 ${isFavorite ? \"fill-current\" : \"\"}`}\r\n />\r\n {isFavorite\r\n ? t(\"removeFromFavorites\", \"Remove\")\r\n : t(\"addToFavorites\", \"Add to Favorites\")}\r\n </Button>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Featured Image */}\r\n {post.featured_image && (\r\n <div className=\"mb-8\">\r\n <img\r\n src={post.featured_image}\r\n alt={post.title}\r\n className=\"w-full h-auto rounded-lg\"\r\n />\r\n </div>\r\n )}\r\n\r\n <Separator className=\"my-8\" />\r\n\r\n {/* Post Content */}\r\n <article className=\"prose prose-lg dark:prose-invert max-w-none\">\r\n <div\r\n dangerouslySetInnerHTML={{ __html: post.content }}\r\n className=\"leading-relaxed\"\r\n />\r\n </article>\r\n\r\n {/* Tags */}\r\n {post.tags && post.tags.length > 0 && (\r\n <div className=\"mt-8\">\r\n <Separator className=\"mb-4\" />\r\n <div className=\"flex items-center gap-2 flex-wrap\">\r\n <span className=\"text-sm font-medium text-muted-foreground\">\r\n {t(\"tags\", \"Tags\")}:\r\n </span>\r\n {post.tags.map((tag, index) => (\r\n <Badge key={index} variant=\"outline\">\r\n {tag}\r\n </Badge>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Author Box */}\r\n {post.author && (\r\n <div className=\"mt-12 p-6 bg-muted/30 rounded-lg\">\r\n <div className=\"flex items-start gap-4\">\r\n {post.author_avatar && (\r\n <Avatar className=\"w-16 h-16\">\r\n <AvatarImage src={post.author_avatar} alt={post.author} />\r\n <AvatarFallback>\r\n {post.author\r\n .split(\" \")\r\n .map((n) => n[0])\r\n .join(\"\")}\r\n </AvatarFallback>\r\n </Avatar>\r\n )}\r\n <div>\r\n <h3 className=\"font-semibold text-lg mb-1\">{post.author}</h3>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"authorDescription\", \"Author of this post\")}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
22
22
  },
23
23
  {
24
24
  "path": "post-detail-block/lang/en.json",
@@ -18,7 +18,7 @@
18
18
  "path": "product-detail-block/product-detail-block.tsx",
19
19
  "type": "registry:block",
20
20
  "target": "$modules$/product-detail-block/product-detail-block.tsx",
21
- "content": "import { useState } from \"react\";\nimport {\n Star,\n Heart,\n Share2,\n Truck,\n RotateCcw,\n Shield,\n Plus,\n Minus,\n ChevronLeft,\n ChevronRight,\n X,\n ZoomIn,\n} from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\nimport { useTranslation } from \"react-i18next\";\nimport constants from \"@/constants/constants.json\";\n\ninterface ProductDetailBlockProps {\n product: Product;\n}\n\nexport function ProductDetailBlock({ product }: ProductDetailBlockProps) {\n const { t } = useTranslation(\"product-detail-block\");\n const { addItem } = useCart();\n\n const [selectedImageIndex, setSelectedImageIndex] = useState(0);\n const [quantity, setQuantity] = useState(1);\n const [selectedVariant, setSelectedVariant] = useState<string>(\"\");\n const [isAdding, setIsAdding] = useState(false);\n const [isImageModalOpen, setIsImageModalOpen] = useState(false);\n const [zoomLevel, setZoomLevel] = useState(1);\n const [position, setPosition] = useState({ x: 0, y: 0 });\n const [isDragging, setIsDragging] = useState(false);\n const [dragStart, setDragStart] = useState({ x: 0, y: 0 });\n\n const handleAddToCart = async () => {\n if (product) {\n setIsAdding(true);\n // Add multiple items by calling addItem quantity times\n for (let i = 0; i < quantity; i++) {\n addItem(product);\n }\n\n // Show success feedback\n setTimeout(() => {\n setIsAdding(false);\n }, 1000);\n }\n };\n\n const handleQuantityChange = (change: number) => {\n const newQuantity = quantity + change;\n if (newQuantity >= 1 && newQuantity <= product.stock) {\n setQuantity(newQuantity);\n }\n };\n\n const features = [\n {\n icon: Truck,\n title: t(\"freeShipping\", \"Free Shipping\"),\n description: t(\"freeShippingDesc\", \"On orders over 50\"),\n },\n {\n icon: RotateCcw,\n title: t(\"easyReturns\", \"Easy Returns\"),\n description: t(\"easyReturnsDesc\", \"30-day return policy\"),\n },\n {\n icon: Shield,\n title: t(\"secureCheckout\", \"Secure Checkout\"),\n description: t(\"secureCheckoutDesc\", \"SSL encrypted payment\"),\n },\n ];\n\n return (\n <>\n <div className=\"grid lg:grid-cols-2 gap-12\">\n {/* Product Images */}\n <div className=\"space-y-4\">\n {/* Main Image */}\n <div className=\"aspect-square relative overflow-hidden rounded-lg bg-muted group\">\n <img\n src={product.images[selectedImageIndex] || \"/images/placeholder.png\"}\n alt={product.name}\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300 cursor-zoom-in\"\n onClick={() => setIsImageModalOpen(true)}\n />\n\n {/* Zoom Indicator */}\n <div className=\"absolute bottom-4 right-4 bg-black/50 text-white p-2 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity\">\n <ZoomIn className=\"w-5 h-5\" />\n </div>\n\n {/* Navigation Arrows */}\n {product.images.length > 1 && (\n <>\n <Button\n variant=\"secondary\"\n size=\"icon\"\n className=\"absolute left-4 top-1/2 transform -translate-y-1/2\"\n onClick={() =>\n setSelectedImageIndex(\n selectedImageIndex === 0\n ? product.images.length - 1\n : selectedImageIndex - 1\n )\n }\n >\n <ChevronLeft className=\"h-4 w-4\" />\n </Button>\n <Button\n variant=\"secondary\"\n size=\"icon\"\n className=\"absolute right-4 top-1/2 transform -translate-y-1/2\"\n onClick={() =>\n setSelectedImageIndex(\n selectedImageIndex === product.images.length - 1\n ? 0\n : selectedImageIndex + 1\n )\n }\n >\n <ChevronRight className=\"h-4 w-4\" />\n </Button>\n </>\n )}\n\n {/* Badges */}\n <div className=\"absolute top-4 left-4 flex flex-col gap-2\">\n {product.on_sale && (\n <Badge variant=\"destructive\">{t(\"sale\", \"Sale\")}</Badge>\n )}\n {product.is_new && (\n <Badge variant=\"secondary\">{t(\"new\", \"New\")}</Badge>\n )}\n </div>\n </div>\n\n {/* Thumbnail Images */}\n {product.images.length > 1 && (\n <div className=\"flex gap-3 overflow-x-auto\">\n {product.images.map((image, index) => (\n <button\n key={index}\n className={`flex-shrink-0 w-20 h-20 rounded-lg overflow-hidden border-2 transition-colors ${\n selectedImageIndex === index\n ? \"border-primary\"\n : \"border-transparent hover:border-muted-foreground\"\n }`}\n onClick={() => setSelectedImageIndex(index)}\n >\n <img\n src={image}\n alt={`${product.name} ${index + 1}`}\n className=\"w-full h-full object-cover\"\n />\n </button>\n ))}\n </div>\n )}\n </div>\n\n {/* Product Info */}\n <div className=\"space-y-6\">\n {/* Header */}\n <div>\n <div className=\"flex items-center gap-2 mb-2\">\n <Badge variant=\"outline\">\n {product.category_name ||\n product.categories?.[0]?.name ||\n product.category}\n </Badge>\n {product.featured && (\n <Badge variant=\"secondary\">{t(\"featured\", \"Featured\")}</Badge>\n )}\n </div>\n <h1 className=\"text-3xl font-bold mb-4\">{product.name}</h1>\n\n {/* Rating */}\n <div className=\"flex items-center gap-4 mb-4\">\n <div className=\"flex items-center gap-1\">\n {[...Array(5)].map((_, i) => (\n <Star\n key={i}\n className={`h-4 w-4 ${\n i < Math.floor(product.rating)\n ? \"fill-current text-yellow-400\"\n : \"text-muted-foreground\"\n }`}\n />\n ))}\n <span className=\"text-sm font-medium ml-1\">\n {product.rating}\n </span>\n </div>\n </div>\n\n {/* Price */}\n <div className=\"flex items-center gap-3 mb-6\">\n <span className=\"text-3xl font-bold\">\n {formatPrice(\n product.on_sale && product.sale_price\n ? product.sale_price\n : product.price,\n constants.site.currency\n )}\n </span>\n {product.on_sale && product.sale_price && (\n <span className=\"text-xl text-muted-foreground line-through\">\n {formatPrice(\n product.price,\n constants.site.currency\n )}\n </span>\n )}\n </div>\n </div>\n\n {/* Description */}\n <div>\n <p className=\"text-muted-foreground leading-relaxed\">\n {product.description}\n </p>\n </div>\n\n {/* Variants */}\n {product.variants && product.variants.length > 0 && (\n <div>\n <label className=\"text-sm font-medium mb-2 block\">\n {t(\"size\", \"Size\")}\n </label>\n <Select\n value={selectedVariant}\n onValueChange={setSelectedVariant}\n >\n <SelectTrigger className=\"w-full\">\n <SelectValue placeholder={t(\"selectSize\", \"Select size\")} />\n </SelectTrigger>\n <SelectContent>\n {product.variants.map((variant: any) => (\n <SelectItem\n key={variant.id}\n value={variant.id}\n disabled={variant.stockQuantity === 0}\n >\n {variant.value}{\" \"}\n {variant.stockQuantity === 0 &&\n `(${t(\"outOfStock\", \"Out of Stock\")})`}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n\n {/* Quantity */}\n <div>\n <label className=\"text-sm font-medium mb-2 block\">\n {t(\"quantity\", \"Quantity\")}\n </label>\n <div className=\"flex items-center gap-3\">\n <div className=\"flex items-center border rounded-lg\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleQuantityChange(-1)}\n disabled={quantity <= 1}\n >\n <Minus className=\"h-4 w-4\" />\n </Button>\n <span className=\"w-12 text-center font-medium\">{quantity}</span>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => handleQuantityChange(1)}\n disabled={quantity >= product.stock}\n >\n <Plus className=\"h-4 w-4\" />\n </Button>\n </div>\n <span className=\"text-sm text-muted-foreground\">\n {product.stock} {t(\"available\", \"available\")}\n </span>\n </div>\n </div>\n\n {/* Actions */}\n <div className=\"space-y-3\">\n <div className=\"flex gap-3\">\n <Button\n size=\"lg\"\n className=\"flex-1\"\n disabled={product.stock <= 0 || isAdding}\n variant=\"default\"\n onClick={handleAddToCart}\n >\n {isAdding ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"adding\", \"Adding...\")}\n </>\n ) : (\n t(\"addToCart\", \"Add to Cart\")\n )}\n </Button>\n <Button variant=\"outline\" size=\"lg\">\n <Heart className=\"h-4 w-4\" />\n </Button>\n <Button variant=\"outline\" size=\"lg\">\n <Share2 className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {product.stock > 0 ? (\n <p className=\"text-sm text-green-600 dark:text-green-400 font-medium\">\n ✓ {t(\"inStockReady\", \"In stock and ready to ship\")} (\n {product.stock} {t(\"available\", \"available\")})\n </p>\n ) : (\n <p className=\"text-sm text-red-600 dark:text-red-400 font-medium\">\n ✗ {t(\"currentlyOutOfStock\", \"Currently out of stock\")}\n </p>\n )}\n </div>\n\n {/* Features */}\n <div className=\"grid grid-cols-3 gap-4 pt-6 border-t\">\n {features.map((feature, index) => {\n const Icon = feature.icon;\n return (\n <div key={index} className=\"text-center\">\n <Icon className=\"h-6 w-6 mx-auto mb-2 text-primary\" />\n <h4 className=\"text-sm font-medium\">{feature.title}</h4>\n <p className=\"text-xs text-muted-foreground\">\n {feature.description}\n </p>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n {/* Product Details Tabs */}\n <Tabs defaultValue=\"description\" className=\"mt-16\">\n <TabsList className=\"grid w-full grid-cols-2\">\n <TabsTrigger value=\"description\">\n {t(\"description\", \"Description\")}\n </TabsTrigger>\n <TabsTrigger value=\"specifications\">\n {t(\"specifications\", \"Specifications\")}\n </TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"description\" className=\"mt-6\">\n <Card>\n <CardContent className=\"pt-6\">\n <div className=\"prose max-w-none\">\n <p className=\"text-muted-foreground leading-relaxed\">\n {product.description}\n </p>\n <Separator className=\"my-6\" />\n <h3 className=\"text-lg font-semibold mb-3\">\n {t(\"keyFeatures\", \"Key Features\")}\n </h3>\n <ul className=\"space-y-2\">\n {Array.isArray(product.tags) && product.tags.map((tag, index) => (\n <li key={index} className=\"flex items-center gap-2\">\n <div className=\"w-1.5 h-1.5 bg-primary rounded-full\"></div>\n <span className=\"capitalize\">{tag}</span>\n </li>\n ))}\n </ul>\n </div>\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"specifications\" className=\"mt-6\">\n <Card>\n <CardContent className=\"pt-6\">\n {product.specifications ? (\n <div className=\"grid gap-4\">\n {Object.entries(product.specifications).map(\n ([key, value]) => (\n <div\n key={key}\n className=\"bg-muted/30 rounded-lg p-4 flex justify-between items-center\"\n >\n <span className=\"font-medium text-foreground capitalize\">\n {key\n .replace(/_/g, \" \")\n .replace(/([A-Z])/g, \" $1\")\n .trim()}\n </span>\n <span className=\"text-muted-foreground font-mono bg-background px-3 py-1 rounded-md border\">\n {value}\n </span>\n </div>\n )\n )}\n </div>\n ) : (\n <p className=\"text-muted-foreground\">\n {t(\"noSpecifications\", \"No specifications available.\")}\n </p>\n )}\n </CardContent>\n </Card>\n </TabsContent>\n </Tabs>\n\n {/* Image Zoom Modal */}\n <Dialog\n open={isImageModalOpen}\n onOpenChange={(open) => {\n setIsImageModalOpen(open);\n if (!open) {\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }\n }}\n >\n <DialogContent className=\"!max-w-[95vw] !w-[95vw] !h-[95vh] p-0 overflow-hidden bg-black/95 border-none [&>button]:hidden\">\n {/* Close Button */}\n <button\n onClick={() => setIsImageModalOpen(false)}\n className=\"absolute top-4 right-4 z-50 rounded-full bg-black/60 hover:bg-black/80 p-2 transition-all border border-white/10 backdrop-blur-md\"\n >\n <X className=\"h-6 w-6 text-white\" />\n </button>\n\n {/* Image Counter */}\n {product.images.length > 1 && (\n <div className=\"absolute top-4 left-1/2 -translate-x-1/2 z-50 bg-black/60 backdrop-blur-md rounded-full px-4 py-2 border border-white/10\">\n <span className=\"text-white text-sm font-semibold\">\n {selectedImageIndex + 1} / {product.images.length}\n </span>\n </div>\n )}\n\n {/* Navigation Buttons */}\n {product.images.length > 1 && (\n <>\n <button\n className=\"absolute left-4 top-1/2 -translate-y-1/2 z-50 bg-black/60 hover:bg-black/80 text-white border border-white/10 backdrop-blur-md rounded-full p-3 transition-all\"\n onClick={() => {\n setSelectedImageIndex(\n selectedImageIndex === 0\n ? product.images.length - 1\n : selectedImageIndex - 1\n );\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n >\n <ChevronLeft className=\"h-6 w-6\" />\n </button>\n <button\n className=\"absolute right-4 top-1/2 -translate-y-1/2 z-50 bg-black/60 hover:bg-black/80 text-white border border-white/10 backdrop-blur-md rounded-full p-3 transition-all\"\n onClick={() => {\n setSelectedImageIndex(\n selectedImageIndex === product.images.length - 1\n ? 0\n : selectedImageIndex + 1\n );\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n >\n <ChevronRight className=\"h-6 w-6\" />\n </button>\n </>\n )}\n\n {/* Zoom Controls */}\n <div className=\"absolute bottom-4 left-1/2 -translate-x-1/2 z-50 flex items-center gap-2 bg-black/60 backdrop-blur-md rounded-full px-4 py-2 border border-white/10\">\n <button\n className=\"text-white hover:bg-white/20 h-8 w-8 rounded-full flex items-center justify-center disabled:opacity-50\"\n onClick={() => {\n const newZoom = Math.max(1, zoomLevel - 0.3);\n setZoomLevel(newZoom);\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\n }}\n disabled={zoomLevel <= 1}\n >\n <Minus className=\"h-4 w-4\" />\n </button>\n <span className=\"text-white text-sm font-semibold min-w-[50px] text-center\">\n {Math.round(zoomLevel * 100)}%\n </span>\n <button\n className=\"text-white hover:bg-white/20 h-8 w-8 rounded-full flex items-center justify-center disabled:opacity-50\"\n onClick={() => setZoomLevel(Math.min(4, zoomLevel + 0.3))}\n disabled={zoomLevel >= 4}\n >\n <Plus className=\"h-4 w-4\" />\n </button>\n <div className=\"w-px h-5 bg-white/20 mx-1\" />\n <button\n className=\"text-white hover:bg-white/20 text-xs px-3 h-8 rounded-full disabled:opacity-50\"\n onClick={() => {\n setZoomLevel(1);\n setPosition({ x: 0, y: 0 });\n }}\n disabled={zoomLevel === 1}\n >\n Reset\n </button>\n </div>\n\n {/* Zoomable Image Container */}\n <div\n className=\"w-full h-full flex items-center justify-center overflow-hidden select-none\"\n style={{\n cursor: zoomLevel > 1 ? (isDragging ? \"grabbing\" : \"grab\") : \"zoom-in\",\n }}\n onWheel={(e) => {\n e.preventDefault();\n const delta = e.deltaY > 0 ? -0.15 : 0.15;\n const newZoom = Math.max(1, Math.min(4, zoomLevel + delta));\n setZoomLevel(newZoom);\n if (newZoom === 1) setPosition({ x: 0, y: 0 });\n }}\n onMouseDown={(e) => {\n if (zoomLevel > 1) {\n setIsDragging(true);\n setDragStart({\n x: e.clientX - position.x,\n y: e.clientY - position.y,\n });\n } else if (e.detail === 2) {\n setZoomLevel(2.5);\n }\n }}\n onMouseMove={(e) => {\n if (isDragging && zoomLevel > 1) {\n setPosition({\n x: e.clientX - dragStart.x,\n y: e.clientY - dragStart.y,\n });\n }\n }}\n onMouseUp={() => setIsDragging(false)}\n onMouseLeave={() => setIsDragging(false)}\n onClick={(e) => {\n if (zoomLevel === 1 && e.detail === 1) {\n setZoomLevel(2.5);\n }\n }}\n >\n <img\n src={product.images[selectedImageIndex] || \"/images/placeholder.png\"}\n alt={product.name}\n className=\"max-w-[85vw] max-h-[85vh] object-contain pointer-events-none transition-transform duration-200\"\n style={{\n transform: `scale(${zoomLevel}) translate(${position.x / zoomLevel}px, ${position.y / zoomLevel}px)`,\n }}\n draggable={false}\n />\n </div>\n </DialogContent>\n </Dialog>\n </>\n );\n}\n"
21
+ "content": "import { useState } 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 } from \"@/modules/ecommerce-core/types\";\r\nimport { useTranslation } from \"react-i18next\";\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\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[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 {product.category_name ||\r\n product.categories?.[0]?.name ||\r\n product.category}\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: any) => (\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 {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[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",
@@ -24,7 +24,7 @@
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 { useDbProducts, useDbCategories } from \"@/modules/ecommerce-core\";\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 { products, loading: productsLoading } = useDbProducts();\n const { categories, loading: categoriesLoading } = useDbCategories();\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 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 return selectedCategories.some((selectedCategory) => {\n if (product.category === selectedCategory) return true;\n return product.categories?.some(\n (cat) => cat.slug === selectedCategory\n );\n });\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, 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 key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\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 { useDbProducts, useDbCategories } from \"@/modules/ecommerce-core\";\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 { products, loading: productsLoading } = useDbProducts();\r\n const { categories, loading: categoriesLoading } = useDbCategories();\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 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 return selectedCategories.some((selectedCategory) => {\r\n if (product.category === selectedCategory) return true;\r\n return product.categories?.some(\r\n (cat) => cat.slug === selectedCategory\r\n );\r\n });\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, 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 key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@promakeai/cli",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "promake": "dist/index.js"
@@ -51,4 +51,4 @@
51
51
  "@types/prompts": "^2.4.9",
52
52
  "typescript": "^5.7.2"
53
53
  }
54
- }
54
+ }