@promakeai/cli 0.0.6 → 0.1.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.
Files changed (114) hide show
  1. package/dist/index.js +31 -31
  2. package/dist/registry/about-page.json +4 -2
  3. package/dist/registry/about-section.json +2 -2
  4. package/dist/registry/announcement-bar.json +43 -0
  5. package/dist/registry/auth-core.json +43 -0
  6. package/dist/registry/auth.json +1 -1
  7. package/dist/registry/blog-list-page.json +3 -2
  8. package/dist/registry/blog-section.json +1 -1
  9. package/dist/registry/cart-page.json +3 -3
  10. package/dist/registry/case-study-page.json +48 -0
  11. package/dist/registry/checkout-page.json +6 -5
  12. package/dist/registry/coming-soon-page-minimal.json +45 -0
  13. package/dist/registry/coming-soon-page.json +47 -0
  14. package/dist/registry/contact-info-grid.json +1 -1
  15. package/dist/registry/contact-page-centered.json +2 -2
  16. package/dist/registry/contact-page-map-overlay.json +4 -3
  17. package/dist/registry/contact-page-map-split.json +4 -3
  18. package/dist/registry/contact-page-split.json +3 -3
  19. package/dist/registry/contact-page.json +5 -3
  20. package/dist/registry/cookie-consent.json +43 -0
  21. package/dist/registry/cookies-page.json +3 -1
  22. package/dist/registry/cta-section.json +1 -1
  23. package/dist/registry/db.json +1 -1
  24. package/dist/registry/docs/about-page.md +5 -0
  25. package/dist/registry/docs/announcement-bar.md +38 -0
  26. package/dist/registry/docs/auth-core.md +64 -0
  27. package/dist/registry/docs/blog-list-page.md +1 -0
  28. package/dist/registry/docs/case-study-page.md +39 -0
  29. package/dist/registry/docs/checkout-page.md +2 -1
  30. package/dist/registry/docs/coming-soon-page-minimal.md +32 -0
  31. package/dist/registry/docs/coming-soon-page.md +37 -0
  32. package/dist/registry/docs/contact-page-map-overlay.md +2 -2
  33. package/dist/registry/docs/contact-page-map-split.md +2 -2
  34. package/dist/registry/docs/contact-page.md +5 -0
  35. package/dist/registry/docs/cookie-consent.md +37 -0
  36. package/dist/registry/docs/cookies-page.md +5 -0
  37. package/dist/registry/docs/ecommerce-core.md +4 -3
  38. package/dist/registry/docs/forgot-password-page-split.md +45 -0
  39. package/dist/registry/docs/forgot-password-page.md +14 -5
  40. package/dist/registry/docs/header-ecommerce.md +1 -0
  41. package/dist/registry/docs/hero-carousel.md +37 -0
  42. package/dist/registry/docs/landing-page-app.md +43 -0
  43. package/dist/registry/docs/landing-page-saas.md +41 -0
  44. package/dist/registry/docs/login-page-split.md +13 -4
  45. package/dist/registry/docs/login-page.md +17 -4
  46. package/dist/registry/docs/logo-cloud.md +33 -0
  47. package/dist/registry/docs/masonry-grid.md +37 -0
  48. package/dist/registry/docs/my-orders-page.md +44 -0
  49. package/dist/registry/docs/order-confirmation-page.md +41 -0
  50. package/dist/registry/docs/portfolio-page.md +38 -0
  51. package/dist/registry/docs/pricing-page.md +38 -0
  52. package/dist/registry/docs/privacy-page.md +5 -0
  53. package/dist/registry/docs/product-quick-view.md +37 -0
  54. package/dist/registry/docs/reading-progress.md +31 -0
  55. package/dist/registry/docs/register-page-split.md +45 -0
  56. package/dist/registry/docs/register-page.md +14 -7
  57. package/dist/registry/docs/reset-password-page-split.md +45 -0
  58. package/dist/registry/docs/reset-password-page.md +36 -0
  59. package/dist/registry/docs/share-buttons.md +37 -0
  60. package/dist/registry/docs/team-page.md +38 -0
  61. package/dist/registry/docs/terms-page.md +5 -0
  62. package/dist/registry/docs/timeline-section.md +37 -0
  63. package/dist/registry/docs/video-hero.md +41 -0
  64. package/dist/registry/ecommerce-core.json +17 -1
  65. package/dist/registry/empty-page.json +1 -1
  66. package/dist/registry/faq-categorized.json +1 -1
  67. package/dist/registry/faq-simple.json +1 -1
  68. package/dist/registry/favorites-ecommerce-block.json +1 -1
  69. package/dist/registry/feature-section.json +2 -2
  70. package/dist/registry/footer.json +1 -1
  71. package/dist/registry/forgot-password-page-split.json +50 -0
  72. package/dist/registry/forgot-password-page.json +9 -7
  73. package/dist/registry/header-ecommerce.json +3 -2
  74. package/dist/registry/header-mega.json +1 -1
  75. package/dist/registry/hero-carousel.json +45 -0
  76. package/dist/registry/hero-cta.json +2 -2
  77. package/dist/registry/hero-gradient.json +1 -1
  78. package/dist/registry/hero.json +1 -1
  79. package/dist/registry/index.json +22 -2
  80. package/dist/registry/landing-page-app.json +47 -0
  81. package/dist/registry/landing-page-saas.json +47 -0
  82. package/dist/registry/login-page-split.json +11 -7
  83. package/dist/registry/login-page.json +4 -4
  84. package/dist/registry/logo-cloud.json +41 -0
  85. package/dist/registry/masonry-grid.json +43 -0
  86. package/dist/registry/my-orders-page.json +52 -0
  87. package/dist/registry/order-confirmation-page.json +49 -0
  88. package/dist/registry/portfolio-page.json +45 -0
  89. package/dist/registry/pricing-page.json +47 -0
  90. package/dist/registry/pricing-section.json +1 -1
  91. package/dist/registry/privacy-page.json +3 -1
  92. package/dist/registry/product-detail-block.json +1 -1
  93. package/dist/registry/product-quick-view.json +46 -0
  94. package/dist/registry/products-page.json +3 -3
  95. package/dist/registry/reading-progress.json +43 -0
  96. package/dist/registry/register-page-split.json +50 -0
  97. package/dist/registry/register-page.json +9 -7
  98. package/dist/registry/reset-password-page-split.json +50 -0
  99. package/dist/registry/reset-password-page.json +39 -0
  100. package/dist/registry/share-buttons.json +46 -0
  101. package/dist/registry/team-page.json +47 -0
  102. package/dist/registry/terms-page.json +3 -1
  103. package/dist/registry/testimonials-carousel.json +1 -1
  104. package/dist/registry/testimonials-grid.json +1 -1
  105. package/dist/registry/timeline-section.json +43 -0
  106. package/dist/registry/video-hero.json +42 -0
  107. package/package.json +1 -1
  108. package/template/index.html +5 -5
  109. package/template/src/App.tsx +4 -0
  110. package/template/src/components/GoogleAnalytics.tsx +34 -0
  111. package/template/src/components/Layout.tsx +1 -1
  112. package/template/src/components/ScriptInjector.tsx +62 -0
  113. package/template/src/constants/constants.json +8 -2
  114. package/template/src/pages/Index.tsx +5 -1
@@ -24,19 +24,19 @@
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 { 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 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"
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"
28
28
  },
29
29
  {
30
30
  "path": "products-page/lang/en.json",
31
31
  "type": "registry:lang",
32
32
  "target": "$modules$/products-page/lang/en.json",
33
- "content": "{\r\n \"title\": \"Products\",\r\n \"allProducts\": \"All Products\",\r\n \"searchResultsFor\": \"Search Results\",\r\n \"showing\": \"Showing\",\r\n \"of\": \"of\",\r\n \"products\": \"products\",\r\n \"clearSearch\": \"Clear Search\",\r\n \"filters\": \"Filters\",\r\n \"refineSearch\": \"Refine your product search\",\r\n \"categories\": \"Categories\",\r\n \"priceRange\": \"Price Range\",\r\n \"minPrice\": \"Min\",\r\n \"maxPrice\": \"Max\",\r\n \"features\": \"Features\",\r\n \"onSale\": \"On Sale\",\r\n \"newArrivals\": \"New Arrivals\",\r\n \"featuredLabel\": \"Featured\",\r\n \"inStock\": \"In Stock\",\r\n \"sortBy\": \"Sort by\",\r\n \"featured\": \"Featured\",\r\n \"sortPriceLow\": \"Price: Low to High\",\r\n \"sortPriceHigh\": \"Price: High to Low\",\r\n \"sortNewest\": \"Newest\",\r\n \"noProductsFound\": \"No products found matching your criteria.\"\r\n}\r\n"
33
+ "content": "{\r\n \"pageTitle\": \"Products\",\r\n \"title\": \"Products\",\r\n \"allProducts\": \"All Products\",\r\n \"searchResultsFor\": \"Search Results\",\r\n \"showing\": \"Showing\",\r\n \"of\": \"of\",\r\n \"products\": \"products\",\r\n \"clearSearch\": \"Clear Search\",\r\n \"filters\": \"Filters\",\r\n \"refineSearch\": \"Refine your product search\",\r\n \"categories\": \"Categories\",\r\n \"priceRange\": \"Price Range\",\r\n \"minPrice\": \"Min\",\r\n \"maxPrice\": \"Max\",\r\n \"features\": \"Features\",\r\n \"onSale\": \"On Sale\",\r\n \"newArrivals\": \"New Arrivals\",\r\n \"featuredLabel\": \"Featured\",\r\n \"inStock\": \"In Stock\",\r\n \"sortBy\": \"Sort by\",\r\n \"featured\": \"Featured\",\r\n \"sortPriceLow\": \"Price: Low to High\",\r\n \"sortPriceHigh\": \"Price: High to Low\",\r\n \"sortNewest\": \"Newest\",\r\n \"noProductsFound\": \"No products found matching your criteria.\"\r\n}\r\n"
34
34
  },
35
35
  {
36
36
  "path": "products-page/lang/tr.json",
37
37
  "type": "registry:lang",
38
38
  "target": "$modules$/products-page/lang/tr.json",
39
- "content": "{\r\n \"title\": \"Ürünler\",\r\n \"allProducts\": \"Tüm Ürünler\",\r\n \"searchResultsFor\": \"Arama Sonuçları\",\r\n \"showing\": \"Gösterilen\",\r\n \"of\": \"/\",\r\n \"products\": \"ürün\",\r\n \"clearSearch\": \"Aramayı Temizle\",\r\n \"filters\": \"Filtreler\",\r\n \"refineSearch\": \"Ürün aramanızı daraltın\",\r\n \"categories\": \"Kategoriler\",\r\n \"priceRange\": \"Fiyat Aralığı\",\r\n \"minPrice\": \"Min\",\r\n \"maxPrice\": \"Max\",\r\n \"features\": \"Özellikler\",\r\n \"onSale\": \"İndirimde\",\r\n \"newArrivals\": \"Yeni Gelenler\",\r\n \"featuredLabel\": \"Öne Çıkan\",\r\n \"inStock\": \"Stokta\",\r\n \"sortBy\": \"Sırala\",\r\n \"featured\": \"Öne Çıkan\",\r\n \"sortPriceLow\": \"Fiyat: Düşükten Yükseğe\",\r\n \"sortPriceHigh\": \"Fiyat: Yüksekten Düşüğe\",\r\n \"sortNewest\": \"En Yeni\",\r\n \"noProductsFound\": \"Kriterlerinize uygun ürün bulunamadı.\"\r\n}\r\n"
39
+ "content": "{\r\n \"pageTitle\": \"Ürünler\",\r\n \"title\": \"Ürünler\",\r\n \"allProducts\": \"Tüm Ürünler\",\r\n \"searchResultsFor\": \"Arama Sonuçları\",\r\n \"showing\": \"Gösterilen\",\r\n \"of\": \"/\",\r\n \"products\": \"ürün\",\r\n \"clearSearch\": \"Aramayı Temizle\",\r\n \"filters\": \"Filtreler\",\r\n \"refineSearch\": \"Ürün aramanızı daraltın\",\r\n \"categories\": \"Kategoriler\",\r\n \"priceRange\": \"Fiyat Aralığı\",\r\n \"minPrice\": \"Min\",\r\n \"maxPrice\": \"Max\",\r\n \"features\": \"Özellikler\",\r\n \"onSale\": \"İndirimde\",\r\n \"newArrivals\": \"Yeni Gelenler\",\r\n \"featuredLabel\": \"Öne Çıkan\",\r\n \"inStock\": \"Stokta\",\r\n \"sortBy\": \"Sırala\",\r\n \"featured\": \"Öne Çıkan\",\r\n \"sortPriceLow\": \"Fiyat: Düşükten Yükseğe\",\r\n \"sortPriceHigh\": \"Fiyat: Yüksekten Düşüğe\",\r\n \"sortNewest\": \"En Yeni\",\r\n \"noProductsFound\": \"Kriterlerinize uygun ürün bulunamadı.\"\r\n}\r\n"
40
40
  }
41
41
  ],
42
42
  "exports": {
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "reading-progress",
3
+ "type": "registry:component",
4
+ "title": "Reading Progress",
5
+ "description": "Fixed progress bar showing scroll/reading progress. Shows a smooth animated bar at the top of the page.",
6
+ "dependencies": [
7
+ "motion"
8
+ ],
9
+ "registryDependencies": [],
10
+ "usage": "import { ReadingProgress } from '@/modules/reading-progress';\n\n<ReadingProgress />\n\n• Installed at: src/modules/reading-progress/\n• Props: color, height, showPercentage",
11
+ "files": [
12
+ {
13
+ "path": "reading-progress/reading-progress.tsx",
14
+ "type": "registry:component",
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"
17
+ },
18
+ {
19
+ "path": "reading-progress/index.ts",
20
+ "type": "registry:index",
21
+ "target": "$modules$/reading-progress/index.ts",
22
+ "content": "export * from \"./reading-progress\";\r\n"
23
+ },
24
+ {
25
+ "path": "reading-progress/lang/en.json",
26
+ "type": "registry:lang",
27
+ "target": "$modules$/reading-progress/lang/en.json",
28
+ "content": "{\r\n \"progress\": \"Reading progress\"\r\n}\r\n"
29
+ },
30
+ {
31
+ "path": "reading-progress/lang/tr.json",
32
+ "type": "registry:lang",
33
+ "target": "$modules$/reading-progress/lang/tr.json",
34
+ "content": "{\r\n \"progress\": \"Okuma ilerlemesi\"\r\n}\r\n"
35
+ }
36
+ ],
37
+ "exports": {
38
+ "types": [],
39
+ "variables": [
40
+ "ReadingProgress"
41
+ ]
42
+ }
43
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "register-page-split",
3
+ "type": "registry:page",
4
+ "title": "Register Page Split",
5
+ "description": "Split-screen registration page with form on the left and full-height image on the right. Features username, email, password fields with confirmation. Uses customerClient for API calls.",
6
+ "registryDependencies": [
7
+ "button",
8
+ "input",
9
+ "auth-core",
10
+ "api"
11
+ ],
12
+ "usage": "import RegisterPageSplit from '@/modules/register-page-split';\n\n<RegisterPageSplit\n image=\"/images/register-bg.jpg\"\n/>\n\n• Installed at: src/modules/register-page-split/\n• Customize text: src/modules/register-page-split/lang/*.json\n• API Integration:\n - customerClient.auth.register() for registration\n - Shows success state after registration\n• Add to your router as a page component",
13
+ "route": {
14
+ "path": "/register",
15
+ "componentName": "RegisterPageSplit"
16
+ },
17
+ "files": [
18
+ {
19
+ "path": "register-page-split/index.ts",
20
+ "type": "registry:index",
21
+ "target": "$modules$/register-page-split/index.ts",
22
+ "content": "export * from './register-page-split';\r\nexport { default } from './register-page-split';\r\n"
23
+ },
24
+ {
25
+ "path": "register-page-split/register-page-split.tsx",
26
+ "type": "registry:page",
27
+ "target": "$modules$/register-page-split/register-page-split.tsx",
28
+ "content": "import { useState } from \"react\";\r\nimport { Link, useNavigate } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { customerClient } from \"@/modules/api/customer-client\";\r\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\r\n\r\ninterface RegisterPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function RegisterPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: RegisterPageSplitProps) {\r\n const { t } = useTranslation(\"register-page-split\");\r\n usePageTitle({ title: t(\"title\", \"Create Account\") });\r\n const navigate = useNavigate();\r\n\r\n const [username, setUsername] = useState(\"\");\r\n const [email, setEmail] = useState(\"\");\r\n const [password, setPassword] = useState(\"\");\r\n const [confirmPassword, setConfirmPassword] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n\r\n // Validate passwords match\r\n if (password !== confirmPassword) {\r\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\r\n return;\r\n }\r\n\r\n setIsLoading(true);\r\n\r\n try {\r\n await customerClient.auth.register({\r\n username,\r\n email,\r\n password,\r\n });\r\n\r\n toast.success(t(\"registerSuccess\", \"Account created successfully!\"), {\r\n description: t(\"checkEmail\", \"Please check your email to verify your account.\"),\r\n });\r\n\r\n // Navigate to login page after successful registration\r\n navigate(\"/login\", {\r\n state: {\r\n message: t(\"verifyEmail\", \"Please verify your email before logging in.\"),\r\n email\r\n }\r\n });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"registerError\", \"Registration failed. Please try again.\")\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\", \"Create Account\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"subtitle\", \"Sign up to get started\")}\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=\"username\">{t(\"username\", \"Username\")}</Label>\r\n <Input\r\n required\r\n id=\"username\"\r\n type=\"text\"\r\n autoComplete=\"username\"\r\n placeholder={t(\"usernamePlaceholder\", \"johndoe\")}\r\n value={username}\r\n onChange={(e) => setUsername(e.target.value)}\r\n disabled={isLoading}\r\n />\r\n </div>\r\n\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"email\">{t(\"email\", \"Email\")}</Label>\r\n <Input\r\n required\r\n id=\"email\"\r\n type=\"email\"\r\n autoComplete=\"email\"\r\n placeholder={t(\"emailPlaceholder\", \"you@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\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"password\">{t(\"password\", \"Password\")}</Label>\r\n <Input\r\n required\r\n id=\"password\"\r\n type=\"password\"\r\n autoComplete=\"new-password\"\r\n placeholder=\"••••••••\"\r\n value={password}\r\n onChange={(e) => setPassword(e.target.value)}\r\n disabled={isLoading}\r\n minLength={8}\r\n />\r\n </div>\r\n\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"confirm-password\">{t(\"confirmPassword\", \"Confirm Password\")}</Label>\r\n <Input\r\n required\r\n id=\"confirm-password\"\r\n type=\"password\"\r\n autoComplete=\"new-password\"\r\n placeholder=\"••••••••\"\r\n value={confirmPassword}\r\n onChange={(e) => setConfirmPassword(e.target.value)}\r\n disabled={isLoading}\r\n minLength={8}\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(\"creatingAccount\", \"Creating account...\")}\r\n </>\r\n ) : (\r\n t(\"signUp\", \"Sign Up\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <div className=\"text-sm text-center\">\r\n <p>\r\n {t(\"hasAccount\", \"Already have an account?\")}{\" \"}\r\n <Link to=\"/login\" className=\"underline\">\r\n {t(\"signIn\", \"Sign in\")}\r\n </Link>\r\n </p>\r\n </div>\r\n\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\", \"Register 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 RegisterPageSplit;\r\n"
29
+ },
30
+ {
31
+ "path": "register-page-split/lang/en.json",
32
+ "type": "registry:lang",
33
+ "target": "$modules$/register-page-split/lang/en.json",
34
+ "content": "{\r\n \"title\": \"Create Account\",\r\n \"subtitle\": \"Sign up to get started\",\r\n \"username\": \"Username\",\r\n \"usernamePlaceholder\": \"johndoe\",\r\n \"email\": \"Email\",\r\n \"emailPlaceholder\": \"you@example.com\",\r\n \"password\": \"Password\",\r\n \"confirmPassword\": \"Confirm Password\",\r\n \"passwordMismatch\": \"Passwords do not match\",\r\n \"signUp\": \"Sign Up\",\r\n \"creatingAccount\": \"Creating account...\",\r\n \"registerSuccess\": \"Account created successfully!\",\r\n \"checkEmail\": \"Please check your email to verify your account.\",\r\n \"verifyEmail\": \"Please verify your email before logging in.\",\r\n \"registerError\": \"Registration failed. Please try again.\",\r\n \"hasAccount\": \"Already have an account?\",\r\n \"signIn\": \"Sign in\",\r\n \"copyright\": \"All rights reserved.\",\r\n \"imageAlt\": \"Register background\"\r\n}\r\n"
35
+ },
36
+ {
37
+ "path": "register-page-split/lang/tr.json",
38
+ "type": "registry:lang",
39
+ "target": "$modules$/register-page-split/lang/tr.json",
40
+ "content": "{\r\n \"title\": \"Hesap Oluştur\",\r\n \"subtitle\": \"Başlamak için kaydolun\",\r\n \"username\": \"Kullanıcı Adı\",\r\n \"usernamePlaceholder\": \"ahmetyilmaz\",\r\n \"email\": \"E-posta\",\r\n \"emailPlaceholder\": \"ornek@email.com\",\r\n \"password\": \"Şifre\",\r\n \"confirmPassword\": \"Şifre Onayı\",\r\n \"passwordMismatch\": \"Şifreler eşleşmiyor\",\r\n \"signUp\": \"Kaydol\",\r\n \"creatingAccount\": \"Hesap oluşturuluyor...\",\r\n \"registerSuccess\": \"Hesap başarıyla oluşturuldu!\",\r\n \"checkEmail\": \"Lütfen hesabınızı doğrulamak için e-postanızı kontrol edin.\",\r\n \"verifyEmail\": \"Lütfen giriş yapmadan önce e-postanızı doğrulayın.\",\r\n \"registerError\": \"Kayıt başarısız. Lütfen tekrar deneyin.\",\r\n \"hasAccount\": \"Zaten hesabınız var mı?\",\r\n \"signIn\": \"Giriş yap\",\r\n \"copyright\": \"Tüm hakları saklıdır.\",\r\n \"imageAlt\": \"Kayıt arka planı\"\r\n}\r\n"
41
+ }
42
+ ],
43
+ "exports": {
44
+ "types": [],
45
+ "variables": [
46
+ "RegisterPageSplit",
47
+ "default"
48
+ ]
49
+ }
50
+ }
@@ -2,13 +2,15 @@
2
2
  "name": "register-page",
3
3
  "type": "registry:page",
4
4
  "title": "Register Page",
5
- "description": "Registration page with name, email, password form, social OAuth buttons (Google, Microsoft), and sign in link. Centered card layout with responsive design.",
5
+ "description": "Centered card registration page with Layout wrapper. Features username, email, password fields with confirmation, success state after registration. Uses useAuth hook from auth-core for API calls.",
6
6
  "registryDependencies": [
7
7
  "button",
8
8
  "input",
9
- "label"
9
+ "card",
10
+ "auth-core",
11
+ "api"
10
12
  ],
11
- "usage": "import RegisterPage from '@/modules/register-page';\n\n<RegisterPage\n onSubmit={(name, email, password) => console.log(name, email, password)}\n onGoogleSignup={() => console.log('Google')}\n onMicrosoftSignup={() => console.log('Microsoft')}\n/>",
13
+ "usage": "import RegisterPage from '@/modules/register-page';\n\n<RegisterPage />\n\n• Installed at: src/modules/register-page/\n• Customize text: src/modules/register-page/lang/*.json\n• Uses useAuth() hook from auth-core:\n const { register } = useAuth();\n await register(username, email, password);\n Shows success state after registration\n Redirects authenticated users to home\n• Add to your router as a page component",
12
14
  "route": {
13
15
  "path": "/register",
14
16
  "componentName": "RegisterPage"
@@ -18,25 +20,25 @@
18
20
  "path": "register-page/index.ts",
19
21
  "type": "registry:index",
20
22
  "target": "$modules$/register-page/index.ts",
21
- "content": "export { default } from './register-page';\r\nexport { default as RegisterPage } from './register-page';\r\n"
23
+ "content": "export * from './register-page';\r\nexport { default } from './register-page';\r\n"
22
24
  },
23
25
  {
24
26
  "path": "register-page/register-page.tsx",
25
27
  "type": "registry:page",
26
28
  "target": "$modules$/register-page/register-page.tsx",
27
- "content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface RegisterPageProps {\r\n onSubmit?: (name: string, email: string, password: string) => void;\r\n onGoogleSignup?: () => void;\r\n onMicrosoftSignup?: () => void;\r\n className?: string;\r\n}\r\n\r\nexport default function RegisterPage({\r\n onSubmit,\r\n onGoogleSignup,\r\n onMicrosoftSignup,\r\n className,\r\n}: RegisterPageProps) {\r\n const { t } = useTranslation(\"register-page\");\r\n const [name, setName] = useState(\"\");\r\n const [email, setEmail] = useState(\"\");\r\n const [password, setPassword] = useState(\"\");\r\n\r\n const handleSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n onSubmit?.(name, email, password);\r\n };\r\n\r\n return (\r\n <section className={cn(\"flex min-h-screen bg-muted/30 px-4 py-16 md:py-32\", className)}>\r\n <form\r\n onSubmit={handleSubmit}\r\n className=\"bg-muted m-auto h-fit w-full max-w-sm overflow-hidden rounded-xl border shadow-md\"\r\n >\r\n <div className=\"bg-card -m-px rounded-xl border p-8 pb-6\">\r\n <div className=\"text-center\">\r\n <Link to=\"/\" aria-label=\"go home\" className=\"mx-auto block w-fit\">\r\n <Logo size=\"sm\" />\r\n </Link>\r\n <h1 className=\"mb-1 mt-4 text-xl font-semibold\">\r\n {t(\"title\", \"Create Account\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"subtitle\", \"Sign up to get started\")}\r\n </p>\r\n </div>\r\n\r\n <div className=\"mt-6 space-y-6\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"name\" className=\"block text-sm\">\r\n {t(\"name\", \"Full Name\")}\r\n </Label>\r\n <Input\r\n type=\"text\"\r\n required\r\n name=\"name\"\r\n id=\"name\"\r\n value={name}\r\n onChange={(e) => setName(e.target.value)}\r\n placeholder={t(\"namePlaceholder\", \"John Doe\")}\r\n />\r\n </div>\r\n\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"email\" className=\"block text-sm\">\r\n {t(\"email\", \"Email\")}\r\n </Label>\r\n <Input\r\n type=\"email\"\r\n required\r\n name=\"email\"\r\n id=\"email\"\r\n value={email}\r\n onChange={(e) => setEmail(e.target.value)}\r\n placeholder={t(\"emailPlaceholder\", \"you@example.com\")}\r\n />\r\n </div>\r\n\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"password\" className=\"text-sm\">\r\n {t(\"password\", \"Password\")}\r\n </Label>\r\n <Input\r\n type=\"password\"\r\n required\r\n name=\"password\"\r\n id=\"password\"\r\n value={password}\r\n onChange={(e) => setPassword(e.target.value)}\r\n placeholder=\"••••••••\"\r\n />\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\">\r\n {t(\"signUp\", \"Sign Up\")}\r\n </Button>\r\n </div>\r\n\r\n <div className=\"my-6 grid grid-cols-[1fr_auto_1fr] items-center gap-3\">\r\n <hr className=\"border-dashed\" />\r\n <span className=\"text-muted-foreground text-xs\">\r\n {t(\"orContinueWith\", \"Or continue with\")}\r\n </span>\r\n <hr className=\"border-dashed\" />\r\n </div>\r\n\r\n <div className=\"grid grid-cols-2 gap-3\">\r\n <Button type=\"button\" variant=\"outline\" onClick={onGoogleSignup}>\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"0.98em\"\r\n height=\"1em\"\r\n viewBox=\"0 0 256 262\"\r\n className=\"mr-2\"\r\n >\r\n <path\r\n fill=\"#4285f4\"\r\n d=\"M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027\"\r\n />\r\n <path\r\n fill=\"#34a853\"\r\n d=\"M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1\"\r\n />\r\n <path\r\n fill=\"#fbbc05\"\r\n d=\"M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z\"\r\n />\r\n <path\r\n fill=\"#eb4335\"\r\n d=\"M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251\"\r\n />\r\n </svg>\r\n <span>Google</span>\r\n </Button>\r\n <Button type=\"button\" variant=\"outline\" onClick={onMicrosoftSignup}>\r\n <svg\r\n xmlns=\"http://www.w3.org/2000/svg\"\r\n width=\"1em\"\r\n height=\"1em\"\r\n viewBox=\"0 0 256 256\"\r\n className=\"mr-2\"\r\n >\r\n <path fill=\"#f1511b\" d=\"M121.666 121.666H0V0h121.666z\" />\r\n <path fill=\"#80cc28\" d=\"M256 121.666H134.335V0H256z\" />\r\n <path fill=\"#00adef\" d=\"M121.663 256.002H0V134.336h121.663z\" />\r\n <path fill=\"#fbbc09\" d=\"M256 256.002H134.335V134.336H256z\" />\r\n </svg>\r\n <span>Microsoft</span>\r\n </Button>\r\n </div>\r\n </div>\r\n\r\n <div className=\"p-3\">\r\n <p className=\"text-center text-sm text-muted-foreground\">\r\n {t(\"hasAccount\", \"Already have an account?\")}\r\n <Button asChild variant=\"link\" className=\"px-2\">\r\n <Link to=\"/login\">{t(\"signIn\", \"Sign in\")}</Link>\r\n </Button>\r\n </p>\r\n </div>\r\n </form>\r\n </section>\r\n );\r\n}\r\n"
29
+ "content": "import { useState, useEffect } from \"react\";\r\nimport { Link, useNavigate } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useAuth } from \"@/modules/auth-core/use-auth\";\r\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport { UserPlus, Eye, EyeOff, CheckCircle } from \"lucide-react\";\r\n\r\nexport function RegisterPage() {\r\n const { t } = useTranslation(\"register-page\");\r\n usePageTitle({ title: t(\"title\", \"Create Account\") });\r\n\r\n const navigate = useNavigate();\r\n const { register, isAuthenticated } = useAuth();\r\n\r\n const [formData, setFormData] = useState({\r\n username: \"\",\r\n email: \"\",\r\n password: \"\",\r\n confirmPassword: \"\",\r\n });\r\n const [showPassword, setShowPassword] = useState(false);\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [success, setSuccess] = useState(false);\r\n\r\n // Redirect if already authenticated\r\n useEffect(() => {\r\n if (isAuthenticated) {\r\n navigate(\"/\", { replace: true });\r\n }\r\n }, [isAuthenticated, navigate]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setError(null);\r\n\r\n // Validate passwords match\r\n if (formData.password !== formData.confirmPassword) {\r\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\r\n toast.error(t(\"toastErrorTitle\", \"Registration failed\"), {\r\n description: t(\"passwordMismatch\", \"Passwords do not match\"),\r\n });\r\n setIsSubmitting(false);\r\n return;\r\n }\r\n\r\n try {\r\n await register(formData.username, formData.email, formData.password);\r\n setSuccess(true);\r\n toast.success(t(\"toastSuccessTitle\", \"Account created!\"), {\r\n description: t(\r\n \"toastSuccessDesc\",\r\n \"Please check your email to verify your account.\",\r\n ),\r\n });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(err, t(\"errorGeneric\", \"Registration failed. Please try again.\"));\r\n setError(errorMessage);\r\n toast.error(t(\"toastErrorTitle\", \"Registration failed\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n if (success) {\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardContent className=\"pt-6\">\r\n <div className=\"text-center space-y-4\">\r\n <CheckCircle className=\"w-16 h-16 text-green-500 mx-auto\" />\r\n <h2 className=\"text-2xl font-bold text-foreground\">\r\n {t(\"successTitle\", \"Account Created!\")}\r\n </h2>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"successMessage\", \"Please check your email to verify your account.\")}\r\n </p>\r\n <Button asChild className=\"mt-4\">\r\n <Link to=\"/login\">{t(\"goToLogin\", \"Go to Login\")}</Link>\r\n </Button>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Hero Section */}\r\n <div className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\r\n {t(\"title\", \"Create Account\")}\r\n </h1>\r\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\r\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\r\n {t(\"description\", \"Create an account to get started\")}\r\n </p>\r\n </div>\r\n\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <UserPlus className=\"w-5 h-5 text-primary\" />\r\n {t(\"cardTitle\", \"Sign Up\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <form onSubmit={handleSubmit} className=\"space-y-6\">\r\n <div>\r\n <Label htmlFor=\"username\">{t(\"username\", \"Username\")} *</Label>\r\n <Input\r\n id=\"username\"\r\n name=\"username\"\r\n type=\"text\"\r\n value={formData.username}\r\n onChange={handleChange}\r\n placeholder={t(\"usernamePlaceholder\", \"Enter your username\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"username\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"email\">{t(\"email\", \"Email\")} *</Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\", \"Enter your email\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"email\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"password\">{t(\"password\", \"Password\")} *</Label>\r\n <div className=\"relative\">\r\n <Input\r\n id=\"password\"\r\n name=\"password\"\r\n type={showPassword ? \"text\" : \"password\"}\r\n value={formData.password}\r\n onChange={handleChange}\r\n placeholder={t(\"passwordPlaceholder\", \"Enter password\")}\r\n required\r\n className=\"mt-1 pr-10\"\r\n autoComplete=\"new-password\"\r\n />\r\n <button\r\n type=\"button\"\r\n onClick={() => setShowPassword(!showPassword)}\r\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\r\n >\r\n {showPassword ? (\r\n <EyeOff className=\"w-4 h-4\" />\r\n ) : (\r\n <Eye className=\"w-4 h-4\" />\r\n )}\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"confirmPassword\">\r\n {t(\"confirmPassword\", \"Confirm Password\")} *\r\n </Label>\r\n <Input\r\n id=\"confirmPassword\"\r\n name=\"confirmPassword\"\r\n type={showPassword ? \"text\" : \"password\"}\r\n value={formData.confirmPassword}\r\n onChange={handleChange}\r\n placeholder={t(\"confirmPasswordPlaceholder\", \"Confirm your password\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"new-password\"\r\n />\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\r\n <p className=\"text-red-800 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"submitting\", \"Creating account...\")}\r\n </>\r\n ) : (\r\n t(\"submit\", \"Create Account\")\r\n )}\r\n </Button>\r\n\r\n <div className=\"text-center text-sm text-muted-foreground\">\r\n {t(\"hasAccount\", \"Already have an account?\")}{\" \"}\r\n <Link\r\n to=\"/login\"\r\n className=\"text-primary hover:underline font-medium\"\r\n >\r\n {t(\"loginLink\", \"Sign in\")}\r\n </Link>\r\n </div>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default RegisterPage;\r\n"
28
30
  },
29
31
  {
30
32
  "path": "register-page/lang/en.json",
31
33
  "type": "registry:lang",
32
34
  "target": "$modules$/register-page/lang/en.json",
33
- "content": "{\r\n \"title\": \"Create Account\",\r\n \"subtitle\": \"Sign up to get started\",\r\n \"name\": \"Full Name\",\r\n \"namePlaceholder\": \"John Doe\",\r\n \"email\": \"Email\",\r\n \"emailPlaceholder\": \"you@example.com\",\r\n \"password\": \"Password\",\r\n \"signUp\": \"Sign Up\",\r\n \"orContinueWith\": \"Or continue with\",\r\n \"hasAccount\": \"Already have an account?\",\r\n \"signIn\": \"Sign in\"\r\n}\r\n"
35
+ "content": "{\r\n \"title\": \"Create Account\",\r\n \"description\": \"Create an account to get started\",\r\n \"cardTitle\": \"Sign Up\",\r\n \"username\": \"Username\",\r\n \"usernamePlaceholder\": \"Enter your username\",\r\n \"email\": \"Email\",\r\n \"emailPlaceholder\": \"Enter your email\",\r\n \"password\": \"Password\",\r\n \"passwordPlaceholder\": \"Enter password\",\r\n \"confirmPassword\": \"Confirm Password\",\r\n \"confirmPasswordPlaceholder\": \"Confirm your password\",\r\n \"passwordMismatch\": \"Passwords do not match\",\r\n \"submit\": \"Create Account\",\r\n \"submitting\": \"Creating account...\",\r\n \"hasAccount\": \"Already have an account?\",\r\n \"loginLink\": \"Sign in\",\r\n \"toastSuccessTitle\": \"Account created!\",\r\n \"toastSuccessDesc\": \"Please check your email to verify your account.\",\r\n \"toastErrorTitle\": \"Registration failed\",\r\n \"errorGeneric\": \"Registration failed. Please try again.\",\r\n \"successTitle\": \"Account Created!\",\r\n \"successMessage\": \"Please check your email to verify your account.\",\r\n \"goToLogin\": \"Go to Login\"\r\n}\r\n"
34
36
  },
35
37
  {
36
38
  "path": "register-page/lang/tr.json",
37
39
  "type": "registry:lang",
38
40
  "target": "$modules$/register-page/lang/tr.json",
39
- "content": "{\r\n \"title\": \"Hesap Oluştur\",\r\n \"subtitle\": \"Başlamak için kaydolun\",\r\n \"name\": \"Ad Soyad\",\r\n \"namePlaceholder\": \"Ahmet Yılmaz\",\r\n \"email\": \"E-posta\",\r\n \"emailPlaceholder\": \"ornek@email.com\",\r\n \"password\": \"Şifre\",\r\n \"signUp\": \"Kaydol\",\r\n \"orContinueWith\": \"Veya şununla devam et\",\r\n \"hasAccount\": \"Zaten hesabınız var mı?\",\r\n \"signIn\": \"Giriş yap\"\r\n}\r\n"
41
+ "content": "{\r\n \"title\": \"Hesap Oluştur\",\r\n \"description\": \"Başlamak için bir hesap oluşturun\",\r\n \"cardTitle\": \"Kayıt Ol\",\r\n \"username\": \"Kullanıcı Adı\",\r\n \"usernamePlaceholder\": \"Kullanıcı adınızı girin\",\r\n \"email\": \"E-posta\",\r\n \"emailPlaceholder\": \"E-posta adresinizi girin\",\r\n \"password\": \"Şifre\",\r\n \"passwordPlaceholder\": \"Şifre girin\",\r\n \"confirmPassword\": \"Şifre Onayı\",\r\n \"confirmPasswordPlaceholder\": \"Şifrenizi onaylayın\",\r\n \"passwordMismatch\": \"Şifreler eşleşmiyor\",\r\n \"submit\": \"Hesap Oluştur\",\r\n \"submitting\": \"Hesap oluşturuluyor...\",\r\n \"hasAccount\": \"Zaten hesabınız var mı?\",\r\n \"loginLink\": \"Giriş yap\",\r\n \"toastSuccessTitle\": \"Hesap oluşturuldu!\",\r\n \"toastSuccessDesc\": \"Lütfen hesabınızı doğrulamak için e-postanızı kontrol edin.\",\r\n \"toastErrorTitle\": \"Kayıt başarısız\",\r\n \"errorGeneric\": \"Kayıt başarısız. Lütfen tekrar deneyin.\",\r\n \"successTitle\": \"Hesap Oluşturuldu!\",\r\n \"successMessage\": \"Lütfen hesabınızı doğrulamak için e-postanızı kontrol edin.\",\r\n \"goToLogin\": \"Girişe Git\"\r\n}\r\n"
40
42
  }
41
43
  ],
42
44
  "exports": {
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "reset-password-page-split",
3
+ "type": "registry:page",
4
+ "title": "Reset Password Page Split",
5
+ "description": "Split-screen password reset page with form on the left and full-height image on the right. Features new password input with confirmation, validates reset code from URL. Uses customerClient for API calls.",
6
+ "registryDependencies": [
7
+ "button",
8
+ "input",
9
+ "auth-core",
10
+ "api"
11
+ ],
12
+ "usage": "import ResetPasswordPageSplit from '@/modules/reset-password-page-split';\n\n<ResetPasswordPageSplit\n image=\"/images/reset-bg.jpg\"\n/>\n\n• Installed at: src/modules/reset-password-page-split/\n• Customize text: src/modules/reset-password-page-split/lang/*.json\n• API Integration:\n - customerClient.auth.resetPassword() for resetting password\n - Expects ?code= and ?username= URL parameters from email link\n• Add to your router as a page component",
13
+ "route": {
14
+ "path": "/reset-password",
15
+ "componentName": "ResetPasswordPageSplit"
16
+ },
17
+ "files": [
18
+ {
19
+ "path": "reset-password-page-split/index.ts",
20
+ "type": "registry:index",
21
+ "target": "$modules$/reset-password-page-split/index.ts",
22
+ "content": "export * from './reset-password-page-split';\r\nexport { default } from './reset-password-page-split';\r\n"
23
+ },
24
+ {
25
+ "path": "reset-password-page-split/reset-password-page-split.tsx",
26
+ "type": "registry:page",
27
+ "target": "$modules$/reset-password-page-split/reset-password-page-split.tsx",
28
+ "content": "import { useState } from \"react\";\r\nimport { Link, useNavigate, useSearchParams } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { KeyRound, ArrowLeft, CheckCircle } from \"lucide-react\";\r\nimport { customerClient } from \"@/modules/api/customer-client\";\r\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\r\n\r\ninterface ResetPasswordPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function ResetPasswordPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: ResetPasswordPageSplitProps) {\r\n const { t } = useTranslation(\"reset-password-page-split\");\r\n usePageTitle({ title: t(\"title\", \"Reset Password\") });\r\n const navigate = useNavigate();\r\n const [searchParams] = useSearchParams();\r\n\r\n // Get code and username from URL params\r\n const code = searchParams.get(\"code\") || \"\";\r\n const username = searchParams.get(\"username\") || searchParams.get(\"email\") || \"\";\r\n\r\n const [password, setPassword] = useState(\"\");\r\n const [confirmPassword, setConfirmPassword] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [isSuccess, setIsSuccess] = useState(false);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n\r\n // Validate passwords match\r\n if (password !== confirmPassword) {\r\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\r\n return;\r\n }\r\n\r\n // Validate we have required params\r\n if (!code || !username) {\r\n setError(t(\"invalidLink\", \"Invalid or expired reset link. Please request a new one.\"));\r\n return;\r\n }\r\n\r\n setIsLoading(true);\r\n\r\n try {\r\n await customerClient.auth.resetPassword({\r\n username,\r\n code,\r\n newPassword: password,\r\n });\r\n\r\n setIsSuccess(true);\r\n toast.success(t(\"passwordResetSuccess\", \"Password reset successfully!\"));\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"resetPasswordError\", \"Failed to reset password. Please try again.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n // Success state\r\n if (isSuccess) {\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 text-center\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center\">\r\n <CheckCircle className=\"w-8 h-8 text-green-600 dark:text-green-400\" />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"passwordReset\", \"Password Reset!\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-2\">\r\n {t(\"passwordResetDescription\", \"Your password has been reset successfully. You can now log in with your new password.\")}\r\n </p>\r\n </div>\r\n\r\n <Button onClick={() => navigate(\"/login\")} className=\"w-full\">\r\n {t(\"goToLogin\", \"Go to Login\")}\r\n </Button>\r\n\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\", \"Reset password 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\n // Invalid link state\r\n if (!code || !username) {\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 text-center\">\r\n <Logo />\r\n <hr />\r\n\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight text-red-600\">\r\n {t(\"invalidLinkTitle\", \"Invalid Reset Link\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-2\">\r\n {t(\"invalidLinkDescription\", \"This password reset link is invalid or has expired. Please request a new one.\")}\r\n </p>\r\n </div>\r\n\r\n <Button onClick={() => navigate(\"/forgot-password\")} className=\"w-full\">\r\n {t(\"requestNewLink\", \"Request New Link\")}\r\n </Button>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\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\", \"Reset password 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\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\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center\">\r\n <KeyRound className=\"w-6 h-6 text-primary\" />\r\n </div>\r\n </div>\r\n\r\n <div className=\"text-center\">\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Reset Password\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"subtitle\", \"Enter your new password below\")}\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=\"password\">{t(\"newPassword\", \"New Password\")}</Label>\r\n <Input\r\n required\r\n id=\"password\"\r\n type=\"password\"\r\n autoComplete=\"new-password\"\r\n placeholder=\"••••••••\"\r\n value={password}\r\n onChange={(e) => setPassword(e.target.value)}\r\n disabled={isLoading}\r\n minLength={8}\r\n />\r\n </div>\r\n\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"confirm-password\">{t(\"confirmPassword\", \"Confirm Password\")}</Label>\r\n <Input\r\n required\r\n id=\"confirm-password\"\r\n type=\"password\"\r\n autoComplete=\"new-password\"\r\n placeholder=\"••••••••\"\r\n value={confirmPassword}\r\n onChange={(e) => setConfirmPassword(e.target.value)}\r\n disabled={isLoading}\r\n minLength={8}\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(\"resetting\", \"Resetting...\")}\r\n </>\r\n ) : (\r\n t(\"resetPassword\", \"Reset Password\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\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\", \"Reset password 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 ResetPasswordPageSplit;\r\n"
29
+ },
30
+ {
31
+ "path": "reset-password-page-split/lang/en.json",
32
+ "type": "registry:lang",
33
+ "target": "$modules$/reset-password-page-split/lang/en.json",
34
+ "content": "{\r\n \"title\": \"Reset Password\",\r\n \"subtitle\": \"Enter your new password below\",\r\n \"newPassword\": \"New Password\",\r\n \"confirmPassword\": \"Confirm Password\",\r\n \"resetPassword\": \"Reset Password\",\r\n \"resetting\": \"Resetting...\",\r\n \"passwordMismatch\": \"Passwords do not match\",\r\n \"invalidLink\": \"Invalid or expired reset link. Please request a new one.\",\r\n \"passwordResetSuccess\": \"Password reset successfully!\",\r\n \"resetPasswordError\": \"Failed to reset password. Please try again.\",\r\n \"passwordReset\": \"Password Reset!\",\r\n \"passwordResetDescription\": \"Your password has been reset successfully. You can now log in with your new password.\",\r\n \"goToLogin\": \"Go to Login\",\r\n \"invalidLinkTitle\": \"Invalid Reset Link\",\r\n \"invalidLinkDescription\": \"This password reset link is invalid or has expired. Please request a new one.\",\r\n \"requestNewLink\": \"Request New Link\",\r\n \"backToLogin\": \"Back to login\",\r\n \"copyright\": \"All rights reserved.\",\r\n \"imageAlt\": \"Reset password background\"\r\n}\r\n"
35
+ },
36
+ {
37
+ "path": "reset-password-page-split/lang/tr.json",
38
+ "type": "registry:lang",
39
+ "target": "$modules$/reset-password-page-split/lang/tr.json",
40
+ "content": "{\r\n \"title\": \"Şifre Sıfırla\",\r\n \"subtitle\": \"Yeni şifrenizi aşağıya girin\",\r\n \"newPassword\": \"Yeni Şifre\",\r\n \"confirmPassword\": \"Şifreyi Onayla\",\r\n \"resetPassword\": \"Şifreyi Sıfırla\",\r\n \"resetting\": \"Sıfırlanıyor...\",\r\n \"passwordMismatch\": \"Şifreler eşleşmiyor\",\r\n \"invalidLink\": \"Geçersiz veya süresi dolmuş sıfırlama bağlantısı. Lütfen yeni bir tane isteyin.\",\r\n \"passwordResetSuccess\": \"Şifre başarıyla sıfırlandı!\",\r\n \"resetPasswordError\": \"Şifre sıfırlanamadı. Lütfen tekrar deneyin.\",\r\n \"passwordReset\": \"Şifre Sıfırlandı!\",\r\n \"passwordResetDescription\": \"Şifreniz başarıyla sıfırlandı. Artık yeni şifrenizle giriş yapabilirsiniz.\",\r\n \"goToLogin\": \"Girişe Git\",\r\n \"invalidLinkTitle\": \"Geçersiz Sıfırlama Bağlantısı\",\r\n \"invalidLinkDescription\": \"Bu şifre sıfırlama bağlantısı geçersiz veya süresi dolmuş. Lütfen yeni bir tane isteyin.\",\r\n \"requestNewLink\": \"Yeni Bağlantı İste\",\r\n \"backToLogin\": \"Girişe dön\",\r\n \"copyright\": \"Tüm hakları saklıdır.\",\r\n \"imageAlt\": \"Şifre sıfırlama arka planı\"\r\n}\r\n"
41
+ }
42
+ ],
43
+ "exports": {
44
+ "types": [],
45
+ "variables": [
46
+ "ResetPasswordPageSplit",
47
+ "default"
48
+ ]
49
+ }
50
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "reset-password-page",
3
+ "type": "registry:page",
4
+ "title": "Reset Password Page",
5
+ "description": "Split-screen password reset page with form on the left and full-height image on the right. Features new password input with confirmation, validates reset code from URL. Clean minimal design with API integration and responsive layout.",
6
+ "registryDependencies": [
7
+ "button",
8
+ "input",
9
+ "auth",
10
+ "api"
11
+ ],
12
+ "usage": "import ResetPasswordPage from '@/modules/reset-password-page';\n\n<ResetPasswordPage\n image=\"/images/reset-bg.jpg\"\n/>\n\n• Installed at: src/modules/reset-password-page/\n• Customize text: src/modules/reset-password-page/lang/*.json\n• Uses customerClient.auth.resetPassword() for API\n• Expects ?code= URL parameter from email link\n• Add to your router as a page component",
13
+ "route": {
14
+ "path": "/reset-password",
15
+ "componentName": "ResetPasswordPage"
16
+ },
17
+ "files": [
18
+ {
19
+ "path": "reset-password-page/index.ts",
20
+ "type": "registry:index",
21
+ "target": "$modules$/reset-password-page/index.ts"
22
+ },
23
+ {
24
+ "path": "reset-password-page/reset-password-page.tsx",
25
+ "type": "registry:page",
26
+ "target": "$modules$/reset-password-page/reset-password-page.tsx"
27
+ },
28
+ {
29
+ "path": "reset-password-page/lang/en.json",
30
+ "type": "registry:lang",
31
+ "target": "$modules$/reset-password-page/lang/en.json"
32
+ },
33
+ {
34
+ "path": "reset-password-page/lang/tr.json",
35
+ "type": "registry:lang",
36
+ "target": "$modules$/reset-password-page/lang/tr.json"
37
+ }
38
+ ]
39
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "share-buttons",
3
+ "type": "registry:component",
4
+ "title": "Share Buttons",
5
+ "description": "Social media share buttons with copy link functionality. Supports Twitter/X, Facebook, LinkedIn, WhatsApp, Email, and copy to clipboard.",
6
+ "dependencies": [
7
+ "sonner"
8
+ ],
9
+ "registryDependencies": [
10
+ "button",
11
+ "tooltip"
12
+ ],
13
+ "usage": "import { ShareButtons } from '@/modules/share-buttons';\n\n<ShareButtons />\n\n• Installed at: src/modules/share-buttons/\n• Props: url, title, platforms[], layout, size, showLabels",
14
+ "files": [
15
+ {
16
+ "path": "share-buttons/share-buttons.tsx",
17
+ "type": "registry:component",
18
+ "target": "$modules$/share-buttons/share-buttons.tsx",
19
+ "content": "\"use client\";\r\n\r\nimport { useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { toast } from \"sonner\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipProvider,\r\n TooltipTrigger,\r\n} from \"@/components/ui/tooltip\";\r\n\r\ntype Platform = \"twitter\" | \"facebook\" | \"linkedin\" | \"whatsapp\" | \"email\" | \"copy\";\r\n\r\ninterface ShareButtonsProps {\r\n url?: string;\r\n title?: string;\r\n platforms?: Platform[];\r\n layout?: \"horizontal\" | \"vertical\";\r\n size?: \"sm\" | \"md\" | \"lg\";\r\n showLabels?: boolean;\r\n className?: string;\r\n}\r\n\r\nconst platformConfig: Record<Platform, { icon: React.FC<{ className?: string }>; color: string }> = {\r\n twitter: {\r\n icon: ({ className }) => (\r\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\n <path d=\"M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z\" />\r\n </svg>\r\n ),\r\n color: \"hover:bg-black hover:text-white\",\r\n },\r\n facebook: {\r\n icon: ({ className }) => (\r\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\n <path d=\"M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z\" />\r\n </svg>\r\n ),\r\n color: \"hover:bg-[#1877F2] hover:text-white\",\r\n },\r\n linkedin: {\r\n icon: ({ className }) => (\r\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\n <path d=\"M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z\" />\r\n </svg>\r\n ),\r\n color: \"hover:bg-[#0A66C2] hover:text-white\",\r\n },\r\n whatsapp: {\r\n icon: ({ className }) => (\r\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\n <path d=\"M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z\" />\r\n </svg>\r\n ),\r\n color: \"hover:bg-[#25D366] hover:text-white\",\r\n },\r\n email: {\r\n icon: ({ className }) => (\r\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\r\n <rect width=\"20\" height=\"16\" x=\"2\" y=\"4\" rx=\"2\" />\r\n <path d=\"m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7\" />\r\n </svg>\r\n ),\r\n color: \"hover:bg-gray-600 hover:text-white\",\r\n },\r\n copy: {\r\n icon: ({ className }) => (\r\n <svg className={className} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\r\n <rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\" />\r\n <path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\" />\r\n </svg>\r\n ),\r\n color: \"hover:bg-gray-600 hover:text-white\",\r\n },\r\n};\r\n\r\nconst sizeClasses = {\r\n sm: \"h-8 w-8\",\r\n md: \"h-10 w-10\",\r\n lg: \"h-12 w-12\",\r\n};\r\n\r\nconst iconSizeClasses = {\r\n sm: \"h-4 w-4\",\r\n md: \"h-5 w-5\",\r\n lg: \"h-6 w-6\",\r\n};\r\n\r\nexport function ShareButtons({\r\n url,\r\n title,\r\n platforms = [\"twitter\", \"facebook\", \"linkedin\", \"whatsapp\", \"copy\"],\r\n layout = \"horizontal\",\r\n size = \"md\",\r\n showLabels = false,\r\n className,\r\n}: ShareButtonsProps) {\r\n const { t } = useTranslation(\"share-buttons\");\r\n const [copied, setCopied] = useState(false);\r\n\r\n const shareUrl = url || (typeof window !== \"undefined\" ? window.location.href : \"\");\r\n const shareTitle = title || (typeof document !== \"undefined\" ? document.title : \"\");\r\n\r\n const getShareUrl = (platform: Platform): string => {\r\n const encodedUrl = encodeURIComponent(shareUrl);\r\n const encodedTitle = encodeURIComponent(shareTitle);\r\n\r\n switch (platform) {\r\n case \"twitter\":\r\n return `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`;\r\n case \"facebook\":\r\n return `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`;\r\n case \"linkedin\":\r\n return `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`;\r\n case \"whatsapp\":\r\n return `https://wa.me/?text=${encodedTitle}%20${encodedUrl}`;\r\n case \"email\":\r\n return `mailto:?subject=${encodedTitle}&body=${encodedUrl}`;\r\n default:\r\n return \"\";\r\n }\r\n };\r\n\r\n const handleShare = async (platform: Platform) => {\r\n if (platform === \"copy\") {\r\n try {\r\n await navigator.clipboard.writeText(shareUrl);\r\n setCopied(true);\r\n toast.success(t(\"copied\"));\r\n setTimeout(() => setCopied(false), 2000);\r\n } catch {\r\n toast.error(t(\"copyFailed\"));\r\n }\r\n return;\r\n }\r\n\r\n const shareUrlForPlatform = getShareUrl(platform);\r\n if (shareUrlForPlatform) {\r\n window.open(shareUrlForPlatform, \"_blank\", \"noopener,noreferrer,width=600,height=400\");\r\n }\r\n };\r\n\r\n return (\r\n <TooltipProvider>\r\n <div\r\n className={cn(\r\n \"flex gap-2\",\r\n layout === \"vertical\" ? \"flex-col\" : \"flex-row flex-wrap\",\r\n className\r\n )}\r\n >\r\n {platforms.map((platform) => {\r\n const config = platformConfig[platform];\r\n const Icon = config.icon;\r\n const label = t(`platforms.${platform}`);\r\n\r\n return (\r\n <Tooltip key={platform}>\r\n <TooltipTrigger asChild>\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className={cn(\r\n sizeClasses[size],\r\n \"transition-colors duration-200\",\r\n config.color,\r\n platform === \"copy\" && copied && \"bg-green-500 text-white hover:bg-green-500\"\r\n )}\r\n onClick={() => handleShare(platform)}\r\n aria-label={label}\r\n >\r\n <Icon className={iconSizeClasses[size]} />\r\n {showLabels && (\r\n <span className=\"ml-2 text-sm\">{label}</span>\r\n )}\r\n </Button>\r\n </TooltipTrigger>\r\n <TooltipContent>\r\n <p>{label}</p>\r\n </TooltipContent>\r\n </Tooltip>\r\n );\r\n })}\r\n </div>\r\n </TooltipProvider>\r\n );\r\n}\r\n"
20
+ },
21
+ {
22
+ "path": "share-buttons/index.ts",
23
+ "type": "registry:index",
24
+ "target": "$modules$/share-buttons/index.ts",
25
+ "content": "export * from \"./share-buttons\";\r\n"
26
+ },
27
+ {
28
+ "path": "share-buttons/lang/en.json",
29
+ "type": "registry:lang",
30
+ "target": "$modules$/share-buttons/lang/en.json",
31
+ "content": "{\r\n \"copied\": \"Link copied to clipboard!\",\r\n \"copyFailed\": \"Failed to copy link\",\r\n \"platforms\": {\r\n \"twitter\": \"Share on X\",\r\n \"facebook\": \"Share on Facebook\",\r\n \"linkedin\": \"Share on LinkedIn\",\r\n \"whatsapp\": \"Share on WhatsApp\",\r\n \"email\": \"Share via Email\",\r\n \"copy\": \"Copy link\"\r\n }\r\n}\r\n"
32
+ },
33
+ {
34
+ "path": "share-buttons/lang/tr.json",
35
+ "type": "registry:lang",
36
+ "target": "$modules$/share-buttons/lang/tr.json",
37
+ "content": "{\r\n \"copied\": \"Link panoya kopyalandı!\",\r\n \"copyFailed\": \"Link kopyalanamadı\",\r\n \"platforms\": {\r\n \"twitter\": \"X'te Paylaş\",\r\n \"facebook\": \"Facebook'ta Paylaş\",\r\n \"linkedin\": \"LinkedIn'de Paylaş\",\r\n \"whatsapp\": \"WhatsApp'ta Paylaş\",\r\n \"email\": \"E-posta ile Paylaş\",\r\n \"copy\": \"Linki Kopyala\"\r\n }\r\n}\r\n"
38
+ }
39
+ ],
40
+ "exports": {
41
+ "types": [],
42
+ "variables": [
43
+ "ShareButtons"
44
+ ]
45
+ }
46
+ }