@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.
- package/README.md +71 -0
- package/dist/index.js +149 -156
- package/dist/registry/blog-core.json +7 -26
- package/dist/registry/blog-list-page.json +2 -2
- package/dist/registry/blog-section.json +1 -1
- package/dist/registry/cart-drawer.json +1 -1
- package/dist/registry/cart-page.json +1 -1
- package/dist/registry/category-section.json +1 -1
- package/dist/registry/checkout-page.json +1 -1
- package/dist/registry/docs/blog-core.md +12 -13
- package/dist/registry/docs/blog-list-page.md +1 -1
- package/dist/registry/docs/ecommerce-core.md +10 -13
- package/dist/registry/docs/featured-products.md +1 -1
- package/dist/registry/docs/post-detail-page.md +2 -2
- package/dist/registry/docs/product-detail-page.md +2 -2
- package/dist/registry/docs/products-page.md +1 -1
- package/dist/registry/ecommerce-core.json +5 -25
- package/dist/registry/featured-products.json +2 -2
- package/dist/registry/header-centered-pill.json +1 -1
- package/dist/registry/header-ecommerce.json +1 -1
- package/dist/registry/index.json +0 -1
- package/dist/registry/login-page.json +1 -1
- package/dist/registry/post-card.json +1 -1
- package/dist/registry/post-detail-block.json +1 -1
- package/dist/registry/post-detail-page.json +3 -3
- package/dist/registry/product-card-detailed.json +1 -1
- package/dist/registry/product-card.json +1 -1
- package/dist/registry/product-detail-block.json +1 -1
- package/dist/registry/product-detail-page.json +3 -3
- package/dist/registry/product-detail-section.json +1 -1
- package/dist/registry/product-quick-view.json +1 -1
- package/dist/registry/products-page.json +2 -2
- package/dist/registry/register-page.json +1 -1
- package/dist/registry/related-products-block.json +1 -1
- package/package.json +4 -2
- package/template/README.md +54 -73
- package/template/package.json +4 -3
- package/template/public/data/database.db +0 -0
- package/template/public/data/database.db-shm +0 -0
- package/template/public/data/database.db-wal +0 -0
- package/template/scripts/init-db.ts +13 -126
- package/template/src/App.tsx +8 -5
- package/template/src/db/index.ts +20 -0
- package/template/src/db/provider.tsx +77 -0
- package/template/src/db/schema.json +259 -0
- package/template/src/db/types.ts +195 -0
- package/template/src/hooks/use-debounced-value.ts +12 -0
- package/dist/registry/db.json +0 -129
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Blog Core
|
|
2
2
|
|
|
3
|
-
Complete blog state management with Zustand. Includes useBlogStore for saved/favorite posts functionality
|
|
3
|
+
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.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
@@ -9,34 +9,33 @@ Complete blog state management with Zustand. Includes useBlogStore for saved/fav
|
|
|
9
9
|
| `$modules$/blog-core/index.ts` | index |
|
|
10
10
|
| `$modules$/blog-core/types.ts` | type |
|
|
11
11
|
| `$modules$/blog-core/stores/blog-store.ts` | store |
|
|
12
|
-
| `$modules$/blog-core/useDbPosts.ts` | hook |
|
|
13
12
|
| `$modules$/blog-core/lang/en.json` | lang |
|
|
14
13
|
| `$modules$/blog-core/lang/tr.json` | lang |
|
|
15
14
|
|
|
16
15
|
## Exports
|
|
17
16
|
|
|
18
|
-
**Types:** `Author`, `BlogCategory`, `BlogContextType`, `BlogSettings`, `Comment`, `Post
|
|
17
|
+
**Types:** `Author`, `BlogCategory`, `BlogContextType`, `BlogSettings`, `Comment`, `Post`
|
|
19
18
|
|
|
20
|
-
**Components/Functions:** `useBlog`, `useBlogStore
|
|
19
|
+
**Components/Functions:** `useBlog`, `useBlogStore`
|
|
21
20
|
|
|
22
21
|
```typescript
|
|
23
|
-
import { useBlog, useBlogStore,
|
|
22
|
+
import { useBlog, useBlogStore, Author, ... } from '@/modules/blog-core';
|
|
24
23
|
```
|
|
25
24
|
|
|
26
25
|
## Usage
|
|
27
26
|
|
|
28
27
|
```
|
|
29
|
-
import { useBlog
|
|
28
|
+
import { useBlog } from '@/modules/blog-core';
|
|
29
|
+
import { useDbList, useDbGet } from '@/db';
|
|
30
|
+
import type { Post } from '@/modules/blog-core';
|
|
30
31
|
|
|
31
|
-
//
|
|
32
|
+
// Blog store (favorites, saved posts):
|
|
32
33
|
const { favorites, addToFavorites, isFavorite } = useBlog();
|
|
33
|
-
|
|
34
|
+
|
|
35
|
+
// Data fetching via @/db hooks:
|
|
36
|
+
const { data: posts } = useDbList<Post>('posts');
|
|
37
|
+
const { data: post } = useDbGet<Post>('posts', { where: { slug } });
|
|
34
38
|
|
|
35
39
|
// Or use store directly with selectors:
|
|
36
40
|
const favorites = useBlogStore((s) => s.favorites);
|
|
37
41
|
```
|
|
38
|
-
|
|
39
|
-
## Dependencies
|
|
40
|
-
|
|
41
|
-
This component requires:
|
|
42
|
-
- `db`
|
|
@@ -26,7 +26,7 @@ import { BlogListPage } from '@/modules/blog-list-page';
|
|
|
26
26
|
|
|
27
27
|
<Route path="/blog" element={<BlogListPage />} />
|
|
28
28
|
|
|
29
|
-
• Uses
|
|
29
|
+
• Uses useDbList() from @/db for post fetching
|
|
30
30
|
• Features: category tabs, search, grid/list view
|
|
31
31
|
• Sidebar: popular posts, categories, newsletter
|
|
32
32
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# E-commerce Core
|
|
2
2
|
|
|
3
|
-
Complete e-commerce state management with Zustand. Includes useCartStore for shopping cart operations (add/remove/update items, totals), useFavoritesStore for wishlist,
|
|
3
|
+
Complete e-commerce state management with Zustand. Includes useCartStore for shopping cart operations (add/remove/update items, totals), useFavoritesStore for wishlist, formatPrice utility, and payment config. No provider wrapping needed. Data fetching uses useDbList/useDbGet from @/db.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
@@ -10,8 +10,6 @@ Complete e-commerce state management with Zustand. Includes useCartStore for sho
|
|
|
10
10
|
| `$modules$/ecommerce-core/types.ts` | type |
|
|
11
11
|
| `$modules$/ecommerce-core/stores/cart-store.ts` | store |
|
|
12
12
|
| `$modules$/ecommerce-core/stores/favorites-store.ts` | store |
|
|
13
|
-
| `$modules$/ecommerce-core/useDbProducts.ts` | hook |
|
|
14
|
-
| `$modules$/ecommerce-core/useDbSearch.ts` | hook |
|
|
15
13
|
| `$modules$/ecommerce-core/format-price.ts` | lib |
|
|
16
14
|
| `$modules$/ecommerce-core/payment-config.ts` | lib |
|
|
17
15
|
| `$modules$/ecommerce-core/lang/en.json` | lang |
|
|
@@ -19,9 +17,9 @@ Complete e-commerce state management with Zustand. Includes useCartStore for sho
|
|
|
19
17
|
|
|
20
18
|
## Exports
|
|
21
19
|
|
|
22
|
-
**Types:** `Address`, `CartContextType`, `CartItem`, `CartState`, `Category`, `FavoritesContextType`, `OnlinePaymentProvider`, `Order`, `OrderItem`, `PaymentMethod`, `PaymentMethodConfig`, `Product`, `
|
|
20
|
+
**Types:** `Address`, `CartContextType`, `CartItem`, `CartState`, `Category`, `FavoritesContextType`, `OnlinePaymentProvider`, `Order`, `OrderItem`, `PaymentMethod`, `PaymentMethodConfig`, `Product`, `ProductVariant`, `User`
|
|
23
21
|
|
|
24
|
-
**Components/Functions:** `ONLINE_PROVIDER_CONFIGS`, `PAYMENT_METHOD_CONFIGS`, `formatPrice`, `getAvailablePaymentMethods`, `getFilteredPaymentMethodConfigs`, `getOnlinePaymentProviders`, `isOnlineProviderAvailable`, `isPaymentMethodAvailable`, `useCart`, `useCartStore`, `
|
|
22
|
+
**Components/Functions:** `ONLINE_PROVIDER_CONFIGS`, `PAYMENT_METHOD_CONFIGS`, `formatPrice`, `getAvailablePaymentMethods`, `getFilteredPaymentMethodConfigs`, `getOnlinePaymentProviders`, `isOnlineProviderAvailable`, `isPaymentMethodAvailable`, `useCart`, `useCartStore`, `useFavorites`, `useFavoritesStore`
|
|
25
23
|
|
|
26
24
|
```typescript
|
|
27
25
|
import { ONLINE_PROVIDER_CONFIGS, PAYMENT_METHOD_CONFIGS, formatPrice, ... } from '@/modules/ecommerce-core';
|
|
@@ -30,18 +28,17 @@ import { ONLINE_PROVIDER_CONFIGS, PAYMENT_METHOD_CONFIGS, formatPrice, ... } fro
|
|
|
30
28
|
## Usage
|
|
31
29
|
|
|
32
30
|
```
|
|
33
|
-
import { useCart, useFavorites
|
|
31
|
+
import { useCart, useFavorites } from '@/modules/ecommerce-core';
|
|
32
|
+
import { useDbList } from '@/db';
|
|
33
|
+
import type { Product } from '@/modules/ecommerce-core';
|
|
34
34
|
|
|
35
|
-
//
|
|
35
|
+
// Cart & favorites stores:
|
|
36
36
|
const { addItem, removeItem, state, itemCount } = useCart();
|
|
37
37
|
const { addToFavorites, isFavorite } = useFavorites();
|
|
38
|
-
|
|
38
|
+
|
|
39
|
+
// Data fetching via @/db hooks:
|
|
40
|
+
const { data: products } = useDbList<Product>('products');
|
|
39
41
|
|
|
40
42
|
// Or use stores directly with selectors:
|
|
41
43
|
const itemCount = useCartStore((s) => s.itemCount);
|
|
42
44
|
```
|
|
43
|
-
|
|
44
|
-
## Dependencies
|
|
45
|
-
|
|
46
|
-
This component requires:
|
|
47
|
-
- `db`
|
|
@@ -28,7 +28,7 @@ import { FeaturedProducts } from '@/modules/featured-products';
|
|
|
28
28
|
|
|
29
29
|
• Installed at: src/modules/featured-products/
|
|
30
30
|
• Customize content: src/modules/featured-products/lang/*.json
|
|
31
|
-
• Products auto-loaded via
|
|
31
|
+
• Products auto-loaded via useDbList from @/db
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
## Dependencies
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Post Detail Page
|
|
2
2
|
|
|
3
|
-
Blog post detail page that fetches post data by slug from URL params. Uses
|
|
3
|
+
Blog post detail page that fetches post data by slug from URL params. Uses useDbGet from @/db and renders PostDetailBlock. Includes loading skeleton, error handling for not found posts, and automatic page title.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ import { PostDetailPage } from '@/modules/post-detail-page';
|
|
|
26
26
|
|
|
27
27
|
<Route path="/blog/:slug" element={<PostDetailPage />} />
|
|
28
28
|
|
|
29
|
-
• Uses
|
|
29
|
+
• Uses useDbGet() from @/db to fetch post by slug
|
|
30
30
|
• Fetches post by slug from URL params
|
|
31
31
|
• Shows loading skeleton while fetching
|
|
32
32
|
• Handles post not found state
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Product Detail Page
|
|
2
2
|
|
|
3
|
-
Product detail page that fetches product data by slug from URL params. Uses
|
|
3
|
+
Product detail page that fetches product data by slug from URL params. Uses useDbGet from @/db and renders ProductDetailBlock. Includes loading skeleton, error handling for not found products, and automatic page title.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ import { ProductDetailPage } from '@/modules/product-detail-page';
|
|
|
26
26
|
|
|
27
27
|
<Route path="/products/:slug" element={<ProductDetailPage />} />
|
|
28
28
|
|
|
29
|
-
• Uses
|
|
29
|
+
• Uses useDbGet() from @/db to fetch product by slug
|
|
30
30
|
• Fetches product by slug from URL params
|
|
31
31
|
• Shows loading skeleton while fetching
|
|
32
32
|
• Handles product not found state
|
|
@@ -29,7 +29,7 @@ import ProductsPage from '@/modules/products-page';
|
|
|
29
29
|
• Installed at: src/modules/products-page/
|
|
30
30
|
• Add link: <Link to="/products">Browse Products</Link>
|
|
31
31
|
• Supports filters, sorting, grid/list view, pagination
|
|
32
|
-
• Uses
|
|
32
|
+
• Uses useDbList from @/db for data fetching
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
## Dependencies
|
|
@@ -2,26 +2,24 @@
|
|
|
2
2
|
"name": "ecommerce-core",
|
|
3
3
|
"type": "registry:module",
|
|
4
4
|
"title": "E-commerce Core",
|
|
5
|
-
"description": "Complete e-commerce state management with Zustand. Includes useCartStore for shopping cart operations (add/remove/update items, totals), useFavoritesStore for wishlist,
|
|
5
|
+
"description": "Complete e-commerce state management with Zustand. Includes useCartStore for shopping cart operations (add/remove/update items, totals), useFavoritesStore for wishlist, formatPrice utility, and payment config. No provider wrapping needed. Data fetching uses useDbList/useDbGet from @/db.",
|
|
6
6
|
"dependencies": [
|
|
7
7
|
"zustand"
|
|
8
8
|
],
|
|
9
|
-
"registryDependencies": [
|
|
10
|
-
|
|
11
|
-
],
|
|
12
|
-
"usage": "import { useCart, useFavorites, useDbProducts } from '@/modules/ecommerce-core';\n\n// No provider needed - just use the hooks:\nconst { addItem, removeItem, state, itemCount } = useCart();\nconst { addToFavorites, isFavorite } = useFavorites();\nconst { products, loading } = useDbProducts();\n\n// Or use stores directly with selectors:\nconst itemCount = useCartStore((s) => s.itemCount);",
|
|
9
|
+
"registryDependencies": [],
|
|
10
|
+
"usage": "import { useCart, useFavorites } from '@/modules/ecommerce-core';\nimport { useDbList } from '@/db';\nimport type { Product } from '@/modules/ecommerce-core';\n\n// Cart & favorites stores:\nconst { addItem, removeItem, state, itemCount } = useCart();\nconst { addToFavorites, isFavorite } = useFavorites();\n\n// Data fetching via @/db hooks:\nconst { data: products } = useDbList<Product>('products');\n\n// Or use stores directly with selectors:\nconst itemCount = useCartStore((s) => s.itemCount);",
|
|
13
11
|
"files": [
|
|
14
12
|
{
|
|
15
13
|
"path": "ecommerce-core/index.ts",
|
|
16
14
|
"type": "registry:index",
|
|
17
15
|
"target": "$modules$/ecommerce-core/index.ts",
|
|
18
|
-
"content": "// Types\r\nexport * from './types';\r\n\r\n// Stores (Zustand)\r\nexport { useCartStore, useCart } from './stores/cart-store';\r\nexport { useFavoritesStore, useFavorites } from './stores/favorites-store';\r\n\r\n//
|
|
16
|
+
"content": "// Types\r\nexport * from './types';\r\n\r\n// Stores (Zustand)\r\nexport { useCartStore, useCart } from './stores/cart-store';\r\nexport { useFavoritesStore, useFavorites } from './stores/favorites-store';\r\n\r\n// Utilities\r\nexport { formatPrice } from './format-price';\r\n\r\n// Payment Config\r\nexport {\r\n type PaymentMethod,\r\n type OnlinePaymentProvider,\r\n type PaymentMethodConfig,\r\n PAYMENT_METHOD_CONFIGS,\r\n ONLINE_PROVIDER_CONFIGS,\r\n getAvailablePaymentMethods,\r\n getOnlinePaymentProviders,\r\n getFilteredPaymentMethodConfigs,\r\n isPaymentMethodAvailable,\r\n isOnlineProviderAvailable,\r\n} from './payment-config';\r\n"
|
|
19
17
|
},
|
|
20
18
|
{
|
|
21
19
|
"path": "ecommerce-core/types.ts",
|
|
22
20
|
"type": "registry:type",
|
|
23
21
|
"target": "$modules$/ecommerce-core/types.ts",
|
|
24
|
-
"content": "export interface
|
|
22
|
+
"content": "export interface Product {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n description: string;\r\n price: number;\r\n sale_price?: number;\r\n on_sale: boolean;\r\n images: string[];\r\n categories: number[];\r\n brand?: string;\r\n sku?: string;\r\n stock: number;\r\n tags: string[];\r\n rating: number;\r\n review_count: number;\r\n featured: boolean;\r\n is_new: boolean;\r\n published: boolean;\r\n specifications?: Record<string, string | number | boolean>;\r\n variants?: ProductVariant[];\r\n created_at?: string;\r\n updated_at?: string;\r\n meta_description?: string;\r\n meta_keywords?: string;\r\n}\r\n\r\nexport interface ProductVariant {\r\n id: string;\r\n name: string;\r\n value: string;\r\n price?: number;\r\n image?: string;\r\n stockQuantity: number;\r\n}\r\n\r\nexport interface CartItem {\r\n id: string | number;\r\n product: Product;\r\n quantity: number;\r\n}\r\n\r\nexport interface CartState {\r\n items: CartItem[];\r\n total: number;\r\n}\r\n\r\nexport interface CartContextType {\r\n state: CartState;\r\n addItem: (product: Product) => void;\r\n removeItem: (id: string | number) => void;\r\n updateQuantity: (id: string | number, quantity: number) => void;\r\n clearCart: () => void;\r\n itemCount: number;\r\n}\r\n\r\nexport interface FavoritesContextType {\r\n favorites: Product[];\r\n addToFavorites: (product: Product) => void;\r\n removeFromFavorites: (productId: string | number) => void;\r\n isFavorite: (productId: string | number) => boolean;\r\n favoriteCount: number;\r\n clearFavorites: () => void;\r\n}\r\n\r\nexport interface Category {\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 User {\r\n id: string;\r\n email: string;\r\n name: string;\r\n avatar?: string;\r\n addresses?: Address[];\r\n orders?: Order[];\r\n}\r\n\r\nexport interface Address {\r\n name: string;\r\n line1: string;\r\n line2?: string;\r\n city: string;\r\n state: string;\r\n postalCode: string;\r\n country: string;\r\n}\r\n\r\nexport interface Order {\r\n id: number;\r\n user_id: string;\r\n total_price: number;\r\n status: 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled';\r\n payment_method: string;\r\n shipping_address: Address;\r\n notes?: string;\r\n created_at?: string;\r\n}\r\n\r\nexport interface OrderItem {\r\n id: number;\r\n order_id: number;\r\n product_id: number;\r\n quantity: number;\r\n price: number;\r\n product?: {\r\n name: string;\r\n slug: string;\r\n images: string[];\r\n price: number;\r\n };\r\n}\r\n"
|
|
25
23
|
},
|
|
26
24
|
{
|
|
27
25
|
"path": "ecommerce-core/stores/cart-store.ts",
|
|
@@ -35,18 +33,6 @@
|
|
|
35
33
|
"target": "$modules$/ecommerce-core/stores/favorites-store.ts",
|
|
36
34
|
"content": "import { create } from \"zustand\";\r\nimport { persist } from \"zustand/middleware\";\r\nimport type { Product, FavoritesContextType } from \"../types\";\r\n\r\ninterface FavoritesStore {\r\n favorites: Product[];\r\n favoriteCount: number;\r\n addToFavorites: (product: Product) => void;\r\n removeFromFavorites: (productId: string | number) => void;\r\n isFavorite: (productId: string | number) => boolean;\r\n clearFavorites: () => void;\r\n}\r\n\r\nexport const useFavoritesStore = create<FavoritesStore>()(\r\n persist(\r\n (set, get) => ({\r\n favorites: [],\r\n favoriteCount: 0,\r\n\r\n addToFavorites: (product) =>\r\n set((state) => {\r\n if (state.favorites.some((fav) => fav.id === product.id)) {\r\n return state;\r\n }\r\n const favorites = [...state.favorites, product];\r\n return { favorites, favoriteCount: favorites.length };\r\n }),\r\n\r\n removeFromFavorites: (productId) =>\r\n set((state) => {\r\n const favorites = state.favorites.filter((fav) => fav.id !== productId);\r\n return { favorites, favoriteCount: favorites.length };\r\n }),\r\n\r\n isFavorite: (productId) => {\r\n return get().favorites.some((fav) => fav.id === productId);\r\n },\r\n\r\n clearFavorites: () => set({ favorites: [], favoriteCount: 0 }),\r\n }),\r\n { name: \"ecommerce_favorites\" }\r\n )\r\n);\r\n\r\n// Backward compatible hook - matches FavoritesContextType\r\nexport const useFavorites = (): FavoritesContextType => {\r\n const store = useFavoritesStore();\r\n return {\r\n favorites: store.favorites,\r\n addToFavorites: store.addToFavorites,\r\n removeFromFavorites: store.removeFromFavorites,\r\n isFavorite: store.isFavorite,\r\n favoriteCount: store.favoriteCount,\r\n clearFavorites: store.clearFavorites,\r\n };\r\n};\r\n"
|
|
37
35
|
},
|
|
38
|
-
{
|
|
39
|
-
"path": "ecommerce-core/useDbProducts.ts",
|
|
40
|
-
"type": "registry:hook",
|
|
41
|
-
"target": "$modules$/ecommerce-core/useDbProducts.ts",
|
|
42
|
-
"content": "import { useMemo } from 'react';\r\nimport type { Product, Category, ProductCategory } from './types';\r\nimport {\r\n useRepositoryQuery,\r\n useRawQuery,\r\n useRawQueryOne,\r\n parseStringToArray,\r\n parseJSONString,\r\n parseSQLiteBoolean,\r\n parseNumberSafe\r\n} from '@/modules/db';\r\n\r\nconst transformProduct = (row: any): Product => {\r\n const categoryNames = row.category_names ? row.category_names.split(',') : [];\r\n const categorySlugs = row.category_slugs ? row.category_slugs.split(',') : [];\r\n const categoryIds = row.category_ids ? row.category_ids.split(',').map(Number) : [];\r\n\r\n const categories: ProductCategory[] = categoryIds.map((id: number, index: number) => ({\r\n id,\r\n name: categoryNames[index] || '',\r\n slug: categorySlugs[index] || '',\r\n is_primary: index === 0\r\n }));\r\n\r\n const primaryCategory = categories.length > 0 ? categories[0] : null;\r\n\r\n return {\r\n id: parseNumberSafe(row.id),\r\n name: String(row.name || ''),\r\n slug: String(row.slug || ''),\r\n description: row.description || '',\r\n price: parseNumberSafe(row.price),\r\n sale_price: row.sale_price ? parseNumberSafe(row.sale_price) : undefined,\r\n on_sale: parseSQLiteBoolean(row.on_sale),\r\n images: parseStringToArray(row.images),\r\n brand: row.brand || '',\r\n sku: row.sku || '',\r\n stock: parseNumberSafe(row.stock),\r\n tags: parseJSONString(row.tags, []) || [],\r\n rating: parseNumberSafe(row.rating),\r\n review_count: parseNumberSafe(row.review_count),\r\n featured: parseSQLiteBoolean(row.featured),\r\n is_new: parseSQLiteBoolean(row.is_new),\r\n published: parseSQLiteBoolean(row.published),\r\n specifications: parseJSONString(row.specifications, {}) || {},\r\n variants: parseJSONString(row.variants, []) || [],\r\n created_at: row.created_at || new Date().toISOString(),\r\n updated_at: row.updated_at || new Date().toISOString(),\r\n meta_description: row.meta_description || '',\r\n meta_keywords: row.meta_keywords || '',\r\n category: primaryCategory?.slug || '',\r\n category_name: primaryCategory?.name || '',\r\n categories\r\n };\r\n};\r\n\r\nconst PRODUCTS_WITH_CATEGORIES_SQL = `\r\n SELECT p.*,\r\n GROUP_CONCAT(c.name) as category_names,\r\n GROUP_CONCAT(c.slug) as category_slugs,\r\n GROUP_CONCAT(c.id) as category_ids\r\n FROM products p\r\n LEFT JOIN product_category_relations pcr ON p.id = pcr.product_id\r\n LEFT JOIN product_categories c ON pcr.category_id = c.id\r\n WHERE p.published = 1\r\n GROUP BY p.id\r\n`;\r\n\r\nexport function useDbCategories() {\r\n const { data, isLoading: loading, error } = useRepositoryQuery<Category>('product_categories', {\r\n orderBy: [{ field: 'name', direction: 'ASC' }]\r\n });\r\n\r\n return {\r\n categories: data ?? [],\r\n loading,\r\n error: error?.message ?? null\r\n };\r\n}\r\n\r\nexport function useDbProducts() {\r\n const sql = `${PRODUCTS_WITH_CATEGORIES_SQL} ORDER BY p.name`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['products', 'all'],\r\n sql\r\n );\r\n\r\n const products = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformProduct);\r\n }, [data]);\r\n\r\n return { products, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbProductBySlug(slug: string) {\r\n const sql = `\r\n SELECT p.*,\r\n GROUP_CONCAT(c.name) as category_names,\r\n GROUP_CONCAT(c.slug) as category_slugs,\r\n GROUP_CONCAT(c.id) as category_ids\r\n FROM products p\r\n LEFT JOIN product_category_relations pcr ON p.id = pcr.product_id\r\n LEFT JOIN product_categories c ON pcr.category_id = c.id\r\n WHERE p.slug = ? AND p.published = 1\r\n GROUP BY p.id\r\n `;\r\n\r\n const { data, isLoading: loading, error } = useRawQueryOne<any>(\r\n ['products', 'slug', slug],\r\n sql,\r\n [slug],\r\n { enabled: !!slug }\r\n );\r\n\r\n const product = useMemo(() => {\r\n if (!data) return null;\r\n return transformProduct(data);\r\n }, [data]);\r\n\r\n return {\r\n product,\r\n loading,\r\n error: !data && !loading && slug ? 'Product not found' : (error?.message ?? null)\r\n };\r\n}\r\n\r\nexport function useDbFeaturedProducts() {\r\n const sql = `\r\n SELECT p.*,\r\n GROUP_CONCAT(c.name) as category_names,\r\n GROUP_CONCAT(c.slug) as category_slugs,\r\n GROUP_CONCAT(c.id) as category_ids\r\n FROM products p\r\n LEFT JOIN product_category_relations pcr ON p.id = pcr.product_id\r\n LEFT JOIN product_categories c ON pcr.category_id = c.id\r\n WHERE p.published = 1 AND p.featured = 1\r\n GROUP BY p.id\r\n ORDER BY p.created_at DESC LIMIT 8\r\n `;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['products', 'featured'],\r\n sql\r\n );\r\n\r\n const products = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformProduct);\r\n }, [data]);\r\n\r\n return { products, loading, error: error?.message ?? null };\r\n}\r\n"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"path": "ecommerce-core/useDbSearch.ts",
|
|
46
|
-
"type": "registry:hook",
|
|
47
|
-
"target": "$modules$/ecommerce-core/useDbSearch.ts",
|
|
48
|
-
"content": "import { useState, useEffect, useCallback } from 'react';\r\nimport type { Product } from './types';\r\nimport { useDbProducts } from './useDbProducts';\r\n\r\nexport const useDbSearch = () => {\r\n const [searchTerm, setSearchTerm] = useState('');\r\n const [results, setResults] = useState<Product[]>([]);\r\n const [isSearching, setIsSearching] = useState(false);\r\n\r\n // Load all products via useDbProducts hook\r\n const { products: allProducts } = useDbProducts();\r\n\r\n // Perform search when searchTerm changes\r\n useEffect(() => {\r\n if (!searchTerm.trim()) {\r\n setResults([]);\r\n setIsSearching(false);\r\n return;\r\n }\r\n\r\n setIsSearching(true);\r\n\r\n const searchTimeout = setTimeout(() => {\r\n const filtered = allProducts.filter(product => {\r\n const term = searchTerm.toLowerCase();\r\n \r\n // Search in product name\r\n if (product.name.toLowerCase().includes(term)) return true;\r\n \r\n // Search in description\r\n if (product.description.toLowerCase().includes(term)) return true;\r\n \r\n // Search in category\r\n if (product.category_name?.toLowerCase().includes(term)) return true;\r\n \r\n // Search in brand\r\n if (product.brand?.toLowerCase().includes(term)) return true;\r\n \r\n // Search in tags\r\n if (product.tags.some(tag => tag.toLowerCase().includes(term))) return true;\r\n \r\n return false;\r\n });\r\n\r\n setResults(filtered);\r\n setIsSearching(false);\r\n }, 300); // Debounce search\r\n\r\n return () => clearTimeout(searchTimeout);\r\n }, [searchTerm, allProducts]);\r\n\r\n const clearSearch = useCallback(() => {\r\n setSearchTerm('');\r\n setResults([]);\r\n setIsSearching(false);\r\n }, []);\r\n\r\n // search function that takes a term and sets the searchTerm\r\n const search = useCallback((term: string) => {\r\n setSearchTerm(term);\r\n }, []);\r\n\r\n return {\r\n searchTerm,\r\n setSearchTerm,\r\n results,\r\n isSearching,\r\n clearSearch,\r\n clearResults: clearSearch, // alias for header-ecommerce compatibility\r\n search, // function to trigger search\r\n hasResults: results.length > 0\r\n };\r\n};\r\n"
|
|
49
|
-
},
|
|
50
36
|
{
|
|
51
37
|
"path": "ecommerce-core/format-price.ts",
|
|
52
38
|
"type": "registry:lib",
|
|
@@ -86,7 +72,6 @@
|
|
|
86
72
|
"PaymentMethod",
|
|
87
73
|
"PaymentMethodConfig",
|
|
88
74
|
"Product",
|
|
89
|
-
"ProductCategory",
|
|
90
75
|
"ProductVariant",
|
|
91
76
|
"User"
|
|
92
77
|
],
|
|
@@ -101,11 +86,6 @@
|
|
|
101
86
|
"isPaymentMethodAvailable",
|
|
102
87
|
"useCart",
|
|
103
88
|
"useCartStore",
|
|
104
|
-
"useDbCategories",
|
|
105
|
-
"useDbFeaturedProducts",
|
|
106
|
-
"useDbProductBySlug",
|
|
107
|
-
"useDbProducts",
|
|
108
|
-
"useDbSearch",
|
|
109
89
|
"useFavorites",
|
|
110
90
|
"useFavoritesStore"
|
|
111
91
|
]
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"ecommerce-core",
|
|
8
8
|
"product-card"
|
|
9
9
|
],
|
|
10
|
-
"usage": "import { FeaturedProducts } from '@/modules/featured-products';\n\n<FeaturedProducts />\n\n• Installed at: src/modules/featured-products/\n• Customize content: src/modules/featured-products/lang/*.json\n• Products auto-loaded via
|
|
10
|
+
"usage": "import { FeaturedProducts } from '@/modules/featured-products';\n\n<FeaturedProducts />\n\n• Installed at: src/modules/featured-products/\n• Customize content: src/modules/featured-products/lang/*.json\n• Products auto-loaded via useDbList from @/db",
|
|
11
11
|
"files": [
|
|
12
12
|
{
|
|
13
13
|
"path": "featured-products/index.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"path": "featured-products/featured-products.tsx",
|
|
20
20
|
"type": "registry:component",
|
|
21
21
|
"target": "$modules$/featured-products/featured-products.tsx",
|
|
22
|
-
"content": "import { Link } from \"react-router\";\r\nimport { ArrowRight } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { ProductCard } from \"@/modules/product-card/product-card\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport {
|
|
22
|
+
"content": "import { Link } from \"react-router\";\r\nimport { ArrowRight } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { ProductCard } from \"@/modules/product-card/product-card\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useDbList } from \"@/db\";\r\nimport type { Product } from \"@/modules/ecommerce-core/types\";\r\n\r\ninterface FeaturedProductsProps {\r\n products?: Product[];\r\n loading?: boolean;\r\n}\r\n\r\nexport function FeaturedProducts({\r\n products: propProducts,\r\n loading: propLoading,\r\n}: FeaturedProductsProps) {\r\n const { t } = useTranslation(\"featured-products\");\r\n const { data: hookProducts = [], isLoading: hookLoading } = useDbList<Product>(\"products\", {\r\n where: { featured: 1 },\r\n limit: 8,\r\n });\r\n\r\n const products = propProducts ?? hookProducts;\r\n const loading = propLoading ?? hookLoading;\r\n\r\n return (\r\n <section className=\"py-8 sm:py-12 md:py-16 lg:py-20 bg-background border-t border-border/20 relative\">\r\n <div className=\"absolute top-0 left-1/2 transform -translate-x-1/2 w-16 sm:w-24 h-px bg-primary/30\"></div>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\r\n <div className=\"text-center mb-6 sm:mb-8 md:mb-12 lg:mb-16 px-2\">\r\n <h2 className=\"text-xl sm:text-2xl md:text-3xl lg:text-4xl xl:text-5xl font-bold mb-2 sm:mb-3 md:mb-4 bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent leading-normal pb-1\">\r\n {t('title', 'Featured Products')}\r\n </h2>\r\n <div className=\"w-12 sm:w-16 md:w-20 h-1 bg-gradient-to-r from-primary/50 to-primary/20 mx-auto mb-3 sm:mb-4 md:mb-6 rounded-full\"></div>\r\n <p className=\"text-xs sm:text-sm md:text-base lg:text-lg xl:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed\">\r\n {t('subtitle', 'Hand-picked favorites from our collection')}\r\n </p>\r\n </div>\r\n\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-8 xl:gap-10\">\r\n {loading ? (\r\n [...Array(3)].map((_, i) => (\r\n <div key={i} className=\"animate-pulse group\">\r\n <div className=\"aspect-square bg-gradient-to-br from-muted to-muted/50 rounded-2xl mb-6\"></div>\r\n <div className=\"space-y-3\">\r\n <div className=\"h-6 bg-muted rounded-lg w-3/4\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/2\"></div>\r\n <div className=\"h-5 bg-muted rounded w-2/3\"></div>\r\n </div>\r\n </div>\r\n ))\r\n ) : (\r\n products.map((product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <ProductCard\r\n product={product}\r\n variant=\"featured\"\r\n />\r\n </div>\r\n ))\r\n )}\r\n </div>\r\n\r\n <div className=\"text-center mt-8 sm:mt-12 lg:mt-16\">\r\n <Button size=\"lg\" asChild className=\"px-6 sm:px-8 py-3 sm:py-4 text-base sm:text-lg\">\r\n <Link to=\"/products\">\r\n {t('viewAll', 'View All Products')}\r\n <ArrowRight className=\"w-4 h-4 sm:w-5 sm:h-5 ml-2\" />\r\n </Link>\r\n </Button>\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "featured-products/lang/en.json",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"path": "header-centered-pill/header-centered-pill.tsx",
|
|
22
22
|
"type": "registry:component",
|
|
23
23
|
"target": "$modules$/header-centered-pill/header-centered-pill.tsx",
|
|
24
|
-
"content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { Menu } from \"lucide-react\";\r\nimport { Button, buttonVariants } from \"@/components/ui/button\";\r\nimport {\r\n Sheet,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetContent,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useTranslation } from \"react-i18next\";\r\n\r\nexport function HeaderCenteredPill() {\r\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\r\n const { t } = useTranslation(\"header-centered-pill\");\r\n\r\n const navigation = [\r\n { name: t(\"features\", \"Features\"), href: \"/features\" },\r\n { name: t(\"pricing\", \"Pricing\"), href: \"/pricing\" },\r\n { name: t(\"blog\", \"Blog\"), href: \"/blog\" },\r\n { name: t(\"company\", \"Company\"), href: \"/about\" },\r\n { name: t(\"signIn\", \"Sign in\"), href: \"/login\" },\r\n ];\r\n\r\n return (\r\n <header className=\"p-4\">\r\n <div className=\"mx-auto flex max-w-2xl items-center justify-between space-x-4 rounded-full border bg-background p-1.5 pl-4\">\r\n <
|
|
24
|
+
"content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { Menu } from \"lucide-react\";\r\nimport { Button, buttonVariants } from \"@/components/ui/button\";\r\nimport {\r\n Sheet,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetContent,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useTranslation } from \"react-i18next\";\r\n\r\nexport function HeaderCenteredPill() {\r\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\r\n const { t } = useTranslation(\"header-centered-pill\");\r\n\r\n const navigation = [\r\n { name: t(\"features\", \"Features\"), href: \"/features\" },\r\n { name: t(\"pricing\", \"Pricing\"), href: \"/pricing\" },\r\n { name: t(\"blog\", \"Blog\"), href: \"/blog\" },\r\n { name: t(\"company\", \"Company\"), href: \"/about\" },\r\n { name: t(\"signIn\", \"Sign in\"), href: \"/login\" },\r\n ];\r\n\r\n return (\r\n <header className=\"p-4\">\r\n <div className=\"mx-auto flex max-w-2xl items-center justify-between space-x-4 rounded-full border bg-background p-1.5 pl-4\">\r\n <Logo size=\"sm\" />\r\n\r\n {/* Desktop Navigation */}\r\n <div className=\"hidden md:inline-flex\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className={buttonVariants({ variant: \"ghost\", size: \"sm\" })}\r\n >\r\n {item.name}\r\n </Link>\r\n ))}\r\n </div>\r\n\r\n {/* Desktop CTA */}\r\n <div className=\"hidden md:inline-flex\">\r\n <Link\r\n to=\"/register\"\r\n className={cn(buttonVariants({ size: \"sm\" }), \"rounded-full\")}\r\n >\r\n {t(\"getStarted\", \"Get Started\")}\r\n </Link>\r\n </div>\r\n\r\n {/* Mobile Menu */}\r\n <div className=\"md:hidden\">\r\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\r\n <SheetTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"rounded-full\">\r\n <Menu className=\"h-5 w-5\" />\r\n <span className=\"sr-only\">{t(\"menu\", \"Menu\")}</span>\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent side=\"right\" className=\"w-[300px] px-6\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"menu\", \"Menu\")}</SheetTitle>\r\n </SheetHeader>\r\n <div className=\"flex flex-col space-y-4 mt-8\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n {item.name}\r\n </Link>\r\n ))}\r\n <Link\r\n to=\"/register\"\r\n className={cn(buttonVariants(), \"rounded-full mt-4\")}\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n {t(\"getStarted\", \"Get Started\")}\r\n </Link>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n </div>\r\n </div>\r\n </header>\r\n );\r\n}\r\n"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"path": "header-centered-pill/lang/en.json",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"path": "header-ecommerce/header-ecommerce.tsx",
|
|
21
21
|
"type": "registry:component",
|
|
22
22
|
"target": "$modules$/header-ecommerce/header-ecommerce.tsx",
|
|
23
|
-
"content": "import { useState } from \"react\";\r\nimport { Link, useNavigate } from \"react-router\";\r\nimport { ShoppingCart, Menu, Search, Heart, Package, User, LogOut } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport {\r\n Sheet,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetContent,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport {\r\n Dialog,\r\n DialogContent,\r\n DialogHeader,\r\n DialogTitle,\r\n DialogTrigger,\r\n} from \"@/components/ui/dialog\";\r\nimport {\r\n DropdownMenu,\r\n DropdownMenuContent,\r\n DropdownMenuItem,\r\n DropdownMenuLabel,\r\n DropdownMenuSeparator,\r\n DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { CartDrawer } from \"@/modules/cart-drawer\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport type { Product } from \"@/modules/ecommerce-core/types\";\r\nimport {\r\n useCart,\r\n useFavorites,\r\n useDbSearch,\r\n formatPrice,\r\n} from \"@/modules/ecommerce-core\";\r\n\r\nexport function HeaderEcommerce() {\r\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\r\n const [mobileSearchOpen, setMobileSearchOpen] = useState(false);\r\n const [desktopSearchOpen, setDesktopSearchOpen] = useState(false);\r\n const [showResults, setShowResults] = useState(false);\r\n const { itemCount, state } = useCart();\r\n const { favoriteCount } = useFavorites();\r\n const { isAuthenticated, user, logout } = useAuth();\r\n const navigate = useNavigate();\r\n const { t } = useTranslation(\"header-ecommerce\");\r\n\r\n const handleLogout = () => {\r\n logout();\r\n toast.success(t(\"logoutToastTitle\", \"Goodbye!\"), {\r\n description: t(\"logoutToastDesc\", \"You have been logged out successfully.\"),\r\n });\r\n };\r\n\r\n const {\r\n searchTerm,\r\n setSearchTerm,\r\n results: searchResults,\r\n clearSearch,\r\n } = useDbSearch();\r\n\r\n const handleSearchSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n if (searchTerm.trim()) {\r\n navigate(`/products?search=${encodeURIComponent(searchTerm)}`);\r\n setShowResults(false);\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }\r\n };\r\n\r\n const handleSearchFocus = () => {\r\n setShowResults(true);\r\n };\r\n\r\n const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n setSearchTerm(e.target.value);\r\n setShowResults(true);\r\n };\r\n\r\n const navigation = [\r\n { name: t(\"home\"), href: \"/\" },\r\n { name: t(\"products\"), href: \"/products\" },\r\n { name: t(\"about\"), href: \"/about\" },\r\n { name: t(\"contact\"), href: \"/contact\" },\r\n ];\r\n\r\n return (\r\n <header className=\"sticky top-0 z-50 w-full border-b border-border/20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\r\n <div className=\"flex h-14 sm:h-16 md:h-20 items-center justify-between gap-2\">\r\n {/* Logo */}\r\n <div className=\"flex-shrink-0 min-w-0\">\r\n <Logo size=\"sm\" className=\"text-base sm:text-xl lg:text-2xl\" />\r\n </div>\r\n\r\n {/* Desktop Navigation - Centered */}\r\n <nav className=\"hidden lg:flex items-center space-x-12 absolute left-1/2 transform -translate-x-1/2\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-base font-medium transition-colors hover:text-primary relative group py-2\"\r\n >\r\n {item.name}\r\n <span className=\"absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full\"></span>\r\n </Link>\r\n ))}\r\n </nav>\r\n\r\n {/* Search & Actions - Right Aligned */}\r\n <div className=\"flex items-center space-x-1 sm:space-x-2 lg:space-x-4 flex-shrink-0\">\r\n {/* Desktop Search - Modal */}\r\n <Dialog\r\n open={desktopSearchOpen}\r\n onOpenChange={setDesktopSearchOpen}\r\n >\r\n <DialogTrigger asChild>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"hidden lg:flex h-10 w-10\"\r\n >\r\n <Search className=\"h-5 w-5\" />\r\n </Button>\r\n </DialogTrigger>\r\n <DialogContent className=\"sm:max-w-2xl\">\r\n <DialogHeader>\r\n <DialogTitle>\r\n {t(\"searchProducts\", \"Search Products\")}\r\n </DialogTitle>\r\n </DialogHeader>\r\n <div className=\"space-y-4\">\r\n <form onSubmit={handleSearchSubmit}>\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\r\n \"searchPlaceholder\",\r\n \"Search for products...\"\r\n )}\r\n value={searchTerm}\r\n onChange={handleSearchChange}\r\n className=\"pl-11 h-12 text-base\"\r\n autoFocus\r\n />\r\n </div>\r\n </form>\r\n\r\n {/* Desktop Search Results */}\r\n {searchTerm.trim() && (\r\n <div className=\"max-h-[400px] overflow-y-auto rounded-lg border bg-card\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"divide-y\">\r\n <div className=\"px-4 py-3 bg-muted/50\">\r\n <p className=\"text-sm font-medium text-muted-foreground\">\r\n {searchResults.length}{\" \"}\r\n {searchResults.length === 1\r\n ? \"result\"\r\n : \"results\"}{\" \"}\r\n found\r\n </p>\r\n </div>\r\n {searchResults.slice(0, 8).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"flex items-center gap-4 p-4 hover:bg-muted/50 transition-colors\"\r\n >\r\n <img\r\n src={\r\n product.images[0] || \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-16 h-16 object-cover rounded flex-shrink-0\"\r\n />\r\n <div className=\"flex-1 min-w-0\">\r\n <h4 className=\"font-medium text-base line-clamp-1\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-sm text-muted-foreground capitalize\">\r\n {product.category}\r\n </p>\r\n <p className=\"text-base font-semibold text-primary mt-1\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n {searchResults.length > 8 && (\r\n <div className=\"px-4 py-3 bg-muted/30 text-center\">\r\n <button\r\n onClick={() => {\r\n navigate(\r\n `/products?search=${encodeURIComponent(\r\n searchTerm\r\n )}`\r\n );\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"text-sm font-medium text-primary hover:underline\"\r\n >\r\n {t(\r\n \"viewAllResults\",\r\n `View all ${searchResults.length} results`\r\n )}\r\n </button>\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"p-8 text-center\">\r\n <Search className=\"h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50\" />\r\n <p className=\"text-base text-muted-foreground\">\r\n {t(\"noResults\", \"No products found\")}\r\n </p>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\r\n \"tryDifferentKeywords\",\r\n \"Try different keywords\"\r\n )}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </DialogContent>\r\n </Dialog>\r\n\r\n {/* Search - Mobile (Hidden - moved to hamburger menu) */}\r\n <Dialog open={mobileSearchOpen} onOpenChange={setMobileSearchOpen}>\r\n <DialogTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"hidden\">\r\n <Search className=\"h-4 w-4 sm:h-5 sm:w-5\" />\r\n </Button>\r\n </DialogTrigger>\r\n <DialogContent className=\"sm:max-w-md\">\r\n <DialogHeader>\r\n <DialogTitle>{t(\"searchProducts\")}</DialogTitle>\r\n </DialogHeader>\r\n <form\r\n onSubmit={(e) => {\r\n e.preventDefault();\r\n if (searchTerm.trim()) {\r\n navigate(\r\n `/products?search=${encodeURIComponent(searchTerm)}`\r\n );\r\n setMobileSearchOpen(false);\r\n clearSearch();\r\n }\r\n }}\r\n className=\"space-y-4\"\r\n >\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={(e) => setSearchTerm(e.target.value)}\r\n className=\"pl-10\"\r\n autoFocus\r\n />\r\n </div>\r\n <div className=\"flex gap-2\">\r\n <Button type=\"submit\" className=\"flex-1\">\r\n {t(\"searchButton\", \"Search\")}\r\n </Button>\r\n <Button\r\n type=\"button\"\r\n variant=\"outline\"\r\n onClick={() => {\r\n clearSearch();\r\n setMobileSearchOpen(false);\r\n }}\r\n >\r\n {t(\"cancel\", \"Cancel\")}\r\n </Button>\r\n </div>\r\n </form>\r\n\r\n {/* Mobile Search Results */}\r\n {searchTerm.trim() && (\r\n <div className=\"mt-4 max-h-64 overflow-y-auto\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"space-y-2\">\r\n <p className=\"text-sm text-muted-foreground mb-2\">\r\n {searchResults.length} result\r\n {searchResults.length !== 1 ? \"s\" : \"\"} found\r\n </p>\r\n {searchResults.slice(0, 5).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setMobileSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"block p-2 rounded hover:bg-muted/50 transition-colors\"\r\n >\r\n <div className=\"flex items-center gap-3\">\r\n <img\r\n src={\r\n product.images[0] || \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-10 h-10 object-cover rounded\"\r\n />\r\n <div className=\"flex-1\">\r\n <h4 className=\"font-medium text-sm\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {product.category}\r\n </p>\r\n <p className=\"text-sm font-medium\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n </div>\r\n ) : (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"noResults\")}\r\n </p>\r\n )}\r\n </div>\r\n )}\r\n </DialogContent>\r\n </Dialog>\r\n\r\n {/* Wishlist - Desktop Only */}\r\n <Link to=\"/favorites\" className=\"hidden lg:block\">\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"relative h-10 w-10\"\r\n >\r\n <Heart className=\"h-5 w-5\" />\r\n {favoriteCount > 0 && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\r\n >\r\n {favoriteCount}\r\n </Badge>\r\n )}\r\n </Button>\r\n </Link>\r\n\r\n {/* Cart - Desktop Only (Goes to Cart Page) */}\r\n <Link to=\"/cart\" className=\"hidden lg:block\">\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"relative h-10 w-10\"\r\n >\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {itemCount > 0 && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\r\n >\r\n {itemCount}\r\n </Badge>\r\n )}\r\n </Button>\r\n </Link>\r\n\r\n {/* Auth - Desktop Only */}\r\n <div className=\"hidden lg:flex\">\r\n {isAuthenticated ? (\r\n <DropdownMenu>\r\n <DropdownMenuTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\r\n <User className=\"h-5 w-5\" />\r\n </Button>\r\n </DropdownMenuTrigger>\r\n <DropdownMenuContent align=\"end\" className=\"w-56\">\r\n <DropdownMenuLabel className=\"font-normal\">\r\n <div className=\"flex flex-col space-y-1\">\r\n <p className=\"text-sm font-medium\">{user?.username}</p>\r\n {user?.email && (\r\n <p className=\"text-xs text-muted-foreground\">{user.email}</p>\r\n )}\r\n </div>\r\n </DropdownMenuLabel>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuItem asChild className=\"cursor-pointer\">\r\n <Link to=\"/my-orders\" className=\"flex items-center\">\r\n <Package className=\"mr-2 h-4 w-4\" />\r\n {t(\"myOrders\", \"My Orders\")}\r\n </Link>\r\n </DropdownMenuItem>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuItem\r\n onClick={handleLogout}\r\n className=\"text-red-600 focus:text-red-600 focus:bg-red-50 cursor-pointer\"\r\n >\r\n <LogOut className=\"mr-2 h-4 w-4\" />\r\n {t(\"logout\", \"Logout\")}\r\n </DropdownMenuItem>\r\n </DropdownMenuContent>\r\n </DropdownMenu>\r\n ) : (\r\n <Link to=\"/login\">\r\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\r\n <User className=\"h-5 w-5\" />\r\n </Button>\r\n </Link>\r\n )}\r\n </div>\r\n\r\n {/* Mobile Menu */}\r\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\r\n <SheetTrigger asChild>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"lg:hidden h-8 w-8 sm:h-10 sm:w-10\"\r\n >\r\n <Menu className=\"h-4 w-4 sm:h-5 sm:w-5\" />\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent side=\"right\" className=\"w-[300px] sm:w-[400px] px-6\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"menu\")}</SheetTitle>\r\n </SheetHeader>\r\n\r\n {/* Mobile Search in Hamburger */}\r\n <div className=\"mt-6 pb-4 border-b\">\r\n <form onSubmit={handleSearchSubmit}>\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={handleSearchChange}\r\n onFocus={handleSearchFocus}\r\n className=\"pl-10 h-11\"\r\n />\r\n </div>\r\n </form>\r\n\r\n {/* Search Results in Hamburger */}\r\n {showResults && searchTerm && (\r\n <div className=\"mt-3 max-h-[300px] overflow-y-auto rounded-lg border bg-card\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"divide-y\">\r\n <div className=\"px-3 py-2 bg-muted/50\">\r\n <p className=\"text-xs font-medium text-muted-foreground\">\r\n {searchResults.length}{\" \"}\r\n {searchResults.length === 1\r\n ? \"result\"\r\n : \"results\"}\r\n </p>\r\n </div>\r\n {searchResults.slice(0, 5).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setMobileMenuOpen(false);\r\n clearSearch();\r\n setShowResults(false);\r\n }}\r\n className=\"flex items-center gap-3 p-3 hover:bg-muted/50 transition-colors\"\r\n >\r\n <img\r\n src={\r\n product.images[0] || \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-14 h-14 object-cover rounded flex-shrink-0\"\r\n />\r\n <div className=\"flex-1 min-w-0\">\r\n <h4 className=\"font-medium text-sm line-clamp-1\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-xs text-muted-foreground capitalize\">\r\n {product.category}\r\n </p>\r\n <p className=\"text-sm font-semibold text-primary mt-1\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n {searchResults.length > 5 && (\r\n <div className=\"px-3 py-2 bg-muted/30 text-center\">\r\n <button\r\n onClick={() => {\r\n navigate(\r\n `/products?search=${encodeURIComponent(\r\n searchTerm\r\n )}`\r\n );\r\n setMobileMenuOpen(false);\r\n clearSearch();\r\n setShowResults(false);\r\n }}\r\n className=\"text-xs font-medium text-primary hover:underline\"\r\n >\r\n {t(\r\n \"viewAllResults\",\r\n `View all ${searchResults.length} results`\r\n )}\r\n </button>\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"p-6 text-center\">\r\n <Search className=\"h-8 w-8 text-muted-foreground mx-auto mb-2 opacity-50\" />\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"noResults\", \"No results found\")}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n\r\n <div className=\"flex flex-col space-y-4 mt-6\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n {item.name}\r\n </Link>\r\n ))}\r\n <div className=\"border-t pt-4 space-y-4\">\r\n <Link\r\n to=\"/favorites\"\r\n className=\"flex items-center justify-between text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <div className=\"flex items-center space-x-2\">\r\n <Heart className=\"h-5 w-5\" />\r\n <span>{t(\"favorites\")}</span>\r\n </div>\r\n <Badge variant=\"secondary\">{favoriteCount}</Badge>\r\n </Link>\r\n <Link\r\n to=\"/cart\"\r\n className=\"flex items-center justify-between w-full text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <div className=\"flex items-center space-x-2\">\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n <span>{t(\"cart\")}</span>\r\n </div>\r\n <div className=\"flex flex-col items-end\">\r\n <Badge variant=\"secondary\">{itemCount}</Badge>\r\n <span className=\"text-xs text-muted-foreground\">\r\n {formatPrice(state.total, constants.site.currency)}\r\n </span>\r\n </div>\r\n </Link>\r\n\r\n {/* Auth - Mobile */}\r\n {isAuthenticated ? (\r\n <div className=\"space-y-3\">\r\n <div className=\"flex items-center space-x-3 p-3 bg-muted/50 rounded-lg\">\r\n <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <User className=\"h-5 w-5 text-primary\" />\r\n </div>\r\n <div className=\"flex-1 min-w-0\">\r\n <p className=\"text-sm font-medium truncate\">{user?.username}</p>\r\n {user?.email && (\r\n <p className=\"text-xs text-muted-foreground truncate\">{user.email}</p>\r\n )}\r\n </div>\r\n </div>\r\n <Link\r\n to=\"/my-orders\"\r\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <Package className=\"h-5 w-5\" />\r\n <span>{t(\"myOrders\", \"My Orders\")}</span>\r\n </Link>\r\n <button\r\n onClick={() => {\r\n handleLogout();\r\n setMobileMenuOpen(false);\r\n }}\r\n className=\"flex items-center space-x-2 text-lg font-medium text-red-600 hover:text-red-700 transition-colors w-full\"\r\n >\r\n <LogOut className=\"h-5 w-5\" />\r\n <span>{t(\"logout\", \"Logout\")}</span>\r\n </button>\r\n </div>\r\n ) : (\r\n <Link\r\n to=\"/login\"\r\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <User className=\"h-5 w-5\" />\r\n <span>{t(\"login\", \"Login\")}</span>\r\n </Link>\r\n )}\r\n </div>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n </div>\r\n </div>\r\n </div>\r\n {/* Cart Drawer */}\r\n <CartDrawer showTrigger={false} />\r\n </header>\r\n );\r\n}\r\n"
|
|
23
|
+
"content": "import { useState, useMemo } from \"react\";\r\nimport { useDebouncedValue } from \"@/hooks/use-debounced-value\";\r\nimport { Link, useNavigate } from \"react-router\";\r\nimport { ShoppingCart, Menu, Search, Heart, Package, User, LogOut } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport {\r\n Sheet,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetContent,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport {\r\n Dialog,\r\n DialogContent,\r\n DialogHeader,\r\n DialogTitle,\r\n DialogTrigger,\r\n} from \"@/components/ui/dialog\";\r\nimport {\r\n DropdownMenu,\r\n DropdownMenuContent,\r\n DropdownMenuItem,\r\n DropdownMenuLabel,\r\n DropdownMenuSeparator,\r\n DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { CartDrawer } from \"@/modules/cart-drawer\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport type { Product, Category } from \"@/modules/ecommerce-core/types\";\r\nimport {\r\n useCart,\r\n useFavorites,\r\n formatPrice,\r\n} from \"@/modules/ecommerce-core\";\r\nimport { useDbList } from \"@/db\";\r\n\r\nexport function HeaderEcommerce() {\r\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\r\n const [mobileSearchOpen, setMobileSearchOpen] = useState(false);\r\n const [desktopSearchOpen, setDesktopSearchOpen] = useState(false);\r\n const [showResults, setShowResults] = useState(false);\r\n const { itemCount, state } = useCart();\r\n const { favoriteCount } = useFavorites();\r\n const { isAuthenticated, user, logout } = useAuth();\r\n const navigate = useNavigate();\r\n const { t } = useTranslation(\"header-ecommerce\");\r\n\r\n const handleLogout = () => {\r\n logout();\r\n toast.success(t(\"logoutToastTitle\", \"Goodbye!\"), {\r\n description: t(\"logoutToastDesc\", \"You have been logged out successfully.\"),\r\n });\r\n };\r\n\r\n const [searchTerm, setSearchTerm] = useState(\"\");\r\n const debouncedTerm = useDebouncedValue(searchTerm, 300);\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 { data: searchResults = [] } = useDbList<Product>(\"products\", {\r\n where: debouncedTerm ? { name: { $like: `%${debouncedTerm}%` } } : {},\r\n limit: 20,\r\n enabled: debouncedTerm.length > 0,\r\n });\r\n\r\n const clearSearch = () => { setSearchTerm(\"\"); };\r\n\r\n const handleSearchSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n if (searchTerm.trim()) {\r\n navigate(`/products?search=${encodeURIComponent(searchTerm)}`);\r\n setShowResults(false);\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }\r\n };\r\n\r\n const handleSearchFocus = () => {\r\n setShowResults(true);\r\n };\r\n\r\n const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n setSearchTerm(e.target.value);\r\n setShowResults(true);\r\n };\r\n\r\n const navigation = [\r\n { name: t(\"home\"), href: \"/\" },\r\n { name: t(\"products\"), href: \"/products\" },\r\n { name: t(\"about\"), href: \"/about\" },\r\n { name: t(\"contact\"), href: \"/contact\" },\r\n ];\r\n\r\n return (\r\n <header className=\"sticky top-0 z-50 w-full border-b border-border/20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\r\n <div className=\"flex h-14 sm:h-16 md:h-20 items-center justify-between gap-2\">\r\n {/* Logo */}\r\n <div className=\"flex-shrink-0 min-w-0\">\r\n <Logo size=\"sm\" className=\"text-base sm:text-xl lg:text-2xl\" />\r\n </div>\r\n\r\n {/* Desktop Navigation - Centered */}\r\n <nav className=\"hidden lg:flex items-center space-x-12 absolute left-1/2 transform -translate-x-1/2\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-base font-medium transition-colors hover:text-primary relative group py-2\"\r\n >\r\n {item.name}\r\n <span className=\"absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full\"></span>\r\n </Link>\r\n ))}\r\n </nav>\r\n\r\n {/* Search & Actions - Right Aligned */}\r\n <div className=\"flex items-center space-x-1 sm:space-x-2 lg:space-x-4 flex-shrink-0\">\r\n {/* Desktop Search - Modal */}\r\n <Dialog\r\n open={desktopSearchOpen}\r\n onOpenChange={setDesktopSearchOpen}\r\n >\r\n <DialogTrigger asChild>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"hidden lg:flex h-10 w-10\"\r\n >\r\n <Search className=\"h-5 w-5\" />\r\n </Button>\r\n </DialogTrigger>\r\n <DialogContent className=\"sm:max-w-2xl\">\r\n <DialogHeader>\r\n <DialogTitle>\r\n {t(\"searchProducts\", \"Search Products\")}\r\n </DialogTitle>\r\n </DialogHeader>\r\n <div className=\"space-y-4\">\r\n <form onSubmit={handleSearchSubmit}>\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\r\n \"searchPlaceholder\",\r\n \"Search for products...\"\r\n )}\r\n value={searchTerm}\r\n onChange={handleSearchChange}\r\n className=\"pl-11 h-12 text-base\"\r\n autoFocus\r\n />\r\n </div>\r\n </form>\r\n\r\n {/* Desktop Search Results */}\r\n {searchTerm.trim() && (\r\n <div className=\"max-h-[400px] overflow-y-auto rounded-lg border bg-card\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"divide-y\">\r\n <div className=\"px-4 py-3 bg-muted/50\">\r\n <p className=\"text-sm font-medium text-muted-foreground\">\r\n {searchResults.length}{\" \"}\r\n {searchResults.length === 1\r\n ? \"result\"\r\n : \"results\"}{\" \"}\r\n found\r\n </p>\r\n </div>\r\n {searchResults.slice(0, 8).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"flex items-center gap-4 p-4 hover:bg-muted/50 transition-colors\"\r\n >\r\n <img\r\n src={\r\n product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-16 h-16 object-cover rounded flex-shrink-0\"\r\n />\r\n <div className=\"flex-1 min-w-0\">\r\n <h4 className=\"font-medium text-base line-clamp-1\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-sm text-muted-foreground capitalize\">\r\n {categoryMap.get(product.categories?.[0] as number)?.name}\r\n </p>\r\n <p className=\"text-base font-semibold text-primary mt-1\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n {searchResults.length > 8 && (\r\n <div className=\"px-4 py-3 bg-muted/30 text-center\">\r\n <button\r\n onClick={() => {\r\n navigate(\r\n `/products?search=${encodeURIComponent(\r\n searchTerm\r\n )}`\r\n );\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"text-sm font-medium text-primary hover:underline\"\r\n >\r\n {t(\r\n \"viewAllResults\",\r\n `View all ${searchResults.length} results`\r\n )}\r\n </button>\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"p-8 text-center\">\r\n <Search className=\"h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50\" />\r\n <p className=\"text-base text-muted-foreground\">\r\n {t(\"noResults\", \"No products found\")}\r\n </p>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\r\n \"tryDifferentKeywords\",\r\n \"Try different keywords\"\r\n )}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </DialogContent>\r\n </Dialog>\r\n\r\n {/* Search - Mobile (Hidden - moved to hamburger menu) */}\r\n <Dialog open={mobileSearchOpen} onOpenChange={setMobileSearchOpen}>\r\n <DialogTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"hidden\">\r\n <Search className=\"h-4 w-4 sm:h-5 sm:w-5\" />\r\n </Button>\r\n </DialogTrigger>\r\n <DialogContent className=\"sm:max-w-md\">\r\n <DialogHeader>\r\n <DialogTitle>{t(\"searchProducts\")}</DialogTitle>\r\n </DialogHeader>\r\n <form\r\n onSubmit={(e) => {\r\n e.preventDefault();\r\n if (searchTerm.trim()) {\r\n navigate(\r\n `/products?search=${encodeURIComponent(searchTerm)}`\r\n );\r\n setMobileSearchOpen(false);\r\n clearSearch();\r\n }\r\n }}\r\n className=\"space-y-4\"\r\n >\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={(e) => setSearchTerm(e.target.value)}\r\n className=\"pl-10\"\r\n autoFocus\r\n />\r\n </div>\r\n <div className=\"flex gap-2\">\r\n <Button type=\"submit\" className=\"flex-1\">\r\n {t(\"searchButton\", \"Search\")}\r\n </Button>\r\n <Button\r\n type=\"button\"\r\n variant=\"outline\"\r\n onClick={() => {\r\n clearSearch();\r\n setMobileSearchOpen(false);\r\n }}\r\n >\r\n {t(\"cancel\", \"Cancel\")}\r\n </Button>\r\n </div>\r\n </form>\r\n\r\n {/* Mobile Search Results */}\r\n {searchTerm.trim() && (\r\n <div className=\"mt-4 max-h-64 overflow-y-auto\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"space-y-2\">\r\n <p className=\"text-sm text-muted-foreground mb-2\">\r\n {searchResults.length} result\r\n {searchResults.length !== 1 ? \"s\" : \"\"} found\r\n </p>\r\n {searchResults.slice(0, 5).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setMobileSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"block p-2 rounded hover:bg-muted/50 transition-colors\"\r\n >\r\n <div className=\"flex items-center gap-3\">\r\n <img\r\n src={\r\n product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-10 h-10 object-cover rounded\"\r\n />\r\n <div className=\"flex-1\">\r\n <h4 className=\"font-medium text-sm\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {categoryMap.get(product.categories?.[0] as number)?.name}\r\n </p>\r\n <p className=\"text-sm font-medium\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n </div>\r\n ) : (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"noResults\")}\r\n </p>\r\n )}\r\n </div>\r\n )}\r\n </DialogContent>\r\n </Dialog>\r\n\r\n {/* Wishlist - Desktop Only */}\r\n <Link to=\"/favorites\" className=\"hidden lg:block\">\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"relative h-10 w-10\"\r\n >\r\n <Heart className=\"h-5 w-5\" />\r\n {favoriteCount > 0 && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\r\n >\r\n {favoriteCount}\r\n </Badge>\r\n )}\r\n </Button>\r\n </Link>\r\n\r\n {/* Cart - Desktop Only (Goes to Cart Page) */}\r\n <Link to=\"/cart\" className=\"hidden lg:block\">\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"relative h-10 w-10\"\r\n >\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {itemCount > 0 && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\r\n >\r\n {itemCount}\r\n </Badge>\r\n )}\r\n </Button>\r\n </Link>\r\n\r\n {/* Auth - Desktop Only */}\r\n <div className=\"hidden lg:flex\">\r\n {isAuthenticated ? (\r\n <DropdownMenu>\r\n <DropdownMenuTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\r\n <User className=\"h-5 w-5\" />\r\n </Button>\r\n </DropdownMenuTrigger>\r\n <DropdownMenuContent align=\"end\" className=\"w-56\">\r\n <DropdownMenuLabel className=\"font-normal\">\r\n <div className=\"flex flex-col space-y-1\">\r\n <p className=\"text-sm font-medium\">{user?.username}</p>\r\n {user?.email && (\r\n <p className=\"text-xs text-muted-foreground\">{user.email}</p>\r\n )}\r\n </div>\r\n </DropdownMenuLabel>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuItem asChild className=\"cursor-pointer\">\r\n <Link to=\"/my-orders\" className=\"flex items-center\">\r\n <Package className=\"mr-2 h-4 w-4\" />\r\n {t(\"myOrders\", \"My Orders\")}\r\n </Link>\r\n </DropdownMenuItem>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuItem\r\n onClick={handleLogout}\r\n className=\"text-red-600 focus:text-red-600 focus:bg-red-50 cursor-pointer\"\r\n >\r\n <LogOut className=\"mr-2 h-4 w-4\" />\r\n {t(\"logout\", \"Logout\")}\r\n </DropdownMenuItem>\r\n </DropdownMenuContent>\r\n </DropdownMenu>\r\n ) : (\r\n <Link to=\"/login\">\r\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\r\n <User className=\"h-5 w-5\" />\r\n </Button>\r\n </Link>\r\n )}\r\n </div>\r\n\r\n {/* Mobile Menu */}\r\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\r\n <SheetTrigger asChild>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"lg:hidden h-8 w-8 sm:h-10 sm:w-10\"\r\n >\r\n <Menu className=\"h-4 w-4 sm:h-5 sm:w-5\" />\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent side=\"right\" className=\"w-[300px] sm:w-[400px] px-6\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"menu\")}</SheetTitle>\r\n </SheetHeader>\r\n\r\n {/* Mobile Search in Hamburger */}\r\n <div className=\"mt-6 pb-4 border-b\">\r\n <form onSubmit={handleSearchSubmit}>\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={handleSearchChange}\r\n onFocus={handleSearchFocus}\r\n className=\"pl-10 h-11\"\r\n />\r\n </div>\r\n </form>\r\n\r\n {/* Search Results in Hamburger */}\r\n {showResults && searchTerm && (\r\n <div className=\"mt-3 max-h-[300px] overflow-y-auto rounded-lg border bg-card\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"divide-y\">\r\n <div className=\"px-3 py-2 bg-muted/50\">\r\n <p className=\"text-xs font-medium text-muted-foreground\">\r\n {searchResults.length}{\" \"}\r\n {searchResults.length === 1\r\n ? \"result\"\r\n : \"results\"}\r\n </p>\r\n </div>\r\n {searchResults.slice(0, 5).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setMobileMenuOpen(false);\r\n clearSearch();\r\n setShowResults(false);\r\n }}\r\n className=\"flex items-center gap-3 p-3 hover:bg-muted/50 transition-colors\"\r\n >\r\n <img\r\n src={\r\n product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-14 h-14 object-cover rounded flex-shrink-0\"\r\n />\r\n <div className=\"flex-1 min-w-0\">\r\n <h4 className=\"font-medium text-sm line-clamp-1\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-xs text-muted-foreground capitalize\">\r\n {categoryMap.get(product.categories?.[0] as number)?.name}\r\n </p>\r\n <p className=\"text-sm font-semibold text-primary mt-1\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n {searchResults.length > 5 && (\r\n <div className=\"px-3 py-2 bg-muted/30 text-center\">\r\n <button\r\n onClick={() => {\r\n navigate(\r\n `/products?search=${encodeURIComponent(\r\n searchTerm\r\n )}`\r\n );\r\n setMobileMenuOpen(false);\r\n clearSearch();\r\n setShowResults(false);\r\n }}\r\n className=\"text-xs font-medium text-primary hover:underline\"\r\n >\r\n {t(\r\n \"viewAllResults\",\r\n `View all ${searchResults.length} results`\r\n )}\r\n </button>\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"p-6 text-center\">\r\n <Search className=\"h-8 w-8 text-muted-foreground mx-auto mb-2 opacity-50\" />\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"noResults\", \"No results found\")}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n\r\n <div className=\"flex flex-col space-y-4 mt-6\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n {item.name}\r\n </Link>\r\n ))}\r\n <div className=\"border-t pt-4 space-y-4\">\r\n <Link\r\n to=\"/favorites\"\r\n className=\"flex items-center justify-between text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <div className=\"flex items-center space-x-2\">\r\n <Heart className=\"h-5 w-5\" />\r\n <span>{t(\"favorites\")}</span>\r\n </div>\r\n <Badge variant=\"secondary\">{favoriteCount}</Badge>\r\n </Link>\r\n <Link\r\n to=\"/cart\"\r\n className=\"flex items-center justify-between w-full text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <div className=\"flex items-center space-x-2\">\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n <span>{t(\"cart\")}</span>\r\n </div>\r\n <div className=\"flex flex-col items-end\">\r\n <Badge variant=\"secondary\">{itemCount}</Badge>\r\n <span className=\"text-xs text-muted-foreground\">\r\n {formatPrice(state.total, constants.site.currency)}\r\n </span>\r\n </div>\r\n </Link>\r\n\r\n {/* Auth - Mobile */}\r\n {isAuthenticated ? (\r\n <div className=\"space-y-3\">\r\n <div className=\"flex items-center space-x-3 p-3 bg-muted/50 rounded-lg\">\r\n <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <User className=\"h-5 w-5 text-primary\" />\r\n </div>\r\n <div className=\"flex-1 min-w-0\">\r\n <p className=\"text-sm font-medium truncate\">{user?.username}</p>\r\n {user?.email && (\r\n <p className=\"text-xs text-muted-foreground truncate\">{user.email}</p>\r\n )}\r\n </div>\r\n </div>\r\n <Link\r\n to=\"/my-orders\"\r\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <Package className=\"h-5 w-5\" />\r\n <span>{t(\"myOrders\", \"My Orders\")}</span>\r\n </Link>\r\n <button\r\n onClick={() => {\r\n handleLogout();\r\n setMobileMenuOpen(false);\r\n }}\r\n className=\"flex items-center space-x-2 text-lg font-medium text-red-600 hover:text-red-700 transition-colors w-full\"\r\n >\r\n <LogOut className=\"h-5 w-5\" />\r\n <span>{t(\"logout\", \"Logout\")}</span>\r\n </button>\r\n </div>\r\n ) : (\r\n <Link\r\n to=\"/login\"\r\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <User className=\"h-5 w-5\" />\r\n <span>{t(\"login\", \"Login\")}</span>\r\n </Link>\r\n )}\r\n </div>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n </div>\r\n </div>\r\n </div>\r\n {/* Cart Drawer */}\r\n <CartDrawer showTrigger={false} />\r\n </header>\r\n );\r\n}\r\n"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "header-ecommerce/lang/en.json",
|
package/dist/registry/index.json
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"path": "login-page/login-page.tsx",
|
|
24
24
|
"type": "registry:page",
|
|
25
25
|
"target": "$modules$/login-page/login-page.tsx",
|
|
26
|
-
"content": "import { useState } from \"react\";\r\nimport { Link, useNavigate, useLocation } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { toast } from \"sonner\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\n\r\ninterface LoginPageProps {\r\n className?: string;\r\n}\r\n\r\nexport function LoginPage({ className }: LoginPageProps) {\r\n const { t } = useTranslation(\"login-page\");\r\n usePageTitle({ title: t(\"title\", \"Sign In\") });\r\n const navigate = useNavigate();\r\n const location = useLocation();\r\n const { login } = useAuth();\r\n\r\n const [username, setUsername] = useState(\"\");\r\n const [password, setPassword] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const from = (location.state as { from?: string })?.from || \"/\";\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n setIsLoading(true);\r\n\r\n try {\r\n await login(username, password);\r\n toast.success(t(\"loginSuccess\", \"Login successful!\"));\r\n navigate(from, { replace: true });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"loginError\", \"Login failed. Please check your credentials.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n return (\r\n <section\r\n className={cn(\"flex min-h-screen bg-muted/30 px-4 py-16 md:py-32\", className)}\r\n >\r\n <form\r\n onSubmit={handleSubmit}\r\n className=\"bg-muted m-auto h-fit w-full max-w-sm overflow-hidden rounded-xl border shadow-md\"\r\n >\r\n <div className=\"bg-card -m-px rounded-xl border p-8 pb-6\">\r\n <div className=\"text-center\">\r\n <
|
|
26
|
+
"content": "import { useState } from \"react\";\r\nimport { Link, useNavigate, useLocation } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { toast } from \"sonner\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\n\r\ninterface LoginPageProps {\r\n className?: string;\r\n}\r\n\r\nexport function LoginPage({ className }: LoginPageProps) {\r\n const { t } = useTranslation(\"login-page\");\r\n usePageTitle({ title: t(\"title\", \"Sign In\") });\r\n const navigate = useNavigate();\r\n const location = useLocation();\r\n const { login } = useAuth();\r\n\r\n const [username, setUsername] = useState(\"\");\r\n const [password, setPassword] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const from = (location.state as { from?: string })?.from || \"/\";\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n setIsLoading(true);\r\n\r\n try {\r\n await login(username, password);\r\n toast.success(t(\"loginSuccess\", \"Login successful!\"));\r\n navigate(from, { replace: true });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"loginError\", \"Login failed. Please check your credentials.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n return (\r\n <section\r\n className={cn(\"flex min-h-screen bg-muted/30 px-4 py-16 md:py-32\", className)}\r\n >\r\n <form\r\n onSubmit={handleSubmit}\r\n className=\"bg-muted m-auto h-fit w-full max-w-sm overflow-hidden rounded-xl border shadow-md\"\r\n >\r\n <div className=\"bg-card -m-px rounded-xl border p-8 pb-6\">\r\n <div className=\"text-center\">\r\n <div className=\"mx-auto block w-fit\">\r\n <Logo size=\"sm\" />\r\n </div>\r\n <h1 className=\"mb-1 mt-4 text-xl font-semibold\">\r\n {t(\"title\", \"Sign In\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"subtitle\", \"Welcome back! Sign in to continue\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"mt-4 p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <div className=\"mt-6 space-y-6\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"username\" className=\"block text-sm\">\r\n {t(\"username\", \"Username\")}\r\n </Label>\r\n <Input\r\n type=\"text\"\r\n required\r\n name=\"username\"\r\n id=\"username\"\r\n autoComplete=\"username\"\r\n value={username}\r\n onChange={(e) => setUsername(e.target.value)}\r\n placeholder={t(\"usernamePlaceholder\", \"Enter your username\")}\r\n disabled={isLoading}\r\n />\r\n </div>\r\n\r\n <div className=\"space-y-2\">\r\n <div className=\"flex items-center justify-between\">\r\n <Label htmlFor=\"password\" className=\"text-sm\">\r\n {t(\"password\", \"Password\")}\r\n </Label>\r\n <Button asChild variant=\"link\" size=\"sm\" className=\"h-auto p-0\">\r\n <Link to=\"/forgot-password\" className=\"text-xs\">\r\n {t(\"forgotPassword\", \"Forgot password?\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n <Input\r\n type=\"password\"\r\n required\r\n name=\"password\"\r\n id=\"password\"\r\n value={password}\r\n onChange={(e) => setPassword(e.target.value)}\r\n placeholder=\"••••••••\"\r\n disabled={isLoading}\r\n />\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\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(\"signingIn\", \"Signing in...\")}\r\n </>\r\n ) : (\r\n t(\"signIn\", \"Sign In\")\r\n )}\r\n </Button>\r\n </div>\r\n </div>\r\n\r\n <div className=\"p-3\">\r\n <p className=\"text-center text-sm text-muted-foreground\">\r\n {t(\"noAccount\", \"Don't have an account?\")}\r\n <Button asChild variant=\"link\" className=\"px-2\">\r\n <Link to=\"/register\">{t(\"createAccount\", \"Create account\")}</Link>\r\n </Button>\r\n </p>\r\n </div>\r\n </form>\r\n </section>\r\n );\r\n}\r\n\r\nexport default LoginPage;\r\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "login-page/lang/en.json",
|