@promakeai/cli 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +71 -0
  2. package/dist/index.js +149 -156
  3. package/dist/registry/blog-core.json +7 -26
  4. package/dist/registry/blog-list-page.json +2 -2
  5. package/dist/registry/blog-section.json +1 -1
  6. package/dist/registry/cart-drawer.json +1 -1
  7. package/dist/registry/cart-page.json +1 -1
  8. package/dist/registry/category-section.json +1 -1
  9. package/dist/registry/checkout-page.json +1 -1
  10. package/dist/registry/docs/blog-core.md +12 -13
  11. package/dist/registry/docs/blog-list-page.md +1 -1
  12. package/dist/registry/docs/ecommerce-core.md +10 -13
  13. package/dist/registry/docs/featured-products.md +1 -1
  14. package/dist/registry/docs/post-detail-page.md +2 -2
  15. package/dist/registry/docs/product-detail-page.md +2 -2
  16. package/dist/registry/docs/products-page.md +1 -1
  17. package/dist/registry/ecommerce-core.json +5 -25
  18. package/dist/registry/featured-products.json +2 -2
  19. package/dist/registry/header-centered-pill.json +1 -1
  20. package/dist/registry/header-ecommerce.json +1 -1
  21. package/dist/registry/index.json +0 -1
  22. package/dist/registry/login-page.json +1 -1
  23. package/dist/registry/post-card.json +1 -1
  24. package/dist/registry/post-detail-block.json +1 -1
  25. package/dist/registry/post-detail-page.json +3 -3
  26. package/dist/registry/product-card-detailed.json +1 -1
  27. package/dist/registry/product-card.json +1 -1
  28. package/dist/registry/product-detail-block.json +1 -1
  29. package/dist/registry/product-detail-page.json +3 -3
  30. package/dist/registry/product-detail-section.json +1 -1
  31. package/dist/registry/product-quick-view.json +1 -1
  32. package/dist/registry/products-page.json +2 -2
  33. package/dist/registry/register-page.json +1 -1
  34. package/dist/registry/related-products-block.json +1 -1
  35. package/package.json +4 -2
  36. package/template/README.md +54 -73
  37. package/template/package.json +4 -3
  38. package/template/public/data/database.db +0 -0
  39. package/template/public/data/database.db-shm +0 -0
  40. package/template/public/data/database.db-wal +0 -0
  41. package/template/scripts/init-db.ts +13 -126
  42. package/template/src/App.tsx +8 -5
  43. package/template/src/db/index.ts +20 -0
  44. package/template/src/db/provider.tsx +77 -0
  45. package/template/src/db/schema.json +259 -0
  46. package/template/src/db/types.ts +195 -0
  47. package/template/src/hooks/use-debounced-value.ts +12 -0
  48. package/dist/registry/db.json +0 -129
@@ -2,26 +2,24 @@
2
2
  "name": "blog-core",
3
3
  "type": "registry:module",
4
4
  "title": "Blog Core",
5
- "description": "Complete blog state management with Zustand. Includes useBlogStore for saved/favorite posts functionality, useDbPosts hook for fetching posts with category filtering, search, and pagination. TypeScript types for Post, Category, and Author. No provider wrapping needed.",
5
+ "description": "Complete blog state management with Zustand. Includes useBlogStore for saved/favorite posts functionality. TypeScript types for Post, Category, and Author. No provider wrapping needed. Data fetching uses useDbList/useDbGet from @/db.",
6
6
  "dependencies": [
7
7
  "zustand"
8
8
  ],
9
- "registryDependencies": [
10
- "db"
11
- ],
12
- "usage": "import { useBlog, useDbPosts, useDbPostBySlug } from '@/modules/blog-core';\n\n// No provider needed - just use the hooks:\nconst { favorites, addToFavorites, isFavorite } = useBlog();\nconst { posts, loading } = useDbPosts();\n\n// Or use store directly with selectors:\nconst favorites = useBlogStore((s) => s.favorites);",
9
+ "registryDependencies": [],
10
+ "usage": "import { useBlog } from '@/modules/blog-core';\nimport { useDbList, useDbGet } from '@/db';\nimport type { Post } from '@/modules/blog-core';\n\n// Blog store (favorites, saved posts):\nconst { favorites, addToFavorites, isFavorite } = useBlog();\n\n// Data fetching via @/db hooks:\nconst { data: posts } = useDbList<Post>('posts');\nconst { data: post } = useDbGet<Post>('posts', { where: { slug } });\n\n// Or use store directly with selectors:\nconst favorites = useBlogStore((s) => s.favorites);",
13
11
  "files": [
14
12
  {
15
13
  "path": "blog-core/index.ts",
16
14
  "type": "registry:index",
17
15
  "target": "$modules$/blog-core/index.ts",
18
- "content": "// Types\r\nexport * from './types';\r\n\r\n// Store (Zustand)\r\nexport { useBlogStore, useBlog } from './stores/blog-store';\r\n\r\n// Hooks\r\nexport {\r\n useDbPosts,\r\n useDbPostBySlug,\r\n useDbFeaturedPosts,\r\n useDbRecentPosts,\r\n useDbPopularPosts,\r\n useDbPostsByCategory,\r\n useDbPostsByTag,\r\n useDbPostSearch,\r\n useDbBlogCategories,\r\n useDbPostStats\r\n} from './useDbPosts';\r\n"
16
+ "content": "// Types\r\nexport * from './types';\r\n\r\n// Store (Zustand)\r\nexport { useBlogStore, useBlog } from './stores/blog-store';\r\n"
19
17
  },
20
18
  {
21
19
  "path": "blog-core/types.ts",
22
20
  "type": "registry:type",
23
21
  "target": "$modules$/blog-core/types.ts",
24
- "content": "export interface PostCategory {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n is_primary: boolean;\r\n}\r\n\r\nexport interface Post {\r\n id: number;\r\n title: string;\r\n slug: string;\r\n content: string;\r\n excerpt: string;\r\n featured_image: string;\r\n images: string[];\r\n category: string; // Primary category slug (backward compatibility)\r\n category_name?: string; // Primary category name (backward compatibility)\r\n categories: PostCategory[]; // Multi-category support\r\n author: string;\r\n author_avatar: string;\r\n published_at: string;\r\n updated_at: string;\r\n tags: string[];\r\n read_time: number;\r\n view_count: number;\r\n featured: boolean;\r\n published: boolean;\r\n meta_description: string;\r\n meta_keywords: string;\r\n}\r\n\r\nexport interface Author {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n bio?: string;\r\n avatar?: string;\r\n social?: {\r\n twitter?: string;\r\n linkedin?: string;\r\n github?: string;\r\n website?: string;\r\n };\r\n}\r\n\r\nexport interface Comment {\r\n id: number;\r\n post_id: number;\r\n author_name: string;\r\n author_email: string;\r\n content: string;\r\n created_at: string;\r\n approved: boolean;\r\n}\r\n\r\nexport interface BlogCategory {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n description?: string;\r\n image?: string;\r\n}\r\n\r\nexport interface BlogContextType {\r\n favorites: Post[];\r\n addToFavorites: (post: Post) => void;\r\n removeFromFavorites: (postId: string) => void;\r\n isFavorite: (postId: string) => boolean;\r\n clearFavorites: () => void;\r\n}\r\n\r\nexport interface BlogSettings {\r\n site: {\r\n name: string;\r\n description: string;\r\n url: string;\r\n logo?: string;\r\n };\r\n author: {\r\n name: string;\r\n bio?: string;\r\n avatar?: string;\r\n };\r\n social: {\r\n twitter?: string;\r\n linkedin?: string;\r\n github?: string;\r\n facebook?: string;\r\n };\r\n comments: {\r\n enabled: boolean;\r\n moderation: boolean;\r\n };\r\n newsletter: {\r\n enabled: boolean;\r\n provider?: string;\r\n };\r\n}\r\n"
22
+ "content": "export interface Post {\r\n id: number;\r\n title: string;\r\n slug: string;\r\n content: string;\r\n excerpt: string;\r\n featured_image: string;\r\n images: string[];\r\n categories: number[];\r\n author: string;\r\n author_avatar: string;\r\n published_at: string;\r\n updated_at: string;\r\n tags: string[];\r\n read_time: number;\r\n view_count: number;\r\n featured: boolean;\r\n published: boolean;\r\n meta_description: string;\r\n meta_keywords: string;\r\n}\r\n\r\nexport interface Author {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n bio?: string;\r\n avatar?: string;\r\n social?: {\r\n twitter?: string;\r\n linkedin?: string;\r\n github?: string;\r\n website?: string;\r\n };\r\n}\r\n\r\nexport interface Comment {\r\n id: number;\r\n post_id: number;\r\n author_name: string;\r\n author_email: string;\r\n content: string;\r\n created_at: string;\r\n approved: boolean;\r\n}\r\n\r\nexport interface BlogCategory {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n description?: string;\r\n image?: string;\r\n}\r\n\r\nexport interface BlogContextType {\r\n favorites: Post[];\r\n addToFavorites: (post: Post) => void;\r\n removeFromFavorites: (postId: string) => void;\r\n isFavorite: (postId: string) => boolean;\r\n clearFavorites: () => void;\r\n}\r\n\r\nexport interface BlogSettings {\r\n site: {\r\n name: string;\r\n description: string;\r\n url: string;\r\n logo?: string;\r\n };\r\n author: {\r\n name: string;\r\n bio?: string;\r\n avatar?: string;\r\n };\r\n social: {\r\n twitter?: string;\r\n linkedin?: string;\r\n github?: string;\r\n facebook?: string;\r\n };\r\n comments: {\r\n enabled: boolean;\r\n moderation: boolean;\r\n };\r\n newsletter: {\r\n enabled: boolean;\r\n provider?: string;\r\n };\r\n}\r\n"
25
23
  },
26
24
  {
27
25
  "path": "blog-core/stores/blog-store.ts",
@@ -29,12 +27,6 @@
29
27
  "target": "$modules$/blog-core/stores/blog-store.ts",
30
28
  "content": "import { create } from \"zustand\";\r\nimport { persist } from \"zustand/middleware\";\r\nimport type { Post, BlogContextType } from \"../types\";\r\n\r\ninterface BlogStore {\r\n favorites: Post[];\r\n addToFavorites: (post: Post) => void;\r\n removeFromFavorites: (postId: string) => void;\r\n isFavorite: (postId: string) => boolean;\r\n clearFavorites: () => void;\r\n}\r\n\r\nexport const useBlogStore = create<BlogStore>()(\r\n persist(\r\n (set, get) => ({\r\n favorites: [],\r\n\r\n addToFavorites: (post) =>\r\n set((state) => {\r\n if (state.favorites.some((p) => p.id === post.id)) {\r\n return state;\r\n }\r\n return { favorites: [...state.favorites, post] };\r\n }),\r\n\r\n removeFromFavorites: (postId) =>\r\n set((state) => ({\r\n favorites: state.favorites.filter((p) => String(p.id) !== postId),\r\n })),\r\n\r\n isFavorite: (postId) => {\r\n return get().favorites.some((p) => String(p.id) === postId);\r\n },\r\n\r\n clearFavorites: () => set({ favorites: [] }),\r\n }),\r\n { name: \"blog_favorites\" }\r\n )\r\n);\r\n\r\n// Backward compatible hook - matches BlogContextType\r\nexport const useBlog = (): BlogContextType => {\r\n const store = useBlogStore();\r\n return {\r\n favorites: store.favorites,\r\n addToFavorites: store.addToFavorites,\r\n removeFromFavorites: store.removeFromFavorites,\r\n isFavorite: store.isFavorite,\r\n clearFavorites: store.clearFavorites,\r\n };\r\n};\r\n"
31
29
  },
32
- {
33
- "path": "blog-core/useDbPosts.ts",
34
- "type": "registry:hook",
35
- "target": "$modules$/blog-core/useDbPosts.ts",
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
- },
38
30
  {
39
31
  "path": "blog-core/lang/en.json",
40
32
  "type": "registry:lang",
@@ -55,22 +47,11 @@
55
47
  "BlogContextType",
56
48
  "BlogSettings",
57
49
  "Comment",
58
- "Post",
59
- "PostCategory"
50
+ "Post"
60
51
  ],
61
52
  "variables": [
62
53
  "useBlog",
63
- "useBlogStore",
64
- "useDbBlogCategories",
65
- "useDbFeaturedPosts",
66
- "useDbPopularPosts",
67
- "useDbPostBySlug",
68
- "useDbPostSearch",
69
- "useDbPostStats",
70
- "useDbPosts",
71
- "useDbPostsByCategory",
72
- "useDbPostsByTag",
73
- "useDbRecentPosts"
54
+ "useBlogStore"
74
55
  ]
75
56
  }
76
57
  }
@@ -8,7 +8,7 @@
8
8
  "post-card",
9
9
  "animations"
10
10
  ],
11
- "usage": "import { BlogListPage } from '@/modules/blog-list-page';\n\n<Route path=\"/blog\" element={<BlogListPage />} />\n\n• Uses useDbPosts() from blog-core (Zustand)\n• Features: category tabs, search, grid/list view\n• Sidebar: popular posts, categories, newsletter",
11
+ "usage": "import { BlogListPage } from '@/modules/blog-list-page';\n\n<Route path=\"/blog\" element={<BlogListPage />} />\n\n• Uses useDbList() from @/db for post fetching\n• Features: category tabs, search, grid/list view\n• Sidebar: popular posts, categories, newsletter",
12
12
  "route": {
13
13
  "path": "/blog",
14
14
  "componentName": "BlogListPage"
@@ -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\";\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"
27
+ "content": "import { useState, useEffect, useMemo } 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 { useDbList } from \"@/db\";\r\nimport type { Post, BlogCategory } from \"@/modules/blog-core/types\";\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 { data: posts = [], isLoading: loading, error } = useDbList<Post>(\"posts\", {\r\n where: { published: 1 },\r\n });\r\n const { data: categories = [] } = useDbList<BlogCategory>(\"blog_categories\");\r\n\r\n const selectedCategoryIds = useMemo(() => {\r\n if (selectedCategories.length === 0) return new Set<number>();\r\n return new Set(\r\n categories.filter(c => selectedCategories.includes(c.slug)).map(c => c.id)\r\n );\r\n }, [selectedCategories, categories]);\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 = post.categories?.some(\r\n (id) => selectedCategoryIds.has(id)\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",
@@ -21,7 +21,7 @@
21
21
  "path": "blog-section/blog-section.tsx",
22
22
  "type": "registry:component",
23
23
  "target": "$modules$/blog-section/blog-section.tsx",
24
- "content": "import { Link } from \"react-router\";\r\nimport { ArrowRight } from \"lucide-react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardFooter,\r\n CardHeader,\r\n} from \"@/components/ui/card\";\r\nimport { useDbRecentPosts } from \"@/modules/blog-core\";\r\nimport type { Post } from \"@/modules/blog-core/types\";\r\n\r\ninterface BlogSectionProps {\r\n posts?: Post[];\r\n loading?: boolean;\r\n className?: string;\r\n}\r\n\r\nexport function BlogSection({\r\n posts: propPosts,\r\n loading: propLoading,\r\n className,\r\n}: BlogSectionProps) {\r\n const { t } = useTranslation(\"blog-section\");\r\n const { posts: hookPosts, loading: hookLoading } = useDbRecentPosts(3);\r\n\r\n const posts = propPosts ?? hookPosts;\r\n const loading = propLoading ?? hookLoading;\r\n\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Header */}\r\n <div className=\"text-center mb-12\">\r\n <Badge variant=\"secondary\" className=\"mb-4\">\r\n {t(\"tagline\", \"Latest Updates\")}\r\n </Badge>\r\n <h2 className=\"text-3xl font-bold md:text-4xl lg:text-5xl mb-4\">\r\n {t(\"title\", \"From Our Blog\")}\r\n </h2>\r\n <p className=\"text-muted-foreground max-w-2xl mx-auto mb-6\">\r\n {t(\r\n \"subtitle\",\r\n \"Discover the latest trends, tips, and insights from our team of experts.\"\r\n )}\r\n </p>\r\n <Button variant=\"link\" asChild>\r\n <Link to=\"/blog\">\r\n {t(\"viewAll\", \"View all articles\")}\r\n <ArrowRight className=\"ml-2 h-4 w-4\" />\r\n </Link>\r\n </Button>\r\n </div>\r\n\r\n {/* Posts Grid */}\r\n <div className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto\">\r\n {loading ? (\r\n [...Array(3)].map((_, i) => (\r\n <Card key={i} className=\"overflow-hidden p-0 animate-pulse\">\r\n <div className=\"aspect-video bg-muted\"></div>\r\n <CardHeader className=\"pt-6 pb-2\">\r\n <div className=\"flex items-center gap-2 mb-2\">\r\n <div className=\"h-5 w-16 bg-muted rounded\"></div>\r\n <div className=\"h-4 w-20 bg-muted rounded\"></div>\r\n </div>\r\n <div className=\"h-6 w-3/4 bg-muted rounded\"></div>\r\n </CardHeader>\r\n <CardContent className=\"py-0\">\r\n <div className=\"h-4 w-full bg-muted rounded mb-2\"></div>\r\n <div className=\"h-4 w-2/3 bg-muted rounded\"></div>\r\n </CardContent>\r\n <CardFooter className=\"pb-2\">\r\n <div className=\"h-4 w-24 bg-muted rounded\"></div>\r\n </CardFooter>\r\n </Card>\r\n ))\r\n ) : (\r\n posts.map((post) => (\r\n <div key={post.id} className=\"contents\" data-db-table=\"posts\" data-db-id={post.id}>\r\n <Card className=\"overflow-hidden group p-0\">\r\n <div className=\"aspect-video overflow-hidden\">\r\n <Link to={`/blog/${post.slug}`}>\r\n <img\r\n src={post.featured_image || \"/images/placeholder.png\"}\r\n alt={post.title}\r\n className=\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\"\r\n onError={(e) => {\r\n e.currentTarget.style.display = \"none\";\r\n }}\r\n />\r\n </Link>\r\n </div>\r\n <CardHeader className=\"pt-6 pb-2\">\r\n <div className=\"flex items-center gap-2 mb-2\">\r\n <Badge variant=\"outline\" className=\"text-xs\">\r\n {post.category_name || post.category}\r\n </Badge>\r\n <span className=\"text-xs text-muted-foreground\">\r\n {new Date(post.published_at).toLocaleDateString()}\r\n </span>\r\n </div>\r\n <Link to={`/blog/${post.slug}`}>\r\n <h3 className=\"text-lg font-semibold hover:text-primary transition-colors line-clamp-2\">\r\n {post.title}\r\n </h3>\r\n </Link>\r\n </CardHeader>\r\n <CardContent className=\"py-0\">\r\n <p className=\"text-sm text-muted-foreground line-clamp-2\">\r\n {post.excerpt}\r\n </p>\r\n </CardContent>\r\n <CardFooter className=\"pb-2\">\r\n <Link\r\n to={`/blog/${post.slug}`}\r\n className=\"text-sm font-medium text-primary hover:underline inline-flex items-center whitespace-nowrap\"\r\n >\r\n {t(\"readMore\", \"Read more\")}\r\n <ArrowRight className=\"ml-1 h-3 w-3 shrink-0\" />\r\n </Link>\r\n </CardFooter>\r\n </Card>\r\n </div>\r\n ))\r\n )}\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
24
+ "content": "import { useMemo } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { ArrowRight } from \"lucide-react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardFooter,\r\n CardHeader,\r\n} from \"@/components/ui/card\";\r\nimport { useDbList } from \"@/db\";\r\nimport type { Post, BlogCategory } from \"@/modules/blog-core/types\";\r\n\r\ninterface BlogSectionProps {\r\n posts?: Post[];\r\n loading?: boolean;\r\n className?: string;\r\n}\r\n\r\nexport function BlogSection({\r\n posts: propPosts,\r\n loading: propLoading,\r\n className,\r\n}: BlogSectionProps) {\r\n const { t } = useTranslation(\"blog-section\");\r\n const { data: blogCategories = [] } = useDbList<BlogCategory>(\"blog_categories\");\r\n const categoryMap = useMemo(() => new Map(blogCategories.map(c => [c.id, c])), [blogCategories]);\r\n const { data: hookPosts = [], isLoading: hookLoading } = useDbList<Post>(\"posts\", {\r\n where: { published: 1 },\r\n orderBy: [{ field: \"published_at\", direction: \"DESC\" }],\r\n limit: 3,\r\n });\r\n\r\n const posts = propPosts ?? hookPosts;\r\n const loading = propLoading ?? hookLoading;\r\n\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Header */}\r\n <div className=\"text-center mb-12\">\r\n <Badge variant=\"secondary\" className=\"mb-4\">\r\n {t(\"tagline\", \"Latest Updates\")}\r\n </Badge>\r\n <h2 className=\"text-3xl font-bold md:text-4xl lg:text-5xl mb-4\">\r\n {t(\"title\", \"From Our Blog\")}\r\n </h2>\r\n <p className=\"text-muted-foreground max-w-2xl mx-auto mb-6\">\r\n {t(\r\n \"subtitle\",\r\n \"Discover the latest trends, tips, and insights from our team of experts.\"\r\n )}\r\n </p>\r\n <Button variant=\"link\" asChild>\r\n <Link to=\"/blog\">\r\n {t(\"viewAll\", \"View all articles\")}\r\n <ArrowRight className=\"ml-2 h-4 w-4\" />\r\n </Link>\r\n </Button>\r\n </div>\r\n\r\n {/* Posts Grid */}\r\n <div className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto\">\r\n {loading ? (\r\n [...Array(3)].map((_, i) => (\r\n <Card key={i} className=\"overflow-hidden p-0 animate-pulse\">\r\n <div className=\"aspect-video bg-muted\"></div>\r\n <CardHeader className=\"pt-6 pb-2\">\r\n <div className=\"flex items-center gap-2 mb-2\">\r\n <div className=\"h-5 w-16 bg-muted rounded\"></div>\r\n <div className=\"h-4 w-20 bg-muted rounded\"></div>\r\n </div>\r\n <div className=\"h-6 w-3/4 bg-muted rounded\"></div>\r\n </CardHeader>\r\n <CardContent className=\"py-0\">\r\n <div className=\"h-4 w-full bg-muted rounded mb-2\"></div>\r\n <div className=\"h-4 w-2/3 bg-muted rounded\"></div>\r\n </CardContent>\r\n <CardFooter className=\"pb-2\">\r\n <div className=\"h-4 w-24 bg-muted rounded\"></div>\r\n </CardFooter>\r\n </Card>\r\n ))\r\n ) : (\r\n posts.map((post) => (\r\n <div key={post.id} className=\"contents\" data-db-table=\"posts\" data-db-id={post.id}>\r\n <Card className=\"overflow-hidden group p-0\">\r\n <div className=\"aspect-video overflow-hidden\">\r\n <Link to={`/blog/${post.slug}`}>\r\n <img\r\n src={post.featured_image || \"/images/placeholder.png\"}\r\n alt={post.title}\r\n className=\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\"\r\n onError={(e) => {\r\n e.currentTarget.style.display = \"none\";\r\n }}\r\n />\r\n </Link>\r\n </div>\r\n <CardHeader className=\"pt-6 pb-2\">\r\n <div className=\"flex items-center gap-2 mb-2\">\r\n <Badge variant=\"outline\" className=\"text-xs\">\r\n {categoryMap.get(post.categories?.[0] as number)?.name}\r\n </Badge>\r\n <span className=\"text-xs text-muted-foreground\">\r\n {new Date(post.published_at).toLocaleDateString()}\r\n </span>\r\n </div>\r\n <Link to={`/blog/${post.slug}`}>\r\n <h3 className=\"text-lg font-semibold hover:text-primary transition-colors line-clamp-2\">\r\n {post.title}\r\n </h3>\r\n </Link>\r\n </CardHeader>\r\n <CardContent className=\"py-0\">\r\n <p className=\"text-sm text-muted-foreground line-clamp-2\">\r\n {post.excerpt}\r\n </p>\r\n </CardContent>\r\n <CardFooter className=\"pb-2\">\r\n <Link\r\n to={`/blog/${post.slug}`}\r\n className=\"text-sm font-medium text-primary hover:underline inline-flex items-center whitespace-nowrap\"\r\n >\r\n {t(\"readMore\", \"Read more\")}\r\n <ArrowRight className=\"ml-1 h-3 w-3 shrink-0\" />\r\n </Link>\r\n </CardFooter>\r\n </Card>\r\n </div>\r\n ))\r\n )}\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
25
25
  },
26
26
  {
27
27
  "path": "blog-section/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\";\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"
23
+ "content": "import { useMemo } from \"react\";\r\nimport { 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 type { Category } from \"@/modules/ecommerce-core/types\";\r\nimport { useDbList } from \"@/db\";\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 { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\r\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\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?.length ? 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 {categoryMap.get(item.product.categories?.[0] as number)?.name && (\r\n <p className=\"mt-1 text-sm text-muted-foreground\">\r\n {categoryMap.get(item.product.categories?.[0] as number)?.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\";\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"
26
+ "content": "import { useMemo } from \"react\";\r\nimport { 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 type { Category } from \"@/modules/ecommerce-core/types\";\r\nimport { useDbList } from \"@/db\";\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 const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\r\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\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?.length ? 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 {categoryMap.get(item.product.categories?.[0] as number)?.name}\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\";\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"
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 { useDbList, type DbProductCategory } from \"@/db\";\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 { data: hookCategories = [], isLoading: hookLoading } = useDbList<DbProductCategory>(\"product_categories\");\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",
@@ -24,7 +24,7 @@
24
24
  "path": "checkout-page/checkout-page.tsx",
25
25
  "type": "registry:page",
26
26
  "target": "$modules$/checkout-page/checkout-page.tsx",
27
- "content": "import { useState, useEffect } from \"react\";\nimport { Link } from \"react-router\";\nimport { ArrowLeft, CreditCard, Banknote, Truck, Check } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { toast } from \"sonner\";\nimport {\n useCart,\n formatPrice,\n type PaymentMethod,\n type OnlinePaymentProvider,\n getFilteredPaymentMethodConfigs,\n getOnlinePaymentProviders,\n ONLINE_PROVIDER_CONFIGS,\n} from \"@/modules/ecommerce-core\";\nimport { customerClient, getErrorMessage } from \"@/modules/api\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn } from \"@/modules/animations\";\n\ninterface Country {\n value: string;\n label: string;\n}\n\ninterface CheckoutFormData {\n firstName: string;\n lastName: string;\n email: string;\n phone: string;\n address: string;\n city: string;\n postalCode: string;\n country: string;\n notes: string;\n}\n\ninterface BankTransferInfo {\n bank_name: string;\n bank_account_name: string;\n iban: string;\n}\n\nconst DEFAULT_COUNTRIES: Country[] = [\n { value: \"US\", label: \"United States\" },\n { value: \"GB\", label: \"United Kingdom\" },\n { value: \"CA\", label: \"Canada\" },\n { value: \"AU\", label: \"Australia\" },\n { value: \"DE\", label: \"Germany\" },\n { value: \"FR\", label: \"France\" },\n { value: \"IT\", label: \"Italy\" },\n { value: \"ES\", label: \"Spain\" },\n { value: \"NL\", label: \"Netherlands\" },\n { value: \"TR\", label: \"Turkey\" },\n { value: \"JP\", label: \"Japan\" },\n];\n\nexport function CheckoutPage() {\n const { t } = useTranslation(\"checkout-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Checkout\") });\n const { state, clearCart } = useCart();\n const { items, total } = state;\n\n const currency = (constants as any).site?.currency || \"USD\";\n const taxRate = (constants as any).payments?.taxRate || 0;\n const freeShippingThreshold = (constants as any).payments?.freeShippingThreshold || 0;\n const shippingCost = (constants as any).shipping?.domesticShipping?.standard?.cost || 0;\n\n // Calculate shipping and tax\n const shipping = total >= freeShippingThreshold ? 0 : shippingCost;\n const tax = total * taxRate;\n\n const countries = DEFAULT_COUNTRIES;\n\n // Get available payment methods and providers from config\n const availablePaymentMethods = getFilteredPaymentMethodConfigs();\n const availableProviders = getOnlinePaymentProviders();\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 [paymentMethod, setPaymentMethod] = useState<PaymentMethod>(\n availablePaymentMethods[0]?.id || \"card\"\n );\n const [selectedProvider, setSelectedProvider] = useState<OnlinePaymentProvider>(\n availableProviders[0] || \"stripe\"\n );\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [formData, setFormData] = useState<CheckoutFormData>({\n firstName: \"\",\n lastName: \"\",\n email: \"\",\n phone: \"\",\n address: \"\",\n city: \"\",\n postalCode: \"\",\n country: \"\",\n notes: \"\",\n });\n const [agreedToTerms, setAgreedToTerms] = useState(false);\n\n // Bank transfer info state\n const [bankInfo, setBankInfo] = useState<BankTransferInfo | null>(null);\n const [isBankInfoLoading, setIsBankInfoLoading] = useState(false);\n const [bankInfoError, setBankInfoError] = useState<string | null>(null);\n\n const finalTotal = total + shipping + tax;\n\n // Fetch bank info when transfer is selected\n useEffect(() => {\n if (paymentMethod === \"transfer\") {\n const fetchBankInfo = async () => {\n setIsBankInfoLoading(true);\n setBankInfoError(null);\n try {\n const info = await customerClient.payment.getBankTransferInfo();\n setBankInfo(info);\n } catch (err: any) {\n setBankInfoError(\n err.message || t(\"bankInfoError\", \"Failed to load bank information\")\n );\n } finally {\n setIsBankInfoLoading(false);\n }\n };\n fetchBankInfo();\n }\n }, [paymentMethod, t]);\n\n const handleInputChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n const { name, value } = e.target;\n setFormData((prev) => ({ ...prev, [name]: value }));\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!agreedToTerms) {\n toast.error(t(\"agreeToTermsError\", \"Please agree to the terms and conditions\"));\n return;\n }\n\n setIsSubmitting(true);\n setError(null);\n\n try {\n // Determine payment type based on selection\n let paymentType: \"stripe\" | \"iyzico\" | \"bank_transfer\" | \"cash_on_delivery\";\n\n if (paymentMethod === \"card\") {\n paymentType = selectedProvider;\n } else if (paymentMethod === \"transfer\") {\n paymentType = \"bank_transfer\";\n } else {\n paymentType = \"cash_on_delivery\";\n }\n\n // Save checkout data to localStorage for success page\n const checkoutData = {\n items: items,\n total: finalTotal,\n customerInfo: formData,\n paymentMethod,\n paymentProvider: paymentType,\n };\n localStorage.setItem(\"pending_checkout\", JSON.stringify(checkoutData));\n\n // Build product data for checkout\n const productData = items.map((item) => {\n const price = getProductPrice(item.product);\n const qty = item.quantity || 1;\n\n return {\n quantity: qty,\n name: item.product.name || \"Product\",\n description: item.product.description || item.product.name || \"Product\",\n amount: Math.round(price * 100), // Convert to cents\n img: item.product.images?.[0] || \"/images/placeholder.png\",\n optionals: {\n productId: item.product.id,\n },\n };\n });\n\n // Tax amount in cents\n const taxAmountInCents = tax && !isNaN(tax) ? Math.round(tax * 100) : undefined;\n\n // Create checkout session\n const response = await customerClient.payment.createCheckout({\n currency: currency.toLowerCase(),\n taxAmount: taxAmountInCents,\n paymentType: paymentType,\n productData,\n contactData: {\n firstname: formData.firstName,\n lastname: formData.lastName,\n email: formData.email,\n phone: formData.phone,\n },\n shippingData: {\n address: formData.address,\n country: formData.country,\n city: formData.city,\n zip: formData.postalCode,\n },\n });\n\n // Clear cart and redirect to payment URL or confirmation page\n clearCart();\n if (response.url) {\n window.location.href = response.url;\n } else {\n window.location.href = `/order-confirmation?session_id=${response.sessionId}`;\n }\n } catch (err) {\n const errorMessage = getErrorMessage(err, t(\"orderError\", \"Failed to place order. Please try again.\"));\n setError(errorMessage);\n toast.error(t(\"orderErrorTitle\", \"Order Failed\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // Get icon component based on payment method\n const getPaymentIcon = (iconName: string) => {\n switch (iconName) {\n case \"CreditCard\":\n return CreditCard;\n case \"Banknote\":\n return Banknote;\n case \"Truck\":\n return Truck;\n default:\n return CreditCard;\n }\n };\n\n // Get icon color based on payment method\n const getIconColor = (methodId: string) => {\n switch (methodId) {\n case \"card\":\n return \"text-blue-600\";\n case \"transfer\":\n return \"text-primary\";\n case \"cash\":\n return \"text-green-600 dark:text-green-400\";\n default:\n return \"text-primary\";\n }\n };\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\">\n <h1 className=\"text-3xl font-bold mb-4\">\n {t(\"cartEmpty\", \"Your cart is empty\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\n \"cartEmptyDescription\",\n \"Please add items to your cart before proceeding to checkout.\"\n )}\n </p>\n <Button asChild>\n <Link to=\"/products\">\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\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=\"/cart\">\n <ArrowLeft className=\"h-4 w-4\" />\n </Link>\n </Button>\n <div>\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Checkout\")}</h1>\n <p className=\"text-muted-foreground\">\n {t(\"completeOrder\", \"Complete your order\")}\n </p>\n </div>\n </FadeIn>\n\n <form onSubmit={handleSubmit}>\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n <div className=\"lg:col-span-2 space-y-6\">\n {/* Contact Information */}\n <FadeIn delay={0.1}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"contactInformation\", \"Contact Information\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"firstName\">\n {t(\"firstName\", \"First Name\")} *\n </Label>\n <Input\n id=\"firstName\"\n name=\"firstName\"\n value={formData.firstName}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"lastName\">\n {t(\"lastName\", \"Last Name\")} *\n </Label>\n <Input\n id=\"lastName\"\n name=\"lastName\"\n value={formData.lastName}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"email\">\n {t(\"email\", \"Email Address\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"phone\">\n {t(\"phone\", \"Phone Number\")} *\n </Label>\n <Input\n id=\"phone\"\n name=\"phone\"\n type=\"tel\"\n value={formData.phone}\n onChange={handleInputChange}\n required\n />\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Shipping Address */}\n <FadeIn delay={0.2}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"shippingAddress\", \"Shipping Address\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"address\">{t(\"address\", \"Address\")} *</Label>\n <Textarea\n id=\"address\"\n name=\"address\"\n value={formData.address}\n onChange={handleInputChange}\n placeholder={t(\n \"addressPlaceholder\",\n \"Street address, apartment, suite, etc.\"\n )}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"country\">{t(\"country\", \"Country\")} *</Label>\n <Select\n value={formData.country}\n onValueChange={(value) =>\n setFormData((prev) => ({ ...prev, country: value }))\n }\n required\n >\n <SelectTrigger id=\"country\">\n <SelectValue\n placeholder={t(\"selectCountry\", \"Select a country\")}\n />\n </SelectTrigger>\n <SelectContent>\n {countries.map((country) => (\n <SelectItem key={country.value} value={country.value}>\n {country.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"city\">{t(\"city\", \"City\")} *</Label>\n <Input\n id=\"city\"\n name=\"city\"\n value={formData.city}\n onChange={handleInputChange}\n required\n />\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"postalCode\">\n {t(\"postalCode\", \"Postal Code\")} *\n </Label>\n <Input\n id=\"postalCode\"\n name=\"postalCode\"\n value={formData.postalCode}\n onChange={handleInputChange}\n required\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Payment Method */}\n <FadeIn delay={0.3}>\n <Card>\n <CardHeader>\n <CardTitle>{t(\"paymentMethod\", \"Payment Method\")}</CardTitle>\n </CardHeader>\n <CardContent>\n <RadioGroup\n value={paymentMethod}\n onValueChange={(value) =>\n setPaymentMethod(value as PaymentMethod)\n }\n className=\"space-y-4\"\n >\n {availablePaymentMethods.map((method) => {\n const IconComponent = getPaymentIcon(method.icon);\n const iconColor = getIconColor(method.id);\n\n return (\n <div\n key={method.id}\n className=\"flex items-center space-x-2 p-4 border rounded-lg\"\n >\n <RadioGroupItem value={method.id} id={method.id} />\n <Label\n htmlFor={method.id}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"flex items-center gap-3\">\n <IconComponent\n className={`h-5 w-5 ${iconColor}`}\n />\n <div>\n <div className=\"font-medium\">\n {t(method.id, method.label)}\n </div>\n <div className=\"text-sm text-muted-foreground\">\n {t(`${method.id}Description`, method.description)}\n </div>\n </div>\n </div>\n </Label>\n </div>\n );\n })}\n </RadioGroup>\n\n {/* Bank Transfer Details */}\n {paymentMethod === \"transfer\" && (\n <div className=\"mt-4 p-4 bg-primary/10 rounded-lg border border-primary/20\">\n <h4 className=\"font-medium mb-2\">\n {t(\"bankTransferDetailsTitle\", \"Bank Transfer Details\")}:\n </h4>\n {isBankInfoLoading ? (\n <div className=\"text-sm space-y-2\">\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-full\" />\n </div>\n ) : bankInfoError ? (\n <p className=\"text-sm text-red-600\">{bankInfoError}</p>\n ) : bankInfo ? (\n <div className=\"text-sm space-y-1\">\n <p>\n <strong>{t(\"bank\", \"Bank\")}:</strong> {bankInfo.bank_name}\n </p>\n <p>\n <strong>{t(\"accountName\", \"Account Name\")}:</strong>{\" \"}\n {bankInfo.bank_account_name}\n </p>\n <p>\n <strong>IBAN:</strong> {bankInfo.iban}\n </p>\n </div>\n ) : (\n <p className=\"text-sm text-muted-foreground\">\n {t(\"bankInfoNotAvailable\", \"Bank account information not available\")}\n </p>\n )}\n </div>\n )}\n\n {/* Card Payment - Provider Selection */}\n {paymentMethod === \"card\" && availableProviders.length > 1 && (\n <div className=\"mt-4 space-y-4\">\n <div className=\"p-4 bg-blue-50 dark:bg-blue-950/30 rounded-lg border border-blue-200 dark:border-blue-800\">\n <h4 className=\"font-medium text-blue-900 dark:text-blue-100 mb-3\">\n {t(\"selectPaymentProvider\", \"Select Payment Provider\")}\n </h4>\n <RadioGroup\n value={selectedProvider}\n onValueChange={(value) =>\n setSelectedProvider(value as OnlinePaymentProvider)\n }\n className=\"space-y-2\"\n >\n {availableProviders.map((provider) => (\n <div\n key={provider}\n className=\"flex items-center space-x-2 p-3 bg-background rounded border\"\n >\n <RadioGroupItem\n value={provider}\n id={`provider-${provider}`}\n />\n <Label\n htmlFor={`provider-${provider}`}\n className=\"flex-1 cursor-pointer\"\n >\n <div className=\"font-medium\">\n {t(`provider_${provider}_label`, ONLINE_PROVIDER_CONFIGS[provider].label)}\n </div>\n <div className=\"text-xs text-muted-foreground\">\n {t(`provider_${provider}_description`, ONLINE_PROVIDER_CONFIGS[provider].description)}\n </div>\n </Label>\n </div>\n ))}\n </RadioGroup>\n <p className=\"text-sm text-blue-700 dark:text-blue-300 mt-3\">\n {t(\n \"creditCardRedirectNote\",\n \"You will be redirected to the secure payment page to complete your purchase.\"\n )}\n </p>\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Order Notes */}\n <FadeIn delay={0.4}>\n <Card>\n <CardHeader>\n <CardTitle>\n {t(\"orderNotesOptional\", \"Order Notes (Optional)\")}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Textarea\n name=\"notes\"\n value={formData.notes}\n onChange={handleInputChange}\n placeholder={t(\n \"orderNotesPlaceholder\",\n \"Special instructions for your order...\"\n )}\n rows={3}\n />\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n\n {/* Order Summary */}\n <FadeIn delay={0.2} className=\"lg:col-span-1\">\n <Card className=\"sticky top-24\">\n <CardHeader>\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"space-y-3\">\n {items.map((item) => (\n <div key={item.id} className=\"flex gap-3\">\n <img\n src={\n item.product.images?.[0] ||\n \"/images/placeholder.png\"\n }\n alt={item.product.name}\n className=\"w-12 h-12 object-cover rounded\"\n />\n <div className=\"flex-1 space-y-1\">\n <h4 className=\"text-sm font-medium leading-normal\">\n {item.product.name}\n </h4>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-muted-foreground\">\n {t(\"qty\", \"Qty\")}: {item.quantity}\n </span>\n <span>\n {formatPrice(\n getProductPrice(item.product) * item.quantity,\n currency\n )}\n </span>\n </div>\n </div>\n </div>\n ))}\n </div>\n\n <Separator />\n\n <div className=\"space-y-2\">\n <div className=\"flex justify-between\">\n <span>{t(\"subtotal\", \"Subtotal\")}</span>\n <span>{formatPrice(total, currency)}</span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"shipping\", \"Shipping\")}</span>\n <span>\n {shipping === 0\n ? t(\"free\", \"Free\")\n : formatPrice(shipping, currency)}\n </span>\n </div>\n <div className=\"flex justify-between\">\n <span>{t(\"tax\", \"Tax\")}</span>\n <span>{formatPrice(tax, currency)}</span>\n </div>\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 {error && (\n <div className=\"p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg\">\n <p className=\"text-red-800 dark:text-red-200 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"terms\"\n checked={agreedToTerms}\n onCheckedChange={(checked) =>\n setAgreedToTerms(checked as boolean)\n }\n />\n <span className=\"text-sm\">\n {t(\"agreeToTermsTextBefore\", \"I agree to the\")}{\" \"}\n <Link\n to=\"/terms\"\n className=\"text-primary hover:underline\"\n >\n {t(\"termsOfService\", \"Terms of Service\")}\n </Link>{\" \"}\n {t(\"and\", \"and\")}{\" \"}\n <Link\n to=\"/privacy\"\n className=\"text-primary hover:underline\"\n >\n {t(\"privacyPolicy\", \"Privacy Policy\")}\n </Link>\n </span>\n </div>\n\n <Button\n type=\"submit\"\n className=\"w-full\"\n size=\"lg\"\n disabled={!agreedToTerms || isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"processing\", \"Processing...\")}\n </>\n ) : (\n <>\n <Check className=\"w-4 h-4 mr-2\" />\n {paymentMethod === \"card\"\n ? t(\"proceedToPayment\", \"Proceed to Payment\")\n : t(\"placeOrder\", \"Place Order\")}\n </>\n )}\n </Button>\n </CardContent>\n </Card>\n </FadeIn>\n </div>\n </form>\n </div>\n </Layout>\n );\n}\n\nexport default CheckoutPage;\n"
27
+ "content": "import { useState, useEffect } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { ArrowLeft, CreditCard, Banknote, Truck, Check } 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 { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport { Skeleton } from \"@/components/ui/skeleton\";\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 { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { toast } from \"sonner\";\r\nimport {\r\n useCart,\r\n formatPrice,\r\n type PaymentMethod,\r\n type OnlinePaymentProvider,\r\n getFilteredPaymentMethodConfigs,\r\n getOnlinePaymentProviders,\r\n ONLINE_PROVIDER_CONFIGS,\r\n} from \"@/modules/ecommerce-core\";\r\nimport { customerClient, getErrorMessage } from \"@/modules/api\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\n\r\ninterface Country {\r\n value: string;\r\n label: string;\r\n}\r\n\r\ninterface CheckoutFormData {\r\n firstName: string;\r\n lastName: string;\r\n email: string;\r\n phone: string;\r\n address: string;\r\n city: string;\r\n postalCode: string;\r\n country: string;\r\n notes: string;\r\n}\r\n\r\ninterface BankTransferInfo {\r\n bank_name: string;\r\n bank_account_name: string;\r\n iban: string;\r\n}\r\n\r\nconst DEFAULT_COUNTRIES: Country[] = [\r\n { value: \"US\", label: \"United States\" },\r\n { value: \"GB\", label: \"United Kingdom\" },\r\n { value: \"CA\", label: \"Canada\" },\r\n { value: \"AU\", label: \"Australia\" },\r\n { value: \"DE\", label: \"Germany\" },\r\n { value: \"FR\", label: \"France\" },\r\n { value: \"IT\", label: \"Italy\" },\r\n { value: \"ES\", label: \"Spain\" },\r\n { value: \"NL\", label: \"Netherlands\" },\r\n { value: \"TR\", label: \"Turkey\" },\r\n { value: \"JP\", label: \"Japan\" },\r\n];\r\n\r\nexport function CheckoutPage() {\r\n const { t } = useTranslation(\"checkout-page\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Checkout\") });\r\n const { state, clearCart } = useCart();\r\n const { items, total } = state;\r\n\r\n const currency = (constants as any).site?.currency || \"USD\";\r\n const taxRate = (constants as any).payments?.taxRate || 0;\r\n const freeShippingThreshold = (constants as any).payments?.freeShippingThreshold || 0;\r\n const shippingCost = (constants as any).shipping?.domesticShipping?.standard?.cost || 0;\r\n\r\n // Calculate shipping and tax\r\n const shipping = total >= freeShippingThreshold ? 0 : shippingCost;\r\n const tax = total * taxRate;\r\n\r\n const countries = DEFAULT_COUNTRIES;\r\n\r\n // Get available payment methods and providers from config\r\n const availablePaymentMethods = getFilteredPaymentMethodConfigs();\r\n const availableProviders = getOnlinePaymentProviders();\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 [paymentMethod, setPaymentMethod] = useState<PaymentMethod>(\r\n availablePaymentMethods[0]?.id || \"card\"\r\n );\r\n const [selectedProvider, setSelectedProvider] = useState<OnlinePaymentProvider>(\r\n availableProviders[0] || \"stripe\"\r\n );\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [formData, setFormData] = useState<CheckoutFormData>({\r\n firstName: \"\",\r\n lastName: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n address: \"\",\r\n city: \"\",\r\n postalCode: \"\",\r\n country: \"\",\r\n notes: \"\",\r\n });\r\n const [agreedToTerms, setAgreedToTerms] = useState(false);\r\n\r\n // Bank transfer info state\r\n const [bankInfo, setBankInfo] = useState<BankTransferInfo | null>(null);\r\n const [isBankInfoLoading, setIsBankInfoLoading] = useState(false);\r\n const [bankInfoError, setBankInfoError] = useState<string | null>(null);\r\n\r\n const finalTotal = total + shipping + tax;\r\n\r\n // Fetch bank info when transfer is selected\r\n useEffect(() => {\r\n if (paymentMethod === \"transfer\") {\r\n const fetchBankInfo = async () => {\r\n setIsBankInfoLoading(true);\r\n setBankInfoError(null);\r\n try {\r\n const info = await customerClient.payment.getBankTransferInfo();\r\n setBankInfo(info);\r\n } catch (err: any) {\r\n setBankInfoError(\r\n err.message || t(\"bankInfoError\", \"Failed to load bank information\")\r\n );\r\n } finally {\r\n setIsBankInfoLoading(false);\r\n }\r\n };\r\n fetchBankInfo();\r\n }\r\n }, [paymentMethod, t]);\r\n\r\n const handleInputChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n const { name, value } = e.target;\r\n setFormData((prev) => ({ ...prev, [name]: value }));\r\n };\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n if (!agreedToTerms) {\r\n toast.error(t(\"agreeToTermsError\", \"Please agree to the terms and conditions\"));\r\n return;\r\n }\r\n\r\n setIsSubmitting(true);\r\n setError(null);\r\n\r\n try {\r\n // Determine payment type based on selection\r\n let paymentType: \"stripe\" | \"iyzico\" | \"bank_transfer\" | \"cash_on_delivery\";\r\n\r\n if (paymentMethod === \"card\") {\r\n paymentType = selectedProvider;\r\n } else if (paymentMethod === \"transfer\") {\r\n paymentType = \"bank_transfer\";\r\n } else {\r\n paymentType = \"cash_on_delivery\";\r\n }\r\n\r\n // Save checkout data to localStorage for success page\r\n const checkoutData = {\r\n items: items,\r\n total: finalTotal,\r\n customerInfo: formData,\r\n paymentMethod,\r\n paymentProvider: paymentType,\r\n };\r\n localStorage.setItem(\"pending_checkout\", JSON.stringify(checkoutData));\r\n\r\n // Build product data for checkout\r\n const productData = items.map((item) => {\r\n const price = getProductPrice(item.product);\r\n const qty = item.quantity || 1;\r\n\r\n return {\r\n quantity: qty,\r\n name: item.product.name || \"Product\",\r\n description: item.product.description || item.product.name || \"Product\",\r\n amount: Math.round(price * 100), // Convert to cents\r\n img: item.product.images?.[0] || \"/images/placeholder.png\",\r\n optionals: {\r\n productId: item.product.id,\r\n },\r\n };\r\n });\r\n\r\n // Tax amount in cents\r\n const taxAmountInCents = tax && !isNaN(tax) ? Math.round(tax * 100) : undefined;\r\n\r\n // Create checkout session\r\n const response = await customerClient.payment.createCheckout({\r\n currency: currency.toLowerCase(),\r\n taxAmount: taxAmountInCents,\r\n paymentType: paymentType,\r\n productData,\r\n contactData: {\r\n firstname: formData.firstName,\r\n lastname: formData.lastName,\r\n email: formData.email,\r\n phone: formData.phone,\r\n },\r\n shippingData: {\r\n address: formData.address,\r\n country: formData.country,\r\n city: formData.city,\r\n zip: formData.postalCode,\r\n },\r\n });\r\n\r\n // Clear cart and redirect to payment URL or confirmation page\r\n clearCart();\r\n if (response.url) {\r\n window.location.href = response.url;\r\n } else {\r\n window.location.href = `/order-confirmation?session_id=${response.sessionId}`;\r\n }\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(err, t(\"orderError\", \"Failed to place order. Please try again.\"));\r\n setError(errorMessage);\r\n toast.error(t(\"orderErrorTitle\", \"Order Failed\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n // Get icon component based on payment method\r\n const getPaymentIcon = (iconName: string) => {\r\n switch (iconName) {\r\n case \"CreditCard\":\r\n return CreditCard;\r\n case \"Banknote\":\r\n return Banknote;\r\n case \"Truck\":\r\n return Truck;\r\n default:\r\n return CreditCard;\r\n }\r\n };\r\n\r\n // Get icon color based on payment method\r\n const getIconColor = (methodId: string) => {\r\n switch (methodId) {\r\n case \"card\":\r\n return \"text-blue-600\";\r\n case \"transfer\":\r\n return \"text-primary\";\r\n case \"cash\":\r\n return \"text-green-600 dark:text-green-400\";\r\n default:\r\n return \"text-primary\";\r\n }\r\n };\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\">\r\n <h1 className=\"text-3xl font-bold mb-4\">\r\n {t(\"cartEmpty\", \"Your cart is empty\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-8\">\r\n {t(\r\n \"cartEmptyDescription\",\r\n \"Please add items to your cart before proceeding to checkout.\"\r\n )}\r\n </p>\r\n <Button asChild>\r\n <Link to=\"/products\">\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\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=\"/cart\">\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\", \"Checkout\")}</h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"completeOrder\", \"Complete your order\")}\r\n </p>\r\n </div>\r\n </FadeIn>\r\n\r\n <form onSubmit={handleSubmit}>\r\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\r\n <div className=\"lg:col-span-2 space-y-6\">\r\n {/* Contact Information */}\r\n <FadeIn delay={0.1}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>\r\n {t(\"contactInformation\", \"Contact Information\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"firstName\">\r\n {t(\"firstName\", \"First Name\")} *\r\n </Label>\r\n <Input\r\n id=\"firstName\"\r\n name=\"firstName\"\r\n value={formData.firstName}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"lastName\">\r\n {t(\"lastName\", \"Last Name\")} *\r\n </Label>\r\n <Input\r\n id=\"lastName\"\r\n name=\"lastName\"\r\n value={formData.lastName}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </div>\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"email\">\r\n {t(\"email\", \"Email Address\")} *\r\n </Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"phone\">\r\n {t(\"phone\", \"Phone Number\")} *\r\n </Label>\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n\r\n {/* Shipping Address */}\r\n <FadeIn delay={0.2}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>\r\n {t(\"shippingAddress\", \"Shipping Address\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"address\">{t(\"address\", \"Address\")} *</Label>\r\n <Textarea\r\n id=\"address\"\r\n name=\"address\"\r\n value={formData.address}\r\n onChange={handleInputChange}\r\n placeholder={t(\r\n \"addressPlaceholder\",\r\n \"Street address, apartment, suite, etc.\"\r\n )}\r\n required\r\n />\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"country\">{t(\"country\", \"Country\")} *</Label>\r\n <Select\r\n value={formData.country}\r\n onValueChange={(value) =>\r\n setFormData((prev) => ({ ...prev, country: value }))\r\n }\r\n required\r\n >\r\n <SelectTrigger id=\"country\">\r\n <SelectValue\r\n placeholder={t(\"selectCountry\", \"Select a country\")}\r\n />\r\n </SelectTrigger>\r\n <SelectContent>\r\n {countries.map((country) => (\r\n <SelectItem key={country.value} value={country.value}>\r\n {country.label}\r\n </SelectItem>\r\n ))}\r\n </SelectContent>\r\n </Select>\r\n </div>\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"city\">{t(\"city\", \"City\")} *</Label>\r\n <Input\r\n id=\"city\"\r\n name=\"city\"\r\n value={formData.city}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </div>\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"postalCode\">\r\n {t(\"postalCode\", \"Postal Code\")} *\r\n </Label>\r\n <Input\r\n id=\"postalCode\"\r\n name=\"postalCode\"\r\n value={formData.postalCode}\r\n onChange={handleInputChange}\r\n required\r\n />\r\n </div>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n\r\n {/* Payment Method */}\r\n <FadeIn delay={0.3}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"paymentMethod\", \"Payment Method\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <RadioGroup\r\n value={paymentMethod}\r\n onValueChange={(value) =>\r\n setPaymentMethod(value as PaymentMethod)\r\n }\r\n className=\"space-y-4\"\r\n >\r\n {availablePaymentMethods.map((method) => {\r\n const IconComponent = getPaymentIcon(method.icon);\r\n const iconColor = getIconColor(method.id);\r\n\r\n return (\r\n <div\r\n key={method.id}\r\n className=\"flex items-center space-x-2 p-4 border rounded-lg\"\r\n >\r\n <RadioGroupItem value={method.id} id={method.id} />\r\n <Label\r\n htmlFor={method.id}\r\n className=\"flex-1 cursor-pointer\"\r\n >\r\n <div className=\"flex items-center gap-3\">\r\n <IconComponent\r\n className={`h-5 w-5 ${iconColor}`}\r\n />\r\n <div>\r\n <div className=\"font-medium\">\r\n {t(method.id, method.label)}\r\n </div>\r\n <div className=\"text-sm text-muted-foreground\">\r\n {t(`${method.id}Description`, method.description)}\r\n </div>\r\n </div>\r\n </div>\r\n </Label>\r\n </div>\r\n );\r\n })}\r\n </RadioGroup>\r\n\r\n {/* Bank Transfer Details */}\r\n {paymentMethod === \"transfer\" && (\r\n <div className=\"mt-4 p-4 bg-primary/10 rounded-lg border border-primary/20\">\r\n <h4 className=\"font-medium mb-2\">\r\n {t(\"bankTransferDetailsTitle\", \"Bank Transfer Details\")}:\r\n </h4>\r\n {isBankInfoLoading ? (\r\n <div className=\"text-sm space-y-2\">\r\n <Skeleton className=\"h-4 w-full\" />\r\n <Skeleton className=\"h-4 w-3/4\" />\r\n <Skeleton className=\"h-4 w-full\" />\r\n </div>\r\n ) : bankInfoError ? (\r\n <p className=\"text-sm text-red-600\">{bankInfoError}</p>\r\n ) : bankInfo ? (\r\n <div className=\"text-sm space-y-1\">\r\n <p>\r\n <strong>{t(\"bank\", \"Bank\")}:</strong> {bankInfo.bank_name}\r\n </p>\r\n <p>\r\n <strong>{t(\"accountName\", \"Account Name\")}:</strong>{\" \"}\r\n {bankInfo.bank_account_name}\r\n </p>\r\n <p>\r\n <strong>IBAN:</strong> {bankInfo.iban}\r\n </p>\r\n </div>\r\n ) : (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"bankInfoNotAvailable\", \"Bank account information not available\")}\r\n </p>\r\n )}\r\n </div>\r\n )}\r\n\r\n {/* Card Payment - Provider Selection */}\r\n {paymentMethod === \"card\" && availableProviders.length > 1 && (\r\n <div className=\"mt-4 space-y-4\">\r\n <div className=\"p-4 bg-blue-50 dark:bg-blue-950/30 rounded-lg border border-blue-200 dark:border-blue-800\">\r\n <h4 className=\"font-medium text-blue-900 dark:text-blue-100 mb-3\">\r\n {t(\"selectPaymentProvider\", \"Select Payment Provider\")}\r\n </h4>\r\n <RadioGroup\r\n value={selectedProvider}\r\n onValueChange={(value) =>\r\n setSelectedProvider(value as OnlinePaymentProvider)\r\n }\r\n className=\"space-y-2\"\r\n >\r\n {availableProviders.map((provider) => (\r\n <div\r\n key={provider}\r\n className=\"flex items-center space-x-2 p-3 bg-background rounded border\"\r\n >\r\n <RadioGroupItem\r\n value={provider}\r\n id={`provider-${provider}`}\r\n />\r\n <Label\r\n htmlFor={`provider-${provider}`}\r\n className=\"flex-1 cursor-pointer\"\r\n >\r\n <div className=\"font-medium\">\r\n {t(`provider_${provider}_label`, ONLINE_PROVIDER_CONFIGS[provider].label)}\r\n </div>\r\n <div className=\"text-xs text-muted-foreground\">\r\n {t(`provider_${provider}_description`, ONLINE_PROVIDER_CONFIGS[provider].description)}\r\n </div>\r\n </Label>\r\n </div>\r\n ))}\r\n </RadioGroup>\r\n <p className=\"text-sm text-blue-700 dark:text-blue-300 mt-3\">\r\n {t(\r\n \"creditCardRedirectNote\",\r\n \"You will be redirected to the secure payment page to complete your purchase.\"\r\n )}\r\n </p>\r\n </div>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n\r\n {/* Order Notes */}\r\n <FadeIn delay={0.4}>\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>\r\n {t(\"orderNotesOptional\", \"Order Notes (Optional)\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <Textarea\r\n name=\"notes\"\r\n value={formData.notes}\r\n onChange={handleInputChange}\r\n placeholder={t(\r\n \"orderNotesPlaceholder\",\r\n \"Special instructions for your order...\"\r\n )}\r\n rows={3}\r\n />\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n </div>\r\n\r\n {/* Order Summary */}\r\n <FadeIn delay={0.2} className=\"lg:col-span-1\">\r\n <Card className=\"sticky top-24\">\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=\"space-y-3\">\r\n {items.map((item) => (\r\n <div key={item.id} className=\"flex gap-3\">\r\n <img\r\n src={\r\n item.product.images?.[0] ||\r\n \"/images/placeholder.png\"\r\n }\r\n alt={item.product.name}\r\n className=\"w-12 h-12 object-cover rounded\"\r\n />\r\n <div className=\"flex-1 space-y-1\">\r\n <h4 className=\"text-sm font-medium leading-normal\">\r\n {item.product.name}\r\n </h4>\r\n <div className=\"flex justify-between text-sm\">\r\n <span className=\"text-muted-foreground\">\r\n {t(\"qty\", \"Qty\")}: {item.quantity}\r\n </span>\r\n <span>\r\n {formatPrice(\r\n getProductPrice(item.product) * item.quantity,\r\n currency\r\n )}\r\n </span>\r\n </div>\r\n </div>\r\n </div>\r\n ))}\r\n </div>\r\n\r\n <Separator />\r\n\r\n <div className=\"space-y-2\">\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"subtotal\", \"Subtotal\")}</span>\r\n <span>{formatPrice(total, currency)}</span>\r\n </div>\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"shipping\", \"Shipping\")}</span>\r\n <span>\r\n {shipping === 0\r\n ? t(\"free\", \"Free\")\r\n : formatPrice(shipping, currency)}\r\n </span>\r\n </div>\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 </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 {error && (\r\n <div className=\"p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg\">\r\n <p className=\"text-red-800 dark:text-red-200 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <div className=\"flex items-center gap-2\">\r\n <Checkbox\r\n id=\"terms\"\r\n checked={agreedToTerms}\r\n onCheckedChange={(checked) =>\r\n setAgreedToTerms(checked as boolean)\r\n }\r\n />\r\n <span className=\"text-sm\">\r\n {t(\"agreeToTermsTextBefore\", \"I agree to the\")}{\" \"}\r\n <Link\r\n to=\"/terms\"\r\n className=\"text-primary hover:underline\"\r\n >\r\n {t(\"termsOfService\", \"Terms of Service\")}\r\n </Link>{\" \"}\r\n {t(\"and\", \"and\")}{\" \"}\r\n <Link\r\n to=\"/privacy\"\r\n className=\"text-primary hover:underline\"\r\n >\r\n {t(\"privacyPolicy\", \"Privacy Policy\")}\r\n </Link>\r\n </span>\r\n </div>\r\n\r\n <Button\r\n type=\"submit\"\r\n className=\"w-full\"\r\n size=\"lg\"\r\n disabled={!agreedToTerms || isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"processing\", \"Processing...\")}\r\n </>\r\n ) : (\r\n <>\r\n <Check className=\"w-4 h-4 mr-2\" />\r\n {paymentMethod === \"card\"\r\n ? t(\"proceedToPayment\", \"Proceed to Payment\")\r\n : t(\"placeOrder\", \"Place Order\")}\r\n </>\r\n )}\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </FadeIn>\r\n </div>\r\n </form>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default CheckoutPage;\r\n"
28
28
  },
29
29
  {
30
30
  "path": "checkout-page/lang/en.json",