@promakeai/cli 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +214 -135
- package/dist/registry/about-page.json +1 -1
- package/dist/registry/about-section.json +1 -1
- package/dist/registry/api.json +55 -0
- package/dist/registry/auth.json +70 -0
- package/dist/registry/bento-grid-section.json +1 -1
- package/dist/registry/blog-list-page.json +1 -1
- package/dist/registry/blog-section.json +1 -1
- package/dist/registry/cart-drawer.json +1 -1
- package/dist/registry/cart-page.json +3 -2
- package/dist/registry/category-section.json +1 -1
- package/dist/registry/checkout-page.json +3 -2
- package/dist/registry/contact-info-grid.json +1 -1
- package/dist/registry/contact-page-centered.json +1 -1
- package/dist/registry/contact-page-map-overlay.json +1 -1
- package/dist/registry/contact-page.json +1 -1
- package/dist/registry/cookies-page.json +1 -1
- package/dist/registry/cta-section.json +1 -1
- package/dist/registry/db.json +129 -0
- package/dist/registry/docs/cart-page.md +1 -0
- package/dist/registry/docs/checkout-page.md +1 -0
- package/dist/registry/docs/forgot-password-page.md +37 -0
- package/dist/registry/docs/header-ecommerce.md +1 -0
- package/dist/registry/docs/products-page.md +1 -0
- package/dist/registry/docs/register-page.md +39 -0
- package/dist/registry/ecommerce-core.json +1 -1
- package/dist/registry/empty-page.json +1 -1
- package/dist/registry/faq-categorized.json +1 -1
- package/dist/registry/faq-simple.json +1 -1
- package/dist/registry/favorites-blog-block.json +1 -1
- package/dist/registry/favorites-ecommerce-block.json +1 -1
- package/dist/registry/feature-section.json +1 -1
- package/dist/registry/featured-products.json +1 -1
- package/dist/registry/footer-detailed.json +1 -1
- package/dist/registry/footer-minimal.json +3 -3
- package/dist/registry/footer.json +1 -1
- package/dist/registry/forgot-password-page.json +49 -0
- package/dist/registry/header-ecommerce.json +3 -2
- package/dist/registry/header-mega.json +1 -1
- package/dist/registry/header-minimal.json +1 -1
- package/dist/registry/header-simple.json +1 -1
- package/dist/registry/hero-cta.json +1 -1
- package/dist/registry/hero-gradient.json +1 -1
- package/dist/registry/hero-profile.json +1 -1
- package/dist/registry/hero.json +1 -1
- package/dist/registry/index.json +3 -0
- package/dist/registry/orders-list-block.json +1 -1
- package/dist/registry/payment-success-block.json +1 -1
- package/dist/registry/post-detail-block.json +1 -1
- package/dist/registry/pricing-section.json +1 -1
- package/dist/registry/privacy-page.json +1 -1
- package/dist/registry/products-page.json +3 -2
- package/dist/registry/register-page.json +49 -0
- package/dist/registry/related-posts-block.json +1 -1
- package/dist/registry/terms-page.json +1 -1
- package/dist/registry/testimonials-carousel.json +1 -1
- package/dist/registry/testimonials-grid.json +1 -1
- package/package.json +1 -1
- package/template/src/App.tsx +3 -24
- package/template/src/components/Layout.tsx +0 -4
- package/template/src/index.css +1 -0
- package/template/src/lang/en/index.json +1 -28
- package/template/src/lang/tr/index.json +1 -28
- package/template/src/pages/Index.tsx +1 -102
- package/template/src/components/Footer.tsx +0 -100
- package/template/src/components/Header.tsx +0 -79
- package/template/src/components/Hero.tsx +0 -69
- package/template/src/modules/api/USAGE.md +0 -515
- package/template/src/modules/api/customer-client.ts +0 -20
- package/template/src/modules/api/get-error-message.ts +0 -18
- package/template/src/modules/api/validation/en.json +0 -29
- package/template/src/modules/api/validation/tr.json +0 -29
- package/template/src/modules/auth/USAGE.md +0 -248
- package/template/src/modules/auth/auth-header-menu.tsx +0 -123
- package/template/src/modules/auth/auth-store.ts +0 -57
- package/template/src/modules/auth/forgot-password-page.tsx +0 -371
- package/template/src/modules/auth/login-page.tsx +0 -183
- package/template/src/modules/auth/register-page.tsx +0 -252
- package/template/src/modules/auth/use-auth.ts +0 -273
- package/template/src/modules/db/adapters/IDataAdapter.ts +0 -26
- package/template/src/modules/db/adapters/SqliteAdapter.ts +0 -364
- package/template/src/modules/db/adapters/index.ts +0 -2
- package/template/src/modules/db/config.ts +0 -59
- package/template/src/modules/db/core/DataManager.ts +0 -125
- package/template/src/modules/db/core/types.ts +0 -101
- package/template/src/modules/db/index.ts +0 -42
- package/template/src/modules/db/react/QueryProvider.tsx +0 -16
- package/template/src/modules/db/react/index.ts +0 -23
- package/template/src/modules/db/react/queryClient.ts +0 -64
- package/template/src/modules/db/react/useRepository.ts +0 -400
- package/template/src/modules/db/utils/parsers.ts +0 -96
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "db",
|
|
3
|
+
"type": "registry:module",
|
|
4
|
+
"title": "Database Module",
|
|
5
|
+
"description": "Universal data management with adapter pattern and React Query integration. Includes SQLite adapter for client-side persistence, DataManager for CRUD operations, and useRepository hook for React components. Supports complex queries, pagination, joins, and type-safe operations.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"sql.js",
|
|
8
|
+
"@tanstack/react-query"
|
|
9
|
+
],
|
|
10
|
+
"registryDependencies": [],
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "db/index.ts",
|
|
14
|
+
"type": "registry:index",
|
|
15
|
+
"target": "$modules$/db/index.ts",
|
|
16
|
+
"content": "/**\n * DB Module\n * Universal data management with adapter pattern + React Query\n */\n\n// Core\nexport { DataManager } from \"./core/DataManager\";\nexport type {\n QueryOptions,\n OrderBy,\n PaginatedResult,\n // Complex query types\n WhereOperator,\n WhereCondition,\n WhereGroup,\n JoinType,\n JoinClause,\n} from \"./core/types\";\n\n// Adapters\nexport type { IDataAdapter } from \"./adapters/IDataAdapter\";\nexport { SqliteAdapter } from \"./adapters/SqliteAdapter\";\n\n// Config\nexport { getAdapter, createAdapter, resetAdapter } from \"./config\";\nexport type { DataConfig, AdapterType } from \"./config\";\n\n// React exports (re-export from react/index.ts)\nexport * from \"./react\";\n\n// Re-export for convenience\nexport { DBQueryProvider as QueryProvider } from \"./react\";\n\n// Utility exports (parsers for client-side data transformation)\nexport {\n parseCommaSeparatedString,\n parseJSONStringToArray,\n parseStringToArray,\n parseJSONString,\n parseSQLiteBoolean,\n parseNumberSafe,\n} from \"./utils/parsers\";\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "db/config.ts",
|
|
20
|
+
"type": "registry:lib",
|
|
21
|
+
"target": "$modules$/db/config.ts",
|
|
22
|
+
"content": "import type { IDataAdapter } from \"./adapters/IDataAdapter\";\nimport { SqliteAdapter } from \"./adapters/SqliteAdapter\";\n\nexport type AdapterType = \"sqlite\";\n\nexport interface DataConfig {\n adapter: AdapterType;\n options?: {\n dbPath?: string;\n };\n}\n\n/**\n * Get configuration from environment variables\n */\nfunction getConfig(): DataConfig {\n const adapter =\n (import.meta.env.VITE_DATA_ADAPTER as AdapterType) || \"sqlite\";\n\n return {\n adapter,\n options: {\n dbPath: import.meta.env.VITE_DB_PATH || \"/data/database.db\",\n },\n };\n}\n\n/**\n * Create adapter instance based on configuration\n */\nexport function createAdapter(config: DataConfig): IDataAdapter {\n switch (config.adapter) {\n case \"sqlite\":\n return new SqliteAdapter(config.options?.dbPath);\n\n default:\n throw new Error(`Unknown adapter: ${config.adapter}`);\n }\n}\n\n/**\n * Get the configured adapter instance (singleton)\n */\nlet adapterInstance: IDataAdapter | null = null;\n\nexport function getAdapter(): IDataAdapter {\n if (!adapterInstance) {\n const config = getConfig();\n adapterInstance = createAdapter(config);\n }\n return adapterInstance;\n}\n\n/**\n * Reset adapter instance (useful for testing or switching adapters)\n */\nexport function resetAdapter(): void {\n adapterInstance = null;\n}\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "db/core/DataManager.ts",
|
|
26
|
+
"type": "registry:lib",
|
|
27
|
+
"target": "$modules$/db/core/DataManager.ts",
|
|
28
|
+
"content": "import type { IDataAdapter } from \"../adapters/IDataAdapter\";\nimport type { QueryOptions, PaginatedResult } from \"./types\";\n\n/**\n * DataManager - Simple proxy to adapter\n */\nexport class DataManager {\n private static instance: DataManager;\n private adapter: IDataAdapter;\n\n private constructor(adapter: IDataAdapter) {\n this.adapter = adapter;\n }\n\n static getInstance(adapter: IDataAdapter): DataManager {\n if (!DataManager.instance) {\n DataManager.instance = new DataManager(adapter);\n }\n return DataManager.instance;\n }\n\n // Simple pass-through methods - no cache logic\n async query<T = any>(table: string, options?: QueryOptions): Promise<T[]> {\n return this.adapter.findMany<T>(table, options);\n }\n\n async queryOne<T = any>(\n table: string,\n options?: QueryOptions,\n ): Promise<T | null> {\n const results = await this.query<T>(table, { ...options, limit: 1 });\n return results[0] || null;\n }\n\n async queryById<T = any>(\n table: string,\n id: number | string,\n ): Promise<T | null> {\n return this.adapter.findById<T>(table, id);\n }\n\n async count(table: string, options?: QueryOptions): Promise<number> {\n return this.adapter.count(table, options);\n }\n\n async paginate<T = any>(\n table: string,\n page: number = 1,\n limit: number = 10,\n options?: QueryOptions,\n ): Promise<PaginatedResult<T>> {\n const offset = (page - 1) * limit;\n\n const [data, total] = await Promise.all([\n this.query<T>(table, { ...options, limit, offset }),\n this.count(table, options),\n ]);\n\n return {\n data,\n page,\n limit,\n total,\n totalPages: Math.ceil(total / limit),\n hasMore: page * limit < total,\n };\n }\n\n // Mutations - no cache invalidation here (React Query handles it)\n async create<T = any>(table: string, data: Partial<T>): Promise<T> {\n return this.adapter.create<T>(table, data);\n }\n\n async update<T = any>(\n table: string,\n id: number | string,\n data: Partial<T>,\n ): Promise<T> {\n return this.adapter.update<T>(table, id, data);\n }\n\n async delete(table: string, id: number | string): Promise<boolean> {\n return this.adapter.delete(table, id);\n }\n\n // Direct adapter access for advanced use cases\n getAdapter(): IDataAdapter {\n return this.adapter;\n }\n\n // ============================================\n // RAW SQL QUERIES\n // ============================================\n\n /**\n * Execute raw SQL query - returns multiple rows\n * Use for complex queries that can't be expressed with QueryOptions\n *\n * @example\n * const posts = await dm.raw<Post>(`\n * SELECT DISTINCT p.*, c.name as category_name\n * FROM posts p\n * JOIN post_categories pc ON p.id = pc.post_id\n * JOIN blog_categories c ON pc.category_id = c.id\n * WHERE c.slug = ? AND p.published = 1\n * `, [categorySlug]);\n */\n async raw<T = any>(sql: string, params?: any[]): Promise<T[]> {\n return this.adapter.raw<T>(sql, params);\n }\n\n /**\n * Execute raw SQL query - returns single row or null\n * Use for aggregations, single record lookups\n *\n * @example\n * const priceRange = await dm.rawOne<{min: number, max: number}>(`\n * SELECT MIN(price) as min, MAX(price) as max\n * FROM products WHERE published = 1\n * `);\n */\n async rawOne<T = any>(sql: string, params?: any[]): Promise<T | null> {\n return this.adapter.rawOne<T>(sql, params);\n }\n}\n"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "db/core/types.ts",
|
|
32
|
+
"type": "registry:type",
|
|
33
|
+
"target": "$modules$/db/core/types.ts",
|
|
34
|
+
"content": "/**\n * Core type definitions for data-access module\n */\n\n// ============================================\n// WHERE CONDITIONS\n// ============================================\n\n/** WHERE condition operators */\nexport type WhereOperator =\n | \"=\"\n | \"!=\"\n | \"<>\"\n | \">\"\n | \"<\"\n | \">=\"\n | \"<=\"\n | \"LIKE\"\n | \"NOT LIKE\"\n | \"IN\"\n | \"NOT IN\"\n | \"BETWEEN\"\n | \"NOT BETWEEN\"\n | \"IS NULL\"\n | \"IS NOT NULL\";\n\n/** Single WHERE condition */\nexport interface WhereCondition {\n field: string;\n operator: WhereOperator;\n value: any;\n}\n\n/** WHERE groups (AND/OR) - recursive structure */\nexport interface WhereGroup {\n type: \"AND\" | \"OR\";\n conditions: (WhereCondition | WhereGroup)[];\n}\n\n// ============================================\n// JOIN DEFINITIONS\n// ============================================\n\n/** JOIN types */\nexport type JoinType = \"INNER\" | \"LEFT\" | \"RIGHT\" | \"CROSS\";\n\n/** JOIN definition */\nexport interface JoinClause {\n type: JoinType;\n table: string;\n alias?: string;\n on: {\n leftField: string;\n rightField: string;\n };\n}\n\n// ============================================\n// QUERY OPTIONS\n// ============================================\n\n/** Order definition */\nexport interface OrderBy {\n field: string;\n direction: \"ASC\" | \"DESC\";\n}\n\n/** Extended Query Options */\nexport interface QueryOptions {\n // Existing (backwards compatible)\n where?: Record<string, any>;\n limit?: number;\n offset?: number;\n orderBy?: OrderBy[];\n include?: string[];\n\n // New features\n select?: string[]; // SELECT fields: ['p.*', 'c.name as category_name']\n distinct?: boolean; // DISTINCT usage\n joins?: JoinClause[]; // JOIN definitions\n whereAdvanced?: WhereGroup; // Complex WHERE conditions (AND/OR groups)\n groupBy?: string[]; // GROUP BY fields\n having?: WhereGroup; // HAVING conditions\n}\n\n// ============================================\n// RESULT TYPES\n// ============================================\n\n/** Paginated result */\nexport interface PaginatedResult<T> {\n data: T[];\n page: number;\n limit: number;\n total: number;\n totalPages: number;\n hasMore: boolean;\n}\n\n/** Query key (for cache) */\nexport type QueryKey = (string | number | object)[];\n"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"path": "db/adapters/IDataAdapter.ts",
|
|
38
|
+
"type": "registry:type",
|
|
39
|
+
"target": "$modules$/db/adapters/IDataAdapter.ts",
|
|
40
|
+
"content": "import type { QueryOptions } from \"../core/types\";\n\n/**\n * Data Adapter Interface\n * Implement this interface to create custom data sources\n */\nexport interface IDataAdapter {\n // Connection\n connect(): Promise<void>;\n disconnect(): Promise<void>;\n\n // CRUD operations\n findMany<T>(table: string, options?: QueryOptions): Promise<T[]>;\n findOne<T>(table: string, options: QueryOptions): Promise<T | null>;\n findById<T>(table: string, id: number | string): Promise<T | null>;\n create<T>(table: string, data: Partial<T>): Promise<T>;\n update<T>(table: string, id: number | string, data: Partial<T>): Promise<T>;\n delete(table: string, id: number | string): Promise<boolean>;\n\n // Utilities\n count(table: string, options?: QueryOptions): Promise<number>;\n\n // Raw SQL queries - for complex queries that can't be expressed with QueryOptions\n raw<T>(sql: string, params?: any[]): Promise<T[]>;\n rawOne<T>(sql: string, params?: any[]): Promise<T | null>;\n}\n"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"path": "db/adapters/index.ts",
|
|
44
|
+
"type": "registry:index",
|
|
45
|
+
"target": "$modules$/db/adapters/index.ts",
|
|
46
|
+
"content": "export type { IDataAdapter } from \"./IDataAdapter\";\nexport { SqliteAdapter } from \"./SqliteAdapter\";\n"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"path": "db/adapters/SqliteAdapter.ts",
|
|
50
|
+
"type": "registry:lib",
|
|
51
|
+
"target": "$modules$/db/adapters/SqliteAdapter.ts",
|
|
52
|
+
"content": "import initSqlJs, { Database } from \"sql.js\";\nimport type { IDataAdapter } from \"./IDataAdapter\";\nimport type {\n QueryOptions,\n WhereCondition,\n WhereGroup,\n JoinClause,\n} from \"../core/types\";\n\n/**\n * SQLite Adapter\n * Loads database from file using sql.js\n * Supports complex queries: JOIN, WHERE groups, LIKE, IN, BETWEEN\n */\nexport class SqliteAdapter implements IDataAdapter {\n private db: Database | null = null;\n private dbPath: string;\n\n constructor(dbPath: string = \"/data/database.db\") {\n this.dbPath = dbPath;\n }\n\n // ============================================\n // CONNECTION\n // ============================================\n\n async connect(): Promise<void> {\n if (this.db) return;\n\n try {\n const SQL = await initSqlJs({\n locateFile: (file: string) => `https://sql.js.org/dist/${file}`,\n });\n\n const response = await fetch(this.dbPath);\n if (!response.ok) {\n throw new Error(`Database file not found: ${this.dbPath}`);\n }\n\n const buffer = await response.arrayBuffer();\n this.db = new SQL.Database(new Uint8Array(buffer));\n console.log(\"SQLite adapter connected\");\n } catch (error) {\n console.error(\"SQLite adapter connection failed:\", error);\n throw error;\n }\n }\n\n async disconnect(): Promise<void> {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n // ============================================\n // CRUD OPERATIONS\n // ============================================\n\n async findMany<T>(table: string, options?: QueryOptions): Promise<T[]> {\n await this.connect();\n if (!this.db) {\n console.warn(\"Database not connected\");\n return [];\n }\n try {\n const { sql, params } = this.buildSelectQuery(table, options);\n return this.executeQuery<T>(sql, params);\n } catch (error) {\n console.error(`Error querying table ${table}:`, error);\n return [];\n }\n }\n\n async findOne<T>(table: string, options: QueryOptions): Promise<T | null> {\n const results = await this.findMany<T>(table, { ...options, limit: 1 });\n return results[0] || null;\n }\n\n async findById<T>(table: string, id: number | string): Promise<T | null> {\n return this.findOne<T>(table, { where: { id } });\n }\n\n async create<T>(table: string, data: Partial<T>): Promise<T> {\n await this.connect();\n\n // Otomatik timestamp - sadece yoksa ekle\n const dataWithTimestamps: any = { ...data };\n\n if (!dataWithTimestamps.created_at) {\n dataWithTimestamps.created_at = new Date().toISOString();\n }\n\n if (!dataWithTimestamps.updated_at) {\n dataWithTimestamps.updated_at = new Date().toISOString();\n }\n\n const keys = Object.keys(dataWithTimestamps);\n const values = Object.values(dataWithTimestamps) as any[];\n const placeholders = keys.map(() => \"?\").join(\", \");\n\n const sql = `INSERT INTO ${table} (${keys.join(\", \")}) VALUES (${placeholders})`;\n this.db!.run(sql, values);\n\n const lastIdResult = this.db!.exec(\"SELECT last_insert_rowid() as id\");\n const lastId = lastIdResult[0]?.values[0]?.[0] as number;\n\n return this.findById<T>(table, lastId) as Promise<T>;\n }\n\n async update<T>(\n table: string,\n id: number | string,\n data: Partial<T>,\n ): Promise<T> {\n await this.connect();\n\n // Otomatik updated_at - her zaman güncelle\n const dataWithTimestamp = {\n ...data,\n updated_at: new Date().toISOString(),\n };\n\n const keys = Object.keys(dataWithTimestamp);\n const values = Object.values(dataWithTimestamp) as any[];\n const setClause = keys.map((key) => `${key} = ?`).join(\", \");\n\n const sql = `UPDATE ${table} SET ${setClause} WHERE id = ?`;\n this.db!.run(sql, [...values, id]);\n\n return this.findById<T>(table, id) as Promise<T>;\n }\n\n async delete(table: string, id: number | string): Promise<boolean> {\n await this.connect();\n const sql = `DELETE FROM ${table} WHERE id = ?`;\n this.db!.run(sql, [id]);\n return true;\n }\n\n async count(table: string, options?: QueryOptions): Promise<number> {\n await this.connect();\n const { sql, params } = this.buildCountQuery(table, options);\n const results = await this.executeQuery<{ count: number }>(sql, params);\n return results[0]?.count || 0;\n }\n\n // ============================================\n // RAW SQL QUERIES\n // ============================================\n\n async raw<T>(sql: string, params?: any[]): Promise<T[]> {\n await this.connect();\n return this.executeQuery<T>(sql, params);\n }\n\n async rawOne<T>(sql: string, params?: any[]): Promise<T | null> {\n const results = await this.raw<T>(sql, params);\n return results[0] || null;\n }\n\n // ============================================\n // PRIVATE: QUERY EXECUTION\n // ============================================\n\n private async executeQuery<T>(sql: string, params?: any[]): Promise<T[]> {\n if (!this.db) {\n console.warn(\"Database not connected\");\n return [];\n }\n try {\n const stmt = this.db.prepare(sql);\n if (params && params.length > 0) stmt.bind(params);\n\n const results: T[] = [];\n while (stmt.step()) {\n results.push(stmt.getAsObject() as T);\n }\n stmt.free();\n return results;\n } catch (error) {\n console.error(`Query error: ${sql}`, error);\n return [];\n }\n }\n\n // ============================================\n // PRIVATE: QUERY BUILDERS\n // ============================================\n\n private buildSelectQuery(\n table: string,\n options?: QueryOptions,\n ): { sql: string; params: any[] } {\n const params: any[] = [];\n\n // SELECT clause\n const selectFields = options?.select?.join(\", \") || \"*\";\n const distinct = options?.distinct ? \"DISTINCT \" : \"\";\n let sql = `SELECT ${distinct}${selectFields} FROM ${table}`;\n\n // JOIN clauses\n if (options?.joins && options.joins.length > 0) {\n sql += this.buildJoinClause(options.joins);\n }\n\n // WHERE clause\n const whereClause = this.buildWhereClause(options, params);\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n // GROUP BY clause\n if (options?.groupBy && options.groupBy.length > 0) {\n sql += ` GROUP BY ${options.groupBy.join(\", \")}`;\n }\n\n // HAVING clause\n if (options?.having) {\n const havingClause = this.buildWhereGroupClause(options.having, params);\n if (havingClause) {\n sql += ` HAVING ${havingClause}`;\n }\n }\n\n // ORDER BY clause\n if (options?.orderBy && options.orderBy.length > 0) {\n const orderClauses = options.orderBy.map(\n (o) => `${o.field} ${o.direction}`,\n );\n sql += ` ORDER BY ${orderClauses.join(\", \")}`;\n }\n\n // LIMIT & OFFSET\n if (options?.limit) {\n sql += ` LIMIT ?`;\n params.push(options.limit);\n }\n\n if (options?.offset) {\n sql += ` OFFSET ?`;\n params.push(options.offset);\n }\n\n return { sql, params };\n }\n\n private buildCountQuery(\n table: string,\n options?: QueryOptions,\n ): { sql: string; params: any[] } {\n const params: any[] = [];\n let sql = `SELECT COUNT(*) as count FROM ${table}`;\n\n // JOIN clauses\n if (options?.joins && options.joins.length > 0) {\n sql += this.buildJoinClause(options.joins);\n }\n\n // WHERE clause\n const whereClause = this.buildWhereClause(options, params);\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n return { sql, params };\n }\n\n private buildJoinClause(joins: JoinClause[]): string {\n return joins\n .map((join) => {\n const alias = join.alias ? ` ${join.alias}` : \"\";\n const leftField = join.on.leftField;\n const rightField = join.on.rightField;\n return ` ${join.type} JOIN ${join.table}${alias} ON ${leftField} = ${rightField}`;\n })\n .join(\"\");\n }\n\n private buildWhereClause(\n options: QueryOptions | undefined,\n params: any[],\n ): string {\n const clauses: string[] = [];\n\n // Simple where (backwards compatible)\n if (options?.where) {\n const simpleConditions = Object.entries(options.where).map(\n ([key, value]) => {\n params.push(value);\n return `${key} = ?`;\n },\n );\n if (simpleConditions.length > 0) {\n clauses.push(simpleConditions.join(\" AND \"));\n }\n }\n\n // Advanced where (new feature)\n if (options?.whereAdvanced) {\n const advancedClause = this.buildWhereGroupClause(\n options.whereAdvanced,\n params,\n );\n if (advancedClause) {\n clauses.push(advancedClause);\n }\n }\n\n return clauses.length > 0 ? clauses.join(\" AND \") : \"\";\n }\n\n private buildWhereGroupClause(group: WhereGroup, params: any[]): string {\n const conditions = group.conditions\n .map((condition) => {\n // Recursive: WhereGroup\n if (\"type\" in condition && \"conditions\" in condition) {\n return `(${this.buildWhereGroupClause(condition as WhereGroup, params)})`;\n }\n // WhereCondition\n return this.buildConditionClause(condition as WhereCondition, params);\n })\n .filter(Boolean);\n\n return conditions.join(` ${group.type} `);\n }\n\n private buildConditionClause(\n condition: WhereCondition,\n params: any[],\n ): string {\n const { field, operator, value } = condition;\n\n switch (operator) {\n case \"IS NULL\":\n return `${field} IS NULL`;\n\n case \"IS NOT NULL\":\n return `${field} IS NOT NULL`;\n\n case \"IN\":\n case \"NOT IN\":\n if (Array.isArray(value)) {\n const placeholders = value.map(() => \"?\").join(\", \");\n params.push(...value);\n return `${field} ${operator} (${placeholders})`;\n }\n return \"\";\n\n case \"BETWEEN\":\n case \"NOT BETWEEN\":\n if (Array.isArray(value) && value.length === 2) {\n params.push(value[0], value[1]);\n return `${field} ${operator} ? AND ?`;\n }\n return \"\";\n\n default:\n // =, !=, <>, >, <, >=, <=, LIKE, NOT LIKE\n params.push(value);\n return `${field} ${operator} ?`;\n }\n }\n}\n"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"path": "db/react/index.ts",
|
|
56
|
+
"type": "registry:index",
|
|
57
|
+
"target": "$modules$/db/react/index.ts",
|
|
58
|
+
"content": "// Provider\nexport { DBQueryProvider } from \"./QueryProvider\";\n\n// Query client and utilities\nexport { queryClient, queryKeys, cacheUtils } from \"./queryClient\";\n\n// Generic repository hooks\nexport {\n useRepositoryQuery,\n useRepositoryQueryOne,\n useRepositoryQueryById,\n useRepositoryPagination,\n useRepositoryInfiniteQuery,\n useRepositoryCreate,\n useRepositoryUpdate,\n useRepositoryDelete,\n // Raw SQL hooks\n useRawQuery,\n useRawQueryOne,\n} from \"./useRepository\";\n\n// Types\nexport type { RepositoryQueryOptions } from \"./useRepository\";\n"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"path": "db/react/queryClient.ts",
|
|
62
|
+
"type": "registry:lib",
|
|
63
|
+
"target": "$modules$/db/react/queryClient.ts",
|
|
64
|
+
"content": "import { QueryClient } from \"@tanstack/react-query\";\n\n/**\n * React Query handles ALL caching, refetching, and invalidation\n * No custom cache needed!\n */\nexport const queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 30 * 1000, // 30 seconds fresh\n gcTime: 5 * 60 * 1000, // 5 minutes in cache (was cacheTime)\n retry: 1,\n refetchOnWindowFocus: false, // Don't auto-refetch on window focus\n refetchOnReconnect: false, // Don't refetch on reconnect\n refetchOnMount: false, // Don't refetch on component mount (prevent loops)\n },\n mutations: {\n retry: 0,\n },\n },\n});\n\n/**\n * Query key factory - for cache management\n * React Query uses these keys to cache and invalidate queries\n */\nexport const queryKeys = {\n all: (table: string) => [table] as const,\n lists: (table: string) => [table, \"list\"] as const,\n list: (table: string, options?: any) => [table, \"list\", options] as const,\n details: (table: string) => [table, \"detail\"] as const,\n detail: (table: string, id: number | string) =>\n [table, \"detail\", id] as const,\n paginated: (table: string, page: number, limit: number, options?: any) =>\n [table, \"paginated\", page, limit, options] as const,\n infinite: (table: string, limit: number, options?: any) =>\n [table, \"infinite\", limit, options] as const,\n count: (table: string, options?: any) => [table, \"count\", options] as const,\n};\n\n/**\n * Manual cache utilities (rarely needed)\n */\nexport const cacheUtils = {\n // Invalidate all queries for a table\n invalidateTable: (table: string) => {\n return queryClient.invalidateQueries({ queryKey: queryKeys.all(table) });\n },\n\n // Clear all cache\n clearAll: () => {\n return queryClient.clear();\n },\n\n // Get cached data\n getCachedData: <T>(queryKey: any[]) => {\n return queryClient.getQueryData<T>(queryKey);\n },\n\n // Set cached data manually\n setCachedData: <T>(queryKey: any[], data: T) => {\n return queryClient.setQueryData<T>(queryKey, data);\n },\n};\n"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"path": "db/react/QueryProvider.tsx",
|
|
68
|
+
"type": "registry:component",
|
|
69
|
+
"target": "$modules$/db/react/QueryProvider.tsx",
|
|
70
|
+
"content": "import { QueryClientProvider } from \"@tanstack/react-query\";\nimport { queryClient } from \"./queryClient\";\n\ninterface DBQueryProviderProps {\n children: React.ReactNode;\n}\n\n/**\n * DBQueryProvider - DB module's React Query provider\n * Wraps components that use db module hooks\n */\nexport function DBQueryProvider({ children }: DBQueryProviderProps) {\n return (\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n );\n}\n"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"path": "db/react/useRepository.ts",
|
|
74
|
+
"type": "registry:hook",
|
|
75
|
+
"target": "$modules$/db/react/useRepository.ts",
|
|
76
|
+
"content": "import {\n useQuery,\n useMutation,\n useQueryClient,\n useInfiniteQuery,\n type UseQueryOptions,\n type UseMutationOptions,\n type UseInfiniteQueryOptions,\n} from \"@tanstack/react-query\";\nimport { DataManager } from \"../core/DataManager\";\nimport { getAdapter } from \"../config\";\nimport { queryKeys } from \"./queryClient\";\nimport type { QueryOptions } from \"../core/types\";\n\n// Singleton manager\nlet managerInstance: DataManager | null = null;\nfunction getManager() {\n if (!managerInstance) {\n managerInstance = DataManager.getInstance(getAdapter());\n }\n return managerInstance;\n}\n\n// ==========================================\n// QUERY HOOKS\n// ==========================================\n\n// Omit 'select' from QueryOptions to avoid conflict with React Query's select\nexport interface RepositoryQueryOptions<T> extends Omit<\n QueryOptions,\n \"select\"\n> {\n // SQL SELECT fields (renamed to avoid conflict)\n selectFields?: string[];\n\n // React Query options\n enabled?: boolean;\n staleTime?: number;\n gcTime?: number;\n refetchOnWindowFocus?: boolean;\n refetchInterval?: number | false;\n select?: (data: T[]) => any; // React Query data transformation\n}\n\n/**\n * Generic query hook - React Query handles all caching\n * @example\n * // Simple query\n * const { data: posts, isLoading } = useRepositoryQuery('posts', {\n * where: { published: 1 },\n * orderBy: [{ field: 'created_at', direction: 'DESC' }],\n * staleTime: 60000\n * });\n *\n * // With JOIN\n * const { data: posts } = useRepositoryQuery('posts', {\n * selectFields: ['posts.*', 'c.name as category_name'],\n * joins: [{\n * type: 'INNER',\n * table: 'post_categories',\n * alias: 'pc',\n * on: { leftField: 'posts.id', rightField: 'pc.post_id' }\n * }],\n * distinct: true\n * });\n *\n * // With complex WHERE\n * const { data: products } = useRepositoryQuery('products', {\n * whereAdvanced: {\n * type: 'AND',\n * conditions: [\n * { field: 'published', operator: '=', value: 1 },\n * { type: 'OR', conditions: [\n * { field: 'name', operator: 'LIKE', value: '%phone%' },\n * { field: 'description', operator: 'LIKE', value: '%phone%' }\n * ]}\n * ]\n * }\n * });\n */\nexport function useRepositoryQuery<T = any>(\n table: string,\n options: RepositoryQueryOptions<T> = {},\n queryOptions?: Omit<UseQueryOptions<T[], Error>, \"queryKey\" | \"queryFn\">,\n) {\n const manager = getManager();\n const {\n // QueryOptions fields\n where,\n limit,\n offset,\n orderBy,\n include,\n // New complex query fields\n selectFields,\n distinct,\n joins,\n whereAdvanced,\n groupBy,\n having,\n // React Query options\n select,\n enabled,\n staleTime,\n gcTime,\n refetchOnWindowFocus,\n refetchInterval,\n } = options;\n\n const queryOpts: QueryOptions = {\n where,\n limit,\n offset,\n orderBy,\n include,\n select: selectFields,\n distinct,\n joins,\n whereAdvanced,\n groupBy,\n having,\n };\n\n return useQuery<T[], Error>({\n queryKey: queryKeys.list(table, queryOpts),\n queryFn: () => manager.query<T>(table, queryOpts),\n select,\n enabled,\n staleTime,\n gcTime,\n refetchOnWindowFocus,\n refetchInterval,\n ...queryOptions,\n });\n}\n\n/**\n * Query single record\n * @example\n * const { data: post } = useRepositoryQueryOne('posts', {\n * where: { slug: 'my-post' }\n * });\n */\nexport function useRepositoryQueryOne<T = any>(\n table: string,\n options: RepositoryQueryOptions<T> = {},\n) {\n const result = useRepositoryQuery<T>(table, { ...options, limit: 1 });\n\n return {\n ...result,\n data: result.data?.[0] || null,\n };\n}\n\n/**\n * Query by ID - React Query caches by ID automatically\n * @example\n * const { data: post, isLoading } = useRepositoryQueryById('posts', postId);\n */\nexport function useRepositoryQueryById<T = any>(\n table: string,\n id: number | string | null | undefined,\n options: Omit<UseQueryOptions<T | null, Error>, \"queryKey\" | \"queryFn\"> = {},\n) {\n const manager = getManager();\n\n return useQuery<T | null, Error>({\n queryKey: queryKeys.detail(table, id as any),\n queryFn: () => manager.queryById<T>(table, id as any),\n enabled: options.enabled !== false && id != null,\n ...options,\n });\n}\n\n/**\n * Paginated query - React Query caches each page\n * @example\n * const { data, totalPages, hasMore } = useRepositoryPagination('products', page, 20);\n */\nexport function useRepositoryPagination<T = any>(\n table: string,\n page: number = 1,\n limit: number = 10,\n options: QueryOptions = {},\n) {\n const manager = getManager();\n\n return useQuery({\n queryKey: queryKeys.paginated(table, page, limit, options),\n queryFn: () => manager.paginate<T>(table, page, limit, options),\n });\n}\n\n/**\n * Infinite query for infinite scroll / load more\n * @example\n * const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =\n * useRepositoryInfiniteQuery('posts', 20, {\n * where: { published: 1 },\n * orderBy: [{ field: 'created_at', direction: 'DESC' }]\n * });\n *\n * // data.pages = [page1Data, page2Data, page3Data, ...]\n * const allPosts = data?.pages.flatMap(page => page.data) ?? [];\n */\nexport function useRepositoryInfiniteQuery<T = any>(\n table: string,\n pageSize: number = 20,\n options: QueryOptions = {},\n queryOptions?: Omit<\n UseInfiniteQueryOptions<\n { data: T[]; page: number; totalPages: number; hasMore: boolean },\n Error\n >,\n \"queryKey\" | \"queryFn\" | \"getNextPageParam\" | \"initialPageParam\"\n >,\n) {\n const manager = getManager();\n\n return useInfiniteQuery({\n queryKey: queryKeys.infinite(table, pageSize, options),\n queryFn: ({ pageParam }) =>\n manager.paginate<T>(table, pageParam as number, pageSize, options),\n initialPageParam: 1,\n getNextPageParam: (lastPage) => {\n if (!lastPage.hasMore) return undefined;\n return lastPage.page + 1;\n },\n ...queryOptions,\n });\n}\n\n// ==========================================\n// MUTATION HOOKS (Auto-invalidation via React Query)\n// ==========================================\n\n/**\n * Create mutation - React Query handles cache invalidation\n * @example\n * const { mutate: createPost } = useRepositoryCreate('posts', {\n * onSuccess: () => toast.success('Created!')\n * });\n */\nexport function useRepositoryCreate<T = any>(\n table: string,\n options: Omit<UseMutationOptions<T, Error, Partial<T>>, \"mutationFn\"> & {\n invalidate?: string[];\n } = {},\n) {\n const manager = getManager();\n const queryClient = useQueryClient();\n const { invalidate = [table], ...mutationOptions } = options;\n\n return useMutation<T, Error, Partial<T>>({\n mutationFn: (data) => manager.create<T>(table, data),\n onSuccess: () => {\n // React Query automatically invalidates and refetches\n invalidate.forEach((t) => {\n queryClient.invalidateQueries({ queryKey: queryKeys.all(t) });\n });\n },\n ...mutationOptions,\n });\n}\n\n/**\n * Update mutation - Optimistic update via React Query\n * @example\n * const { mutate: updatePost } = useRepositoryUpdate('posts', {\n * onSuccess: () => toast.success('Updated!')\n * });\n * updatePost({ id: 1, data: { title: 'New Title' } });\n */\nexport function useRepositoryUpdate<T = any>(\n table: string,\n options: Omit<\n UseMutationOptions<T, Error, { id: number | string; data: Partial<T> }>,\n \"mutationFn\"\n > = {},\n) {\n const manager = getManager();\n const queryClient = useQueryClient();\n\n return useMutation<T, Error, { id: number | string; data: Partial<T> }>({\n mutationFn: ({ id, data }) => manager.update<T>(table, id, data),\n onSuccess: (data, variables) => {\n // Invalidate list queries\n queryClient.invalidateQueries({ queryKey: queryKeys.all(table) });\n\n // Update detail cache optimistically\n queryClient.setQueryData(queryKeys.detail(table, variables.id), data);\n },\n ...options,\n });\n}\n\n/**\n * Delete mutation\n * @example\n * const { mutate: deletePost } = useRepositoryDelete('posts', {\n * onSuccess: () => toast.success('Deleted!')\n * });\n * deletePost(postId);\n */\nexport function useRepositoryDelete(\n table: string,\n options: Omit<\n UseMutationOptions<boolean, Error, number | string>,\n \"mutationFn\"\n > = {},\n) {\n const manager = getManager();\n const queryClient = useQueryClient();\n\n return useMutation<boolean, Error, number | string>({\n mutationFn: (id) => manager.delete(table, id),\n onSuccess: (_data, id) => {\n // Invalidate and remove from cache\n queryClient.invalidateQueries({ queryKey: queryKeys.all(table) });\n queryClient.removeQueries({ queryKey: queryKeys.detail(table, id) });\n },\n ...options,\n });\n}\n\n// ==========================================\n// RAW SQL QUERY HOOKS\n// ==========================================\n\n/**\n * Raw SQL query hook - for complex queries that can't be expressed with QueryOptions\n * @example\n * // Complex JOIN query\n * const { data: posts } = useRawQuery<Post>(\n * ['posts-with-categories', categorySlug],\n * `SELECT DISTINCT p.*, c.name as category_name\n * FROM posts p\n * JOIN post_categories pc ON p.id = pc.post_id\n * JOIN blog_categories c ON pc.category_id = c.id\n * WHERE c.slug = ? AND p.published = 1\n * ORDER BY p.published_at DESC`,\n * [categorySlug]\n * );\n *\n * // Aggregation query\n * const { data: stats } = useRawQuery<{total: number, avg: number}>(\n * ['product-stats'],\n * `SELECT COUNT(*) as total, AVG(price) as avg FROM products WHERE published = 1`\n * );\n */\nexport function useRawQuery<T = any>(\n queryKey: any[],\n sql: string,\n params?: any[],\n options?: Omit<UseQueryOptions<T[], Error>, \"queryKey\" | \"queryFn\">,\n) {\n const manager = getManager();\n\n return useQuery<T[], Error>({\n queryKey: [\"raw\", ...queryKey],\n queryFn: () => manager.raw<T>(sql, params),\n ...options,\n });\n}\n\n/**\n * Raw SQL query hook for single result - aggregations, single lookups\n * @example\n * // Get price range\n * const { data: priceRange } = useRawQueryOne<{min: number, max: number}>(\n * ['price-range'],\n * `SELECT MIN(price) as min, MAX(price) as max FROM products WHERE published = 1`\n * );\n *\n * // Get single post with category\n * const { data: post } = useRawQueryOne<Post>(\n * ['post-detail', slug],\n * `SELECT p.*, c.name as category_name\n * FROM posts p\n * LEFT JOIN post_categories pc ON p.id = pc.post_id\n * LEFT JOIN blog_categories c ON pc.category_id = c.id\n * WHERE p.slug = ?`,\n * [slug]\n * );\n */\nexport function useRawQueryOne<T = any>(\n queryKey: any[],\n sql: string,\n params?: any[],\n options?: Omit<UseQueryOptions<T | null, Error>, \"queryKey\" | \"queryFn\">,\n) {\n const manager = getManager();\n\n return useQuery<T | null, Error>({\n queryKey: [\"raw\", ...queryKey],\n queryFn: () => manager.rawOne<T>(sql, params),\n ...options,\n });\n}\n"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"path": "db/utils/parsers.ts",
|
|
80
|
+
"type": "registry:lib",
|
|
81
|
+
"target": "$modules$/db/utils/parsers.ts",
|
|
82
|
+
"content": "/**\n * Database field parsers - Client-side utilities\n * NO automatic parsing - client decides what to parse and when\n */\n\n/**\n * Parse comma-separated string to array\n * @example \"tag1,tag2,tag3\" -> [\"tag1\", \"tag2\", \"tag3\"]\n */\nexport const parseCommaSeparatedString = (value: string): string[] => {\n if (!value || typeof value !== \"string\") return [];\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n};\n\n/**\n * Parse JSON string to array\n * @example '[\"img1.jpg\",\"img2.jpg\"]' -> [\"img1.jpg\", \"img2.jpg\"]\n */\nexport const parseJSONStringToArray = (value: string): string[] => {\n if (!value || typeof value !== \"string\") return [];\n try {\n const parsed = JSON.parse(value);\n return Array.isArray(parsed) ? parsed : [];\n } catch (e) {\n console.warn(\"Failed to parse JSON array:\", value);\n return [];\n }\n};\n\n/**\n * Smart array parser - tries JSON first, falls back to comma-separated\n * @example '[\"a\",\"b\"]' -> [\"a\", \"b\"] OR \"a,b\" -> [\"a\", \"b\"]\n */\nexport const parseStringToArray = (value: any): string[] => {\n if (!value) return [];\n if (Array.isArray(value)) return value;\n if (typeof value !== \"string\") return [];\n\n // Try JSON first\n if (value.trim().startsWith(\"[\")) {\n const jsonResult = parseJSONStringToArray(value);\n if (jsonResult.length > 0) return jsonResult;\n }\n\n // Fall back to comma-separated\n return parseCommaSeparatedString(value);\n};\n\n/**\n * Parse JSON string to object\n * @example '{\"key\":\"value\"}' -> {key: \"value\"}\n */\nexport const parseJSONString = <T = any>(\n value: any,\n defaultValue: T | null = null,\n): T | null => {\n if (!value) return defaultValue;\n if (typeof value === \"object\") return value; // Already parsed\n if (typeof value !== \"string\") return defaultValue;\n\n try {\n return JSON.parse(value);\n } catch (e) {\n console.warn(\"Failed to parse JSON:\", value);\n return defaultValue;\n }\n};\n\n/**\n * Parse SQLite boolean (0/1) to JavaScript boolean\n * @example 1 -> true, 0 -> false\n */\nexport const parseSQLiteBoolean = (value: any): boolean => {\n if (typeof value === \"boolean\") return value;\n if (typeof value === \"number\") return value !== 0;\n if (typeof value === \"string\") {\n const lower = value.toLowerCase();\n return lower === \"true\" || lower === \"1\" || lower === \"yes\";\n }\n return Boolean(value);\n};\n\n/**\n * Parse number safely with default fallback\n * @example \"123\" -> 123, \"invalid\" -> 0 (or provided default)\n */\nexport const parseNumberSafe = (\n value: any,\n defaultValue: number = 0,\n): number => {\n const num = Number(value);\n return isNaN(num) ? defaultValue : num;\n};\n"
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"exports": {
|
|
86
|
+
"types": [
|
|
87
|
+
"AdapterType",
|
|
88
|
+
"DataConfig",
|
|
89
|
+
"IDataAdapter",
|
|
90
|
+
"JoinClause",
|
|
91
|
+
"JoinType",
|
|
92
|
+
"OrderBy",
|
|
93
|
+
"PaginatedResult",
|
|
94
|
+
"QueryOptions",
|
|
95
|
+
"RepositoryQueryOptions",
|
|
96
|
+
"WhereCondition",
|
|
97
|
+
"WhereGroup",
|
|
98
|
+
"WhereOperator"
|
|
99
|
+
],
|
|
100
|
+
"variables": [
|
|
101
|
+
"DBQueryProvider",
|
|
102
|
+
"DataManager",
|
|
103
|
+
"QueryProvider",
|
|
104
|
+
"SqliteAdapter",
|
|
105
|
+
"cacheUtils",
|
|
106
|
+
"createAdapter",
|
|
107
|
+
"getAdapter",
|
|
108
|
+
"parseCommaSeparatedString",
|
|
109
|
+
"parseJSONString",
|
|
110
|
+
"parseJSONStringToArray",
|
|
111
|
+
"parseNumberSafe",
|
|
112
|
+
"parseSQLiteBoolean",
|
|
113
|
+
"parseStringToArray",
|
|
114
|
+
"queryClient",
|
|
115
|
+
"queryKeys",
|
|
116
|
+
"resetAdapter",
|
|
117
|
+
"useRawQuery",
|
|
118
|
+
"useRawQueryOne",
|
|
119
|
+
"useRepositoryCreate",
|
|
120
|
+
"useRepositoryDelete",
|
|
121
|
+
"useRepositoryInfiniteQuery",
|
|
122
|
+
"useRepositoryPagination",
|
|
123
|
+
"useRepositoryQuery",
|
|
124
|
+
"useRepositoryQueryById",
|
|
125
|
+
"useRepositoryQueryOne",
|
|
126
|
+
"useRepositoryUpdate"
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Forgot Password Page
|
|
2
|
+
|
|
3
|
+
Password recovery page with email form and sign in link. Centered card layout with responsive design.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| Target | Type |
|
|
8
|
+
|--------|------|
|
|
9
|
+
| `$modules$/forgot-password-page/index.ts` | index |
|
|
10
|
+
| `$modules$/forgot-password-page/forgot-password-page.tsx` | page |
|
|
11
|
+
| `$modules$/forgot-password-page/lang/en.json` | lang |
|
|
12
|
+
| `$modules$/forgot-password-page/lang/tr.json` | lang |
|
|
13
|
+
|
|
14
|
+
## Exports
|
|
15
|
+
|
|
16
|
+
**Components/Functions:** `ForgotPasswordPage`, `default`
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { ForgotPasswordPage, default } from '@/modules/forgot-password-page';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
import ForgotPasswordPage from '@/modules/forgot-password-page';
|
|
26
|
+
|
|
27
|
+
<ForgotPasswordPage
|
|
28
|
+
onSubmit={(email) => console.log(email)}
|
|
29
|
+
/>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Dependencies
|
|
33
|
+
|
|
34
|
+
This component requires:
|
|
35
|
+
- `button`
|
|
36
|
+
- `input`
|
|
37
|
+
- `label`
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Register Page
|
|
2
|
+
|
|
3
|
+
Registration page with name, email, password form, social OAuth buttons (Google, Microsoft), and sign in link. Centered card layout with responsive design.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| Target | Type |
|
|
8
|
+
|--------|------|
|
|
9
|
+
| `$modules$/register-page/index.ts` | index |
|
|
10
|
+
| `$modules$/register-page/register-page.tsx` | page |
|
|
11
|
+
| `$modules$/register-page/lang/en.json` | lang |
|
|
12
|
+
| `$modules$/register-page/lang/tr.json` | lang |
|
|
13
|
+
|
|
14
|
+
## Exports
|
|
15
|
+
|
|
16
|
+
**Components/Functions:** `RegisterPage`, `default`
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { RegisterPage, default } from '@/modules/register-page';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
import RegisterPage from '@/modules/register-page';
|
|
26
|
+
|
|
27
|
+
<RegisterPage
|
|
28
|
+
onSubmit={(name, email, password) => console.log(name, email, password)}
|
|
29
|
+
onGoogleSignup={() => console.log('Google')}
|
|
30
|
+
onMicrosoftSignup={() => console.log('Microsoft')}
|
|
31
|
+
/>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Dependencies
|
|
35
|
+
|
|
36
|
+
This component requires:
|
|
37
|
+
- `button`
|
|
38
|
+
- `input`
|
|
39
|
+
- `label`
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"path": "ecommerce-core/stores/cart-store.ts",
|
|
26
26
|
"type": "registry:store",
|
|
27
27
|
"target": "$modules$/ecommerce-core/stores/cart-store.ts",
|
|
28
|
-
"content": "import { create } from \"zustand\";\r\nimport { persist } from \"zustand/middleware\";\r\nimport type { Product, CartItem, CartState, CartContextType } from \"../types\";\r\n\r\nconst getProductPrice = (product: Product): number => {\r\n return product.on_sale && product.sale_price ? product.sale_price : product.price;\r\n};\r\n\r\nconst calculateTotal = (items: CartItem[]): number => {\r\n return items.reduce((total, item) => {\r\n const price = getProductPrice(item.product);\r\n return total + price * item.quantity;\r\n }, 0);\r\n};\r\n\r\ninterface CartStore extends 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 const useCartStore = create<CartStore>()(\r\n persist(\r\n (set, _get) => ({\r\n items: [],\r\n total: 0,\r\n itemCount: 0,\r\n\r\n addItem: (product) =>\r\n set((state) => {\r\n const existingItem = state.items.find(\r\n (item) => item.product.id === product.id\r\n );\r\n\r\n if (existingItem) {\r\n const items = state.items.map((item) =>\r\n item.product.id === product.id\r\n ? { ...item, quantity: item.quantity + 1 }\r\n : item\r\n );\r\n return {\r\n items,\r\n total: calculateTotal(items),\r\n itemCount: items.reduce((sum, i) => sum + i.quantity, 0),\r\n };\r\n }\r\n\r\n const items = [\r\n ...state.items,\r\n { id: product.id, product, quantity: 1 },\r\n ];\r\n return {\r\n items,\r\n total: calculateTotal(items),\r\n itemCount: items.reduce((sum, i) => sum + i.quantity, 0),\r\n };\r\n }),\r\n\r\n removeItem: (id) =>\r\n set((state) => {\r\n const items = state.items.filter((item) => item.id !== id);\r\n return {\r\n items,\r\n total: calculateTotal(items),\r\n itemCount: items.reduce((sum, i) => sum + i.quantity, 0),\r\n };\r\n }),\r\n\r\n updateQuantity: (id, quantity) =>\r\n set((state) => {\r\n if (quantity <= 0) {\r\n const items = state.items.filter((item) => item.id !== id);\r\n return {\r\n items,\r\n total: calculateTotal(items),\r\n itemCount: items.reduce((sum, i) => sum + i.quantity, 0),\r\n };\r\n }\r\n\r\n const items = state.items.map((item) =>\r\n item.id === id ? { ...item, quantity } : item\r\n );\r\n return {\r\n items,\r\n total: calculateTotal(items),\r\n itemCount: items.reduce((sum, i) => sum + i.quantity, 0),\r\n };\r\n }),\r\n\r\n clearCart: () => set({ items: [], total: 0, itemCount: 0 }),\r\n }),\r\n { name: \"ecommerce_cart\" }\r\n )\r\n);\r\n\r\n// Backward compatible hook - matches CartContextType\r\nexport const useCart = (): CartContextType => {\r\n const store = useCartStore();\r\n return {\r\n state: { items: store.items, total: store.total },\r\n addItem: store.addItem,\r\n removeItem: store.removeItem,\r\n updateQuantity: store.updateQuantity,\r\n clearCart: store.clearCart,\r\n itemCount: store.itemCount,\r\n };\r\n};\r\n"
|
|
28
|
+
"content": "import { create } from \"zustand\";\r\nimport { persist } from \"zustand/middleware\";\r\nimport type { Product, CartItem, CartState, CartContextType } from \"../types\";\r\n\r\nconst getProductPrice = (product: Product): number => {\r\n return product.on_sale && product.sale_price ? product.sale_price : product.price;\r\n};\r\n\r\nconst calculateTotal = (items: CartItem[]): number => {\r\n return items.reduce((total, item) => {\r\n const price = getProductPrice(item.product);\r\n return total + price * item.quantity;\r\n }, 0);\r\n};\r\n\r\ninterface CartStore extends 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 isDrawerOpen: boolean;\r\n setDrawerOpen: (open: boolean) => void;\r\n}\r\n\r\nexport const useCartStore = create<CartStore>()(\r\n persist(\r\n (set, _get) => ({\r\n items: [],\r\n total: 0,\r\n itemCount: 0,\r\n isDrawerOpen: false,\r\n setDrawerOpen: (open: boolean) => set({ isDrawerOpen: open }),\r\n\r\n addItem: (product) =>\r\n set((state) => {\r\n const existingItem = state.items.find(\r\n (item) => item.product.id === product.id\r\n );\r\n\r\n if (existingItem) {\r\n const items = state.items.map((item) =>\r\n item.product.id === product.id\r\n ? { ...item, quantity: item.quantity + 1 }\r\n : item\r\n );\r\n return {\r\n items,\r\n total: calculateTotal(items),\r\n itemCount: items.reduce((sum, i) => sum + i.quantity, 0),\r\n isDrawerOpen: true,\r\n };\r\n }\r\n\r\n const items = [\r\n ...state.items,\r\n { id: product.id, product, quantity: 1 },\r\n ];\r\n return {\r\n items,\r\n total: calculateTotal(items),\r\n itemCount: items.reduce((sum, i) => sum + i.quantity, 0),\r\n isDrawerOpen: true,\r\n };\r\n }),\r\n\r\n removeItem: (id) =>\r\n set((state) => {\r\n const items = state.items.filter((item) => item.id !== id);\r\n return {\r\n items,\r\n total: calculateTotal(items),\r\n itemCount: items.reduce((sum, i) => sum + i.quantity, 0),\r\n };\r\n }),\r\n\r\n updateQuantity: (id, quantity) =>\r\n set((state) => {\r\n if (quantity <= 0) {\r\n const items = state.items.filter((item) => item.id !== id);\r\n return {\r\n items,\r\n total: calculateTotal(items),\r\n itemCount: items.reduce((sum, i) => sum + i.quantity, 0),\r\n };\r\n }\r\n\r\n const items = state.items.map((item) =>\r\n item.id === id ? { ...item, quantity } : item\r\n );\r\n return {\r\n items,\r\n total: calculateTotal(items),\r\n itemCount: items.reduce((sum, i) => sum + i.quantity, 0),\r\n };\r\n }),\r\n\r\n clearCart: () => set({ items: [], total: 0, itemCount: 0 }),\r\n }),\r\n { name: \"ecommerce_cart\" }\r\n )\r\n);\r\n\r\n// Backward compatible hook - matches CartContextType with drawer state\r\nexport const useCart = (): CartContextType & { isDrawerOpen: boolean; setDrawerOpen: (open: boolean) => void } => {\r\n const store = useCartStore();\r\n return {\r\n state: { items: store.items, total: store.total },\r\n addItem: store.addItem,\r\n removeItem: store.removeItem,\r\n updateQuantity: store.updateQuantity,\r\n clearCart: store.clearCart,\r\n itemCount: store.itemCount,\r\n isDrawerOpen: store.isDrawerOpen,\r\n setDrawerOpen: store.setDrawerOpen,\r\n };\r\n};\r\n"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "ecommerce-core/stores/favorites-store.ts",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"path": "empty-page/empty-page.tsx",
|
|
21
21
|
"type": "registry:page",
|
|
22
22
|
"target": "$modules$/empty-page/empty-page.tsx",
|
|
23
|
-
"content": "import { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Layout } from \"@/components/Layout\";\n\nexport function EmptyPage() {\n const { t } = useTranslation(\"empty-page\");\n usePageTitle({ title: t(\"title\") });\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"container mx-auto px-4\">\n <h1 className=\"text-3xl font-bold\">{t(\"heading\")}</h1>\n <p className=\"text-muted-foreground mt-2\">{t(\"description\")}</p>\n\n {/* Add your page content here */}\n </div>\n </div>\n </Layout>\n );\n}\n"
|
|
23
|
+
"content": "import { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Layout } from \"@/components/Layout\";\n\nexport function EmptyPage() {\n const { t } = useTranslation(\"empty-page\");\n usePageTitle({ title: t(\"title\") });\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n <h1 className=\"text-3xl font-bold\">{t(\"heading\")}</h1>\n <p className=\"text-muted-foreground mt-2\">{t(\"description\")}</p>\n\n {/* Add your page content here */}\n </div>\n </div>\n </Layout>\n );\n}\n"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "empty-page/lang/en.json",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"path": "faq-categorized/faq-categorized.tsx",
|
|
19
19
|
"type": "registry:component",
|
|
20
20
|
"target": "$modules$/faq-categorized/faq-categorized.tsx",
|
|
21
|
-
"content": "import { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n Accordion,\r\n AccordionContent,\r\n AccordionItem,\r\n AccordionTrigger,\r\n} from \"@/components/ui/accordion\";\r\n\r\ninterface FaqCategorizedProps {\r\n className?: string;\r\n}\r\n\r\nexport function FaqCategorized({ className }: FaqCategorizedProps) {\r\n const { t } = useTranslation(\"faq-categorized\");\r\n\r\n const categories = [\r\n {\r\n title: t(\"generalTitle\", \"General\"),\r\n items: [\r\n {\r\n question: t(\"general1Q\", \"What is this platform?\"),\r\n answer: t(\"general1A\", \"Our platform is a comprehensive solution for building modern web applications. It provides tools, components, and infrastructure to help you ship faster.\"),\r\n },\r\n {\r\n question: t(\"general2Q\", \"How do I get started?\"),\r\n answer: t(\"general2A\", \"Getting started is easy! Sign up for a free account, follow our quick start guide, and you'll be up and running in minutes.\"),\r\n },\r\n {\r\n question: t(\"general3Q\", \"Is there a free trial?\"),\r\n answer: t(\"general3A\", \"Yes, we offer a 14-day free trial with full access to all features. No credit card required to start.\"),\r\n },\r\n ],\r\n },\r\n {\r\n title: t(\"billingTitle\", \"Billing\"),\r\n items: [\r\n {\r\n question: t(\"billing1Q\", \"What payment methods do you accept?\"),\r\n answer: t(\"billing1A\", \"We accept all major credit cards (Visa, MasterCard, American Express), PayPal, and bank transfers for annual plans.\"),\r\n },\r\n {\r\n question: t(\"billing2Q\", \"Can I change my plan later?\"),\r\n answer: t(\"billing2A\", \"Absolutely! You can upgrade or downgrade your plan at any time. Changes take effect immediately, and we'll prorate the difference.\"),\r\n },\r\n {\r\n question: t(\"billing3Q\", \"Do you offer refunds?\"),\r\n answer: t(\"billing3A\", \"Yes, we offer a 30-day money-back guarantee. If you're not satisfied within the first 30 days, we'll issue a full refund.\"),\r\n },\r\n ],\r\n },\r\n {\r\n title: t(\"technicalTitle\", \"Technical\"),\r\n items: [\r\n {\r\n question: t(\"technical1Q\", \"What technologies do you support?\"),\r\n answer: t(\"technical1A\", \"We support React, Vue, Angular, and vanilla JavaScript. Our APIs are RESTful and work with any backend technology.\"),\r\n },\r\n {\r\n question: t(\"technical2Q\", \"Is there an API available?\"),\r\n answer: t(\"technical2A\", \"Yes, we provide a comprehensive REST API with detailed documentation, SDKs for popular languages, and webhook support.\"),\r\n },\r\n {\r\n question: t(\"technical3Q\", \"How is my data protected?\"),\r\n answer: t(\"technical3A\", \"We use industry-standard encryption (AES-256 at rest, TLS 1.3 in transit), regular security audits, and comply with SOC 2 and GDPR requirements.\"),\r\n },\r\n ],\r\n },\r\n ];\r\n\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"container mx-auto px-4\">\r\n <div className=\"text-center mb-12\">\r\n <h2 className=\"text-3xl font-bold md:text-4xl mb-4\">\r\n {t(\"title\", \"Frequently Asked Questions\")}\r\n </h2>\r\n <p className=\"text-muted-foreground max-w-2xl mx-auto\">\r\n {t(\"subtitle\", \"Browse through our categorized FAQ to find answers to your questions.\")}\r\n </p>\r\n </div>\r\n\r\n <div className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto\">\r\n {categories.map((category, categoryIndex) => (\r\n <div key={categoryIndex}>\r\n <h3 className=\"font-semibold text-lg mb-4 text-primary\">\r\n {category.title}\r\n </h3>\r\n <Accordion type=\"single\" collapsible className=\"w-full\">\r\n {category.items.map((item, itemIndex) => (\r\n <AccordionItem\r\n key={itemIndex}\r\n value={`${categoryIndex}-${itemIndex}`}\r\n >\r\n <AccordionTrigger className=\"text-left text-sm font-medium hover:no-underline\">\r\n {item.question}\r\n </AccordionTrigger>\r\n <AccordionContent className=\"text-sm text-muted-foreground\">\r\n {item.answer}\r\n </AccordionContent>\r\n </AccordionItem>\r\n ))}\r\n </Accordion>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
|
|
21
|
+
"content": "import { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n Accordion,\r\n AccordionContent,\r\n AccordionItem,\r\n AccordionTrigger,\r\n} from \"@/components/ui/accordion\";\r\n\r\ninterface FaqCategorizedProps {\r\n className?: string;\r\n}\r\n\r\nexport function FaqCategorized({ className }: FaqCategorizedProps) {\r\n const { t } = useTranslation(\"faq-categorized\");\r\n\r\n const categories = [\r\n {\r\n title: t(\"generalTitle\", \"General\"),\r\n items: [\r\n {\r\n question: t(\"general1Q\", \"What is this platform?\"),\r\n answer: t(\"general1A\", \"Our platform is a comprehensive solution for building modern web applications. It provides tools, components, and infrastructure to help you ship faster.\"),\r\n },\r\n {\r\n question: t(\"general2Q\", \"How do I get started?\"),\r\n answer: t(\"general2A\", \"Getting started is easy! Sign up for a free account, follow our quick start guide, and you'll be up and running in minutes.\"),\r\n },\r\n {\r\n question: t(\"general3Q\", \"Is there a free trial?\"),\r\n answer: t(\"general3A\", \"Yes, we offer a 14-day free trial with full access to all features. No credit card required to start.\"),\r\n },\r\n ],\r\n },\r\n {\r\n title: t(\"billingTitle\", \"Billing\"),\r\n items: [\r\n {\r\n question: t(\"billing1Q\", \"What payment methods do you accept?\"),\r\n answer: t(\"billing1A\", \"We accept all major credit cards (Visa, MasterCard, American Express), PayPal, and bank transfers for annual plans.\"),\r\n },\r\n {\r\n question: t(\"billing2Q\", \"Can I change my plan later?\"),\r\n answer: t(\"billing2A\", \"Absolutely! You can upgrade or downgrade your plan at any time. Changes take effect immediately, and we'll prorate the difference.\"),\r\n },\r\n {\r\n question: t(\"billing3Q\", \"Do you offer refunds?\"),\r\n answer: t(\"billing3A\", \"Yes, we offer a 30-day money-back guarantee. If you're not satisfied within the first 30 days, we'll issue a full refund.\"),\r\n },\r\n ],\r\n },\r\n {\r\n title: t(\"technicalTitle\", \"Technical\"),\r\n items: [\r\n {\r\n question: t(\"technical1Q\", \"What technologies do you support?\"),\r\n answer: t(\"technical1A\", \"We support React, Vue, Angular, and vanilla JavaScript. Our APIs are RESTful and work with any backend technology.\"),\r\n },\r\n {\r\n question: t(\"technical2Q\", \"Is there an API available?\"),\r\n answer: t(\"technical2A\", \"Yes, we provide a comprehensive REST API with detailed documentation, SDKs for popular languages, and webhook support.\"),\r\n },\r\n {\r\n question: t(\"technical3Q\", \"How is my data protected?\"),\r\n answer: t(\"technical3A\", \"We use industry-standard encryption (AES-256 at rest, TLS 1.3 in transit), regular security audits, and comply with SOC 2 and GDPR requirements.\"),\r\n },\r\n ],\r\n },\r\n ];\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 <div className=\"text-center mb-12\">\r\n <h2 className=\"text-3xl font-bold md:text-4xl mb-4\">\r\n {t(\"title\", \"Frequently Asked Questions\")}\r\n </h2>\r\n <p className=\"text-muted-foreground max-w-2xl mx-auto\">\r\n {t(\"subtitle\", \"Browse through our categorized FAQ to find answers to your questions.\")}\r\n </p>\r\n </div>\r\n\r\n <div className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto\">\r\n {categories.map((category, categoryIndex) => (\r\n <div key={categoryIndex}>\r\n <h3 className=\"font-semibold text-lg mb-4 text-primary\">\r\n {category.title}\r\n </h3>\r\n <Accordion type=\"single\" collapsible className=\"w-full\">\r\n {category.items.map((item, itemIndex) => (\r\n <AccordionItem\r\n key={itemIndex}\r\n value={`${categoryIndex}-${itemIndex}`}\r\n >\r\n <AccordionTrigger className=\"text-left text-sm font-medium hover:no-underline\">\r\n {item.question}\r\n </AccordionTrigger>\r\n <AccordionContent className=\"text-sm text-muted-foreground\">\r\n {item.answer}\r\n </AccordionContent>\r\n </AccordionItem>\r\n ))}\r\n </Accordion>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"path": "faq-categorized/lang/en.json",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"path": "faq-simple/faq-simple.tsx",
|
|
19
19
|
"type": "registry:component",
|
|
20
20
|
"target": "$modules$/faq-simple/faq-simple.tsx",
|
|
21
|
-
"content": "import { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n Accordion,\r\n AccordionContent,\r\n AccordionItem,\r\n AccordionTrigger,\r\n} from \"@/components/ui/accordion\";\r\n\r\ninterface FaqSimpleProps {\r\n className?: string;\r\n}\r\n\r\nexport function FaqSimple({ className }: FaqSimpleProps) {\r\n const { t } = useTranslation(\"faq-simple\");\r\n\r\n const faqItems = [\r\n {\r\n id: \"faq-1\",\r\n question: t(\"q1\", \"What is included in the free plan?\"),\r\n answer: t(\"a1\", \"The free plan includes basic features such as up to 3 projects, 1GB storage, and email support. Perfect for individuals and small teams getting started.\"),\r\n },\r\n {\r\n id: \"faq-2\",\r\n question: t(\"q2\", \"Can I upgrade or downgrade my plan?\"),\r\n answer: t(\"a2\", \"Yes, you can upgrade or downgrade your plan at any time. Changes will be reflected in your next billing cycle. No penalties for switching plans.\"),\r\n },\r\n {\r\n id: \"faq-3\",\r\n question: t(\"q3\", \"How do I cancel my subscription?\"),\r\n answer: t(\"a3\", \"You can cancel your subscription from your account settings. Your access will continue until the end of your current billing period.\"),\r\n },\r\n {\r\n id: \"faq-4\",\r\n question: t(\"q4\", \"Is my data secure?\"),\r\n answer: t(\"a4\", \"Yes, we take security seriously. All data is encrypted at rest and in transit. We comply with industry standards and regularly undergo security audits.\"),\r\n },\r\n {\r\n id: \"faq-5\",\r\n question: t(\"q5\", \"Do you offer customer support?\"),\r\n answer: t(\"a5\", \"We offer 24/7 customer support via email and live chat. Premium plans also include phone support and dedicated account managers.\"),\r\n },\r\n {\r\n id: \"faq-6\",\r\n question: t(\"q6\", \"Can I get a refund?\"),\r\n answer: t(\"a6\", \"We offer a 30-day money-back guarantee for all paid plans. If you're not satisfied, contact our support team for a full refund.\"),\r\n },\r\n ];\r\n\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"container mx-auto px-4 max-w-3xl\">\r\n <div className=\"text-center mb-12\">\r\n <h2 className=\"text-3xl font-bold md:text-4xl mb-4\">\r\n {t(\"title\", \"Frequently Asked Questions\")}\r\n </h2>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"subtitle\", \"Find answers to common questions about our service.\")}\r\n </p>\r\n </div>\r\n\r\n <Accordion type=\"single\" collapsible className=\"w-full\">\r\n {faqItems.map((item, index) => (\r\n <AccordionItem key={item.id} value={`item-${index}`}>\r\n <AccordionTrigger className=\"text-left font-semibold hover:no-underline\">\r\n {item.question}\r\n </AccordionTrigger>\r\n <AccordionContent className=\"text-muted-foreground\">\r\n {item.answer}\r\n </AccordionContent>\r\n </AccordionItem>\r\n ))}\r\n </Accordion>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
|
|
21
|
+
"content": "import { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport {\r\n Accordion,\r\n AccordionContent,\r\n AccordionItem,\r\n AccordionTrigger,\r\n} from \"@/components/ui/accordion\";\r\n\r\ninterface FaqSimpleProps {\r\n className?: string;\r\n}\r\n\r\nexport function FaqSimple({ className }: FaqSimpleProps) {\r\n const { t } = useTranslation(\"faq-simple\");\r\n\r\n const faqItems = [\r\n {\r\n id: \"faq-1\",\r\n question: t(\"q1\", \"What is included in the free plan?\"),\r\n answer: t(\"a1\", \"The free plan includes basic features such as up to 3 projects, 1GB storage, and email support. Perfect for individuals and small teams getting started.\"),\r\n },\r\n {\r\n id: \"faq-2\",\r\n question: t(\"q2\", \"Can I upgrade or downgrade my plan?\"),\r\n answer: t(\"a2\", \"Yes, you can upgrade or downgrade your plan at any time. Changes will be reflected in your next billing cycle. No penalties for switching plans.\"),\r\n },\r\n {\r\n id: \"faq-3\",\r\n question: t(\"q3\", \"How do I cancel my subscription?\"),\r\n answer: t(\"a3\", \"You can cancel your subscription from your account settings. Your access will continue until the end of your current billing period.\"),\r\n },\r\n {\r\n id: \"faq-4\",\r\n question: t(\"q4\", \"Is my data secure?\"),\r\n answer: t(\"a4\", \"Yes, we take security seriously. All data is encrypted at rest and in transit. We comply with industry standards and regularly undergo security audits.\"),\r\n },\r\n {\r\n id: \"faq-5\",\r\n question: t(\"q5\", \"Do you offer customer support?\"),\r\n answer: t(\"a5\", \"We offer 24/7 customer support via email and live chat. Premium plans also include phone support and dedicated account managers.\"),\r\n },\r\n {\r\n id: \"faq-6\",\r\n question: t(\"q6\", \"Can I get a refund?\"),\r\n answer: t(\"a6\", \"We offer a 30-day money-back guarantee for all paid plans. If you're not satisfied, contact our support team for a full refund.\"),\r\n },\r\n ];\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 max-w-3xl\">\r\n <div className=\"text-center mb-12\">\r\n <h2 className=\"text-3xl font-bold md:text-4xl mb-4\">\r\n {t(\"title\", \"Frequently Asked Questions\")}\r\n </h2>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"subtitle\", \"Find answers to common questions about our service.\")}\r\n </p>\r\n </div>\r\n\r\n <Accordion type=\"single\" collapsible className=\"w-full\">\r\n {faqItems.map((item, index) => (\r\n <AccordionItem key={item.id} value={`item-${index}`}>\r\n <AccordionTrigger className=\"text-left font-semibold hover:no-underline\">\r\n {item.question}\r\n </AccordionTrigger>\r\n <AccordionContent className=\"text-muted-foreground\">\r\n {item.answer}\r\n </AccordionContent>\r\n </AccordionItem>\r\n ))}\r\n </Accordion>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
"path": "faq-simple/lang/en.json",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"path": "favorites-blog-block/favorites-blog-block.tsx",
|
|
20
20
|
"type": "registry:block",
|
|
21
21
|
"target": "$modules$/favorites-blog-block/favorites-blog-block.tsx",
|
|
22
|
-
"content": "import { Link } from \"react-router\";\nimport { Heart, BookOpen } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { PostCard } from \"@/modules/post-card/post-card\";\nimport { useTranslation } from \"react-i18next\";\nimport type { Post } from \"@/modules/blog-core/types\";\n\ninterface FavoritesBlogBlockProps {\n favorites: Post[];\n onClearAll?: () => void;\n}\n\nexport function FavoritesBlogBlock({\n favorites,\n onClearAll,\n}: FavoritesBlogBlockProps) {\n const { t } = useTranslation(\"favorites-blog-block\");\n\n // Empty State\n if (favorites.length === 0) {\n return (\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"container mx-auto px-4\">\n <div className=\"text-center max-w-md mx-auto\">\n <Heart className=\"w-16 h-16 text-muted-foreground mx-auto mb-6\" />\n <h1 className=\"text-3xl font-bold text-foreground mb-4\">\n {t(\"noFavoritesYet\", \"No Favorites Yet\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\n \"noFavoritesDescription\",\n \"Start browsing our blog and add posts to your favorites by clicking the heart icon.\"\n )}\n </p>\n <Button asChild size=\"lg\">\n <Link to=\"/blog\">\n <BookOpen className=\"w-5 h-5 mr-2\" />\n {t(\"browseBlog\", \"Browse Blog\")}\n </Link>\n </Button>\n </div>\n </div>\n </div>\n );\n }\n\n // Favorites Grid\n return (\n <div className=\"min-h-screen py-12\">\n <div className=\"container mx-auto px-4\">\n {/* Header */}\n <div className=\"flex justify-between items-center mb-8\">\n <div>\n <h1 className=\"text-3xl font-bold mb-2\">\n {t(\"title\", \"My Favorites\")}\n </h1>\n <p className=\"text-muted-foreground\">\n {t(\n \"favoritesCount\",\n `You have ${favorites.length} favorite posts`\n )}\n </p>\n </div>\n {onClearAll && favorites.length > 0 && (\n <Button variant=\"outline\" onClick={onClearAll}>\n {t(\"clearAll\", \"Clear All\")}\n </Button>\n )}\n </div>\n\n {/* Posts Grid */}\n <div className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-3\">\n {favorites.map((post) => (\n <PostCard key={post.id} post={post} layout=\"grid\" />\n ))}\n </div>\n </div>\n </div>\n );\n}\n"
|
|
22
|
+
"content": "import { Link } from \"react-router\";\nimport { Heart, BookOpen } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { PostCard } from \"@/modules/post-card/post-card\";\nimport { useTranslation } from \"react-i18next\";\nimport type { Post } from \"@/modules/blog-core/types\";\n\ninterface FavoritesBlogBlockProps {\n favorites: Post[];\n onClearAll?: () => void;\n}\n\nexport function FavoritesBlogBlock({\n favorites,\n onClearAll,\n}: FavoritesBlogBlockProps) {\n const { t } = useTranslation(\"favorites-blog-block\");\n\n // Empty State\n if (favorites.length === 0) {\n return (\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n <div className=\"text-center max-w-md mx-auto\">\n <Heart className=\"w-16 h-16 text-muted-foreground mx-auto mb-6\" />\n <h1 className=\"text-3xl font-bold text-foreground mb-4\">\n {t(\"noFavoritesYet\", \"No Favorites Yet\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\n \"noFavoritesDescription\",\n \"Start browsing our blog and add posts to your favorites by clicking the heart icon.\"\n )}\n </p>\n <Button asChild size=\"lg\">\n <Link to=\"/blog\">\n <BookOpen className=\"w-5 h-5 mr-2\" />\n {t(\"browseBlog\", \"Browse Blog\")}\n </Link>\n </Button>\n </div>\n </div>\n </div>\n );\n }\n\n // Favorites Grid\n return (\n <div className=\"min-h-screen py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n {/* Header */}\n <div className=\"flex justify-between items-center mb-8\">\n <div>\n <h1 className=\"text-3xl font-bold mb-2\">\n {t(\"title\", \"My Favorites\")}\n </h1>\n <p className=\"text-muted-foreground\">\n {t(\n \"favoritesCount\",\n `You have ${favorites.length} favorite posts`\n )}\n </p>\n </div>\n {onClearAll && favorites.length > 0 && (\n <Button variant=\"outline\" onClick={onClearAll}>\n {t(\"clearAll\", \"Clear All\")}\n </Button>\n )}\n </div>\n\n {/* Posts Grid */}\n <div className=\"grid gap-6 md:grid-cols-2 lg:grid-cols-3\">\n {favorites.map((post) => (\n <PostCard key={post.id} post={post} layout=\"grid\" />\n ))}\n </div>\n </div>\n </div>\n );\n}\n"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "favorites-blog-block/lang/en.json",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"path": "favorites-ecommerce-block/favorites-ecommerce-block.tsx",
|
|
20
20
|
"type": "registry:block",
|
|
21
21
|
"target": "$modules$/favorites-ecommerce-block/favorites-ecommerce-block.tsx",
|
|
22
|
-
"content": "import { Link } from \"react-router\";\nimport { Heart, ShoppingBag } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useTranslation } from \"react-i18next\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\n\ninterface FavoritesEcommerceBlockProps {\n favorites: Product[];\n onClearAll?: () => void;\n}\n\nexport function FavoritesEcommerceBlock({\n favorites,\n onClearAll,\n}: FavoritesEcommerceBlockProps) {\n const { t } = useTranslation(\"favorites-ecommerce-block\");\n\n // Empty State\n if (favorites.length === 0) {\n return (\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"container mx-auto px-4\">\n <div className=\"text-center max-w-md mx-auto\">\n <Heart className=\"w-16 h-16 text-muted-foreground mx-auto mb-6\" />\n <h1 className=\"text-3xl font-bold text-foreground mb-4\">\n {t(\"noFavoritesYet\", \"No Favorites Yet\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\n \"noFavoritesDescription\",\n \"Start browsing our products and add items to your favorites by clicking the heart icon.\"\n )}\n </p>\n <Button asChild size=\"lg\">\n <Link to=\"/products\">\n <ShoppingBag className=\"w-5 h-5 mr-2\" />\n {t(\"browseProducts\", \"Browse Products\")}\n </Link>\n </Button>\n </div>\n </div>\n </div>\n );\n }\n\n // Favorites Grid\n return (\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"container mx-auto px-4\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-8\">\n <div>\n <h1 className=\"text-3xl font-bold text-foreground mb-2\">\n {t(\"title\", \"My Favorites\")}\n </h1>\n <p className=\"text-muted-foreground\">\n {favorites.length}{\" \"}\n {t(\n \"itemsInFavorites\",\n `item${favorites.length !== 1 ? \"s\" : \"\"} in your favorites`\n )}\n </p>\n </div>\n {onClearAll && (\n <Button variant=\"outline\" onClick={onClearAll}>\n {t(\"clearAll\", \"Clear All\")}\n </Button>\n )}\n </div>\n\n {/* Products Grid */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6\">\n {favorites.map((product) => (\n <ProductCard key={product.id} product={product} variant=\"grid\" />\n ))}\n </div>\n </div>\n </div>\n );\n}\n"
|
|
22
|
+
"content": "import { Link } from \"react-router\";\nimport { Heart, ShoppingBag } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useTranslation } from \"react-i18next\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\n\ninterface FavoritesEcommerceBlockProps {\n favorites: Product[];\n onClearAll?: () => void;\n}\n\nexport function FavoritesEcommerceBlock({\n favorites,\n onClearAll,\n}: FavoritesEcommerceBlockProps) {\n const { t } = useTranslation(\"favorites-ecommerce-block\");\n\n // Empty State\n if (favorites.length === 0) {\n return (\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n <div className=\"text-center max-w-md mx-auto\">\n <Heart className=\"w-16 h-16 text-muted-foreground mx-auto mb-6\" />\n <h1 className=\"text-3xl font-bold text-foreground mb-4\">\n {t(\"noFavoritesYet\", \"No Favorites Yet\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\n \"noFavoritesDescription\",\n \"Start browsing our products and add items to your favorites by clicking the heart icon.\"\n )}\n </p>\n <Button asChild size=\"lg\">\n <Link to=\"/products\">\n <ShoppingBag className=\"w-5 h-5 mr-2\" />\n {t(\"browseProducts\", \"Browse Products\")}\n </Link>\n </Button>\n </div>\n </div>\n </div>\n );\n }\n\n // Favorites Grid\n return (\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n {/* Header */}\n <div className=\"flex items-center justify-between mb-8\">\n <div>\n <h1 className=\"text-3xl font-bold text-foreground mb-2\">\n {t(\"title\", \"My Favorites\")}\n </h1>\n <p className=\"text-muted-foreground\">\n {favorites.length}{\" \"}\n {t(\n \"itemsInFavorites\",\n `item${favorites.length !== 1 ? \"s\" : \"\"} in your favorites`\n )}\n </p>\n </div>\n {onClearAll && (\n <Button variant=\"outline\" onClick={onClearAll}>\n {t(\"clearAll\", \"Clear All\")}\n </Button>\n )}\n </div>\n\n {/* Products Grid */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6\">\n {favorites.map((product) => (\n <ProductCard key={product.id} product={product} variant=\"grid\" />\n ))}\n </div>\n </div>\n </div>\n );\n}\n"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "favorites-ecommerce-block/lang/en.json",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"path": "feature-section/feature-section.tsx",
|
|
17
17
|
"type": "registry:component",
|
|
18
18
|
"target": "$modules$/feature-section/feature-section.tsx",
|
|
19
|
-
"content": "import { Link } from \"react-router\";\nimport { Button } from \"@/components/ui/button\";\nimport { useTranslation } from \"react-i18next\";\n\nexport function FeatureSection() {\n const { t } = useTranslation(\"feature-section\");\n\n return (\n <section className=\"py-20 bg-muted/30\">\n <div className=\"container mx-auto px-4\">\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-12 items-center\">\n <div>\n <h2 className=\"text-3xl lg:text-4xl font-bold mb-6 text-foreground\">\n {t(\"heading\", \"Your Business Title Here\")}\n </h2>\n <p className=\"text-lg text-muted-foreground mb-6\">\n {t(\n \"description\",\n \"This is where your main business description will appear. AI will customize this content based on your specific industry and services.\"\n )}\n </p>\n <div className=\"space-y-4 mb-8\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-2 h-2 bg-primary rounded-full\"></div>\n <span className=\"text-foreground\">\n {t(\"feature1\", \"Key feature or benefit #1\")}\n </span>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"w-2 h-2 bg-primary rounded-full\"></div>\n <span className=\"text-foreground\">\n {t(\"feature2\", \"Key feature or benefit #2\")}\n </span>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"w-2 h-2 bg-primary rounded-full\"></div>\n <span className=\"text-foreground\">\n {t(\"feature3\", \"Key feature or benefit #3\")}\n </span>\n </div>\n </div>\n <div className=\"flex gap-4\">\n <Button asChild>\n <Link to=\"/about\">{t(\"primaryButton\", \"Learn More\")}</Link>\n </Button>\n <Button variant=\"outline\" asChild>\n <Link to=\"/contact\">{t(\"secondaryButton\", \"Get Started\")}</Link>\n </Button>\n </div>\n </div>\n <div>\n <div className=\"aspect-square bg-gradient-to-br from-primary/10 to-primary/5 rounded-2xl overflow-hidden\">\n <img\n src=\"/images/placeholder.png\"\n alt={t(\"imageAlt\", \"Business Solutions\")}\n className=\"w-full h-full object-cover\"\n />\n </div>\n </div>\n </div>\n </div>\n </section>\n );\n}\n"
|
|
19
|
+
"content": "import { Link } from \"react-router\";\nimport { Button } from \"@/components/ui/button\";\nimport { useTranslation } from \"react-i18next\";\n\nexport function FeatureSection() {\n const { t } = useTranslation(\"feature-section\");\n\n return (\n <section className=\"py-20 bg-muted/30\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-12 items-center\">\n <div>\n <h2 className=\"text-3xl lg:text-4xl font-bold mb-6 text-foreground\">\n {t(\"heading\", \"Your Business Title Here\")}\n </h2>\n <p className=\"text-lg text-muted-foreground mb-6\">\n {t(\n \"description\",\n \"This is where your main business description will appear. AI will customize this content based on your specific industry and services.\"\n )}\n </p>\n <div className=\"space-y-4 mb-8\">\n <div className=\"flex items-center gap-3\">\n <div className=\"w-2 h-2 bg-primary rounded-full\"></div>\n <span className=\"text-foreground\">\n {t(\"feature1\", \"Key feature or benefit #1\")}\n </span>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"w-2 h-2 bg-primary rounded-full\"></div>\n <span className=\"text-foreground\">\n {t(\"feature2\", \"Key feature or benefit #2\")}\n </span>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"w-2 h-2 bg-primary rounded-full\"></div>\n <span className=\"text-foreground\">\n {t(\"feature3\", \"Key feature or benefit #3\")}\n </span>\n </div>\n </div>\n <div className=\"flex gap-4\">\n <Button asChild>\n <Link to=\"/about\">{t(\"primaryButton\", \"Learn More\")}</Link>\n </Button>\n <Button variant=\"outline\" asChild>\n <Link to=\"/contact\">{t(\"secondaryButton\", \"Get Started\")}</Link>\n </Button>\n </div>\n </div>\n <div>\n <div className=\"aspect-square bg-gradient-to-br from-primary/10 to-primary/5 rounded-2xl overflow-hidden\">\n <img\n src=\"/images/placeholder.png\"\n alt={t(\"imageAlt\", \"Business Solutions\")}\n className=\"w-full h-full object-cover\"\n />\n </div>\n </div>\n </div>\n </div>\n </section>\n );\n}\n"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "feature-section/lang/en.json",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"path": "featured-products/featured-products.tsx",
|
|
20
20
|
"type": "registry:component",
|
|
21
21
|
"target": "$modules$/featured-products/featured-products.tsx",
|
|
22
|
-
"content": "import { Link } from \"react-router\";\nimport { ArrowRight } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useTranslation } from \"react-i18next\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\n\ninterface FeaturedProductsProps {\n products
|
|
22
|
+
"content": "import { Link } from \"react-router\";\nimport { ArrowRight } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useTranslation } from \"react-i18next\";\nimport { useFeaturedProducts } from \"@/modules/ecommerce-core\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\n\ninterface FeaturedProductsProps {\n products?: Product[];\n loading?: boolean;\n}\n\nexport function FeaturedProducts({\n products: propProducts,\n loading: propLoading,\n}: FeaturedProductsProps) {\n const { t } = useTranslation(\"featured-products\");\n const { products: hookProducts, loading: hookLoading } = useFeaturedProducts();\n\n const products = propProducts ?? hookProducts;\n const loading = propLoading ?? hookLoading;\n\n return (\n <section className=\"py-8 sm:py-12 md:py-16 lg:py-20 bg-background border-t border-border/20 relative\">\n <div className=\"absolute top-0 left-1/2 transform -translate-x-1/2 w-16 sm:w-24 h-px bg-primary/30\"></div>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\n <div className=\"text-center mb-6 sm:mb-8 md:mb-12 lg:mb-16 px-2\">\n <h2 className=\"text-xl sm:text-2xl md:text-3xl lg:text-4xl xl:text-5xl font-bold mb-2 sm:mb-3 md:mb-4 bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent leading-normal pb-1\">\n {t('title', 'Featured Products')}\n </h2>\n <div className=\"w-12 sm:w-16 md:w-20 h-1 bg-gradient-to-r from-primary/50 to-primary/20 mx-auto mb-3 sm:mb-4 md:mb-6 rounded-full\"></div>\n <p className=\"text-xs sm:text-sm md:text-base lg:text-lg xl:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed\">\n {t('subtitle', 'Hand-picked favorites from our collection')}\n </p>\n </div>\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-8 xl:gap-10\">\n {loading ? (\n [...Array(3)].map((_, i) => (\n <div key={i} className=\"animate-pulse group\">\n <div className=\"aspect-square bg-gradient-to-br from-muted to-muted/50 rounded-2xl mb-6\"></div>\n <div className=\"space-y-3\">\n <div className=\"h-6 bg-muted rounded-lg w-3/4\"></div>\n <div className=\"h-4 bg-muted rounded w-1/2\"></div>\n <div className=\"h-5 bg-muted rounded w-2/3\"></div>\n </div>\n </div>\n ))\n ) : (\n products.map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n variant=\"featured\"\n />\n ))\n )}\n </div>\n\n <div className=\"text-center mt-8 sm:mt-12 lg:mt-16\">\n <Button size=\"lg\" asChild className=\"px-6 sm:px-8 py-3 sm:py-4 text-base sm:text-lg\">\n <Link to=\"/products\">\n {t('viewAll', 'View All Products')}\n <ArrowRight className=\"w-4 h-4 sm:w-5 sm:h-5 ml-2\" />\n </Link>\n </Button>\n </div>\n </div>\n </section>\n );\n}\n"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "featured-products/lang/en.json",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"path": "footer-detailed/footer-detailed.tsx",
|
|
20
20
|
"type": "registry:component",
|
|
21
21
|
"target": "$modules$/footer-detailed/footer-detailed.tsx",
|
|
22
|
-
"content": "import { useState, useMemo } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport {\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Youtube,\r\n Mail,\r\n Phone,\r\n ArrowUp,\r\n Send,\r\n CreditCard,\r\n} from \"lucide-react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n youtube: Youtube,\r\n};\r\n\r\ninterface FooterDetailedProps {\r\n className?: string;\r\n}\r\n\r\nexport function FooterDetailed({ className }: FooterDetailedProps) {\r\n const { t } = useTranslation(\"footer-detailed\");\r\n const [email, setEmail] = useState(\"\");\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as Record<string, string> | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({ platform, url, Icon: socialIcons[platform] }));\r\n }, []);\r\n\r\n const handleNewsletterSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n // Newsletter subscription logic\r\n console.log(\"Newsletter subscription:\", email);\r\n setEmail(\"\");\r\n };\r\n\r\n const scrollToTop = () => {\r\n window.scrollTo({ top: 0, behavior: \"smooth\" });\r\n };\r\n\r\n const currentYear = new Date().getFullYear();\r\n\r\n const linkSections = [\r\n {\r\n title: t(\"shop\", \"Shop\"),\r\n links: [\r\n { text: t(\"allProducts\", \"All Products\"), url: \"/products\" },\r\n { text: t(\"featured\", \"Featured\"), url: \"/products?featured=true\" },\r\n { text: t(\"newArrivals\", \"New Arrivals\"), url: \"/products?new=true\" },\r\n { text: t(\"onSale\", \"On Sale\"), url: \"/products?sale=true\" },\r\n ],\r\n },\r\n {\r\n title: t(\"company\", \"Company\"),\r\n links: [\r\n { text: t(\"about\", \"About Us\"), url: \"/about\" },\r\n { text: t(\"careers\", \"Careers\"), url: \"/careers\" },\r\n { text: t(\"blog\", \"Blog\"), url: \"/blog\" },\r\n { text: t(\"press\", \"Press\"), url: \"/press\" },\r\n ],\r\n },\r\n {\r\n title: t(\"support\", \"Support\"),\r\n links: [\r\n { text: t(\"helpCenter\", \"Help Center\"), url: \"/help\" },\r\n { text: t(\"faq\", \"FAQ\"), url: \"/faq\" },\r\n { text: t(\"contact\", \"Contact Us\"), url: \"/contact\" },\r\n { text: t(\"returns\", \"Returns\"), url: \"/returns\" },\r\n ],\r\n },\r\n {\r\n title: t(\"legal\", \"Legal\"),\r\n links: [\r\n { text: t(\"privacy\", \"Privacy Policy\"), url: \"/privacy\" },\r\n { text: t(\"terms\", \"Terms of Service\"), url: \"/terms\" },\r\n { text: t(\"cookies\", \"Cookie Policy\"), url: \"/cookies\" },\r\n { text: t(\"refund\", \"Refund Policy\"), url: \"/refund\" },\r\n ],\r\n },\r\n ];\r\n\r\n return (\r\n <footer className={cn(\"border-t bg-muted/30\", className)}>\r\n {/* Main Footer */}\r\n <div className=\"container mx-auto px-4 py-12 lg:py-16\">\r\n <div className=\"grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-6\">\r\n {/* Brand & Newsletter - spans 2 columns */}\r\n <div className=\"sm:col-span-2 space-y-6\">\r\n <Logo size=\"lg\" />\r\n <p className=\"text-sm text-muted-foreground leading-relaxed max-w-sm\">\r\n {t(\"description\", \"Your trusted destination for quality products. We deliver excellence with every order.\")}\r\n </p>\r\n\r\n {/* Newsletter */}\r\n <div className=\"space-y-3\">\r\n <h4 className=\"font-semibold text-sm\">\r\n {t(\"newsletter\", \"Subscribe to our newsletter\")}\r\n </h4>\r\n <form onSubmit={handleNewsletterSubmit} className=\"flex gap-2\">\r\n <Input\r\n type=\"email\"\r\n placeholder={t(\"emailPlaceholder\", \"Enter your email\")}\r\n value={email}\r\n onChange={(e) => setEmail(e.target.value)}\r\n className=\"flex-1 h-10\"\r\n required\r\n />\r\n <Button type=\"submit\" size=\"sm\" className=\"h-10 px-4\">\r\n <Send className=\"h-4 w-4\" />\r\n </Button>\r\n </form>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {t(\"newsletterNote\", \"Get updates on new products and exclusive offers.\")}\r\n </p>\r\n </div>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"flex gap-1 pt-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors\"\r\n >\r\n <Icon className=\"h-5 w-5\" />\r\n </a>\r\n ))}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Link Sections */}\r\n {linkSections.map((section, idx) => (\r\n <div key={idx} className=\"space-y-4\">\r\n <h4 className=\"font-semibold\">{section.title}</h4>\r\n <ul className=\"space-y-2.5\">\r\n {section.links.map((link, linkIdx) => (\r\n <li key={linkIdx}>\r\n <Link\r\n to={link.url}\r\n className=\"text-sm text-muted-foreground hover:text-foreground transition-colors\"\r\n >\r\n {link.text}\r\n </Link>\r\n </li>\r\n ))}\r\n </ul>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n\r\n {/* Bottom Bar */}\r\n <div className=\"border-t bg-muted/50\">\r\n <div className=\"container mx-auto px-4 py-6\">\r\n <div className=\"flex flex-col lg:flex-row justify-between items-center gap-6\">\r\n {/* Copyright & Contact */}\r\n <div className=\"flex flex-col sm:flex-row items-center gap-4 sm:gap-6 text-sm text-muted-foreground\">\r\n <span>© {currentYear} {constants.site.name}. {t(\"allRightsReserved\", \"All rights reserved.\")}</span>\r\n <div className=\"hidden sm:block w-px h-4 bg-border\" />\r\n <div className=\"flex items-center gap-4\">\r\n <a href={`tel:${constants.phone}`} className=\"flex items-center gap-1.5 hover:text-foreground transition-colors\">\r\n <Phone className=\"h-3.5 w-3.5\" />\r\n <span>{constants.phone}</span>\r\n </a>\r\n <a href={`mailto:${constants.email}`} className=\"flex items-center gap-1.5 hover:text-foreground transition-colors\">\r\n <Mail className=\"h-3.5 w-3.5\" />\r\n <span>{constants.email}</span>\r\n </a>\r\n </div>\r\n </div>\r\n\r\n {/* Payment & Back to Top */}\r\n <div className=\"flex items-center gap-6\">\r\n {/* Payment Methods */}\r\n <div className=\"flex items-center gap-2 text-muted-foreground\">\r\n <CreditCard className=\"h-5 w-5\" />\r\n <span className=\"text-xs\">{t(\"securePayment\", \"Secure Payment\")}</span>\r\n </div>\r\n\r\n {/* Back to Top */}\r\n <Button\r\n variant=\"outline\"\r\n size=\"sm\"\r\n onClick={scrollToTop}\r\n className=\"gap-2\"\r\n >\r\n <ArrowUp className=\"h-4 w-4\" />\r\n {t(\"backToTop\", \"Top\")}\r\n </Button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </footer>\r\n );\r\n}\r\n"
|
|
22
|
+
"content": "import { useState, useMemo } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport {\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Youtube,\r\n Mail,\r\n Phone,\r\n ArrowUp,\r\n Send,\r\n CreditCard,\r\n} from \"lucide-react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n youtube: Youtube,\r\n};\r\n\r\ninterface FooterDetailedProps {\r\n className?: string;\r\n}\r\n\r\nexport function FooterDetailed({ className }: FooterDetailedProps) {\r\n const { t } = useTranslation(\"footer-detailed\");\r\n const [email, setEmail] = useState(\"\");\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as Record<string, string> | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({ platform, url, Icon: socialIcons[platform] }));\r\n }, []);\r\n\r\n const handleNewsletterSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n // Newsletter subscription logic\r\n console.log(\"Newsletter subscription:\", email);\r\n setEmail(\"\");\r\n };\r\n\r\n const scrollToTop = () => {\r\n window.scrollTo({ top: 0, behavior: \"smooth\" });\r\n };\r\n\r\n const currentYear = new Date().getFullYear();\r\n\r\n const linkSections = [\r\n {\r\n title: t(\"shop\", \"Shop\"),\r\n links: [\r\n { text: t(\"allProducts\", \"All Products\"), url: \"/products\" },\r\n { text: t(\"featured\", \"Featured\"), url: \"/products?featured=true\" },\r\n { text: t(\"newArrivals\", \"New Arrivals\"), url: \"/products?new=true\" },\r\n { text: t(\"onSale\", \"On Sale\"), url: \"/products?sale=true\" },\r\n ],\r\n },\r\n {\r\n title: t(\"company\", \"Company\"),\r\n links: [\r\n { text: t(\"about\", \"About Us\"), url: \"/about\" },\r\n { text: t(\"careers\", \"Careers\"), url: \"/careers\" },\r\n { text: t(\"blog\", \"Blog\"), url: \"/blog\" },\r\n { text: t(\"press\", \"Press\"), url: \"/press\" },\r\n ],\r\n },\r\n {\r\n title: t(\"support\", \"Support\"),\r\n links: [\r\n { text: t(\"helpCenter\", \"Help Center\"), url: \"/help\" },\r\n { text: t(\"faq\", \"FAQ\"), url: \"/faq\" },\r\n { text: t(\"contact\", \"Contact Us\"), url: \"/contact\" },\r\n { text: t(\"returns\", \"Returns\"), url: \"/returns\" },\r\n ],\r\n },\r\n {\r\n title: t(\"legal\", \"Legal\"),\r\n links: [\r\n { text: t(\"privacy\", \"Privacy Policy\"), url: \"/privacy\" },\r\n { text: t(\"terms\", \"Terms of Service\"), url: \"/terms\" },\r\n { text: t(\"cookies\", \"Cookie Policy\"), url: \"/cookies\" },\r\n { text: t(\"refund\", \"Refund Policy\"), url: \"/refund\" },\r\n ],\r\n },\r\n ];\r\n\r\n return (\r\n <footer className={cn(\"border-t bg-muted/30\", className)}>\r\n {/* Main Footer */}\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-12 lg:py-16\">\r\n <div className=\"grid grid-cols-1 gap-10 sm:grid-cols-2 lg:grid-cols-6\">\r\n {/* Brand & Newsletter - spans 2 columns */}\r\n <div className=\"sm:col-span-2 space-y-6\">\r\n <Logo size=\"lg\" />\r\n <p className=\"text-sm text-muted-foreground leading-relaxed max-w-sm\">\r\n {t(\"description\", \"Your trusted destination for quality products. We deliver excellence with every order.\")}\r\n </p>\r\n\r\n {/* Newsletter */}\r\n <div className=\"space-y-3\">\r\n <h4 className=\"font-semibold text-sm\">\r\n {t(\"newsletter\", \"Subscribe to our newsletter\")}\r\n </h4>\r\n <form onSubmit={handleNewsletterSubmit} className=\"flex gap-2\">\r\n <Input\r\n type=\"email\"\r\n placeholder={t(\"emailPlaceholder\", \"Enter your email\")}\r\n value={email}\r\n onChange={(e) => setEmail(e.target.value)}\r\n className=\"flex-1 h-10\"\r\n required\r\n />\r\n <Button type=\"submit\" size=\"sm\" className=\"h-10 px-4\">\r\n <Send className=\"h-4 w-4\" />\r\n </Button>\r\n </form>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {t(\"newsletterNote\", \"Get updates on new products and exclusive offers.\")}\r\n </p>\r\n </div>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"flex gap-1 pt-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors\"\r\n >\r\n <Icon className=\"h-5 w-5\" />\r\n </a>\r\n ))}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Link Sections */}\r\n {linkSections.map((section, idx) => (\r\n <div key={idx} className=\"space-y-4\">\r\n <h4 className=\"font-semibold\">{section.title}</h4>\r\n <ul className=\"space-y-2.5\">\r\n {section.links.map((link, linkIdx) => (\r\n <li key={linkIdx}>\r\n <Link\r\n to={link.url}\r\n className=\"text-sm text-muted-foreground hover:text-foreground transition-colors\"\r\n >\r\n {link.text}\r\n </Link>\r\n </li>\r\n ))}\r\n </ul>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n\r\n {/* Bottom Bar */}\r\n <div className=\"border-t bg-muted/50\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-6\">\r\n <div className=\"flex flex-col lg:flex-row justify-between items-center gap-6\">\r\n {/* Copyright & Contact */}\r\n <div className=\"flex flex-col sm:flex-row items-center gap-4 sm:gap-6 text-sm text-muted-foreground\">\r\n <span>© {currentYear} {constants.site.name}. {t(\"allRightsReserved\", \"All rights reserved.\")}</span>\r\n <div className=\"hidden sm:block w-px h-4 bg-border\" />\r\n <div className=\"flex items-center gap-4\">\r\n <a href={`tel:${constants.phone}`} className=\"flex items-center gap-1.5 hover:text-foreground transition-colors\">\r\n <Phone className=\"h-3.5 w-3.5\" />\r\n <span>{constants.phone}</span>\r\n </a>\r\n <a href={`mailto:${constants.email}`} className=\"flex items-center gap-1.5 hover:text-foreground transition-colors\">\r\n <Mail className=\"h-3.5 w-3.5\" />\r\n <span>{constants.email}</span>\r\n </a>\r\n </div>\r\n </div>\r\n\r\n {/* Payment & Back to Top */}\r\n <div className=\"flex items-center gap-6\">\r\n {/* Payment Methods */}\r\n <div className=\"flex items-center gap-2 text-muted-foreground\">\r\n <CreditCard className=\"h-5 w-5\" />\r\n <span className=\"text-xs\">{t(\"securePayment\", \"Secure Payment\")}</span>\r\n </div>\r\n\r\n {/* Back to Top */}\r\n <Button\r\n variant=\"outline\"\r\n size=\"sm\"\r\n onClick={scrollToTop}\r\n className=\"gap-2\"\r\n >\r\n <ArrowUp className=\"h-4 w-4\" />\r\n {t(\"backToTop\", \"Top\")}\r\n </Button>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </footer>\r\n );\r\n}\r\n"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "footer-detailed/lang/en.json",
|