@promakeai/cli 0.4.5 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,7 +33,7 @@
33
33
  "path": "blog-core/useDbPosts.ts",
34
34
  "type": "registry:hook",
35
35
  "target": "$modules$/blog-core/useDbPosts.ts",
36
- "content": "import { useMemo } from 'react';\nimport type { Post, BlogCategory, PostCategory } from './types';\nimport {\n useRepositoryQuery,\n useRawQuery,\n useRawQueryOne,\n parseStringToArray,\n parseSQLiteBoolean,\n parseNumberSafe\n} from '@/modules/db';\n\nconst transformPost = (row: any): Post => {\n const categoryNames = row.category_names ? row.category_names.split(',') : [];\n const categorySlugs = row.category_slugs ? row.category_slugs.split(',') : [];\n const categoryIds = row.category_ids ? row.category_ids.split(',').map(Number) : [];\n\n const categories: PostCategory[] = categoryIds.map((id: number, index: number) => ({\n id,\n name: categoryNames[index] || '',\n slug: categorySlugs[index] || '',\n is_primary: index === 0\n }));\n\n const primaryCategory = categories.length > 0 ? categories[0] : null;\n\n return {\n id: parseNumberSafe(row.id),\n title: String(row.title || ''),\n slug: String(row.slug || ''),\n excerpt: row.excerpt || '',\n content: row.content || '',\n author: row.author || '',\n author_avatar: row.author_avatar || '',\n published_at: row.published_at || new Date().toISOString(),\n updated_at: row.updated_at || new Date().toISOString(),\n featured_image: row.featured_image || '',\n images: parseStringToArray(row.images),\n tags: parseStringToArray(row.tags),\n read_time: parseNumberSafe(row.read_time),\n view_count: parseNumberSafe(row.view_count),\n featured: parseSQLiteBoolean(row.featured),\n published: parseSQLiteBoolean(row.published),\n meta_description: row.meta_description || '',\n meta_keywords: row.meta_keywords || '',\n category: primaryCategory?.slug || '',\n category_name: primaryCategory?.name || '',\n categories\n };\n};\n\nconst POSTS_WITH_CATEGORIES_SQL = `\n SELECT p.*,\n GROUP_CONCAT(c.name) as category_names,\n GROUP_CONCAT(c.slug) as category_slugs,\n GROUP_CONCAT(c.id) as category_ids\n FROM posts p\n LEFT JOIN post_categories pc ON p.id = pc.post_id\n LEFT JOIN blog_categories c ON pc.category_id = c.id\n WHERE p.published = 1\n GROUP BY p.id\n`;\n\nexport function useDbPosts() {\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.published_at DESC`;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'all'],\n sql\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbFeaturedPosts() {\n const sql = `\n SELECT p.*,\n GROUP_CONCAT(c.name) as category_names,\n GROUP_CONCAT(c.slug) as category_slugs,\n GROUP_CONCAT(c.id) as category_ids\n FROM posts p\n LEFT JOIN post_categories pc ON p.id = pc.post_id\n LEFT JOIN blog_categories c ON pc.category_id = c.id\n WHERE p.published = 1 AND p.featured = 1\n GROUP BY p.id\n ORDER BY p.published_at DESC\n `;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'featured'],\n sql\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbRecentPosts(limit: number = 10) {\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.published_at DESC LIMIT ?`;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'recent', limit],\n sql,\n [limit]\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbPopularPosts(limit: number = 10) {\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.view_count DESC LIMIT ?`;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'popular', limit],\n sql,\n [limit]\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbPostsByCategory(categorySlug: string) {\n const sql = `\n SELECT p.*,\n GROUP_CONCAT(c.name) as category_names,\n GROUP_CONCAT(c.slug) as category_slugs,\n GROUP_CONCAT(c.id) as category_ids\n FROM posts p\n LEFT JOIN post_categories pc ON p.id = pc.post_id\n LEFT JOIN blog_categories c ON pc.category_id = c.id\n WHERE p.published = 1 AND p.id IN (\n SELECT pc2.post_id FROM post_categories pc2\n JOIN blog_categories c2 ON pc2.category_id = c2.id\n WHERE c2.slug = ?\n )\n GROUP BY p.id\n ORDER BY p.published_at DESC\n `;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'category', categorySlug],\n sql,\n [categorySlug],\n { enabled: !!categorySlug }\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbPostsByTag(tag: string) {\n const sql = `${POSTS_WITH_CATEGORIES_SQL} AND (p.tags LIKE ? OR p.tags LIKE ? OR p.tags LIKE ? OR p.tags = ?) ORDER BY p.published_at DESC`;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'tag', tag],\n sql,\n [`%\"${tag}\"%`, `%,${tag},%`, `${tag},%`, tag],\n { enabled: !!tag }\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbPostSearch(query: string) {\n const searchTerm = `%${query}%`;\n const sql = `${POSTS_WITH_CATEGORIES_SQL} AND (p.title LIKE ? OR p.content LIKE ? OR p.excerpt LIKE ?) ORDER BY p.published_at DESC`;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'search', query],\n sql,\n [searchTerm, searchTerm, searchTerm],\n { enabled: !!query.trim() }\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbPostBySlug(slug: string) {\n const sql = `\n SELECT p.*,\n GROUP_CONCAT(c.name) as category_names,\n GROUP_CONCAT(c.slug) as category_slugs,\n GROUP_CONCAT(c.id) as category_ids\n FROM posts p\n LEFT JOIN post_categories pc ON p.id = pc.post_id\n LEFT JOIN blog_categories c ON pc.category_id = c.id\n WHERE p.slug = ? AND p.published = 1\n GROUP BY p.id\n `;\n\n const { data, isLoading: loading, error } = useRawQueryOne<any>(\n ['posts', 'slug', slug],\n sql,\n [slug],\n { enabled: !!slug }\n );\n\n const post = useMemo(() => {\n if (!data) return null;\n return transformPost(data);\n }, [data]);\n\n return {\n post,\n loading,\n error: !data && !loading && slug ? 'Post not found' : (error?.message ?? null)\n };\n}\n\nexport function useDbBlogCategories() {\n const { data, isLoading: loading, error } = useRepositoryQuery<BlogCategory>('blog_categories', {\n orderBy: [{ field: 'name', direction: 'ASC' }]\n });\n\n return {\n categories: data ?? [],\n loading,\n error: error?.message ?? null\n };\n}\n\nexport function useDbPostStats() {\n const { data: statsData, isLoading: loading, error } = useRawQueryOne<{\n totalPosts: number;\n totalViews: number;\n featuredPosts: number;\n }>(\n ['posts', 'stats'],\n `SELECT\n COUNT(*) as totalPosts,\n COALESCE(SUM(view_count), 0) as totalViews,\n SUM(CASE WHEN featured = 1 THEN 1 ELSE 0 END) as featuredPosts\n FROM posts WHERE published = 1`\n );\n\n const { data: categoriesData } = useRawQueryOne<{ count: number }>(\n ['categories', 'count'],\n `SELECT COUNT(*) as count FROM blog_categories`\n );\n\n const stats = useMemo(() => ({\n totalPosts: statsData?.totalPosts ?? 0,\n totalViews: statsData?.totalViews ?? 0,\n featuredPosts: statsData?.featuredPosts ?? 0,\n categoriesCount: categoriesData?.count ?? 0\n }), [statsData, categoriesData]);\n\n return { stats, loading, error: error?.message ?? null };\n}\n"
36
+ "content": "import { useMemo } from 'react';\r\nimport type { Post, BlogCategory, PostCategory } from './types';\r\nimport {\r\n useRepositoryQuery,\r\n useRawQuery,\r\n useRawQueryOne,\r\n parseStringToArray,\r\n parseSQLiteBoolean,\r\n parseNumberSafe\r\n} from '@/modules/db';\r\n\r\nconst transformPost = (row: any): Post => {\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: PostCategory[] = 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 title: String(row.title || ''),\r\n slug: String(row.slug || ''),\r\n excerpt: row.excerpt || '',\r\n content: row.content || '',\r\n author: row.author || '',\r\n author_avatar: row.author_avatar || '',\r\n published_at: row.published_at || new Date().toISOString(),\r\n updated_at: row.updated_at || new Date().toISOString(),\r\n featured_image: row.featured_image || '',\r\n images: parseStringToArray(row.images),\r\n tags: parseStringToArray(row.tags),\r\n read_time: parseNumberSafe(row.read_time),\r\n view_count: parseNumberSafe(row.view_count),\r\n featured: parseSQLiteBoolean(row.featured),\r\n published: parseSQLiteBoolean(row.published),\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 POSTS_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 posts p\r\n LEFT JOIN post_categories pc ON p.id = pc.post_id\r\n LEFT JOIN blog_categories c ON pc.category_id = c.id\r\n WHERE p.published = 1\r\n GROUP BY p.id\r\n`;\r\n\r\nexport function useDbPosts() {\r\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.published_at DESC`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'all'],\r\n sql\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbFeaturedPosts() {\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 posts p\r\n LEFT JOIN post_categories pc ON p.id = pc.post_id\r\n LEFT JOIN blog_categories c ON pc.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.published_at DESC\r\n `;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'featured'],\r\n sql\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbRecentPosts(limit: number = 10) {\r\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.published_at DESC LIMIT ?`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'recent', limit],\r\n sql,\r\n [limit]\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbPopularPosts(limit: number = 10) {\r\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.view_count DESC LIMIT ?`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'popular', limit],\r\n sql,\r\n [limit]\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbPostsByCategory(categorySlug: 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 posts p\r\n LEFT JOIN post_categories pc ON p.id = pc.post_id\r\n LEFT JOIN blog_categories c ON pc.category_id = c.id\r\n WHERE p.published = 1 AND p.id IN (\r\n SELECT pc2.post_id FROM post_categories pc2\r\n JOIN blog_categories c2 ON pc2.category_id = c2.id\r\n WHERE c2.slug = ?\r\n )\r\n GROUP BY p.id\r\n ORDER BY p.published_at DESC\r\n `;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'category', categorySlug],\r\n sql,\r\n [categorySlug],\r\n { enabled: !!categorySlug }\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbPostsByTag(tag: string) {\r\n const sql = `${POSTS_WITH_CATEGORIES_SQL} AND (p.tags LIKE ? OR p.tags LIKE ? OR p.tags LIKE ? OR p.tags = ?) ORDER BY p.published_at DESC`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'tag', tag],\r\n sql,\r\n [`%\"${tag}\"%`, `%,${tag},%`, `${tag},%`, tag],\r\n { enabled: !!tag }\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbPostSearch(query: string) {\r\n const searchTerm = `%${query}%`;\r\n const sql = `${POSTS_WITH_CATEGORIES_SQL} AND (p.title LIKE ? OR p.content LIKE ? OR p.excerpt LIKE ?) ORDER BY p.published_at DESC`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'search', query],\r\n sql,\r\n [searchTerm, searchTerm, searchTerm],\r\n { enabled: !!query.trim() }\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbPostBySlug(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 posts p\r\n LEFT JOIN post_categories pc ON p.id = pc.post_id\r\n LEFT JOIN blog_categories c ON pc.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 ['posts', 'slug', slug],\r\n sql,\r\n [slug],\r\n { enabled: !!slug }\r\n );\r\n\r\n const post = useMemo(() => {\r\n if (!data) return null;\r\n return transformPost(data);\r\n }, [data]);\r\n\r\n return {\r\n post,\r\n loading,\r\n error: !data && !loading && slug ? 'Post not found' : (error?.message ?? null)\r\n };\r\n}\r\n\r\nexport function useDbBlogCategories() {\r\n const { data, isLoading: loading, error } = useRepositoryQuery<BlogCategory>('blog_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 useDbPostStats() {\r\n const { data: statsData, isLoading: loading, error } = useRawQueryOne<{\r\n totalPosts: number;\r\n totalViews: number;\r\n featuredPosts: number;\r\n }>(\r\n ['posts', 'stats'],\r\n `SELECT\r\n COUNT(*) as totalPosts,\r\n COALESCE(SUM(view_count), 0) as totalViews,\r\n SUM(CASE WHEN featured = 1 THEN 1 ELSE 0 END) as featuredPosts\r\n FROM posts WHERE published = 1`\r\n );\r\n\r\n const { data: categoriesData } = useRawQueryOne<{ count: number }>(\r\n ['categories', 'count'],\r\n `SELECT COUNT(*) as count FROM blog_categories`\r\n );\r\n\r\n const stats = useMemo(() => ({\r\n totalPosts: statsData?.totalPosts ?? 0,\r\n totalViews: statsData?.totalViews ?? 0,\r\n featuredPosts: statsData?.featuredPosts ?? 0,\r\n categoriesCount: categoriesData?.count ?? 0\r\n }), [statsData, categoriesData]);\r\n\r\n return { stats, loading, error: error?.message ?? null };\r\n}\r\n"
37
37
  },
38
38
  {
39
39
  "path": "blog-core/lang/en.json",
@@ -24,7 +24,7 @@
24
24
  "path": "blog-list-page/blog-list-page.tsx",
25
25
  "type": "registry:page",
26
26
  "target": "$modules$/blog-list-page/blog-list-page.tsx",
27
- "content": "import { useState, useEffect } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { Layout } from \"@/components/Layout\";\nimport { Search, Filter } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { FadeIn } from \"@/modules/animations\";\nimport { Input } from \"@/components/ui/input\";\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 { PostCard } from \"@/modules/post-card/post-card\";\nimport { useDbPosts, useDbBlogCategories, type BlogCategory } from \"@/modules/blog-core\";\n\ninterface FilterSectionProps {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n t: any;\n searchTerm: string;\n setSearchTerm: (term: string) => void;\n categories: BlogCategory[];\n selectedCategories: string[];\n handleCategoryChange: (slug: string, checked: boolean) => void;\n allTags: string[];\n selectedTags: string[];\n handleTagChange: (tag: string, checked: boolean) => void;\n clearFilters: () => void;\n}\n\nfunction FilterSection({\n t,\n searchTerm,\n setSearchTerm,\n categories,\n selectedCategories,\n handleCategoryChange,\n allTags,\n selectedTags,\n handleTagChange,\n clearFilters,\n}: FilterSectionProps) {\n return (\n <div className=\"space-y-6\">\n <div>\n <h3 className=\"font-semibold mb-3 flex items-center gap-2\">\n <Search className=\"h-4 w-4\" />\n {t(\"search\")}\n </h3>\n <Input\n placeholder={t(\"searchPlaceholder\")}\n value={searchTerm}\n onChange={(e) => setSearchTerm(e.target.value)}\n />\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-3\">{t(\"categories\")}</h3>\n <div className=\"space-y-2\">\n {categories.map((category) => (\n <div key={category.slug} className=\"flex items-center space-x-2\" data-db-table=\"blog_categories\" data-db-id={category.id || category.slug}>\n <Checkbox\n id={`category-${category.slug}`}\n checked={selectedCategories.includes(category.slug)}\n onCheckedChange={(checked) =>\n handleCategoryChange(category.slug, checked as boolean)\n }\n />\n <label\n htmlFor={`category-${category.slug}`}\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\n >\n {category.name}\n </label>\n </div>\n ))}\n </div>\n </div>\n\n {allTags.length > 0 && (\n <div>\n <h3 className=\"font-semibold mb-3\">{t(\"tags\")}</h3>\n <div className=\"space-y-2 max-h-48 overflow-y-auto\">\n {allTags.slice(0, 20).map((tag) => (\n <div key={tag} className=\"flex items-center space-x-2\">\n <Checkbox\n id={`tag-${tag}`}\n checked={selectedTags.includes(tag)}\n onCheckedChange={(checked) =>\n handleTagChange(tag, checked as boolean)\n }\n />\n <label\n htmlFor={`tag-${tag}`}\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\n >\n {tag}\n </label>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {(searchTerm ||\n selectedCategories.length > 0 ||\n selectedTags.length > 0) && (\n <Button variant=\"outline\" onClick={clearFilters} className=\"w-full\">\n {t(\"clearFilters\")}\n </Button>\n )}\n </div>\n );\n}\n\nexport function BlogListPage() {\n const { t } = useTranslation(\"blog-list-page\");\n usePageTitle({ title: t(\"title\") });\n\n const [searchParams, setSearchParams] = useSearchParams();\n const [searchTerm, setSearchTerm] = useState(\n searchParams.get(\"search\") || \"\"\n );\n const [selectedCategories, setSelectedCategories] = useState<string[]>(\n searchParams.get(\"categories\")?.split(\",\").filter(Boolean) || []\n );\n const [selectedTags, setSelectedTags] = useState<string[]>(\n searchParams.get(\"tags\")?.split(\",\").filter(Boolean) || []\n );\n const [sortBy, setSortBy] = useState(searchParams.get(\"sort\") || \"newest\");\n const [viewMode, _setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\n\n const { posts, loading, error } = useDbPosts();\n const { categories } = useDbBlogCategories();\n\n const filteredPosts = posts.filter((post) => {\n if (searchTerm) {\n const searchLower = searchTerm.toLowerCase();\n if (\n !post.title.toLowerCase().includes(searchLower) &&\n !post.excerpt.toLowerCase().includes(searchLower) &&\n !post.content.toLowerCase().includes(searchLower)\n ) {\n return false;\n }\n }\n\n if (selectedCategories.length > 0) {\n const hasMatchingCategory = selectedCategories.some(\n (categorySlug) =>\n post.category === categorySlug ||\n post.categories?.some((cat) => cat.slug === categorySlug)\n );\n if (!hasMatchingCategory) return false;\n }\n\n if (selectedTags.length > 0) {\n const hasMatchingTag = selectedTags.some((tag) =>\n post.tags.includes(tag)\n );\n if (!hasMatchingTag) return false;\n }\n\n return true;\n });\n\n const sortedPosts = [...filteredPosts].sort((a, b) => {\n switch (sortBy) {\n case \"oldest\":\n return (\n new Date(a.published_at).getTime() -\n new Date(b.published_at).getTime()\n );\n case \"popular\":\n return (b.view_count || 0) - (a.view_count || 0);\n case \"reading-time\":\n return (a.read_time || 0) - (b.read_time || 0);\n case \"newest\":\n default:\n return (\n new Date(b.published_at).getTime() -\n new Date(a.published_at).getTime()\n );\n }\n });\n\n useEffect(() => {\n const params = new URLSearchParams();\n if (searchTerm) params.set(\"search\", searchTerm);\n if (selectedCategories.length)\n params.set(\"categories\", selectedCategories.join(\",\"));\n if (selectedTags.length) params.set(\"tags\", selectedTags.join(\",\"));\n if (sortBy !== \"newest\") params.set(\"sort\", sortBy);\n\n setSearchParams(params);\n }, [searchTerm, selectedCategories, selectedTags, sortBy, setSearchParams]);\n\n const handleCategoryChange = (categorySlug: string, checked: boolean) => {\n if (checked) {\n setSelectedCategories([...selectedCategories, categorySlug]);\n } else {\n setSelectedCategories(\n selectedCategories.filter((c) => c !== categorySlug)\n );\n }\n };\n\n const handleTagChange = (tag: string, checked: boolean) => {\n if (checked) {\n setSelectedTags([...selectedTags, tag]);\n } else {\n setSelectedTags(selectedTags.filter((t) => t !== tag));\n }\n };\n\n const allTags = Array.from(new Set(posts.flatMap((post) => post.tags)));\n\n const clearFilters = () => {\n setSearchTerm(\"\");\n setSelectedCategories([]);\n setSelectedTags([]);\n setSortBy(\"newest\");\n };\n\n const filterProps: FilterSectionProps = {\n t,\n searchTerm,\n setSearchTerm,\n categories,\n selectedCategories,\n handleCategoryChange,\n allTags,\n selectedTags,\n handleTagChange,\n clearFilters,\n };\n\n if (loading) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"animate-pulse space-y-4\">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className=\"h-48 bg-muted rounded-lg\"></div>\n ))}\n </div>\n </div>\n </Layout>\n );\n }\n\n if (error) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8 text-center\">\n <p className=\"text-destructive\">{t(\"error\")}</p>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 mb-8\">\n <div>\n <h1 className=\"text-3xl font-bold mb-2\">{t(\"title\")}</h1>\n <p className=\"text-muted-foreground\">{t(\"subtitle\")}</p>\n </div>\n\n <div className=\"flex items-center gap-4\">\n <Select value={sortBy} onValueChange={setSortBy}>\n <SelectTrigger className=\"w-[180px]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"newest\">{t(\"sortNewest\")}</SelectItem>\n <SelectItem value=\"oldest\">{t(\"sortOldest\")}</SelectItem>\n <SelectItem value=\"popular\">{t(\"sortPopular\")}</SelectItem>\n <SelectItem value=\"reading-time\">\n {t(\"sortReadingTime\")}\n </SelectItem>\n </SelectContent>\n </Select>\n\n <Sheet>\n <SheetTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"lg:hidden\">\n <Filter className=\"h-4 w-4 mr-2\" />\n {t(\"filters\")}\n </Button>\n </SheetTrigger>\n <SheetContent>\n <SheetHeader>\n <SheetTitle>{t(\"filters\")}</SheetTitle>\n <SheetDescription>{t(\"filterDescription\")}</SheetDescription>\n </SheetHeader>\n <div className=\"mt-6\">\n <FilterSection {...filterProps} />\n </div>\n </SheetContent>\n </Sheet>\n </div>\n </FadeIn>\n\n <div className=\"flex flex-col lg:flex-row gap-8\">\n <div className=\"hidden lg:block w-64 flex-shrink-0\">\n <div className=\"sticky top-4\">\n <FilterSection {...filterProps} />\n </div>\n </div>\n\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between mb-6\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\"showing\")} {sortedPosts.length} {t(\"of\")} {posts.length}{\" \"}\n {t(\"posts\")}\n {searchTerm && (\n <span className=\"ml-1\">\n {t(\"for\")} \"<strong>{searchTerm}</strong>\"\n </span>\n )}\n </p>\n </div>\n\n {sortedPosts.length > 0 ? (\n <div\n className={`grid gap-6 ${\n viewMode === \"grid\"\n ? \"grid-cols-1 md:grid-cols-2 xl:grid-cols-3\"\n : \"grid-cols-1\"\n }`}\n >\n {sortedPosts.map((post) => (\n <div key={post.id} className=\"contents\" data-db-table=\"posts\" data-db-id={post.id}>\n <PostCard post={post} layout={viewMode} />\n </div>\n ))}\n </div>\n ) : (\n <div className=\"text-center py-12\">\n <p className=\"text-muted-foreground mb-4\">\n {t(\"noPostsFound\")}\n </p>\n <Button onClick={clearFilters} variant=\"outline\">\n {t(\"clearFilters\")}\n </Button>\n </div>\n )}\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default BlogListPage;\n"
27
+ "content": "import { useState, useEffect } from \"react\";\r\nimport { useSearchParams } from \"react-router\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Search, Filter } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport {\r\n Sheet,\r\n SheetContent,\r\n SheetDescription,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport { PostCard } from \"@/modules/post-card/post-card\";\r\nimport { useDbPosts, useDbBlogCategories, type BlogCategory } from \"@/modules/blog-core\";\r\n\r\ninterface FilterSectionProps {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n t: any;\r\n searchTerm: string;\r\n setSearchTerm: (term: string) => void;\r\n categories: BlogCategory[];\r\n selectedCategories: string[];\r\n handleCategoryChange: (slug: string, checked: boolean) => void;\r\n allTags: string[];\r\n selectedTags: string[];\r\n handleTagChange: (tag: string, checked: boolean) => void;\r\n clearFilters: () => void;\r\n}\r\n\r\nfunction FilterSection({\r\n t,\r\n searchTerm,\r\n setSearchTerm,\r\n categories,\r\n selectedCategories,\r\n handleCategoryChange,\r\n allTags,\r\n selectedTags,\r\n handleTagChange,\r\n clearFilters,\r\n}: FilterSectionProps) {\r\n return (\r\n <div className=\"space-y-6\">\r\n <div>\r\n <h3 className=\"font-semibold mb-3 flex items-center gap-2\">\r\n <Search className=\"h-4 w-4\" />\r\n {t(\"search\")}\r\n </h3>\r\n <Input\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={(e) => setSearchTerm(e.target.value)}\r\n />\r\n </div>\r\n\r\n <div>\r\n <h3 className=\"font-semibold mb-3\">{t(\"categories\")}</h3>\r\n <div className=\"space-y-2\">\r\n {categories.map((category) => (\r\n <div key={category.slug} className=\"flex items-center space-x-2\" data-db-table=\"blog_categories\" data-db-id={category.id || category.slug}>\r\n <Checkbox\r\n id={`category-${category.slug}`}\r\n checked={selectedCategories.includes(category.slug)}\r\n onCheckedChange={(checked) =>\r\n handleCategoryChange(category.slug, checked as boolean)\r\n }\r\n />\r\n <label\r\n htmlFor={`category-${category.slug}`}\r\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\r\n >\r\n {category.name}\r\n </label>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n\r\n {allTags.length > 0 && (\r\n <div>\r\n <h3 className=\"font-semibold mb-3\">{t(\"tags\")}</h3>\r\n <div className=\"space-y-2 max-h-48 overflow-y-auto\">\r\n {allTags.slice(0, 20).map((tag) => (\r\n <div key={tag} className=\"flex items-center space-x-2\">\r\n <Checkbox\r\n id={`tag-${tag}`}\r\n checked={selectedTags.includes(tag)}\r\n onCheckedChange={(checked) =>\r\n handleTagChange(tag, checked as boolean)\r\n }\r\n />\r\n <label\r\n htmlFor={`tag-${tag}`}\r\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\r\n >\r\n {tag}\r\n </label>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {(searchTerm ||\r\n selectedCategories.length > 0 ||\r\n selectedTags.length > 0) && (\r\n <Button variant=\"outline\" onClick={clearFilters} className=\"w-full\">\r\n {t(\"clearFilters\")}\r\n </Button>\r\n )}\r\n </div>\r\n );\r\n}\r\n\r\nexport function BlogListPage() {\r\n const { t } = useTranslation(\"blog-list-page\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const [searchParams, setSearchParams] = useSearchParams();\r\n const [searchTerm, setSearchTerm] = useState(\r\n searchParams.get(\"search\") || \"\"\r\n );\r\n const [selectedCategories, setSelectedCategories] = useState<string[]>(\r\n searchParams.get(\"categories\")?.split(\",\").filter(Boolean) || []\r\n );\r\n const [selectedTags, setSelectedTags] = useState<string[]>(\r\n searchParams.get(\"tags\")?.split(\",\").filter(Boolean) || []\r\n );\r\n const [sortBy, setSortBy] = useState(searchParams.get(\"sort\") || \"newest\");\r\n const [viewMode, _setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\r\n\r\n const { posts, loading, error } = useDbPosts();\r\n const { categories } = useDbBlogCategories();\r\n\r\n const filteredPosts = posts.filter((post) => {\r\n if (searchTerm) {\r\n const searchLower = searchTerm.toLowerCase();\r\n if (\r\n !post.title.toLowerCase().includes(searchLower) &&\r\n !post.excerpt.toLowerCase().includes(searchLower) &&\r\n !post.content.toLowerCase().includes(searchLower)\r\n ) {\r\n return false;\r\n }\r\n }\r\n\r\n if (selectedCategories.length > 0) {\r\n const hasMatchingCategory = selectedCategories.some(\r\n (categorySlug) =>\r\n post.category === categorySlug ||\r\n post.categories?.some((cat) => cat.slug === categorySlug)\r\n );\r\n if (!hasMatchingCategory) return false;\r\n }\r\n\r\n if (selectedTags.length > 0) {\r\n const hasMatchingTag = selectedTags.some((tag) =>\r\n post.tags.includes(tag)\r\n );\r\n if (!hasMatchingTag) return false;\r\n }\r\n\r\n return true;\r\n });\r\n\r\n const sortedPosts = [...filteredPosts].sort((a, b) => {\r\n switch (sortBy) {\r\n case \"oldest\":\r\n return (\r\n new Date(a.published_at).getTime() -\r\n new Date(b.published_at).getTime()\r\n );\r\n case \"popular\":\r\n return (b.view_count || 0) - (a.view_count || 0);\r\n case \"reading-time\":\r\n return (a.read_time || 0) - (b.read_time || 0);\r\n case \"newest\":\r\n default:\r\n return (\r\n new Date(b.published_at).getTime() -\r\n new Date(a.published_at).getTime()\r\n );\r\n }\r\n });\r\n\r\n useEffect(() => {\r\n const params = new URLSearchParams();\r\n if (searchTerm) params.set(\"search\", searchTerm);\r\n if (selectedCategories.length)\r\n params.set(\"categories\", selectedCategories.join(\",\"));\r\n if (selectedTags.length) params.set(\"tags\", selectedTags.join(\",\"));\r\n if (sortBy !== \"newest\") params.set(\"sort\", sortBy);\r\n\r\n setSearchParams(params);\r\n }, [searchTerm, selectedCategories, selectedTags, sortBy, setSearchParams]);\r\n\r\n const handleCategoryChange = (categorySlug: string, checked: boolean) => {\r\n if (checked) {\r\n setSelectedCategories([...selectedCategories, categorySlug]);\r\n } else {\r\n setSelectedCategories(\r\n selectedCategories.filter((c) => c !== categorySlug)\r\n );\r\n }\r\n };\r\n\r\n const handleTagChange = (tag: string, checked: boolean) => {\r\n if (checked) {\r\n setSelectedTags([...selectedTags, tag]);\r\n } else {\r\n setSelectedTags(selectedTags.filter((t) => t !== tag));\r\n }\r\n };\r\n\r\n const allTags = Array.from(new Set(posts.flatMap((post) => post.tags)));\r\n\r\n const clearFilters = () => {\r\n setSearchTerm(\"\");\r\n setSelectedCategories([]);\r\n setSelectedTags([]);\r\n setSortBy(\"newest\");\r\n };\r\n\r\n const filterProps: FilterSectionProps = {\r\n t,\r\n searchTerm,\r\n setSearchTerm,\r\n categories,\r\n selectedCategories,\r\n handleCategoryChange,\r\n allTags,\r\n selectedTags,\r\n handleTagChange,\r\n clearFilters,\r\n };\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 {Array.from({ length: 6 }).map((_, i) => (\r\n <div key={i} className=\"h-48 bg-muted rounded-lg\"></div>\r\n ))}\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n if (error) {\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 <p className=\"text-destructive\">{t(\"error\")}</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 <FadeIn className=\"flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 mb-8\">\r\n <div>\r\n <h1 className=\"text-3xl font-bold mb-2\">{t(\"title\")}</h1>\r\n <p className=\"text-muted-foreground\">{t(\"subtitle\")}</p>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-4\">\r\n <Select value={sortBy} onValueChange={setSortBy}>\r\n <SelectTrigger className=\"w-[180px]\">\r\n <SelectValue />\r\n </SelectTrigger>\r\n <SelectContent>\r\n <SelectItem value=\"newest\">{t(\"sortNewest\")}</SelectItem>\r\n <SelectItem value=\"oldest\">{t(\"sortOldest\")}</SelectItem>\r\n <SelectItem value=\"popular\">{t(\"sortPopular\")}</SelectItem>\r\n <SelectItem value=\"reading-time\">\r\n {t(\"sortReadingTime\")}\r\n </SelectItem>\r\n </SelectContent>\r\n </Select>\r\n\r\n <Sheet>\r\n <SheetTrigger asChild>\r\n <Button variant=\"outline\" size=\"sm\" className=\"lg:hidden\">\r\n <Filter className=\"h-4 w-4 mr-2\" />\r\n {t(\"filters\")}\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent>\r\n <SheetHeader>\r\n <SheetTitle>{t(\"filters\")}</SheetTitle>\r\n <SheetDescription>{t(\"filterDescription\")}</SheetDescription>\r\n </SheetHeader>\r\n <div className=\"mt-6\">\r\n <FilterSection {...filterProps} />\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n </div>\r\n </FadeIn>\r\n\r\n <div className=\"flex flex-col lg:flex-row gap-8\">\r\n <div className=\"hidden lg:block w-64 flex-shrink-0\">\r\n <div className=\"sticky top-4\">\r\n <FilterSection {...filterProps} />\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex-1\">\r\n <div className=\"flex items-center justify-between mb-6\">\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"showing\")} {sortedPosts.length} {t(\"of\")} {posts.length}{\" \"}\r\n {t(\"posts\")}\r\n {searchTerm && (\r\n <span className=\"ml-1\">\r\n {t(\"for\")} \"<strong>{searchTerm}</strong>\"\r\n </span>\r\n )}\r\n </p>\r\n </div>\r\n\r\n {sortedPosts.length > 0 ? (\r\n <div\r\n className={`grid gap-6 ${\r\n viewMode === \"grid\"\r\n ? \"grid-cols-1 md:grid-cols-2 xl:grid-cols-3\"\r\n : \"grid-cols-1\"\r\n }`}\r\n >\r\n {sortedPosts.map((post) => (\r\n <div key={post.id} className=\"contents\" data-db-table=\"posts\" data-db-id={post.id}>\r\n <PostCard post={post} layout={viewMode} />\r\n </div>\r\n ))}\r\n </div>\r\n ) : (\r\n <div className=\"text-center py-12\">\r\n <p className=\"text-muted-foreground mb-4\">\r\n {t(\"noPostsFound\")}\r\n </p>\r\n <Button onClick={clearFilters} variant=\"outline\">\r\n {t(\"clearFilters\")}\r\n </Button>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default BlogListPage;\r\n"
28
28
  },
29
29
  {
30
30
  "path": "blog-list-page/lang/en.json",
@@ -20,7 +20,7 @@
20
20
  "path": "cart-drawer/cart-drawer.tsx",
21
21
  "type": "registry:component",
22
22
  "target": "$modules$/cart-drawer/cart-drawer.tsx",
23
- "content": "import { Link } from \"react-router\";\nimport { Minus, Plus } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n} from \"@/components/ui/sheet\";\nimport { useTranslation } from \"react-i18next\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport constants from \"@/constants/constants.json\";\n\ninterface CartDrawerProps {\n checkoutHref?: string;\n className?: string;\n showTrigger?: boolean;\n}\n\nexport function CartDrawer({\n checkoutHref = \"/checkout\",\n className,\n showTrigger = true,\n}: CartDrawerProps) {\n const { t } = useTranslation(\"cart-drawer\");\n const {\n state,\n removeItem,\n updateQuantity,\n isDrawerOpen,\n setDrawerOpen,\n } = useCart();\n const { items, total } = state;\n const currency = (constants.site as any).currency || \"USD\";\n\n const getProductPrice = (product: {\n price: number;\n sale_price?: number;\n on_sale?: boolean;\n }) => {\n return product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n };\n\n const handleQuantityChange = (id: string | number, newQuantity: number) => {\n if (newQuantity <= 0) {\n removeItem(id);\n } else {\n updateQuantity(id, newQuantity);\n }\n };\n\n return (\n <Sheet open={isDrawerOpen} onOpenChange={setDrawerOpen}>\n <SheetContent className=\"w-full sm:max-w-md flex flex-col px-6 pb-8\">\n <SheetHeader>\n <SheetTitle>{t(\"title\", \"Shopping cart\")}</SheetTitle>\n </SheetHeader>\n\n <div className=\"flex-1 overflow-y-auto mt-8\">\n {items.length === 0 ? (\n <p className=\"text-center text-muted-foreground py-8\">\n {t(\"empty\", \"Your cart is empty\")}\n </p>\n ) : (\n <ul className=\"-my-6 divide-y divide-border\">\n {items.map((item) => (\n <li key={item.id} className=\"flex py-6\">\n <div className=\"size-24 shrink-0 overflow-hidden rounded-md border border-border\">\n <img\n alt={item.product.name}\n src={item.product.images[0] || \"/images/placeholder.png\"}\n className=\"size-full object-cover\"\n />\n </div>\n\n <div className=\"ml-4 flex flex-1 flex-col\">\n <div>\n <div className=\"flex justify-between text-base font-medium\">\n <h3>\n <Link\n to={`/products/${item.product.slug}`}\n onClick={() => setDrawerOpen(false)}\n >\n {item.product.name}\n </Link>\n </h3>\n <p className=\"ml-4\">\n {formatPrice(getProductPrice(item.product), currency)}\n </p>\n </div>\n {item.product.category_name && (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {item.product.category_name}\n </p>\n )}\n </div>\n <div className=\"flex flex-1 items-end justify-between text-sm\">\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-6 w-6\"\n onClick={() =>\n handleQuantityChange(item.id, item.quantity - 1)\n }\n >\n <Minus className=\"h-3 w-3\" />\n </Button>\n <span className=\"w-8 text-center text-sm\">\n {item.quantity}\n </span>\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-6 w-6\"\n onClick={() =>\n handleQuantityChange(item.id, item.quantity + 1)\n }\n >\n <Plus className=\"h-3 w-3\" />\n </Button>\n </div>\n\n <button\n type=\"button\"\n onClick={() => removeItem(item.id)}\n className=\"font-medium text-primary hover:text-primary/80\"\n >\n {t(\"remove\", \"Remove\")}\n </button>\n </div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n\n <div className=\"border-t border-border pt-6 mt-6\">\n <div className=\"flex justify-between text-base font-medium\">\n <p>{t(\"subtotal\", \"Subtotal\")}</p>\n <p>{formatPrice(total, currency)}</p>\n </div>\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\n {t(\"shippingNote\", \"Shipping and taxes calculated at checkout.\")}\n </p>\n <div className=\"mt-6\">\n <Button asChild className=\"w-full\" disabled={items.length === 0}>\n <Link to={checkoutHref} onClick={() => setDrawerOpen(false)}>\n {t(\"checkout\", \"Checkout\")}\n </Link>\n </Button>\n </div>\n </div>\n </SheetContent>\n </Sheet>\n );\n}\n"
23
+ "content": "import { Link } from \"react-router\";\r\nimport { Minus, Plus } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Sheet,\r\n SheetContent,\r\n SheetHeader,\r\n SheetTitle,\r\n} from \"@/components/ui/sheet\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\ninterface CartDrawerProps {\r\n checkoutHref?: string;\r\n className?: string;\r\n showTrigger?: boolean;\r\n}\r\n\r\nexport function CartDrawer({\r\n checkoutHref = \"/checkout\",\r\n className,\r\n showTrigger = true,\r\n}: CartDrawerProps) {\r\n const { t } = useTranslation(\"cart-drawer\");\r\n const {\r\n state,\r\n removeItem,\r\n updateQuantity,\r\n isDrawerOpen,\r\n setDrawerOpen,\r\n } = useCart();\r\n const { items, total } = state;\r\n const currency = (constants.site as any).currency || \"USD\";\r\n\r\n const getProductPrice = (product: {\r\n price: number;\r\n sale_price?: number;\r\n on_sale?: boolean;\r\n }) => {\r\n return product.on_sale && product.sale_price\r\n ? product.sale_price\r\n : product.price;\r\n };\r\n\r\n const handleQuantityChange = (id: string | number, newQuantity: number) => {\r\n if (newQuantity <= 0) {\r\n removeItem(id);\r\n } else {\r\n updateQuantity(id, newQuantity);\r\n }\r\n };\r\n\r\n return (\r\n <Sheet open={isDrawerOpen} onOpenChange={setDrawerOpen}>\r\n <SheetContent className=\"w-full sm:max-w-md flex flex-col px-6 pb-8\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"title\", \"Shopping cart\")}</SheetTitle>\r\n </SheetHeader>\r\n\r\n <div className=\"flex-1 overflow-y-auto mt-8\">\r\n {items.length === 0 ? (\r\n <p className=\"text-center text-muted-foreground py-8\">\r\n {t(\"empty\", \"Your cart is empty\")}\r\n </p>\r\n ) : (\r\n <ul className=\"-my-6 divide-y divide-border\">\r\n {items.map((item) => (\r\n <li key={item.id} className=\"flex py-6\">\r\n <div className=\"size-24 shrink-0 overflow-hidden rounded-md border border-border\">\r\n <img\r\n alt={item.product.name}\r\n src={item.product.images[0] || \"/images/placeholder.png\"}\r\n className=\"size-full object-cover\"\r\n />\r\n </div>\r\n\r\n <div className=\"ml-4 flex flex-1 flex-col\">\r\n <div>\r\n <div className=\"flex justify-between text-base font-medium\">\r\n <h3>\r\n <Link\r\n to={`/products/${item.product.slug}`}\r\n onClick={() => setDrawerOpen(false)}\r\n >\r\n {item.product.name}\r\n </Link>\r\n </h3>\r\n <p className=\"ml-4\">\r\n {formatPrice(getProductPrice(item.product), currency)}\r\n </p>\r\n </div>\r\n {item.product.category_name && (\r\n <p className=\"mt-1 text-sm text-muted-foreground\">\r\n {item.product.category_name}\r\n </p>\r\n )}\r\n </div>\r\n <div className=\"flex flex-1 items-end justify-between text-sm\">\r\n <div className=\"flex items-center gap-1\">\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-6 w-6\"\r\n onClick={() =>\r\n handleQuantityChange(item.id, item.quantity - 1)\r\n }\r\n >\r\n <Minus className=\"h-3 w-3\" />\r\n </Button>\r\n <span className=\"w-8 text-center text-sm\">\r\n {item.quantity}\r\n </span>\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-6 w-6\"\r\n onClick={() =>\r\n handleQuantityChange(item.id, item.quantity + 1)\r\n }\r\n >\r\n <Plus className=\"h-3 w-3\" />\r\n </Button>\r\n </div>\r\n\r\n <button\r\n type=\"button\"\r\n onClick={() => removeItem(item.id)}\r\n className=\"font-medium text-primary hover:text-primary/80\"\r\n >\r\n {t(\"remove\", \"Remove\")}\r\n </button>\r\n </div>\r\n </div>\r\n </li>\r\n ))}\r\n </ul>\r\n )}\r\n </div>\r\n\r\n <div className=\"border-t border-border pt-6 mt-6\">\r\n <div className=\"flex justify-between text-base font-medium\">\r\n <p>{t(\"subtotal\", \"Subtotal\")}</p>\r\n <p>{formatPrice(total, currency)}</p>\r\n </div>\r\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\r\n {t(\"shippingNote\", \"Shipping and taxes calculated at checkout.\")}\r\n </p>\r\n <div className=\"mt-6\">\r\n <Button asChild className=\"w-full\" disabled={items.length === 0}>\r\n <Link to={checkoutHref} onClick={() => setDrawerOpen(false)}>\r\n {t(\"checkout\", \"Checkout\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n );\r\n}\r\n"
24
24
  },
25
25
  {
26
26
  "path": "cart-drawer/lang/en.json",
@@ -23,7 +23,7 @@
23
23
  "path": "cart-page/cart-page.tsx",
24
24
  "type": "registry:page",
25
25
  "target": "$modules$/cart-page/cart-page.tsx",
26
- "content": "import { Link } from \"react-router\";\nimport { Trash2, Plus, Minus, ArrowLeft, ShoppingBag } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Input } from \"@/components/ui/input\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn } from \"@/modules/animations\";\n\nexport function CartPage() {\n const { t } = useTranslation(\"cart-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Shopping Cart\") });\n const { state, removeItem, updateQuantity } = useCart();\n const { items, total } = state;\n\n const currency = constants.site.currency || \"USD\";\n const shipping = 0;\n const tax = 0;\n const freeShippingThreshold = 100;\n\n const getProductPrice = (product: { price: number; sale_price?: number; on_sale?: boolean }) => {\n return product.on_sale && product.sale_price ? product.sale_price : product.price;\n };\n\n const handleQuantityChange = (productId: number | string, newQuantity: number) => {\n if (newQuantity <= 0) {\n removeItem(productId);\n } else {\n updateQuantity(productId, newQuantity);\n }\n };\n\n const handleQuantityInputChange = (productId: number | string, value: string) => {\n const quantity = parseInt(value) || 1;\n handleQuantityChange(productId, quantity);\n };\n\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\n const finalTotal = total + shipping + tax;\n\n if (items.length === 0) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"max-w-2xl mx-auto text-center py-16\">\n <div className=\"mb-8\">\n <ShoppingBag className=\"h-24 w-24 mx-auto text-muted-foreground mb-4\" />\n <h1 className=\"text-3xl font-bold mb-4\">\n {t(\"empty\", \"Your Cart is Empty\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\"emptyDescription\", \"Looks like you haven't added any items to your cart yet.\")}\n </p>\n <Button asChild size=\"lg\">\n <Link to=\"/products\">\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"flex items-center gap-4 mb-8\">\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <Link to=\"/products\">\n <ArrowLeft className=\"h-4 w-4\" />\n </Link>\n </Button>\n <div>\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Shopping Cart\")}</h1>\n <p className=\"text-muted-foreground\">\n {itemCount} {t(\"itemsInCart\", \"items in your cart\")}\n </p>\n </div>\n </FadeIn>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n <div className=\"lg:col-span-2 space-y-4\">\n {items.map((item) => (\n <Card key={item.id}>\n <CardContent className=\"p-6\">\n <div className=\"flex gap-4\">\n <div className=\"w-24 h-24 flex-shrink-0\">\n <img\n src={item.product.images[0] || \"/images/placeholder.png\"}\n alt={item.product.name}\n className=\"w-full h-full object-cover rounded-lg\"\n />\n </div>\n\n <div className=\"flex-1 space-y-2\">\n <div className=\"flex items-start justify-between\">\n <div>\n <h3 className=\"font-semibold\">{item.product.name}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {item.product.category_name ||\n item.product.categories?.[0]?.name ||\n item.product.category}\n </p>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeItem(item.product.id)}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => handleQuantityChange(item.product.id, item.quantity - 1)}\n >\n <Minus className=\"h-3 w-3\" />\n </Button>\n <Input\n type=\"number\"\n value={item.quantity}\n onChange={(e) => handleQuantityInputChange(item.product.id, e.target.value)}\n className=\"w-16 text-center\"\n min=\"1\"\n />\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => handleQuantityChange(item.product.id, item.quantity + 1)}\n >\n <Plus className=\"h-3 w-3\" />\n </Button>\n </div>\n\n <div className=\"text-right\">\n <p className=\"font-semibold\">\n {formatPrice(getProductPrice(item.product) * item.quantity, currency)}\n </p>\n {item.quantity > 1 && (\n <p className=\"text-sm text-muted-foreground\">\n {formatPrice(getProductPrice(item.product), currency)} {t(\"each\", \"each\")}\n </p>\n )}\n </div>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"flex justify-between\">\n <span>\n {t(\"subtotal\", \"Subtotal\")} ({itemCount} {t(\"items\", \"items\")})\n </span>\n <span>{formatPrice(total, currency)}</span>\n </div>\n\n <div className=\"flex justify-between\">\n <span>{t(\"shipping\", \"Shipping\")}</span>\n <span>\n {shipping === 0 ? t(\"free\", \"Free\") : formatPrice(shipping, currency)}\n </span>\n </div>\n\n <div className=\"flex justify-between\">\n <span>{t(\"tax\", \"Tax\")}</span>\n <span>{formatPrice(tax, currency)}</span>\n </div>\n\n <Separator />\n\n <div className=\"flex justify-between text-lg font-semibold\">\n <span>{t(\"total\", \"Total\")}</span>\n <span>{formatPrice(finalTotal, currency)}</span>\n </div>\n\n {shipping > 0 && freeShippingThreshold && freeShippingThreshold > total && (\n <div className=\"text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg\">\n {t(\"freeShippingMessage\", \"Add {{amount}} more for free shipping!\").replace(\n \"{{amount}}\",\n formatPrice(freeShippingThreshold - total, currency)\n )}\n </div>\n )}\n\n <Button asChild className=\"w-full\" size=\"lg\">\n <Link to=\"/checkout\">{t(\"proceedToCheckout\", \"Proceed to Checkout\")}</Link>\n </Button>\n\n <Button variant=\"outline\" asChild className=\"w-full\">\n <Link to=\"/products\">{t(\"continueShopping\", \"Continue Shopping\")}</Link>\n </Button>\n </CardContent>\n </Card>\n\n <Card>\n <CardContent className=\"p-4\">\n <div className=\"text-center space-y-2\">\n <div className=\"text-sm text-muted-foreground\">\n {t(\"secureCheckout\", \"Secure Checkout\")}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t(\"secureCheckoutDescription\", \"Your payment information is encrypted and secure\")}\n </p>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default CartPage;\n"
26
+ "content": "import { Link } from \"react-router\";\r\nimport { Trash2, Plus, Minus, ArrowLeft, ShoppingBag } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\n\r\nexport function CartPage() {\r\n const { t } = useTranslation(\"cart-page\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Shopping Cart\") });\r\n const { state, removeItem, updateQuantity } = useCart();\r\n const { items, total } = state;\r\n\r\n const currency = constants.site.currency || \"USD\";\r\n const shipping = 0;\r\n const tax = 0;\r\n const freeShippingThreshold = 100;\r\n\r\n const getProductPrice = (product: { price: number; sale_price?: number; on_sale?: boolean }) => {\r\n return product.on_sale && product.sale_price ? product.sale_price : product.price;\r\n };\r\n\r\n const handleQuantityChange = (productId: number | string, newQuantity: number) => {\r\n if (newQuantity <= 0) {\r\n removeItem(productId);\r\n } else {\r\n updateQuantity(productId, newQuantity);\r\n }\r\n };\r\n\r\n const handleQuantityInputChange = (productId: number | string, value: string) => {\r\n const quantity = parseInt(value) || 1;\r\n handleQuantityChange(productId, quantity);\r\n };\r\n\r\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\r\n const finalTotal = total + shipping + tax;\r\n\r\n if (items.length === 0) {\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-2xl mx-auto text-center py-16\">\r\n <div className=\"mb-8\">\r\n <ShoppingBag className=\"h-24 w-24 mx-auto text-muted-foreground mb-4\" />\r\n <h1 className=\"text-3xl font-bold mb-4\">\r\n {t(\"empty\", \"Your Cart is Empty\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-8\">\r\n {t(\"emptyDescription\", \"Looks like you haven't added any items to your cart yet.\")}\r\n </p>\r\n <Button asChild size=\"lg\">\r\n <Link to=\"/products\">\r\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n </div>\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 <FadeIn className=\"flex items-center gap-4 mb-8\">\r\n <Button variant=\"ghost\" size=\"icon\" asChild>\r\n <Link to=\"/products\">\r\n <ArrowLeft className=\"h-4 w-4\" />\r\n </Link>\r\n </Button>\r\n <div>\r\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Shopping Cart\")}</h1>\r\n <p className=\"text-muted-foreground\">\r\n {itemCount} {t(\"itemsInCart\", \"items in your cart\")}\r\n </p>\r\n </div>\r\n </FadeIn>\r\n\r\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\r\n <div className=\"lg:col-span-2 space-y-4\">\r\n {items.map((item) => (\r\n <Card key={item.id}>\r\n <CardContent className=\"p-6\">\r\n <div className=\"flex gap-4\">\r\n <div className=\"w-24 h-24 flex-shrink-0\">\r\n <img\r\n src={item.product.images[0] || \"/images/placeholder.png\"}\r\n alt={item.product.name}\r\n className=\"w-full h-full object-cover rounded-lg\"\r\n />\r\n </div>\r\n\r\n <div className=\"flex-1 space-y-2\">\r\n <div className=\"flex items-start justify-between\">\r\n <div>\r\n <h3 className=\"font-semibold\">{item.product.name}</h3>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {item.product.category_name ||\r\n item.product.categories?.[0]?.name ||\r\n item.product.category}\r\n </p>\r\n </div>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n onClick={() => removeItem(item.product.id)}\r\n className=\"text-destructive hover:text-destructive\"\r\n >\r\n <Trash2 className=\"h-4 w-4\" />\r\n </Button>\r\n </div>\r\n\r\n <div className=\"flex items-center justify-between\">\r\n <div className=\"flex items-center gap-2\">\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-8 w-8\"\r\n onClick={() => handleQuantityChange(item.product.id, item.quantity - 1)}\r\n >\r\n <Minus className=\"h-3 w-3\" />\r\n </Button>\r\n <Input\r\n type=\"number\"\r\n value={item.quantity}\r\n onChange={(e) => handleQuantityInputChange(item.product.id, e.target.value)}\r\n className=\"w-16 text-center\"\r\n min=\"1\"\r\n />\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-8 w-8\"\r\n onClick={() => handleQuantityChange(item.product.id, item.quantity + 1)}\r\n >\r\n <Plus className=\"h-3 w-3\" />\r\n </Button>\r\n </div>\r\n\r\n <div className=\"text-right\">\r\n <p className=\"font-semibold\">\r\n {formatPrice(getProductPrice(item.product) * item.quantity, currency)}\r\n </p>\r\n {item.quantity > 1 && (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {formatPrice(getProductPrice(item.product), currency)} {t(\"each\", \"each\")}\r\n </p>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n ))}\r\n </div>\r\n\r\n <div className=\"space-y-6\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"flex justify-between\">\r\n <span>\r\n {t(\"subtotal\", \"Subtotal\")} ({itemCount} {t(\"items\", \"items\")})\r\n </span>\r\n <span>{formatPrice(total, currency)}</span>\r\n </div>\r\n\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"shipping\", \"Shipping\")}</span>\r\n <span>\r\n {shipping === 0 ? t(\"free\", \"Free\") : formatPrice(shipping, currency)}\r\n </span>\r\n </div>\r\n\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"tax\", \"Tax\")}</span>\r\n <span>{formatPrice(tax, currency)}</span>\r\n </div>\r\n\r\n <Separator />\r\n\r\n <div className=\"flex justify-between text-lg font-semibold\">\r\n <span>{t(\"total\", \"Total\")}</span>\r\n <span>{formatPrice(finalTotal, currency)}</span>\r\n </div>\r\n\r\n {shipping > 0 && freeShippingThreshold && freeShippingThreshold > total && (\r\n <div className=\"text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg\">\r\n {t(\"freeShippingMessage\", \"Add {{amount}} more for free shipping!\").replace(\r\n \"{{amount}}\",\r\n formatPrice(freeShippingThreshold - total, currency)\r\n )}\r\n </div>\r\n )}\r\n\r\n <Button asChild className=\"w-full\" size=\"lg\">\r\n <Link to=\"/checkout\">{t(\"proceedToCheckout\", \"Proceed to Checkout\")}</Link>\r\n </Button>\r\n\r\n <Button variant=\"outline\" asChild className=\"w-full\">\r\n <Link to=\"/products\">{t(\"continueShopping\", \"Continue Shopping\")}</Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n\r\n <Card>\r\n <CardContent className=\"p-4\">\r\n <div className=\"text-center space-y-2\">\r\n <div className=\"text-sm text-muted-foreground\">\r\n {t(\"secureCheckout\", \"Secure Checkout\")}\r\n </div>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {t(\"secureCheckoutDescription\", \"Your payment information is encrypted and secure\")}\r\n </p>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default CartPage;\r\n"
27
27
  },
28
28
  {
29
29
  "path": "cart-page/lang/en.json",
@@ -16,7 +16,7 @@
16
16
  "path": "category-section/category-section.tsx",
17
17
  "type": "registry:component",
18
18
  "target": "$modules$/category-section/category-section.tsx",
19
- "content": "import { Link } from \"react-router\";\nimport { ArrowRight } from \"lucide-react\";\nimport { Card } from \"@/components/ui/card\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDbCategories } from \"@/modules/ecommerce-core\";\n\nexport interface CategoryItem {\n id: string | number;\n slug: string;\n name: string;\n description?: string;\n image: string;\n}\n\nexport interface CategorySectionProps {\n categories?: CategoryItem[];\n loading?: boolean;\n}\n\nexport function CategorySection({\n categories: propCategories,\n loading: propLoading,\n}: CategorySectionProps) {\n const { t } = useTranslation(\"category-section\");\n const { categories: hookCategories, loading: hookLoading } = useDbCategories();\n\n const categories = propCategories ?? hookCategories;\n const loading = propLoading ?? hookLoading;\n\n return (\n <section className=\"py-8 sm:py-12 md:py-16 lg:py-20 bg-gradient-to-b from-background to-muted/20 border-t border-border/20\">\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\", \"Shop by Category\")}\n </h2>\n <div className=\"w-12 sm:w-16 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 text-muted-foreground max-w-3xl mx-auto leading-relaxed\">\n {t(\"subtitle\", \"Discover our carefully curated collections\")}\n </p>\n </div>\n\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6 lg:gap-8\">\n {loading\n ? [...Array(4)].map((_, i) => (\n <div key={i} className=\"animate-pulse\">\n <div className=\"aspect-[3/2] bg-muted rounded-xl mb-4\"></div>\n <div className=\"h-5 bg-muted rounded w-3/4 mb-2\"></div>\n <div className=\"h-4 bg-muted rounded w-1/2\"></div>\n </div>\n ))\n : categories.map((category) => (\n <div key={category.id} className=\"contents\" data-db-table=\"product_categories\" data-db-id={category.id}>\n <Link\n to={`/products?category=${category.slug}`}\n className=\"group block\"\n >\n <Card className=\"overflow-hidden border-0 p-0 shadow-lg hover:shadow-2xl transition-all duration-500 group-hover:-translate-y-2 rounded-2xl\">\n <div className=\"aspect-[4/3] relative overflow-hidden\">\n <img\n src={category.image}\n alt={category.name}\n className=\"absolute inset-0 w-full h-full object-cover group-hover:scale-110 transition-transform duration-700\"\n />\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent group-hover:from-black/70 transition-all duration-300\"></div>\n\n <div className=\"absolute bottom-0 left-0 right-0 p-4 sm:p-6\">\n <h3 className=\"text-lg sm:text-xl font-bold text-white mb-1 sm:mb-2 group-hover:text-primary-foreground transition-colors\">\n {category.name}\n </h3>\n {category.description && (\n <p className=\"text-xs sm:text-sm text-white/90 line-clamp-2 group-hover:text-white transition-colors\">\n {category.description}\n </p>\n )}\n\n <div className=\"flex items-center mt-2 sm:mt-3 text-white/80 group-hover:text-white transition-all duration-300 transform group-hover:translate-x-1\">\n <span className=\"text-xs sm:text-sm font-medium mr-2\">\n {t(\"explore\", \"Explore\")}\n </span>\n <ArrowRight className=\"w-3 h-3 sm:w-4 sm:h-4\" />\n </div>\n </div>\n </div>\n </Card>\n </Link>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
19
+ "content": "import { Link } from \"react-router\";\r\nimport { ArrowRight } from \"lucide-react\";\r\nimport { Card } from \"@/components/ui/card\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useDbCategories } from \"@/modules/ecommerce-core\";\r\n\r\nexport interface CategoryItem {\r\n id: string | number;\r\n slug: string;\r\n name: string;\r\n description?: string;\r\n image: string;\r\n}\r\n\r\nexport interface CategorySectionProps {\r\n categories?: CategoryItem[];\r\n loading?: boolean;\r\n}\r\n\r\nexport function CategorySection({\r\n categories: propCategories,\r\n loading: propLoading,\r\n}: CategorySectionProps) {\r\n const { t } = useTranslation(\"category-section\");\r\n const { categories: hookCategories, loading: hookLoading } = useDbCategories();\r\n\r\n const categories = propCategories ?? hookCategories;\r\n const loading = propLoading ?? hookLoading;\r\n\r\n return (\r\n <section className=\"py-8 sm:py-12 md:py-16 lg:py-20 bg-gradient-to-b from-background to-muted/20 border-t border-border/20\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\r\n <div className=\"text-center mb-6 sm:mb-8 md:mb-12 lg:mb-16 px-2\">\r\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\">\r\n {t(\"title\", \"Shop by Category\")}\r\n </h2>\r\n <div className=\"w-12 sm:w-16 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>\r\n <p className=\"text-xs sm:text-sm md:text-base lg:text-lg text-muted-foreground max-w-3xl mx-auto leading-relaxed\">\r\n {t(\"subtitle\", \"Discover our carefully curated collections\")}\r\n </p>\r\n </div>\r\n\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6 lg:gap-8\">\r\n {loading\r\n ? [...Array(4)].map((_, i) => (\r\n <div key={i} className=\"animate-pulse\">\r\n <div className=\"aspect-[3/2] bg-muted rounded-xl mb-4\"></div>\r\n <div className=\"h-5 bg-muted rounded w-3/4 mb-2\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/2\"></div>\r\n </div>\r\n ))\r\n : categories.map((category) => (\r\n <div key={category.id} className=\"contents\" data-db-table=\"product_categories\" data-db-id={category.id}>\r\n <Link\r\n to={`/products?category=${category.slug}`}\r\n className=\"group block\"\r\n >\r\n <Card className=\"overflow-hidden border-0 p-0 shadow-lg hover:shadow-2xl transition-all duration-500 group-hover:-translate-y-2 rounded-2xl\">\r\n <div className=\"aspect-[4/3] relative overflow-hidden\">\r\n <img\r\n src={category.image}\r\n alt={category.name}\r\n className=\"absolute inset-0 w-full h-full object-cover group-hover:scale-110 transition-transform duration-700\"\r\n />\r\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent group-hover:from-black/70 transition-all duration-300\"></div>\r\n\r\n <div className=\"absolute bottom-0 left-0 right-0 p-4 sm:p-6\">\r\n <h3 className=\"text-lg sm:text-xl font-bold text-white mb-1 sm:mb-2 group-hover:text-primary-foreground transition-colors\">\r\n {category.name}\r\n </h3>\r\n {category.description && (\r\n <p className=\"text-xs sm:text-sm text-white/90 line-clamp-2 group-hover:text-white transition-colors\">\r\n {category.description}\r\n </p>\r\n )}\r\n\r\n <div className=\"flex items-center mt-2 sm:mt-3 text-white/80 group-hover:text-white transition-all duration-300 transform group-hover:translate-x-1\">\r\n <span className=\"text-xs sm:text-sm font-medium mr-2\">\r\n {t(\"explore\", \"Explore\")}\r\n </span>\r\n <ArrowRight className=\"w-3 h-3 sm:w-4 sm:h-4\" />\r\n </div>\r\n </div>\r\n </div>\r\n </Card>\r\n </Link>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
20
20
  },
21
21
  {
22
22
  "path": "category-section/lang/en.json",
@@ -19,7 +19,7 @@
19
19
  "path": "db/config.ts",
20
20
  "type": "registry:lib",
21
21
  "target": "$modules$/db/config.ts",
22
- "content": "import type { IDataAdapter } from \"./adapters/IDataAdapter\";\nimport { SqliteAdapter } from \"./adapters/SqliteAdapter\";\n\nexport type AdapterType = \"sqlite\";\n\nexport interface DataConfig {\n adapter: AdapterType;\n options?: {\n dbPath?: string;\n };\n}\n\n/**\n * Get configuration from environment variables\n */\nfunction getConfig(): DataConfig {\n const adapter =\n (import.meta.env.VITE_DATA_ADAPTER as AdapterType) || \"sqlite\";\n\n return {\n adapter,\n options: {\n dbPath: import.meta.env.VITE_DB_PATH || \"/data/database.db\",\n },\n };\n}\n\n/**\n * Create adapter instance based on configuration\n */\nexport function createAdapter(config: DataConfig): IDataAdapter {\n switch (config.adapter) {\n case \"sqlite\":\n return new SqliteAdapter(config.options?.dbPath);\n\n default:\n throw new Error(`Unknown adapter: ${config.adapter}`);\n }\n}\n\n/**\n * Get the configured adapter instance (singleton)\n */\nlet adapterInstance: IDataAdapter | null = null;\n\nexport function getAdapter(): IDataAdapter {\n if (!adapterInstance) {\n const config = getConfig();\n adapterInstance = createAdapter(config);\n }\n return adapterInstance;\n}\n\n/**\n * Reset adapter instance (useful for testing or switching adapters)\n */\nexport function resetAdapter(): void {\n adapterInstance = null;\n}\n"
22
+ "content": "import type { IDataAdapter } from \"./adapters/IDataAdapter\";\r\nimport { SqliteAdapter } from \"./adapters/SqliteAdapter\";\r\n\r\nexport type AdapterType = \"sqlite\";\r\n\r\nexport interface DataConfig {\r\n adapter: AdapterType;\r\n options?: {\r\n dbPath?: string;\r\n };\r\n}\r\n\r\n/**\r\n * Get configuration from environment variables\r\n */\r\nfunction getConfig(): DataConfig {\r\n const adapter =\r\n (import.meta.env.VITE_DATA_ADAPTER as AdapterType) || \"sqlite\";\r\n\r\n return {\r\n adapter,\r\n options: {\r\n dbPath: import.meta.env.VITE_DB_PATH || \"/data/database.db\",\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Create adapter instance based on configuration\r\n */\r\nexport function createAdapter(config: DataConfig): IDataAdapter {\r\n switch (config.adapter) {\r\n case \"sqlite\":\r\n return new SqliteAdapter(config.options?.dbPath);\r\n\r\n default:\r\n throw new Error(`Unknown adapter: ${config.adapter}`);\r\n }\r\n}\r\n\r\n/**\r\n * Get the configured adapter instance (singleton)\r\n */\r\nlet adapterInstance: IDataAdapter | null = null;\r\n\r\nexport function getAdapter(): IDataAdapter {\r\n if (!adapterInstance) {\r\n const config = getConfig();\r\n adapterInstance = createAdapter(config);\r\n }\r\n return adapterInstance;\r\n}\r\n\r\n/**\r\n * Reset adapter instance (useful for testing or switching adapters)\r\n */\r\nexport function resetAdapter(): void {\r\n adapterInstance = null;\r\n}\r\n"
23
23
  },
24
24
  {
25
25
  "path": "db/core/DataManager.ts",
@@ -43,7 +43,7 @@
43
43
  "path": "db/adapters/index.ts",
44
44
  "type": "registry:index",
45
45
  "target": "$modules$/db/adapters/index.ts",
46
- "content": "export type { IDataAdapter } from \"./IDataAdapter\";\nexport { SqliteAdapter } from \"./SqliteAdapter\";\n"
46
+ "content": "export type { IDataAdapter } from \"./IDataAdapter\";\r\nexport { SqliteAdapter } from \"./SqliteAdapter\";\r\n"
47
47
  },
48
48
  {
49
49
  "path": "db/adapters/SqliteAdapter.ts",
@@ -55,19 +55,19 @@
55
55
  "path": "db/react/index.ts",
56
56
  "type": "registry:index",
57
57
  "target": "$modules$/db/react/index.ts",
58
- "content": "// Provider\nexport { DBQueryProvider } from \"./QueryProvider\";\n\n// Query client and utilities\nexport { queryClient, queryKeys, cacheUtils } from \"./queryClient\";\n\n// Generic repository hooks\nexport {\n useRepositoryQuery,\n useRepositoryQueryOne,\n useRepositoryQueryById,\n useRepositoryPagination,\n useRepositoryInfiniteQuery,\n useRepositoryCreate,\n useRepositoryUpdate,\n useRepositoryDelete,\n // Raw SQL hooks\n useRawQuery,\n useRawQueryOne,\n} from \"./useRepository\";\n\n// Types\nexport type { RepositoryQueryOptions } from \"./useRepository\";\n"
58
+ "content": "// Provider\r\nexport { DBQueryProvider } from \"./QueryProvider\";\r\n\r\n// Query client and utilities\r\nexport { queryClient, queryKeys, cacheUtils } from \"./queryClient\";\r\n\r\n// Generic repository hooks\r\nexport {\r\n useRepositoryQuery,\r\n useRepositoryQueryOne,\r\n useRepositoryQueryById,\r\n useRepositoryPagination,\r\n useRepositoryInfiniteQuery,\r\n useRepositoryCreate,\r\n useRepositoryUpdate,\r\n useRepositoryDelete,\r\n // Raw SQL hooks\r\n useRawQuery,\r\n useRawQueryOne,\r\n} from \"./useRepository\";\r\n\r\n// Types\r\nexport type { RepositoryQueryOptions } from \"./useRepository\";\r\n"
59
59
  },
60
60
  {
61
61
  "path": "db/react/queryClient.ts",
62
62
  "type": "registry:lib",
63
63
  "target": "$modules$/db/react/queryClient.ts",
64
- "content": "import { QueryClient } from \"@tanstack/react-query\";\n\n/**\n * React Query handles ALL caching, refetching, and invalidation\n * No custom cache needed!\n */\nexport const queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 30 * 1000, // 30 seconds fresh\n gcTime: 5 * 60 * 1000, // 5 minutes in cache (was cacheTime)\n retry: 1,\n refetchOnWindowFocus: false, // Don't auto-refetch on window focus\n refetchOnReconnect: false, // Don't refetch on reconnect\n refetchOnMount: false, // Don't refetch on component mount (prevent loops)\n },\n mutations: {\n retry: 0,\n },\n },\n});\n\n/**\n * Query key factory - for cache management\n * React Query uses these keys to cache and invalidate queries\n */\nexport const queryKeys = {\n all: (table: string) => [table] as const,\n lists: (table: string) => [table, \"list\"] as const,\n list: (table: string, options?: any) => [table, \"list\", options] as const,\n details: (table: string) => [table, \"detail\"] as const,\n detail: (table: string, id: number | string) =>\n [table, \"detail\", id] as const,\n paginated: (table: string, page: number, limit: number, options?: any) =>\n [table, \"paginated\", page, limit, options] as const,\n infinite: (table: string, limit: number, options?: any) =>\n [table, \"infinite\", limit, options] as const,\n count: (table: string, options?: any) => [table, \"count\", options] as const,\n};\n\n/**\n * Manual cache utilities (rarely needed)\n */\nexport const cacheUtils = {\n // Invalidate all queries for a table\n invalidateTable: (table: string) => {\n return queryClient.invalidateQueries({ queryKey: queryKeys.all(table) });\n },\n\n // Clear all cache\n clearAll: () => {\n return queryClient.clear();\n },\n\n // Get cached data\n getCachedData: <T>(queryKey: any[]) => {\n return queryClient.getQueryData<T>(queryKey);\n },\n\n // Set cached data manually\n setCachedData: <T>(queryKey: any[], data: T) => {\n return queryClient.setQueryData<T>(queryKey, data);\n },\n};\n"
64
+ "content": "import { QueryClient } from \"@tanstack/react-query\";\r\n\r\n/**\r\n * React Query handles ALL caching, refetching, and invalidation\r\n * No custom cache needed!\r\n */\r\nexport const queryClient = new QueryClient({\r\n defaultOptions: {\r\n queries: {\r\n staleTime: 30 * 1000, // 30 seconds fresh\r\n gcTime: 5 * 60 * 1000, // 5 minutes in cache (was cacheTime)\r\n retry: 1,\r\n refetchOnWindowFocus: false, // Don't auto-refetch on window focus\r\n refetchOnReconnect: false, // Don't refetch on reconnect\r\n refetchOnMount: false, // Don't refetch on component mount (prevent loops)\r\n },\r\n mutations: {\r\n retry: 0,\r\n },\r\n },\r\n});\r\n\r\n/**\r\n * Query key factory - for cache management\r\n * React Query uses these keys to cache and invalidate queries\r\n */\r\nexport const queryKeys = {\r\n all: (table: string) => [table] as const,\r\n lists: (table: string) => [table, \"list\"] as const,\r\n list: (table: string, options?: any) => [table, \"list\", options] as const,\r\n details: (table: string) => [table, \"detail\"] as const,\r\n detail: (table: string, id: number | string) =>\r\n [table, \"detail\", id] as const,\r\n paginated: (table: string, page: number, limit: number, options?: any) =>\r\n [table, \"paginated\", page, limit, options] as const,\r\n infinite: (table: string, limit: number, options?: any) =>\r\n [table, \"infinite\", limit, options] as const,\r\n count: (table: string, options?: any) => [table, \"count\", options] as const,\r\n};\r\n\r\n/**\r\n * Manual cache utilities (rarely needed)\r\n */\r\nexport const cacheUtils = {\r\n // Invalidate all queries for a table\r\n invalidateTable: (table: string) => {\r\n return queryClient.invalidateQueries({ queryKey: queryKeys.all(table) });\r\n },\r\n\r\n // Clear all cache\r\n clearAll: () => {\r\n return queryClient.clear();\r\n },\r\n\r\n // Get cached data\r\n getCachedData: <T>(queryKey: any[]) => {\r\n return queryClient.getQueryData<T>(queryKey);\r\n },\r\n\r\n // Set cached data manually\r\n setCachedData: <T>(queryKey: any[], data: T) => {\r\n return queryClient.setQueryData<T>(queryKey, data);\r\n },\r\n};\r\n"
65
65
  },
66
66
  {
67
67
  "path": "db/react/QueryProvider.tsx",
68
68
  "type": "registry:component",
69
69
  "target": "$modules$/db/react/QueryProvider.tsx",
70
- "content": "import { QueryClientProvider } from \"@tanstack/react-query\";\nimport { queryClient } from \"./queryClient\";\n\ninterface DBQueryProviderProps {\n children: React.ReactNode;\n}\n\n/**\n * DBQueryProvider - DB module's React Query provider\n * Wraps components that use db module hooks\n */\nexport function DBQueryProvider({ children }: DBQueryProviderProps) {\n return (\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n );\n}\n"
70
+ "content": "import { QueryClientProvider } from \"@tanstack/react-query\";\r\nimport { queryClient } from \"./queryClient\";\r\n\r\ninterface DBQueryProviderProps {\r\n children: React.ReactNode;\r\n}\r\n\r\n/**\r\n * DBQueryProvider - DB module's React Query provider\r\n * Wraps components that use db module hooks\r\n */\r\nexport function DBQueryProvider({ children }: DBQueryProviderProps) {\r\n return (\r\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\r\n );\r\n}\r\n"
71
71
  },
72
72
  {
73
73
  "path": "db/react/useRepository.ts",
@@ -79,7 +79,7 @@
79
79
  "path": "db/utils/parsers.ts",
80
80
  "type": "registry:lib",
81
81
  "target": "$modules$/db/utils/parsers.ts",
82
- "content": "/**\n * Database field parsers - Client-side utilities\n * NO automatic parsing - client decides what to parse and when\n */\n\n/**\n * Parse comma-separated string to array\n * @example \"tag1,tag2,tag3\" -> [\"tag1\", \"tag2\", \"tag3\"]\n */\nexport const parseCommaSeparatedString = (value: string): string[] => {\n if (!value || typeof value !== \"string\") return [];\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n};\n\n/**\n * Parse JSON string to array\n * @example '[\"img1.jpg\",\"img2.jpg\"]' -> [\"img1.jpg\", \"img2.jpg\"]\n */\nexport const parseJSONStringToArray = (value: string): string[] => {\n if (!value || typeof value !== \"string\") return [];\n try {\n const parsed = JSON.parse(value);\n return Array.isArray(parsed) ? parsed : [];\n } catch (e) {\n console.warn(\"Failed to parse JSON array:\", value);\n return [];\n }\n};\n\n/**\n * Smart array parser - tries JSON first, falls back to comma-separated\n * @example '[\"a\",\"b\"]' -> [\"a\", \"b\"] OR \"a,b\" -> [\"a\", \"b\"]\n */\nexport const parseStringToArray = (value: any): string[] => {\n if (!value) return [];\n if (Array.isArray(value)) return value;\n if (typeof value !== \"string\") return [];\n\n // Try JSON first\n if (value.trim().startsWith(\"[\")) {\n const jsonResult = parseJSONStringToArray(value);\n if (jsonResult.length > 0) return jsonResult;\n }\n\n // Fall back to comma-separated\n return parseCommaSeparatedString(value);\n};\n\n/**\n * Parse JSON string to object\n * @example '{\"key\":\"value\"}' -> {key: \"value\"}\n */\nexport const parseJSONString = <T = any>(\n value: any,\n defaultValue: T | null = null,\n): T | null => {\n if (!value) return defaultValue;\n if (typeof value === \"object\") return value; // Already parsed\n if (typeof value !== \"string\") return defaultValue;\n\n try {\n return JSON.parse(value);\n } catch (e) {\n console.warn(\"Failed to parse JSON:\", value);\n return defaultValue;\n }\n};\n\n/**\n * Parse SQLite boolean (0/1) to JavaScript boolean\n * @example 1 -> true, 0 -> false\n */\nexport const parseSQLiteBoolean = (value: any): boolean => {\n if (typeof value === \"boolean\") return value;\n if (typeof value === \"number\") return value !== 0;\n if (typeof value === \"string\") {\n const lower = value.toLowerCase();\n return lower === \"true\" || lower === \"1\" || lower === \"yes\";\n }\n return Boolean(value);\n};\n\n/**\n * Parse number safely with default fallback\n * @example \"123\" -> 123, \"invalid\" -> 0 (or provided default)\n */\nexport const parseNumberSafe = (\n value: any,\n defaultValue: number = 0,\n): number => {\n const num = Number(value);\n return isNaN(num) ? defaultValue : num;\n};\n"
82
+ "content": "/**\r\n * Database field parsers - Client-side utilities\r\n * NO automatic parsing - client decides what to parse and when\r\n */\r\n\r\n/**\r\n * Parse comma-separated string to array\r\n * @example \"tag1,tag2,tag3\" -> [\"tag1\", \"tag2\", \"tag3\"]\r\n */\r\nexport const parseCommaSeparatedString = (value: string): string[] => {\r\n if (!value || typeof value !== \"string\") return [];\r\n return value\r\n .split(\",\")\r\n .map((item) => item.trim())\r\n .filter(Boolean);\r\n};\r\n\r\n/**\r\n * Parse JSON string to array\r\n * @example '[\"img1.jpg\",\"img2.jpg\"]' -> [\"img1.jpg\", \"img2.jpg\"]\r\n */\r\nexport const parseJSONStringToArray = (value: string): string[] => {\r\n if (!value || typeof value !== \"string\") return [];\r\n try {\r\n const parsed = JSON.parse(value);\r\n return Array.isArray(parsed) ? parsed : [];\r\n } catch (e) {\r\n console.warn(\"Failed to parse JSON array:\", value);\r\n return [];\r\n }\r\n};\r\n\r\n/**\r\n * Smart array parser - tries JSON first, falls back to comma-separated\r\n * @example '[\"a\",\"b\"]' -> [\"a\", \"b\"] OR \"a,b\" -> [\"a\", \"b\"]\r\n */\r\nexport const parseStringToArray = (value: any): string[] => {\r\n if (!value) return [];\r\n if (Array.isArray(value)) return value;\r\n if (typeof value !== \"string\") return [];\r\n\r\n // Try JSON first\r\n if (value.trim().startsWith(\"[\")) {\r\n const jsonResult = parseJSONStringToArray(value);\r\n if (jsonResult.length > 0) return jsonResult;\r\n }\r\n\r\n // Fall back to comma-separated\r\n return parseCommaSeparatedString(value);\r\n};\r\n\r\n/**\r\n * Parse JSON string to object\r\n * @example '{\"key\":\"value\"}' -> {key: \"value\"}\r\n */\r\nexport const parseJSONString = <T = any>(\r\n value: any,\r\n defaultValue: T | null = null,\r\n): T | null => {\r\n if (!value) return defaultValue;\r\n if (typeof value === \"object\") return value; // Already parsed\r\n if (typeof value !== \"string\") return defaultValue;\r\n\r\n try {\r\n return JSON.parse(value);\r\n } catch (e) {\r\n console.warn(\"Failed to parse JSON:\", value);\r\n return defaultValue;\r\n }\r\n};\r\n\r\n/**\r\n * Parse SQLite boolean (0/1) to JavaScript boolean\r\n * @example 1 -> true, 0 -> false\r\n */\r\nexport const parseSQLiteBoolean = (value: any): boolean => {\r\n if (typeof value === \"boolean\") return value;\r\n if (typeof value === \"number\") return value !== 0;\r\n if (typeof value === \"string\") {\r\n const lower = value.toLowerCase();\r\n return lower === \"true\" || lower === \"1\" || lower === \"yes\";\r\n }\r\n return Boolean(value);\r\n};\r\n\r\n/**\r\n * Parse number safely with default fallback\r\n * @example \"123\" -> 123, \"invalid\" -> 0 (or provided default)\r\n */\r\nexport const parseNumberSafe = (\r\n value: any,\r\n defaultValue: number = 0,\r\n): number => {\r\n const num = Number(value);\r\n return isNaN(num) ? defaultValue : num;\r\n};\r\n"
83
83
  }
84
84
  ],
85
85
  "exports": {
@@ -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 { 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"
22
+ "content": "import { Link } from \"react-router\";\r\nimport { ArrowRight } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { ProductCard } from \"@/modules/product-card/product-card\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useDbFeaturedProducts } from \"@/modules/ecommerce-core\";\r\nimport type { Product } from \"@/modules/ecommerce-core/types\";\r\n\r\ninterface FeaturedProductsProps {\r\n products?: Product[];\r\n loading?: boolean;\r\n}\r\n\r\nexport function FeaturedProducts({\r\n products: propProducts,\r\n loading: propLoading,\r\n}: FeaturedProductsProps) {\r\n const { t } = useTranslation(\"featured-products\");\r\n const { products: hookProducts, loading: hookLoading } = useDbFeaturedProducts();\r\n\r\n const products = propProducts ?? hookProducts;\r\n const loading = propLoading ?? hookLoading;\r\n\r\n return (\r\n <section className=\"py-8 sm:py-12 md:py-16 lg:py-20 bg-background border-t border-border/20 relative\">\r\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>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\r\n <div className=\"text-center mb-6 sm:mb-8 md:mb-12 lg:mb-16 px-2\">\r\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\">\r\n {t('title', 'Featured Products')}\r\n </h2>\r\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>\r\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\">\r\n {t('subtitle', 'Hand-picked favorites from our collection')}\r\n </p>\r\n </div>\r\n\r\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\">\r\n {loading ? (\r\n [...Array(3)].map((_, i) => (\r\n <div key={i} className=\"animate-pulse group\">\r\n <div className=\"aspect-square bg-gradient-to-br from-muted to-muted/50 rounded-2xl mb-6\"></div>\r\n <div className=\"space-y-3\">\r\n <div className=\"h-6 bg-muted rounded-lg w-3/4\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/2\"></div>\r\n <div className=\"h-5 bg-muted rounded w-2/3\"></div>\r\n </div>\r\n </div>\r\n ))\r\n ) : (\r\n products.map((product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <ProductCard\r\n product={product}\r\n variant=\"featured\"\r\n />\r\n </div>\r\n ))\r\n )}\r\n </div>\r\n\r\n <div className=\"text-center mt-8 sm:mt-12 lg:mt-16\">\r\n <Button size=\"lg\" asChild className=\"px-6 sm:px-8 py-3 sm:py-4 text-base sm:text-lg\">\r\n <Link to=\"/products\">\r\n {t('viewAll', 'View All Products')}\r\n <ArrowRight className=\"w-4 h-4 sm:w-5 sm:h-5 ml-2\" />\r\n </Link>\r\n </Button>\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\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 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"
23
+ "content": "import { useState } from \"react\";\r\nimport { Link, useNavigate } from \"react-router\";\r\nimport { ShoppingCart, Menu, Search, Heart, Package, User, LogOut } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport {\r\n Sheet,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetContent,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport {\r\n Dialog,\r\n DialogContent,\r\n DialogHeader,\r\n DialogTitle,\r\n DialogTrigger,\r\n} from \"@/components/ui/dialog\";\r\nimport {\r\n DropdownMenu,\r\n DropdownMenuContent,\r\n DropdownMenuItem,\r\n DropdownMenuLabel,\r\n DropdownMenuSeparator,\r\n DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { CartDrawer } from \"@/modules/cart-drawer\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport type { Product } from \"@/modules/ecommerce-core/types\";\r\nimport {\r\n useCart,\r\n useFavorites,\r\n useDbSearch,\r\n formatPrice,\r\n} from \"@/modules/ecommerce-core\";\r\n\r\nexport function HeaderEcommerce() {\r\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\r\n const [mobileSearchOpen, setMobileSearchOpen] = useState(false);\r\n const [desktopSearchOpen, setDesktopSearchOpen] = useState(false);\r\n const [showResults, setShowResults] = useState(false);\r\n const { itemCount, state } = useCart();\r\n const { favoriteCount } = useFavorites();\r\n const { isAuthenticated, user, logout } = useAuth();\r\n const navigate = useNavigate();\r\n const { t } = useTranslation(\"header-ecommerce\");\r\n\r\n const handleLogout = () => {\r\n logout();\r\n toast.success(t(\"logoutToastTitle\", \"Goodbye!\"), {\r\n description: t(\"logoutToastDesc\", \"You have been logged out successfully.\"),\r\n });\r\n };\r\n\r\n const {\r\n searchTerm,\r\n setSearchTerm,\r\n results: searchResults,\r\n clearSearch,\r\n } = useDbSearch();\r\n\r\n const handleSearchSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n if (searchTerm.trim()) {\r\n navigate(`/products?search=${encodeURIComponent(searchTerm)}`);\r\n setShowResults(false);\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }\r\n };\r\n\r\n const handleSearchFocus = () => {\r\n setShowResults(true);\r\n };\r\n\r\n const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n setSearchTerm(e.target.value);\r\n setShowResults(true);\r\n };\r\n\r\n const navigation = [\r\n { name: t(\"home\"), href: \"/\" },\r\n { name: t(\"products\"), href: \"/products\" },\r\n { name: t(\"about\"), href: \"/about\" },\r\n { name: t(\"contact\"), href: \"/contact\" },\r\n ];\r\n\r\n return (\r\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\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\r\n <div className=\"flex h-14 sm:h-16 md:h-20 items-center justify-between gap-2\">\r\n {/* Logo */}\r\n <div className=\"flex-shrink-0 min-w-0\">\r\n <Logo size=\"sm\" className=\"text-base sm:text-xl lg:text-2xl\" />\r\n </div>\r\n\r\n {/* Desktop Navigation - Centered */}\r\n <nav className=\"hidden lg:flex items-center space-x-12 absolute left-1/2 transform -translate-x-1/2\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-base font-medium transition-colors hover:text-primary relative group py-2\"\r\n >\r\n {item.name}\r\n <span className=\"absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full\"></span>\r\n </Link>\r\n ))}\r\n </nav>\r\n\r\n {/* Search & Actions - Right Aligned */}\r\n <div className=\"flex items-center space-x-1 sm:space-x-2 lg:space-x-4 flex-shrink-0\">\r\n {/* Desktop Search - Modal */}\r\n <Dialog\r\n open={desktopSearchOpen}\r\n onOpenChange={setDesktopSearchOpen}\r\n >\r\n <DialogTrigger asChild>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"hidden lg:flex h-10 w-10\"\r\n >\r\n <Search className=\"h-5 w-5\" />\r\n </Button>\r\n </DialogTrigger>\r\n <DialogContent className=\"sm:max-w-2xl\">\r\n <DialogHeader>\r\n <DialogTitle>\r\n {t(\"searchProducts\", \"Search Products\")}\r\n </DialogTitle>\r\n </DialogHeader>\r\n <div className=\"space-y-4\">\r\n <form onSubmit={handleSearchSubmit}>\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\r\n \"searchPlaceholder\",\r\n \"Search for products...\"\r\n )}\r\n value={searchTerm}\r\n onChange={handleSearchChange}\r\n className=\"pl-11 h-12 text-base\"\r\n autoFocus\r\n />\r\n </div>\r\n </form>\r\n\r\n {/* Desktop Search Results */}\r\n {searchTerm.trim() && (\r\n <div className=\"max-h-[400px] overflow-y-auto rounded-lg border bg-card\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"divide-y\">\r\n <div className=\"px-4 py-3 bg-muted/50\">\r\n <p className=\"text-sm font-medium text-muted-foreground\">\r\n {searchResults.length}{\" \"}\r\n {searchResults.length === 1\r\n ? \"result\"\r\n : \"results\"}{\" \"}\r\n found\r\n </p>\r\n </div>\r\n {searchResults.slice(0, 8).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"flex items-center gap-4 p-4 hover:bg-muted/50 transition-colors\"\r\n >\r\n <img\r\n src={\r\n product.images[0] || \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-16 h-16 object-cover rounded flex-shrink-0\"\r\n />\r\n <div className=\"flex-1 min-w-0\">\r\n <h4 className=\"font-medium text-base line-clamp-1\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-sm text-muted-foreground capitalize\">\r\n {product.category}\r\n </p>\r\n <p className=\"text-base font-semibold text-primary mt-1\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n {searchResults.length > 8 && (\r\n <div className=\"px-4 py-3 bg-muted/30 text-center\">\r\n <button\r\n onClick={() => {\r\n navigate(\r\n `/products?search=${encodeURIComponent(\r\n searchTerm\r\n )}`\r\n );\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"text-sm font-medium text-primary hover:underline\"\r\n >\r\n {t(\r\n \"viewAllResults\",\r\n `View all ${searchResults.length} results`\r\n )}\r\n </button>\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"p-8 text-center\">\r\n <Search className=\"h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50\" />\r\n <p className=\"text-base text-muted-foreground\">\r\n {t(\"noResults\", \"No products found\")}\r\n </p>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\r\n \"tryDifferentKeywords\",\r\n \"Try different keywords\"\r\n )}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </DialogContent>\r\n </Dialog>\r\n\r\n {/* Search - Mobile (Hidden - moved to hamburger menu) */}\r\n <Dialog open={mobileSearchOpen} onOpenChange={setMobileSearchOpen}>\r\n <DialogTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"hidden\">\r\n <Search className=\"h-4 w-4 sm:h-5 sm:w-5\" />\r\n </Button>\r\n </DialogTrigger>\r\n <DialogContent className=\"sm:max-w-md\">\r\n <DialogHeader>\r\n <DialogTitle>{t(\"searchProducts\")}</DialogTitle>\r\n </DialogHeader>\r\n <form\r\n onSubmit={(e) => {\r\n e.preventDefault();\r\n if (searchTerm.trim()) {\r\n navigate(\r\n `/products?search=${encodeURIComponent(searchTerm)}`\r\n );\r\n setMobileSearchOpen(false);\r\n clearSearch();\r\n }\r\n }}\r\n className=\"space-y-4\"\r\n >\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={(e) => setSearchTerm(e.target.value)}\r\n className=\"pl-10\"\r\n autoFocus\r\n />\r\n </div>\r\n <div className=\"flex gap-2\">\r\n <Button type=\"submit\" className=\"flex-1\">\r\n {t(\"searchButton\", \"Search\")}\r\n </Button>\r\n <Button\r\n type=\"button\"\r\n variant=\"outline\"\r\n onClick={() => {\r\n clearSearch();\r\n setMobileSearchOpen(false);\r\n }}\r\n >\r\n {t(\"cancel\", \"Cancel\")}\r\n </Button>\r\n </div>\r\n </form>\r\n\r\n {/* Mobile Search Results */}\r\n {searchTerm.trim() && (\r\n <div className=\"mt-4 max-h-64 overflow-y-auto\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"space-y-2\">\r\n <p className=\"text-sm text-muted-foreground mb-2\">\r\n {searchResults.length} result\r\n {searchResults.length !== 1 ? \"s\" : \"\"} found\r\n </p>\r\n {searchResults.slice(0, 5).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setMobileSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"block p-2 rounded hover:bg-muted/50 transition-colors\"\r\n >\r\n <div className=\"flex items-center gap-3\">\r\n <img\r\n src={\r\n product.images[0] || \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-10 h-10 object-cover rounded\"\r\n />\r\n <div className=\"flex-1\">\r\n <h4 className=\"font-medium text-sm\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {product.category}\r\n </p>\r\n <p className=\"text-sm font-medium\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n </div>\r\n ) : (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"noResults\")}\r\n </p>\r\n )}\r\n </div>\r\n )}\r\n </DialogContent>\r\n </Dialog>\r\n\r\n {/* Wishlist - Desktop Only */}\r\n <Link to=\"/favorites\" className=\"hidden lg:block\">\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"relative h-10 w-10\"\r\n >\r\n <Heart className=\"h-5 w-5\" />\r\n {favoriteCount > 0 && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\r\n >\r\n {favoriteCount}\r\n </Badge>\r\n )}\r\n </Button>\r\n </Link>\r\n\r\n {/* Cart - Desktop Only (Goes to Cart Page) */}\r\n <Link to=\"/cart\" className=\"hidden lg:block\">\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"relative h-10 w-10\"\r\n >\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {itemCount > 0 && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\r\n >\r\n {itemCount}\r\n </Badge>\r\n )}\r\n </Button>\r\n </Link>\r\n\r\n {/* Auth - Desktop Only */}\r\n <div className=\"hidden lg:flex\">\r\n {isAuthenticated ? (\r\n <DropdownMenu>\r\n <DropdownMenuTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\r\n <User className=\"h-5 w-5\" />\r\n </Button>\r\n </DropdownMenuTrigger>\r\n <DropdownMenuContent align=\"end\" className=\"w-56\">\r\n <DropdownMenuLabel className=\"font-normal\">\r\n <div className=\"flex flex-col space-y-1\">\r\n <p className=\"text-sm font-medium\">{user?.username}</p>\r\n {user?.email && (\r\n <p className=\"text-xs text-muted-foreground\">{user.email}</p>\r\n )}\r\n </div>\r\n </DropdownMenuLabel>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuItem asChild className=\"cursor-pointer\">\r\n <Link to=\"/my-orders\" className=\"flex items-center\">\r\n <Package className=\"mr-2 h-4 w-4\" />\r\n {t(\"myOrders\", \"My Orders\")}\r\n </Link>\r\n </DropdownMenuItem>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuItem\r\n onClick={handleLogout}\r\n className=\"text-red-600 focus:text-red-600 focus:bg-red-50 cursor-pointer\"\r\n >\r\n <LogOut className=\"mr-2 h-4 w-4\" />\r\n {t(\"logout\", \"Logout\")}\r\n </DropdownMenuItem>\r\n </DropdownMenuContent>\r\n </DropdownMenu>\r\n ) : (\r\n <Link to=\"/login\">\r\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\r\n <User className=\"h-5 w-5\" />\r\n </Button>\r\n </Link>\r\n )}\r\n </div>\r\n\r\n {/* Mobile Menu */}\r\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\r\n <SheetTrigger asChild>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"lg:hidden h-8 w-8 sm:h-10 sm:w-10\"\r\n >\r\n <Menu className=\"h-4 w-4 sm:h-5 sm:w-5\" />\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent side=\"right\" className=\"w-[300px] sm:w-[400px] px-6\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"menu\")}</SheetTitle>\r\n </SheetHeader>\r\n\r\n {/* Mobile Search in Hamburger */}\r\n <div className=\"mt-6 pb-4 border-b\">\r\n <form onSubmit={handleSearchSubmit}>\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={handleSearchChange}\r\n onFocus={handleSearchFocus}\r\n className=\"pl-10 h-11\"\r\n />\r\n </div>\r\n </form>\r\n\r\n {/* Search Results in Hamburger */}\r\n {showResults && searchTerm && (\r\n <div className=\"mt-3 max-h-[300px] overflow-y-auto rounded-lg border bg-card\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"divide-y\">\r\n <div className=\"px-3 py-2 bg-muted/50\">\r\n <p className=\"text-xs font-medium text-muted-foreground\">\r\n {searchResults.length}{\" \"}\r\n {searchResults.length === 1\r\n ? \"result\"\r\n : \"results\"}\r\n </p>\r\n </div>\r\n {searchResults.slice(0, 5).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setMobileMenuOpen(false);\r\n clearSearch();\r\n setShowResults(false);\r\n }}\r\n className=\"flex items-center gap-3 p-3 hover:bg-muted/50 transition-colors\"\r\n >\r\n <img\r\n src={\r\n product.images[0] || \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-14 h-14 object-cover rounded flex-shrink-0\"\r\n />\r\n <div className=\"flex-1 min-w-0\">\r\n <h4 className=\"font-medium text-sm line-clamp-1\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-xs text-muted-foreground capitalize\">\r\n {product.category}\r\n </p>\r\n <p className=\"text-sm font-semibold text-primary mt-1\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n {searchResults.length > 5 && (\r\n <div className=\"px-3 py-2 bg-muted/30 text-center\">\r\n <button\r\n onClick={() => {\r\n navigate(\r\n `/products?search=${encodeURIComponent(\r\n searchTerm\r\n )}`\r\n );\r\n setMobileMenuOpen(false);\r\n clearSearch();\r\n setShowResults(false);\r\n }}\r\n className=\"text-xs font-medium text-primary hover:underline\"\r\n >\r\n {t(\r\n \"viewAllResults\",\r\n `View all ${searchResults.length} results`\r\n )}\r\n </button>\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"p-6 text-center\">\r\n <Search className=\"h-8 w-8 text-muted-foreground mx-auto mb-2 opacity-50\" />\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"noResults\", \"No results found\")}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n\r\n <div className=\"flex flex-col space-y-4 mt-6\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n {item.name}\r\n </Link>\r\n ))}\r\n <div className=\"border-t pt-4 space-y-4\">\r\n <Link\r\n to=\"/favorites\"\r\n className=\"flex items-center justify-between text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <div className=\"flex items-center space-x-2\">\r\n <Heart className=\"h-5 w-5\" />\r\n <span>{t(\"favorites\")}</span>\r\n </div>\r\n <Badge variant=\"secondary\">{favoriteCount}</Badge>\r\n </Link>\r\n <Link\r\n to=\"/cart\"\r\n className=\"flex items-center justify-between w-full text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <div className=\"flex items-center space-x-2\">\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n <span>{t(\"cart\")}</span>\r\n </div>\r\n <div className=\"flex flex-col items-end\">\r\n <Badge variant=\"secondary\">{itemCount}</Badge>\r\n <span className=\"text-xs text-muted-foreground\">\r\n {formatPrice(state.total, constants.site.currency)}\r\n </span>\r\n </div>\r\n </Link>\r\n\r\n {/* Auth - Mobile */}\r\n {isAuthenticated ? (\r\n <div className=\"space-y-3\">\r\n <div className=\"flex items-center space-x-3 p-3 bg-muted/50 rounded-lg\">\r\n <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <User className=\"h-5 w-5 text-primary\" />\r\n </div>\r\n <div className=\"flex-1 min-w-0\">\r\n <p className=\"text-sm font-medium truncate\">{user?.username}</p>\r\n {user?.email && (\r\n <p className=\"text-xs text-muted-foreground truncate\">{user.email}</p>\r\n )}\r\n </div>\r\n </div>\r\n <Link\r\n to=\"/my-orders\"\r\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <Package className=\"h-5 w-5\" />\r\n <span>{t(\"myOrders\", \"My Orders\")}</span>\r\n </Link>\r\n <button\r\n onClick={() => {\r\n handleLogout();\r\n setMobileMenuOpen(false);\r\n }}\r\n className=\"flex items-center space-x-2 text-lg font-medium text-red-600 hover:text-red-700 transition-colors w-full\"\r\n >\r\n <LogOut className=\"h-5 w-5\" />\r\n <span>{t(\"logout\", \"Logout\")}</span>\r\n </button>\r\n </div>\r\n ) : (\r\n <Link\r\n to=\"/login\"\r\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <User className=\"h-5 w-5\" />\r\n <span>{t(\"login\", \"Login\")}</span>\r\n </Link>\r\n )}\r\n </div>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n </div>\r\n </div>\r\n </div>\r\n {/* Cart Drawer */}\r\n <CartDrawer showTrigger={false} />\r\n </header>\r\n );\r\n}\r\n"
24
24
  },
25
25
  {
26
26
  "path": "header-ecommerce/lang/en.json",