@promakeai/cli 0.1.2 → 0.2.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.
- package/dist/index.js +412 -201
- package/dist/registry/about-page.json +2 -2
- package/dist/registry/about-section.json +2 -2
- package/dist/registry/announcement-bar.json +2 -2
- package/dist/registry/blog-list-page.json +1 -1
- package/dist/registry/blog-section.json +6 -4
- package/dist/registry/cards-carousel-section.json +1 -1
- package/dist/registry/cart-drawer.json +5 -4
- package/dist/registry/case-study-page.json +2 -2
- package/dist/registry/coming-soon-page-minimal.json +1 -1
- package/dist/registry/coming-soon-page.json +1 -1
- package/dist/registry/contact-info-grid.json +2 -2
- package/dist/registry/contact-page-centered.json +2 -2
- package/dist/registry/contact-page-split.json +2 -2
- package/dist/registry/contact-page.json +2 -2
- package/dist/registry/cta-section.json +2 -2
- package/dist/registry/docs/blog-section.md +3 -1
- package/dist/registry/docs/cart-drawer.md +9 -9
- package/dist/registry/docs/favorites-blog-block.md +10 -3
- package/dist/registry/docs/favorites-blog-page.md +38 -0
- package/dist/registry/docs/favorites-ecommerce-block.md +10 -3
- package/dist/registry/docs/favorites-ecommerce-page.md +38 -0
- package/dist/registry/docs/login-page.md +6 -16
- package/dist/registry/docs/payment-success-block.md +8 -1
- package/dist/registry/docs/post-detail-page.md +39 -0
- package/dist/registry/docs/product-card-detailed.md +7 -11
- package/dist/registry/docs/product-detail-page.md +39 -0
- package/dist/registry/docs/product-detail-section.md +7 -13
- package/dist/registry/docs/product-quick-view.md +4 -2
- package/dist/registry/ecommerce-core.json +2 -2
- package/dist/registry/faq-categorized.json +2 -2
- package/dist/registry/faq-simple.json +2 -2
- package/dist/registry/favorites-blog-block.json +1 -1
- package/dist/registry/favorites-blog-page.json +48 -0
- package/dist/registry/favorites-ecommerce-block.json +1 -1
- package/dist/registry/favorites-ecommerce-page.json +48 -0
- package/dist/registry/feature-section.json +2 -2
- package/dist/registry/footer.json +2 -2
- package/dist/registry/header-ecommerce.json +1 -1
- package/dist/registry/hero-carousel.json +3 -3
- package/dist/registry/hero-cta.json +2 -2
- package/dist/registry/hero-gradient.json +2 -2
- package/dist/registry/hero.json +2 -2
- package/dist/registry/index.json +9 -0
- package/dist/registry/landing-page-app.json +1 -1
- package/dist/registry/landing-page-saas.json +1 -1
- package/dist/registry/login-page-split.json +1 -1
- package/dist/registry/login-page.json +8 -6
- package/dist/registry/logo-cloud.json +1 -1
- package/dist/registry/payment-success-block.json +7 -3
- package/dist/registry/portfolio-page.json +2 -2
- package/dist/registry/post-card.json +1 -1
- package/dist/registry/post-detail-page.json +48 -0
- package/dist/registry/pricing-page.json +1 -1
- package/dist/registry/pricing-section.json +2 -2
- package/dist/registry/product-card-detailed.json +5 -4
- package/dist/registry/product-detail-page.json +48 -0
- package/dist/registry/product-detail-section.json +5 -4
- package/dist/registry/product-quick-view.json +5 -4
- package/dist/registry/products-page.json +1 -1
- package/dist/registry/reading-progress.json +1 -1
- package/dist/registry/team-page.json +1 -1
- package/dist/registry/testimonials-carousel.json +2 -2
- package/dist/registry/testimonials-grid.json +2 -2
- package/dist/registry/timeline-section.json +2 -2
- package/dist/registry/video-hero.json +1 -1
- package/package.json +53 -51
- package/template/eslint.config.js +5 -4
- package/template/src/components/ui/sidebar.tsx +2 -4
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
"name": "product-quick-view",
|
|
3
3
|
"type": "registry:component",
|
|
4
4
|
"title": "Product Quick View",
|
|
5
|
-
"description": "Modal overlay for quick product preview
|
|
5
|
+
"description": "Modal overlay for quick product preview. Features image gallery and add to cart functionality. Integrated with ecommerce-core for cart and favorites.",
|
|
6
6
|
"dependencies": [
|
|
7
7
|
"lucide-react"
|
|
8
8
|
],
|
|
9
9
|
"registryDependencies": [
|
|
10
10
|
"button",
|
|
11
|
-
"dialog"
|
|
11
|
+
"dialog",
|
|
12
|
+
"ecommerce-core"
|
|
12
13
|
],
|
|
13
|
-
"usage": "import { ProductQuickView } from '@/modules/product-quick-view';\n\n<ProductQuickView product={product} open={open} onOpenChange={setOpen} />\n\n• Installed at: src/modules/product-quick-view/\n•
|
|
14
|
+
"usage": "import { ProductQuickView } from '@/modules/product-quick-view';\nimport type { Product } from '@/modules/ecommerce-core';\n\n<ProductQuickView product={product} open={open} onOpenChange={setOpen} />\n\n• Installed at: src/modules/product-quick-view/\n• Uses useCart and useFavorites from ecommerce-core",
|
|
14
15
|
"files": [
|
|
15
16
|
{
|
|
16
17
|
"path": "product-quick-view/product-quick-view.tsx",
|
|
17
18
|
"type": "registry:component",
|
|
18
19
|
"target": "$modules$/product-quick-view/product-quick-view.tsx",
|
|
19
|
-
"content": "\"use client\";\r\n\r\nimport { useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Link } from \"react-router\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Dialog,\r\n DialogContent,\r\n DialogTitle,\r\n} from \"@/components/ui/dialog\";\r\nimport { X, Minus, Plus, ShoppingCart } from \"lucide-react\";\r\n\r\ninterface ProductVariant {\r\n id: string;\r\n name: string;\r\n available?: boolean;\r\n}\r\n\r\ninterface Product {\r\n id: string;\r\n title: string;\r\n price: number;\r\n originalPrice?: number;\r\n description: string;\r\n images: string[];\r\n sizes?: ProductVariant[];\r\n colors?: ProductVariant[];\r\n link?: string;\r\n}\r\n\r\ninterface ProductQuickViewProps {\r\n product: Product;\r\n open: boolean;\r\n onOpenChange: (open: boolean) => void;\r\n onAddToCart?: (product: Product, quantity: number, selectedSize?: string, selectedColor?: string) => void;\r\n className?: string;\r\n}\r\n\r\nexport function ProductQuickView({\r\n product,\r\n open,\r\n onOpenChange,\r\n onAddToCart,\r\n className,\r\n}: ProductQuickViewProps) {\r\n const { t } = useTranslation(\"product-quick-view\");\r\n const [selectedImage, setSelectedImage] = useState(0);\r\n const [quantity, setQuantity] = useState(1);\r\n const [selectedSize, setSelectedSize] = useState<string | undefined>(\r\n product.sizes?.[0]?.id\r\n );\r\n const [selectedColor, setSelectedColor] = useState<string | undefined>(\r\n product.colors?.[0]?.id\r\n );\r\n\r\n const formatPrice = (price: number) => {\r\n return new Intl.NumberFormat(\"en-US\", {\r\n style: \"currency\",\r\n currency: \"USD\",\r\n }).format(price);\r\n };\r\n\r\n const handleAddToCart = () => {\r\n onAddToCart?.(product, quantity, selectedSize, selectedColor);\r\n onOpenChange(false);\r\n };\r\n\r\n const incrementQuantity = () => setQuantity((q) => q + 1);\r\n const decrementQuantity = () => setQuantity((q) => Math.max(1, q - 1));\r\n\r\n const discount = product.originalPrice\r\n ? Math.round(((product.originalPrice - product.price) / product.originalPrice) * 100)\r\n : 0;\r\n\r\n return (\r\n <Dialog open={open} onOpenChange={onOpenChange}>\r\n <DialogContent className={cn(\"sm:max-w-4xl p-0 gap-0 overflow-hidden\", className)}>\r\n <DialogTitle className=\"sr-only\">{product.title}</DialogTitle>\r\n\r\n {/* Close button */}\r\n <button\r\n onClick={() => onOpenChange(false)}\r\n className=\"absolute right-4 top-4 z-10 rounded-full bg-background/80 backdrop-blur-sm p-2 hover:bg-background transition-colors\"\r\n >\r\n <X className=\"h-4 w-4\" />\r\n <span className=\"sr-only\">Close</span>\r\n </button>\r\n\r\n <div className=\"grid md:grid-cols-2\">\r\n {/* Image Gallery */}\r\n <div className=\"relative bg-muted aspect-square md:aspect-auto md:h-full\">\r\n {/* Main Image */}\r\n <img\r\n src={product.images[selectedImage]}\r\n alt={product.title}\r\n className=\"w-full h-full object-cover\"\r\n />\r\n\r\n {/* Discount Badge */}\r\n {discount > 0 && (\r\n <span className=\"absolute top-4 left-4 bg-destructive text-destructive-foreground text-sm font-semibold px-3 py-1 rounded-full\">\r\n -{discount}%\r\n </span>\r\n )}\r\n\r\n {/* Thumbnails */}\r\n {product.images.length > 1 && (\r\n <div className=\"absolute bottom-4 left-4 flex flex-col gap-2\">\r\n {product.images.map((image, index) => (\r\n <button\r\n key={index}\r\n onClick={() => setSelectedImage(index)}\r\n className={cn(\r\n \"w-14 h-14 rounded-lg overflow-hidden border-2 transition-all bg-background/80 backdrop-blur-sm\",\r\n selectedImage === index\r\n ? \"border-primary ring-2 ring-primary/20\"\r\n : \"border-transparent opacity-70 hover:opacity-100\"\r\n )}\r\n >\r\n <img\r\n src={image}\r\n alt={`${product.title} ${index + 1}`}\r\n className=\"w-full h-full object-cover\"\r\n />\r\n </button>\r\n ))}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Product Info */}\r\n <div className=\"p-6 md:p-8 flex flex-col\">\r\n <div className=\"flex-1\">\r\n <h2 className=\"text-2xl md:text-3xl font-bold mb-2\">\r\n {product.title}\r\n </h2>\r\n\r\n {/* Price */}\r\n <div className=\"flex items-center gap-3 mb-4\">\r\n <span className=\"text-2xl font-bold text-primary\">\r\n {formatPrice(product.price)}\r\n </span>\r\n {product.originalPrice && (\r\n <span className=\"text-lg text-muted-foreground line-through\">\r\n {formatPrice(product.originalPrice)}\r\n </span>\r\n )}\r\n </div>\r\n\r\n {/* Description */}\r\n <p className=\"text-muted-foreground mb-6\">\r\n {product.description}\r\n </p>\r\n\r\n {/* Size Selector */}\r\n {product.sizes && product.sizes.length > 0 && (\r\n <div className=\"mb-6\">\r\n <label className=\"text-sm font-medium mb-2 block\">\r\n {t(\"size\")}\r\n </label>\r\n <div className=\"flex flex-wrap gap-2\">\r\n {product.sizes.map((size) => (\r\n <button\r\n key={size.id}\r\n onClick={() => setSelectedSize(size.id)}\r\n disabled={size.available === false}\r\n className={cn(\r\n \"px-4 py-2 rounded-lg border text-sm font-medium transition-all\",\r\n selectedSize === size.id\r\n ? \"border-primary bg-primary text-primary-foreground\"\r\n : \"border-border hover:border-primary\",\r\n size.available === false && \"opacity-50 cursor-not-allowed line-through\"\r\n )}\r\n >\r\n {size.name}\r\n </button>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Color Selector */}\r\n {product.colors && product.colors.length > 0 && (\r\n <div className=\"mb-6\">\r\n <label className=\"text-sm font-medium mb-2 block\">\r\n {t(\"color\")}\r\n </label>\r\n <div className=\"flex flex-wrap gap-2\">\r\n {product.colors.map((color) => (\r\n <button\r\n key={color.id}\r\n onClick={() => setSelectedColor(color.id)}\r\n disabled={color.available === false}\r\n className={cn(\r\n \"px-4 py-2 rounded-lg border text-sm font-medium transition-all\",\r\n selectedColor === color.id\r\n ? \"border-primary bg-primary text-primary-foreground\"\r\n : \"border-border hover:border-primary\",\r\n color.available === false && \"opacity-50 cursor-not-allowed line-through\"\r\n )}\r\n >\r\n {color.name}\r\n </button>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Quantity */}\r\n <div className=\"mb-6\">\r\n <label className=\"text-sm font-medium mb-2 block\">\r\n {t(\"quantity\")}\r\n </label>\r\n <div className=\"flex items-center gap-3\">\r\n <div className=\"flex items-center border border-border rounded-lg\">\r\n <button\r\n onClick={decrementQuantity}\r\n className=\"p-3 hover:bg-muted transition-colors\"\r\n disabled={quantity <= 1}\r\n >\r\n <Minus className=\"h-4 w-4\" />\r\n </button>\r\n <span className=\"w-12 text-center font-medium\">\r\n {quantity}\r\n </span>\r\n <button\r\n onClick={incrementQuantity}\r\n className=\"p-3 hover:bg-muted transition-colors\"\r\n >\r\n <Plus className=\"h-4 w-4\" />\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Actions */}\r\n <div className=\"space-y-3 pt-4 border-t border-border\">\r\n <Button\r\n onClick={handleAddToCart}\r\n className=\"w-full gap-2\"\r\n size=\"lg\"\r\n >\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {t(\"addToCart\")}\r\n </Button>\r\n\r\n {product.link && (\r\n <Link to={product.link} className=\"block\">\r\n <Button variant=\"outline\" className=\"w-full\" size=\"lg\">\r\n {t(\"viewDetails\")}\r\n </Button>\r\n </Link>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </DialogContent>\r\n </Dialog>\r\n );\r\n}\r\n"
|
|
20
|
+
"content": "\"use client\";\r\n\r\nimport { useState } 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 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 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 {product.category_name && (\r\n <p className=\"text-sm text-muted-foreground mb-4\">\r\n {t(\"category\", \"Category\")}: {product.category_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"
|
|
20
21
|
},
|
|
21
22
|
{
|
|
22
23
|
"path": "product-quick-view/index.ts",
|
|
@@ -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, useEffect, useRef, useCallback } 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 { useProducts, useCategories } from \"@/modules/ecommerce-core\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\n\nexport function ProductsPage() {\n const { t } = useTranslation(\"products-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Products\") });\n const { products, loading: productsLoading } = useProducts();\n const { categories, loading: categoriesLoading } = useCategories();\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 [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);\n const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const minPriceRef = useRef<HTMLInputElement>(null);\n const maxPriceRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n const query = searchParams.get(\"search\") || \"\";\n const categorySlug = searchParams.get(\"category\");\n setSearchQuery(query);\n if (categorySlug) {\n setSelectedCategories([categorySlug]);\n }\n }, [searchParams]);\n\n useEffect(() => {\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 const sorted = [...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\n setFilteredProducts(sorted);\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 FilterSidebar = () => (\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 >\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 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={() => {\n setSearchQuery(\"\");\n setSearchParams({});\n }}\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 />\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 />\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 <ProductCard\n key={product.id}\n product={product}\n variant=\"grid\"\n />\n ))}\n </div>\n ) : (\n <div className=\"space-y-6\">\n {filteredProducts.map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n variant=\"list\"\n />\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\";\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 { useProducts, useCategories } from \"@/modules/ecommerce-core\";\nimport type { Product, Category } from \"@/modules/ecommerce-core/types\";\n\ninterface FilterSidebarProps {\n t: (key: string, fallback?: string) => string;\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 >\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 } = useProducts();\n const { categories, loading: categoriesLoading } = useCategories();\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 <ProductCard\n key={product.id}\n product={product}\n variant=\"grid\"\n />\n ))}\n </div>\n ) : (\n <div className=\"space-y-6\">\n {filteredProducts.map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n variant=\"list\"\n />\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"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "products-page/lang/en.json",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"path": "reading-progress/reading-progress.tsx",
|
|
14
14
|
"type": "registry:component",
|
|
15
15
|
"target": "$modules$/reading-progress/reading-progress.tsx",
|
|
16
|
-
"content": "\"use client\";\r\n\r\nimport { useEffect, useState } from \"react\";\r\nimport { motion, useScroll, useSpring } from \"motion/react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface ReadingProgressProps {\r\n color?: string;\r\n height?: number;\r\n showPercentage?: boolean;\r\n className?: string;\r\n}\r\n\r\nexport function ReadingProgress({\r\n color,\r\n height = 3,\r\n showPercentage = false,\r\n className,\r\n}: ReadingProgressProps) {\r\n const [mounted
|
|
16
|
+
"content": "\"use client\";\r\n\r\nimport { useEffect, useState } from \"react\";\r\nimport { motion, useScroll, useSpring } from \"motion/react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface ReadingProgressProps {\r\n color?: string;\r\n height?: number;\r\n showPercentage?: boolean;\r\n className?: string;\r\n}\r\n\r\nexport function ReadingProgress({\r\n color,\r\n height = 3,\r\n showPercentage = false,\r\n className,\r\n}: ReadingProgressProps) {\r\n const [mounted] = useState(() => typeof window !== \"undefined\");\r\n const { scrollYProgress } = useScroll();\r\n const scaleX = useSpring(scrollYProgress, {\r\n stiffness: 100,\r\n damping: 30,\r\n restDelta: 0.001,\r\n });\r\n\r\n const [percentage, setPercentage] = useState(0);\r\n\r\n useEffect(() => {\r\n if (!showPercentage) return;\r\n\r\n const unsubscribe = scrollYProgress.on(\"change\", (latest) => {\r\n setPercentage(Math.round(latest * 100));\r\n });\r\n\r\n return () => unsubscribe();\r\n }, [scrollYProgress, showPercentage]);\r\n\r\n if (!mounted) return null;\r\n\r\n return (\r\n <>\r\n <motion.div\r\n className={cn(\"fixed top-0 left-0 right-0 z-50 origin-left\", className)}\r\n style={{\r\n scaleX,\r\n height,\r\n backgroundColor: color || \"var(--primary)\",\r\n }}\r\n />\r\n {showPercentage && (\r\n <div\r\n className=\"fixed top-4 right-4 z-50 bg-background/80 backdrop-blur-sm rounded-full px-3 py-1 text-sm font-medium border border-border shadow-sm\"\r\n >\r\n {percentage}%\r\n </div>\r\n )}\r\n </>\r\n );\r\n}\r\n"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"path": "reading-progress/index.ts",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"path": "team-page/lang/en.json",
|
|
29
29
|
"type": "registry:lang",
|
|
30
30
|
"target": "$modules$/team-page/lang/en.json",
|
|
31
|
-
"content": "{\r\n \"title\": \"Our Team\",\r\n \"label\": \"Our Team\",\r\n \"heading\": \"Meet the People Behind the Magic\",\r\n \"description\": \"
|
|
31
|
+
"content": "{\r\n \"title\": \"Our Team\",\r\n \"label\": \"Our Team\",\r\n \"heading\": \"Meet the People Behind the Magic\",\r\n \"description\": \"Let Promake personalize this team description for your company culture.\",\r\n \"member1Name\": \"Team Member\",\r\n \"member1Role\": \"CEO & Founder\",\r\n \"member1Bio\": \"Add real team member bios with Promake.\",\r\n \"member2Name\": \"Team Member\",\r\n \"member2Role\": \"CTO\",\r\n \"member2Bio\": \"Customize team bios using Promake.\",\r\n \"member3Name\": \"Team Member\",\r\n \"member3Role\": \"Head of Design\",\r\n \"member3Bio\": \"Work with Promake to add member details.\",\r\n \"member4Name\": \"Team Member\",\r\n \"member4Role\": \"Lead Developer\",\r\n \"member4Bio\": \"Use Promake to personalize bios.\",\r\n \"member5Name\": \"Team Member\",\r\n \"member5Role\": \"Product Manager\",\r\n \"member5Bio\": \"Have Promake help with team bios.\",\r\n \"member6Name\": \"Team Member\",\r\n \"member6Role\": \"Marketing Director\",\r\n \"member6Bio\": \"Edit member bios via Promake.\",\r\n \"ctaTitle\": \"Want to Join Our Team?\",\r\n \"ctaDescription\": \"Update this CTA using Promake based on your hiring needs.\",\r\n \"ctaButton\": \"View Open Positions\"\r\n}\r\n"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"path": "team-page/lang/tr.json",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"path": "testimonials-carousel/lang/en.json",
|
|
27
27
|
"type": "registry:lang",
|
|
28
28
|
"target": "$modules$/testimonials-carousel/lang/en.json",
|
|
29
|
-
"content": "{\r\n \"title\": \"What Our Customers Say\",\r\n \"subtitle\": \"
|
|
29
|
+
"content": "{\r\n \"title\": \"What Our Customers Say\",\r\n \"subtitle\": \"Edit this subtitle via Promake to match your testimonials section.\",\r\n \"testimonial1Name\": \"Customer Name\",\r\n \"testimonial1Role\": \"Use Promake to update this with an appropriate job title\",\r\n \"testimonial1Review\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Let Promake personalize this testimonial with relevant customer feedback.\",\r\n \"testimonial2Name\": \"Client Name\",\r\n \"testimonial2Role\": \"Promake can help customize this role\",\r\n \"testimonial2Review\": \"This is placeholder testimonial text. Work with Promake to generate reviews for your offerings.\",\r\n \"testimonial3Name\": \"Testimonial Name\",\r\n \"testimonial3Role\": \"Replace with customer role\",\r\n \"testimonial3Review\": \"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Have Promake adjust this review to your context.\",\r\n \"testimonial4Name\": \"Reviewer Name\",\r\n \"testimonial4Role\": \"Customize this role with Promake\",\r\n \"testimonial4Review\": \"Placeholder customer testimonial. Update this content using Promake based on your industry.\"\r\n}\r\n"
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
"path": "testimonials-carousel/lang/tr.json",
|
|
33
33
|
"type": "registry:lang",
|
|
34
34
|
"target": "$modules$/testimonials-carousel/lang/tr.json",
|
|
35
|
-
"content": "{\r\n \"title\": \"Müşterilerimiz Ne Diyor\",\r\n \"subtitle\": \"
|
|
35
|
+
"content": "{\r\n \"title\": \"Müşterilerimiz Ne Diyor\",\r\n \"subtitle\": \"Bu alt başlığı referanslar bölümünüze uyacak şekilde Promake üzerinden düzenleyin.\",\r\n \"testimonial1Name\": \"Müşteri Adı\",\r\n \"testimonial1Role\": \"Bunu uygun iş unvanıyla güncellemek için Promake kullanın\",\r\n \"testimonial1Review\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Promake ile bu referansı ilgili müşteri geri bildirimleriyle kişiselleştirin.\",\r\n \"testimonial2Name\": \"Müşteri Adı\",\r\n \"testimonial2Role\": \"Promake bu rolü özelleştirmenize yardımcı olabilir\",\r\n \"testimonial2Review\": \"Bu placeholder referans metnidir. Promake ile teklifleriniz için yorumlar oluşturun.\",\r\n \"testimonial3Name\": \"Referans Adı\",\r\n \"testimonial3Role\": \"Müşteri rolüyle değiştirin\",\r\n \"testimonial3Review\": \"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Promake'ten bu yorumu bağlamınıza göre ayarlamasını isteyin.\",\r\n \"testimonial4Name\": \"Yorumcu Adı\",\r\n \"testimonial4Role\": \"Bu rolü Promake ile özelleştirin\",\r\n \"testimonial4Review\": \"Placeholder müşteri referansı. Bu içeriği sektörünüze göre Promake kullanarak güncelleyin.\"\r\n}\r\n"
|
|
36
36
|
}
|
|
37
37
|
],
|
|
38
38
|
"exports": {
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"path": "testimonials-grid/lang/en.json",
|
|
26
26
|
"type": "registry:lang",
|
|
27
27
|
"target": "$modules$/testimonials-grid/lang/en.json",
|
|
28
|
-
"content": "{\r\n \"title\": \"What People Say\",\r\n \"subtitle\": \"
|
|
28
|
+
"content": "{\r\n \"title\": \"What People Say\",\r\n \"subtitle\": \"Promake can help personalize this subtitle for your testimonials section.\",\r\n \"testimonial1Name\": \"Industry Leader Name\",\r\n \"testimonial1Role\": \"Update this with an appropriate executive title using Promake\",\r\n \"testimonial1Review\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Let Promake generate a testimonial tailored to your audience.\",\r\n \"testimonial2Name\": \"Customer Name\",\r\n \"testimonial2Role\": \"Customize this role with Promake\",\r\n \"testimonial2Review\": \"Placeholder testimonial text. Work with Promake to add relevant feedback for your services.\",\r\n \"testimonial3Name\": \"Expert Name\",\r\n \"testimonial3Role\": \"Replace with expert role\",\r\n \"testimonial3Review\": \"Sed do eiusmod tempor incididunt ut labore. Have Promake adjust this review to match your context.\"\r\n}\r\n"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "testimonials-grid/lang/tr.json",
|
|
32
32
|
"type": "registry:lang",
|
|
33
33
|
"target": "$modules$/testimonials-grid/lang/tr.json",
|
|
34
|
-
"content": "{\r\n \"title\": \"İnsanların Yorumları\",\r\n \"subtitle\": \"
|
|
34
|
+
"content": "{\r\n \"title\": \"İnsanların Yorumları\",\r\n \"subtitle\": \"Promake bu alt başlığı referanslar bölümünüze uyacak şekilde kişiselleştirmenize yardımcı olabilir.\",\r\n \"testimonial1Name\": \"Kullanıcı Adı\",\r\n \"testimonial1Role\": \"Bunu uygun yönetici unvanıyla Promake kullanarak güncelleyin\",\r\n \"testimonial1Review\": \"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Promake ile hedef kitlenize göre uyarlanmış referans oluşturun.\",\r\n \"testimonial2Name\": \"Müşteri Adı\",\r\n \"testimonial2Role\": \"Bu rolü Promake ile özelleştirin\",\r\n \"testimonial2Review\": \"Placeholder referans metni. Promake ile hizmetleriniz için ilgili geri bildirimler ekleyin.\",\r\n \"testimonial3Name\": \"Uzman Adı\",\r\n \"testimonial3Role\": \"Uzman rolüyle değiştirin\",\r\n \"testimonial3Review\": \"Sed do eiusmod tempor incididunt ut labore. Promake'ten bu yorumu bağlamınıza uyacak şekilde ayarlamasını isteyin.\"\r\n}\r\n"
|
|
35
35
|
}
|
|
36
36
|
],
|
|
37
37
|
"exports": {
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"path": "timeline-section/lang/en.json",
|
|
26
26
|
"type": "registry:lang",
|
|
27
27
|
"target": "$modules$/timeline-section/lang/en.json",
|
|
28
|
-
"content": "{\r\n \"title\": \"Our Journey\",\r\n \"subtitle\": \"
|
|
28
|
+
"content": "{\r\n \"title\": \"Our Journey\",\r\n \"subtitle\": \"Customize these milestones with Promake based on your company history.\",\r\n \"items\": [\r\n {\r\n \"year\": \"2020\",\r\n \"title\": \"Company Founded\",\r\n \"description\": \"Started with a vision to revolutionize the industry.\"\r\n },\r\n {\r\n \"year\": \"2021\",\r\n \"title\": \"First Major Milestone\",\r\n \"description\": \"Reached 10,000 customers and expanded the team.\"\r\n },\r\n {\r\n \"year\": \"2022\",\r\n \"title\": \"Global Expansion\",\r\n \"description\": \"Opened offices in 5 new countries across 3 continents.\"\r\n },\r\n {\r\n \"year\": \"2023\",\r\n \"title\": \"Industry Recognition\",\r\n \"description\": \"Won multiple awards for innovation and customer satisfaction.\"\r\n }\r\n ]\r\n}\r\n"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "timeline-section/lang/tr.json",
|
|
32
32
|
"type": "registry:lang",
|
|
33
33
|
"target": "$modules$/timeline-section/lang/tr.json",
|
|
34
|
-
"content": "{\r\n \"title\": \"Yolculuğumuz\",\r\n \"subtitle\": \"Bu kilometre taşlarını şirket geçmişinize göre
|
|
34
|
+
"content": "{\r\n \"title\": \"Yolculuğumuz\",\r\n \"subtitle\": \"Bu kilometre taşlarını şirket geçmişinize göre Promake ile özelleştirin.\",\r\n \"items\": [\r\n {\r\n \"year\": \"2020\",\r\n \"title\": \"Şirket Kuruldu\",\r\n \"description\": \"Sektörü dönüştürme vizyonuyla yola çıktık.\"\r\n },\r\n {\r\n \"year\": \"2021\",\r\n \"title\": \"İlk Büyük Başarı\",\r\n \"description\": \"10.000 müşteriye ulaştık ve ekibimizi genişlettik.\"\r\n },\r\n {\r\n \"year\": \"2022\",\r\n \"title\": \"Global Genişleme\",\r\n \"description\": \"3 kıtada 5 yeni ülkede ofis açtık.\"\r\n },\r\n {\r\n \"year\": \"2023\",\r\n \"title\": \"Sektör Takdiri\",\r\n \"description\": \"İnovasyon ve müşteri memnuniyeti alanında birçok ödül kazandık.\"\r\n }\r\n ]\r\n}\r\n"
|
|
35
35
|
}
|
|
36
36
|
],
|
|
37
37
|
"exports": {
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"path": "video-hero/lang/en.json",
|
|
25
25
|
"type": "registry:lang",
|
|
26
26
|
"target": "$modules$/video-hero/lang/en.json",
|
|
27
|
-
"content": "{\r\n \"badge\": \"Watch the Story\",\r\n \"heading\": \"Your Headline Here\",\r\n \"description\": \"
|
|
27
|
+
"content": "{\r\n \"badge\": \"Watch the Story\",\r\n \"heading\": \"Your Headline Here\",\r\n \"description\": \"Work with Promake to personalize this description for your brand. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\",\r\n \"primaryCta\": \"Get Started\",\r\n \"secondaryCta\": \"Learn More\",\r\n \"stat1\": \"Active Users\",\r\n \"stat2\": \"Satisfaction\",\r\n \"stat3\": \"Support\"\r\n}\r\n"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "video-hero/lang/tr.json",
|
package/package.json
CHANGED
|
@@ -1,52 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@promakeai/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"bin": {
|
|
6
|
-
"promake": "dist/index.js"
|
|
7
|
-
},
|
|
8
|
-
"files": [
|
|
9
|
-
"dist/index.js",
|
|
10
|
-
"dist/registry",
|
|
11
|
-
"template"
|
|
12
|
-
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"dev": "bun run src/index.ts",
|
|
15
|
-
"dev:app": "cd dev && bun run dev",
|
|
16
|
-
"dev:populate": "bun run scripts/populate-dev.ts",
|
|
17
|
-
"dev:fresh": "bun run dev:populate && bun run dev:app",
|
|
18
|
-
"playground:create": "rm -rf playground && bun run dev -- create playground --template empty --pm bun",
|
|
19
|
-
"playground:reset": "bun run playground:create",
|
|
20
|
-
"playground:add": "cd playground && bun run ../src/index.ts add",
|
|
21
|
-
"playground:ecommerce": "rm -rf playground && bun run dev -- create playground --template ecommerce --pm bun",
|
|
22
|
-
"build": "bun run build:cli && bun run build:registry",
|
|
23
|
-
"build:cli": "bun build src/index.ts --outdir dist --target node --minify",
|
|
24
|
-
"build:registry": "bun run scripts/build-registry.ts",
|
|
25
|
-
"typecheck": "tsc --noEmit",
|
|
26
|
-
"prepublishOnly": "bun run build",
|
|
27
|
-
"test": "bun test",
|
|
28
|
-
"test:watch": "bun test --watch",
|
|
29
|
-
"test:coverage": "bun test --coverage",
|
|
30
|
-
"release": "bun run build && npm publish --access public"
|
|
31
|
-
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"@types/
|
|
47
|
-
"@types/
|
|
48
|
-
"@types/
|
|
49
|
-
"@types/
|
|
50
|
-
"
|
|
51
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@promakeai/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"promake": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/index.js",
|
|
10
|
+
"dist/registry",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "bun run src/index.ts",
|
|
15
|
+
"dev:app": "cd dev && bun run dev",
|
|
16
|
+
"dev:populate": "bun run scripts/populate-dev.ts",
|
|
17
|
+
"dev:fresh": "bun run dev:populate && bun run dev:app",
|
|
18
|
+
"playground:create": "rm -rf playground && bun run dev -- create playground --template empty --pm bun",
|
|
19
|
+
"playground:reset": "bun run playground:create",
|
|
20
|
+
"playground:add": "cd playground && bun run ../src/index.ts add",
|
|
21
|
+
"playground:ecommerce": "rm -rf playground && bun run dev -- create playground --template ecommerce --pm bun",
|
|
22
|
+
"build": "bun run build:cli && bun run build:registry",
|
|
23
|
+
"build:cli": "bun build src/index.ts --outdir dist --target node --minify",
|
|
24
|
+
"build:registry": "bun run scripts/build-registry.ts",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"prepublishOnly": "bun run build",
|
|
27
|
+
"test": "bun test",
|
|
28
|
+
"test:watch": "bun test --watch",
|
|
29
|
+
"test:coverage": "bun test --coverage",
|
|
30
|
+
"release": "bun run build && npm publish --access public"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@lightpanda/browser": "^1.0.1",
|
|
34
|
+
"adm-zip": "^0.5.16",
|
|
35
|
+
"archiver": "^7.0.1",
|
|
36
|
+
"chalk": "^5.3.0",
|
|
37
|
+
"commander": "^12.1.0",
|
|
38
|
+
"culori": "^4.0.2",
|
|
39
|
+
"fs-extra": "^11.2.0",
|
|
40
|
+
"glob": "^11.0.0",
|
|
41
|
+
"ora": "^8.1.1",
|
|
42
|
+
"prompts": "^2.4.2",
|
|
43
|
+
"puppeteer-core": "^24.36.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/archiver": "^7.0.0",
|
|
47
|
+
"@types/bun": "^1.1.14",
|
|
48
|
+
"@types/culori": "^4.0.1",
|
|
49
|
+
"@types/fs-extra": "^11.0.4",
|
|
50
|
+
"@types/node": "^22.10.2",
|
|
51
|
+
"@types/prompts": "^2.4.9",
|
|
52
|
+
"typescript": "^5.7.2"
|
|
53
|
+
}
|
|
52
54
|
}
|
|
@@ -22,11 +22,12 @@ export default defineConfig([
|
|
|
22
22
|
globals: globals.browser,
|
|
23
23
|
},
|
|
24
24
|
rules: {
|
|
25
|
-
"@typescript-eslint/no-explicit-any": "
|
|
26
|
-
"@typescript-eslint/no-unused-vars": "
|
|
27
|
-
"react-refresh/only-export-components": "
|
|
25
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
26
|
+
"@typescript-eslint/no-unused-vars": "off",
|
|
27
|
+
"react-refresh/only-export-components": "off",
|
|
28
28
|
"react-hooks/purity": "warn",
|
|
29
|
-
"react-hooks/exhaustive-deps": "
|
|
29
|
+
"react-hooks/exhaustive-deps": "off",
|
|
30
|
+
"react-hooks/set-state-in-effect": "warn",
|
|
30
31
|
},
|
|
31
32
|
},
|
|
32
33
|
]);
|
|
@@ -606,10 +606,8 @@ function SidebarMenuSkeleton({
|
|
|
606
606
|
}: React.ComponentProps<"div"> & {
|
|
607
607
|
showIcon?: boolean;
|
|
608
608
|
}) {
|
|
609
|
-
//
|
|
610
|
-
const width =
|
|
611
|
-
return `${Math.floor(Math.random() * 40) + 50}%`;
|
|
612
|
-
}, []);
|
|
609
|
+
// Fixed width for skeleton
|
|
610
|
+
const width = "70%";
|
|
613
611
|
|
|
614
612
|
return (
|
|
615
613
|
<div
|