@promakeai/cli 0.1.3 → 0.2.1

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.
@@ -19,7 +19,7 @@
19
19
  "path": "announcement-bar/announcement-bar.tsx",
20
20
  "type": "registry:component",
21
21
  "target": "$modules$/announcement-bar/announcement-bar.tsx",
22
- "content": "import { useState, useEffect } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { X, ArrowRight, Sparkles, Megaphone, Gift, Zap } from \"lucide-react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { motion, AnimatePresence } from \"motion/react\";\r\n\r\ntype BarVariant = \"default\" | \"primary\" | \"warning\" | \"success\" | \"gradient\";\r\n\r\ninterface AnnouncementBarProps {\r\n message?: string;\r\n linkText?: string;\r\n linkUrl?: string;\r\n variant?: BarVariant;\r\n icon?: \"sparkles\" | \"megaphone\" | \"gift\" | \"zap\" | \"none\";\r\n dismissible?: boolean;\r\n storageKey?: string;\r\n className?: string;\r\n}\r\n\r\nconst icons = {\r\n sparkles: Sparkles,\r\n megaphone: Megaphone,\r\n gift: Gift,\r\n zap: Zap,\r\n none: null,\r\n};\r\n\r\nconst variantStyles: Record<BarVariant, string> = {\r\n default: \"bg-muted text-muted-foreground\",\r\n primary: \"bg-primary text-primary-foreground\",\r\n warning: \"bg-yellow-500 text-yellow-950\",\r\n success: \"bg-green-500 text-white\",\r\n gradient: \"bg-gradient-to-r from-primary via-purple-500 to-pink-500 text-white\",\r\n};\r\n\r\nexport function AnnouncementBar({\r\n message,\r\n linkText,\r\n linkUrl = \"#\",\r\n variant = \"primary\",\r\n icon = \"sparkles\",\r\n dismissible = true,\r\n storageKey = \"announcement-bar-dismissed\",\r\n className,\r\n}: AnnouncementBarProps) {\r\n const { t } = useTranslation(\"announcement-bar\");\r\n const [isVisible, setIsVisible] = useState(false);\r\n\r\n const displayMessage = message || t(\"message\", \"Exciting news! Check out our latest features.\");\r\n const displayLinkText = linkText || t(\"linkText\", \"Learn more\");\r\n\r\n useEffect(() => {\r\n if (dismissible) {\r\n const dismissed = localStorage.getItem(storageKey);\r\n if (!dismissed) {\r\n setIsVisible(true);\r\n }\r\n } else {\r\n setIsVisible(true);\r\n }\r\n }, [dismissible, storageKey]);\r\n\r\n const handleDismiss = () => {\r\n if (dismissible) {\r\n localStorage.setItem(storageKey, \"true\");\r\n }\r\n setIsVisible(false);\r\n };\r\n\r\n const IconComponent = icons[icon];\r\n\r\n return (\r\n <AnimatePresence>\r\n {isVisible && (\r\n <motion.div\r\n initial={{ height: 0, opacity: 0 }}\r\n animate={{ height: \"auto\", opacity: 1 }}\r\n exit={{ height: 0, opacity: 0 }}\r\n transition={{ duration: 0.3 }}\r\n className={cn(\r\n \"relative overflow-hidden\",\r\n variantStyles[variant],\r\n className\r\n )}\r\n >\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"flex items-center justify-center gap-2 py-2.5 text-sm font-medium\">\r\n {IconComponent && (\r\n <IconComponent className=\"h-4 w-4 flex-shrink-0\" />\r\n )}\r\n <span className=\"text-center\">{displayMessage}</span>\r\n {linkUrl && (\r\n <Link\r\n to={linkUrl}\r\n className=\"inline-flex items-center gap-1 font-semibold hover:underline underline-offset-2\"\r\n >\r\n {displayLinkText}\r\n <ArrowRight className=\"h-3 w-3\" />\r\n </Link>\r\n )}\r\n {dismissible && (\r\n <button\r\n onClick={handleDismiss}\r\n className=\"absolute right-4 p-1 rounded hover:bg-black/10 transition-colors\"\r\n aria-label={t(\"dismiss\", \"Dismiss\")}\r\n >\r\n <X className=\"h-4 w-4\" />\r\n </button>\r\n )}\r\n </div>\r\n </div>\r\n </motion.div>\r\n )}\r\n </AnimatePresence>\r\n );\r\n}\r\n"
22
+ "content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { X, ArrowRight, Sparkles, Megaphone, Gift, Zap } from \"lucide-react\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { motion, AnimatePresence } from \"motion/react\";\r\n\r\ntype BarVariant = \"default\" | \"primary\" | \"warning\" | \"success\" | \"gradient\";\r\n\r\ninterface AnnouncementBarProps {\r\n message?: string;\r\n linkText?: string;\r\n linkUrl?: string;\r\n variant?: BarVariant;\r\n icon?: \"sparkles\" | \"megaphone\" | \"gift\" | \"zap\" | \"none\";\r\n dismissible?: boolean;\r\n storageKey?: string;\r\n className?: string;\r\n}\r\n\r\nconst icons = {\r\n sparkles: Sparkles,\r\n megaphone: Megaphone,\r\n gift: Gift,\r\n zap: Zap,\r\n none: null,\r\n};\r\n\r\nconst variantStyles: Record<BarVariant, string> = {\r\n default: \"bg-muted text-muted-foreground\",\r\n primary: \"bg-primary text-primary-foreground\",\r\n warning: \"bg-yellow-500 text-yellow-950\",\r\n success: \"bg-green-500 text-white\",\r\n gradient: \"bg-gradient-to-r from-primary via-purple-500 to-pink-500 text-white\",\r\n};\r\n\r\nexport function AnnouncementBar({\r\n message,\r\n linkText,\r\n linkUrl = \"#\",\r\n variant = \"primary\",\r\n icon = \"sparkles\",\r\n dismissible = true,\r\n storageKey = \"announcement-bar-dismissed\",\r\n className,\r\n}: AnnouncementBarProps) {\r\n const { t } = useTranslation(\"announcement-bar\");\r\n const [isVisible, setIsVisible] = useState(() => {\r\n if (typeof window === \"undefined\") return false;\r\n if (dismissible) {\r\n return !localStorage.getItem(storageKey);\r\n }\r\n return true;\r\n });\r\n\r\n const displayMessage = message || t(\"message\", \"Exciting news! Check out our latest features.\");\r\n const displayLinkText = linkText || t(\"linkText\", \"Learn more\");\r\n\r\n const handleDismiss = () => {\r\n if (dismissible) {\r\n localStorage.setItem(storageKey, \"true\");\r\n }\r\n setIsVisible(false);\r\n };\r\n\r\n const IconComponent = icons[icon];\r\n\r\n return (\r\n <AnimatePresence>\r\n {isVisible && (\r\n <motion.div\r\n initial={{ height: 0, opacity: 0 }}\r\n animate={{ height: \"auto\", opacity: 1 }}\r\n exit={{ height: 0, opacity: 0 }}\r\n transition={{ duration: 0.3 }}\r\n className={cn(\r\n \"relative overflow-hidden\",\r\n variantStyles[variant],\r\n className\r\n )}\r\n >\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"flex items-center justify-center gap-2 py-2.5 text-sm font-medium\">\r\n {IconComponent && (\r\n <IconComponent className=\"h-4 w-4 flex-shrink-0\" />\r\n )}\r\n <span className=\"text-center\">{displayMessage}</span>\r\n {linkUrl && (\r\n <Link\r\n to={linkUrl}\r\n className=\"inline-flex items-center gap-1 font-semibold hover:underline underline-offset-2\"\r\n >\r\n {displayLinkText}\r\n <ArrowRight className=\"h-3 w-3\" />\r\n </Link>\r\n )}\r\n {dismissible && (\r\n <button\r\n onClick={handleDismiss}\r\n className=\"absolute right-4 p-1 rounded hover:bg-black/10 transition-colors\"\r\n aria-label={t(\"dismiss\", \"Dismiss\")}\r\n >\r\n <X className=\"h-4 w-4\" />\r\n </button>\r\n )}\r\n </div>\r\n </div>\r\n </motion.div>\r\n )}\r\n </AnimatePresence>\r\n );\r\n}\r\n"
23
23
  },
24
24
  {
25
25
  "path": "announcement-bar/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 { usePosts, useBlogCategories } from \"@/modules/blog-core\";\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 } = usePosts();\n const { categories } = useBlogCategories();\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 FilterSection = () => (\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\">\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 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 />\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 />\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 <PostCard key={post.id} post={post} layout={viewMode} />\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\";\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 { usePosts, useBlogCategories, type BlogCategory } from \"@/modules/blog-core\";\n\ninterface FilterSectionProps {\n t: (key: string, fallback?: string) => string;\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\">\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 } = usePosts();\n const { categories } = useBlogCategories();\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 <PostCard key={post.id} post={post} layout={viewMode} />\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"
28
28
  },
29
29
  {
30
30
  "path": "blog-list-page/lang/en.json",
@@ -20,7 +20,7 @@
20
20
  "path": "cards-carousel-section/cards-carousel-section.tsx",
21
21
  "type": "registry:component",
22
22
  "target": "$modules$/cards-carousel-section/cards-carousel-section.tsx",
23
- "content": "import { useEffect, useRef, useState, createContext, useContext } from \"react\";\r\nimport type { ReactNode } from \"react\";\r\nimport { motion, AnimatePresence } from \"framer-motion\";\r\nimport { X, ChevronLeft, ChevronRight } from \"lucide-react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\n// Context for outside click detection\r\nconst CarouselContext = createContext<{\r\n onCardClose: (index: number) => void;\r\n currentIndex: number;\r\n}>({\r\n onCardClose: () => {},\r\n currentIndex: 0,\r\n});\r\n\r\n// Card interface\r\ninterface CardData {\r\n category: string;\r\n title: string;\r\n src: string;\r\n content: ReactNode;\r\n}\r\n\r\n// Card Component\r\nexport function Card({\r\n card,\r\n index,\r\n layout = false,\r\n}: {\r\n card: CardData;\r\n index: number;\r\n layout?: boolean;\r\n}) {\r\n const [open, setOpen] = useState(false);\r\n const containerRef = useRef<HTMLDivElement>(null);\r\n const { onCardClose } = useContext(CarouselContext);\r\n\r\n useEffect(() => {\r\n function onKeyDown(event: KeyboardEvent) {\r\n if (event.key === \"Escape\") {\r\n handleClose();\r\n }\r\n }\r\n\r\n if (open) {\r\n document.body.style.overflow = \"hidden\";\r\n window.addEventListener(\"keydown\", onKeyDown);\r\n } else {\r\n document.body.style.overflow = \"auto\";\r\n }\r\n\r\n return () => {\r\n window.removeEventListener(\"keydown\", onKeyDown);\r\n document.body.style.overflow = \"auto\";\r\n };\r\n }, [open]);\r\n\r\n const handleOpen = () => setOpen(true);\r\n const handleClose = () => {\r\n setOpen(false);\r\n onCardClose(index);\r\n };\r\n\r\n return (\r\n <>\r\n <AnimatePresence>\r\n {open && (\r\n <div className=\"fixed inset-0 h-screen z-50 overflow-auto\">\r\n <motion.div\r\n initial={{ opacity: 0 }}\r\n animate={{ opacity: 1 }}\r\n exit={{ opacity: 0 }}\r\n className=\"bg-black/80 backdrop-blur-lg h-full w-full fixed inset-0\"\r\n onClick={handleClose}\r\n />\r\n <motion.div\r\n initial={{ opacity: 0 }}\r\n animate={{ opacity: 1 }}\r\n exit={{ opacity: 0 }}\r\n ref={containerRef}\r\n layoutId={layout ? `card-${card.title}` : undefined}\r\n className=\"max-w-5xl mx-auto bg-white dark:bg-neutral-900 h-fit z-[60] my-10 p-4 md:p-10 rounded-3xl relative\"\r\n >\r\n <button\r\n className=\"sticky top-4 h-8 w-8 right-0 ml-auto bg-black dark:bg-white rounded-full flex items-center justify-center\"\r\n onClick={handleClose}\r\n >\r\n <X className=\"h-6 w-6 text-neutral-100 dark:text-neutral-900\" />\r\n </button>\r\n <motion.p\r\n layoutId={layout ? `category-${card.title}` : undefined}\r\n className=\"text-base font-medium text-black dark:text-white\"\r\n >\r\n {card.category}\r\n </motion.p>\r\n <motion.p\r\n layoutId={layout ? `title-${card.title}` : undefined}\r\n className=\"text-2xl md:text-5xl font-semibold text-neutral-700 mt-4 dark:text-white\"\r\n >\r\n {card.title}\r\n </motion.p>\r\n <div className=\"py-10\">{card.content}</div>\r\n </motion.div>\r\n </div>\r\n )}\r\n </AnimatePresence>\r\n <motion.button\r\n layoutId={layout ? `card-${card.title}` : undefined}\r\n onClick={handleOpen}\r\n className=\"rounded-3xl bg-gray-100 dark:bg-neutral-900 h-80 w-56 md:h-[40rem] md:w-96 overflow-hidden flex flex-col items-start justify-start relative z-10\"\r\n >\r\n <div className=\"absolute h-full top-0 inset-x-0 bg-gradient-to-b from-black/50 via-transparent to-transparent z-30 pointer-events-none\" />\r\n <div className=\"relative z-40 p-8\">\r\n <motion.p\r\n layoutId={layout ? `category-${card.title}` : undefined}\r\n className=\"text-white text-sm md:text-base font-medium text-left\"\r\n >\r\n {card.category}\r\n </motion.p>\r\n <motion.p\r\n layoutId={layout ? `title-${card.title}` : undefined}\r\n className=\"text-white text-xl md:text-3xl font-semibold max-w-xs text-left mt-2\"\r\n >\r\n {card.title}\r\n </motion.p>\r\n </div>\r\n <img\r\n src={card.src}\r\n alt={card.title}\r\n className=\"object-cover absolute z-10 inset-0 w-full h-full\"\r\n />\r\n </motion.button>\r\n </>\r\n );\r\n}\r\n\r\n// Carousel Component\r\nexport function Carousel({ items }: { items: ReactNode[] }) {\r\n const carouselRef = useRef<HTMLDivElement>(null);\r\n const [canScrollLeft, setCanScrollLeft] = useState(false);\r\n const [canScrollRight, setCanScrollRight] = useState(true);\r\n const [currentIndex, setCurrentIndex] = useState(0);\r\n\r\n useEffect(() => {\r\n checkScrollability();\r\n }, []);\r\n\r\n const checkScrollability = () => {\r\n if (carouselRef.current) {\r\n const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;\r\n setCanScrollLeft(scrollLeft > 0);\r\n setCanScrollRight(scrollLeft < scrollWidth - clientWidth);\r\n }\r\n };\r\n\r\n const scrollLeft = () => {\r\n if (carouselRef.current) {\r\n carouselRef.current.scrollBy({ left: -300, behavior: \"smooth\" });\r\n }\r\n };\r\n\r\n const scrollRight = () => {\r\n if (carouselRef.current) {\r\n carouselRef.current.scrollBy({ left: 300, behavior: \"smooth\" });\r\n }\r\n };\r\n\r\n const handleCardClose = (index: number) => {\r\n if (carouselRef.current) {\r\n const cardWidth = isMobile() ? 230 : 384;\r\n const gap = isMobile() ? 4 : 8;\r\n const scrollPosition = (cardWidth + gap) * (index + 1);\r\n carouselRef.current.scrollTo({\r\n left: scrollPosition,\r\n behavior: \"smooth\",\r\n });\r\n setCurrentIndex(index);\r\n }\r\n };\r\n\r\n const isMobile = () => {\r\n return window && window.innerWidth < 768;\r\n };\r\n\r\n return (\r\n <CarouselContext.Provider value={{ onCardClose: handleCardClose, currentIndex }}>\r\n <div className=\"relative w-full\">\r\n <div\r\n className=\"flex w-full overflow-x-scroll overscroll-x-auto py-10 md:py-20 scroll-smooth [scrollbar-width:none]\"\r\n ref={carouselRef}\r\n onScroll={checkScrollability}\r\n >\r\n <div className=\"absolute right-0 z-[1000] h-auto w-[5%] overflow-hidden bg-gradient-to-l from-background to-transparent pointer-events-none\" />\r\n <div className=\"flex flex-row justify-start gap-4 pl-4 max-w-7xl mx-auto\">\r\n {items.map((item, index) => (\r\n <motion.div\r\n initial={{ opacity: 0, y: 20 }}\r\n animate={{\r\n opacity: 1,\r\n y: 0,\r\n transition: {\r\n duration: 0.5,\r\n delay: 0.2 * index,\r\n ease: \"easeOut\",\r\n },\r\n }}\r\n key={\"card\" + index}\r\n className=\"last:pr-[5%] md:last:pr-[33%] rounded-3xl\"\r\n >\r\n {item}\r\n </motion.div>\r\n ))}\r\n </div>\r\n </div>\r\n <div className=\"flex justify-end gap-2 mr-10\">\r\n <button\r\n className=\"relative z-40 h-10 w-10 rounded-full bg-gray-100 flex items-center justify-center disabled:opacity-50\"\r\n onClick={scrollLeft}\r\n disabled={!canScrollLeft}\r\n >\r\n <ChevronLeft className=\"h-6 w-6 text-gray-500\" />\r\n </button>\r\n <button\r\n className=\"relative z-40 h-10 w-10 rounded-full bg-gray-100 flex items-center justify-center disabled:opacity-50\"\r\n onClick={scrollRight}\r\n disabled={!canScrollRight}\r\n >\r\n <ChevronRight className=\"h-6 w-6 text-gray-500\" />\r\n </button>\r\n </div>\r\n </div>\r\n </CarouselContext.Provider>\r\n );\r\n}\r\n\r\n// Section Component\r\ninterface CardsCarouselSectionProps {\r\n title?: string;\r\n items: CardData[];\r\n className?: string;\r\n}\r\n\r\nexport function CardsCarouselSection({\r\n title,\r\n items,\r\n className,\r\n}: CardsCarouselSectionProps) {\r\n const cards = items.map((card, index) => (\r\n <Card key={card.src} card={card} index={index} />\r\n ));\r\n\r\n return (\r\n <section className={cn(\"w-full py-16 md:py-20\", className)}>\r\n {title && (\r\n <h2 className=\"max-w-7xl pl-4 mx-auto text-xl md:text-5xl font-bold text-neutral-800 dark:text-neutral-200\">\r\n {title}\r\n </h2>\r\n )}\r\n <Carousel items={cards} />\r\n </section>\r\n );\r\n}\r\n"
23
+ "content": "import { useEffect, useRef, useState, createContext, useContext } from \"react\";\r\nimport type { ReactNode } from \"react\";\r\nimport { motion, AnimatePresence } from \"framer-motion\";\r\nimport { X, ChevronLeft, ChevronRight } from \"lucide-react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\n// Context for outside click detection\r\nconst CarouselContext = createContext<{\r\n onCardClose: (index: number) => void;\r\n currentIndex: number;\r\n}>({\r\n onCardClose: () => {},\r\n currentIndex: 0,\r\n});\r\n\r\n// Card interface\r\ninterface CardData {\r\n category: string;\r\n title: string;\r\n src: string;\r\n content: ReactNode;\r\n}\r\n\r\n// Card Component\r\nexport function Card({\r\n card,\r\n index,\r\n layout = false,\r\n}: {\r\n card: CardData;\r\n index: number;\r\n layout?: boolean;\r\n}) {\r\n const [open, setOpen] = useState(false);\r\n const containerRef = useRef<HTMLDivElement>(null);\r\n const { onCardClose } = useContext(CarouselContext);\r\n\r\n const handleOpen = () => setOpen(true);\r\n const handleClose = () => {\r\n setOpen(false);\r\n onCardClose(index);\r\n };\r\n\r\n useEffect(() => {\r\n function onKeyDown(event: KeyboardEvent) {\r\n if (event.key === \"Escape\") {\r\n handleClose();\r\n }\r\n }\r\n\r\n if (open) {\r\n document.body.style.overflow = \"hidden\";\r\n window.addEventListener(\"keydown\", onKeyDown);\r\n } else {\r\n document.body.style.overflow = \"auto\";\r\n }\r\n\r\n return () => {\r\n window.removeEventListener(\"keydown\", onKeyDown);\r\n document.body.style.overflow = \"auto\";\r\n };\r\n }, [open, handleClose]);\r\n\r\n return (\r\n <>\r\n <AnimatePresence>\r\n {open && (\r\n <div className=\"fixed inset-0 h-screen z-50 overflow-auto\">\r\n <motion.div\r\n initial={{ opacity: 0 }}\r\n animate={{ opacity: 1 }}\r\n exit={{ opacity: 0 }}\r\n className=\"bg-black/80 backdrop-blur-lg h-full w-full fixed inset-0\"\r\n onClick={handleClose}\r\n />\r\n <motion.div\r\n initial={{ opacity: 0 }}\r\n animate={{ opacity: 1 }}\r\n exit={{ opacity: 0 }}\r\n ref={containerRef}\r\n layoutId={layout ? `card-${card.title}` : undefined}\r\n className=\"max-w-5xl mx-auto bg-white dark:bg-neutral-900 h-fit z-[60] my-10 p-4 md:p-10 rounded-3xl relative\"\r\n >\r\n <button\r\n className=\"sticky top-4 h-8 w-8 right-0 ml-auto bg-black dark:bg-white rounded-full flex items-center justify-center\"\r\n onClick={handleClose}\r\n >\r\n <X className=\"h-6 w-6 text-neutral-100 dark:text-neutral-900\" />\r\n </button>\r\n <motion.p\r\n layoutId={layout ? `category-${card.title}` : undefined}\r\n className=\"text-base font-medium text-black dark:text-white\"\r\n >\r\n {card.category}\r\n </motion.p>\r\n <motion.p\r\n layoutId={layout ? `title-${card.title}` : undefined}\r\n className=\"text-2xl md:text-5xl font-semibold text-neutral-700 mt-4 dark:text-white\"\r\n >\r\n {card.title}\r\n </motion.p>\r\n <div className=\"py-10\">{card.content}</div>\r\n </motion.div>\r\n </div>\r\n )}\r\n </AnimatePresence>\r\n <motion.button\r\n layoutId={layout ? `card-${card.title}` : undefined}\r\n onClick={handleOpen}\r\n className=\"rounded-3xl bg-gray-100 dark:bg-neutral-900 h-80 w-56 md:h-[40rem] md:w-96 overflow-hidden flex flex-col items-start justify-start relative z-10\"\r\n >\r\n <div className=\"absolute h-full top-0 inset-x-0 bg-gradient-to-b from-black/50 via-transparent to-transparent z-30 pointer-events-none\" />\r\n <div className=\"relative z-40 p-8\">\r\n <motion.p\r\n layoutId={layout ? `category-${card.title}` : undefined}\r\n className=\"text-white text-sm md:text-base font-medium text-left\"\r\n >\r\n {card.category}\r\n </motion.p>\r\n <motion.p\r\n layoutId={layout ? `title-${card.title}` : undefined}\r\n className=\"text-white text-xl md:text-3xl font-semibold max-w-xs text-left mt-2\"\r\n >\r\n {card.title}\r\n </motion.p>\r\n </div>\r\n <img\r\n src={card.src}\r\n alt={card.title}\r\n className=\"object-cover absolute z-10 inset-0 w-full h-full\"\r\n />\r\n </motion.button>\r\n </>\r\n );\r\n}\r\n\r\n// Carousel Component\r\nexport function Carousel({ items }: { items: ReactNode[] }) {\r\n const carouselRef = useRef<HTMLDivElement>(null);\r\n const [canScrollLeft, setCanScrollLeft] = useState(false);\r\n const [canScrollRight, setCanScrollRight] = useState(true);\r\n const [currentIndex, setCurrentIndex] = useState(0);\r\n\r\n const checkScrollability = () => {\r\n if (carouselRef.current) {\r\n const { scrollLeft, scrollWidth, clientWidth } = carouselRef.current;\r\n setCanScrollLeft(scrollLeft > 0);\r\n setCanScrollRight(scrollLeft < scrollWidth - clientWidth);\r\n }\r\n };\r\n\r\n useEffect(() => {\r\n checkScrollability();\r\n }, []);\r\n\r\n const scrollLeft = () => {\r\n if (carouselRef.current) {\r\n carouselRef.current.scrollBy({ left: -300, behavior: \"smooth\" });\r\n }\r\n };\r\n\r\n const scrollRight = () => {\r\n if (carouselRef.current) {\r\n carouselRef.current.scrollBy({ left: 300, behavior: \"smooth\" });\r\n }\r\n };\r\n\r\n const handleCardClose = (index: number) => {\r\n if (carouselRef.current) {\r\n const cardWidth = isMobile() ? 230 : 384;\r\n const gap = isMobile() ? 4 : 8;\r\n const scrollPosition = (cardWidth + gap) * (index + 1);\r\n carouselRef.current.scrollTo({\r\n left: scrollPosition,\r\n behavior: \"smooth\",\r\n });\r\n setCurrentIndex(index);\r\n }\r\n };\r\n\r\n const isMobile = () => {\r\n return window && window.innerWidth < 768;\r\n };\r\n\r\n return (\r\n <CarouselContext.Provider value={{ onCardClose: handleCardClose, currentIndex }}>\r\n <div className=\"relative w-full\">\r\n <div\r\n className=\"flex w-full overflow-x-scroll overscroll-x-auto py-10 md:py-20 scroll-smooth [scrollbar-width:none]\"\r\n ref={carouselRef}\r\n onScroll={checkScrollability}\r\n >\r\n <div className=\"absolute right-0 z-[1000] h-auto w-[5%] overflow-hidden bg-gradient-to-l from-background to-transparent pointer-events-none\" />\r\n <div className=\"flex flex-row justify-start gap-4 pl-4 max-w-7xl mx-auto\">\r\n {items.map((item, index) => (\r\n <motion.div\r\n initial={{ opacity: 0, y: 20 }}\r\n animate={{\r\n opacity: 1,\r\n y: 0,\r\n transition: {\r\n duration: 0.5,\r\n delay: 0.2 * index,\r\n ease: \"easeOut\",\r\n },\r\n }}\r\n key={\"card\" + index}\r\n className=\"last:pr-[5%] md:last:pr-[33%] rounded-3xl\"\r\n >\r\n {item}\r\n </motion.div>\r\n ))}\r\n </div>\r\n </div>\r\n <div className=\"flex justify-end gap-2 mr-10\">\r\n <button\r\n className=\"relative z-40 h-10 w-10 rounded-full bg-gray-100 flex items-center justify-center disabled:opacity-50\"\r\n onClick={scrollLeft}\r\n disabled={!canScrollLeft}\r\n >\r\n <ChevronLeft className=\"h-6 w-6 text-gray-500\" />\r\n </button>\r\n <button\r\n className=\"relative z-40 h-10 w-10 rounded-full bg-gray-100 flex items-center justify-center disabled:opacity-50\"\r\n onClick={scrollRight}\r\n disabled={!canScrollRight}\r\n >\r\n <ChevronRight className=\"h-6 w-6 text-gray-500\" />\r\n </button>\r\n </div>\r\n </div>\r\n </CarouselContext.Provider>\r\n );\r\n}\r\n\r\n// Section Component\r\ninterface CardsCarouselSectionProps {\r\n title?: string;\r\n items: CardData[];\r\n className?: string;\r\n}\r\n\r\nexport function CardsCarouselSection({\r\n title,\r\n items,\r\n className,\r\n}: CardsCarouselSectionProps) {\r\n const cards = items.map((card, index) => (\r\n <Card key={card.src} card={card} index={index} />\r\n ));\r\n\r\n return (\r\n <section className={cn(\"w-full py-16 md:py-20\", className)}>\r\n {title && (\r\n <h2 className=\"max-w-7xl pl-4 mx-auto text-xl md:text-5xl font-bold text-neutral-800 dark:text-neutral-200\">\r\n {title}\r\n </h2>\r\n )}\r\n <Carousel items={cards} />\r\n </section>\r\n );\r\n}\r\n"
24
24
  },
25
25
  {
26
26
  "path": "cards-carousel-section/lang/en.json",
@@ -20,7 +20,7 @@
20
20
  "path": "cart-drawer/cart-drawer.tsx",
21
21
  "type": "registry:component",
22
22
  "target": "$modules$/cart-drawer/cart-drawer.tsx",
23
- "content": "import { Link } from \"react-router\";\r\nimport { ShoppingCart, 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 SheetTrigger,\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 showTrigger?: boolean;\r\n className?: string;\r\n}\r\n\r\nexport function CartDrawer({\r\n checkoutHref = \"/checkout\",\r\n showTrigger = true,\r\n className,\r\n}: CartDrawerProps) {\r\n const { t } = useTranslation(\"cart-drawer\");\r\n const {\r\n state,\r\n removeItem,\r\n updateQuantity,\r\n itemCount,\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 {showTrigger && (\r\n <SheetTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"relative\">\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {itemCount > 0 && (\r\n <span className=\"absolute -top-1 -right-1 h-5 w-5 rounded-full bg-primary text-primary-foreground text-xs flex items-center justify-center\">\r\n {itemCount}\r\n </span>\r\n )}\r\n </Button>\r\n </SheetTrigger>\r\n )}\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 to={`/products/${item.product.slug}`}>\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}>{t(\"checkout\", \"Checkout\")}</Link>\r\n </Button>\r\n </div>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n );\r\n}\r\n"
23
+ "content": "import { 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}\r\n\r\nexport function CartDrawer({\r\n checkoutHref = \"/checkout\",\r\n className,\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} className={className}>\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",
@@ -19,7 +19,7 @@
19
19
  "path": "ecommerce-core/types.ts",
20
20
  "type": "registry:type",
21
21
  "target": "$modules$/ecommerce-core/types.ts",
22
- "content": "export interface ProductCategory {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n is_primary: boolean;\r\n}\r\n\r\nexport interface Product {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n description: string;\r\n price: number;\r\n sale_price?: number;\r\n on_sale: boolean;\r\n images: string[];\r\n category: string; // Primary category slug (backward compatibility)\r\n category_name?: string; // Primary category name (backward compatibility)\r\n categories: ProductCategory[]; // NEW: Multi-category support\r\n brand?: string;\r\n sku?: string;\r\n stock: number;\r\n tags: string[];\r\n rating: number;\r\n review_count: number;\r\n featured: boolean;\r\n is_new: boolean;\r\n published: boolean;\r\n specifications?: Record<string, any>;\r\n variants?: any;\r\n created_at?: string;\r\n updated_at?: string;\r\n meta_description?: string;\r\n meta_keywords?: string;\r\n}\r\n\r\nexport interface ProductVariant {\r\n id: string;\r\n name: string;\r\n value: string;\r\n price?: number;\r\n image?: string;\r\n stockQuantity: number;\r\n}\r\n\r\nexport interface CartItem {\r\n id: string | number;\r\n product: Product;\r\n quantity: number;\r\n}\r\n\r\nexport interface CartState {\r\n items: CartItem[];\r\n total: number;\r\n}\r\n\r\nexport interface CartContextType {\r\n state: CartState;\r\n addItem: (product: Product) => void;\r\n removeItem: (id: string | number) => void;\r\n updateQuantity: (id: string | number, quantity: number) => void;\r\n clearCart: () => void;\r\n itemCount: number;\r\n}\r\n\r\nexport interface FavoritesContextType {\r\n favorites: Product[];\r\n addToFavorites: (product: Product) => void;\r\n removeFromFavorites: (productId: string | number) => void;\r\n isFavorite: (productId: string | number) => boolean;\r\n favoriteCount: number;\r\n}\r\n\r\nexport interface Category {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n description?: string;\r\n image?: string;\r\n}\r\n\r\nexport interface User {\r\n id: string;\r\n email: string;\r\n name: string;\r\n avatar?: string;\r\n addresses?: Address[];\r\n orders?: Order[];\r\n}\r\n\r\nexport interface Address {\r\n name: string;\r\n line1: string;\r\n line2?: string;\r\n city: string;\r\n state: string;\r\n postalCode: string;\r\n country: string;\r\n}\r\n\r\nexport interface Order {\r\n id: number;\r\n user_id: string;\r\n total_price: number;\r\n status: 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled';\r\n payment_method: string;\r\n shipping_address: Address;\r\n notes?: string;\r\n created_at?: string;\r\n}\r\n\r\nexport interface OrderItem {\r\n id: number;\r\n order_id: number;\r\n product_id: number;\r\n quantity: number;\r\n price: number;\r\n product?: {\r\n name: string;\r\n slug: string;\r\n images: string[];\r\n price: number;\r\n };\r\n}\r\n"
22
+ "content": "export interface ProductCategory {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n is_primary: boolean;\r\n}\r\n\r\nexport interface Product {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n description: string;\r\n price: number;\r\n sale_price?: number;\r\n on_sale: boolean;\r\n images: string[];\r\n category: string; // Primary category slug (backward compatibility)\r\n category_name?: string; // Primary category name (backward compatibility)\r\n categories: ProductCategory[]; // NEW: Multi-category support\r\n brand?: string;\r\n sku?: string;\r\n stock: number;\r\n tags: string[];\r\n rating: number;\r\n review_count: number;\r\n featured: boolean;\r\n is_new: boolean;\r\n published: boolean;\r\n specifications?: Record<string, any>;\r\n variants?: any;\r\n created_at?: string;\r\n updated_at?: string;\r\n meta_description?: string;\r\n meta_keywords?: string;\r\n}\r\n\r\nexport interface ProductVariant {\r\n id: string;\r\n name: string;\r\n value: string;\r\n price?: number;\r\n image?: string;\r\n stockQuantity: number;\r\n}\r\n\r\nexport interface CartItem {\r\n id: string | number;\r\n product: Product;\r\n quantity: number;\r\n}\r\n\r\nexport interface CartState {\r\n items: CartItem[];\r\n total: number;\r\n}\r\n\r\nexport interface CartContextType {\r\n state: CartState;\r\n addItem: (product: Product) => void;\r\n removeItem: (id: string | number) => void;\r\n updateQuantity: (id: string | number, quantity: number) => void;\r\n clearCart: () => void;\r\n itemCount: number;\r\n}\r\n\r\nexport interface FavoritesContextType {\r\n favorites: Product[];\r\n addToFavorites: (product: Product) => void;\r\n removeFromFavorites: (productId: string | number) => void;\r\n isFavorite: (productId: string | number) => boolean;\r\n favoriteCount: number;\r\n clearFavorites: () => void;\r\n}\r\n\r\nexport interface Category {\r\n id: number;\r\n name: string;\r\n slug: string;\r\n description?: string;\r\n image?: string;\r\n}\r\n\r\nexport interface User {\r\n id: string;\r\n email: string;\r\n name: string;\r\n avatar?: string;\r\n addresses?: Address[];\r\n orders?: Order[];\r\n}\r\n\r\nexport interface Address {\r\n name: string;\r\n line1: string;\r\n line2?: string;\r\n city: string;\r\n state: string;\r\n postalCode: string;\r\n country: string;\r\n}\r\n\r\nexport interface Order {\r\n id: number;\r\n user_id: string;\r\n total_price: number;\r\n status: 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled';\r\n payment_method: string;\r\n shipping_address: Address;\r\n notes?: string;\r\n created_at?: string;\r\n}\r\n\r\nexport interface OrderItem {\r\n id: number;\r\n order_id: number;\r\n product_id: number;\r\n quantity: number;\r\n price: number;\r\n product?: {\r\n name: string;\r\n slug: string;\r\n images: string[];\r\n price: number;\r\n };\r\n}\r\n"
23
23
  },
24
24
  {
25
25
  "path": "ecommerce-core/stores/cart-store.ts",
@@ -31,7 +31,7 @@
31
31
  "path": "ecommerce-core/stores/favorites-store.ts",
32
32
  "type": "registry:store",
33
33
  "target": "$modules$/ecommerce-core/stores/favorites-store.ts",
34
- "content": "import { create } from \"zustand\";\r\nimport { persist } from \"zustand/middleware\";\r\nimport type { Product, FavoritesContextType } from \"../types\";\r\n\r\ninterface FavoritesStore {\r\n favorites: Product[];\r\n favoriteCount: number;\r\n addToFavorites: (product: Product) => void;\r\n removeFromFavorites: (productId: string | number) => void;\r\n isFavorite: (productId: string | number) => boolean;\r\n clearFavorites: () => void;\r\n}\r\n\r\nexport const useFavoritesStore = create<FavoritesStore>()(\r\n persist(\r\n (set, get) => ({\r\n favorites: [],\r\n favoriteCount: 0,\r\n\r\n addToFavorites: (product) =>\r\n set((state) => {\r\n if (state.favorites.some((fav) => fav.id === product.id)) {\r\n return state;\r\n }\r\n const favorites = [...state.favorites, product];\r\n return { favorites, favoriteCount: favorites.length };\r\n }),\r\n\r\n removeFromFavorites: (productId) =>\r\n set((state) => {\r\n const favorites = state.favorites.filter((fav) => fav.id !== productId);\r\n return { favorites, favoriteCount: favorites.length };\r\n }),\r\n\r\n isFavorite: (productId) => {\r\n return get().favorites.some((fav) => fav.id === productId);\r\n },\r\n\r\n clearFavorites: () => set({ favorites: [], favoriteCount: 0 }),\r\n }),\r\n { name: \"ecommerce_favorites\" }\r\n )\r\n);\r\n\r\n// Backward compatible hook - matches FavoritesContextType\r\nexport const useFavorites = (): FavoritesContextType => {\r\n const store = useFavoritesStore();\r\n return {\r\n favorites: store.favorites,\r\n addToFavorites: store.addToFavorites,\r\n removeFromFavorites: store.removeFromFavorites,\r\n isFavorite: store.isFavorite,\r\n favoriteCount: store.favoriteCount,\r\n };\r\n};\r\n"
34
+ "content": "import { create } from \"zustand\";\r\nimport { persist } from \"zustand/middleware\";\r\nimport type { Product, FavoritesContextType } from \"../types\";\r\n\r\ninterface FavoritesStore {\r\n favorites: Product[];\r\n favoriteCount: number;\r\n addToFavorites: (product: Product) => void;\r\n removeFromFavorites: (productId: string | number) => void;\r\n isFavorite: (productId: string | number) => boolean;\r\n clearFavorites: () => void;\r\n}\r\n\r\nexport const useFavoritesStore = create<FavoritesStore>()(\r\n persist(\r\n (set, get) => ({\r\n favorites: [],\r\n favoriteCount: 0,\r\n\r\n addToFavorites: (product) =>\r\n set((state) => {\r\n if (state.favorites.some((fav) => fav.id === product.id)) {\r\n return state;\r\n }\r\n const favorites = [...state.favorites, product];\r\n return { favorites, favoriteCount: favorites.length };\r\n }),\r\n\r\n removeFromFavorites: (productId) =>\r\n set((state) => {\r\n const favorites = state.favorites.filter((fav) => fav.id !== productId);\r\n return { favorites, favoriteCount: favorites.length };\r\n }),\r\n\r\n isFavorite: (productId) => {\r\n return get().favorites.some((fav) => fav.id === productId);\r\n },\r\n\r\n clearFavorites: () => set({ favorites: [], favoriteCount: 0 }),\r\n }),\r\n { name: \"ecommerce_favorites\" }\r\n )\r\n);\r\n\r\n// Backward compatible hook - matches FavoritesContextType\r\nexport const useFavorites = (): FavoritesContextType => {\r\n const store = useFavoritesStore();\r\n return {\r\n favorites: store.favorites,\r\n addToFavorites: store.addToFavorites,\r\n removeFromFavorites: store.removeFromFavorites,\r\n isFavorite: store.isFavorite,\r\n favoriteCount: store.favoriteCount,\r\n clearFavorites: store.clearFavorites,\r\n };\r\n};\r\n"
35
35
  },
36
36
  {
37
37
  "path": "ecommerce-core/useProducts.ts",
@@ -15,7 +15,7 @@
15
15
  "path": "hero-carousel/hero-carousel.tsx",
16
16
  "type": "registry:component",
17
17
  "target": "$modules$/hero-carousel/hero-carousel.tsx",
18
- "content": "\"use client\";\n\nimport { useState, useEffect, useCallback, useRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\n\ninterface Slide {\n image: string;\n title: string;\n description: string;\n primaryButton?: {\n text: string;\n link: string;\n };\n secondaryButton?: {\n text: string;\n link: string;\n };\n}\n\ninterface HeroCarouselProps {\n slides?: Slide[];\n autoPlay?: boolean;\n interval?: number;\n showDots?: boolean;\n showArrows?: boolean;\n pauseOnHover?: boolean;\n className?: string;\n}\n\nexport function HeroCarousel({\n slides,\n autoPlay = true,\n interval = 5000,\n showDots = true,\n showArrows = true,\n pauseOnHover = true,\n className,\n}: HeroCarouselProps) {\n const { t } = useTranslation(\"hero-carousel\");\n const [currentSlide, setCurrentSlide] = useState(0);\n const [isPaused, setIsPaused] = useState(false);\n const [dragStart, setDragStart] = useState<number | null>(null);\n const [isDragging, setIsDragging] = useState(false);\n const containerRef = useRef<HTMLElement>(null);\n\n const defaultSlides: Slide[] = [\n {\n image: \"/images/placeholder.png\",\n title: t(\"slides.0.title\"),\n description: t(\"slides.0.description\"),\n primaryButton: { text: t(\"slides.0.primaryButton\"), link: \"/get-started\" },\n secondaryButton: { text: t(\"slides.0.secondaryButton\"), link: \"/learn-more\" },\n },\n {\n image: \"/images/placeholder.png\",\n title: t(\"slides.1.title\"),\n description: t(\"slides.1.description\"),\n primaryButton: { text: t(\"slides.1.primaryButton\"), link: \"/features\" },\n },\n {\n image: \"/images/placeholder.png\",\n title: t(\"slides.2.title\"),\n description: t(\"slides.2.description\"),\n primaryButton: { text: t(\"slides.2.primaryButton\"), link: \"/contact\" },\n secondaryButton: { text: t(\"slides.2.secondaryButton\"), link: \"/demo\" },\n },\n ];\n\n const displaySlides = slides ?? defaultSlides;\n\n const goToSlide = useCallback((index: number) => {\n setCurrentSlide(index);\n }, []);\n\n const nextSlide = useCallback(() => {\n setCurrentSlide((prev) => (prev + 1) % displaySlides.length);\n }, [displaySlides.length]);\n\n const prevSlide = useCallback(() => {\n setCurrentSlide((prev) => (prev - 1 + displaySlides.length) % displaySlides.length);\n }, [displaySlides.length]);\n\n // Auto-play\n useEffect(() => {\n if (!autoPlay || isPaused) return;\n\n const timer = setInterval(nextSlide, interval);\n return () => clearInterval(timer);\n }, [autoPlay, interval, isPaused, nextSlide]);\n\n // Mouse drag handlers\n const handleMouseDown = (e: React.MouseEvent) => {\n setIsDragging(true);\n setDragStart(e.clientX);\n };\n\n const handleMouseMove = (e: React.MouseEvent) => {\n if (!isDragging || dragStart === null) return;\n e.preventDefault();\n };\n\n const handleMouseUp = (e: React.MouseEvent) => {\n if (!isDragging || dragStart === null) {\n setIsDragging(false);\n return;\n }\n\n const diff = dragStart - e.clientX;\n\n if (Math.abs(diff) > 50) {\n if (diff > 0) {\n nextSlide();\n } else {\n prevSlide();\n }\n }\n\n setIsDragging(false);\n setDragStart(null);\n };\n\n const handleMouseLeave = () => {\n if (isDragging) {\n setIsDragging(false);\n setDragStart(null);\n }\n };\n\n // Touch handlers for swipe\n const handleTouchStart = (e: React.TouchEvent) => {\n setDragStart(e.touches[0].clientX);\n };\n\n const handleTouchEnd = (e: React.TouchEvent) => {\n if (dragStart === null) return;\n\n const touchEnd = e.changedTouches[0].clientX;\n const diff = dragStart - touchEnd;\n\n if (Math.abs(diff) > 50) {\n if (diff > 0) {\n nextSlide();\n } else {\n prevSlide();\n }\n }\n\n setDragStart(null);\n };\n\n // Wheel handler for trackpad two-finger swipe\n const handleWheel = useCallback((e: WheelEvent) => {\n // Only handle horizontal scroll (trackpad two-finger swipe)\n if (Math.abs(e.deltaX) > Math.abs(e.deltaY) && Math.abs(e.deltaX) > 30) {\n e.preventDefault();\n if (e.deltaX > 0) {\n nextSlide();\n } else {\n prevSlide();\n }\n }\n }, [nextSlide, prevSlide]);\n\n // Add wheel event listener\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n let lastWheelTime = 0;\n const throttledWheel = (e: WheelEvent) => {\n const now = Date.now();\n if (now - lastWheelTime > 500) {\n handleWheel(e);\n lastWheelTime = now;\n }\n };\n\n container.addEventListener(\"wheel\", throttledWheel, { passive: false });\n return () => container.removeEventListener(\"wheel\", throttledWheel);\n }, [handleWheel]);\n\n return (\n <section\n ref={containerRef}\n className={cn(\n \"relative w-full h-[500px] md:h-[600px] lg:h-[700px] overflow-hidden select-none\",\n isDragging ? \"cursor-grabbing\" : \"cursor-grab\",\n className\n )}\n onMouseEnter={() => pauseOnHover && setIsPaused(true)}\n onMouseLeave={() => {\n pauseOnHover && setIsPaused(false);\n handleMouseLeave();\n }}\n onMouseDown={handleMouseDown}\n onMouseMove={handleMouseMove}\n onMouseUp={handleMouseUp}\n onTouchStart={handleTouchStart}\n onTouchEnd={handleTouchEnd}\n >\n {/* Slides */}\n {displaySlides.map((slide, index) => (\n <div\n key={index}\n className={cn(\n \"absolute inset-0 transition-opacity duration-700\",\n index === currentSlide ? \"opacity-100 z-10\" : \"opacity-0 z-0\"\n )}\n >\n {/* Background Image */}\n <img\n src={slide.image}\n alt={slide.title}\n className=\"absolute inset-0 w-full h-full object-cover pointer-events-none\"\n draggable={false}\n />\n\n {/* Overlay */}\n <div className=\"absolute inset-0 bg-black/50\" />\n\n {/* Content */}\n <div className=\"relative z-10 h-full flex items-center\">\n <div className=\"container max-w-[var(--container-max-width)] mx-auto px-6 md:px-24 lg:px-32\">\n <div\n className={cn(\n \"max-w-2xl transition-all duration-700 delay-200\",\n index === currentSlide\n ? \"translate-y-0 opacity-100\"\n : \"translate-y-8 opacity-0\"\n )}\n >\n <h1 className=\"text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-4 md:mb-6\">\n {slide.title}\n </h1>\n <p className=\"text-lg md:text-xl text-white/90 mb-6 md:mb-8\">\n {slide.description}\n </p>\n <div className=\"flex flex-wrap gap-4\">\n {slide.primaryButton && (\n <Link to={slide.primaryButton.link}>\n <Button size=\"lg\" className=\"text-base\">\n {slide.primaryButton.text}\n </Button>\n </Link>\n )}\n {slide.secondaryButton && (\n <Link to={slide.secondaryButton.link}>\n <Button size=\"lg\" variant=\"outline\" className=\"text-base bg-white/10 border-white/30 text-white hover:bg-white/20\">\n {slide.secondaryButton.text}\n </Button>\n </Link>\n )}\n </div>\n </div>\n </div>\n </div>\n </div>\n ))}\n\n {/* Navigation Arrows */}\n {showArrows && (\n <>\n <button\n onClick={prevSlide}\n className=\"absolute left-4 top-1/2 -translate-y-1/2 z-20 w-12 h-12 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center text-white hover:bg-white/30 transition-colors\"\n aria-label=\"Previous slide\"\n >\n <ChevronLeft className=\"w-6 h-6\" />\n </button>\n <button\n onClick={nextSlide}\n className=\"absolute right-4 top-1/2 -translate-y-1/2 z-20 w-12 h-12 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center text-white hover:bg-white/30 transition-colors\"\n aria-label=\"Next slide\"\n >\n <ChevronRight className=\"w-6 h-6\" />\n </button>\n </>\n )}\n\n {/* Dots */}\n {showDots && (\n <div className=\"absolute bottom-6 left-1/2 -translate-x-1/2 z-20 flex gap-2\">\n {displaySlides.map((_, index) => (\n <button\n key={index}\n onClick={() => goToSlide(index)}\n className={cn(\n \"w-3 h-3 rounded-full transition-all duration-300\",\n index === currentSlide\n ? \"bg-white w-8\"\n : \"bg-white/50 hover:bg-white/70\"\n )}\n aria-label={`Go to slide ${index + 1}`}\n />\n ))}\n </div>\n )}\n </section>\n );\n}\n"
18
+ "content": "\"use client\";\n\nimport { useState, useEffect, useCallback, useRef } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { Link } from \"react-router\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/components/ui/button\";\nimport { ChevronLeft, ChevronRight } from \"lucide-react\";\n\ninterface Slide {\n image: string;\n title: string;\n description: string;\n primaryButton?: {\n text: string;\n link: string;\n };\n secondaryButton?: {\n text: string;\n link: string;\n };\n}\n\ninterface HeroCarouselProps {\n slides?: Slide[];\n autoPlay?: boolean;\n interval?: number;\n showDots?: boolean;\n showArrows?: boolean;\n pauseOnHover?: boolean;\n className?: string;\n}\n\nexport function HeroCarousel({\n slides,\n autoPlay = true,\n interval = 5000,\n showDots = true,\n showArrows = true,\n pauseOnHover = true,\n className,\n}: HeroCarouselProps) {\n const { t } = useTranslation(\"hero-carousel\");\n const [currentSlide, setCurrentSlide] = useState(0);\n const [isPaused, setIsPaused] = useState(false);\n const [dragStart, setDragStart] = useState<number | null>(null);\n const [isDragging, setIsDragging] = useState(false);\n const containerRef = useRef<HTMLElement>(null);\n\n const defaultSlides: Slide[] = [\n {\n image: \"/images/placeholder.png\",\n title: t(\"slides.0.title\"),\n description: t(\"slides.0.description\"),\n primaryButton: { text: t(\"slides.0.primaryButton\"), link: \"/get-started\" },\n secondaryButton: { text: t(\"slides.0.secondaryButton\"), link: \"/learn-more\" },\n },\n {\n image: \"/images/placeholder.png\",\n title: t(\"slides.1.title\"),\n description: t(\"slides.1.description\"),\n primaryButton: { text: t(\"slides.1.primaryButton\"), link: \"/features\" },\n },\n {\n image: \"/images/placeholder.png\",\n title: t(\"slides.2.title\"),\n description: t(\"slides.2.description\"),\n primaryButton: { text: t(\"slides.2.primaryButton\"), link: \"/contact\" },\n secondaryButton: { text: t(\"slides.2.secondaryButton\"), link: \"/demo\" },\n },\n ];\n\n const displaySlides = slides ?? defaultSlides;\n\n const goToSlide = useCallback((index: number) => {\n setCurrentSlide(index);\n }, []);\n\n const nextSlide = useCallback(() => {\n setCurrentSlide((prev) => (prev + 1) % displaySlides.length);\n }, [displaySlides.length]);\n\n const prevSlide = useCallback(() => {\n setCurrentSlide((prev) => (prev - 1 + displaySlides.length) % displaySlides.length);\n }, [displaySlides.length]);\n\n // Auto-play\n useEffect(() => {\n if (!autoPlay || isPaused) return;\n\n const timer = setInterval(nextSlide, interval);\n return () => clearInterval(timer);\n }, [autoPlay, interval, isPaused, nextSlide]);\n\n // Mouse drag handlers\n const handleMouseDown = (e: React.MouseEvent) => {\n setIsDragging(true);\n setDragStart(e.clientX);\n };\n\n const handleMouseMove = (e: React.MouseEvent) => {\n if (!isDragging || dragStart === null) return;\n e.preventDefault();\n };\n\n const handleMouseUp = (e: React.MouseEvent) => {\n if (!isDragging || dragStart === null) {\n setIsDragging(false);\n return;\n }\n\n const diff = dragStart - e.clientX;\n\n if (Math.abs(diff) > 50) {\n if (diff > 0) {\n nextSlide();\n } else {\n prevSlide();\n }\n }\n\n setIsDragging(false);\n setDragStart(null);\n };\n\n const handleMouseLeave = () => {\n if (isDragging) {\n setIsDragging(false);\n setDragStart(null);\n }\n };\n\n // Touch handlers for swipe\n const handleTouchStart = (e: React.TouchEvent) => {\n setDragStart(e.touches[0].clientX);\n };\n\n const handleTouchEnd = (e: React.TouchEvent) => {\n if (dragStart === null) return;\n\n const touchEnd = e.changedTouches[0].clientX;\n const diff = dragStart - touchEnd;\n\n if (Math.abs(diff) > 50) {\n if (diff > 0) {\n nextSlide();\n } else {\n prevSlide();\n }\n }\n\n setDragStart(null);\n };\n\n // Wheel handler for trackpad two-finger swipe\n const handleWheel = useCallback((e: WheelEvent) => {\n // Only handle horizontal scroll (trackpad two-finger swipe)\n if (Math.abs(e.deltaX) > Math.abs(e.deltaY) && Math.abs(e.deltaX) > 30) {\n e.preventDefault();\n if (e.deltaX > 0) {\n nextSlide();\n } else {\n prevSlide();\n }\n }\n }, [nextSlide, prevSlide]);\n\n // Add wheel event listener\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n let lastWheelTime = 0;\n const throttledWheel = (e: WheelEvent) => {\n const now = Date.now();\n if (now - lastWheelTime > 500) {\n handleWheel(e);\n lastWheelTime = now;\n }\n };\n\n container.addEventListener(\"wheel\", throttledWheel, { passive: false });\n return () => container.removeEventListener(\"wheel\", throttledWheel);\n }, [handleWheel]);\n\n return (\n <section\n ref={containerRef}\n className={cn(\n \"relative w-full h-[500px] md:h-[600px] lg:h-[700px] overflow-hidden select-none\",\n isDragging ? \"cursor-grabbing\" : \"cursor-grab\",\n className\n )}\n onMouseEnter={() => { if (pauseOnHover) setIsPaused(true); }}\n onMouseLeave={() => {\n if (pauseOnHover) setIsPaused(false);\n handleMouseLeave();\n }}\n onMouseDown={handleMouseDown}\n onMouseMove={handleMouseMove}\n onMouseUp={handleMouseUp}\n onTouchStart={handleTouchStart}\n onTouchEnd={handleTouchEnd}\n >\n {/* Slides */}\n {displaySlides.map((slide, index) => (\n <div\n key={index}\n className={cn(\n \"absolute inset-0 transition-opacity duration-700\",\n index === currentSlide ? \"opacity-100 z-10\" : \"opacity-0 z-0\"\n )}\n >\n {/* Background Image */}\n <img\n src={slide.image}\n alt={slide.title}\n className=\"absolute inset-0 w-full h-full object-cover pointer-events-none\"\n draggable={false}\n />\n\n {/* Overlay */}\n <div className=\"absolute inset-0 bg-black/50\" />\n\n {/* Content */}\n <div className=\"relative z-10 h-full flex items-center\">\n <div className=\"container max-w-[var(--container-max-width)] mx-auto px-6 md:px-24 lg:px-32\">\n <div\n className={cn(\n \"max-w-2xl transition-all duration-700 delay-200\",\n index === currentSlide\n ? \"translate-y-0 opacity-100\"\n : \"translate-y-8 opacity-0\"\n )}\n >\n <h1 className=\"text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-4 md:mb-6\">\n {slide.title}\n </h1>\n <p className=\"text-lg md:text-xl text-white/90 mb-6 md:mb-8\">\n {slide.description}\n </p>\n <div className=\"flex flex-wrap gap-4\">\n {slide.primaryButton && (\n <Link to={slide.primaryButton.link}>\n <Button size=\"lg\" className=\"text-base\">\n {slide.primaryButton.text}\n </Button>\n </Link>\n )}\n {slide.secondaryButton && (\n <Link to={slide.secondaryButton.link}>\n <Button size=\"lg\" variant=\"outline\" className=\"text-base bg-white/10 border-white/30 text-white hover:bg-white/20\">\n {slide.secondaryButton.text}\n </Button>\n </Link>\n )}\n </div>\n </div>\n </div>\n </div>\n </div>\n ))}\n\n {/* Navigation Arrows */}\n {showArrows && (\n <>\n <button\n onClick={prevSlide}\n className=\"absolute left-4 top-1/2 -translate-y-1/2 z-20 w-12 h-12 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center text-white hover:bg-white/30 transition-colors\"\n aria-label=\"Previous slide\"\n >\n <ChevronLeft className=\"w-6 h-6\" />\n </button>\n <button\n onClick={nextSlide}\n className=\"absolute right-4 top-1/2 -translate-y-1/2 z-20 w-12 h-12 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center text-white hover:bg-white/30 transition-colors\"\n aria-label=\"Next slide\"\n >\n <ChevronRight className=\"w-6 h-6\" />\n </button>\n </>\n )}\n\n {/* Dots */}\n {showDots && (\n <div className=\"absolute bottom-6 left-1/2 -translate-x-1/2 z-20 flex gap-2\">\n {displaySlides.map((_, index) => (\n <button\n key={index}\n onClick={() => goToSlide(index)}\n className={cn(\n \"w-3 h-3 rounded-full transition-all duration-300\",\n index === currentSlide\n ? \"bg-white w-8\"\n : \"bg-white/50 hover:bg-white/70\"\n )}\n aria-label={`Go to slide ${index + 1}`}\n />\n ))}\n </div>\n )}\n </section>\n );\n}\n"
19
19
  },
20
20
  {
21
21
  "path": "hero-carousel/index.ts",
@@ -26,7 +26,7 @@
26
26
  "path": "login-page-split/login-page-split.tsx",
27
27
  "type": "registry:page",
28
28
  "target": "$modules$/login-page-split/login-page-split.tsx",
29
- "content": "import { useState } from \"react\";\r\nimport { Link, useNavigate, useLocation } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Checkbox } from \"@/components/ui/checkbox\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useAuthStore } from \"@/modules/auth-core/auth-store\";\r\nimport { customerClient } from \"@/modules/api/customer-client\";\r\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\r\n\r\ninterface LoginPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function LoginPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: LoginPageSplitProps) {\r\n const { t } = useTranslation(\"login-page-split\");\r\n const navigate = useNavigate();\r\n const location = useLocation();\r\n const setAuth = useAuthStore((state) => state.setAuth);\r\n\r\n const [email, setEmail] = useState(\"\");\r\n const [password, setPassword] = useState(\"\");\r\n const [rememberMe, setRememberMe] = useState(false);\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n // Get redirect URL from location state or default to home\r\n const from = (location.state as { from?: string })?.from || \"/\";\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n setIsLoading(true);\r\n\r\n try {\r\n const response = await customerClient.auth.login({\r\n username: email,\r\n password,\r\n expiresInMins: rememberMe ? 60 * 24 * 7 : undefined, // 7 days if remember me\r\n });\r\n\r\n // Set auth state\r\n setAuth(\r\n { username: email, email: (response as any).email || email },\r\n {\r\n accessToken: response.accessToken,\r\n refreshToken: response.refreshToken,\r\n idToken: response.idToken,\r\n encryptionKey: response.encryptionKey,\r\n expiresAt: response.expiresIn\r\n ? Date.now() + response.expiresIn * 1000\r\n : undefined,\r\n }\r\n );\r\n\r\n // Set token for API client\r\n customerClient.setToken(response.accessToken);\r\n\r\n toast.success(t(\"loginSuccess\", \"Login successful!\"));\r\n navigate(from, { replace: true });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"loginError\", \"Login failed. Please check your credentials.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6\">\r\n <Logo />\r\n <hr />\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Login\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"subtitle\", \"Enter your details below to login\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <form onSubmit={handleSubmit} className=\"grid gap-4\">\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"email-split\">{t(\"email\", \"Email\")}</Label>\r\n <Input\r\n required\r\n id=\"email-split\"\r\n type=\"email\"\r\n autoComplete=\"username\"\r\n placeholder={t(\"emailPlaceholder\", \"email@example.com\")}\r\n value={email}\r\n onChange={(e) => setEmail(e.target.value)}\r\n disabled={isLoading}\r\n />\r\n </div>\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"password-split\">{t(\"password\", \"Password\")}</Label>\r\n <Input\r\n required\r\n id=\"password-split\"\r\n type=\"password\"\r\n placeholder=\"••••••••••\"\r\n autoComplete=\"current-password\"\r\n value={password}\r\n onChange={(e) => setPassword(e.target.value)}\r\n disabled={isLoading}\r\n />\r\n </div>\r\n\r\n <div className=\"flex items-center gap-2\">\r\n <Checkbox\r\n id=\"remember-me\"\r\n checked={rememberMe}\r\n onCheckedChange={(checked) => setRememberMe(checked as boolean)}\r\n disabled={isLoading}\r\n />\r\n <Label htmlFor=\"remember-me\" className=\"text-sm font-normal cursor-pointer\">\r\n {t(\"rememberMe\", \"Remember me\")}\r\n </Label>\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"loggingIn\", \"Logging in...\")}\r\n </>\r\n ) : (\r\n t(\"login\", \"Login\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <div className=\"flex flex-col gap-4 text-sm\">\r\n <p>\r\n {t(\"noAccount\", \"Don't have an account?\")}{\" \"}\r\n <Link to=\"/register\" className=\"underline\">\r\n {t(\"signUp\", \"Sign up\")}\r\n </Link>\r\n </p>\r\n <Link to=\"/forgot-password\" className=\"underline\">\r\n {t(\"forgotPassword\", \"Forgot your password?\")}\r\n </Link>\r\n </div>\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Login background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default LoginPageSplit;\r\n"
29
+ "content": "import { useState } from \"react\";\r\nimport { Link, useNavigate, useLocation } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useAuthStore } from \"@/modules/auth-core/auth-store\";\r\nimport { customerClient } from \"@/modules/api/customer-client\";\r\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\r\n\r\ninterface LoginPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function LoginPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: LoginPageSplitProps) {\r\n const { t } = useTranslation(\"login-page-split\");\r\n const navigate = useNavigate();\r\n const location = useLocation();\r\n const setAuth = useAuthStore((state) => state.setAuth);\r\n\r\n const [email, setEmail] = useState(\"\");\r\n const [password, setPassword] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n // Get redirect URL from location state or default to home\r\n const from = (location.state as { from?: string })?.from || \"/\";\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n setIsLoading(true);\r\n\r\n try {\r\n const response = await customerClient.auth.login({\r\n username: email,\r\n password,\r\n });\r\n\r\n // Set auth state\r\n setAuth(\r\n { username: email, email: (response as any).email || email },\r\n {\r\n accessToken: response.accessToken,\r\n refreshToken: response.refreshToken,\r\n idToken: response.idToken,\r\n encryptionKey: response.encryptionKey,\r\n expiresAt: response.expiresIn\r\n ? Date.now() + response.expiresIn * 1000\r\n : undefined,\r\n }\r\n );\r\n\r\n // Set token for API client\r\n customerClient.setToken(response.accessToken);\r\n\r\n toast.success(t(\"loginSuccess\", \"Login successful!\"));\r\n navigate(from, { replace: true });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"loginError\", \"Login failed. Please check your credentials.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6\">\r\n <Logo />\r\n <hr />\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Login\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"subtitle\", \"Enter your details below to login\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <form onSubmit={handleSubmit} className=\"grid gap-4\">\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"email-split\">{t(\"email\", \"Email\")}</Label>\r\n <Input\r\n required\r\n id=\"email-split\"\r\n type=\"email\"\r\n autoComplete=\"username\"\r\n placeholder={t(\"emailPlaceholder\", \"email@example.com\")}\r\n value={email}\r\n onChange={(e) => setEmail(e.target.value)}\r\n disabled={isLoading}\r\n />\r\n </div>\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"password-split\">{t(\"password\", \"Password\")}</Label>\r\n <Input\r\n required\r\n id=\"password-split\"\r\n type=\"password\"\r\n placeholder=\"••••••••••\"\r\n autoComplete=\"current-password\"\r\n value={password}\r\n onChange={(e) => setPassword(e.target.value)}\r\n disabled={isLoading}\r\n />\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"loggingIn\", \"Logging in...\")}\r\n </>\r\n ) : (\r\n t(\"login\", \"Login\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <div className=\"flex flex-col gap-4 text-sm\">\r\n <p>\r\n {t(\"noAccount\", \"Don't have an account?\")}{\" \"}\r\n <Link to=\"/register\" className=\"underline\">\r\n {t(\"signUp\", \"Sign up\")}\r\n </Link>\r\n </p>\r\n <Link to=\"/forgot-password\" className=\"underline\">\r\n {t(\"forgotPassword\", \"Forgot your password?\")}\r\n </Link>\r\n </div>\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Login background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default LoginPageSplit;\r\n"
30
30
  },
31
31
  {
32
32
  "path": "login-page-split/lang/en.json",
@@ -18,7 +18,7 @@
18
18
  "path": "post-card/post-card.tsx",
19
19
  "type": "registry:component",
20
20
  "target": "$modules$/post-card/post-card.tsx",
21
- "content": "import { Link } from \"react-router\";\nimport { Calendar, Eye, Clock, ArrowRight } from \"lucide-react\";\nimport {\n Card,\n CardContent,\n CardFooter,\n CardHeader,\n} from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { useTranslation } from \"react-i18next\";\nimport type { Post } from \"@/modules/blog-core/types\";\n\ninterface PostCardProps {\n post: Post;\n layout?: \"grid\" | \"list\";\n showExcerpt?: boolean;\n className?: string;\n}\n\nexport function PostCard({\n post,\n layout = \"grid\",\n showExcerpt = true,\n className = \"\",\n}: PostCardProps) {\n const { t } = useTranslation(\"post-card\");\n\n const formatDate = (dateString: string) => {\n return new Date(dateString).toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n });\n };\n\n const CardWrapper = ({ children }: { children: React.ReactNode }) => (\n <Card\n className={`group overflow-hidden border-0 p-0 shadow-sm hover:shadow-lg transition-all duration-300 h-full ${className}`}\n >\n {children}\n </Card>\n );\n\n if (layout === \"list\") {\n return (\n <CardWrapper>\n <div className=\"flex flex-col md:flex-row\">\n {post.featured_image && (\n <div className=\"md:w-1/3 flex-shrink-0\">\n <Link to={`/blog/${post.slug}`}>\n <img\n src={post.featured_image}\n alt={post.title}\n className=\"w-full h-48 md:h-full object-cover\"\n onError={(e) => {\n e.currentTarget.src = \"/images/placeholder.png\";\n }}\n />\n </Link>\n </div>\n )}\n\n <div className=\"flex-1 flex flex-col\">\n <CardHeader className=\"pt-6 pb-3\">\n {post.categories && post.categories.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mb-2\">\n {post.categories.slice(0, 2).map((category) => (\n <Badge\n key={category.slug}\n variant=\"secondary\"\n className=\"text-xs\"\n >\n <Link to={`/blog?categories=${category.slug}`}>\n {category.name}\n </Link>\n </Badge>\n ))}\n </div>\n )}\n\n <h3 className=\"text-xl font-semibold line-clamp-2 group-hover:text-primary transition-colors\">\n <Link to={`/blog/${post.slug}`}>{post.title}</Link>\n </h3>\n </CardHeader>\n\n <CardContent className=\"flex-1 pb-3\">\n {showExcerpt && post.excerpt && (\n <p className=\"text-muted-foreground text-sm line-clamp-3 mb-4\">\n {post.excerpt}\n </p>\n )}\n\n <div className=\"flex flex-wrap items-center gap-4 text-xs text-muted-foreground\">\n <div className=\"flex items-center gap-1\">\n <Calendar className=\"h-3 w-3\" />\n <span>{formatDate(post.published_at)}</span>\n </div>\n\n {post.read_time > 0 && (\n <div className=\"flex items-center gap-1\">\n <Clock className=\"h-3 w-3\" />\n <span>\n {post.read_time} {t(\"minRead\", \"min\")}\n </span>\n </div>\n )}\n\n {post.view_count > 0 && (\n <div className=\"flex items-center gap-1\">\n <Eye className=\"h-3 w-3\" />\n <span>{post.view_count.toLocaleString()}</span>\n </div>\n )}\n </div>\n </CardContent>\n\n <CardFooter className=\"pt-0 pb-6\">\n <Button variant=\"ghost\" size=\"sm\" asChild>\n <Link to={`/blog/${post.slug}`}>\n {t(\"readMore\", \"Read More\")}\n <ArrowRight className=\"h-3 w-3 ml-1\" />\n </Link>\n </Button>\n </CardFooter>\n </div>\n </div>\n </CardWrapper>\n );\n }\n\n return (\n <CardWrapper>\n <div className=\"flex flex-col h-full\">\n {post.featured_image && (\n <div className=\"aspect-video overflow-hidden\">\n <Link to={`/blog/${post.slug}`}>\n <img\n src={post.featured_image}\n alt={post.title}\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300\"\n onError={(e) => {\n e.currentTarget.src = \"/images/placeholder.png\";\n }}\n />\n </Link>\n </div>\n )}\n\n <div className=\"flex flex-col flex-1\">\n <CardHeader className=\"pt-6 pb-3\">\n {post.categories && post.categories.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mb-2\">\n {post.categories.slice(0, 2).map((category) => (\n <Badge\n key={category.slug}\n variant=\"secondary\"\n className=\"text-xs\"\n >\n <Link to={`/blog?categories=${category.slug}`}>\n {category.name}\n </Link>\n </Badge>\n ))}\n </div>\n )}\n\n <h3 className=\"text-lg font-semibold line-clamp-2 group-hover:text-primary transition-colors\">\n <Link to={`/blog/${post.slug}`}>{post.title}</Link>\n </h3>\n </CardHeader>\n\n <CardContent className=\"flex-1 pb-3\">\n {showExcerpt && post.excerpt && (\n <p className=\"text-muted-foreground text-sm line-clamp-3 mb-4\">\n {post.excerpt}\n </p>\n )}\n\n <div className=\"flex flex-wrap items-center gap-3 text-xs text-muted-foreground\">\n <div className=\"flex items-center gap-1\">\n <Calendar className=\"h-3 w-3\" />\n <span>{formatDate(post.published_at)}</span>\n </div>\n\n {post.read_time > 0 && (\n <div className=\"flex items-center gap-1\">\n <Clock className=\"h-3 w-3\" />\n <span>\n {post.read_time} {t(\"minRead\", \"min\")}\n </span>\n </div>\n )}\n </div>\n </CardContent>\n\n <CardFooter className=\"pt-0 pb-6 mt-auto\">\n <div className=\"flex items-center justify-between w-full\">\n <Button variant=\"ghost\" size=\"sm\" asChild>\n <Link to={`/blog/${post.slug}`}>\n {t(\"readMore\", \"Read More\")}\n <ArrowRight className=\"h-3 w-3 ml-1\" />\n </Link>\n </Button>\n\n {post.view_count > 0 && (\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n <Eye className=\"h-3 w-3\" />\n <span>{post.view_count.toLocaleString()}</span>\n </div>\n )}\n </div>\n </CardFooter>\n </div>\n </div>\n </CardWrapper>\n );\n}\n"
21
+ "content": "import { Link } from \"react-router\";\nimport { Calendar, Eye, Clock, ArrowRight } from \"lucide-react\";\nimport {\n Card,\n CardContent,\n CardFooter,\n CardHeader,\n} from \"@/components/ui/card\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { useTranslation } from \"react-i18next\";\nimport type { Post } from \"@/modules/blog-core/types\";\n\ninterface CardWrapperProps {\n children: React.ReactNode;\n className?: string;\n}\n\nfunction CardWrapper({ children, className = \"\" }: CardWrapperProps) {\n return (\n <Card\n className={`group overflow-hidden border-0 p-0 shadow-sm hover:shadow-lg transition-all duration-300 h-full ${className}`}\n >\n {children}\n </Card>\n );\n}\n\ninterface PostCardProps {\n post: Post;\n layout?: \"grid\" | \"list\";\n showExcerpt?: boolean;\n className?: string;\n}\n\nexport function PostCard({\n post,\n layout = \"grid\",\n showExcerpt = true,\n className = \"\",\n}: PostCardProps) {\n const { t } = useTranslation(\"post-card\");\n\n const formatDate = (dateString: string) => {\n return new Date(dateString).toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n });\n };\n\n if (layout === \"list\") {\n return (\n <CardWrapper className={className}>\n <div className=\"flex flex-col md:flex-row\">\n {post.featured_image && (\n <div className=\"md:w-1/3 flex-shrink-0\">\n <Link to={`/blog/${post.slug}`}>\n <img\n src={post.featured_image}\n alt={post.title}\n className=\"w-full h-48 md:h-full object-cover\"\n onError={(e) => {\n e.currentTarget.src = \"/images/placeholder.png\";\n }}\n />\n </Link>\n </div>\n )}\n\n <div className=\"flex-1 flex flex-col\">\n <CardHeader className=\"pt-6 pb-3\">\n {post.categories && post.categories.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mb-2\">\n {post.categories.slice(0, 2).map((category) => (\n <Badge\n key={category.slug}\n variant=\"secondary\"\n className=\"text-xs\"\n >\n <Link to={`/blog?categories=${category.slug}`}>\n {category.name}\n </Link>\n </Badge>\n ))}\n </div>\n )}\n\n <h3 className=\"text-xl font-semibold line-clamp-2 group-hover:text-primary transition-colors\">\n <Link to={`/blog/${post.slug}`}>{post.title}</Link>\n </h3>\n </CardHeader>\n\n <CardContent className=\"flex-1 pb-3\">\n {showExcerpt && post.excerpt && (\n <p className=\"text-muted-foreground text-sm line-clamp-3 mb-4\">\n {post.excerpt}\n </p>\n )}\n\n <div className=\"flex flex-wrap items-center gap-4 text-xs text-muted-foreground\">\n <div className=\"flex items-center gap-1\">\n <Calendar className=\"h-3 w-3\" />\n <span>{formatDate(post.published_at)}</span>\n </div>\n\n {post.read_time > 0 && (\n <div className=\"flex items-center gap-1\">\n <Clock className=\"h-3 w-3\" />\n <span>\n {post.read_time} {t(\"minRead\", \"min\")}\n </span>\n </div>\n )}\n\n {post.view_count > 0 && (\n <div className=\"flex items-center gap-1\">\n <Eye className=\"h-3 w-3\" />\n <span>{post.view_count.toLocaleString()}</span>\n </div>\n )}\n </div>\n </CardContent>\n\n <CardFooter className=\"pt-0 pb-6\">\n <Button variant=\"ghost\" size=\"sm\" asChild>\n <Link to={`/blog/${post.slug}`}>\n {t(\"readMore\", \"Read More\")}\n <ArrowRight className=\"h-3 w-3 ml-1\" />\n </Link>\n </Button>\n </CardFooter>\n </div>\n </div>\n </CardWrapper>\n );\n }\n\n return (\n <CardWrapper className={className}>\n <div className=\"flex flex-col h-full\">\n {post.featured_image && (\n <div className=\"aspect-video overflow-hidden\">\n <Link to={`/blog/${post.slug}`}>\n <img\n src={post.featured_image}\n alt={post.title}\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300\"\n onError={(e) => {\n e.currentTarget.src = \"/images/placeholder.png\";\n }}\n />\n </Link>\n </div>\n )}\n\n <div className=\"flex flex-col flex-1\">\n <CardHeader className=\"pt-6 pb-3\">\n {post.categories && post.categories.length > 0 && (\n <div className=\"flex flex-wrap gap-1 mb-2\">\n {post.categories.slice(0, 2).map((category) => (\n <Badge\n key={category.slug}\n variant=\"secondary\"\n className=\"text-xs\"\n >\n <Link to={`/blog?categories=${category.slug}`}>\n {category.name}\n </Link>\n </Badge>\n ))}\n </div>\n )}\n\n <h3 className=\"text-lg font-semibold line-clamp-2 group-hover:text-primary transition-colors\">\n <Link to={`/blog/${post.slug}`}>{post.title}</Link>\n </h3>\n </CardHeader>\n\n <CardContent className=\"flex-1 pb-3\">\n {showExcerpt && post.excerpt && (\n <p className=\"text-muted-foreground text-sm line-clamp-3 mb-4\">\n {post.excerpt}\n </p>\n )}\n\n <div className=\"flex flex-wrap items-center gap-3 text-xs text-muted-foreground\">\n <div className=\"flex items-center gap-1\">\n <Calendar className=\"h-3 w-3\" />\n <span>{formatDate(post.published_at)}</span>\n </div>\n\n {post.read_time > 0 && (\n <div className=\"flex items-center gap-1\">\n <Clock className=\"h-3 w-3\" />\n <span>\n {post.read_time} {t(\"minRead\", \"min\")}\n </span>\n </div>\n )}\n </div>\n </CardContent>\n\n <CardFooter className=\"pt-0 pb-6 mt-auto\">\n <div className=\"flex items-center justify-between w-full\">\n <Button variant=\"ghost\" size=\"sm\" asChild>\n <Link to={`/blog/${post.slug}`}>\n {t(\"readMore\", \"Read More\")}\n <ArrowRight className=\"h-3 w-3 ml-1\" />\n </Link>\n </Button>\n\n {post.view_count > 0 && (\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n <Eye className=\"h-3 w-3\" />\n <span>{post.view_count.toLocaleString()}</span>\n </div>\n )}\n </div>\n </CardFooter>\n </div>\n </div>\n </CardWrapper>\n );\n}\n"
22
22
  },
23
23
  {
24
24
  "path": "post-card/lang/en.json",
@@ -24,7 +24,7 @@
24
24
  "path": "products-page/products-page.tsx",
25
25
  "type": "registry:page",
26
26
  "target": "$modules$/products-page/products-page.tsx",
27
- "content": "import { useState, useEffect, useRef, useCallback } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Filter, Grid, List } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { FadeIn } from \"@/modules/animations\";\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 { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useProducts, useCategories } from \"@/modules/ecommerce-core\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\n\nexport function ProductsPage() {\n const { t } = useTranslation(\"products-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Products\") });\n const { products, loading: productsLoading } = useProducts();\n const { categories, loading: categoriesLoading } = useCategories();\n const loading = productsLoading || categoriesLoading;\n\n const [searchParams, setSearchParams] = useSearchParams();\n const [viewMode, setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\n const [sortBy, setSortBy] = useState(\"featured\");\n const [selectedCategories, setSelectedCategories] = useState<string[]>([]);\n const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);\n const [filteredProducts, setFilteredProducts] = useState<Product[]>([]);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const minPriceRef = useRef<HTMLInputElement>(null);\n const maxPriceRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n const query = searchParams.get(\"search\") || \"\";\n const categorySlug = searchParams.get(\"category\");\n setSearchQuery(query);\n if (categorySlug) {\n setSelectedCategories([categorySlug]);\n }\n }, [searchParams]);\n\n useEffect(() => {\n const minPrice = parseFloat(searchParams.get(\"minPrice\") || \"0\") || 0;\n const maxPrice =\n parseFloat(searchParams.get(\"maxPrice\") || \"999999\") || 999999;\n\n let filtered = products.filter((product) => {\n const currentPrice =\n product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n return currentPrice >= minPrice && currentPrice <= maxPrice;\n });\n\n if (selectedCategories.length > 0) {\n filtered = filtered.filter((product) => {\n return selectedCategories.some((selectedCategory) => {\n if (product.category === selectedCategory) return true;\n return product.categories?.some(\n (cat) => cat.slug === selectedCategory\n );\n });\n });\n }\n\n if (selectedFeatures.length > 0) {\n filtered = filtered.filter((product) => {\n return selectedFeatures.every((feature) => {\n switch (feature) {\n case \"on_sale\":\n return product.on_sale;\n case \"is_new\":\n return product.is_new;\n case \"featured\":\n return product.featured;\n case \"in_stock\":\n return product.stock > 0;\n default:\n return true;\n }\n });\n });\n }\n\n // Apply sorting\n const sorted = [...filtered].sort((a, b) => {\n switch (sortBy) {\n case \"price-low\":\n return (\n (a.on_sale ? a.sale_price || a.price : a.price) -\n (b.on_sale ? b.sale_price || b.price : b.price)\n );\n case \"price-high\":\n return (\n (b.on_sale ? b.sale_price || b.price : b.price) -\n (a.on_sale ? a.sale_price || a.price : a.price)\n );\n case \"newest\":\n return (\n new Date(b.created_at || 0).getTime() -\n new Date(a.created_at || 0).getTime()\n );\n case \"featured\":\n default:\n return (b.featured ? 1 : 0) - (a.featured ? 1 : 0);\n }\n });\n\n setFilteredProducts(sorted);\n }, [products, searchParams, selectedFeatures, selectedCategories, sortBy]);\n\n const handlePriceFilter = useCallback(() => {\n const minPrice = minPriceRef.current?.value || \"\";\n const maxPrice = maxPriceRef.current?.value || \"\";\n const params = new URLSearchParams(searchParams);\n if (minPrice) params.set(\"minPrice\", minPrice);\n else params.delete(\"minPrice\");\n if (maxPrice) params.set(\"maxPrice\", maxPrice);\n else params.delete(\"maxPrice\");\n setSearchParams(params);\n }, [searchParams, setSearchParams]);\n\n const handleCategoryChange = useCallback(\n (category: string, checked: boolean) => {\n if (checked) {\n setSelectedCategories((prev) => [...prev, category]);\n } else {\n setSelectedCategories((prev) => prev.filter((c) => c !== category));\n }\n },\n []\n );\n\n const handleFeatureChange = useCallback(\n (feature: string, checked: boolean) => {\n if (checked) {\n setSelectedFeatures((prev) => [...prev, feature]);\n } else {\n setSelectedFeatures((prev) => prev.filter((f) => f !== feature));\n }\n },\n []\n );\n\n const sortOptions = [\n { value: \"featured\", label: t(\"featured\", \"Featured\") },\n { value: \"price-low\", label: t(\"sortPriceLow\", \"Price: Low to High\") },\n { value: \"price-high\", label: t(\"sortPriceHigh\", \"Price: High to Low\") },\n { value: \"newest\", label: t(\"sortNewest\", \"Newest\") },\n ];\n\n const FilterSidebar = () => (\n <div className=\"space-y-6\">\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"categories\", \"Categories\")}\n </h3>\n <div className=\"space-y-3\">\n {categories.map((category) => (\n <div\n key={category.id}\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\n >\n <Checkbox\n id={`category-${category.id}`}\n checked={selectedCategories.includes(category.slug)}\n onCheckedChange={(checked) =>\n handleCategoryChange(category.slug, checked as boolean)\n }\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\n />\n <label\n htmlFor={`category-${category.id}`}\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\n >\n {category.name}\n </label>\n </div>\n ))}\n </div>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"priceRange\", \"Price Range\")}\n </h3>\n <div className=\"space-y-3 p-3 bg-muted/30 rounded-lg\">\n <div className=\"grid grid-cols-2 gap-3\">\n <input\n ref={minPriceRef}\n type=\"number\"\n placeholder={t(\"minPrice\", \"Min\")}\n defaultValue={searchParams.get(\"minPrice\") || \"\"}\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\n />\n <input\n ref={maxPriceRef}\n type=\"number\"\n placeholder={t(\"maxPrice\", \"Max\")}\n defaultValue={searchParams.get(\"maxPrice\") || \"\"}\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\n />\n </div>\n </div>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"features\", \"Features\")}\n </h3>\n <div className=\"space-y-3\">\n {[\n { key: \"on_sale\", label: t(\"onSale\", \"On Sale\") },\n { key: \"is_new\", label: t(\"newArrivals\", \"New Arrivals\") },\n { key: \"featured\", label: t(\"featuredLabel\", \"Featured\") },\n { key: \"in_stock\", label: t(\"inStock\", \"In Stock\") },\n ].map((feature) => (\n <div\n key={feature.key}\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\n >\n <Checkbox\n id={feature.key}\n checked={selectedFeatures.includes(feature.key)}\n onCheckedChange={(checked) =>\n handleFeatureChange(feature.key, checked as boolean)\n }\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\n />\n <label\n htmlFor={feature.key}\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\n >\n {feature.label}\n </label>\n </div>\n ))}\n </div>\n </div>\n </div>\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=\"mb-8\">\n <div className=\"flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6\">\n <div className=\"space-y-1\">\n <h1 className=\"text-2xl lg:text-3xl font-bold\">\n {searchQuery\n ? t(\"searchResultsFor\", `Search Results for \"${searchQuery}\"`)\n : t(\"allProducts\", \"All Products\")}\n </h1>\n <p className=\"text-sm lg:text-base text-muted-foreground\">\n {t(\"showing\", \"Showing\")} {filteredProducts.length}{\" \"}\n {t(\"of\", \"of\")} {products.length} {t(\"products\", \"products\")}\n </p>\n </div>\n {searchQuery && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setSearchQuery(\"\");\n setSearchParams({});\n }}\n className=\"w-fit\"\n >\n {t(\"clearSearch\", \"Clear Search\")}\n </Button>\n )}\n </div>\n\n <div className=\"flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between\">\n <Sheet>\n <SheetTrigger asChild>\n <Button\n variant=\"outline\"\n className=\"lg:hidden w-full sm:w-auto\"\n >\n <Filter className=\"h-4 w-4 mr-2\" />\n {t(\"filters\", \"Filters\")}\n </Button>\n </SheetTrigger>\n <SheetContent side=\"left\" className=\"w-[300px]\">\n <SheetHeader>\n <SheetTitle>{t(\"filters\", \"Filters\")}</SheetTitle>\n <SheetDescription>\n {t(\"refineSearch\", \"Refine your product search\")}\n </SheetDescription>\n </SheetHeader>\n <div className=\"mt-6\">\n <FilterSidebar />\n </div>\n </SheetContent>\n </Sheet>\n\n <div className=\"flex flex-col sm:flex-row items-stretch sm:items-center gap-3\">\n <Select value={sortBy} onValueChange={setSortBy}>\n <SelectTrigger className=\"w-full sm:w-[160px]\">\n <SelectValue placeholder={t(\"sortBy\", \"Sort by\")} />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n\n <div className=\"flex border rounded-lg p-1 w-full sm:w-auto\">\n <Button\n variant={viewMode === \"grid\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => setViewMode(\"grid\")}\n className=\"flex-1 sm:flex-none\"\n >\n <Grid className=\"h-4 w-4\" />\n </Button>\n <Button\n variant={viewMode === \"list\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => setViewMode(\"list\")}\n className=\"flex-1 sm:flex-none\"\n >\n <List className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n </div>\n </FadeIn>\n\n <div className=\"flex gap-8\">\n <aside className=\"hidden lg:block w-64 flex-shrink-0\">\n <div className=\"sticky top-24\">\n <FilterSidebar />\n </div>\n </aside>\n\n <div className=\"flex-1\">\n {loading ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\n {[...Array(6)].map((_, i) => (\n <div\n key={i}\n className=\"animate-pulse bg-card rounded-lg shadow-md overflow-hidden\"\n >\n <div className=\"aspect-square bg-muted mb-4\"></div>\n <div className=\"p-4\">\n <div className=\"h-4 bg-muted rounded w-3/4 mb-2\"></div>\n <div className=\"h-3 bg-muted rounded w-1/2 mb-3\"></div>\n <div className=\"h-4 bg-muted rounded w-1/3\"></div>\n </div>\n </div>\n ))}\n </div>\n ) : viewMode === \"grid\" ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\n {filteredProducts.map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n variant=\"grid\"\n />\n ))}\n </div>\n ) : (\n <div className=\"space-y-6\">\n {filteredProducts.map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n variant=\"list\"\n />\n ))}\n </div>\n )}\n\n {!loading && filteredProducts.length === 0 && (\n <div className=\"text-center py-12\">\n <p className=\"text-muted-foreground\">\n {t(\n \"noProductsFound\",\n \"No products found matching your criteria.\"\n )}\n </p>\n </div>\n )}\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ProductsPage;\n"
27
+ "content": "import { useState, useRef, useCallback, useMemo } from \"react\";\nimport { useSearchParams } from \"react-router\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Filter, Grid, List } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { FadeIn } from \"@/modules/animations\";\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 { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useProducts, useCategories } from \"@/modules/ecommerce-core\";\nimport type { Product, Category } from \"@/modules/ecommerce-core/types\";\n\ninterface FilterSidebarProps {\n t: (key: string, fallback?: string) => string;\n categories: Category[];\n selectedCategories: string[];\n handleCategoryChange: (category: string, checked: boolean) => void;\n selectedFeatures: string[];\n handleFeatureChange: (feature: string, checked: boolean) => void;\n minPriceRef: React.RefObject<HTMLInputElement | null>;\n maxPriceRef: React.RefObject<HTMLInputElement | null>;\n searchParams: URLSearchParams;\n handlePriceFilter: () => void;\n}\n\nfunction FilterSidebar({\n t,\n categories,\n selectedCategories,\n handleCategoryChange,\n selectedFeatures,\n handleFeatureChange,\n minPriceRef,\n maxPriceRef,\n searchParams,\n handlePriceFilter,\n}: FilterSidebarProps) {\n return (\n <div className=\"space-y-6\">\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"categories\", \"Categories\")}\n </h3>\n <div className=\"space-y-3\">\n {categories.map((category) => (\n <div\n key={category.id}\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\n >\n <Checkbox\n id={`category-${category.id}`}\n checked={selectedCategories.includes(category.slug)}\n onCheckedChange={(checked) =>\n handleCategoryChange(category.slug, checked as boolean)\n }\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\n />\n <label\n htmlFor={`category-${category.id}`}\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\n >\n {category.name}\n </label>\n </div>\n ))}\n </div>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"priceRange\", \"Price Range\")}\n </h3>\n <div className=\"space-y-3 p-3 bg-muted/30 rounded-lg\">\n <div className=\"grid grid-cols-2 gap-3\">\n <input\n ref={minPriceRef}\n type=\"number\"\n placeholder={t(\"minPrice\", \"Min\")}\n defaultValue={searchParams.get(\"minPrice\") || \"\"}\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\n />\n <input\n ref={maxPriceRef}\n type=\"number\"\n placeholder={t(\"maxPrice\", \"Max\")}\n defaultValue={searchParams.get(\"maxPrice\") || \"\"}\n onKeyDown={(e) => e.key === \"Enter\" && handlePriceFilter()}\n className=\"w-full px-3 py-2 border border-input rounded-lg text-sm bg-background\"\n />\n </div>\n </div>\n </div>\n\n <div>\n <h3 className=\"font-semibold mb-4 text-base\">\n {t(\"features\", \"Features\")}\n </h3>\n <div className=\"space-y-3\">\n {[\n { key: \"on_sale\", label: t(\"onSale\", \"On Sale\") },\n { key: \"is_new\", label: t(\"newArrivals\", \"New Arrivals\") },\n { key: \"featured\", label: t(\"featuredLabel\", \"Featured\") },\n { key: \"in_stock\", label: t(\"inStock\", \"In Stock\") },\n ].map((feature) => (\n <div\n key={feature.key}\n className=\"flex items-center space-x-3 p-2 rounded-lg hover:bg-muted/50 transition-colors\"\n >\n <Checkbox\n id={feature.key}\n checked={selectedFeatures.includes(feature.key)}\n onCheckedChange={(checked) =>\n handleFeatureChange(feature.key, checked as boolean)\n }\n className=\"data-[state=checked]:bg-primary data-[state=checked]:border-primary\"\n />\n <label\n htmlFor={feature.key}\n className=\"text-sm font-medium leading-none cursor-pointer flex-1\"\n >\n {feature.label}\n </label>\n </div>\n ))}\n </div>\n </div>\n </div>\n );\n}\n\nexport function ProductsPage() {\n const { t } = useTranslation(\"products-page\");\n usePageTitle({ title: t(\"pageTitle\", \"Products\") });\n const { products, loading: productsLoading } = useProducts();\n const { categories, loading: categoriesLoading } = useCategories();\n const loading = productsLoading || categoriesLoading;\n\n const [searchParams, setSearchParams] = useSearchParams();\n const [viewMode, setViewMode] = useState<\"grid\" | \"list\">(\"grid\");\n const [sortBy, setSortBy] = useState(\"featured\");\n const [selectedCategories, setSelectedCategories] = useState<string[]>(() => {\n const categorySlug = searchParams.get(\"category\");\n return categorySlug ? [categorySlug] : [];\n });\n const [selectedFeatures, setSelectedFeatures] = useState<string[]>([]);\n const searchQuery = searchParams.get(\"search\") || \"\";\n const minPriceRef = useRef<HTMLInputElement>(null);\n const maxPriceRef = useRef<HTMLInputElement>(null);\n\n const filteredProducts = useMemo(() => {\n const minPrice = parseFloat(searchParams.get(\"minPrice\") || \"0\") || 0;\n const maxPrice =\n parseFloat(searchParams.get(\"maxPrice\") || \"999999\") || 999999;\n\n let filtered = products.filter((product) => {\n const currentPrice =\n product.on_sale && product.sale_price\n ? product.sale_price\n : product.price;\n return currentPrice >= minPrice && currentPrice <= maxPrice;\n });\n\n if (selectedCategories.length > 0) {\n filtered = filtered.filter((product) => {\n return selectedCategories.some((selectedCategory) => {\n if (product.category === selectedCategory) return true;\n return product.categories?.some(\n (cat) => cat.slug === selectedCategory\n );\n });\n });\n }\n\n if (selectedFeatures.length > 0) {\n filtered = filtered.filter((product) => {\n return selectedFeatures.every((feature) => {\n switch (feature) {\n case \"on_sale\":\n return product.on_sale;\n case \"is_new\":\n return product.is_new;\n case \"featured\":\n return product.featured;\n case \"in_stock\":\n return product.stock > 0;\n default:\n return true;\n }\n });\n });\n }\n\n // Apply sorting\n return [...filtered].sort((a, b) => {\n switch (sortBy) {\n case \"price-low\":\n return (\n (a.on_sale ? a.sale_price || a.price : a.price) -\n (b.on_sale ? b.sale_price || b.price : b.price)\n );\n case \"price-high\":\n return (\n (b.on_sale ? b.sale_price || b.price : b.price) -\n (a.on_sale ? a.sale_price || a.price : a.price)\n );\n case \"newest\":\n return (\n new Date(b.created_at || 0).getTime() -\n new Date(a.created_at || 0).getTime()\n );\n case \"featured\":\n default:\n return (b.featured ? 1 : 0) - (a.featured ? 1 : 0);\n }\n });\n }, [products, searchParams, selectedFeatures, selectedCategories, sortBy]);\n\n const handlePriceFilter = useCallback(() => {\n const minPrice = minPriceRef.current?.value || \"\";\n const maxPrice = maxPriceRef.current?.value || \"\";\n const params = new URLSearchParams(searchParams);\n if (minPrice) params.set(\"minPrice\", minPrice);\n else params.delete(\"minPrice\");\n if (maxPrice) params.set(\"maxPrice\", maxPrice);\n else params.delete(\"maxPrice\");\n setSearchParams(params);\n }, [searchParams, setSearchParams]);\n\n const handleCategoryChange = useCallback(\n (category: string, checked: boolean) => {\n if (checked) {\n setSelectedCategories((prev) => [...prev, category]);\n } else {\n setSelectedCategories((prev) => prev.filter((c) => c !== category));\n }\n },\n []\n );\n\n const handleFeatureChange = useCallback(\n (feature: string, checked: boolean) => {\n if (checked) {\n setSelectedFeatures((prev) => [...prev, feature]);\n } else {\n setSelectedFeatures((prev) => prev.filter((f) => f !== feature));\n }\n },\n []\n );\n\n const sortOptions = [\n { value: \"featured\", label: t(\"featured\", \"Featured\") },\n { value: \"price-low\", label: t(\"sortPriceLow\", \"Price: Low to High\") },\n { value: \"price-high\", label: t(\"sortPriceHigh\", \"Price: High to Low\") },\n { value: \"newest\", label: t(\"sortNewest\", \"Newest\") },\n ];\n\n const filterSidebarProps: FilterSidebarProps = {\n t,\n categories,\n selectedCategories,\n handleCategoryChange,\n selectedFeatures,\n handleFeatureChange,\n minPriceRef,\n maxPriceRef,\n searchParams,\n handlePriceFilter,\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=\"mb-8\">\n <div className=\"flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4 mb-6\">\n <div className=\"space-y-1\">\n <h1 className=\"text-2xl lg:text-3xl font-bold\">\n {searchQuery\n ? t(\"searchResultsFor\", `Search Results for \"${searchQuery}\"`)\n : t(\"allProducts\", \"All Products\")}\n </h1>\n <p className=\"text-sm lg:text-base text-muted-foreground\">\n {t(\"showing\", \"Showing\")} {filteredProducts.length}{\" \"}\n {t(\"of\", \"of\")} {products.length} {t(\"products\", \"products\")}\n </p>\n </div>\n {searchQuery && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => setSearchParams({})}\n className=\"w-fit\"\n >\n {t(\"clearSearch\", \"Clear Search\")}\n </Button>\n )}\n </div>\n\n <div className=\"flex flex-col sm:flex-row gap-3 items-stretch sm:items-center justify-between\">\n <Sheet>\n <SheetTrigger asChild>\n <Button\n variant=\"outline\"\n className=\"lg:hidden w-full sm:w-auto\"\n >\n <Filter className=\"h-4 w-4 mr-2\" />\n {t(\"filters\", \"Filters\")}\n </Button>\n </SheetTrigger>\n <SheetContent side=\"left\" className=\"w-[300px]\">\n <SheetHeader>\n <SheetTitle>{t(\"filters\", \"Filters\")}</SheetTitle>\n <SheetDescription>\n {t(\"refineSearch\", \"Refine your product search\")}\n </SheetDescription>\n </SheetHeader>\n <div className=\"mt-6\">\n <FilterSidebar {...filterSidebarProps} />\n </div>\n </SheetContent>\n </Sheet>\n\n <div className=\"flex flex-col sm:flex-row items-stretch sm:items-center gap-3\">\n <Select value={sortBy} onValueChange={setSortBy}>\n <SelectTrigger className=\"w-full sm:w-[160px]\">\n <SelectValue placeholder={t(\"sortBy\", \"Sort by\")} />\n </SelectTrigger>\n <SelectContent>\n {sortOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n\n <div className=\"flex border rounded-lg p-1 w-full sm:w-auto\">\n <Button\n variant={viewMode === \"grid\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => setViewMode(\"grid\")}\n className=\"flex-1 sm:flex-none\"\n >\n <Grid className=\"h-4 w-4\" />\n </Button>\n <Button\n variant={viewMode === \"list\" ? \"default\" : \"ghost\"}\n size=\"sm\"\n onClick={() => setViewMode(\"list\")}\n className=\"flex-1 sm:flex-none\"\n >\n <List className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n </div>\n </FadeIn>\n\n <div className=\"flex gap-8\">\n <aside className=\"hidden lg:block w-64 flex-shrink-0\">\n <div className=\"sticky top-24\">\n <FilterSidebar {...filterSidebarProps} />\n </div>\n </aside>\n\n <div className=\"flex-1\">\n {loading ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\n {[...Array(6)].map((_, i) => (\n <div\n key={i}\n className=\"animate-pulse bg-card rounded-lg shadow-md overflow-hidden\"\n >\n <div className=\"aspect-square bg-muted mb-4\"></div>\n <div className=\"p-4\">\n <div className=\"h-4 bg-muted rounded w-3/4 mb-2\"></div>\n <div className=\"h-3 bg-muted rounded w-1/2 mb-3\"></div>\n <div className=\"h-4 bg-muted rounded w-1/3\"></div>\n </div>\n </div>\n ))}\n </div>\n ) : viewMode === \"grid\" ? (\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8\">\n {filteredProducts.map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n variant=\"grid\"\n />\n ))}\n </div>\n ) : (\n <div className=\"space-y-6\">\n {filteredProducts.map((product) => (\n <ProductCard\n key={product.id}\n product={product}\n variant=\"list\"\n />\n ))}\n </div>\n )}\n\n {!loading && filteredProducts.length === 0 && (\n <div className=\"text-center py-12\">\n <p className=\"text-muted-foreground\">\n {t(\n \"noProductsFound\",\n \"No products found matching your criteria.\"\n )}\n </p>\n </div>\n )}\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ProductsPage;\n"
28
28
  },
29
29
  {
30
30
  "path": "products-page/lang/en.json",
@@ -13,7 +13,7 @@
13
13
  "path": "reading-progress/reading-progress.tsx",
14
14
  "type": "registry:component",
15
15
  "target": "$modules$/reading-progress/reading-progress.tsx",
16
- "content": "\"use client\";\r\n\r\nimport { useEffect, useState } from \"react\";\r\nimport { motion, useScroll, useSpring } from \"motion/react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface ReadingProgressProps {\r\n color?: string;\r\n height?: number;\r\n showPercentage?: boolean;\r\n className?: string;\r\n}\r\n\r\nexport function ReadingProgress({\r\n color,\r\n height = 3,\r\n showPercentage = false,\r\n className,\r\n}: ReadingProgressProps) {\r\n const [mounted, setMounted] = useState(false);\r\n const { scrollYProgress } = useScroll();\r\n const scaleX = useSpring(scrollYProgress, {\r\n stiffness: 100,\r\n damping: 30,\r\n restDelta: 0.001,\r\n });\r\n\r\n const [percentage, setPercentage] = useState(0);\r\n\r\n useEffect(() => {\r\n setMounted(true);\r\n }, []);\r\n\r\n useEffect(() => {\r\n if (!showPercentage) return;\r\n\r\n const unsubscribe = scrollYProgress.on(\"change\", (latest) => {\r\n setPercentage(Math.round(latest * 100));\r\n });\r\n\r\n return () => unsubscribe();\r\n }, [scrollYProgress, showPercentage]);\r\n\r\n if (!mounted) return null;\r\n\r\n return (\r\n <>\r\n <motion.div\r\n className={cn(\"fixed top-0 left-0 right-0 z-50 origin-left\", className)}\r\n style={{\r\n scaleX,\r\n height,\r\n backgroundColor: color || \"var(--primary)\",\r\n }}\r\n />\r\n {showPercentage && (\r\n <div\r\n className=\"fixed top-4 right-4 z-50 bg-background/80 backdrop-blur-sm rounded-full px-3 py-1 text-sm font-medium border border-border shadow-sm\"\r\n >\r\n {percentage}%\r\n </div>\r\n )}\r\n </>\r\n );\r\n}\r\n"
16
+ "content": "\"use client\";\r\n\r\nimport { useEffect, useState } from \"react\";\r\nimport { motion, useScroll, useSpring } from \"motion/react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface ReadingProgressProps {\r\n color?: string;\r\n height?: number;\r\n showPercentage?: boolean;\r\n className?: string;\r\n}\r\n\r\nexport function ReadingProgress({\r\n color,\r\n height = 3,\r\n showPercentage = false,\r\n className,\r\n}: ReadingProgressProps) {\r\n const [mounted] = useState(() => typeof window !== \"undefined\");\r\n const { scrollYProgress } = useScroll();\r\n const scaleX = useSpring(scrollYProgress, {\r\n stiffness: 100,\r\n damping: 30,\r\n restDelta: 0.001,\r\n });\r\n\r\n const [percentage, setPercentage] = useState(0);\r\n\r\n useEffect(() => {\r\n if (!showPercentage) return;\r\n\r\n const unsubscribe = scrollYProgress.on(\"change\", (latest) => {\r\n setPercentage(Math.round(latest * 100));\r\n });\r\n\r\n return () => unsubscribe();\r\n }, [scrollYProgress, showPercentage]);\r\n\r\n if (!mounted) return null;\r\n\r\n return (\r\n <>\r\n <motion.div\r\n className={cn(\"fixed top-0 left-0 right-0 z-50 origin-left\", className)}\r\n style={{\r\n scaleX,\r\n height,\r\n backgroundColor: color || \"var(--primary)\",\r\n }}\r\n />\r\n {showPercentage && (\r\n <div\r\n className=\"fixed top-4 right-4 z-50 bg-background/80 backdrop-blur-sm rounded-full px-3 py-1 text-sm font-medium border border-border shadow-sm\"\r\n >\r\n {percentage}%\r\n </div>\r\n )}\r\n </>\r\n );\r\n}\r\n"
17
17
  },
18
18
  {
19
19
  "path": "reading-progress/index.ts",