@promakeai/cli 0.4.0 → 0.4.1
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 +2 -2
- package/dist/registry/auth-core.json +1 -1
- package/dist/registry/blog-core.json +16 -16
- package/dist/registry/blog-list-page.json +2 -2
- package/dist/registry/blog-section.json +1 -1
- package/dist/registry/category-section.json +1 -1
- package/dist/registry/checkout-page.json +1 -1
- package/dist/registry/docs/blog-core.md +6 -6
- package/dist/registry/docs/blog-list-page.md +1 -1
- package/dist/registry/docs/ecommerce-core.md +6 -6
- package/dist/registry/docs/featured-products.md +1 -1
- package/dist/registry/docs/post-detail-page.md +2 -2
- package/dist/registry/docs/product-detail-page.md +2 -2
- package/dist/registry/docs/products-page.md +1 -1
- package/dist/registry/ecommerce-core.json +15 -15
- package/dist/registry/featured-products.json +2 -2
- package/dist/registry/header-ecommerce.json +1 -1
- package/dist/registry/order-confirmation-page.json +1 -1
- package/dist/registry/post-detail-page.json +3 -3
- package/dist/registry/product-detail-page.json +3 -3
- package/dist/registry/products-page.json +2 -2
- package/package.json +1 -1
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"path": "header-ecommerce/header-ecommerce.tsx",
|
|
21
21
|
"type": "registry:component",
|
|
22
22
|
"target": "$modules$/header-ecommerce/header-ecommerce.tsx",
|
|
23
|
-
"content": "import { useState } from \"react\";\nimport { Link, useNavigate } from \"react-router\";\nimport { ShoppingCart, Menu, Search, Heart, Package, User, LogOut } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n Sheet,\n SheetHeader,\n SheetTitle,\n SheetContent,\n SheetTrigger,\n} from \"@/components/ui/sheet\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Logo } from \"@/components/Logo\";\nimport { useAuth } from \"@/modules/auth-core\";\nimport { CartDrawer } from \"@/modules/cart-drawer\";\nimport { toast } from \"sonner\";\nimport { useTranslation } from \"react-i18next\";\nimport constants from \"@/constants/constants.json\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\nimport {\n useCart,\n useFavorites,\n useSearch,\n formatPrice,\n} from \"@/modules/ecommerce-core\";\n\nexport function HeaderEcommerce() {\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [mobileSearchOpen, setMobileSearchOpen] = useState(false);\n const [desktopSearchOpen, setDesktopSearchOpen] = useState(false);\n const [showResults, setShowResults] = useState(false);\n const { itemCount, state } = useCart();\n const { favoriteCount } = useFavorites();\n const { isAuthenticated, user, logout } = useAuth();\n const navigate = useNavigate();\n const { t } = useTranslation(\"header-ecommerce\");\n\n const handleLogout = () => {\n logout();\n toast.success(t(\"logoutToastTitle\", \"Goodbye!\"), {\n description: t(\"logoutToastDesc\", \"You have been logged out successfully.\"),\n });\n };\n\n const {\n searchTerm,\n setSearchTerm,\n results: searchResults,\n clearSearch,\n } = useSearch();\n\n const handleSearchSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (searchTerm.trim()) {\n navigate(`/products?search=${encodeURIComponent(searchTerm)}`);\n setShowResults(false);\n setDesktopSearchOpen(false);\n clearSearch();\n }\n };\n\n const handleSearchFocus = () => {\n setShowResults(true);\n };\n\n const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setSearchTerm(e.target.value);\n setShowResults(true);\n };\n\n const navigation = [\n { name: t(\"home\"), href: \"/\" },\n { name: t(\"products\"), href: \"/products\" },\n { name: t(\"about\"), href: \"/about\" },\n { name: t(\"contact\"), href: \"/contact\" },\n ];\n\n return (\n <header className=\"sticky top-0 z-50 w-full border-b border-border/20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\n <div className=\"flex h-14 sm:h-16 md:h-20 items-center justify-between gap-2\">\n {/* Logo */}\n <div className=\"flex-shrink-0 min-w-0\">\n <Logo size=\"sm\" className=\"text-base sm:text-xl lg:text-2xl\" />\n </div>\n\n {/* Desktop Navigation - Centered */}\n <nav className=\"hidden lg:flex items-center space-x-12 absolute left-1/2 transform -translate-x-1/2\">\n {navigation.map((item) => (\n <Link\n key={item.name}\n to={item.href}\n className=\"text-base font-medium transition-colors hover:text-primary relative group py-2\"\n >\n {item.name}\n <span className=\"absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full\"></span>\n </Link>\n ))}\n </nav>\n\n {/* Search & Actions - Right Aligned */}\n <div className=\"flex items-center space-x-1 sm:space-x-2 lg:space-x-4 flex-shrink-0\">\n {/* Desktop Search - Modal */}\n <Dialog\n open={desktopSearchOpen}\n onOpenChange={setDesktopSearchOpen}\n >\n <DialogTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"hidden lg:flex h-10 w-10\"\n >\n <Search className=\"h-5 w-5\" />\n </Button>\n </DialogTrigger>\n <DialogContent className=\"sm:max-w-2xl\">\n <DialogHeader>\n <DialogTitle>\n {t(\"searchProducts\", \"Search Products\")}\n </DialogTitle>\n </DialogHeader>\n <div className=\"space-y-4\">\n <form onSubmit={handleSearchSubmit}>\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5\" />\n <Input\n type=\"search\"\n placeholder={t(\n \"searchPlaceholder\",\n \"Search for products...\"\n )}\n value={searchTerm}\n onChange={handleSearchChange}\n className=\"pl-11 h-12 text-base\"\n autoFocus\n />\n </div>\n </form>\n\n {/* Desktop Search Results */}\n {searchTerm.trim() && (\n <div className=\"max-h-[400px] overflow-y-auto rounded-lg border bg-card\">\n {searchResults.length > 0 ? (\n <div className=\"divide-y\">\n <div className=\"px-4 py-3 bg-muted/50\">\n <p className=\"text-sm font-medium text-muted-foreground\">\n {searchResults.length}{\" \"}\n {searchResults.length === 1\n ? \"result\"\n : \"results\"}{\" \"}\n found\n </p>\n </div>\n {searchResults.slice(0, 8).map((product: Product) => (\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\n <Link\n to={`/products/${product.slug}`}\n onClick={() => {\n setDesktopSearchOpen(false);\n clearSearch();\n }}\n className=\"flex items-center gap-4 p-4 hover:bg-muted/50 transition-colors\"\n >\n <img\n src={\n product.images[0] || \"/images/placeholder.png\"\n }\n alt={product.name}\n className=\"w-16 h-16 object-cover rounded flex-shrink-0\"\n />\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-medium text-base line-clamp-1\">\n {product.name}\n </h4>\n <p className=\"text-sm text-muted-foreground capitalize\">\n {product.category}\n </p>\n <p className=\"text-base font-semibold text-primary mt-1\">\n {formatPrice(\n product.price,\n constants.site.currency\n )}\n </p>\n </div>\n </Link>\n </div>\n ))}\n {searchResults.length > 8 && (\n <div className=\"px-4 py-3 bg-muted/30 text-center\">\n <button\n onClick={() => {\n navigate(\n `/products?search=${encodeURIComponent(\n searchTerm\n )}`\n );\n setDesktopSearchOpen(false);\n clearSearch();\n }}\n className=\"text-sm font-medium text-primary hover:underline\"\n >\n {t(\n \"viewAllResults\",\n `View all ${searchResults.length} results`\n )}\n </button>\n </div>\n )}\n </div>\n ) : (\n <div className=\"p-8 text-center\">\n <Search className=\"h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50\" />\n <p className=\"text-base text-muted-foreground\">\n {t(\"noResults\", \"No products found\")}\n </p>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {t(\n \"tryDifferentKeywords\",\n \"Try different keywords\"\n )}\n </p>\n </div>\n )}\n </div>\n )}\n </div>\n </DialogContent>\n </Dialog>\n\n {/* Search - Mobile (Hidden - moved to hamburger menu) */}\n <Dialog open={mobileSearchOpen} onOpenChange={setMobileSearchOpen}>\n <DialogTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className=\"hidden\">\n <Search className=\"h-4 w-4 sm:h-5 sm:w-5\" />\n </Button>\n </DialogTrigger>\n <DialogContent className=\"sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>{t(\"searchProducts\")}</DialogTitle>\n </DialogHeader>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (searchTerm.trim()) {\n navigate(\n `/products?search=${encodeURIComponent(searchTerm)}`\n );\n setMobileSearchOpen(false);\n clearSearch();\n }\n }}\n className=\"space-y-4\"\n >\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\n <Input\n type=\"search\"\n placeholder={t(\"searchPlaceholder\")}\n value={searchTerm}\n onChange={(e) => setSearchTerm(e.target.value)}\n className=\"pl-10\"\n autoFocus\n />\n </div>\n <div className=\"flex gap-2\">\n <Button type=\"submit\" className=\"flex-1\">\n {t(\"searchButton\", \"Search\")}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => {\n clearSearch();\n setMobileSearchOpen(false);\n }}\n >\n {t(\"cancel\", \"Cancel\")}\n </Button>\n </div>\n </form>\n\n {/* Mobile Search Results */}\n {searchTerm.trim() && (\n <div className=\"mt-4 max-h-64 overflow-y-auto\">\n {searchResults.length > 0 ? (\n <div className=\"space-y-2\">\n <p className=\"text-sm text-muted-foreground mb-2\">\n {searchResults.length} result\n {searchResults.length !== 1 ? \"s\" : \"\"} found\n </p>\n {searchResults.slice(0, 5).map((product: Product) => (\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\n <Link\n to={`/products/${product.slug}`}\n onClick={() => {\n setMobileSearchOpen(false);\n clearSearch();\n }}\n className=\"block p-2 rounded hover:bg-muted/50 transition-colors\"\n >\n <div className=\"flex items-center gap-3\">\n <img\n src={\n product.images[0] || \"/images/placeholder.png\"\n }\n alt={product.name}\n className=\"w-10 h-10 object-cover rounded\"\n />\n <div className=\"flex-1\">\n <h4 className=\"font-medium text-sm\">\n {product.name}\n </h4>\n <p className=\"text-xs text-muted-foreground\">\n {product.category}\n </p>\n <p className=\"text-sm font-medium\">\n {formatPrice(\n product.price,\n constants.site.currency\n )}\n </p>\n </div>\n </div>\n </Link>\n </div>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t(\"noResults\")}\n </p>\n )}\n </div>\n )}\n </DialogContent>\n </Dialog>\n\n {/* Wishlist - Desktop Only */}\n <Link to=\"/favorites\" className=\"hidden lg:block\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"relative h-10 w-10\"\n >\n <Heart className=\"h-5 w-5\" />\n {favoriteCount > 0 && (\n <Badge\n variant=\"destructive\"\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\n >\n {favoriteCount}\n </Badge>\n )}\n </Button>\n </Link>\n\n {/* Cart - Desktop Only (Goes to Cart Page) */}\n <Link to=\"/cart\" className=\"hidden lg:block\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"relative h-10 w-10\"\n >\n <ShoppingCart className=\"h-5 w-5\" />\n {itemCount > 0 && (\n <Badge\n variant=\"destructive\"\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\n >\n {itemCount}\n </Badge>\n )}\n </Button>\n </Link>\n\n {/* Auth - Desktop Only */}\n <div className=\"hidden lg:flex\">\n {isAuthenticated ? (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\n <User className=\"h-5 w-5\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <DropdownMenuLabel className=\"font-normal\">\n <div className=\"flex flex-col space-y-1\">\n <p className=\"text-sm font-medium\">{user?.username}</p>\n {user?.email && (\n <p className=\"text-xs text-muted-foreground\">{user.email}</p>\n )}\n </div>\n </DropdownMenuLabel>\n <DropdownMenuSeparator />\n <DropdownMenuItem asChild className=\"cursor-pointer\">\n <Link to=\"/my-orders\" className=\"flex items-center\">\n <Package className=\"mr-2 h-4 w-4\" />\n {t(\"myOrders\", \"My Orders\")}\n </Link>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onClick={handleLogout}\n className=\"text-red-600 focus:text-red-600 focus:bg-red-50 cursor-pointer\"\n >\n <LogOut className=\"mr-2 h-4 w-4\" />\n {t(\"logout\", \"Logout\")}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n ) : (\n <Link to=\"/login\">\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\n <User className=\"h-5 w-5\" />\n </Button>\n </Link>\n )}\n </div>\n\n {/* Mobile Menu */}\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\n <SheetTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"lg:hidden h-8 w-8 sm:h-10 sm:w-10\"\n >\n <Menu className=\"h-4 w-4 sm:h-5 sm:w-5\" />\n </Button>\n </SheetTrigger>\n <SheetContent side=\"right\" className=\"w-[300px] sm:w-[400px] px-6\">\n <SheetHeader>\n <SheetTitle>{t(\"menu\")}</SheetTitle>\n </SheetHeader>\n\n {/* Mobile Search in Hamburger */}\n <div className=\"mt-6 pb-4 border-b\">\n <form onSubmit={handleSearchSubmit}>\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\n <Input\n type=\"search\"\n placeholder={t(\"searchPlaceholder\")}\n value={searchTerm}\n onChange={handleSearchChange}\n onFocus={handleSearchFocus}\n className=\"pl-10 h-11\"\n />\n </div>\n </form>\n\n {/* Search Results in Hamburger */}\n {showResults && searchTerm && (\n <div className=\"mt-3 max-h-[300px] overflow-y-auto rounded-lg border bg-card\">\n {searchResults.length > 0 ? (\n <div className=\"divide-y\">\n <div className=\"px-3 py-2 bg-muted/50\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n {searchResults.length}{\" \"}\n {searchResults.length === 1\n ? \"result\"\n : \"results\"}\n </p>\n </div>\n {searchResults.slice(0, 5).map((product: Product) => (\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\n <Link\n to={`/products/${product.slug}`}\n onClick={() => {\n setMobileMenuOpen(false);\n clearSearch();\n setShowResults(false);\n }}\n className=\"flex items-center gap-3 p-3 hover:bg-muted/50 transition-colors\"\n >\n <img\n src={\n product.images[0] || \"/images/placeholder.png\"\n }\n alt={product.name}\n className=\"w-14 h-14 object-cover rounded flex-shrink-0\"\n />\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-medium text-sm line-clamp-1\">\n {product.name}\n </h4>\n <p className=\"text-xs text-muted-foreground capitalize\">\n {product.category}\n </p>\n <p className=\"text-sm font-semibold text-primary mt-1\">\n {formatPrice(\n product.price,\n constants.site.currency\n )}\n </p>\n </div>\n </Link>\n </div>\n ))}\n {searchResults.length > 5 && (\n <div className=\"px-3 py-2 bg-muted/30 text-center\">\n <button\n onClick={() => {\n navigate(\n `/products?search=${encodeURIComponent(\n searchTerm\n )}`\n );\n setMobileMenuOpen(false);\n clearSearch();\n setShowResults(false);\n }}\n className=\"text-xs font-medium text-primary hover:underline\"\n >\n {t(\n \"viewAllResults\",\n `View all ${searchResults.length} results`\n )}\n </button>\n </div>\n )}\n </div>\n ) : (\n <div className=\"p-6 text-center\">\n <Search className=\"h-8 w-8 text-muted-foreground mx-auto mb-2 opacity-50\" />\n <p className=\"text-sm text-muted-foreground\">\n {t(\"noResults\", \"No results found\")}\n </p>\n </div>\n )}\n </div>\n )}\n </div>\n\n <div className=\"flex flex-col space-y-4 mt-6\">\n {navigation.map((item) => (\n <Link\n key={item.name}\n to={item.href}\n className=\"text-lg font-medium hover:text-primary transition-colors\"\n onClick={() => setMobileMenuOpen(false)}\n >\n {item.name}\n </Link>\n ))}\n <div className=\"border-t pt-4 space-y-4\">\n <Link\n to=\"/favorites\"\n className=\"flex items-center justify-between text-lg font-medium hover:text-primary transition-colors\"\n onClick={() => setMobileMenuOpen(false)}\n >\n <div className=\"flex items-center space-x-2\">\n <Heart className=\"h-5 w-5\" />\n <span>{t(\"favorites\")}</span>\n </div>\n <Badge variant=\"secondary\">{favoriteCount}</Badge>\n </Link>\n <Link\n to=\"/cart\"\n className=\"flex items-center justify-between w-full text-lg font-medium hover:text-primary transition-colors\"\n onClick={() => setMobileMenuOpen(false)}\n >\n <div className=\"flex items-center space-x-2\">\n <ShoppingCart className=\"h-5 w-5\" />\n <span>{t(\"cart\")}</span>\n </div>\n <div className=\"flex flex-col items-end\">\n <Badge variant=\"secondary\">{itemCount}</Badge>\n <span className=\"text-xs text-muted-foreground\">\n {formatPrice(state.total, constants.site.currency)}\n </span>\n </div>\n </Link>\n\n {/* Auth - Mobile */}\n {isAuthenticated ? (\n <div className=\"space-y-3\">\n <div className=\"flex items-center space-x-3 p-3 bg-muted/50 rounded-lg\">\n <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center\">\n <User className=\"h-5 w-5 text-primary\" />\n </div>\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">{user?.username}</p>\n {user?.email && (\n <p className=\"text-xs text-muted-foreground truncate\">{user.email}</p>\n )}\n </div>\n </div>\n <Link\n to=\"/my-orders\"\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\n onClick={() => setMobileMenuOpen(false)}\n >\n <Package className=\"h-5 w-5\" />\n <span>{t(\"myOrders\", \"My Orders\")}</span>\n </Link>\n <button\n onClick={() => {\n handleLogout();\n setMobileMenuOpen(false);\n }}\n className=\"flex items-center space-x-2 text-lg font-medium text-red-600 hover:text-red-700 transition-colors w-full\"\n >\n <LogOut className=\"h-5 w-5\" />\n <span>{t(\"logout\", \"Logout\")}</span>\n </button>\n </div>\n ) : (\n <Link\n to=\"/login\"\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\n onClick={() => setMobileMenuOpen(false)}\n >\n <User className=\"h-5 w-5\" />\n <span>{t(\"login\", \"Login\")}</span>\n </Link>\n )}\n </div>\n </div>\n </SheetContent>\n </Sheet>\n </div>\n </div>\n </div>\n {/* Cart Drawer */}\n <CartDrawer showTrigger={false} />\n </header>\n );\n}\n"
|
|
23
|
+
"content": "import { useState } from \"react\";\nimport { Link, useNavigate } from \"react-router\";\nimport { ShoppingCart, Menu, Search, Heart, Package, User, LogOut } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n Sheet,\n SheetHeader,\n SheetTitle,\n SheetContent,\n SheetTrigger,\n} from \"@/components/ui/sheet\";\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Logo } from \"@/components/Logo\";\nimport { useAuth } from \"@/modules/auth-core\";\nimport { CartDrawer } from \"@/modules/cart-drawer\";\nimport { toast } from \"sonner\";\nimport { useTranslation } from \"react-i18next\";\nimport constants from \"@/constants/constants.json\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\nimport {\n useCart,\n useFavorites,\n useDbSearch,\n formatPrice,\n} from \"@/modules/ecommerce-core\";\n\nexport function HeaderEcommerce() {\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\n const [mobileSearchOpen, setMobileSearchOpen] = useState(false);\n const [desktopSearchOpen, setDesktopSearchOpen] = useState(false);\n const [showResults, setShowResults] = useState(false);\n const { itemCount, state } = useCart();\n const { favoriteCount } = useFavorites();\n const { isAuthenticated, user, logout } = useAuth();\n const navigate = useNavigate();\n const { t } = useTranslation(\"header-ecommerce\");\n\n const handleLogout = () => {\n logout();\n toast.success(t(\"logoutToastTitle\", \"Goodbye!\"), {\n description: t(\"logoutToastDesc\", \"You have been logged out successfully.\"),\n });\n };\n\n const {\n searchTerm,\n setSearchTerm,\n results: searchResults,\n clearSearch,\n } = useDbSearch();\n\n const handleSearchSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (searchTerm.trim()) {\n navigate(`/products?search=${encodeURIComponent(searchTerm)}`);\n setShowResults(false);\n setDesktopSearchOpen(false);\n clearSearch();\n }\n };\n\n const handleSearchFocus = () => {\n setShowResults(true);\n };\n\n const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setSearchTerm(e.target.value);\n setShowResults(true);\n };\n\n const navigation = [\n { name: t(\"home\"), href: \"/\" },\n { name: t(\"products\"), href: \"/products\" },\n { name: t(\"about\"), href: \"/about\" },\n { name: t(\"contact\"), href: \"/contact\" },\n ];\n\n return (\n <header className=\"sticky top-0 z-50 w-full border-b border-border/20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\n <div className=\"flex h-14 sm:h-16 md:h-20 items-center justify-between gap-2\">\n {/* Logo */}\n <div className=\"flex-shrink-0 min-w-0\">\n <Logo size=\"sm\" className=\"text-base sm:text-xl lg:text-2xl\" />\n </div>\n\n {/* Desktop Navigation - Centered */}\n <nav className=\"hidden lg:flex items-center space-x-12 absolute left-1/2 transform -translate-x-1/2\">\n {navigation.map((item) => (\n <Link\n key={item.name}\n to={item.href}\n className=\"text-base font-medium transition-colors hover:text-primary relative group py-2\"\n >\n {item.name}\n <span className=\"absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full\"></span>\n </Link>\n ))}\n </nav>\n\n {/* Search & Actions - Right Aligned */}\n <div className=\"flex items-center space-x-1 sm:space-x-2 lg:space-x-4 flex-shrink-0\">\n {/* Desktop Search - Modal */}\n <Dialog\n open={desktopSearchOpen}\n onOpenChange={setDesktopSearchOpen}\n >\n <DialogTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"hidden lg:flex h-10 w-10\"\n >\n <Search className=\"h-5 w-5\" />\n </Button>\n </DialogTrigger>\n <DialogContent className=\"sm:max-w-2xl\">\n <DialogHeader>\n <DialogTitle>\n {t(\"searchProducts\", \"Search Products\")}\n </DialogTitle>\n </DialogHeader>\n <div className=\"space-y-4\">\n <form onSubmit={handleSearchSubmit}>\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5\" />\n <Input\n type=\"search\"\n placeholder={t(\n \"searchPlaceholder\",\n \"Search for products...\"\n )}\n value={searchTerm}\n onChange={handleSearchChange}\n className=\"pl-11 h-12 text-base\"\n autoFocus\n />\n </div>\n </form>\n\n {/* Desktop Search Results */}\n {searchTerm.trim() && (\n <div className=\"max-h-[400px] overflow-y-auto rounded-lg border bg-card\">\n {searchResults.length > 0 ? (\n <div className=\"divide-y\">\n <div className=\"px-4 py-3 bg-muted/50\">\n <p className=\"text-sm font-medium text-muted-foreground\">\n {searchResults.length}{\" \"}\n {searchResults.length === 1\n ? \"result\"\n : \"results\"}{\" \"}\n found\n </p>\n </div>\n {searchResults.slice(0, 8).map((product: Product) => (\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\n <Link\n to={`/products/${product.slug}`}\n onClick={() => {\n setDesktopSearchOpen(false);\n clearSearch();\n }}\n className=\"flex items-center gap-4 p-4 hover:bg-muted/50 transition-colors\"\n >\n <img\n src={\n product.images[0] || \"/images/placeholder.png\"\n }\n alt={product.name}\n className=\"w-16 h-16 object-cover rounded flex-shrink-0\"\n />\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-medium text-base line-clamp-1\">\n {product.name}\n </h4>\n <p className=\"text-sm text-muted-foreground capitalize\">\n {product.category}\n </p>\n <p className=\"text-base font-semibold text-primary mt-1\">\n {formatPrice(\n product.price,\n constants.site.currency\n )}\n </p>\n </div>\n </Link>\n </div>\n ))}\n {searchResults.length > 8 && (\n <div className=\"px-4 py-3 bg-muted/30 text-center\">\n <button\n onClick={() => {\n navigate(\n `/products?search=${encodeURIComponent(\n searchTerm\n )}`\n );\n setDesktopSearchOpen(false);\n clearSearch();\n }}\n className=\"text-sm font-medium text-primary hover:underline\"\n >\n {t(\n \"viewAllResults\",\n `View all ${searchResults.length} results`\n )}\n </button>\n </div>\n )}\n </div>\n ) : (\n <div className=\"p-8 text-center\">\n <Search className=\"h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50\" />\n <p className=\"text-base text-muted-foreground\">\n {t(\"noResults\", \"No products found\")}\n </p>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {t(\n \"tryDifferentKeywords\",\n \"Try different keywords\"\n )}\n </p>\n </div>\n )}\n </div>\n )}\n </div>\n </DialogContent>\n </Dialog>\n\n {/* Search - Mobile (Hidden - moved to hamburger menu) */}\n <Dialog open={mobileSearchOpen} onOpenChange={setMobileSearchOpen}>\n <DialogTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className=\"hidden\">\n <Search className=\"h-4 w-4 sm:h-5 sm:w-5\" />\n </Button>\n </DialogTrigger>\n <DialogContent className=\"sm:max-w-md\">\n <DialogHeader>\n <DialogTitle>{t(\"searchProducts\")}</DialogTitle>\n </DialogHeader>\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (searchTerm.trim()) {\n navigate(\n `/products?search=${encodeURIComponent(searchTerm)}`\n );\n setMobileSearchOpen(false);\n clearSearch();\n }\n }}\n className=\"space-y-4\"\n >\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\n <Input\n type=\"search\"\n placeholder={t(\"searchPlaceholder\")}\n value={searchTerm}\n onChange={(e) => setSearchTerm(e.target.value)}\n className=\"pl-10\"\n autoFocus\n />\n </div>\n <div className=\"flex gap-2\">\n <Button type=\"submit\" className=\"flex-1\">\n {t(\"searchButton\", \"Search\")}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => {\n clearSearch();\n setMobileSearchOpen(false);\n }}\n >\n {t(\"cancel\", \"Cancel\")}\n </Button>\n </div>\n </form>\n\n {/* Mobile Search Results */}\n {searchTerm.trim() && (\n <div className=\"mt-4 max-h-64 overflow-y-auto\">\n {searchResults.length > 0 ? (\n <div className=\"space-y-2\">\n <p className=\"text-sm text-muted-foreground mb-2\">\n {searchResults.length} result\n {searchResults.length !== 1 ? \"s\" : \"\"} found\n </p>\n {searchResults.slice(0, 5).map((product: Product) => (\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\n <Link\n to={`/products/${product.slug}`}\n onClick={() => {\n setMobileSearchOpen(false);\n clearSearch();\n }}\n className=\"block p-2 rounded hover:bg-muted/50 transition-colors\"\n >\n <div className=\"flex items-center gap-3\">\n <img\n src={\n product.images[0] || \"/images/placeholder.png\"\n }\n alt={product.name}\n className=\"w-10 h-10 object-cover rounded\"\n />\n <div className=\"flex-1\">\n <h4 className=\"font-medium text-sm\">\n {product.name}\n </h4>\n <p className=\"text-xs text-muted-foreground\">\n {product.category}\n </p>\n <p className=\"text-sm font-medium\">\n {formatPrice(\n product.price,\n constants.site.currency\n )}\n </p>\n </div>\n </div>\n </Link>\n </div>\n ))}\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t(\"noResults\")}\n </p>\n )}\n </div>\n )}\n </DialogContent>\n </Dialog>\n\n {/* Wishlist - Desktop Only */}\n <Link to=\"/favorites\" className=\"hidden lg:block\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"relative h-10 w-10\"\n >\n <Heart className=\"h-5 w-5\" />\n {favoriteCount > 0 && (\n <Badge\n variant=\"destructive\"\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\n >\n {favoriteCount}\n </Badge>\n )}\n </Button>\n </Link>\n\n {/* Cart - Desktop Only (Goes to Cart Page) */}\n <Link to=\"/cart\" className=\"hidden lg:block\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"relative h-10 w-10\"\n >\n <ShoppingCart className=\"h-5 w-5\" />\n {itemCount > 0 && (\n <Badge\n variant=\"destructive\"\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\n >\n {itemCount}\n </Badge>\n )}\n </Button>\n </Link>\n\n {/* Auth - Desktop Only */}\n <div className=\"hidden lg:flex\">\n {isAuthenticated ? (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\n <User className=\"h-5 w-5\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <DropdownMenuLabel className=\"font-normal\">\n <div className=\"flex flex-col space-y-1\">\n <p className=\"text-sm font-medium\">{user?.username}</p>\n {user?.email && (\n <p className=\"text-xs text-muted-foreground\">{user.email}</p>\n )}\n </div>\n </DropdownMenuLabel>\n <DropdownMenuSeparator />\n <DropdownMenuItem asChild className=\"cursor-pointer\">\n <Link to=\"/my-orders\" className=\"flex items-center\">\n <Package className=\"mr-2 h-4 w-4\" />\n {t(\"myOrders\", \"My Orders\")}\n </Link>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n onClick={handleLogout}\n className=\"text-red-600 focus:text-red-600 focus:bg-red-50 cursor-pointer\"\n >\n <LogOut className=\"mr-2 h-4 w-4\" />\n {t(\"logout\", \"Logout\")}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n ) : (\n <Link to=\"/login\">\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\n <User className=\"h-5 w-5\" />\n </Button>\n </Link>\n )}\n </div>\n\n {/* Mobile Menu */}\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\n <SheetTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"lg:hidden h-8 w-8 sm:h-10 sm:w-10\"\n >\n <Menu className=\"h-4 w-4 sm:h-5 sm:w-5\" />\n </Button>\n </SheetTrigger>\n <SheetContent side=\"right\" className=\"w-[300px] sm:w-[400px] px-6\">\n <SheetHeader>\n <SheetTitle>{t(\"menu\")}</SheetTitle>\n </SheetHeader>\n\n {/* Mobile Search in Hamburger */}\n <div className=\"mt-6 pb-4 border-b\">\n <form onSubmit={handleSearchSubmit}>\n <div className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\n <Input\n type=\"search\"\n placeholder={t(\"searchPlaceholder\")}\n value={searchTerm}\n onChange={handleSearchChange}\n onFocus={handleSearchFocus}\n className=\"pl-10 h-11\"\n />\n </div>\n </form>\n\n {/* Search Results in Hamburger */}\n {showResults && searchTerm && (\n <div className=\"mt-3 max-h-[300px] overflow-y-auto rounded-lg border bg-card\">\n {searchResults.length > 0 ? (\n <div className=\"divide-y\">\n <div className=\"px-3 py-2 bg-muted/50\">\n <p className=\"text-xs font-medium text-muted-foreground\">\n {searchResults.length}{\" \"}\n {searchResults.length === 1\n ? \"result\"\n : \"results\"}\n </p>\n </div>\n {searchResults.slice(0, 5).map((product: Product) => (\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\n <Link\n to={`/products/${product.slug}`}\n onClick={() => {\n setMobileMenuOpen(false);\n clearSearch();\n setShowResults(false);\n }}\n className=\"flex items-center gap-3 p-3 hover:bg-muted/50 transition-colors\"\n >\n <img\n src={\n product.images[0] || \"/images/placeholder.png\"\n }\n alt={product.name}\n className=\"w-14 h-14 object-cover rounded flex-shrink-0\"\n />\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"font-medium text-sm line-clamp-1\">\n {product.name}\n </h4>\n <p className=\"text-xs text-muted-foreground capitalize\">\n {product.category}\n </p>\n <p className=\"text-sm font-semibold text-primary mt-1\">\n {formatPrice(\n product.price,\n constants.site.currency\n )}\n </p>\n </div>\n </Link>\n </div>\n ))}\n {searchResults.length > 5 && (\n <div className=\"px-3 py-2 bg-muted/30 text-center\">\n <button\n onClick={() => {\n navigate(\n `/products?search=${encodeURIComponent(\n searchTerm\n )}`\n );\n setMobileMenuOpen(false);\n clearSearch();\n setShowResults(false);\n }}\n className=\"text-xs font-medium text-primary hover:underline\"\n >\n {t(\n \"viewAllResults\",\n `View all ${searchResults.length} results`\n )}\n </button>\n </div>\n )}\n </div>\n ) : (\n <div className=\"p-6 text-center\">\n <Search className=\"h-8 w-8 text-muted-foreground mx-auto mb-2 opacity-50\" />\n <p className=\"text-sm text-muted-foreground\">\n {t(\"noResults\", \"No results found\")}\n </p>\n </div>\n )}\n </div>\n )}\n </div>\n\n <div className=\"flex flex-col space-y-4 mt-6\">\n {navigation.map((item) => (\n <Link\n key={item.name}\n to={item.href}\n className=\"text-lg font-medium hover:text-primary transition-colors\"\n onClick={() => setMobileMenuOpen(false)}\n >\n {item.name}\n </Link>\n ))}\n <div className=\"border-t pt-4 space-y-4\">\n <Link\n to=\"/favorites\"\n className=\"flex items-center justify-between text-lg font-medium hover:text-primary transition-colors\"\n onClick={() => setMobileMenuOpen(false)}\n >\n <div className=\"flex items-center space-x-2\">\n <Heart className=\"h-5 w-5\" />\n <span>{t(\"favorites\")}</span>\n </div>\n <Badge variant=\"secondary\">{favoriteCount}</Badge>\n </Link>\n <Link\n to=\"/cart\"\n className=\"flex items-center justify-between w-full text-lg font-medium hover:text-primary transition-colors\"\n onClick={() => setMobileMenuOpen(false)}\n >\n <div className=\"flex items-center space-x-2\">\n <ShoppingCart className=\"h-5 w-5\" />\n <span>{t(\"cart\")}</span>\n </div>\n <div className=\"flex flex-col items-end\">\n <Badge variant=\"secondary\">{itemCount}</Badge>\n <span className=\"text-xs text-muted-foreground\">\n {formatPrice(state.total, constants.site.currency)}\n </span>\n </div>\n </Link>\n\n {/* Auth - Mobile */}\n {isAuthenticated ? (\n <div className=\"space-y-3\">\n <div className=\"flex items-center space-x-3 p-3 bg-muted/50 rounded-lg\">\n <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center\">\n <User className=\"h-5 w-5 text-primary\" />\n </div>\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">{user?.username}</p>\n {user?.email && (\n <p className=\"text-xs text-muted-foreground truncate\">{user.email}</p>\n )}\n </div>\n </div>\n <Link\n to=\"/my-orders\"\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\n onClick={() => setMobileMenuOpen(false)}\n >\n <Package className=\"h-5 w-5\" />\n <span>{t(\"myOrders\", \"My Orders\")}</span>\n </Link>\n <button\n onClick={() => {\n handleLogout();\n setMobileMenuOpen(false);\n }}\n className=\"flex items-center space-x-2 text-lg font-medium text-red-600 hover:text-red-700 transition-colors w-full\"\n >\n <LogOut className=\"h-5 w-5\" />\n <span>{t(\"logout\", \"Logout\")}</span>\n </button>\n </div>\n ) : (\n <Link\n to=\"/login\"\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\n onClick={() => setMobileMenuOpen(false)}\n >\n <User className=\"h-5 w-5\" />\n <span>{t(\"login\", \"Login\")}</span>\n </Link>\n )}\n </div>\n </div>\n </SheetContent>\n </Sheet>\n </div>\n </div>\n </div>\n {/* Cart Drawer */}\n <CartDrawer showTrigger={false} />\n </header>\n );\n}\n"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "header-ecommerce/lang/en.json",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"path": "order-confirmation-page/order-confirmation-page.tsx",
|
|
25
25
|
"type": "registry:page",
|
|
26
26
|
"target": "$modules$/order-confirmation-page/order-confirmation-page.tsx",
|
|
27
|
-
"content": "import { useEffect, useState } from \"react\";\r\nimport { Link, useParams, useSearchParams } from \"react-router\";\r\nimport {\r\n CheckCircle,\r\n Package,\r\n Truck,\r\n CreditCard,\r\n ArrowLeft,\r\n Clock,\r\n AlertCircle,\r\n Loader2,\r\n ShoppingBag,\r\n} from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { formatPrice } from \"@/modules/ecommerce-core\";\r\nimport { customerClient } from \"@/modules/api\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\ninterface CheckoutData {\r\n items: any[];\r\n total: number;\r\n customerInfo: {\r\n firstName: string;\r\n lastName: string;\r\n email: string;\r\n phone: string;\r\n address: string;\r\n city: string;\r\n postalCode: string;\r\n country: string;\r\n notes?: string;\r\n };\r\n paymentMethod: string;\r\n paymentProvider: string;\r\n}\r\n\r\nexport function OrderConfirmationPage() {\r\n const { t } = useTranslation(\"order-confirmation-page\");\r\n usePageTitle({ title: t(\"title\", \"Order Confirmation\") });\r\n\r\n const { orderId } = useParams<{ orderId: string }>();\r\n const [searchParams] = useSearchParams();\r\n const sessionId = searchParams.get(\"session_id\");\r\n\r\n const currency = (constants as any).site?.currency || \"USD\";\r\n\r\n const [checkoutData, setCheckoutData] = useState<CheckoutData | null>(null);\r\n const [paymentStatus, setPaymentStatus] = useState<string | null>(null);\r\n const [isCheckingStatus, setIsCheckingStatus] = useState(false);\r\n const [statusError, setStatusError] = useState<string | null>(null);\r\n\r\n // Check if this is a Stripe payment\r\n const isStripePayment = orderId?.startsWith(\"stripe-\") || !!sessionId;\r\n\r\n useEffect(() => {\r\n window.scrollTo(0, 0);\r\n\r\n // Load checkout data from localStorage\r\n const savedData = localStorage.getItem(\"pending_checkout\");\r\n if (savedData) {\r\n try {\r\n setCheckoutData(JSON.parse(savedData));\r\n // Clear after loading\r\n localStorage.removeItem(\"pending_checkout\");\r\n } catch (e) {\r\n console.error(\"Failed to parse checkout data:\", e);\r\n }\r\n }\r\n\r\n // If Stripe payment, check status\r\n if (isStripePayment && (sessionId || orderId)) {\r\n checkStripePaymentStatus();\r\n }\r\n }, []);\r\n\r\n const checkStripePaymentStatus = async () => {\r\n const sid = sessionId || orderId?.replace(\"stripe-\", \"\");\r\n if (!sid) return;\r\n\r\n setIsCheckingStatus(true);\r\n setStatusError(null);\r\n\r\n try {\r\n const response = await customerClient.orders.getBySessionId({ sessionId: sid });\r\n setPaymentStatus(response.payment_status === \"paid\" ? \"succeeded\" : response.payment_status);\r\n } catch (error: any) {\r\n console.error(\"Payment status check error:\", error);\r\n setStatusError(error.message || \"An error occurred\");\r\n } finally {\r\n setIsCheckingStatus(false);\r\n }\r\n };\r\n\r\n // No checkout data found\r\n if (!checkoutData) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\r\n <div className=\"max-w-md mx-auto text-center\">\r\n <Card>\r\n <CardContent className=\"pt-8 pb-8\">\r\n <Package className=\"w-16 h-16 text-muted-foreground mx-auto mb-4\" />\r\n <h1 className=\"text-2xl font-bold mb-2\">\r\n {t(\"orderNotFound\", \"Order Not Found\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-6\">\r\n {t(\r\n \"orderNotFoundDescription\",\r\n \"We couldn't find the order details. Please check your email for confirmation.\"\r\n )}\r\n </p>\r\n <Button asChild>\r\n <Link to=\"/products\">\r\n <ShoppingBag className=\"w-4 h-4 mr-2\" />\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n const { items, total, customerInfo, paymentMethod, paymentProvider } = checkoutData;\r\n\r\n const getPaymentMethodDisplay = () => {\r\n if (paymentProvider === \"cash_on_delivery\" || paymentMethod === \"cash\") {\r\n return {\r\n icon: <Truck className=\"w-4 h-4 text-green-600\" />,\r\n label: t(\"cashOnDelivery\", \"Cash on Delivery\"),\r\n };\r\n }\r\n if (paymentProvider === \"bank_transfer\" || paymentMethod === \"transfer\") {\r\n return {\r\n icon: <CreditCard className=\"w-4 h-4 text-primary\" />,\r\n label: t(\"bankTransfer\", \"Bank Transfer\"),\r\n };\r\n }\r\n return {\r\n icon: <CreditCard className=\"w-4 h-4 text-blue-600\" />,\r\n label: t(\"creditCard\", \"Credit Card\"),\r\n };\r\n };\r\n\r\n const paymentDisplay = getPaymentMethodDisplay();\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"max-w-4xl mx-auto\">\r\n {/* Success Header */}\r\n <div className=\"text-center mb-8\">\r\n <div className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-4\">\r\n <CheckCircle className=\"w-8 h-8 text-green-600 dark:text-green-400\" />\r\n </div>\r\n <h1 className=\"text-3xl font-bold text-green-600 dark:text-green-400 mb-2\">\r\n {t(\"title\", \"Order Confirmed!\")}\r\n </h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\r\n \"thankYou\",\r\n \"Thank you for your order! We've received your order and will process it shortly.\"\r\n )}\r\n </p>\r\n </div>\r\n\r\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\r\n {/* Order Details */}\r\n <div className=\"lg:col-span-2 space-y-6\">\r\n {/* Order Info */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <Package className=\"w-5 h-5\" />\r\n {t(\"orderInformation\", \"Order Information\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n {orderId && (\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"orderId\", \"Order ID\")}\r\n </p>\r\n <p className=\"font-semibold font-mono\">#{orderId.slice(0, 12)}</p>\r\n </div>\r\n )}\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"orderDate\", \"Order Date\")}\r\n </p>\r\n <p className=\"font-semibold\">\r\n {new Date().toLocaleDateString()}\r\n </p>\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"totalAmount\", \"Total Amount\")}\r\n </p>\r\n <p className=\"font-semibold text-lg\">\r\n {formatPrice(total, currency)}\r\n </p>\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"paymentMethod\", \"Payment Method\")}\r\n </p>\r\n <div className=\"flex items-center gap-2\">\r\n {paymentDisplay.icon}\r\n <span className=\"font-semibold\">{paymentDisplay.label}</span>\r\n </div>\r\n </div>\r\n {isStripePayment && (\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"paymentStatus\", \"Payment Status\")}\r\n </p>\r\n <div className=\"flex items-center gap-2\">\r\n {isCheckingStatus ? (\r\n <div className=\"flex items-center gap-2 text-muted-foreground\">\r\n <Loader2 className=\"w-4 h-4 animate-spin\" />\r\n <span className=\"text-sm\">{t(\"checking\", \"Checking...\")}</span>\r\n </div>\r\n ) : statusError ? (\r\n <Badge variant=\"destructive\" className=\"flex items-center gap-1\">\r\n <AlertCircle className=\"w-3 h-3\" />\r\n {t(\"statusError\", \"Error\")}\r\n </Badge>\r\n ) : paymentStatus === \"succeeded\" ? (\r\n <Badge className=\"bg-green-100 text-green-800 border-green-200 flex items-center gap-1\">\r\n <CheckCircle className=\"w-3 h-3\" />\r\n {t(\"paymentSucceeded\", \"Paid\")}\r\n </Badge>\r\n ) : paymentStatus === \"processing\" ? (\r\n <Badge className=\"bg-blue-100 text-blue-800 border-blue-200 flex items-center gap-1\">\r\n <Loader2 className=\"w-3 h-3 animate-spin\" />\r\n {t(\"processing\", \"Processing\")}\r\n </Badge>\r\n ) : paymentStatus === \"requires_payment_method\" ? (\r\n <Badge className=\"bg-yellow-100 text-yellow-800 border-yellow-200 flex items-center gap-1\">\r\n <AlertCircle className=\"w-3 h-3\" />\r\n {t(\"requiresPayment\", \"Requires Payment\")}\r\n </Badge>\r\n ) : paymentStatus ? (\r\n <Badge variant=\"secondary\" className=\"flex items-center gap-1\">\r\n <Clock className=\"w-3 h-3\" />\r\n {paymentStatus}\r\n </Badge>\r\n ) : null}\r\n </div>\r\n </div>\r\n )}\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"orderStatus\", \"Order Status\")}\r\n </p>\r\n <Badge className=\"bg-yellow-100 text-yellow-800 border-yellow-200\">\r\n <Clock className=\"w-3 h-3 mr-1\" />\r\n {t(\"pending\", \"Pending\")}\r\n </Badge>\r\n </div>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Delivery Info */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"deliveryInformation\", \"Delivery Information\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"space-y-2\">\r\n <p className=\"font-semibold\">\r\n {customerInfo.firstName} {customerInfo.lastName}\r\n </p>\r\n <p className=\"text-sm text-muted-foreground\">{customerInfo.email}</p>\r\n <p className=\"text-sm text-muted-foreground\">{customerInfo.phone}</p>\r\n <div className=\"mt-3\">\r\n <p className=\"text-sm font-medium text-muted-foreground\">\r\n {t(\"address\", \"Address\")}:\r\n </p>\r\n <p className=\"text-sm\">{customerInfo.address}</p>\r\n <p className=\"text-sm\">\r\n {customerInfo.city} {customerInfo.postalCode}\r\n </p>\r\n </div>\r\n {customerInfo.notes && (\r\n <div className=\"mt-3\">\r\n <p className=\"text-sm font-medium text-muted-foreground\">\r\n {t(\"notes\", \"Notes\")}:\r\n </p>\r\n <p className=\"text-sm\">{customerInfo.notes}</p>\r\n </div>\r\n )}\r\n </div>\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Order Items */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"orderItems\", \"Order Items\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"space-y-4\">\r\n {items.map((item: any, index: number) => (\r\n <div key={item.id || index} className=\"flex gap-4\">\r\n <img\r\n src={item.product?.images?.[0] || \"/images/placeholder.png\"}\r\n alt={item.product?.name || \"Product\"}\r\n className=\"w-16 h-16 object-cover rounded border\"\r\n />\r\n <div className=\"flex-1\">\r\n <h4 className=\"font-medium\">{item.product?.name}</h4>\r\n <div className=\"flex justify-between items-center mt-2\">\r\n <span className=\"text-sm\">\r\n {t(\"qty\", \"Qty\")}: {item.quantity}\r\n </span>\r\n <span className=\"font-semibold\">\r\n {formatPrice(\r\n (item.product?.on_sale\r\n ? item.product?.sale_price\r\n : item.product?.price) * item.quantity,\r\n currency\r\n )}\r\n </span>\r\n </div>\r\n </div>\r\n </div>\r\n ))}\r\n </div>\r\n\r\n <Separator className=\"my-4\" />\r\n\r\n <div className=\"flex justify-between font-semibold text-lg\">\r\n <span>{t(\"total\", \"Total\")}</span>\r\n <span>{formatPrice(total, currency)}</span>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n\r\n {/* Sidebar */}\r\n <div className=\"space-y-6\">\r\n {/* Bank Transfer Instructions */}\r\n {(paymentProvider === \"bank_transfer\" || paymentMethod === \"transfer\") && (\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"text-lg\">\r\n {t(\"paymentInstructions\", \"Payment Instructions\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"p-4 bg-primary/10 border border-primary/20 rounded-lg\">\r\n <p className=\"text-sm font-medium text-primary mb-2\">\r\n {t(\"bankTransferDetails\", \"Bank Transfer Details\")}\r\n </p>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\r\n \"bankDetailsEmail\",\r\n \"Bank details will be sent via email. Please complete the transfer within 48 hours.\"\r\n )}\r\n </p>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n )}\r\n\r\n {/* Cash on Delivery Info */}\r\n {(paymentProvider === \"cash_on_delivery\" || paymentMethod === \"cash\") && (\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"text-lg\">\r\n {t(\"deliveryPayment\", \"Payment on Delivery\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg\">\r\n <p className=\"text-sm font-medium text-green-800 dark:text-green-200 mb-2\">\r\n {t(\"cashOnDeliveryInfo\", \"Cash on Delivery\")}\r\n </p>\r\n <p className=\"text-sm text-green-700 dark:text-green-300\">\r\n {t(\r\n \"codInstructions\",\r\n \"Pay when your order arrives. Our delivery team will contact you before delivery.\"\r\n )}\r\n </p>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n )}\r\n\r\n {/* Actions */}\r\n <Card>\r\n <CardContent className=\"pt-6 space-y-4\">\r\n <Button asChild className=\"w-full\">\r\n <Link to=\"/products\">\r\n <ShoppingBag className=\"w-4 h-4 mr-2\" />\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\r\n\r\n <Button variant=\"outline\" asChild className=\"w-full\">\r\n <Link to=\"/\">\r\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\r\n {t(\"backToHome\", \"Back to Home\")}\r\n </Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default OrderConfirmationPage;\r\n"
|
|
27
|
+
"content": "import { useEffect, useState } from \"react\";\r\nimport { Link, useSearchParams } from \"react-router\";\r\nimport {\r\n CheckCircle,\r\n Package,\r\n Truck,\r\n CreditCard,\r\n ArrowLeft,\r\n Clock,\r\n AlertCircle,\r\n Loader2,\r\n ShoppingBag,\r\n} from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { formatPrice } from \"@/modules/ecommerce-core\";\r\nimport { customerClient } from \"@/modules/api\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\ninterface CheckoutData {\r\n items: any[];\r\n total: number;\r\n customerInfo: {\r\n firstName: string;\r\n lastName: string;\r\n email: string;\r\n phone: string;\r\n address: string;\r\n city: string;\r\n postalCode: string;\r\n country: string;\r\n notes?: string;\r\n };\r\n paymentMethod: string;\r\n paymentProvider: string;\r\n}\r\n\r\ninterface OrderData {\r\n id: string;\r\n payment_status: string;\r\n status: string;\r\n total_amount: number;\r\n currency: string;\r\n payment_method: string;\r\n product_data: {\r\n quantity: number;\r\n name: string;\r\n description: string;\r\n amount: number;\r\n img: string;\r\n optionals: Record<string, unknown> | null;\r\n }[];\r\n created_at: string;\r\n}\r\n\r\nexport function OrderConfirmationPage() {\r\n const { t } = useTranslation(\"order-confirmation-page\");\r\n usePageTitle({ title: t(\"title\", \"Order Confirmation\") });\r\n\r\n const [searchParams] = useSearchParams();\r\n const sessionId = searchParams.get(\"session_id\");\r\n\r\n const currency = (constants as any).site?.currency || \"USD\";\r\n\r\n const [checkoutData, setCheckoutData] = useState<CheckoutData | null>(null);\r\n const [orderData, setOrderData] = useState<OrderData | null>(null);\r\n const [isLoading, setIsLoading] = useState(!!sessionId);\r\n const [statusError, setStatusError] = useState<string | null>(null);\r\n\r\n useEffect(() => {\r\n window.scrollTo(0, 0);\r\n\r\n // Load checkout data from localStorage\r\n const savedData = localStorage.getItem(\"pending_checkout\");\r\n if (savedData) {\r\n try {\r\n setCheckoutData(JSON.parse(savedData));\r\n // Clear after loading\r\n localStorage.removeItem(\"pending_checkout\");\r\n } catch {\r\n // Ignore parse errors\r\n }\r\n }\r\n\r\n // Fetch order data from backend if we have a session ID\r\n if (sessionId) {\r\n fetchOrderData(sessionId);\r\n } else {\r\n setIsLoading(false);\r\n }\r\n }, []);\r\n\r\n const fetchOrderData = async (sid: string) => {\r\n setIsLoading(true);\r\n setStatusError(null);\r\n\r\n try {\r\n const response = await customerClient.orders.getBySessionId({ sessionId: sid });\r\n setOrderData(response as OrderData);\r\n } catch (error: any) {\r\n setStatusError(error.message || \"An error occurred\");\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n // Loading state\r\n if (isLoading) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\r\n <div className=\"max-w-md mx-auto text-center\">\r\n <Card>\r\n <CardContent className=\"pt-8 pb-8\">\r\n <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(\"loading\", \"Loading Order...\")}\r\n </h1>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n // No data available at all\r\n if (!checkoutData && !orderData) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-16\">\r\n <div className=\"max-w-md mx-auto text-center\">\r\n <Card>\r\n <CardContent className=\"pt-8 pb-8\">\r\n <Package className=\"w-16 h-16 text-muted-foreground mx-auto mb-4\" />\r\n <h1 className=\"text-2xl font-bold mb-2\">\r\n {t(\"orderNotFound\", \"Order Not Found\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-6\">\r\n {t(\r\n \"orderNotFoundDescription\",\r\n \"We couldn't find the order details. Please check your email for confirmation.\"\r\n )}\r\n </p>\r\n <Button asChild>\r\n <Link to=\"/products\">\r\n <ShoppingBag className=\"w-4 h-4 mr-2\" />\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n // Derive display values from either backend data or localStorage\r\n const paymentMethod = checkoutData?.paymentMethod || orderData?.payment_method || \"\";\r\n const paymentProvider = checkoutData?.paymentProvider || orderData?.payment_method || \"\";\r\n const total = orderData ? orderData.total_amount / 100 : (checkoutData?.total || 0);\r\n const displayCurrency = orderData?.currency?.toUpperCase() || currency;\r\n const customerInfo = checkoutData?.customerInfo || null;\r\n\r\n // Use backend product data if available, otherwise fall back to localStorage\r\n const items = orderData?.product_data || checkoutData?.items || [];\r\n const isBackendData = !!orderData?.product_data;\r\n\r\n const paymentStatus = orderData?.payment_status || null;\r\n\r\n const getPaymentMethodDisplay = () => {\r\n if (paymentProvider === \"cash_on_delivery\" || paymentMethod === \"cash\") {\r\n return {\r\n icon: <Truck className=\"w-4 h-4 text-green-600\" />,\r\n label: t(\"cashOnDelivery\", \"Cash on Delivery\"),\r\n };\r\n }\r\n if (paymentProvider === \"bank_transfer\" || paymentMethod === \"transfer\") {\r\n return {\r\n icon: <CreditCard className=\"w-4 h-4 text-primary\" />,\r\n label: t(\"bankTransfer\", \"Bank Transfer\"),\r\n };\r\n }\r\n return {\r\n icon: <CreditCard className=\"w-4 h-4 text-blue-600\" />,\r\n label: t(\"creditCard\", \"Credit Card\"),\r\n };\r\n };\r\n\r\n const paymentDisplay = getPaymentMethodDisplay();\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"max-w-4xl mx-auto\">\r\n {/* Success Header */}\r\n <div className=\"text-center mb-8\">\r\n <div className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-4\">\r\n <CheckCircle className=\"w-8 h-8 text-green-600 dark:text-green-400\" />\r\n </div>\r\n <h1 className=\"text-3xl font-bold text-green-600 dark:text-green-400 mb-2\">\r\n {t(\"title\", \"Order Confirmed!\")}\r\n </h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\r\n \"thankYou\",\r\n \"Thank you for your order! We've received your order and will process it shortly.\"\r\n )}\r\n </p>\r\n </div>\r\n\r\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\r\n {/* Order Details */}\r\n <div className=\"lg:col-span-2 space-y-6\">\r\n {/* Order Info */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <Package className=\"w-5 h-5\" />\r\n {t(\"orderInformation\", \"Order Information\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n {orderData?.id && (\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"orderId\", \"Order ID\")}\r\n </p>\r\n <p className=\"font-semibold font-mono\">#{orderData.id.slice(0, 12)}</p>\r\n </div>\r\n )}\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"orderDate\", \"Order Date\")}\r\n </p>\r\n <p className=\"font-semibold\">\r\n {orderData?.created_at\r\n ? new Date(orderData.created_at).toLocaleDateString()\r\n : new Date().toLocaleDateString()}\r\n </p>\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"totalAmount\", \"Total Amount\")}\r\n </p>\r\n <p className=\"font-semibold text-lg\">\r\n {formatPrice(total, displayCurrency)}\r\n </p>\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"paymentMethod\", \"Payment Method\")}\r\n </p>\r\n <div className=\"flex items-center gap-2\">\r\n {paymentDisplay.icon}\r\n <span className=\"font-semibold\">{paymentDisplay.label}</span>\r\n </div>\r\n </div>\r\n {sessionId && (\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"paymentStatus\", \"Payment Status\")}\r\n </p>\r\n <div className=\"flex items-center gap-2\">\r\n {statusError ? (\r\n <Badge variant=\"destructive\" className=\"flex items-center gap-1\">\r\n <AlertCircle className=\"w-3 h-3\" />\r\n {t(\"statusError\", \"Error\")}\r\n </Badge>\r\n ) : paymentStatus === \"paid\" ? (\r\n <Badge className=\"bg-green-100 text-green-800 border-green-200 flex items-center gap-1\">\r\n <CheckCircle className=\"w-3 h-3\" />\r\n {t(\"paymentSucceeded\", \"Paid\")}\r\n </Badge>\r\n ) : paymentStatus === \"processing\" ? (\r\n <Badge className=\"bg-blue-100 text-blue-800 border-blue-200 flex items-center gap-1\">\r\n <Loader2 className=\"w-3 h-3 animate-spin\" />\r\n {t(\"processing\", \"Processing\")}\r\n </Badge>\r\n ) : paymentStatus === \"requires_payment_method\" ? (\r\n <Badge className=\"bg-yellow-100 text-yellow-800 border-yellow-200 flex items-center gap-1\">\r\n <AlertCircle className=\"w-3 h-3\" />\r\n {t(\"requiresPayment\", \"Requires Payment\")}\r\n </Badge>\r\n ) : paymentStatus ? (\r\n <Badge variant=\"secondary\" className=\"flex items-center gap-1\">\r\n <Clock className=\"w-3 h-3\" />\r\n {paymentStatus}\r\n </Badge>\r\n ) : null}\r\n </div>\r\n </div>\r\n )}\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"orderStatus\", \"Order Status\")}\r\n </p>\r\n <Badge className=\"bg-yellow-100 text-yellow-800 border-yellow-200\">\r\n <Clock className=\"w-3 h-3 mr-1\" />\r\n {t(\"pending\", \"Pending\")}\r\n </Badge>\r\n </div>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Delivery Info - only show if we have customer info from localStorage */}\r\n {customerInfo && (\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"deliveryInformation\", \"Delivery Information\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"space-y-2\">\r\n <p className=\"font-semibold\">\r\n {customerInfo.firstName} {customerInfo.lastName}\r\n </p>\r\n <p className=\"text-sm text-muted-foreground\">{customerInfo.email}</p>\r\n <p className=\"text-sm text-muted-foreground\">{customerInfo.phone}</p>\r\n <div className=\"mt-3\">\r\n <p className=\"text-sm font-medium text-muted-foreground\">\r\n {t(\"address\", \"Address\")}:\r\n </p>\r\n <p className=\"text-sm\">{customerInfo.address}</p>\r\n <p className=\"text-sm\">\r\n {customerInfo.city} {customerInfo.postalCode}\r\n </p>\r\n </div>\r\n {customerInfo.notes && (\r\n <div className=\"mt-3\">\r\n <p className=\"text-sm font-medium text-muted-foreground\">\r\n {t(\"notes\", \"Notes\")}:\r\n </p>\r\n <p className=\"text-sm\">{customerInfo.notes}</p>\r\n </div>\r\n )}\r\n </div>\r\n </CardContent>\r\n </Card>\r\n )}\r\n\r\n {/* Order Items */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"orderItems\", \"Order Items\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"space-y-4\">\r\n {items.map((item: any, index: number) => (\r\n <div key={item.id || index} className=\"flex gap-4\">\r\n <img\r\n src={\r\n isBackendData\r\n ? item.img || \"/images/placeholder.png\"\r\n : item.product?.images?.[0] || \"/images/placeholder.png\"\r\n }\r\n alt={isBackendData ? item.name : item.product?.name || \"Product\"}\r\n className=\"w-16 h-16 object-cover rounded border\"\r\n />\r\n <div className=\"flex-1\">\r\n <h4 className=\"font-medium\">\r\n {isBackendData ? item.name : item.product?.name}\r\n </h4>\r\n <div className=\"flex justify-between items-center mt-2\">\r\n <span className=\"text-sm\">\r\n {t(\"qty\", \"Qty\")}: {item.quantity}\r\n </span>\r\n <span className=\"font-semibold\">\r\n {isBackendData\r\n ? formatPrice(item.amount / 100, displayCurrency)\r\n : formatPrice(\r\n (item.product?.on_sale\r\n ? item.product?.sale_price\r\n : item.product?.price) * item.quantity,\r\n displayCurrency\r\n )}\r\n </span>\r\n </div>\r\n </div>\r\n </div>\r\n ))}\r\n </div>\r\n\r\n <Separator className=\"my-4\" />\r\n\r\n <div className=\"flex justify-between font-semibold text-lg\">\r\n <span>{t(\"total\", \"Total\")}</span>\r\n <span>{formatPrice(total, displayCurrency)}</span>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n\r\n {/* Sidebar */}\r\n <div className=\"space-y-6\">\r\n {/* Bank Transfer Instructions */}\r\n {(paymentProvider === \"bank_transfer\" || paymentMethod === \"transfer\") && (\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"text-lg\">\r\n {t(\"paymentInstructions\", \"Payment Instructions\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"p-4 bg-primary/10 border border-primary/20 rounded-lg\">\r\n <p className=\"text-sm font-medium text-primary mb-2\">\r\n {t(\"bankTransferDetails\", \"Bank Transfer Details\")}\r\n </p>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\r\n \"bankDetailsEmail\",\r\n \"Bank details will be sent via email. Please complete the transfer within 48 hours.\"\r\n )}\r\n </p>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n )}\r\n\r\n {/* Cash on Delivery Info */}\r\n {(paymentProvider === \"cash_on_delivery\" || paymentMethod === \"cash\") && (\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"text-lg\">\r\n {t(\"deliveryPayment\", \"Payment on Delivery\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <div className=\"p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg\">\r\n <p className=\"text-sm font-medium text-green-800 dark:text-green-200 mb-2\">\r\n {t(\"cashOnDeliveryInfo\", \"Cash on Delivery\")}\r\n </p>\r\n <p className=\"text-sm text-green-700 dark:text-green-300\">\r\n {t(\r\n \"codInstructions\",\r\n \"Pay when your order arrives. Our delivery team will contact you before delivery.\"\r\n )}\r\n </p>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n )}\r\n\r\n {/* Actions */}\r\n <Card>\r\n <CardContent className=\"pt-6 space-y-4\">\r\n <Button asChild className=\"w-full\">\r\n <Link to=\"/products\">\r\n <ShoppingBag className=\"w-4 h-4 mr-2\" />\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\r\n\r\n <Button variant=\"outline\" asChild className=\"w-full\">\r\n <Link to=\"/\">\r\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\r\n {t(\"backToHome\", \"Back to Home\")}\r\n </Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default OrderConfirmationPage;\r\n"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "order-confirmation-page/lang/en.json",
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"name": "post-detail-page",
|
|
3
3
|
"type": "registry:page",
|
|
4
4
|
"title": "Post Detail Page",
|
|
5
|
-
"description": "Blog post detail page that fetches post data by slug from URL params. Uses
|
|
5
|
+
"description": "Blog post detail page that fetches post data by slug from URL params. Uses useDbPostBySlug hook from blog-core and renders PostDetailBlock. Includes loading skeleton, error handling for not found posts, and automatic page title.",
|
|
6
6
|
"registryDependencies": [
|
|
7
7
|
"blog-core",
|
|
8
8
|
"post-detail-block"
|
|
9
9
|
],
|
|
10
|
-
"usage": "import { PostDetailPage } from '@/modules/post-detail-page';\n\n<Route path=\"/blog/:slug\" element={<PostDetailPage />} />\n\n• Uses
|
|
10
|
+
"usage": "import { PostDetailPage } from '@/modules/post-detail-page';\n\n<Route path=\"/blog/:slug\" element={<PostDetailPage />} />\n\n• Uses useDbPostBySlug() from blog-core\n• Fetches post by slug from URL params\n• Shows loading skeleton while fetching\n• Handles post not found state",
|
|
11
11
|
"route": {
|
|
12
12
|
"path": "/blog/:slug",
|
|
13
13
|
"componentName": "PostDetailPage"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"path": "post-detail-page/post-detail-page.tsx",
|
|
24
24
|
"type": "registry:page",
|
|
25
25
|
"target": "$modules$/post-detail-page/post-detail-page.tsx",
|
|
26
|
-
"content": "import { useParams } from \"react-router\";\r\nimport {
|
|
26
|
+
"content": "import { useParams } from \"react-router\";\r\nimport { useDbPostBySlug } from \"@/modules/blog-core\";\r\nimport { PostDetailBlock } from \"@/modules/post-detail-block\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\n\r\nexport function PostDetailPage() {\r\n const { t } = useTranslation(\"post-detail-page\");\r\n const { slug } = useParams<{ slug: string }>();\r\n const { post, loading, error } = useDbPostBySlug(slug || \"\");\r\n\r\n usePageTitle({ title: post?.title || t(\"loading\", \"Loading...\") });\r\n\r\n if (loading) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"animate-pulse space-y-4\">\r\n <div className=\"h-8 bg-muted rounded w-1/3\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/4\"></div>\r\n <div className=\"h-64 bg-muted rounded\"></div>\r\n <div className=\"space-y-2\">\r\n <div className=\"h-4 bg-muted rounded\"></div>\r\n <div className=\"h-4 bg-muted rounded\"></div>\r\n <div className=\"h-4 bg-muted rounded w-3/4\"></div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n if (error || !post) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8 text-center\">\r\n <h1 className=\"text-2xl font-bold mb-4\">{t(\"notFound\", \"Post Not Found\")}</h1>\r\n <p className=\"text-muted-foreground\">{t(\"notFoundDescription\", \"The post you're looking for doesn't exist or has been removed.\")}</p>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <PostDetailBlock post={post} />\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default PostDetailPage;\r\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "post-detail-page/lang/en.json",
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"name": "product-detail-page",
|
|
3
3
|
"type": "registry:page",
|
|
4
4
|
"title": "Product Detail Page",
|
|
5
|
-
"description": "Product detail page that fetches product data by slug from URL params. Uses
|
|
5
|
+
"description": "Product detail page that fetches product data by slug from URL params. Uses useDbProductBySlug hook from ecommerce-core and renders ProductDetailBlock. Includes loading skeleton, error handling for not found products, and automatic page title.",
|
|
6
6
|
"registryDependencies": [
|
|
7
7
|
"ecommerce-core",
|
|
8
8
|
"product-detail-block"
|
|
9
9
|
],
|
|
10
|
-
"usage": "import { ProductDetailPage } from '@/modules/product-detail-page';\n\n<Route path=\"/products/:slug\" element={<ProductDetailPage />} />\n\n• Uses
|
|
10
|
+
"usage": "import { ProductDetailPage } from '@/modules/product-detail-page';\n\n<Route path=\"/products/:slug\" element={<ProductDetailPage />} />\n\n• Uses useDbProductBySlug() from ecommerce-core\n• Fetches product by slug from URL params\n• Shows loading skeleton while fetching\n• Handles product not found state",
|
|
11
11
|
"route": {
|
|
12
12
|
"path": "/products/:slug",
|
|
13
13
|
"componentName": "ProductDetailPage"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"path": "product-detail-page/product-detail-page.tsx",
|
|
24
24
|
"type": "registry:page",
|
|
25
25
|
"target": "$modules$/product-detail-page/product-detail-page.tsx",
|
|
26
|
-
"content": "import { useParams } from \"react-router\";\r\nimport {
|
|
26
|
+
"content": "import { useParams } from \"react-router\";\r\nimport { useDbProductBySlug } from \"@/modules/ecommerce-core\";\r\nimport { ProductDetailBlock } from \"@/modules/product-detail-block\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\n\r\nexport function ProductDetailPage() {\r\n const { t } = useTranslation(\"product-detail-page\");\r\n const { slug } = useParams<{ slug: string }>();\r\n const { product, loading, error } = useDbProductBySlug(slug || \"\");\r\n\r\n usePageTitle({ title: product?.name || t(\"loading\", \"Loading...\") });\r\n\r\n if (loading) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"animate-pulse\">\r\n <div className=\"grid lg:grid-cols-2 gap-12\">\r\n <div className=\"aspect-square bg-muted rounded-lg\"></div>\r\n <div className=\"space-y-4\">\r\n <div className=\"h-6 bg-muted rounded w-1/4\"></div>\r\n <div className=\"h-10 bg-muted rounded w-3/4\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/3\"></div>\r\n <div className=\"h-8 bg-muted rounded w-1/4\"></div>\r\n <div className=\"space-y-2\">\r\n <div className=\"h-4 bg-muted rounded\"></div>\r\n <div className=\"h-4 bg-muted rounded\"></div>\r\n <div className=\"h-4 bg-muted rounded w-2/3\"></div>\r\n </div>\r\n <div className=\"h-12 bg-muted rounded w-1/2\"></div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n if (error || !product) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8 text-center\">\r\n <h1 className=\"text-2xl font-bold mb-4\">{t(\"notFound\", \"Product Not Found\")}</h1>\r\n <p className=\"text-muted-foreground\">{t(\"notFoundDescription\", \"The product you're looking for doesn't exist or has been removed.\")}</p>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <ProductDetailBlock product={product} />\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ProductDetailPage;\r\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "product-detail-page/lang/en.json",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"path": "/products",
|
|
13
13
|
"componentName": "ProductsPage"
|
|
14
14
|
},
|
|
15
|
-
"usage": "import ProductsPage from '@/modules/products-page';\n\n<Route path=\"/products\" element={<ProductsPage />} />\n\n• Installed at: src/modules/products-page/\n• Add link: <Link to=\"/products\">Browse Products</Link>\n• Supports filters, sorting, grid/list view, pagination\n• Uses
|
|
15
|
+
"usage": "import ProductsPage from '@/modules/products-page';\n\n<Route path=\"/products\" element={<ProductsPage />} />\n\n• Installed at: src/modules/products-page/\n• Add link: <Link to=\"/products\">Browse Products</Link>\n• Supports filters, sorting, grid/list view, pagination\n• Uses useDbProducts hook for data fetching",
|
|
16
16
|
"files": [
|
|
17
17
|
{
|
|
18
18
|
"path": "products-page/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, 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 // 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 } = 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 <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\";\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"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "products-page/lang/en.json",
|