@promakeai/cli 0.4.0 → 0.4.2
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 +19 -17
- 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/db.json +6 -6
- package/dist/registry/docs/blog-core.md +11 -6
- package/dist/registry/docs/blog-list-page.md +1 -1
- package/dist/registry/docs/ecommerce-core.md +11 -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 +18 -16
- 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
- package/template/src/lib/api.ts +2 -3
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
"name": "ecommerce-core",
|
|
3
3
|
"type": "registry:module",
|
|
4
4
|
"title": "E-commerce Core",
|
|
5
|
-
"description": "Complete e-commerce state management with Zustand. Includes useCartStore for shopping cart operations (add/remove/update items, totals), useFavoritesStore for wishlist,
|
|
5
|
+
"description": "Complete e-commerce state management with Zustand. Includes useCartStore for shopping cart operations (add/remove/update items, totals), useFavoritesStore for wishlist, useDbProducts hook for product fetching with filtering/sorting/pagination, and useDbSearch hook. No provider wrapping needed.",
|
|
6
6
|
"dependencies": [
|
|
7
7
|
"zustand"
|
|
8
8
|
],
|
|
9
|
-
"registryDependencies": [
|
|
10
|
-
|
|
9
|
+
"registryDependencies": [
|
|
10
|
+
"db"
|
|
11
|
+
],
|
|
12
|
+
"usage": "import { useCart, useFavorites, useDbProducts } from '@/modules/ecommerce-core';\n\n// No provider needed - just use the hooks:\nconst { addItem, removeItem, state, itemCount } = useCart();\nconst { addToFavorites, isFavorite } = useFavorites();\nconst { products, loading } = useDbProducts();\n\n// Or use stores directly with selectors:\nconst itemCount = useCartStore((s) => s.itemCount);",
|
|
11
13
|
"files": [
|
|
12
14
|
{
|
|
13
15
|
"path": "ecommerce-core/index.ts",
|
|
14
16
|
"type": "registry:index",
|
|
15
17
|
"target": "$modules$/ecommerce-core/index.ts",
|
|
16
|
-
"content": "// Types\r\nexport * from './types';\r\n\r\n// Stores (Zustand)\r\nexport { useCartStore, useCart } from './stores/cart-store';\r\nexport { useFavoritesStore, useFavorites } from './stores/favorites-store';\r\n\r\n// Hooks\r\nexport {
|
|
18
|
+
"content": "// Types\r\nexport * from './types';\r\n\r\n// Stores (Zustand)\r\nexport { useCartStore, useCart } from './stores/cart-store';\r\nexport { useFavoritesStore, useFavorites } from './stores/favorites-store';\r\n\r\n// Hooks\r\nexport { useDbProducts, useDbProductBySlug, useDbFeaturedProducts, useDbCategories } from './useDbProducts';\r\nexport { useDbSearch } from './useDbSearch';\r\n\r\n// Utilities\r\nexport { formatPrice } from './format-price';\r\n\r\n// Payment Config\r\nexport {\r\n type PaymentMethod,\r\n type OnlinePaymentProvider,\r\n type PaymentMethodConfig,\r\n PAYMENT_METHOD_CONFIGS,\r\n ONLINE_PROVIDER_CONFIGS,\r\n getAvailablePaymentMethods,\r\n getOnlinePaymentProviders,\r\n getFilteredPaymentMethodConfigs,\r\n isPaymentMethodAvailable,\r\n isOnlineProviderAvailable,\r\n} from './payment-config';\r\n"
|
|
17
19
|
},
|
|
18
20
|
{
|
|
19
21
|
"path": "ecommerce-core/types.ts",
|
|
@@ -34,16 +36,16 @@
|
|
|
34
36
|
"content": "import { create } from \"zustand\";\r\nimport { persist } from \"zustand/middleware\";\r\nimport type { Product, FavoritesContextType } from \"../types\";\r\n\r\ninterface FavoritesStore {\r\n favorites: Product[];\r\n favoriteCount: number;\r\n addToFavorites: (product: Product) => void;\r\n removeFromFavorites: (productId: string | number) => void;\r\n isFavorite: (productId: string | number) => boolean;\r\n clearFavorites: () => void;\r\n}\r\n\r\nexport const useFavoritesStore = create<FavoritesStore>()(\r\n persist(\r\n (set, get) => ({\r\n favorites: [],\r\n favoriteCount: 0,\r\n\r\n addToFavorites: (product) =>\r\n set((state) => {\r\n if (state.favorites.some((fav) => fav.id === product.id)) {\r\n return state;\r\n }\r\n const favorites = [...state.favorites, product];\r\n return { favorites, favoriteCount: favorites.length };\r\n }),\r\n\r\n removeFromFavorites: (productId) =>\r\n set((state) => {\r\n const favorites = state.favorites.filter((fav) => fav.id !== productId);\r\n return { favorites, favoriteCount: favorites.length };\r\n }),\r\n\r\n isFavorite: (productId) => {\r\n return get().favorites.some((fav) => fav.id === productId);\r\n },\r\n\r\n clearFavorites: () => set({ favorites: [], favoriteCount: 0 }),\r\n }),\r\n { name: \"ecommerce_favorites\" }\r\n )\r\n);\r\n\r\n// Backward compatible hook - matches FavoritesContextType\r\nexport const useFavorites = (): FavoritesContextType => {\r\n const store = useFavoritesStore();\r\n return {\r\n favorites: store.favorites,\r\n addToFavorites: store.addToFavorites,\r\n removeFromFavorites: store.removeFromFavorites,\r\n isFavorite: store.isFavorite,\r\n favoriteCount: store.favoriteCount,\r\n clearFavorites: store.clearFavorites,\r\n };\r\n};\r\n"
|
|
35
37
|
},
|
|
36
38
|
{
|
|
37
|
-
"path": "ecommerce-core/
|
|
39
|
+
"path": "ecommerce-core/useDbProducts.ts",
|
|
38
40
|
"type": "registry:hook",
|
|
39
|
-
"target": "$modules$/ecommerce-core/
|
|
40
|
-
"content": "import { useMemo } from 'react';\r\nimport type { Product, Category, ProductCategory } from './types';\r\nimport {\r\n useRepositoryQuery,\r\n useRawQuery,\r\n useRawQueryOne,\r\n parseStringToArray,\r\n parseJSONString,\r\n parseSQLiteBoolean,\r\n parseNumberSafe\r\n} from '@/modules/db';\r\n\r\nconst transformProduct = (row: any): Product => {\r\n const categoryNames = row.category_names ? row.category_names.split(',') : [];\r\n const categorySlugs = row.category_slugs ? row.category_slugs.split(',') : [];\r\n const categoryIds = row.category_ids ? row.category_ids.split(',').map(Number) : [];\r\n\r\n const categories: ProductCategory[] = categoryIds.map((id: number, index: number) => ({\r\n id,\r\n name: categoryNames[index] || '',\r\n slug: categorySlugs[index] || '',\r\n is_primary: index === 0\r\n }));\r\n\r\n const primaryCategory = categories.length > 0 ? categories[0] : null;\r\n\r\n return {\r\n id: parseNumberSafe(row.id),\r\n name: String(row.name || ''),\r\n slug: String(row.slug || ''),\r\n description: row.description || '',\r\n price: parseNumberSafe(row.price),\r\n sale_price: row.sale_price ? parseNumberSafe(row.sale_price) : undefined,\r\n on_sale: parseSQLiteBoolean(row.on_sale),\r\n images: parseStringToArray(row.images),\r\n brand: row.brand || '',\r\n sku: row.sku || '',\r\n stock: parseNumberSafe(row.stock),\r\n tags: parseJSONString(row.tags, []) || [],\r\n rating: parseNumberSafe(row.rating),\r\n review_count: parseNumberSafe(row.review_count),\r\n featured: parseSQLiteBoolean(row.featured),\r\n is_new: parseSQLiteBoolean(row.is_new),\r\n published: parseSQLiteBoolean(row.published),\r\n specifications: parseJSONString(row.specifications, {}) || {},\r\n variants: parseJSONString(row.variants, []) || [],\r\n created_at: row.created_at || new Date().toISOString(),\r\n updated_at: row.updated_at || new Date().toISOString(),\r\n meta_description: row.meta_description || '',\r\n meta_keywords: row.meta_keywords || '',\r\n category: primaryCategory?.slug || '',\r\n category_name: primaryCategory?.name || '',\r\n categories\r\n };\r\n};\r\n\r\nconst PRODUCTS_WITH_CATEGORIES_SQL = `\r\n SELECT p.*,\r\n GROUP_CONCAT(c.name) as category_names,\r\n GROUP_CONCAT(c.slug) as category_slugs,\r\n GROUP_CONCAT(c.id) as category_ids\r\n FROM products p\r\n LEFT JOIN product_category_relations pcr ON p.id = pcr.product_id\r\n LEFT JOIN product_categories c ON pcr.category_id = c.id\r\n WHERE p.published = 1\r\n GROUP BY p.id\r\n`;\r\n\r\nexport function
|
|
41
|
+
"target": "$modules$/ecommerce-core/useDbProducts.ts",
|
|
42
|
+
"content": "import { useMemo } from 'react';\r\nimport type { Product, Category, ProductCategory } from './types';\r\nimport {\r\n useRepositoryQuery,\r\n useRawQuery,\r\n useRawQueryOne,\r\n parseStringToArray,\r\n parseJSONString,\r\n parseSQLiteBoolean,\r\n parseNumberSafe\r\n} from '@/modules/db';\r\n\r\nconst transformProduct = (row: any): Product => {\r\n const categoryNames = row.category_names ? row.category_names.split(',') : [];\r\n const categorySlugs = row.category_slugs ? row.category_slugs.split(',') : [];\r\n const categoryIds = row.category_ids ? row.category_ids.split(',').map(Number) : [];\r\n\r\n const categories: ProductCategory[] = categoryIds.map((id: number, index: number) => ({\r\n id,\r\n name: categoryNames[index] || '',\r\n slug: categorySlugs[index] || '',\r\n is_primary: index === 0\r\n }));\r\n\r\n const primaryCategory = categories.length > 0 ? categories[0] : null;\r\n\r\n return {\r\n id: parseNumberSafe(row.id),\r\n name: String(row.name || ''),\r\n slug: String(row.slug || ''),\r\n description: row.description || '',\r\n price: parseNumberSafe(row.price),\r\n sale_price: row.sale_price ? parseNumberSafe(row.sale_price) : undefined,\r\n on_sale: parseSQLiteBoolean(row.on_sale),\r\n images: parseStringToArray(row.images),\r\n brand: row.brand || '',\r\n sku: row.sku || '',\r\n stock: parseNumberSafe(row.stock),\r\n tags: parseJSONString(row.tags, []) || [],\r\n rating: parseNumberSafe(row.rating),\r\n review_count: parseNumberSafe(row.review_count),\r\n featured: parseSQLiteBoolean(row.featured),\r\n is_new: parseSQLiteBoolean(row.is_new),\r\n published: parseSQLiteBoolean(row.published),\r\n specifications: parseJSONString(row.specifications, {}) || {},\r\n variants: parseJSONString(row.variants, []) || [],\r\n created_at: row.created_at || new Date().toISOString(),\r\n updated_at: row.updated_at || new Date().toISOString(),\r\n meta_description: row.meta_description || '',\r\n meta_keywords: row.meta_keywords || '',\r\n category: primaryCategory?.slug || '',\r\n category_name: primaryCategory?.name || '',\r\n categories\r\n };\r\n};\r\n\r\nconst PRODUCTS_WITH_CATEGORIES_SQL = `\r\n SELECT p.*,\r\n GROUP_CONCAT(c.name) as category_names,\r\n GROUP_CONCAT(c.slug) as category_slugs,\r\n GROUP_CONCAT(c.id) as category_ids\r\n FROM products p\r\n LEFT JOIN product_category_relations pcr ON p.id = pcr.product_id\r\n LEFT JOIN product_categories c ON pcr.category_id = c.id\r\n WHERE p.published = 1\r\n GROUP BY p.id\r\n`;\r\n\r\nexport function useDbCategories() {\r\n const { data, isLoading: loading, error } = useRepositoryQuery<Category>('product_categories', {\r\n orderBy: [{ field: 'name', direction: 'ASC' }]\r\n });\r\n\r\n return {\r\n categories: data ?? [],\r\n loading,\r\n error: error?.message ?? null\r\n };\r\n}\r\n\r\nexport function useDbProducts() {\r\n const sql = `${PRODUCTS_WITH_CATEGORIES_SQL} ORDER BY p.name`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['products', 'all'],\r\n sql\r\n );\r\n\r\n const products = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformProduct);\r\n }, [data]);\r\n\r\n return { products, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbProductBySlug(slug: string) {\r\n const sql = `\r\n SELECT p.*,\r\n GROUP_CONCAT(c.name) as category_names,\r\n GROUP_CONCAT(c.slug) as category_slugs,\r\n GROUP_CONCAT(c.id) as category_ids\r\n FROM products p\r\n LEFT JOIN product_category_relations pcr ON p.id = pcr.product_id\r\n LEFT JOIN product_categories c ON pcr.category_id = c.id\r\n WHERE p.slug = ? AND p.published = 1\r\n GROUP BY p.id\r\n `;\r\n\r\n const { data, isLoading: loading, error } = useRawQueryOne<any>(\r\n ['products', 'slug', slug],\r\n sql,\r\n [slug],\r\n { enabled: !!slug }\r\n );\r\n\r\n const product = useMemo(() => {\r\n if (!data) return null;\r\n return transformProduct(data);\r\n }, [data]);\r\n\r\n return {\r\n product,\r\n loading,\r\n error: !data && !loading && slug ? 'Product not found' : (error?.message ?? null)\r\n };\r\n}\r\n\r\nexport function useDbFeaturedProducts() {\r\n const sql = `\r\n SELECT p.*,\r\n GROUP_CONCAT(c.name) as category_names,\r\n GROUP_CONCAT(c.slug) as category_slugs,\r\n GROUP_CONCAT(c.id) as category_ids\r\n FROM products p\r\n LEFT JOIN product_category_relations pcr ON p.id = pcr.product_id\r\n LEFT JOIN product_categories c ON pcr.category_id = c.id\r\n WHERE p.published = 1 AND p.featured = 1\r\n GROUP BY p.id\r\n ORDER BY p.created_at DESC LIMIT 8\r\n `;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['products', 'featured'],\r\n sql\r\n );\r\n\r\n const products = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformProduct);\r\n }, [data]);\r\n\r\n return { products, loading, error: error?.message ?? null };\r\n}\r\n"
|
|
41
43
|
},
|
|
42
44
|
{
|
|
43
|
-
"path": "ecommerce-core/
|
|
45
|
+
"path": "ecommerce-core/useDbSearch.ts",
|
|
44
46
|
"type": "registry:hook",
|
|
45
|
-
"target": "$modules$/ecommerce-core/
|
|
46
|
-
"content": "import { useState, useEffect, useCallback } from 'react';\r\nimport type { Product } from './types';\r\nimport {
|
|
47
|
+
"target": "$modules$/ecommerce-core/useDbSearch.ts",
|
|
48
|
+
"content": "import { useState, useEffect, useCallback } from 'react';\r\nimport type { Product } from './types';\r\nimport { useDbProducts } from './useDbProducts';\r\n\r\nexport const useDbSearch = () => {\r\n const [searchTerm, setSearchTerm] = useState('');\r\n const [results, setResults] = useState<Product[]>([]);\r\n const [isSearching, setIsSearching] = useState(false);\r\n\r\n // Load all products via useDbProducts hook\r\n const { products: allProducts } = useDbProducts();\r\n\r\n // Perform search when searchTerm changes\r\n useEffect(() => {\r\n if (!searchTerm.trim()) {\r\n setResults([]);\r\n setIsSearching(false);\r\n return;\r\n }\r\n\r\n setIsSearching(true);\r\n\r\n const searchTimeout = setTimeout(() => {\r\n const filtered = allProducts.filter(product => {\r\n const term = searchTerm.toLowerCase();\r\n \r\n // Search in product name\r\n if (product.name.toLowerCase().includes(term)) return true;\r\n \r\n // Search in description\r\n if (product.description.toLowerCase().includes(term)) return true;\r\n \r\n // Search in category\r\n if (product.category_name?.toLowerCase().includes(term)) return true;\r\n \r\n // Search in brand\r\n if (product.brand?.toLowerCase().includes(term)) return true;\r\n \r\n // Search in tags\r\n if (product.tags.some(tag => tag.toLowerCase().includes(term))) return true;\r\n \r\n return false;\r\n });\r\n\r\n setResults(filtered);\r\n setIsSearching(false);\r\n }, 300); // Debounce search\r\n\r\n return () => clearTimeout(searchTimeout);\r\n }, [searchTerm, allProducts]);\r\n\r\n const clearSearch = useCallback(() => {\r\n setSearchTerm('');\r\n setResults([]);\r\n setIsSearching(false);\r\n }, []);\r\n\r\n // search function that takes a term and sets the searchTerm\r\n const search = useCallback((term: string) => {\r\n setSearchTerm(term);\r\n }, []);\r\n\r\n return {\r\n searchTerm,\r\n setSearchTerm,\r\n results,\r\n isSearching,\r\n clearSearch,\r\n clearResults: clearSearch, // alias for header-ecommerce compatibility\r\n search, // function to trigger search\r\n hasResults: results.length > 0\r\n };\r\n};\r\n"
|
|
47
49
|
},
|
|
48
50
|
{
|
|
49
51
|
"path": "ecommerce-core/format-price.ts",
|
|
@@ -99,13 +101,13 @@
|
|
|
99
101
|
"isPaymentMethodAvailable",
|
|
100
102
|
"useCart",
|
|
101
103
|
"useCartStore",
|
|
102
|
-
"
|
|
104
|
+
"useDbCategories",
|
|
105
|
+
"useDbFeaturedProducts",
|
|
106
|
+
"useDbProductBySlug",
|
|
107
|
+
"useDbProducts",
|
|
108
|
+
"useDbSearch",
|
|
103
109
|
"useFavorites",
|
|
104
|
-
"useFavoritesStore"
|
|
105
|
-
"useFeaturedProducts",
|
|
106
|
-
"useProductBySlug",
|
|
107
|
-
"useProducts",
|
|
108
|
-
"useSearch"
|
|
110
|
+
"useFavoritesStore"
|
|
109
111
|
]
|
|
110
112
|
}
|
|
111
113
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"ecommerce-core",
|
|
8
8
|
"product-card"
|
|
9
9
|
],
|
|
10
|
-
"usage": "import { FeaturedProducts } from '@/modules/featured-products';\n\n<FeaturedProducts />\n\n• Installed at: src/modules/featured-products/\n• Customize content: src/modules/featured-products/lang/*.json\n• Products auto-loaded via
|
|
10
|
+
"usage": "import { FeaturedProducts } from '@/modules/featured-products';\n\n<FeaturedProducts />\n\n• Installed at: src/modules/featured-products/\n• Customize content: src/modules/featured-products/lang/*.json\n• Products auto-loaded via useDbProducts hook",
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "featured-products/index.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"path": "featured-products/featured-products.tsx",
|
|
20
20
|
"type": "registry:component",
|
|
21
21
|
"target": "$modules$/featured-products/featured-products.tsx",
|
|
22
|
-
"content": "import { Link } from \"react-router\";\nimport { ArrowRight } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useTranslation } from \"react-i18next\";\nimport {
|
|
22
|
+
"content": "import { Link } from \"react-router\";\nimport { ArrowRight } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDbFeaturedProducts } from \"@/modules/ecommerce-core\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\n\ninterface FeaturedProductsProps {\n products?: Product[];\n loading?: boolean;\n}\n\nexport function FeaturedProducts({\n products: propProducts,\n loading: propLoading,\n}: FeaturedProductsProps) {\n const { t } = useTranslation(\"featured-products\");\n const { products: hookProducts, loading: hookLoading } = useDbFeaturedProducts();\n\n const products = propProducts ?? hookProducts;\n const loading = propLoading ?? hookLoading;\n\n return (\n <section className=\"py-8 sm:py-12 md:py-16 lg:py-20 bg-background border-t border-border/20 relative\">\n <div className=\"absolute top-0 left-1/2 transform -translate-x-1/2 w-16 sm:w-24 h-px bg-primary/30\"></div>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\n <div className=\"text-center mb-6 sm:mb-8 md:mb-12 lg:mb-16 px-2\">\n <h2 className=\"text-xl sm:text-2xl md:text-3xl lg:text-4xl xl:text-5xl font-bold mb-2 sm:mb-3 md:mb-4 bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent leading-normal pb-1\">\n {t('title', 'Featured Products')}\n </h2>\n <div className=\"w-12 sm:w-16 md:w-20 h-1 bg-gradient-to-r from-primary/50 to-primary/20 mx-auto mb-3 sm:mb-4 md:mb-6 rounded-full\"></div>\n <p className=\"text-xs sm:text-sm md:text-base lg:text-lg xl:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed\">\n {t('subtitle', 'Hand-picked favorites from our collection')}\n </p>\n </div>\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-8 xl:gap-10\">\n {loading ? (\n [...Array(3)].map((_, i) => (\n <div key={i} className=\"animate-pulse group\">\n <div className=\"aspect-square bg-gradient-to-br from-muted to-muted/50 rounded-2xl mb-6\"></div>\n <div className=\"space-y-3\">\n <div className=\"h-6 bg-muted rounded-lg w-3/4\"></div>\n <div className=\"h-4 bg-muted rounded w-1/2\"></div>\n <div className=\"h-5 bg-muted rounded w-2/3\"></div>\n </div>\n </div>\n ))\n ) : (\n products.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=\"featured\"\n />\n </div>\n ))\n )}\n </div>\n\n <div className=\"text-center mt-8 sm:mt-12 lg:mt-16\">\n <Button size=\"lg\" asChild className=\"px-6 sm:px-8 py-3 sm:py-4 text-base sm:text-lg\">\n <Link to=\"/products\">\n {t('viewAll', 'View All Products')}\n <ArrowRight className=\"w-4 h-4 sm:w-5 sm:h-5 ml-2\" />\n </Link>\n </Button>\n </div>\n </div>\n </section>\n );\n}\n"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "featured-products/lang/en.json",
|
|
@@ -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",
|