@promakeai/cli 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,7 +33,7 @@
33
33
  "path": "blog-core/useDbPosts.ts",
34
34
  "type": "registry:hook",
35
35
  "target": "$modules$/blog-core/useDbPosts.ts",
36
- "content": "import { useMemo } from 'react';\nimport type { Post, BlogCategory, PostCategory } from './types';\nimport {\n useRepositoryQuery,\n useRawQuery,\n useRawQueryOne,\n parseStringToArray,\n parseSQLiteBoolean,\n parseNumberSafe\n} from '@/modules/db';\n\nconst transformPost = (row: any): Post => {\n const categoryNames = row.category_names ? row.category_names.split(',') : [];\n const categorySlugs = row.category_slugs ? row.category_slugs.split(',') : [];\n const categoryIds = row.category_ids ? row.category_ids.split(',').map(Number) : [];\n\n const categories: PostCategory[] = categoryIds.map((id: number, index: number) => ({\n id,\n name: categoryNames[index] || '',\n slug: categorySlugs[index] || '',\n is_primary: index === 0\n }));\n\n const primaryCategory = categories.length > 0 ? categories[0] : null;\n\n return {\n id: parseNumberSafe(row.id),\n title: String(row.title || ''),\n slug: String(row.slug || ''),\n excerpt: row.excerpt || '',\n content: row.content || '',\n author: row.author || '',\n author_avatar: row.author_avatar || '',\n published_at: row.published_at || new Date().toISOString(),\n updated_at: row.updated_at || new Date().toISOString(),\n featured_image: row.featured_image || '',\n images: parseStringToArray(row.images),\n tags: parseStringToArray(row.tags),\n read_time: parseNumberSafe(row.read_time),\n view_count: parseNumberSafe(row.view_count),\n featured: parseSQLiteBoolean(row.featured),\n published: parseSQLiteBoolean(row.published),\n meta_description: row.meta_description || '',\n meta_keywords: row.meta_keywords || '',\n category: primaryCategory?.slug || '',\n category_name: primaryCategory?.name || '',\n categories\n };\n};\n\nconst POSTS_WITH_CATEGORIES_SQL = `\n SELECT p.*,\n GROUP_CONCAT(c.name) as category_names,\n GROUP_CONCAT(c.slug) as category_slugs,\n GROUP_CONCAT(c.id) as category_ids\n FROM posts p\n LEFT JOIN post_categories pc ON p.id = pc.post_id\n LEFT JOIN blog_categories c ON pc.category_id = c.id\n WHERE p.published = 1\n GROUP BY p.id\n`;\n\nexport function useDbPosts() {\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.published_at DESC`;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'all'],\n sql\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbFeaturedPosts() {\n const sql = `\n SELECT p.*,\n GROUP_CONCAT(c.name) as category_names,\n GROUP_CONCAT(c.slug) as category_slugs,\n GROUP_CONCAT(c.id) as category_ids\n FROM posts p\n LEFT JOIN post_categories pc ON p.id = pc.post_id\n LEFT JOIN blog_categories c ON pc.category_id = c.id\n WHERE p.published = 1 AND p.featured = 1\n GROUP BY p.id\n ORDER BY p.published_at DESC\n `;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'featured'],\n sql\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbRecentPosts(limit: number = 10) {\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.published_at DESC LIMIT ?`;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'recent', limit],\n sql,\n [limit]\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbPopularPosts(limit: number = 10) {\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.view_count DESC LIMIT ?`;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'popular', limit],\n sql,\n [limit]\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbPostsByCategory(categorySlug: string) {\n const sql = `\n SELECT p.*,\n GROUP_CONCAT(c.name) as category_names,\n GROUP_CONCAT(c.slug) as category_slugs,\n GROUP_CONCAT(c.id) as category_ids\n FROM posts p\n LEFT JOIN post_categories pc ON p.id = pc.post_id\n LEFT JOIN blog_categories c ON pc.category_id = c.id\n WHERE p.published = 1 AND p.id IN (\n SELECT pc2.post_id FROM post_categories pc2\n JOIN blog_categories c2 ON pc2.category_id = c2.id\n WHERE c2.slug = ?\n )\n GROUP BY p.id\n ORDER BY p.published_at DESC\n `;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'category', categorySlug],\n sql,\n [categorySlug],\n { enabled: !!categorySlug }\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbPostsByTag(tag: string) {\n const sql = `${POSTS_WITH_CATEGORIES_SQL} AND (p.tags LIKE ? OR p.tags LIKE ? OR p.tags LIKE ? OR p.tags = ?) ORDER BY p.published_at DESC`;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'tag', tag],\n sql,\n [`%\"${tag}\"%`, `%,${tag},%`, `${tag},%`, tag],\n { enabled: !!tag }\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbPostSearch(query: string) {\n const searchTerm = `%${query}%`;\n const sql = `${POSTS_WITH_CATEGORIES_SQL} AND (p.title LIKE ? OR p.content LIKE ? OR p.excerpt LIKE ?) ORDER BY p.published_at DESC`;\n\n const { data, isLoading: loading, error } = useRawQuery<any>(\n ['posts', 'search', query],\n sql,\n [searchTerm, searchTerm, searchTerm],\n { enabled: !!query.trim() }\n );\n\n const posts = useMemo(() => {\n if (!data) return [];\n return data.map(transformPost);\n }, [data]);\n\n return { posts, loading, error: error?.message ?? null };\n}\n\nexport function useDbPostBySlug(slug: string) {\n const sql = `\n SELECT p.*,\n GROUP_CONCAT(c.name) as category_names,\n GROUP_CONCAT(c.slug) as category_slugs,\n GROUP_CONCAT(c.id) as category_ids\n FROM posts p\n LEFT JOIN post_categories pc ON p.id = pc.post_id\n LEFT JOIN blog_categories c ON pc.category_id = c.id\n WHERE p.slug = ? AND p.published = 1\n GROUP BY p.id\n `;\n\n const { data, isLoading: loading, error } = useRawQueryOne<any>(\n ['posts', 'slug', slug],\n sql,\n [slug],\n { enabled: !!slug }\n );\n\n const post = useMemo(() => {\n if (!data) return null;\n return transformPost(data);\n }, [data]);\n\n return {\n post,\n loading,\n error: !data && !loading && slug ? 'Post not found' : (error?.message ?? null)\n };\n}\n\nexport function useDbBlogCategories() {\n const { data, isLoading: loading, error } = useRepositoryQuery<BlogCategory>('blog_categories', {\n orderBy: [{ field: 'name', direction: 'ASC' }]\n });\n\n return {\n categories: data ?? [],\n loading,\n error: error?.message ?? null\n };\n}\n\nexport function useDbPostStats() {\n const { data: statsData, isLoading: loading, error } = useRawQueryOne<{\n totalPosts: number;\n totalViews: number;\n featuredPosts: number;\n }>(\n ['posts', 'stats'],\n `SELECT\n COUNT(*) as totalPosts,\n COALESCE(SUM(view_count), 0) as totalViews,\n SUM(CASE WHEN featured = 1 THEN 1 ELSE 0 END) as featuredPosts\n FROM posts WHERE published = 1`\n );\n\n const { data: categoriesData } = useRawQueryOne<{ count: number }>(\n ['categories', 'count'],\n `SELECT COUNT(*) as count FROM blog_categories`\n );\n\n const stats = useMemo(() => ({\n totalPosts: statsData?.totalPosts ?? 0,\n totalViews: statsData?.totalViews ?? 0,\n featuredPosts: statsData?.featuredPosts ?? 0,\n categoriesCount: categoriesData?.count ?? 0\n }), [statsData, categoriesData]);\n\n return { stats, loading, error: error?.message ?? null };\n}\n"
36
+ "content": "import { useMemo } from 'react';\r\nimport type { Post, BlogCategory, PostCategory } from './types';\r\nimport {\r\n useRepositoryQuery,\r\n useRawQuery,\r\n useRawQueryOne,\r\n parseStringToArray,\r\n parseSQLiteBoolean,\r\n parseNumberSafe\r\n} from '@/modules/db';\r\n\r\nconst transformPost = (row: any): Post => {\r\n const categoryNames = row.category_names ? row.category_names.split(',') : [];\r\n const categorySlugs = row.category_slugs ? row.category_slugs.split(',') : [];\r\n const categoryIds = row.category_ids ? row.category_ids.split(',').map(Number) : [];\r\n\r\n const categories: PostCategory[] = categoryIds.map((id: number, index: number) => ({\r\n id,\r\n name: categoryNames[index] || '',\r\n slug: categorySlugs[index] || '',\r\n is_primary: index === 0\r\n }));\r\n\r\n const primaryCategory = categories.length > 0 ? categories[0] : null;\r\n\r\n return {\r\n id: parseNumberSafe(row.id),\r\n title: String(row.title || ''),\r\n slug: String(row.slug || ''),\r\n excerpt: row.excerpt || '',\r\n content: row.content || '',\r\n author: row.author || '',\r\n author_avatar: row.author_avatar || '',\r\n published_at: row.published_at || new Date().toISOString(),\r\n updated_at: row.updated_at || new Date().toISOString(),\r\n featured_image: row.featured_image || '',\r\n images: parseStringToArray(row.images),\r\n tags: parseStringToArray(row.tags),\r\n read_time: parseNumberSafe(row.read_time),\r\n view_count: parseNumberSafe(row.view_count),\r\n featured: parseSQLiteBoolean(row.featured),\r\n published: parseSQLiteBoolean(row.published),\r\n meta_description: row.meta_description || '',\r\n meta_keywords: row.meta_keywords || '',\r\n category: primaryCategory?.slug || '',\r\n category_name: primaryCategory?.name || '',\r\n categories\r\n };\r\n};\r\n\r\nconst POSTS_WITH_CATEGORIES_SQL = `\r\n SELECT p.*,\r\n GROUP_CONCAT(c.name) as category_names,\r\n GROUP_CONCAT(c.slug) as category_slugs,\r\n GROUP_CONCAT(c.id) as category_ids\r\n FROM posts p\r\n LEFT JOIN post_categories pc ON p.id = pc.post_id\r\n LEFT JOIN blog_categories c ON pc.category_id = c.id\r\n WHERE p.published = 1\r\n GROUP BY p.id\r\n`;\r\n\r\nexport function useDbPosts() {\r\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.published_at DESC`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'all'],\r\n sql\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbFeaturedPosts() {\r\n const sql = `\r\n SELECT p.*,\r\n GROUP_CONCAT(c.name) as category_names,\r\n GROUP_CONCAT(c.slug) as category_slugs,\r\n GROUP_CONCAT(c.id) as category_ids\r\n FROM posts p\r\n LEFT JOIN post_categories pc ON p.id = pc.post_id\r\n LEFT JOIN blog_categories c ON pc.category_id = c.id\r\n WHERE p.published = 1 AND p.featured = 1\r\n GROUP BY p.id\r\n ORDER BY p.published_at DESC\r\n `;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'featured'],\r\n sql\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbRecentPosts(limit: number = 10) {\r\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.published_at DESC LIMIT ?`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'recent', limit],\r\n sql,\r\n [limit]\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbPopularPosts(limit: number = 10) {\r\n const sql = `${POSTS_WITH_CATEGORIES_SQL} ORDER BY p.view_count DESC LIMIT ?`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'popular', limit],\r\n sql,\r\n [limit]\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbPostsByCategory(categorySlug: string) {\r\n const sql = `\r\n SELECT p.*,\r\n GROUP_CONCAT(c.name) as category_names,\r\n GROUP_CONCAT(c.slug) as category_slugs,\r\n GROUP_CONCAT(c.id) as category_ids\r\n FROM posts p\r\n LEFT JOIN post_categories pc ON p.id = pc.post_id\r\n LEFT JOIN blog_categories c ON pc.category_id = c.id\r\n WHERE p.published = 1 AND p.id IN (\r\n SELECT pc2.post_id FROM post_categories pc2\r\n JOIN blog_categories c2 ON pc2.category_id = c2.id\r\n WHERE c2.slug = ?\r\n )\r\n GROUP BY p.id\r\n ORDER BY p.published_at DESC\r\n `;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'category', categorySlug],\r\n sql,\r\n [categorySlug],\r\n { enabled: !!categorySlug }\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbPostsByTag(tag: string) {\r\n const sql = `${POSTS_WITH_CATEGORIES_SQL} AND (p.tags LIKE ? OR p.tags LIKE ? OR p.tags LIKE ? OR p.tags = ?) ORDER BY p.published_at DESC`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'tag', tag],\r\n sql,\r\n [`%\"${tag}\"%`, `%,${tag},%`, `${tag},%`, tag],\r\n { enabled: !!tag }\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbPostSearch(query: string) {\r\n const searchTerm = `%${query}%`;\r\n const sql = `${POSTS_WITH_CATEGORIES_SQL} AND (p.title LIKE ? OR p.content LIKE ? OR p.excerpt LIKE ?) ORDER BY p.published_at DESC`;\r\n\r\n const { data, isLoading: loading, error } = useRawQuery<any>(\r\n ['posts', 'search', query],\r\n sql,\r\n [searchTerm, searchTerm, searchTerm],\r\n { enabled: !!query.trim() }\r\n );\r\n\r\n const posts = useMemo(() => {\r\n if (!data) return [];\r\n return data.map(transformPost);\r\n }, [data]);\r\n\r\n return { posts, loading, error: error?.message ?? null };\r\n}\r\n\r\nexport function useDbPostBySlug(slug: string) {\r\n const sql = `\r\n SELECT p.*,\r\n GROUP_CONCAT(c.name) as category_names,\r\n GROUP_CONCAT(c.slug) as category_slugs,\r\n GROUP_CONCAT(c.id) as category_ids\r\n FROM posts p\r\n LEFT JOIN post_categories pc ON p.id = pc.post_id\r\n LEFT JOIN blog_categories c ON pc.category_id = c.id\r\n WHERE p.slug = ? AND p.published = 1\r\n GROUP BY p.id\r\n `;\r\n\r\n const { data, isLoading: loading, error } = useRawQueryOne<any>(\r\n ['posts', 'slug', slug],\r\n sql,\r\n [slug],\r\n { enabled: !!slug }\r\n );\r\n\r\n const post = useMemo(() => {\r\n if (!data) return null;\r\n return transformPost(data);\r\n }, [data]);\r\n\r\n return {\r\n post,\r\n loading,\r\n error: !data && !loading && slug ? 'Post not found' : (error?.message ?? null)\r\n };\r\n}\r\n\r\nexport function useDbBlogCategories() {\r\n const { data, isLoading: loading, error } = useRepositoryQuery<BlogCategory>('blog_categories', {\r\n orderBy: [{ field: 'name', direction: 'ASC' }]\r\n });\r\n\r\n return {\r\n categories: data ?? [],\r\n loading,\r\n error: error?.message ?? null\r\n };\r\n}\r\n\r\nexport function useDbPostStats() {\r\n const { data: statsData, isLoading: loading, error } = useRawQueryOne<{\r\n totalPosts: number;\r\n totalViews: number;\r\n featuredPosts: number;\r\n }>(\r\n ['posts', 'stats'],\r\n `SELECT\r\n COUNT(*) as totalPosts,\r\n COALESCE(SUM(view_count), 0) as totalViews,\r\n SUM(CASE WHEN featured = 1 THEN 1 ELSE 0 END) as featuredPosts\r\n FROM posts WHERE published = 1`\r\n );\r\n\r\n const { data: categoriesData } = useRawQueryOne<{ count: number }>(\r\n ['categories', 'count'],\r\n `SELECT COUNT(*) as count FROM blog_categories`\r\n );\r\n\r\n const stats = useMemo(() => ({\r\n totalPosts: statsData?.totalPosts ?? 0,\r\n totalViews: statsData?.totalViews ?? 0,\r\n featuredPosts: statsData?.featuredPosts ?? 0,\r\n categoriesCount: categoriesData?.count ?? 0\r\n }), [statsData, categoriesData]);\r\n\r\n return { stats, loading, error: error?.message ?? null };\r\n}\r\n"
37
37
  },
38
38
  {
39
39
  "path": "blog-core/lang/en.json",
@@ -24,7 +24,7 @@
24
24
  "path": "blog-list-page/blog-list-page.tsx",
25
25
  "type": "registry:page",
26
26
  "target": "$modules$/blog-list-page/blog-list-page.tsx",
27
- "content": "import { useState, useEffect } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { Layout } from \"@/components/Layout\";\nimport { Search, Filter } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { FadeIn } from \"@/modules/animations\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport {\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n SheetTrigger,\n} from \"@/components/ui/sheet\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { PostCard } from \"@/modules/post-card/post-card\";\nimport { useDbPosts, useDbBlogCategories, type BlogCategory } from \"@/modules/blog-core\";\n\ninterface FilterSectionProps {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n t: any;\n searchTerm: string;\n setSearchTerm: (term: string) => void;\n categories: BlogCategory[];\n selectedCategories: string[];\n handleCategoryChange: (slug: string, checked: boolean) => void;\n allTags: string[];\n selectedTags: string[];\n handleTagChange: (tag: string, checked: boolean) => void;\n clearFilters: () => void;\n}\n\nfunction FilterSection({\n t,\n searchTerm,\n setSearchTerm,\n categories,\n selectedCategories,\n handleCategoryChange,\n allTags,\n selectedTags,\n handleTagChange,\n clearFilters,\n}: FilterSectionProps) {\n return (\n <div className=\"space-y-6\">\n <div>\n <h3 className=\"font-semibold mb-3 flex items-center gap-2\">\n <Search className=\"h-4 w-4\" />\n {t(\"search\")}\n </h3>\n <Input\n placeholder={t(\"searchPlaceholder\")}\n value={searchTerm}\n onChange={(e) => setSearchTerm(e.target.value)}\n />\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-3\">{t(\"categories\")}</h3>\n <div className=\"space-y-2\">\n {categories.map((category) => (\n <div key={category.slug} className=\"flex items-center space-x-2\" data-db-table=\"blog_categories\" data-db-id={category.id || category.slug}>\n <Checkbox\n id={`category-${category.slug}`}\n checked={selectedCategories.includes(category.slug)}\n onCheckedChange={(checked) =>\n handleCategoryChange(category.slug, checked as boolean)\n }\n />\n <label\n htmlFor={`category-${category.slug}`}\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\n >\n {category.name}\n </label>\n </div>\n ))}\n </div>\n </div>\n\n {allTags.length > 0 && (\n <div>\n <h3 className=\"font-semibold mb-3\">{t(\"tags\")}</h3>\n <div className=\"space-y-2 max-h-48 overflow-y-auto\">\n {allTags.slice(0, 20).map((tag) => (\n <div key={tag} className=\"flex items-center space-x-2\">\n <Checkbox\n id={`tag-${tag}`}\n checked={selectedTags.includes(tag)}\n onCheckedChange={(checked) =>\n handleTagChange(tag, checked as boolean)\n }\n />\n <label\n htmlFor={`tag-${tag}`}\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\n >\n {tag}\n </label>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {(searchTerm ||\n selectedCategories.length > 0 ||\n selectedTags.length > 0) && (\n <Button variant=\"outline\" onClick={clearFilters} className=\"w-full\">\n {t(\"clearFilters\")}\n </Button>\n )}\n </div>\n );\n}\n\nexport function BlogListPage() {\n const { t } = useTranslation(\"blog-list-page\");\n usePageTitle({ title: t(\"title\") });\n\n const [searchParams, setSearchParams] = useSearchParams();\n const [searchTerm, setSearchTerm] = useState(\n searchParams.get(\"search\") || \"\"\n );\n const [selectedCategories, setSelectedCategories] = useState<string[]>(\n searchParams.get(\"categories\")?.split(\",\").filter(Boolean) || []\n );\n const [selectedTags, setSelectedTags] = useState<string[]>(\n searchParams.get(\"tags\")?.split(\",\").filter(Boolean) || []\n );\n const [sortBy, setSortBy] = useState(searchParams.get(\"sort\") || \"newest\");\n const [viewMode, _setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\n\n const { posts, loading, error } = useDbPosts();\n const { categories } = useDbBlogCategories();\n\n const filteredPosts = posts.filter((post) => {\n if (searchTerm) {\n const searchLower = searchTerm.toLowerCase();\n if (\n !post.title.toLowerCase().includes(searchLower) &&\n !post.excerpt.toLowerCase().includes(searchLower) &&\n !post.content.toLowerCase().includes(searchLower)\n ) {\n return false;\n }\n }\n\n if (selectedCategories.length > 0) {\n const hasMatchingCategory = selectedCategories.some(\n (categorySlug) =>\n post.category === categorySlug ||\n post.categories?.some((cat) => cat.slug === categorySlug)\n );\n if (!hasMatchingCategory) return false;\n }\n\n if (selectedTags.length > 0) {\n const hasMatchingTag = selectedTags.some((tag) =>\n post.tags.includes(tag)\n );\n if (!hasMatchingTag) return false;\n }\n\n return true;\n });\n\n const sortedPosts = [...filteredPosts].sort((a, b) => {\n switch (sortBy) {\n case \"oldest\":\n return (\n new Date(a.published_at).getTime() -\n new Date(b.published_at).getTime()\n );\n case \"popular\":\n return (b.view_count || 0) - (a.view_count || 0);\n case \"reading-time\":\n return (a.read_time || 0) - (b.read_time || 0);\n case \"newest\":\n default:\n return (\n new Date(b.published_at).getTime() -\n new Date(a.published_at).getTime()\n );\n }\n });\n\n useEffect(() => {\n const params = new URLSearchParams();\n if (searchTerm) params.set(\"search\", searchTerm);\n if (selectedCategories.length)\n params.set(\"categories\", selectedCategories.join(\",\"));\n if (selectedTags.length) params.set(\"tags\", selectedTags.join(\",\"));\n if (sortBy !== \"newest\") params.set(\"sort\", sortBy);\n\n setSearchParams(params);\n }, [searchTerm, selectedCategories, selectedTags, sortBy, setSearchParams]);\n\n const handleCategoryChange = (categorySlug: string, checked: boolean) => {\n if (checked) {\n setSelectedCategories([...selectedCategories, categorySlug]);\n } else {\n setSelectedCategories(\n selectedCategories.filter((c) => c !== categorySlug)\n );\n }\n };\n\n const handleTagChange = (tag: string, checked: boolean) => {\n if (checked) {\n setSelectedTags([...selectedTags, tag]);\n } else {\n setSelectedTags(selectedTags.filter((t) => t !== tag));\n }\n };\n\n const allTags = Array.from(new Set(posts.flatMap((post) => post.tags)));\n\n const clearFilters = () => {\n setSearchTerm(\"\");\n setSelectedCategories([]);\n setSelectedTags([]);\n setSortBy(\"newest\");\n };\n\n const filterProps: FilterSectionProps = {\n t,\n searchTerm,\n setSearchTerm,\n categories,\n selectedCategories,\n handleCategoryChange,\n allTags,\n selectedTags,\n handleTagChange,\n clearFilters,\n };\n\n if (loading) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"animate-pulse space-y-4\">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className=\"h-48 bg-muted rounded-lg\"></div>\n ))}\n </div>\n </div>\n </Layout>\n );\n }\n\n if (error) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8 text-center\">\n <p className=\"text-destructive\">{t(\"error\")}</p>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 mb-8\">\n <div>\n <h1 className=\"text-3xl font-bold mb-2\">{t(\"title\")}</h1>\n <p className=\"text-muted-foreground\">{t(\"subtitle\")}</p>\n </div>\n\n <div className=\"flex items-center gap-4\">\n <Select value={sortBy} onValueChange={setSortBy}>\n <SelectTrigger className=\"w-[180px]\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"newest\">{t(\"sortNewest\")}</SelectItem>\n <SelectItem value=\"oldest\">{t(\"sortOldest\")}</SelectItem>\n <SelectItem value=\"popular\">{t(\"sortPopular\")}</SelectItem>\n <SelectItem value=\"reading-time\">\n {t(\"sortReadingTime\")}\n </SelectItem>\n </SelectContent>\n </Select>\n\n <Sheet>\n <SheetTrigger asChild>\n <Button variant=\"outline\" size=\"sm\" className=\"lg:hidden\">\n <Filter className=\"h-4 w-4 mr-2\" />\n {t(\"filters\")}\n </Button>\n </SheetTrigger>\n <SheetContent>\n <SheetHeader>\n <SheetTitle>{t(\"filters\")}</SheetTitle>\n <SheetDescription>{t(\"filterDescription\")}</SheetDescription>\n </SheetHeader>\n <div className=\"mt-6\">\n <FilterSection {...filterProps} />\n </div>\n </SheetContent>\n </Sheet>\n </div>\n </FadeIn>\n\n <div className=\"flex flex-col lg:flex-row gap-8\">\n <div className=\"hidden lg:block w-64 flex-shrink-0\">\n <div className=\"sticky top-4\">\n <FilterSection {...filterProps} />\n </div>\n </div>\n\n <div className=\"flex-1\">\n <div className=\"flex items-center justify-between mb-6\">\n <p className=\"text-sm text-muted-foreground\">\n {t(\"showing\")} {sortedPosts.length} {t(\"of\")} {posts.length}{\" \"}\n {t(\"posts\")}\n {searchTerm && (\n <span className=\"ml-1\">\n {t(\"for\")} \"<strong>{searchTerm}</strong>\"\n </span>\n )}\n </p>\n </div>\n\n {sortedPosts.length > 0 ? (\n <div\n className={`grid gap-6 ${\n viewMode === \"grid\"\n ? \"grid-cols-1 md:grid-cols-2 xl:grid-cols-3\"\n : \"grid-cols-1\"\n }`}\n >\n {sortedPosts.map((post) => (\n <div key={post.id} className=\"contents\" data-db-table=\"posts\" data-db-id={post.id}>\n <PostCard post={post} layout={viewMode} />\n </div>\n ))}\n </div>\n ) : (\n <div className=\"text-center py-12\">\n <p className=\"text-muted-foreground mb-4\">\n {t(\"noPostsFound\")}\n </p>\n <Button onClick={clearFilters} variant=\"outline\">\n {t(\"clearFilters\")}\n </Button>\n </div>\n )}\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default BlogListPage;\n"
27
+ "content": "import { useState, useEffect } from \"react\";\r\nimport { useSearchParams } from \"react-router\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Search, Filter } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport {\r\n Sheet,\r\n SheetContent,\r\n SheetDescription,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport { PostCard } from \"@/modules/post-card/post-card\";\r\nimport { useDbPosts, useDbBlogCategories, type BlogCategory } from \"@/modules/blog-core\";\r\n\r\ninterface FilterSectionProps {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n t: any;\r\n searchTerm: string;\r\n setSearchTerm: (term: string) => void;\r\n categories: BlogCategory[];\r\n selectedCategories: string[];\r\n handleCategoryChange: (slug: string, checked: boolean) => void;\r\n allTags: string[];\r\n selectedTags: string[];\r\n handleTagChange: (tag: string, checked: boolean) => void;\r\n clearFilters: () => void;\r\n}\r\n\r\nfunction FilterSection({\r\n t,\r\n searchTerm,\r\n setSearchTerm,\r\n categories,\r\n selectedCategories,\r\n handleCategoryChange,\r\n allTags,\r\n selectedTags,\r\n handleTagChange,\r\n clearFilters,\r\n}: FilterSectionProps) {\r\n return (\r\n <div className=\"space-y-6\">\r\n <div>\r\n <h3 className=\"font-semibold mb-3 flex items-center gap-2\">\r\n <Search className=\"h-4 w-4\" />\r\n {t(\"search\")}\r\n </h3>\r\n <Input\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={(e) => setSearchTerm(e.target.value)}\r\n />\r\n </div>\r\n\r\n <div>\r\n <h3 className=\"font-semibold mb-3\">{t(\"categories\")}</h3>\r\n <div className=\"space-y-2\">\r\n {categories.map((category) => (\r\n <div key={category.slug} className=\"flex items-center space-x-2\" data-db-table=\"blog_categories\" data-db-id={category.id || category.slug}>\r\n <Checkbox\r\n id={`category-${category.slug}`}\r\n checked={selectedCategories.includes(category.slug)}\r\n onCheckedChange={(checked) =>\r\n handleCategoryChange(category.slug, checked as boolean)\r\n }\r\n />\r\n <label\r\n htmlFor={`category-${category.slug}`}\r\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\r\n >\r\n {category.name}\r\n </label>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n\r\n {allTags.length > 0 && (\r\n <div>\r\n <h3 className=\"font-semibold mb-3\">{t(\"tags\")}</h3>\r\n <div className=\"space-y-2 max-h-48 overflow-y-auto\">\r\n {allTags.slice(0, 20).map((tag) => (\r\n <div key={tag} className=\"flex items-center space-x-2\">\r\n <Checkbox\r\n id={`tag-${tag}`}\r\n checked={selectedTags.includes(tag)}\r\n onCheckedChange={(checked) =>\r\n handleTagChange(tag, checked as boolean)\r\n }\r\n />\r\n <label\r\n htmlFor={`tag-${tag}`}\r\n className=\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer\"\r\n >\r\n {tag}\r\n </label>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {(searchTerm ||\r\n selectedCategories.length > 0 ||\r\n selectedTags.length > 0) && (\r\n <Button variant=\"outline\" onClick={clearFilters} className=\"w-full\">\r\n {t(\"clearFilters\")}\r\n </Button>\r\n )}\r\n </div>\r\n );\r\n}\r\n\r\nexport function BlogListPage() {\r\n const { t } = useTranslation(\"blog-list-page\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const [searchParams, setSearchParams] = useSearchParams();\r\n const [searchTerm, setSearchTerm] = useState(\r\n searchParams.get(\"search\") || \"\"\r\n );\r\n const [selectedCategories, setSelectedCategories] = useState<string[]>(\r\n searchParams.get(\"categories\")?.split(\",\").filter(Boolean) || []\r\n );\r\n const [selectedTags, setSelectedTags] = useState<string[]>(\r\n searchParams.get(\"tags\")?.split(\",\").filter(Boolean) || []\r\n );\r\n const [sortBy, setSortBy] = useState(searchParams.get(\"sort\") || \"newest\");\r\n const [viewMode, _setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\r\n\r\n const { posts, loading, error } = useDbPosts();\r\n const { categories } = useDbBlogCategories();\r\n\r\n const filteredPosts = posts.filter((post) => {\r\n if (searchTerm) {\r\n const searchLower = searchTerm.toLowerCase();\r\n if (\r\n !post.title.toLowerCase().includes(searchLower) &&\r\n !post.excerpt.toLowerCase().includes(searchLower) &&\r\n !post.content.toLowerCase().includes(searchLower)\r\n ) {\r\n return false;\r\n }\r\n }\r\n\r\n if (selectedCategories.length > 0) {\r\n const hasMatchingCategory = selectedCategories.some(\r\n (categorySlug) =>\r\n post.category === categorySlug ||\r\n post.categories?.some((cat) => cat.slug === categorySlug)\r\n );\r\n if (!hasMatchingCategory) return false;\r\n }\r\n\r\n if (selectedTags.length > 0) {\r\n const hasMatchingTag = selectedTags.some((tag) =>\r\n post.tags.includes(tag)\r\n );\r\n if (!hasMatchingTag) return false;\r\n }\r\n\r\n return true;\r\n });\r\n\r\n const sortedPosts = [...filteredPosts].sort((a, b) => {\r\n switch (sortBy) {\r\n case \"oldest\":\r\n return (\r\n new Date(a.published_at).getTime() -\r\n new Date(b.published_at).getTime()\r\n );\r\n case \"popular\":\r\n return (b.view_count || 0) - (a.view_count || 0);\r\n case \"reading-time\":\r\n return (a.read_time || 0) - (b.read_time || 0);\r\n case \"newest\":\r\n default:\r\n return (\r\n new Date(b.published_at).getTime() -\r\n new Date(a.published_at).getTime()\r\n );\r\n }\r\n });\r\n\r\n useEffect(() => {\r\n const params = new URLSearchParams();\r\n if (searchTerm) params.set(\"search\", searchTerm);\r\n if (selectedCategories.length)\r\n params.set(\"categories\", selectedCategories.join(\",\"));\r\n if (selectedTags.length) params.set(\"tags\", selectedTags.join(\",\"));\r\n if (sortBy !== \"newest\") params.set(\"sort\", sortBy);\r\n\r\n setSearchParams(params);\r\n }, [searchTerm, selectedCategories, selectedTags, sortBy, setSearchParams]);\r\n\r\n const handleCategoryChange = (categorySlug: string, checked: boolean) => {\r\n if (checked) {\r\n setSelectedCategories([...selectedCategories, categorySlug]);\r\n } else {\r\n setSelectedCategories(\r\n selectedCategories.filter((c) => c !== categorySlug)\r\n );\r\n }\r\n };\r\n\r\n const handleTagChange = (tag: string, checked: boolean) => {\r\n if (checked) {\r\n setSelectedTags([...selectedTags, tag]);\r\n } else {\r\n setSelectedTags(selectedTags.filter((t) => t !== tag));\r\n }\r\n };\r\n\r\n const allTags = Array.from(new Set(posts.flatMap((post) => post.tags)));\r\n\r\n const clearFilters = () => {\r\n setSearchTerm(\"\");\r\n setSelectedCategories([]);\r\n setSelectedTags([]);\r\n setSortBy(\"newest\");\r\n };\r\n\r\n const filterProps: FilterSectionProps = {\r\n t,\r\n searchTerm,\r\n setSearchTerm,\r\n categories,\r\n selectedCategories,\r\n handleCategoryChange,\r\n allTags,\r\n selectedTags,\r\n handleTagChange,\r\n clearFilters,\r\n };\r\n\r\n if (loading) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"animate-pulse space-y-4\">\r\n {Array.from({ length: 6 }).map((_, i) => (\r\n <div key={i} className=\"h-48 bg-muted rounded-lg\"></div>\r\n ))}\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n if (error) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8 text-center\">\r\n <p className=\"text-destructive\">{t(\"error\")}</p>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <FadeIn className=\"flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 mb-8\">\r\n <div>\r\n <h1 className=\"text-3xl font-bold mb-2\">{t(\"title\")}</h1>\r\n <p className=\"text-muted-foreground\">{t(\"subtitle\")}</p>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-4\">\r\n <Select value={sortBy} onValueChange={setSortBy}>\r\n <SelectTrigger className=\"w-[180px]\">\r\n <SelectValue />\r\n </SelectTrigger>\r\n <SelectContent>\r\n <SelectItem value=\"newest\">{t(\"sortNewest\")}</SelectItem>\r\n <SelectItem value=\"oldest\">{t(\"sortOldest\")}</SelectItem>\r\n <SelectItem value=\"popular\">{t(\"sortPopular\")}</SelectItem>\r\n <SelectItem value=\"reading-time\">\r\n {t(\"sortReadingTime\")}\r\n </SelectItem>\r\n </SelectContent>\r\n </Select>\r\n\r\n <Sheet>\r\n <SheetTrigger asChild>\r\n <Button variant=\"outline\" size=\"sm\" className=\"lg:hidden\">\r\n <Filter className=\"h-4 w-4 mr-2\" />\r\n {t(\"filters\")}\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent>\r\n <SheetHeader>\r\n <SheetTitle>{t(\"filters\")}</SheetTitle>\r\n <SheetDescription>{t(\"filterDescription\")}</SheetDescription>\r\n </SheetHeader>\r\n <div className=\"mt-6\">\r\n <FilterSection {...filterProps} />\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n </div>\r\n </FadeIn>\r\n\r\n <div className=\"flex flex-col lg:flex-row gap-8\">\r\n <div className=\"hidden lg:block w-64 flex-shrink-0\">\r\n <div className=\"sticky top-4\">\r\n <FilterSection {...filterProps} />\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex-1\">\r\n <div className=\"flex items-center justify-between mb-6\">\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"showing\")} {sortedPosts.length} {t(\"of\")} {posts.length}{\" \"}\r\n {t(\"posts\")}\r\n {searchTerm && (\r\n <span className=\"ml-1\">\r\n {t(\"for\")} \"<strong>{searchTerm}</strong>\"\r\n </span>\r\n )}\r\n </p>\r\n </div>\r\n\r\n {sortedPosts.length > 0 ? (\r\n <div\r\n className={`grid gap-6 ${\r\n viewMode === \"grid\"\r\n ? \"grid-cols-1 md:grid-cols-2 xl:grid-cols-3\"\r\n : \"grid-cols-1\"\r\n }`}\r\n >\r\n {sortedPosts.map((post) => (\r\n <div key={post.id} className=\"contents\" data-db-table=\"posts\" data-db-id={post.id}>\r\n <PostCard post={post} layout={viewMode} />\r\n </div>\r\n ))}\r\n </div>\r\n ) : (\r\n <div className=\"text-center py-12\">\r\n <p className=\"text-muted-foreground mb-4\">\r\n {t(\"noPostsFound\")}\r\n </p>\r\n <Button onClick={clearFilters} variant=\"outline\">\r\n {t(\"clearFilters\")}\r\n </Button>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default BlogListPage;\r\n"
28
28
  },
29
29
  {
30
30
  "path": "blog-list-page/lang/en.json",
@@ -20,7 +20,7 @@
20
20
  "path": "cart-drawer/cart-drawer.tsx",
21
21
  "type": "registry:component",
22
22
  "target": "$modules$/cart-drawer/cart-drawer.tsx",
23
- "content": "import { Link } from \"react-router\";\nimport { Minus, Plus } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Sheet,\n SheetContent,\n SheetHeader,\n SheetTitle,\n} from \"@/components/ui/sheet\";\nimport { useTranslation } from \"react-i18next\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport constants from \"@/constants/constants.json\";\n\ninterface CartDrawerProps {\n checkoutHref?: string;\n className?: string;\n showTrigger?: boolean;\n}\n\nexport function CartDrawer({\n checkoutHref = \"/checkout\",\n className,\n showTrigger = true,\n}: CartDrawerProps) {\n const { t } = useTranslation(\"cart-drawer\");\n const {\n state,\n removeItem,\n updateQuantity,\n isDrawerOpen,\n setDrawerOpen,\n } = useCart();\n const { items, total } = state;\n const currency = (constants.site as any).currency || \"USD\";\n\n const getProductPrice = (product: {\n price: number;\n sale_price?: number;\n on_sale?: boolean;\n }) => {\n return product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n };\n\n const handleQuantityChange = (id: string | number, newQuantity: number) => {\n if (newQuantity <= 0) {\n removeItem(id);\n } else {\n updateQuantity(id, newQuantity);\n }\n };\n\n return (\n <Sheet open={isDrawerOpen} onOpenChange={setDrawerOpen}>\n <SheetContent className=\"w-full sm:max-w-md flex flex-col px-6 pb-8\">\n <SheetHeader>\n <SheetTitle>{t(\"title\", \"Shopping cart\")}</SheetTitle>\n </SheetHeader>\n\n <div className=\"flex-1 overflow-y-auto mt-8\">\n {items.length === 0 ? (\n <p className=\"text-center text-muted-foreground py-8\">\n {t(\"empty\", \"Your cart is empty\")}\n </p>\n ) : (\n <ul className=\"-my-6 divide-y divide-border\">\n {items.map((item) => (\n <li key={item.id} className=\"flex py-6\">\n <div className=\"size-24 shrink-0 overflow-hidden rounded-md border border-border\">\n <img\n alt={item.product.name}\n src={item.product.images[0] || \"/images/placeholder.png\"}\n className=\"size-full object-cover\"\n />\n </div>\n\n <div className=\"ml-4 flex flex-1 flex-col\">\n <div>\n <div className=\"flex justify-between text-base font-medium\">\n <h3>\n <Link\n to={`/products/${item.product.slug}`}\n onClick={() => setDrawerOpen(false)}\n >\n {item.product.name}\n </Link>\n </h3>\n <p className=\"ml-4\">\n {formatPrice(getProductPrice(item.product), currency)}\n </p>\n </div>\n {item.product.category_name && (\n <p className=\"mt-1 text-sm text-muted-foreground\">\n {item.product.category_name}\n </p>\n )}\n </div>\n <div className=\"flex flex-1 items-end justify-between text-sm\">\n <div className=\"flex items-center gap-1\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-6 w-6\"\n onClick={() =>\n handleQuantityChange(item.id, item.quantity - 1)\n }\n >\n <Minus className=\"h-3 w-3\" />\n </Button>\n <span className=\"w-8 text-center text-sm\">\n {item.quantity}\n </span>\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-6 w-6\"\n onClick={() =>\n handleQuantityChange(item.id, item.quantity + 1)\n }\n >\n <Plus className=\"h-3 w-3\" />\n </Button>\n </div>\n\n <button\n type=\"button\"\n onClick={() => removeItem(item.id)}\n className=\"font-medium text-primary hover:text-primary/80\"\n >\n {t(\"remove\", \"Remove\")}\n </button>\n </div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n\n <div className=\"border-t border-border pt-6 mt-6\">\n <div className=\"flex justify-between text-base font-medium\">\n <p>{t(\"subtotal\", \"Subtotal\")}</p>\n <p>{formatPrice(total, currency)}</p>\n </div>\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\n {t(\"shippingNote\", \"Shipping and taxes calculated at checkout.\")}\n </p>\n <div className=\"mt-6\">\n <Button asChild className=\"w-full\" disabled={items.length === 0}>\n <Link to={checkoutHref} onClick={() => setDrawerOpen(false)}>\n {t(\"checkout\", \"Checkout\")}\n </Link>\n </Button>\n </div>\n </div>\n </SheetContent>\n </Sheet>\n );\n}\n"
23
+ "content": "import { Link } from \"react-router\";\r\nimport { Minus, Plus } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Sheet,\r\n SheetContent,\r\n SheetHeader,\r\n SheetTitle,\r\n} from \"@/components/ui/sheet\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\ninterface CartDrawerProps {\r\n checkoutHref?: string;\r\n className?: string;\r\n showTrigger?: boolean;\r\n}\r\n\r\nexport function CartDrawer({\r\n checkoutHref = \"/checkout\",\r\n className,\r\n showTrigger = true,\r\n}: CartDrawerProps) {\r\n const { t } = useTranslation(\"cart-drawer\");\r\n const {\r\n state,\r\n removeItem,\r\n updateQuantity,\r\n isDrawerOpen,\r\n setDrawerOpen,\r\n } = useCart();\r\n const { items, total } = state;\r\n const currency = (constants.site as any).currency || \"USD\";\r\n\r\n const getProductPrice = (product: {\r\n price: number;\r\n sale_price?: number;\r\n on_sale?: boolean;\r\n }) => {\r\n return product.on_sale && product.sale_price\r\n ? product.sale_price\r\n : product.price;\r\n };\r\n\r\n const handleQuantityChange = (id: string | number, newQuantity: number) => {\r\n if (newQuantity <= 0) {\r\n removeItem(id);\r\n } else {\r\n updateQuantity(id, newQuantity);\r\n }\r\n };\r\n\r\n return (\r\n <Sheet open={isDrawerOpen} onOpenChange={setDrawerOpen}>\r\n <SheetContent className=\"w-full sm:max-w-md flex flex-col px-6 pb-8\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"title\", \"Shopping cart\")}</SheetTitle>\r\n </SheetHeader>\r\n\r\n <div className=\"flex-1 overflow-y-auto mt-8\">\r\n {items.length === 0 ? (\r\n <p className=\"text-center text-muted-foreground py-8\">\r\n {t(\"empty\", \"Your cart is empty\")}\r\n </p>\r\n ) : (\r\n <ul className=\"-my-6 divide-y divide-border\">\r\n {items.map((item) => (\r\n <li key={item.id} className=\"flex py-6\">\r\n <div className=\"size-24 shrink-0 overflow-hidden rounded-md border border-border\">\r\n <img\r\n alt={item.product.name}\r\n src={item.product.images[0] || \"/images/placeholder.png\"}\r\n className=\"size-full object-cover\"\r\n />\r\n </div>\r\n\r\n <div className=\"ml-4 flex flex-1 flex-col\">\r\n <div>\r\n <div className=\"flex justify-between text-base font-medium\">\r\n <h3>\r\n <Link\r\n to={`/products/${item.product.slug}`}\r\n onClick={() => setDrawerOpen(false)}\r\n >\r\n {item.product.name}\r\n </Link>\r\n </h3>\r\n <p className=\"ml-4\">\r\n {formatPrice(getProductPrice(item.product), currency)}\r\n </p>\r\n </div>\r\n {item.product.category_name && (\r\n <p className=\"mt-1 text-sm text-muted-foreground\">\r\n {item.product.category_name}\r\n </p>\r\n )}\r\n </div>\r\n <div className=\"flex flex-1 items-end justify-between text-sm\">\r\n <div className=\"flex items-center gap-1\">\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-6 w-6\"\r\n onClick={() =>\r\n handleQuantityChange(item.id, item.quantity - 1)\r\n }\r\n >\r\n <Minus className=\"h-3 w-3\" />\r\n </Button>\r\n <span className=\"w-8 text-center text-sm\">\r\n {item.quantity}\r\n </span>\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-6 w-6\"\r\n onClick={() =>\r\n handleQuantityChange(item.id, item.quantity + 1)\r\n }\r\n >\r\n <Plus className=\"h-3 w-3\" />\r\n </Button>\r\n </div>\r\n\r\n <button\r\n type=\"button\"\r\n onClick={() => removeItem(item.id)}\r\n className=\"font-medium text-primary hover:text-primary/80\"\r\n >\r\n {t(\"remove\", \"Remove\")}\r\n </button>\r\n </div>\r\n </div>\r\n </li>\r\n ))}\r\n </ul>\r\n )}\r\n </div>\r\n\r\n <div className=\"border-t border-border pt-6 mt-6\">\r\n <div className=\"flex justify-between text-base font-medium\">\r\n <p>{t(\"subtotal\", \"Subtotal\")}</p>\r\n <p>{formatPrice(total, currency)}</p>\r\n </div>\r\n <p className=\"mt-0.5 text-sm text-muted-foreground\">\r\n {t(\"shippingNote\", \"Shipping and taxes calculated at checkout.\")}\r\n </p>\r\n <div className=\"mt-6\">\r\n <Button asChild className=\"w-full\" disabled={items.length === 0}>\r\n <Link to={checkoutHref} onClick={() => setDrawerOpen(false)}>\r\n {t(\"checkout\", \"Checkout\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n );\r\n}\r\n"
24
24
  },
25
25
  {
26
26
  "path": "cart-drawer/lang/en.json",
@@ -23,7 +23,7 @@
23
23
  "path": "cart-page/cart-page.tsx",
24
24
  "type": "registry:page",
25
25
  "target": "$modules$/cart-page/cart-page.tsx",
26
- "content": "import { Link } from \"react-router\";\nimport { Trash2, Plus, Minus, ArrowLeft, ShoppingBag } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Input } from \"@/components/ui/input\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn } from \"@/modules/animations\";\n\nexport function CartPage() {\n const { t } = useTranslation(\"cart-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Shopping Cart\") });\n const { state, removeItem, updateQuantity } = useCart();\n const { items, total } = state;\n\n const currency = constants.site.currency || \"USD\";\n const shipping = 0;\n const tax = 0;\n const freeShippingThreshold = 100;\n\n const getProductPrice = (product: { price: number; sale_price?: number; on_sale?: boolean }) => {\n return product.on_sale && product.sale_price ? product.sale_price : product.price;\n };\n\n const handleQuantityChange = (productId: number | string, newQuantity: number) => {\n if (newQuantity <= 0) {\n removeItem(productId);\n } else {\n updateQuantity(productId, newQuantity);\n }\n };\n\n const handleQuantityInputChange = (productId: number | string, value: string) => {\n const quantity = parseInt(value) || 1;\n handleQuantityChange(productId, quantity);\n };\n\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\n const finalTotal = total + shipping + tax;\n\n if (items.length === 0) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"max-w-2xl mx-auto text-center py-16\">\n <div className=\"mb-8\">\n <ShoppingBag className=\"h-24 w-24 mx-auto text-muted-foreground mb-4\" />\n <h1 className=\"text-3xl font-bold mb-4\">\n {t(\"empty\", \"Your Cart is Empty\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\"emptyDescription\", \"Looks like you haven't added any items to your cart yet.\")}\n </p>\n <Button asChild size=\"lg\">\n <Link to=\"/products\">\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"flex items-center gap-4 mb-8\">\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <Link to=\"/products\">\n <ArrowLeft className=\"h-4 w-4\" />\n </Link>\n </Button>\n <div>\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Shopping Cart\")}</h1>\n <p className=\"text-muted-foreground\">\n {itemCount} {t(\"itemsInCart\", \"items in your cart\")}\n </p>\n </div>\n </FadeIn>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n <div className=\"lg:col-span-2 space-y-4\">\n {items.map((item) => (\n <Card key={item.id}>\n <CardContent className=\"p-6\">\n <div className=\"flex gap-4\">\n <div className=\"w-24 h-24 flex-shrink-0\">\n <img\n src={item.product.images[0] || \"/images/placeholder.png\"}\n alt={item.product.name}\n className=\"w-full h-full object-cover rounded-lg\"\n />\n </div>\n\n <div className=\"flex-1 space-y-2\">\n <div className=\"flex items-start justify-between\">\n <div>\n <h3 className=\"font-semibold\">{item.product.name}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {item.product.category_name ||\n item.product.categories?.[0]?.name ||\n item.product.category}\n </p>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeItem(item.product.id)}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => handleQuantityChange(item.product.id, item.quantity - 1)}\n >\n <Minus className=\"h-3 w-3\" />\n </Button>\n <Input\n type=\"number\"\n value={item.quantity}\n onChange={(e) => handleQuantityInputChange(item.product.id, e.target.value)}\n className=\"w-16 text-center\"\n min=\"1\"\n />\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => handleQuantityChange(item.product.id, item.quantity + 1)}\n >\n <Plus className=\"h-3 w-3\" />\n </Button>\n </div>\n\n <div className=\"text-right\">\n <p className=\"font-semibold\">\n {formatPrice(getProductPrice(item.product) * item.quantity, currency)}\n </p>\n {item.quantity > 1 && (\n <p className=\"text-sm text-muted-foreground\">\n {formatPrice(getProductPrice(item.product), currency)} {t(\"each\", \"each\")}\n </p>\n )}\n </div>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"flex justify-between\">\n <span>\n {t(\"subtotal\", \"Subtotal\")} ({itemCount} {t(\"items\", \"items\")})\n </span>\n <span>{formatPrice(total, currency)}</span>\n </div>\n\n <div className=\"flex justify-between\">\n <span>{t(\"shipping\", \"Shipping\")}</span>\n <span>\n {shipping === 0 ? t(\"free\", \"Free\") : formatPrice(shipping, currency)}\n </span>\n </div>\n\n <div className=\"flex justify-between\">\n <span>{t(\"tax\", \"Tax\")}</span>\n <span>{formatPrice(tax, currency)}</span>\n </div>\n\n <Separator />\n\n <div className=\"flex justify-between text-lg font-semibold\">\n <span>{t(\"total\", \"Total\")}</span>\n <span>{formatPrice(finalTotal, currency)}</span>\n </div>\n\n {shipping > 0 && freeShippingThreshold && freeShippingThreshold > total && (\n <div className=\"text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg\">\n {t(\"freeShippingMessage\", \"Add {{amount}} more for free shipping!\").replace(\n \"{{amount}}\",\n formatPrice(freeShippingThreshold - total, currency)\n )}\n </div>\n )}\n\n <Button asChild className=\"w-full\" size=\"lg\">\n <Link to=\"/checkout\">{t(\"proceedToCheckout\", \"Proceed to Checkout\")}</Link>\n </Button>\n\n <Button variant=\"outline\" asChild className=\"w-full\">\n <Link to=\"/products\">{t(\"continueShopping\", \"Continue Shopping\")}</Link>\n </Button>\n </CardContent>\n </Card>\n\n <Card>\n <CardContent className=\"p-4\">\n <div className=\"text-center space-y-2\">\n <div className=\"text-sm text-muted-foreground\">\n {t(\"secureCheckout\", \"Secure Checkout\")}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t(\"secureCheckoutDescription\", \"Your payment information is encrypted and secure\")}\n </p>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default CartPage;\n"
26
+ "content": "import { Link } from \"react-router\";\r\nimport { Trash2, Plus, Minus, ArrowLeft, ShoppingBag } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FadeIn } from \"@/modules/animations\";\r\n\r\nexport function CartPage() {\r\n const { t } = useTranslation(\"cart-page\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Shopping Cart\") });\r\n const { state, removeItem, updateQuantity } = useCart();\r\n const { items, total } = state;\r\n\r\n const currency = constants.site.currency || \"USD\";\r\n const shipping = 0;\r\n const tax = 0;\r\n const freeShippingThreshold = 100;\r\n\r\n const getProductPrice = (product: { price: number; sale_price?: number; on_sale?: boolean }) => {\r\n return product.on_sale && product.sale_price ? product.sale_price : product.price;\r\n };\r\n\r\n const handleQuantityChange = (productId: number | string, newQuantity: number) => {\r\n if (newQuantity <= 0) {\r\n removeItem(productId);\r\n } else {\r\n updateQuantity(productId, newQuantity);\r\n }\r\n };\r\n\r\n const handleQuantityInputChange = (productId: number | string, value: string) => {\r\n const quantity = parseInt(value) || 1;\r\n handleQuantityChange(productId, quantity);\r\n };\r\n\r\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\r\n const finalTotal = total + shipping + tax;\r\n\r\n if (items.length === 0) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"max-w-2xl mx-auto text-center py-16\">\r\n <div className=\"mb-8\">\r\n <ShoppingBag className=\"h-24 w-24 mx-auto text-muted-foreground mb-4\" />\r\n <h1 className=\"text-3xl font-bold mb-4\">\r\n {t(\"empty\", \"Your Cart is Empty\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-8\">\r\n {t(\"emptyDescription\", \"Looks like you haven't added any items to your cart yet.\")}\r\n </p>\r\n <Button asChild size=\"lg\">\r\n <Link to=\"/products\">\r\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <FadeIn className=\"flex items-center gap-4 mb-8\">\r\n <Button variant=\"ghost\" size=\"icon\" asChild>\r\n <Link to=\"/products\">\r\n <ArrowLeft className=\"h-4 w-4\" />\r\n </Link>\r\n </Button>\r\n <div>\r\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Shopping Cart\")}</h1>\r\n <p className=\"text-muted-foreground\">\r\n {itemCount} {t(\"itemsInCart\", \"items in your cart\")}\r\n </p>\r\n </div>\r\n </FadeIn>\r\n\r\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\r\n <div className=\"lg:col-span-2 space-y-4\">\r\n {items.map((item) => (\r\n <Card key={item.id}>\r\n <CardContent className=\"p-6\">\r\n <div className=\"flex gap-4\">\r\n <div className=\"w-24 h-24 flex-shrink-0\">\r\n <img\r\n src={item.product.images[0] || \"/images/placeholder.png\"}\r\n alt={item.product.name}\r\n className=\"w-full h-full object-cover rounded-lg\"\r\n />\r\n </div>\r\n\r\n <div className=\"flex-1 space-y-2\">\r\n <div className=\"flex items-start justify-between\">\r\n <div>\r\n <h3 className=\"font-semibold\">{item.product.name}</h3>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {item.product.category_name ||\r\n item.product.categories?.[0]?.name ||\r\n item.product.category}\r\n </p>\r\n </div>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n onClick={() => removeItem(item.product.id)}\r\n className=\"text-destructive hover:text-destructive\"\r\n >\r\n <Trash2 className=\"h-4 w-4\" />\r\n </Button>\r\n </div>\r\n\r\n <div className=\"flex items-center justify-between\">\r\n <div className=\"flex items-center gap-2\">\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-8 w-8\"\r\n onClick={() => handleQuantityChange(item.product.id, item.quantity - 1)}\r\n >\r\n <Minus className=\"h-3 w-3\" />\r\n </Button>\r\n <Input\r\n type=\"number\"\r\n value={item.quantity}\r\n onChange={(e) => handleQuantityInputChange(item.product.id, e.target.value)}\r\n className=\"w-16 text-center\"\r\n min=\"1\"\r\n />\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-8 w-8\"\r\n onClick={() => handleQuantityChange(item.product.id, item.quantity + 1)}\r\n >\r\n <Plus className=\"h-3 w-3\" />\r\n </Button>\r\n </div>\r\n\r\n <div className=\"text-right\">\r\n <p className=\"font-semibold\">\r\n {formatPrice(getProductPrice(item.product) * item.quantity, currency)}\r\n </p>\r\n {item.quantity > 1 && (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {formatPrice(getProductPrice(item.product), currency)} {t(\"each\", \"each\")}\r\n </p>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n ))}\r\n </div>\r\n\r\n <div className=\"space-y-6\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"flex justify-between\">\r\n <span>\r\n {t(\"subtotal\", \"Subtotal\")} ({itemCount} {t(\"items\", \"items\")})\r\n </span>\r\n <span>{formatPrice(total, currency)}</span>\r\n </div>\r\n\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"shipping\", \"Shipping\")}</span>\r\n <span>\r\n {shipping === 0 ? t(\"free\", \"Free\") : formatPrice(shipping, currency)}\r\n </span>\r\n </div>\r\n\r\n <div className=\"flex justify-between\">\r\n <span>{t(\"tax\", \"Tax\")}</span>\r\n <span>{formatPrice(tax, currency)}</span>\r\n </div>\r\n\r\n <Separator />\r\n\r\n <div className=\"flex justify-between text-lg font-semibold\">\r\n <span>{t(\"total\", \"Total\")}</span>\r\n <span>{formatPrice(finalTotal, currency)}</span>\r\n </div>\r\n\r\n {shipping > 0 && freeShippingThreshold && freeShippingThreshold > total && (\r\n <div className=\"text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg\">\r\n {t(\"freeShippingMessage\", \"Add {{amount}} more for free shipping!\").replace(\r\n \"{{amount}}\",\r\n formatPrice(freeShippingThreshold - total, currency)\r\n )}\r\n </div>\r\n )}\r\n\r\n <Button asChild className=\"w-full\" size=\"lg\">\r\n <Link to=\"/checkout\">{t(\"proceedToCheckout\", \"Proceed to Checkout\")}</Link>\r\n </Button>\r\n\r\n <Button variant=\"outline\" asChild className=\"w-full\">\r\n <Link to=\"/products\">{t(\"continueShopping\", \"Continue Shopping\")}</Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n\r\n <Card>\r\n <CardContent className=\"p-4\">\r\n <div className=\"text-center space-y-2\">\r\n <div className=\"text-sm text-muted-foreground\">\r\n {t(\"secureCheckout\", \"Secure Checkout\")}\r\n </div>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {t(\"secureCheckoutDescription\", \"Your payment information is encrypted and secure\")}\r\n </p>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default CartPage;\r\n"
27
27
  },
28
28
  {
29
29
  "path": "cart-page/lang/en.json",
@@ -16,7 +16,7 @@
16
16
  "path": "category-section/category-section.tsx",
17
17
  "type": "registry:component",
18
18
  "target": "$modules$/category-section/category-section.tsx",
19
- "content": "import { Link } from \"react-router\";\nimport { ArrowRight } from \"lucide-react\";\nimport { Card } from \"@/components/ui/card\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDbCategories } from \"@/modules/ecommerce-core\";\n\nexport interface CategoryItem {\n id: string | number;\n slug: string;\n name: string;\n description?: string;\n image: string;\n}\n\nexport interface CategorySectionProps {\n categories?: CategoryItem[];\n loading?: boolean;\n}\n\nexport function CategorySection({\n categories: propCategories,\n loading: propLoading,\n}: CategorySectionProps) {\n const { t } = useTranslation(\"category-section\");\n const { categories: hookCategories, loading: hookLoading } = useDbCategories();\n\n const categories = propCategories ?? hookCategories;\n const loading = propLoading ?? hookLoading;\n\n return (\n <section className=\"py-8 sm:py-12 md:py-16 lg:py-20 bg-gradient-to-b from-background to-muted/20 border-t border-border/20\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\n <div className=\"text-center mb-6 sm:mb-8 md:mb-12 lg:mb-16 px-2\">\n <h2 className=\"text-xl sm:text-2xl md:text-3xl lg:text-4xl xl:text-5xl font-bold mb-2 sm:mb-3 md:mb-4 bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent leading-normal pb-1\">\n {t(\"title\", \"Shop by Category\")}\n </h2>\n <div className=\"w-12 sm:w-16 h-1 bg-gradient-to-r from-primary/50 to-primary/20 mx-auto mb-3 sm:mb-4 md:mb-6 rounded-full\"></div>\n <p className=\"text-xs sm:text-sm md:text-base lg:text-lg text-muted-foreground max-w-3xl mx-auto leading-relaxed\">\n {t(\"subtitle\", \"Discover our carefully curated collections\")}\n </p>\n </div>\n\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6 lg:gap-8\">\n {loading\n ? [...Array(4)].map((_, i) => (\n <div key={i} className=\"animate-pulse\">\n <div className=\"aspect-[3/2] bg-muted rounded-xl mb-4\"></div>\n <div className=\"h-5 bg-muted rounded w-3/4 mb-2\"></div>\n <div className=\"h-4 bg-muted rounded w-1/2\"></div>\n </div>\n ))\n : categories.map((category) => (\n <div key={category.id} className=\"contents\" data-db-table=\"product_categories\" data-db-id={category.id}>\n <Link\n to={`/products?category=${category.slug}`}\n className=\"group block\"\n >\n <Card className=\"overflow-hidden border-0 p-0 shadow-lg hover:shadow-2xl transition-all duration-500 group-hover:-translate-y-2 rounded-2xl\">\n <div className=\"aspect-[4/3] relative overflow-hidden\">\n <img\n src={category.image}\n alt={category.name}\n className=\"absolute inset-0 w-full h-full object-cover group-hover:scale-110 transition-transform duration-700\"\n />\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent group-hover:from-black/70 transition-all duration-300\"></div>\n\n <div className=\"absolute bottom-0 left-0 right-0 p-4 sm:p-6\">\n <h3 className=\"text-lg sm:text-xl font-bold text-white mb-1 sm:mb-2 group-hover:text-primary-foreground transition-colors\">\n {category.name}\n </h3>\n {category.description && (\n <p className=\"text-xs sm:text-sm text-white/90 line-clamp-2 group-hover:text-white transition-colors\">\n {category.description}\n </p>\n )}\n\n <div className=\"flex items-center mt-2 sm:mt-3 text-white/80 group-hover:text-white transition-all duration-300 transform group-hover:translate-x-1\">\n <span className=\"text-xs sm:text-sm font-medium mr-2\">\n {t(\"explore\", \"Explore\")}\n </span>\n <ArrowRight className=\"w-3 h-3 sm:w-4 sm:h-4\" />\n </div>\n </div>\n </div>\n </Card>\n </Link>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
19
+ "content": "import { Link } from \"react-router\";\r\nimport { ArrowRight } from \"lucide-react\";\r\nimport { Card } from \"@/components/ui/card\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useDbCategories } from \"@/modules/ecommerce-core\";\r\n\r\nexport interface CategoryItem {\r\n id: string | number;\r\n slug: string;\r\n name: string;\r\n description?: string;\r\n image: string;\r\n}\r\n\r\nexport interface CategorySectionProps {\r\n categories?: CategoryItem[];\r\n loading?: boolean;\r\n}\r\n\r\nexport function CategorySection({\r\n categories: propCategories,\r\n loading: propLoading,\r\n}: CategorySectionProps) {\r\n const { t } = useTranslation(\"category-section\");\r\n const { categories: hookCategories, loading: hookLoading } = useDbCategories();\r\n\r\n const categories = propCategories ?? hookCategories;\r\n const loading = propLoading ?? hookLoading;\r\n\r\n return (\r\n <section className=\"py-8 sm:py-12 md:py-16 lg:py-20 bg-gradient-to-b from-background to-muted/20 border-t border-border/20\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\r\n <div className=\"text-center mb-6 sm:mb-8 md:mb-12 lg:mb-16 px-2\">\r\n <h2 className=\"text-xl sm:text-2xl md:text-3xl lg:text-4xl xl:text-5xl font-bold mb-2 sm:mb-3 md:mb-4 bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent leading-normal pb-1\">\r\n {t(\"title\", \"Shop by Category\")}\r\n </h2>\r\n <div className=\"w-12 sm:w-16 h-1 bg-gradient-to-r from-primary/50 to-primary/20 mx-auto mb-3 sm:mb-4 md:mb-6 rounded-full\"></div>\r\n <p className=\"text-xs sm:text-sm md:text-base lg:text-lg text-muted-foreground max-w-3xl mx-auto leading-relaxed\">\r\n {t(\"subtitle\", \"Discover our carefully curated collections\")}\r\n </p>\r\n </div>\r\n\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-6 lg:gap-8\">\r\n {loading\r\n ? [...Array(4)].map((_, i) => (\r\n <div key={i} className=\"animate-pulse\">\r\n <div className=\"aspect-[3/2] bg-muted rounded-xl mb-4\"></div>\r\n <div className=\"h-5 bg-muted rounded w-3/4 mb-2\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/2\"></div>\r\n </div>\r\n ))\r\n : categories.map((category) => (\r\n <div key={category.id} className=\"contents\" data-db-table=\"product_categories\" data-db-id={category.id}>\r\n <Link\r\n to={`/products?category=${category.slug}`}\r\n className=\"group block\"\r\n >\r\n <Card className=\"overflow-hidden border-0 p-0 shadow-lg hover:shadow-2xl transition-all duration-500 group-hover:-translate-y-2 rounded-2xl\">\r\n <div className=\"aspect-[4/3] relative overflow-hidden\">\r\n <img\r\n src={category.image}\r\n alt={category.name}\r\n className=\"absolute inset-0 w-full h-full object-cover group-hover:scale-110 transition-transform duration-700\"\r\n />\r\n <div className=\"absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent group-hover:from-black/70 transition-all duration-300\"></div>\r\n\r\n <div className=\"absolute bottom-0 left-0 right-0 p-4 sm:p-6\">\r\n <h3 className=\"text-lg sm:text-xl font-bold text-white mb-1 sm:mb-2 group-hover:text-primary-foreground transition-colors\">\r\n {category.name}\r\n </h3>\r\n {category.description && (\r\n <p className=\"text-xs sm:text-sm text-white/90 line-clamp-2 group-hover:text-white transition-colors\">\r\n {category.description}\r\n </p>\r\n )}\r\n\r\n <div className=\"flex items-center mt-2 sm:mt-3 text-white/80 group-hover:text-white transition-all duration-300 transform group-hover:translate-x-1\">\r\n <span className=\"text-xs sm:text-sm font-medium mr-2\">\r\n {t(\"explore\", \"Explore\")}\r\n </span>\r\n <ArrowRight className=\"w-3 h-3 sm:w-4 sm:h-4\" />\r\n </div>\r\n </div>\r\n </div>\r\n </Card>\r\n </Link>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
20
20
  },
21
21
  {
22
22
  "path": "category-section/lang/en.json",
@@ -25,19 +25,19 @@
25
25
  "path": "contact-page-centered/contact-page-centered.tsx",
26
26
  "type": "registry:component",
27
27
  "target": "$modules$/contact-page-centered/contact-page-centered.tsx",
28
- "content": "import React, { useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Mail, Phone, MapPin } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport constants from \"@/constants/constants.json\";\r\n\r\ninterface ContactPageCenteredProps {\r\n className?: string;\r\n}\r\n\r\nexport function ContactPageCentered({ className }: ContactPageCenteredProps) {\r\n const { t } = useTranslation(\"contact-page-centered\");\r\n usePageTitle({ title: t(\"title\", \"Contact Us\") });\r\n const apiService = useApiService();\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\"idle\" | \"success\" | \"error\">(\"idle\");\r\n\r\n const contactCards = [\r\n {\r\n icon: Mail,\r\n title: t(\"emailTitle\", \"Email\"),\r\n value: constants.email || \"hello@example.com\",\r\n href: `mailto:${constants.email || \"hello@example.com\"}`,\r\n },\r\n {\r\n icon: Phone,\r\n title: t(\"phoneTitle\", \"Phone\"),\r\n value: constants.phone || \"+1 234 567 890\",\r\n href: `tel:${constants.phone || \"+1234567890\"}`,\r\n },\r\n {\r\n icon: MapPin,\r\n title: t(\"addressTitle\", \"Address\"),\r\n value: constants.address?.city || \"New York, USA\",\r\n href: \"#\",\r\n },\r\n ];\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n\r\n try {\r\n await apiService.submitForm(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"message\", required: true },\r\n ],\r\n },\r\n constants.site.defaultLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({ name: \"\", email: \"\", message: \"\" });\r\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n } catch {\r\n setSubmitStatus(\"error\");\r\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {\r\n setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));\r\n };\r\n\r\n return (\r\n <Layout>\r\n <div className={cn(\"min-h-screen bg-muted/30 py-16 md:py-24\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 max-w-4xl\">\r\n {/* Header */}\r\n <div className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold mb-4\">{t(\"title\", \"Contact Us\")}</h1>\r\n <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto\">\r\n {t(\"subtitle\", \"We'd love to hear from you. Send us a message and we'll respond as soon as possible.\")}\r\n </p>\r\n </div>\r\n\r\n {/* Contact Cards */}\r\n <div className=\"grid sm:grid-cols-3 gap-4 mb-12\">\r\n {contactCards.map((card, index) => (\r\n <Card key={index} className=\"text-center\">\r\n <CardContent className=\"pt-6\">\r\n <div className=\"mx-auto w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-4\">\r\n <card.icon className=\"h-6 w-6 text-primary\" />\r\n </div>\r\n <h3 className=\"font-semibold mb-1\">{card.title}</h3>\r\n <a\r\n href={card.href}\r\n className=\"text-sm text-muted-foreground hover:text-primary transition-colors\"\r\n >\r\n {card.value}\r\n </a>\r\n </CardContent>\r\n </Card>\r\n ))}\r\n </div>\r\n\r\n {/* Form */}\r\n <Card>\r\n <CardContent className=\"pt-6\">\r\n <form onSubmit={handleSubmit} className=\"space-y-6\">\r\n <div className=\"grid sm:grid-cols-2 gap-4\">\r\n <div>\r\n <Label htmlFor=\"name\">{t(\"nameLabel\", \"Name\")} *</Label>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"namePlaceholder\", \"Your name\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n <div>\r\n <Label htmlFor=\"email\">{t(\"emailLabel\", \"Email\")} *</Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\", \"your@email.com\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"message\">{t(\"messageLabel\", \"Message\")} *</Label>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\", \"How can we help you?\")}\r\n required\r\n rows={6}\r\n className=\"mt-1 resize-none\"\r\n />\r\n </div>\r\n\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-4 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm font-medium\">\r\n {t(\"success\", \"Message sent successfully! We'll get back to you soon.\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-4 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm font-medium\">\r\n {t(\"error\", \"Something went wrong. Please try again.\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button type=\"submit\" size=\"lg\" className=\"w-full\" disabled={isSubmitting}>\r\n {isSubmitting ? t(\"sending\", \"Sending...\") : t(\"submit\", \"Send Message\")}\r\n </Button>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageCentered;\r\n"
28
+ "content": "import React, { useRef, useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Mail, Phone, MapPin, Upload } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\n\r\ninterface ContactPageCenteredProps {\r\n className?: string;\r\n}\r\n\r\nexport function ContactPageCentered({ className }: ContactPageCenteredProps) {\r\n const { t } = useTranslation(\"contact-page-centered\");\r\n usePageTitle({ title: t(\"title\", \"Contact Us\") });\r\n const apiService = useApiService();\r\n const fileAcceptTypes = constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\";\r\n const fileMaxFiles = constants.file?.maxFiles || 5;\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\"idle\" | \"success\" | \"error\">(\"idle\");\r\n const fileInputRef = useRef<HTMLInputElement>(null);\r\n\r\n const contactCards = [\r\n {\r\n icon: Mail,\r\n title: t(\"emailTitle\", \"Email\"),\r\n value: constants.email || \"hello@example.com\",\r\n href: `mailto:${constants.email || \"hello@example.com\"}`,\r\n },\r\n {\r\n icon: Phone,\r\n title: t(\"phoneTitle\", \"Phone\"),\r\n value: constants.phone || \"+1 234 567 890\",\r\n href: `tel:${constants.phone || \"+1234567890\"}`,\r\n },\r\n {\r\n icon: MapPin,\r\n title: t(\"addressTitle\", \"Address\"),\r\n value: constants.address?.city || \"New York, USA\",\r\n href: \"#\",\r\n },\r\n ];\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n\r\n const remainingSlots = fileMaxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: fileMaxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n\r\n try {\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n constants.site.defaultLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({ name: \"\", email: \"\", message: \"\", attachments: [] });\r\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n } catch {\r\n setSubmitStatus(\"error\");\r\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {\r\n setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));\r\n };\r\n\r\n return (\r\n <Layout>\r\n <div className={cn(\"min-h-screen bg-muted/30 py-16 md:py-24\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 max-w-4xl\">\r\n {/* Header */}\r\n <div className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold mb-4\">{t(\"title\", \"Contact Us\")}</h1>\r\n <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto\">\r\n {t(\"subtitle\", \"We'd love to hear from you. Send us a message and we'll respond as soon as possible.\")}\r\n </p>\r\n </div>\r\n\r\n {/* Contact Cards */}\r\n <div className=\"grid sm:grid-cols-3 gap-4 mb-12\">\r\n {contactCards.map((card, index) => (\r\n <Card key={index} className=\"text-center\">\r\n <CardContent className=\"pt-6\">\r\n <div className=\"mx-auto w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-4\">\r\n <card.icon className=\"h-6 w-6 text-primary\" />\r\n </div>\r\n <h3 className=\"font-semibold mb-1\">{card.title}</h3>\r\n <a\r\n href={card.href}\r\n className=\"text-sm text-muted-foreground hover:text-primary transition-colors\"\r\n >\r\n {card.value}\r\n </a>\r\n </CardContent>\r\n </Card>\r\n ))}\r\n </div>\r\n\r\n {/* Form */}\r\n <Card>\r\n <CardContent className=\"pt-6\">\r\n <form onSubmit={handleSubmit} className=\"space-y-6\">\r\n <div className=\"grid sm:grid-cols-2 gap-4\">\r\n <div>\r\n <Label htmlFor=\"name\">{t(\"nameLabel\", \"Name\")} *</Label>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"namePlaceholder\", \"Your name\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n <div>\r\n <Label htmlFor=\"email\">{t(\"emailLabel\", \"Email\")} *</Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\", \"your@email.com\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"message\">{t(\"messageLabel\", \"Message\")} *</Label>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\", \"How can we help you?\")}\r\n required\r\n rows={6}\r\n className=\"mt-1 resize-none\"\r\n />\r\n </div>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-4 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm font-medium\">\r\n {t(\"success\", \"Message sent successfully! We'll get back to you soon.\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-4 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm font-medium\">\r\n {t(\"error\", \"Something went wrong. Please try again.\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button type=\"submit\" size=\"lg\" className=\"w-full\" disabled={isSubmitting}>\r\n {isSubmitting ? t(\"sending\", \"Sending...\") : t(\"submit\", \"Send Message\")}\r\n </Button>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageCentered;\r\n"
29
29
  },
30
30
  {
31
31
  "path": "contact-page-centered/lang/en.json",
32
32
  "type": "registry:lang",
33
33
  "target": "$modules$/contact-page-centered/lang/en.json",
34
- "content": "{\r\n \"title\": \"Contact Us\",\r\n \"subtitle\": \"This subtitle appears below your contact page heading. Use it to encourage visitors to reach out, explain your availability, or highlight your commitment to customer service. Customize with Promake to reflect your brand's communication style.\",\r\n \"emailTitle\": \"Email\",\r\n \"phoneTitle\": \"Phone\",\r\n \"addressTitle\": \"Address\",\r\n \"nameLabel\": \"Name\",\r\n \"namePlaceholder\": \"Your name\",\r\n \"emailLabel\": \"Email\",\r\n \"emailPlaceholder\": \"your@email.com\",\r\n \"messageLabel\": \"Message\",\r\n \"messagePlaceholder\": \"How can we help you?\",\r\n \"submit\": \"Send Message\",\r\n \"sending\": \"Sending...\",\r\n \"success\": \"Message sent successfully! We'll get back to you soon.\",\r\n \"error\": \"Something went wrong. Please try again.\"\r\n}\r\n"
34
+ "content": "{\r\n \"title\": \"Contact Us\",\r\n \"subtitle\": \"This subtitle appears below your contact page heading. Use it to encourage visitors to reach out, explain your availability, or highlight your commitment to customer service. Customize with Promake to reflect your brand's communication style.\",\r\n \"emailTitle\": \"Email\",\r\n \"phoneTitle\": \"Phone\",\r\n \"addressTitle\": \"Address\",\r\n \"nameLabel\": \"Name\",\r\n \"namePlaceholder\": \"Your name\",\r\n \"emailLabel\": \"Email\",\r\n \"emailPlaceholder\": \"your@email.com\",\r\n \"messageLabel\": \"Message\",\r\n \"messagePlaceholder\": \"How can we help you?\",\r\n \"submit\": \"Send Message\",\r\n \"sending\": \"Sending...\",\r\n \"success\": \"Message sent successfully! We'll get back to you soon.\",\r\n \"error\": \"Something went wrong. Please try again.\",\r\n \"addFiles\": \"Add Files\",\r\n \"maxFilesReached\": \"Maximum files reached\",\r\n \"maxFilesLimit\": \"You can add up to {{max}} files\"\r\n}\r\n"
35
35
  },
36
36
  {
37
37
  "path": "contact-page-centered/lang/tr.json",
38
38
  "type": "registry:lang",
39
39
  "target": "$modules$/contact-page-centered/lang/tr.json",
40
- "content": "{\r\n \"title\": \"İletişim\",\r\n \"subtitle\": \"Bu alt başlık iletişim sayfası başlığınızın altında görünür. Ziyaretçileri iletişime geçmeye teşvik etmek, müsaitliğinizi açıklamak veya müşteri hizmetlerine olan bağlılığınızı vurgulamak için kullanın. Promake ile markanızın iletişim tarzını yansıtacak şekilde özelleştirin.\",\r\n \"emailTitle\": \"E-posta\",\r\n \"phoneTitle\": \"Telefon\",\r\n \"addressTitle\": \"Adres\",\r\n \"nameLabel\": \"İsim\",\r\n \"namePlaceholder\": \"Adınız\",\r\n \"emailLabel\": \"E-posta\",\r\n \"emailPlaceholder\": \"email@adresiniz.com\",\r\n \"messageLabel\": \"Mesaj\",\r\n \"messagePlaceholder\": \"Size nasıl yardımcı olabiliriz?\",\r\n \"submit\": \"Mesaj Gönder\",\r\n \"sending\": \"Gönderiliyor...\",\r\n \"success\": \"Mesajınız başarıyla gönderildi! En kısa sürede size döneceğiz.\",\r\n \"error\": \"Bir şeyler yanlış gitti. Lütfen tekrar deneyin.\"\r\n}\r\n"
40
+ "content": "{\r\n \"title\": \"İletişim\",\r\n \"subtitle\": \"Bu alt başlık iletişim sayfası başlığınızın altında görünür. Ziyaretçileri iletişime geçmeye teşvik etmek, müsaitliğinizi açıklamak veya müşteri hizmetlerine olan bağlılığınızı vurgulamak için kullanın. Promake ile markanızın iletişim tarzını yansıtacak şekilde özelleştirin.\",\r\n \"emailTitle\": \"E-posta\",\r\n \"phoneTitle\": \"Telefon\",\r\n \"addressTitle\": \"Adres\",\r\n \"nameLabel\": \"İsim\",\r\n \"namePlaceholder\": \"Adınız\",\r\n \"emailLabel\": \"E-posta\",\r\n \"emailPlaceholder\": \"email@adresiniz.com\",\r\n \"messageLabel\": \"Mesaj\",\r\n \"messagePlaceholder\": \"Size nasıl yardımcı olabiliriz?\",\r\n \"submit\": \"Mesaj Gönder\",\r\n \"sending\": \"Gönderiliyor...\",\r\n \"success\": \"Mesajınız başarıyla gönderildi! En kısa sürede size döneceğiz.\",\r\n \"error\": \"Bir şeyler yanlış gitti. Lütfen tekrar deneyin.\",\r\n \"addFiles\": \"Dosya Ekle\",\r\n \"maxFilesReached\": \"Maksimum dosya sayısına ulaşıldı\",\r\n \"maxFilesLimit\": \"En fazla {{max}} kadar dosya ekleyebilirsiniz\"\r\n}\r\n"
41
41
  }
42
42
  ],
43
43
  "exports": {
@@ -30,19 +30,19 @@
30
30
  "path": "contact-page-map-overlay/contact-page-map-overlay.tsx",
31
31
  "type": "registry:component",
32
32
  "target": "$modules$/contact-page-map-overlay/contact-page-map-overlay.tsx",
33
- "content": "import React, { useState, useMemo, useEffect } from \"react\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { Layout } from \"@/components/Layout\";\nimport { useApiService } from \"@/lib/api\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Label } from \"@/components/ui/label\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Mail,\n Phone,\n MapPin,\n Facebook,\n Twitter,\n Instagram,\n Linkedin,\n Send,\n ExternalLink,\n} from \"lucide-react\";\nimport constants from \"@/constants/constants.json\";\nimport { GoogleMap } from \"@/modules/google-map\";\n\nconst socialIcons: Record<string, React.ElementType> = {\n facebook: Facebook,\n twitter: Twitter,\n instagram: Instagram,\n linkedin: Linkedin,\n};\n\nexport function ContactPageMapOverlay() {\n const { t } = useTranslation(\"contact-page-map-overlay\");\n usePageTitle({ title: t(\"title\") });\n\n const apiService = useApiService();\n\n const socialLinks = useMemo(() => {\n const socialMedia = constants.socialMedia as\n | Record<string, string>\n | undefined;\n if (!socialMedia) return [];\n return Object.entries(socialMedia)\n .filter(([platform, url]) => url && socialIcons[platform])\n .map(([platform, url]) => ({\n platform,\n url,\n Icon: socialIcons[platform],\n }));\n }, []);\n\n const [formData, setFormData] = useState({\n name: \"\",\n email: \"\",\n message: \"\",\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [submitStatus, setSubmitStatus] = useState<\n \"idle\" | \"success\" | \"error\"\n >(\"idle\");\n\n // Auto-reset status after 5 seconds with proper cleanup\n useEffect(() => {\n if (submitStatus === \"idle\") return;\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\n return () => clearTimeout(timer);\n }, [submitStatus]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setSubmitStatus(\"idle\");\n\n try {\n const currentLanguage = constants.site.defaultLanguage;\n\n await apiService.submitForm(\n formData,\n {\n email_subject1: \"Thank you for contacting us\",\n email_subject2: \"New Contact Form Submission\",\n fields: [\n { name: \"name\", required: true },\n { name: \"email\", required: true },\n { name: \"message\", required: true },\n ],\n },\n currentLanguage\n );\n\n setSubmitStatus(\"success\");\n setFormData({\n name: \"\",\n email: \"\",\n message: \"\",\n });\n } catch (error: unknown) {\n console.error(\"Form submission failed:\", error);\n setSubmitStatus(\"error\");\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n // Default coordinates (can be customized via constants)\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\n\n return (\n <Layout>\n <div className=\"relative min-h-[calc(100vh-4rem)]\">\n {/* Full-screen Map Background */}\n <div className=\"absolute inset-0\">\n <GoogleMap\n latitude={mapLatitude}\n longitude={mapLongitude}\n zoom={14}\n height=\"100%\"\n className=\"rounded-none border-0 h-full\"\n title={t(\"mapTitle\")}\n />\n {/* Dark overlay for better readability */}\n <div className=\"absolute inset-0 bg-black/30 pointer-events-none\" />\n </div>\n\n {/* Content Overlay */}\n <div className=\"relative z-10 min-h-[calc(100vh-4rem)] flex items-center py-12 px-4\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto\">\n <div className=\"grid lg:grid-cols-2 gap-8 items-stretch\">\n {/* Form Card - Glassmorphism */}\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\n <CardHeader>\n <CardTitle className=\"text-2xl lg:text-3xl\">\n {t(\"title\")}\n </CardTitle>\n <p className=\"text-muted-foreground mt-2\">\n {t(\"description\")}\n </p>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\n {t(\"fullName\")} *\n </Label>\n <Input\n id=\"name\"\n name=\"name\"\n type=\"text\"\n value={formData.name}\n onChange={handleChange}\n placeholder={t(\"fullNamePlaceholder\")}\n required\n className=\"mt-1.5 bg-background/50\"\n />\n </div>\n <div>\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\n {t(\"emailAddress\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1.5 bg-background/50\"\n />\n </div>\n </div>\n\n <div>\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\n {t(\"message\")} *\n </Label>\n <Textarea\n id=\"message\"\n name=\"message\"\n value={formData.message}\n onChange={handleChange}\n placeholder={t(\"messagePlaceholder\")}\n required\n rows={4}\n className=\"mt-1.5 resize-none bg-background/50\"\n />\n </div>\n\n {submitStatus === \"success\" && (\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\n {t(\"success\")}\n </p>\n </div>\n )}\n\n {submitStatus === \"error\" && (\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"sending\")}\n </>\n ) : (\n <>\n <Send className=\"w-4 h-4 mr-2\" />\n {t(\"submit\")}\n </>\n )}\n </Button>\n </form>\n </CardContent>\n </Card>\n\n {/* Contact Info Card - Glassmorphism */}\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\n <CardHeader>\n <CardTitle>{t(\"contactInfo\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <Mail className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"email\")}</p>\n <a\n href={`mailto:${constants.email}`}\n className=\"font-medium hover:text-primary transition-colors\"\n >\n {constants.email}\n </a>\n </div>\n </div>\n\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <Phone className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"phone\")}</p>\n <a\n href={`tel:${constants.phone}`}\n className=\"font-medium hover:text-primary transition-colors\"\n >\n {constants.phone}\n </a>\n </div>\n </div>\n\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <MapPin className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"address\")}</p>\n <p className=\"font-medium\">\n {constants.address.line1}\n <br />\n {constants.address.city}, {constants.address.state} {constants.address.postalCode}\n </p>\n </div>\n </div>\n\n {/* Open in Maps Link */}\n <a\n href={`https://www.google.com/maps?q=${mapLatitude},${mapLongitude}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-2 text-sm text-primary hover:underline mt-2\"\n >\n <ExternalLink className=\"w-4 h-4\" />\n {t(\"openInMaps\")}\n </a>\n\n {/* Social Links */}\n {socialLinks.length > 0 && (\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-muted-foreground mb-3\">\n {t(\"followUs\")}\n </p>\n <div className=\"flex gap-2\">\n {socialLinks.map(({ platform, url, Icon }) => (\n <a\n key={platform}\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"h-10 w-10 flex items-center justify-center rounded-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\n >\n <Icon className=\"h-4 w-4\" />\n </a>\n ))}\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ContactPageMapOverlay;\n"
33
+ "content": "import React, { useState, useMemo, useEffect, useRef } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport {\r\n Mail,\r\n Phone,\r\n MapPin,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Send,\r\n ExternalLink,\r\n Upload,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { GoogleMap } from \"@/modules/google-map\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\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};\r\n\r\nexport function ContactPageMapOverlay() {\r\n const { t } = useTranslation(\"contact-page-map-overlay\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | 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]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n const fileInputRef = useRef<HTMLInputElement>(null);\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const maxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = maxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n // Auto-reset status after 5 seconds with proper cleanup\r\n useEffect(() => {\r\n if (submitStatus === \"idle\") return;\r\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n return () => clearTimeout(timer);\r\n }, [submitStatus]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: []\r\n });\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n // Default coordinates (can be customized via constants)\r\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\r\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\r\n\r\n return (\r\n <Layout>\r\n <div className=\"relative min-h-[calc(100vh-4rem)]\">\r\n {/* Full-screen Map Background */}\r\n <div className=\"absolute inset-0\">\r\n <GoogleMap\r\n latitude={mapLatitude}\r\n longitude={mapLongitude}\r\n zoom={14}\r\n height=\"100%\"\r\n className=\"rounded-none border-0 h-full\"\r\n title={t(\"mapTitle\")}\r\n />\r\n {/* Dark overlay for better readability */}\r\n <div className=\"absolute inset-0 bg-black/30 pointer-events-none\" />\r\n </div>\r\n\r\n {/* Content Overlay */}\r\n <div className=\"relative z-10 min-h-[calc(100vh-4rem)] flex items-center py-12 px-4\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto\">\r\n <div className=\"grid lg:grid-cols-2 gap-8 items-stretch\">\r\n {/* Form Card - Glassmorphism */}\r\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\r\n <CardHeader>\r\n <CardTitle className=\"text-2xl lg:text-3xl\">\r\n {t(\"title\")}\r\n </CardTitle>\r\n <p className=\"text-muted-foreground mt-2\">\r\n {t(\"description\")}\r\n </p>\r\n </CardHeader>\r\n <CardContent>\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\r\n <div>\r\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\r\n {t(\"fullName\")} *\r\n </Label>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1.5 bg-background/50\"\r\n />\r\n </div>\r\n <div>\r\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\r\n {t(\"emailAddress\")} *\r\n </Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1.5 bg-background/50\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\r\n {t(\"message\")} *\r\n </Label>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={4}\r\n className=\"mt-1.5 resize-none bg-background/50\"\r\n />\r\n </div>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n <>\r\n <Send className=\"w-4 h-4 mr-2\" />\r\n {t(\"submit\")}\r\n </>\r\n )}\r\n </Button>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Contact Info Card - Glassmorphism */}\r\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\r\n <CardHeader>\r\n <CardTitle>{t(\"contactInfo\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-5\">\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <Mail className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"email\")}</p>\r\n <a\r\n href={`mailto:${constants.email}`}\r\n className=\"font-medium hover:text-primary transition-colors\"\r\n >\r\n {constants.email}\r\n </a>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <Phone className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"phone\")}</p>\r\n <a\r\n href={`tel:${constants.phone}`}\r\n className=\"font-medium hover:text-primary transition-colors\"\r\n >\r\n {constants.phone}\r\n </a>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"address\")}</p>\r\n <p className=\"font-medium\">\r\n {constants.address.line1}\r\n <br />\r\n {constants.address.city}, {constants.address.state} {constants.address.postalCode}\r\n </p>\r\n </div>\r\n </div>\r\n\r\n {/* Open in Maps Link */}\r\n <a\r\n href={`https://www.google.com/maps?q=${mapLatitude},${mapLongitude}`}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"inline-flex items-center gap-2 text-sm text-primary hover:underline mt-2\"\r\n >\r\n <ExternalLink className=\"w-4 h-4\" />\r\n {t(\"openInMaps\")}\r\n </a>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"pt-4 border-t\">\r\n <p className=\"text-sm text-muted-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-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-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-4 w-4\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageMapOverlay;\r\n"
34
34
  },
35
35
  {
36
36
  "path": "contact-page-map-overlay/lang/en.json",
37
37
  "type": "registry:lang",
38
38
  "target": "$modules$/contact-page-map-overlay/lang/en.json",
39
- "content": "{\n \"title\": \"Contact Us\",\n \"description\": \"We'd love to hear from you. Send us a message and we'll respond as soon as possible.\",\n \"contactInfo\": \"Contact Information\",\n \"email\": \"Email\",\n \"phone\": \"Phone\",\n \"address\": \"Address\",\n \"fullName\": \"Full Name\",\n \"fullNamePlaceholder\": \"Your name\",\n \"emailAddress\": \"Email Address\",\n \"emailPlaceholder\": \"your@email.com\",\n \"message\": \"Message\",\n \"messagePlaceholder\": \"How can we help you?\",\n \"submit\": \"Send Message\",\n \"sending\": \"Sending...\",\n \"success\": \"Message sent successfully! We'll get back to you soon.\",\n \"error\": \"Something went wrong. Please try again.\",\n \"followUs\": \"Follow us\",\n \"mapTitle\": \"Our Location\",\n \"openInMaps\": \"Open in Google Maps\"\n}\n"
39
+ "content": "{\r\n \"title\": \"Contact Us\",\r\n \"description\": \"We'd love to hear from you. Send us a message and we'll respond as soon as possible.\",\r\n \"contactInfo\": \"Contact Information\",\r\n \"email\": \"Email\",\r\n \"phone\": \"Phone\",\r\n \"address\": \"Address\",\r\n \"fullName\": \"Full Name\",\r\n \"fullNamePlaceholder\": \"Your name\",\r\n \"emailAddress\": \"Email Address\",\r\n \"emailPlaceholder\": \"your@email.com\",\r\n \"message\": \"Message\",\r\n \"messagePlaceholder\": \"How can we help you?\",\r\n \"submit\": \"Send Message\",\r\n \"sending\": \"Sending...\",\r\n \"success\": \"Message sent successfully! We'll get back to you soon.\",\r\n \"error\": \"Something went wrong. Please try again.\",\r\n \"followUs\": \"Follow us\",\r\n \"mapTitle\": \"Our Location\",\r\n \"openInMaps\": \"Open in Google Maps\",\r\n \"addFiles\": \"Add Files\",\r\n \"maxFilesReached\": \"Maximum files reached\",\r\n \"maxFilesLimit\": \"You can add up to {{max}} files\"\r\n}\r\n"
40
40
  },
41
41
  {
42
42
  "path": "contact-page-map-overlay/lang/tr.json",
43
43
  "type": "registry:lang",
44
44
  "target": "$modules$/contact-page-map-overlay/lang/tr.json",
45
- "content": "{\n \"title\": \"Bize Ulaşın\",\n \"description\": \"Sizden haber almak isteriz. Bize mesaj gönderin, en kısa sürede yanıtlayacağız.\",\n \"contactInfo\": \"İletişim Bilgileri\",\n \"email\": \"E-posta\",\n \"phone\": \"Telefon\",\n \"address\": \"Adres\",\n \"fullName\": \"Ad Soyad\",\n \"fullNamePlaceholder\": \"Adınız\",\n \"emailAddress\": \"E-posta Adresi\",\n \"emailPlaceholder\": \"email@adresiniz.com\",\n \"message\": \"Mesaj\",\n \"messagePlaceholder\": \"Size nasıl yardımcı olabiliriz?\",\n \"submit\": \"Mesaj Gönder\",\n \"sending\": \"Gönderiliyor...\",\n \"success\": \"Mesaj başarıyla gönderildi! En kısa sürede yanıtlayacağız.\",\n \"error\": \"Bir hata oluştu. Lütfen tekrar deneyin.\",\n \"followUs\": \"Bizi takip edin\",\n \"mapTitle\": \"Konumumuz\",\n \"openInMaps\": \"Google Maps'te Aç\"\n}\n"
45
+ "content": "{\r\n \"title\": \"Bize Ulaşın\",\r\n \"description\": \"Sizden haber almak isteriz. Bize mesaj gönderin, en kısa sürede yanıtlayacağız.\",\r\n \"contactInfo\": \"İletişim Bilgileri\",\r\n \"email\": \"E-posta\",\r\n \"phone\": \"Telefon\",\r\n \"address\": \"Adres\",\r\n \"fullName\": \"Ad Soyad\",\r\n \"fullNamePlaceholder\": \"Adınız\",\r\n \"emailAddress\": \"E-posta Adresi\",\r\n \"emailPlaceholder\": \"email@adresiniz.com\",\r\n \"message\": \"Mesaj\",\r\n \"messagePlaceholder\": \"Size nasıl yardımcı olabiliriz?\",\r\n \"submit\": \"Mesaj Gönder\",\r\n \"sending\": \"Gönderiliyor...\",\r\n \"success\": \"Mesaj başarıyla gönderildi! En kısa sürede yanıtlayacağız.\",\r\n \"error\": \"Bir hata oluştu. Lütfen tekrar deneyin.\",\r\n \"followUs\": \"Bizi takip edin\",\r\n \"mapTitle\": \"Konumumuz\",\r\n \"openInMaps\": \"Google Maps'te Aç\",\r\n \"addFiles\": \"Dosya Ekle\",\r\n \"maxFilesReached\": \"Maksimum dosya sayısına ulaşıldı\",\r\n \"maxFilesLimit\": \"En fazla {{max}} kadar dosya ekleyebilirsiniz\"\r\n}\r\n"
46
46
  }
47
47
  ],
48
48
  "exports": {
@@ -30,19 +30,19 @@
30
30
  "path": "contact-page-map-split/contact-page-map-split.tsx",
31
31
  "type": "registry:component",
32
32
  "target": "$modules$/contact-page-map-split/contact-page-map-split.tsx",
33
- "content": "import React, { useState, useMemo, useEffect } from \"react\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { Layout } from \"@/components/Layout\";\nimport { useApiService } from \"@/lib/api\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n Mail,\n Phone,\n MapPin,\n Facebook,\n Twitter,\n Instagram,\n Linkedin,\n Send,\n} from \"lucide-react\";\nimport constants from \"@/constants/constants.json\";\nimport { GoogleMap } from \"@/modules/google-map\";\n\nconst socialIcons: Record<string, React.ElementType> = {\n facebook: Facebook,\n twitter: Twitter,\n instagram: Instagram,\n linkedin: Linkedin,\n};\n\nexport function ContactPageMapSplit() {\n const { t } = useTranslation(\"contact-page-map-split\");\n usePageTitle({ title: t(\"title\") });\n\n const apiService = useApiService();\n\n const socialLinks = useMemo(() => {\n const socialMedia = constants.socialMedia as\n | Record<string, string>\n | undefined;\n if (!socialMedia) return [];\n return Object.entries(socialMedia)\n .filter(([platform, url]) => url && socialIcons[platform])\n .map(([platform, url]) => ({\n platform,\n url,\n Icon: socialIcons[platform],\n }));\n }, []);\n\n const [formData, setFormData] = useState({\n name: \"\",\n email: \"\",\n phone: \"\",\n message: \"\",\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [submitStatus, setSubmitStatus] = useState<\n \"idle\" | \"success\" | \"error\"\n >(\"idle\");\n\n // Auto-reset status after 5 seconds with proper cleanup\n useEffect(() => {\n if (submitStatus === \"idle\") return;\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\n return () => clearTimeout(timer);\n }, [submitStatus]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setSubmitStatus(\"idle\");\n\n try {\n const currentLanguage = constants.site.defaultLanguage;\n\n await apiService.submitForm(\n formData,\n {\n email_subject1: \"Thank you for contacting us\",\n email_subject2: \"New Contact Form Submission\",\n fields: [\n { name: \"name\", required: true },\n { name: \"email\", required: true },\n { name: \"phone\", required: false },\n { name: \"message\", required: true },\n ],\n },\n currentLanguage\n );\n\n setSubmitStatus(\"success\");\n setFormData({\n name: \"\",\n email: \"\",\n phone: \"\",\n message: \"\",\n });\n } catch (error: unknown) {\n console.error(\"Form submission failed:\", error);\n setSubmitStatus(\"error\");\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n // Default coordinates (can be customized via constants)\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\n\n return (\n <Layout>\n <div className=\"min-h-screen flex flex-col lg:flex-row\">\n {/* Left Side - Form & Info */}\n <div className=\"w-full lg:w-1/2 bg-background py-12 lg:py-16 px-6 lg:px-12 flex flex-col justify-center\">\n <div className=\"max-w-lg mx-auto w-full\">\n {/* Header */}\n <div className=\"mb-10\">\n <h1 className=\"text-3xl lg:text-4xl font-bold text-foreground mb-3\">\n {t(\"title\")}\n </h1>\n <p className=\"text-muted-foreground\">\n {t(\"description\")}\n </p>\n </div>\n\n {/* Contact Info Cards */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4 mb-10\">\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\n <Mail className=\"w-5 h-5 text-primary\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-muted-foreground\">{t(\"email\")}</p>\n <p className=\"text-sm font-medium truncate\">{constants.email}</p>\n </div>\n </div>\n\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\n <Phone className=\"w-5 h-5 text-primary\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-muted-foreground\">{t(\"phone\")}</p>\n <p className=\"text-sm font-medium truncate\">{constants.phone}</p>\n </div>\n </div>\n\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50 sm:col-span-2\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <MapPin className=\"w-5 h-5 text-primary\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-muted-foreground\">{t(\"address\")}</p>\n <p className=\"text-sm font-medium\">\n {constants.address.line1}, {constants.address.city}\n </p>\n </div>\n </div>\n </div>\n\n {/* Contact Form */}\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <div>\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\n {t(\"fullName\")} *\n </Label>\n <Input\n id=\"name\"\n name=\"name\"\n type=\"text\"\n value={formData.name}\n onChange={handleChange}\n placeholder={t(\"fullNamePlaceholder\")}\n required\n className=\"mt-1.5\"\n />\n </div>\n <div>\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\n {t(\"emailAddress\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1.5\"\n />\n </div>\n </div>\n\n <div>\n <Label htmlFor=\"phone\" className=\"text-sm font-medium\">\n {t(\"phoneNumber\")}\n </Label>\n <Input\n id=\"phone\"\n name=\"phone\"\n type=\"tel\"\n value={formData.phone}\n onChange={handleChange}\n placeholder={t(\"phonePlaceholder\")}\n className=\"mt-1.5\"\n />\n </div>\n\n <div>\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\n {t(\"message\")} *\n </Label>\n <Textarea\n id=\"message\"\n name=\"message\"\n value={formData.message}\n onChange={handleChange}\n placeholder={t(\"messagePlaceholder\")}\n required\n rows={4}\n className=\"mt-1.5 resize-none\"\n />\n </div>\n\n {submitStatus === \"success\" && (\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\n {t(\"success\")}\n </p>\n </div>\n )}\n\n {submitStatus === \"error\" && (\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"sending\")}\n </>\n ) : (\n <>\n <Send className=\"w-4 h-4 mr-2\" />\n {t(\"submit\")}\n </>\n )}\n </Button>\n </form>\n\n {/* Social Links */}\n {socialLinks.length > 0 && (\n <div className=\"mt-10 pt-6 border-t\">\n <p className=\"text-sm text-muted-foreground mb-3\">\n {t(\"followUs\")}\n </p>\n <div className=\"flex gap-2\">\n {socialLinks.map(({ platform, url, Icon }) => (\n <a\n key={platform}\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"h-10 w-10 flex items-center justify-center rounded-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\n >\n <Icon className=\"h-4 w-4\" />\n </a>\n ))}\n </div>\n </div>\n )}\n </div>\n </div>\n\n {/* Right Side - Map */}\n <div className=\"w-full lg:w-1/2 h-[400px] lg:h-[calc(100vh-4rem)] relative\">\n <GoogleMap\n latitude={mapLatitude}\n longitude={mapLongitude}\n zoom={14}\n height=\"100%\"\n className=\"rounded-none border-0 h-full\"\n title={t(\"mapTitle\")}\n />\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ContactPageMapSplit;\n"
33
+ "content": "import React, { useState, useMemo, useEffect, useRef } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport {\r\n Mail,\r\n Phone,\r\n MapPin,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Send,\r\n Upload,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { GoogleMap } from \"@/modules/google-map\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\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};\r\n\r\nexport function ContactPageMapSplit() {\r\n const { t } = useTranslation(\"contact-page-map-split\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | 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]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n const fileInputRef = useRef<HTMLInputElement>(null);\r\n\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const maxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = maxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n // Auto-reset status after 5 seconds with proper cleanup\r\n useEffect(() => {\r\n if (submitStatus === \"idle\") return;\r\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n return () => clearTimeout(timer);\r\n }, [submitStatus]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"phone\", required: false },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: []\r\n });\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n // Default coordinates (can be customized via constants)\r\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\r\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\r\n\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen flex flex-col lg:flex-row\">\r\n {/* Left Side - Form & Info */}\r\n <div className=\"w-full lg:w-1/2 bg-background py-12 lg:py-16 px-6 lg:px-12 flex flex-col justify-center\">\r\n <div className=\"max-w-lg mx-auto w-full\">\r\n {/* Header */}\r\n <div className=\"mb-10\">\r\n <h1 className=\"text-3xl lg:text-4xl font-bold text-foreground mb-3\">\r\n {t(\"title\")}\r\n </h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"description\")}\r\n </p>\r\n </div>\r\n\r\n {/* Contact Info Cards */}\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4 mb-10\">\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <Mail className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"email\")}</p>\r\n <p className=\"text-sm font-medium truncate\">{constants.email}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <Phone className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"phone\")}</p>\r\n <p className=\"text-sm font-medium truncate\">{constants.phone}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50 sm:col-span-2\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"address\")}</p>\r\n <p className=\"text-sm font-medium\">\r\n {constants.address.line1}, {constants.address.city}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Contact Form */}\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\r\n <div>\r\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\r\n {t(\"fullName\")} *\r\n </Label>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1.5\"\r\n />\r\n </div>\r\n <div>\r\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\r\n {t(\"emailAddress\")} *\r\n </Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1.5\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"phone\" className=\"text-sm font-medium\">\r\n {t(\"phoneNumber\")}\r\n </Label>\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleChange}\r\n placeholder={t(\"phonePlaceholder\")}\r\n className=\"mt-1.5\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\r\n {t(\"message\")} *\r\n </Label>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={4}\r\n className=\"mt-1.5 resize-none\"\r\n />\r\n </div>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n <>\r\n <Send className=\"w-4 h-4 mr-2\" />\r\n {t(\"submit\")}\r\n </>\r\n )}\r\n </Button>\r\n </form>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"mt-10 pt-6 border-t\">\r\n <p className=\"text-sm text-muted-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-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-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-4 w-4\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Right Side - Map */}\r\n <div className=\"w-full lg:w-1/2 h-[400px] lg:h-[calc(100vh-4rem)] relative\">\r\n <GoogleMap\r\n latitude={mapLatitude}\r\n longitude={mapLongitude}\r\n zoom={14}\r\n height=\"100%\"\r\n className=\"rounded-none border-0 h-full\"\r\n title={t(\"mapTitle\")}\r\n />\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageMapSplit;\r\n"
34
34
  },
35
35
  {
36
36
  "path": "contact-page-map-split/lang/en.json",
37
37
  "type": "registry:lang",
38
38
  "target": "$modules$/contact-page-map-split/lang/en.json",
39
- "content": "{\n \"title\": \"Get in Touch\",\n \"description\": \"Have a question or want to work together? Fill out the form below and we'll get back to you as soon as possible.\",\n \"email\": \"Email\",\n \"phone\": \"Phone\",\n \"address\": \"Address\",\n \"fullName\": \"Full Name\",\n \"fullNamePlaceholder\": \"Your name\",\n \"emailAddress\": \"Email Address\",\n \"emailPlaceholder\": \"your@email.com\",\n \"phoneNumber\": \"Phone Number\",\n \"phonePlaceholder\": \"+1 (555) 000-0000\",\n \"message\": \"Message\",\n \"messagePlaceholder\": \"How can we help you?\",\n \"submit\": \"Send Message\",\n \"sending\": \"Sending...\",\n \"success\": \"Your message has been sent successfully! We'll get back to you soon.\",\n \"error\": \"Something went wrong. Please try again later.\",\n \"followUs\": \"Follow us on social media\",\n \"mapTitle\": \"Our Location\"\n}\n"
39
+ "content": "{\r\n \"title\": \"Get in Touch\",\r\n \"description\": \"Have a question or want to work together? Fill out the form below and we'll get back to you as soon as possible.\",\r\n \"email\": \"Email\",\r\n \"phone\": \"Phone\",\r\n \"address\": \"Address\",\r\n \"fullName\": \"Full Name\",\r\n \"fullNamePlaceholder\": \"Your name\",\r\n \"emailAddress\": \"Email Address\",\r\n \"emailPlaceholder\": \"your@email.com\",\r\n \"phoneNumber\": \"Phone Number\",\r\n \"phonePlaceholder\": \"+1 (555) 000-0000\",\r\n \"message\": \"Message\",\r\n \"messagePlaceholder\": \"How can we help you?\",\r\n \"submit\": \"Send Message\",\r\n \"sending\": \"Sending...\",\r\n \"success\": \"Your message has been sent successfully! We'll get back to you soon.\",\r\n \"error\": \"Something went wrong. Please try again later.\",\r\n \"followUs\": \"Follow us on social media\",\r\n \"mapTitle\": \"Our Location\",\r\n \"addFiles\": \"Add Files\",\r\n \"maxFilesReached\": \"Maximum files reached\",\r\n \"maxFilesLimit\": \"You can add up to {{max}} files\"\r\n}\r\n"
40
40
  },
41
41
  {
42
42
  "path": "contact-page-map-split/lang/tr.json",
43
43
  "type": "registry:lang",
44
44
  "target": "$modules$/contact-page-map-split/lang/tr.json",
45
- "content": "{\n \"title\": \"Bize Ulaşın\",\n \"description\": \"Sorunuz mu var veya birlikte çalışmak mı istiyorsunuz? Aşağıdaki formu doldurun, en kısa sürede size döneceğiz.\",\n \"email\": \"E-posta\",\n \"phone\": \"Telefon\",\n \"address\": \"Adres\",\n \"fullName\": \"Ad Soyad\",\n \"fullNamePlaceholder\": \"Adınız\",\n \"emailAddress\": \"E-posta Adresi\",\n \"emailPlaceholder\": \"email@adresiniz.com\",\n \"phoneNumber\": \"Telefon Numarası\",\n \"phonePlaceholder\": \"+90 (555) 000 00 00\",\n \"message\": \"Mesaj\",\n \"messagePlaceholder\": \"Size nasıl yardımcı olabiliriz?\",\n \"submit\": \"Mesaj Gönder\",\n \"sending\": \"Gönderiliyor...\",\n \"success\": \"Mesajınız başarıyla gönderildi! En kısa sürede size döneceğiz.\",\n \"error\": \"Bir hata oluştu. Lütfen daha sonra tekrar deneyin.\",\n \"followUs\": \"Sosyal medyada bizi takip edin\",\n \"mapTitle\": \"Konumumuz\"\n}\n"
45
+ "content": "{\r\n \"title\": \"Bize Ulaşın\",\r\n \"description\": \"Sorunuz mu var veya birlikte çalışmak mı istiyorsunuz? Aşağıdaki formu doldurun, en kısa sürede size döneceğiz.\",\r\n \"email\": \"E-posta\",\r\n \"phone\": \"Telefon\",\r\n \"address\": \"Adres\",\r\n \"fullName\": \"Ad Soyad\",\r\n \"fullNamePlaceholder\": \"Adınız\",\r\n \"emailAddress\": \"E-posta Adresi\",\r\n \"emailPlaceholder\": \"email@adresiniz.com\",\r\n \"phoneNumber\": \"Telefon Numarası\",\r\n \"phonePlaceholder\": \"+90 (555) 000 00 00\",\r\n \"message\": \"Mesaj\",\r\n \"messagePlaceholder\": \"Size nasıl yardımcı olabiliriz?\",\r\n \"submit\": \"Mesaj Gönder\",\r\n \"sending\": \"Gönderiliyor...\",\r\n \"success\": \"Mesajınız başarıyla gönderildi! En kısa sürede size döneceğiz.\",\r\n \"error\": \"Bir hata oluştu. Lütfen daha sonra tekrar deneyin.\",\r\n \"followUs\": \"Sosyal medyada bizi takip edin\",\r\n \"mapTitle\": \"Konumumuz\",\r\n \"addFiles\": \"Dosya Ekle\",\r\n \"maxFilesReached\": \"Maksimum dosya sayısına ulaşıldı\",\r\n \"maxFilesLimit\": \"En fazla {{max}} kadar dosya ekleyebilirsiniz\"\r\n}\r\n"
46
46
  }
47
47
  ],
48
48
  "exports": {