@promakeai/cli 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/index.js +214 -135
  2. package/dist/registry/about-page.json +1 -1
  3. package/dist/registry/about-section.json +1 -1
  4. package/dist/registry/api.json +55 -0
  5. package/dist/registry/auth.json +70 -0
  6. package/dist/registry/bento-grid-section.json +1 -1
  7. package/dist/registry/blog-list-page.json +1 -1
  8. package/dist/registry/blog-section.json +1 -1
  9. package/dist/registry/cart-drawer.json +1 -1
  10. package/dist/registry/cart-page.json +3 -2
  11. package/dist/registry/category-section.json +1 -1
  12. package/dist/registry/checkout-page.json +3 -2
  13. package/dist/registry/contact-info-grid.json +1 -1
  14. package/dist/registry/contact-page-centered.json +1 -1
  15. package/dist/registry/contact-page-map-overlay.json +1 -1
  16. package/dist/registry/contact-page.json +1 -1
  17. package/dist/registry/cookies-page.json +1 -1
  18. package/dist/registry/cta-section.json +1 -1
  19. package/dist/registry/db.json +129 -0
  20. package/dist/registry/docs/cart-page.md +1 -0
  21. package/dist/registry/docs/checkout-page.md +1 -0
  22. package/dist/registry/docs/forgot-password-page.md +37 -0
  23. package/dist/registry/docs/header-ecommerce.md +1 -0
  24. package/dist/registry/docs/products-page.md +1 -0
  25. package/dist/registry/docs/register-page.md +39 -0
  26. package/dist/registry/ecommerce-core.json +1 -1
  27. package/dist/registry/empty-page.json +1 -1
  28. package/dist/registry/faq-categorized.json +1 -1
  29. package/dist/registry/faq-simple.json +1 -1
  30. package/dist/registry/favorites-blog-block.json +1 -1
  31. package/dist/registry/favorites-ecommerce-block.json +1 -1
  32. package/dist/registry/feature-section.json +1 -1
  33. package/dist/registry/featured-products.json +1 -1
  34. package/dist/registry/footer-detailed.json +1 -1
  35. package/dist/registry/footer-minimal.json +3 -3
  36. package/dist/registry/footer.json +1 -1
  37. package/dist/registry/forgot-password-page.json +49 -0
  38. package/dist/registry/header-ecommerce.json +3 -2
  39. package/dist/registry/header-mega.json +1 -1
  40. package/dist/registry/header-minimal.json +1 -1
  41. package/dist/registry/header-simple.json +1 -1
  42. package/dist/registry/hero-cta.json +1 -1
  43. package/dist/registry/hero-gradient.json +1 -1
  44. package/dist/registry/hero-profile.json +1 -1
  45. package/dist/registry/hero.json +1 -1
  46. package/dist/registry/index.json +3 -0
  47. package/dist/registry/orders-list-block.json +1 -1
  48. package/dist/registry/payment-success-block.json +1 -1
  49. package/dist/registry/post-detail-block.json +1 -1
  50. package/dist/registry/pricing-section.json +1 -1
  51. package/dist/registry/privacy-page.json +1 -1
  52. package/dist/registry/products-page.json +3 -2
  53. package/dist/registry/register-page.json +49 -0
  54. package/dist/registry/related-posts-block.json +1 -1
  55. package/dist/registry/terms-page.json +1 -1
  56. package/dist/registry/testimonials-carousel.json +1 -1
  57. package/dist/registry/testimonials-grid.json +1 -1
  58. package/package.json +1 -1
  59. package/template/src/App.tsx +3 -24
  60. package/template/src/components/Layout.tsx +0 -4
  61. package/template/src/index.css +1 -0
  62. package/template/src/lang/en/index.json +1 -28
  63. package/template/src/lang/tr/index.json +1 -28
  64. package/template/src/pages/Index.tsx +1 -102
  65. package/template/src/components/Footer.tsx +0 -100
  66. package/template/src/components/Header.tsx +0 -79
  67. package/template/src/components/Hero.tsx +0 -69
  68. package/template/src/modules/api/USAGE.md +0 -515
  69. package/template/src/modules/api/customer-client.ts +0 -20
  70. package/template/src/modules/api/get-error-message.ts +0 -18
  71. package/template/src/modules/api/validation/en.json +0 -29
  72. package/template/src/modules/api/validation/tr.json +0 -29
  73. package/template/src/modules/auth/USAGE.md +0 -248
  74. package/template/src/modules/auth/auth-header-menu.tsx +0 -123
  75. package/template/src/modules/auth/auth-store.ts +0 -57
  76. package/template/src/modules/auth/forgot-password-page.tsx +0 -371
  77. package/template/src/modules/auth/login-page.tsx +0 -183
  78. package/template/src/modules/auth/register-page.tsx +0 -252
  79. package/template/src/modules/auth/use-auth.ts +0 -273
  80. package/template/src/modules/db/adapters/IDataAdapter.ts +0 -26
  81. package/template/src/modules/db/adapters/SqliteAdapter.ts +0 -364
  82. package/template/src/modules/db/adapters/index.ts +0 -2
  83. package/template/src/modules/db/config.ts +0 -59
  84. package/template/src/modules/db/core/DataManager.ts +0 -125
  85. package/template/src/modules/db/core/types.ts +0 -101
  86. package/template/src/modules/db/index.ts +0 -42
  87. package/template/src/modules/db/react/QueryProvider.tsx +0 -16
  88. package/template/src/modules/db/react/index.ts +0 -23
  89. package/template/src/modules/db/react/queryClient.ts +0 -64
  90. package/template/src/modules/db/react/useRepository.ts +0 -400
  91. package/template/src/modules/db/utils/parsers.ts +0 -96
@@ -20,7 +20,7 @@
20
20
  "path": "about-page/about-page.tsx",
21
21
  "type": "registry:page",
22
22
  "target": "$modules$/about-page/about-page.tsx",
23
- "content": "import { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Layout } from \"@/components/Layout\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Users, Target, Award, Heart } from \"lucide-react\";\nimport { FadeIn, StaggerContainer, StaggerItem, ScaleUp } from \"@/modules/animations\";\n\nexport function AboutPage() {\n const { t } = useTranslation(\"about-page\");\n usePageTitle({ title: t(\"title\") });\n\n const values = [\n { icon: Target, titleKey: \"missionTitle\", descKey: \"missionDesc\" },\n { icon: Heart, titleKey: \"valuesTitle\", descKey: \"valuesDesc\" },\n { icon: Users, titleKey: \"teamTitle\", descKey: \"teamDesc\" },\n { icon: Award, titleKey: \"qualityTitle\", descKey: \"qualityDesc\" },\n ];\n\n const stats = [\n { valueKey: \"customersValue\", labelKey: \"customersLabel\" },\n { valueKey: \"projectsValue\", labelKey: \"projectsLabel\" },\n { valueKey: \"experienceValue\", labelKey: \"experienceLabel\" },\n { valueKey: \"satisfactionValue\", labelKey: \"satisfactionLabel\" },\n ];\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"container mx-auto px-4\">\n {/* Hero Section */}\n <FadeIn className=\"text-center mb-16\">\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\")}\n </h1>\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\n <p className=\"text-lg text-muted-foreground max-w-3xl mx-auto\">\n {t(\"subtitle\")}\n </p>\n </FadeIn>\n\n {/* Story Section */}\n <FadeIn delay={0.1} className=\"max-w-4xl mx-auto mb-16\">\n <Card>\n <CardContent className=\"p-8\">\n <h2 className=\"text-2xl font-semibold mb-4\">{t(\"storyTitle\")}</h2>\n <div className=\"space-y-4 text-muted-foreground\">\n <p>{t(\"storyP1\")}</p>\n <p>{t(\"storyP2\")}</p>\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Values & Stats */}\n <div className=\"max-w-6xl mx-auto\">\n {/* Values Grid */}\n <StaggerContainer className=\"grid md:grid-cols-2 lg:grid-cols-4 gap-6 mb-16\">\n {values.map(({ icon: Icon, titleKey, descKey }) => (\n <StaggerItem key={titleKey}>\n <Card className=\"text-center h-full\">\n <CardContent className=\"p-6\">\n <div className=\"w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4\">\n <Icon className=\"w-6 h-6 text-primary\" />\n </div>\n <h3 className=\"font-semibold mb-2\">{t(titleKey)}</h3>\n <p className=\"text-sm text-muted-foreground\">{t(descKey)}</p>\n </CardContent>\n </Card>\n </StaggerItem>\n ))}\n </StaggerContainer>\n\n {/* Stats Section */}\n <ScaleUp className=\"bg-primary/5 rounded-2xl p-8 mb-16\">\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-8 text-center\">\n {stats.map(({ valueKey, labelKey }) => (\n <div key={valueKey}>\n <div className=\"text-3xl font-bold text-primary mb-1\">\n {t(valueKey)}\n </div>\n <div className=\"text-sm text-muted-foreground\">\n {t(labelKey)}\n </div>\n </div>\n ))}\n </div>\n </ScaleUp>\n </div>\n\n {/* CTA Section */}\n <FadeIn className=\"text-center\">\n <h2 className=\"text-2xl font-semibold mb-4\">{t(\"ctaTitle\")}</h2>\n <p className=\"text-muted-foreground mb-6 max-w-2xl mx-auto\">\n {t(\"ctaDesc\")}\n </p>\n </FadeIn>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default AboutPage;\n"
23
+ "content": "import { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Layout } from \"@/components/Layout\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Users, Target, Award, Heart } from \"lucide-react\";\nimport { FadeIn, StaggerContainer, StaggerItem, ScaleUp } from \"@/modules/animations\";\n\nexport function AboutPage() {\n const { t } = useTranslation(\"about-page\");\n usePageTitle({ title: t(\"title\") });\n\n const values = [\n { icon: Target, titleKey: \"missionTitle\", descKey: \"missionDesc\" },\n { icon: Heart, titleKey: \"valuesTitle\", descKey: \"valuesDesc\" },\n { icon: Users, titleKey: \"teamTitle\", descKey: \"teamDesc\" },\n { icon: Award, titleKey: \"qualityTitle\", descKey: \"qualityDesc\" },\n ];\n\n const stats = [\n { valueKey: \"customersValue\", labelKey: \"customersLabel\" },\n { valueKey: \"projectsValue\", labelKey: \"projectsLabel\" },\n { valueKey: \"experienceValue\", labelKey: \"experienceLabel\" },\n { valueKey: \"satisfactionValue\", labelKey: \"satisfactionLabel\" },\n ];\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n {/* Hero Section */}\n <FadeIn className=\"text-center mb-16\">\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\")}\n </h1>\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\n <p className=\"text-lg text-muted-foreground max-w-3xl mx-auto\">\n {t(\"subtitle\")}\n </p>\n </FadeIn>\n\n {/* Story Section */}\n <FadeIn delay={0.1} className=\"mb-16\">\n <Card>\n <CardContent className=\"p-8\">\n <h2 className=\"text-2xl font-semibold mb-4\">{t(\"storyTitle\")}</h2>\n <div className=\"space-y-4 text-muted-foreground\">\n <p>{t(\"storyP1\")}</p>\n <p>{t(\"storyP2\")}</p>\n </div>\n </CardContent>\n </Card>\n </FadeIn>\n\n {/* Values & Stats */}\n <div>\n {/* Values Grid */}\n <StaggerContainer className=\"grid md:grid-cols-2 lg:grid-cols-4 gap-6 mb-16\">\n {values.map(({ icon: Icon, titleKey, descKey }) => (\n <StaggerItem key={titleKey}>\n <Card className=\"text-center h-full\">\n <CardContent className=\"p-6\">\n <div className=\"w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4\">\n <Icon className=\"w-6 h-6 text-primary\" />\n </div>\n <h3 className=\"font-semibold mb-2\">{t(titleKey)}</h3>\n <p className=\"text-sm text-muted-foreground\">{t(descKey)}</p>\n </CardContent>\n </Card>\n </StaggerItem>\n ))}\n </StaggerContainer>\n\n {/* Stats Section */}\n <ScaleUp className=\"bg-primary/5 rounded-2xl p-8 mb-16\">\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-8 text-center\">\n {stats.map(({ valueKey, labelKey }) => (\n <div key={valueKey}>\n <div className=\"text-3xl font-bold text-primary mb-1\">\n {t(valueKey)}\n </div>\n <div className=\"text-sm text-muted-foreground\">\n {t(labelKey)}\n </div>\n </div>\n ))}\n </div>\n </ScaleUp>\n </div>\n\n {/* CTA Section */}\n <FadeIn className=\"text-center\">\n <h2 className=\"text-2xl font-semibold mb-4\">{t(\"ctaTitle\")}</h2>\n <p className=\"text-muted-foreground mb-6 max-w-2xl mx-auto\">\n {t(\"ctaDesc\")}\n </p>\n </FadeIn>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default AboutPage;\n"
24
24
  },
25
25
  {
26
26
  "path": "about-page/lang/en.json",
@@ -16,7 +16,7 @@
16
16
  "path": "about-section/about-section.tsx",
17
17
  "type": "registry:component",
18
18
  "target": "$modules$/about-section/about-section.tsx",
19
- "content": "import { Link } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\n\r\ninterface AboutSectionProps {\r\n className?: string;\r\n}\r\n\r\nexport function AboutSection({ className }: AboutSectionProps) {\r\n const { t } = useTranslation(\"about-section\");\r\n\r\n const stats = [\r\n { value: t(\"stat1Value\", \"500+\"), label: t(\"stat1Label\", \"Happy Clients\") },\r\n { value: t(\"stat2Value\", \"1000+\"), label: t(\"stat2Label\", \"Projects Completed\") },\r\n { value: t(\"stat3Value\", \"99%\"), label: t(\"stat3Label\", \"Satisfaction Rate\") },\r\n { value: t(\"stat4Value\", \"15+\"), label: t(\"stat4Label\", \"Years Experience\") },\r\n ];\r\n\r\n const companies = [\"Google\", \"Microsoft\", \"Amazon\", \"Apple\", \"Meta\"];\r\n\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"container mx-auto px-4\">\r\n {/* Header */}\r\n <div className=\"grid md:grid-cols-2 gap-8 mb-12\">\r\n <h2 className=\"text-3xl md:text-4xl lg:text-5xl font-bold\">\r\n {t(\"title\", \"About Our Company\")}\r\n </h2>\r\n <p className=\"text-muted-foreground text-lg\">\r\n {t(\"description\", \"We are a passionate team dedicated to creating innovative solutions that empower businesses to thrive in the digital age. Our mission is to deliver excellence in everything we do.\")}\r\n </p>\r\n </div>\r\n\r\n {/* Images Grid */}\r\n <div className=\"grid lg:grid-cols-3 gap-6 mb-16\">\r\n {/* Main Image */}\r\n <div className=\"lg:col-span-2\">\r\n <div className=\"aspect-[16/10] rounded-2xl bg-muted overflow-hidden\">\r\n <img\r\n src=\"/images/placeholder.png\"\r\n alt={t(\"mainImageAlt\", \"Our team\")}\r\n className=\"w-full h-full object-cover\"\r\n onError={(e) => {\r\n e.currentTarget.style.display = \"none\";\r\n }}\r\n />\r\n </div>\r\n </div>\r\n\r\n {/* Side Column */}\r\n <div className=\"flex flex-col gap-6\">\r\n {/* Info Card */}\r\n <div className=\"bg-muted rounded-2xl p-6 flex flex-col justify-between flex-1\">\r\n <div>\r\n <h3 className=\"text-lg font-semibold mb-2\">\r\n {t(\"cardTitle\", \"Our Mission\")}\r\n </h3>\r\n <p className=\"text-muted-foreground text-sm mb-4\">\r\n {t(\"cardDescription\", \"Providing businesses with effective tools to improve workflows, boost efficiency, and encourage sustainable growth.\")}\r\n </p>\r\n </div>\r\n <Button variant=\"outline\" asChild className=\"w-fit\">\r\n <Link to=\"/about\">{t(\"cardButton\", \"Learn More\")}</Link>\r\n </Button>\r\n </div>\r\n\r\n {/* Secondary Image */}\r\n <div className=\"aspect-square rounded-2xl bg-muted overflow-hidden flex-1\">\r\n <img\r\n src=\"/images/placeholder.png\"\r\n alt={t(\"secondaryImageAlt\", \"Our office\")}\r\n className=\"w-full h-full object-cover\"\r\n onError={(e) => {\r\n e.currentTarget.style.display = \"none\";\r\n }}\r\n />\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Companies */}\r\n <div className=\"text-center mb-16\">\r\n <p className=\"text-muted-foreground mb-6\">\r\n {t(\"companiesTitle\", \"Trusted by leading companies worldwide\")}\r\n </p>\r\n <div className=\"flex flex-wrap justify-center items-center gap-8 md:gap-12\">\r\n {companies.map((company) => (\r\n <span\r\n key={company}\r\n className=\"text-xl md:text-2xl font-semibold text-muted-foreground/50\"\r\n >\r\n {company}\r\n </span>\r\n ))}\r\n </div>\r\n </div>\r\n\r\n {/* Stats */}\r\n <div className=\"bg-muted rounded-2xl p-8 md:p-12\">\r\n <div className=\"text-center md:text-left mb-8\">\r\n <h3 className=\"text-2xl md:text-3xl font-bold mb-2\">\r\n {t(\"statsTitle\", \"Our Achievements\")}\r\n </h3>\r\n <p className=\"text-muted-foreground max-w-xl\">\r\n {t(\"statsDescription\", \"Numbers that reflect our commitment to excellence and client satisfaction.\")}\r\n </p>\r\n </div>\r\n <div className=\"grid grid-cols-2 lg:grid-cols-4 gap-8\">\r\n {stats.map((stat, index) => (\r\n <div key={index} className=\"text-center\">\r\n <div className=\"text-3xl md:text-4xl lg:text-5xl font-bold text-primary mb-2\">\r\n {stat.value}\r\n </div>\r\n <p className=\"text-sm md:text-base text-muted-foreground\">\r\n {stat.label}\r\n </p>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
19
+ "content": "import { Link } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Button } from \"@/components/ui/button\";\r\n\r\ninterface AboutSectionProps {\r\n className?: string;\r\n}\r\n\r\nexport function AboutSection({ className }: AboutSectionProps) {\r\n const { t } = useTranslation(\"about-section\");\r\n\r\n const stats = [\r\n { value: t(\"stat1Value\", \"500+\"), label: t(\"stat1Label\", \"Happy Clients\") },\r\n { value: t(\"stat2Value\", \"1000+\"), label: t(\"stat2Label\", \"Projects Completed\") },\r\n { value: t(\"stat3Value\", \"99%\"), label: t(\"stat3Label\", \"Satisfaction Rate\") },\r\n { value: t(\"stat4Value\", \"15+\"), label: t(\"stat4Label\", \"Years Experience\") },\r\n ];\r\n\r\n const companies = [\"Google\", \"Microsoft\", \"Amazon\", \"Apple\", \"Meta\"];\r\n\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Header */}\r\n <div className=\"grid md:grid-cols-2 gap-8 mb-12\">\r\n <h2 className=\"text-3xl md:text-4xl lg:text-5xl font-bold\">\r\n {t(\"title\", \"About Our Company\")}\r\n </h2>\r\n <p className=\"text-muted-foreground text-lg\">\r\n {t(\"description\", \"We are a passionate team dedicated to creating innovative solutions that empower businesses to thrive in the digital age. Our mission is to deliver excellence in everything we do.\")}\r\n </p>\r\n </div>\r\n\r\n {/* Images Grid */}\r\n <div className=\"grid lg:grid-cols-3 gap-6 mb-16\">\r\n {/* Main Image */}\r\n <div className=\"lg:col-span-2\">\r\n <div className=\"aspect-[16/10] rounded-2xl bg-muted overflow-hidden\">\r\n <img\r\n src=\"/images/placeholder.png\"\r\n alt={t(\"mainImageAlt\", \"Our team\")}\r\n className=\"w-full h-full object-cover\"\r\n onError={(e) => {\r\n e.currentTarget.style.display = \"none\";\r\n }}\r\n />\r\n </div>\r\n </div>\r\n\r\n {/* Side Column */}\r\n <div className=\"flex flex-col gap-6\">\r\n {/* Info Card */}\r\n <div className=\"bg-muted rounded-2xl p-6 flex flex-col justify-between flex-1\">\r\n <div>\r\n <h3 className=\"text-lg font-semibold mb-2\">\r\n {t(\"cardTitle\", \"Our Mission\")}\r\n </h3>\r\n <p className=\"text-muted-foreground text-sm mb-4\">\r\n {t(\"cardDescription\", \"Providing businesses with effective tools to improve workflows, boost efficiency, and encourage sustainable growth.\")}\r\n </p>\r\n </div>\r\n <Button variant=\"outline\" asChild className=\"w-fit\">\r\n <Link to=\"/about\">{t(\"cardButton\", \"Learn More\")}</Link>\r\n </Button>\r\n </div>\r\n\r\n {/* Secondary Image */}\r\n <div className=\"aspect-square rounded-2xl bg-muted overflow-hidden flex-1\">\r\n <img\r\n src=\"/images/placeholder.png\"\r\n alt={t(\"secondaryImageAlt\", \"Our office\")}\r\n className=\"w-full h-full object-cover\"\r\n onError={(e) => {\r\n e.currentTarget.style.display = \"none\";\r\n }}\r\n />\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Companies */}\r\n <div className=\"text-center mb-16\">\r\n <p className=\"text-muted-foreground mb-6\">\r\n {t(\"companiesTitle\", \"Trusted by leading companies worldwide\")}\r\n </p>\r\n <div className=\"flex flex-wrap justify-center items-center gap-8 md:gap-12\">\r\n {companies.map((company) => (\r\n <span\r\n key={company}\r\n className=\"text-xl md:text-2xl font-semibold text-muted-foreground/50\"\r\n >\r\n {company}\r\n </span>\r\n ))}\r\n </div>\r\n </div>\r\n\r\n {/* Stats */}\r\n <div className=\"bg-muted rounded-2xl p-8 md:p-12\">\r\n <div className=\"text-center md:text-left mb-8\">\r\n <h3 className=\"text-2xl md:text-3xl font-bold mb-2\">\r\n {t(\"statsTitle\", \"Our Achievements\")}\r\n </h3>\r\n <p className=\"text-muted-foreground max-w-xl\">\r\n {t(\"statsDescription\", \"Numbers that reflect our commitment to excellence and client satisfaction.\")}\r\n </p>\r\n </div>\r\n <div className=\"grid grid-cols-2 lg:grid-cols-4 gap-8\">\r\n {stats.map((stat, index) => (\r\n <div key={index} className=\"text-center\">\r\n <div className=\"text-3xl md:text-4xl lg:text-5xl font-bold text-primary mb-2\">\r\n {stat.value}\r\n </div>\r\n <p className=\"text-sm md:text-base text-muted-foreground\">\r\n {stat.label}\r\n </p>\r\n </div>\r\n ))}\r\n </div>\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
20
20
  },
21
21
  {
22
22
  "path": "about-section/lang/en.json",
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "api",
3
+ "type": "registry:module",
4
+ "title": "API Client",
5
+ "description": "Pre-configured API client using @promakeai/customer-backend-client. Includes multi-language validation messages, automatic token management, and VITE_API_CUSTOMER / VITE_TENANT_UUID environment variable support.",
6
+ "dependencies": [
7
+ "@promakeai/customer-backend-client"
8
+ ],
9
+ "registryDependencies": [],
10
+ "envVars": {
11
+ "VITE_API_CUSTOMER": "https://your-tenant.backend.promake.ai/",
12
+ "VITE_TENANT_UUID": "your-tenant-uuid"
13
+ },
14
+ "files": [
15
+ {
16
+ "path": "api/index.ts",
17
+ "type": "registry:index",
18
+ "target": "$modules$/api/index.ts",
19
+ "content": "export { customerClient } from \"./customer-client\";\r\nexport type { ApiClient } from \"./customer-client\";\r\nexport { getErrorMessage } from \"./get-error-message\";\r\n"
20
+ },
21
+ {
22
+ "path": "api/customer-client.ts",
23
+ "type": "registry:lib",
24
+ "target": "$modules$/api/customer-client.ts",
25
+ "content": "import {\n createClient,\n type ApiClient,\n} from \"@promakeai/customer-backend-client\";\nimport enValidation from \"./validation/en.json\";\nimport trValidation from \"./validation/tr.json\";\n\nconst customerClient = createClient({\n baseURL:\n import.meta.env.VITE_API_CUSTOMER ||\n `https://${import.meta.env.VITE_TENANT_UUID}.backend.promake.ai/`,\n messages: {\n en: enValidation,\n tr: trValidation,\n },\n defaultLanguage: \"en\",\n});\n\nexport { customerClient };\nexport type { ApiClient };\n"
26
+ },
27
+ {
28
+ "path": "api/get-error-message.ts",
29
+ "type": "registry:lib",
30
+ "target": "$modules$/api/get-error-message.ts",
31
+ "content": "export function getErrorMessage(\n err: unknown,\n fallback: string = \"An error occurred\",\n): string {\n if (err && typeof err === \"object\") {\n const e = err as {\n response?: { data?: { error?: string; message?: string } };\n message?: string;\n };\n return (\n e.response?.data?.message ||\n e.response?.data?.error ||\n e.message ||\n fallback\n );\n }\n return fallback;\n}\n"
32
+ },
33
+ {
34
+ "path": "api/validation/en.json",
35
+ "type": "registry:lang",
36
+ "target": "$modules$/api/validation/en.json",
37
+ "content": "{\n \"username_required\": \"Username is required\",\n \"username_min_length\": \"Username must be at least 3 characters\",\n \"email_invalid\": \"Invalid email address\",\n \"email_required\": \"Email is required\",\n \"password_required\": \"Password is required\",\n \"password_min_length\": \"Password must be at least 8 characters\",\n \"password_letter_required\": \"Password must contain at least 1 letter\",\n \"password_number_required\": \"Password must contain at least 1 number\",\n \"code_required\": \"Verification code is required\",\n \"reset_code_required\": \"Reset code is required\",\n \"session_id_required\": \"Session ID is required\",\n \"quantity_positive\": \"Quantity must be a positive integer\",\n \"product_name_required\": \"Product name is required\",\n \"product_description_required\": \"Product description is required\",\n \"amount_positive\": \"Amount must be a positive integer\",\n \"image_required\": \"Image URL is required\",\n \"firstname_required\": \"First name is required\",\n \"lastname_required\": \"Last name is required\",\n \"phone_required\": \"Phone number is required\",\n \"address_required\": \"Address is required\",\n \"country_required\": \"Country is required\",\n \"city_required\": \"City is required\",\n \"zip_required\": \"Zip code is required\",\n \"currency_required\": \"Currency is required\",\n \"tax_amount_non_negative\": \"Tax amount must be non-negative\",\n \"at_least_one_product\": \"At least one product is required\",\n \"payment_type_invalid\": \"Payment type must be 'stripe', 'iyzico', 'bank_transfer', or 'cash_on_delivery'\"\n}\n"
38
+ },
39
+ {
40
+ "path": "api/validation/tr.json",
41
+ "type": "registry:lang",
42
+ "target": "$modules$/api/validation/tr.json",
43
+ "content": "{\n \"username_required\": \"Kullanıcı adı zorunludur\",\n \"username_min_length\": \"Kullanıcı adı en az 3 karakter olmalıdır\",\n \"email_invalid\": \"Geçersiz e-posta adresi\",\n \"email_required\": \"E-posta zorunludur\",\n \"password_required\": \"Şifre zorunludur\",\n \"password_min_length\": \"Şifre en az 8 karakter olmalıdır\",\n \"password_letter_required\": \"Şifre en az 1 harf içermelidir\",\n \"password_number_required\": \"Şifre en az 1 rakam içermelidir\",\n \"code_required\": \"Doğrulama kodu zorunludur\",\n \"reset_code_required\": \"Sıfırlama kodu zorunludur\",\n \"session_id_required\": \"Oturum ID zorunludur\",\n \"quantity_positive\": \"Miktar pozitif bir tam sayı olmalıdır\",\n \"product_name_required\": \"Ürün adı zorunludur\",\n \"product_description_required\": \"Ürün açıklaması zorunludur\",\n \"amount_positive\": \"Tutar pozitif bir tam sayı olmalıdır\",\n \"image_required\": \"Görsel URL zorunludur\",\n \"firstname_required\": \"Ad zorunludur\",\n \"lastname_required\": \"Soyad zorunludur\",\n \"phone_required\": \"Telefon numarası zorunludur\",\n \"address_required\": \"Adres zorunludur\",\n \"country_required\": \"Ülke zorunludur\",\n \"city_required\": \"Şehir zorunludur\",\n \"zip_required\": \"Posta kodu zorunludur\",\n \"currency_required\": \"Para birimi zorunludur\",\n \"tax_amount_non_negative\": \"Vergi tutarı negatif olamaz\",\n \"at_least_one_product\": \"En az bir ürün gereklidir\",\n \"payment_type_invalid\": \"Ödeme tipi 'stripe', 'iyzico', 'bank_transfer' veya 'cash_on_delivery' olmalıdır\"\n}\n"
44
+ }
45
+ ],
46
+ "exports": {
47
+ "types": [
48
+ "ApiClient"
49
+ ],
50
+ "variables": [
51
+ "customerClient",
52
+ "getErrorMessage"
53
+ ]
54
+ }
55
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "auth",
3
+ "type": "registry:module",
4
+ "title": "Authentication Module",
5
+ "description": "Complete authentication system with Zustand store, JWT token management with auto-refresh, login/register/forgot-password pages, and header menu component. Includes secure token storage, automatic 401 handling, and seamless API integration.",
6
+ "dependencies": [
7
+ "zustand"
8
+ ],
9
+ "registryDependencies": [
10
+ "api"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "auth/index.ts",
15
+ "type": "registry:index",
16
+ "target": "$modules$/auth/index.ts",
17
+ "content": "// Store\r\nexport { useAuthStore } from \"./auth-store\";\r\nexport type { User, AuthTokens } from \"./auth-store\";\r\n\r\n// Hook\r\nexport { useAuth } from \"./use-auth\";\r\n\r\n// Components\r\nexport { AuthHeaderMenu } from \"./auth-header-menu\";\r\nexport { LoginPage } from \"./login-page\";\r\nexport { RegisterPage } from \"./register-page\";\r\nexport { ForgotPasswordPage } from \"./forgot-password-page\";\r\n"
18
+ },
19
+ {
20
+ "path": "auth/auth-store.ts",
21
+ "type": "registry:store",
22
+ "target": "$modules$/auth/auth-store.ts",
23
+ "content": "import { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\n\nexport interface User {\n username: string;\n email?: string;\n}\n\nexport interface AuthTokens {\n accessToken: string;\n refreshToken?: string;\n idToken?: string;\n encryptionKey?: string;\n expiresAt?: number; // Unix timestamp in milliseconds\n}\n\ninterface AuthState {\n user: User | null;\n tokens: AuthTokens | null;\n isAuthenticated: boolean;\n setAuth: (user: User, tokens: AuthTokens) => void;\n updateTokens: (tokens: AuthTokens) => void;\n clearAuth: () => void;\n isTokenExpired: () => boolean;\n getTimeUntilExpiry: () => number | null;\n}\n\nexport const useAuthStore = create<AuthState>()(\n persist(\n (set, get) => ({\n user: null,\n tokens: null,\n isAuthenticated: false,\n\n setAuth: (user, tokens) => set({ user, tokens, isAuthenticated: true }),\n\n updateTokens: (tokens) => set({ tokens }),\n\n clearAuth: () =>\n set({ user: null, tokens: null, isAuthenticated: false }),\n\n isTokenExpired: () => {\n const { tokens } = get();\n if (!tokens?.expiresAt) return false;\n // Consider token expired 30 seconds before actual expiry for safety margin\n return Date.now() >= tokens.expiresAt - 30000;\n },\n\n getTimeUntilExpiry: () => {\n const { tokens } = get();\n if (!tokens?.expiresAt) return null;\n return tokens.expiresAt - Date.now();\n },\n }),\n { name: \"auth-storage\" },\n ),\n);\n"
24
+ },
25
+ {
26
+ "path": "auth/use-auth.ts",
27
+ "type": "registry:hook",
28
+ "target": "$modules$/auth/use-auth.ts",
29
+ "content": "import { useCallback, useEffect, useRef } from \"react\";\nimport {\n useAuthStore,\n type User,\n type AuthTokens,\n} from \"@/modules/auth/auth-store\";\nimport { customerClient } from \"@/modules/api/customer-client\";\n\n// Refresh token 1 minute before expiry\nconst REFRESH_BUFFER_MS = 60 * 1000;\n\nexport function useAuth() {\n const {\n user,\n tokens,\n isAuthenticated,\n setAuth,\n updateTokens,\n clearAuth,\n isTokenExpired,\n getTimeUntilExpiry,\n } = useAuthStore();\n\n const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const isRefreshingRef = useRef(false);\n\n // Refresh token using the refresh token\n const refreshAccessToken = useCallback(async (): Promise<boolean> => {\n const currentTokens = useAuthStore.getState().tokens;\n\n // Don't refresh if no refresh token exists\n if (!currentTokens?.refreshToken || isRefreshingRef.current) {\n console.log(\"⚠️ No refresh token available, skipping refresh\");\n return false;\n }\n\n isRefreshingRef.current = true;\n\n try {\n // Make a refresh request using the axios instance directly\n const response = await customerClient.axios.post<{\n accessToken: string;\n refreshToken?: string;\n expiresIn?: number;\n }>(\"/auth/refresh\", {\n refreshToken: currentTokens.refreshToken,\n });\n\n const { accessToken, refreshToken, expiresIn } = response.data;\n\n // Validate response has required data\n if (!accessToken) {\n console.error(\"❌ Refresh response missing accessToken\");\n return false;\n }\n\n const newTokens: AuthTokens = {\n accessToken,\n refreshToken: refreshToken || currentTokens.refreshToken,\n idToken: currentTokens.idToken, // Preserve existing idToken\n encryptionKey: currentTokens.encryptionKey, // Preserve existing encryptionKey\n expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : undefined,\n };\n\n customerClient.setToken(accessToken);\n updateTokens(newTokens);\n\n console.log(\"✅ Token refreshed successfully\");\n return true;\n } catch (error) {\n console.error(\"❌ Token refresh failed:\", error);\n // DON'T clear auth on refresh failure - just return false\n // User can still use their existing token until it expires\n return false;\n } finally {\n isRefreshingRef.current = false;\n }\n }, [updateTokens]);\n\n // Schedule automatic token refresh\n const scheduleTokenRefresh = useCallback(() => {\n // Clear any existing timeout\n if (refreshTimeoutRef.current) {\n clearTimeout(refreshTimeoutRef.current);\n refreshTimeoutRef.current = null;\n }\n\n const timeUntilExpiry = getTimeUntilExpiry();\n\n // Only schedule if we have an expiry time and a refresh token\n if (timeUntilExpiry === null || !tokens?.refreshToken) {\n return;\n }\n\n // Calculate when to refresh (REFRESH_BUFFER_MS before expiry)\n const refreshIn = Math.max(timeUntilExpiry - REFRESH_BUFFER_MS, 0);\n\n // Don't schedule if expiry is too far in the future (> 24 hours)\n if (refreshIn > 24 * 60 * 60 * 1000) {\n return;\n }\n\n refreshTimeoutRef.current = setTimeout(async () => {\n const success = await refreshAccessToken();\n if (success) {\n // Reschedule for the new token\n scheduleTokenRefresh();\n }\n }, refreshIn);\n }, [getTimeUntilExpiry, tokens?.refreshToken, refreshAccessToken]);\n\n // Sync token with API client and set up refresh on mount and token changes\n useEffect(() => {\n if (tokens?.accessToken) {\n console.log(\"🔑 Setting token in API client\");\n customerClient.setToken(tokens.accessToken);\n\n // Only try to refresh if we have a refresh token AND token is expired\n if (isTokenExpired() && tokens.refreshToken) {\n console.log(\"⏰ Token expired, attempting refresh...\");\n refreshAccessToken().then((success) => {\n if (success) {\n scheduleTokenRefresh();\n } else {\n console.log(\"⚠️ Refresh failed, but keeping existing token\");\n }\n });\n } else if (tokens.refreshToken) {\n // Only schedule refresh if we have a refresh token\n scheduleTokenRefresh();\n }\n } else if (tokens && Object.keys(tokens).length === 0) {\n // tokens is empty object {} - this shouldn't happen, log it\n console.warn(\"⚠️ Tokens object is empty, this may indicate a bug\");\n } else {\n customerClient.setToken(null);\n }\n\n // Cleanup timeout on unmount\n return () => {\n if (refreshTimeoutRef.current) {\n clearTimeout(refreshTimeoutRef.current);\n }\n };\n }, [\n tokens?.accessToken,\n tokens?.refreshToken,\n isTokenExpired,\n refreshAccessToken,\n scheduleTokenRefresh,\n ]);\n\n // Set up axios interceptor for 401 responses (token expired during request)\n useEffect(() => {\n const interceptorId = customerClient.axios.interceptors.response.use(\n (response) => response,\n async (error) => {\n const originalRequest = error.config;\n\n // Skip refresh for auth endpoints to prevent infinite loops\n const isAuthEndpoint = originalRequest?.url?.includes(\"/auth/\");\n\n // If we get a 401 and haven't retried yet, try to refresh\n if (\n error.response?.status === 401 &&\n !originalRequest._retry &&\n tokens?.refreshToken &&\n !isAuthEndpoint\n ) {\n originalRequest._retry = true;\n\n const success = await refreshAccessToken();\n if (success) {\n // Retry the original request with new token\n const newTokens = useAuthStore.getState().tokens;\n if (newTokens?.accessToken) {\n originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`;\n return customerClient.axios(originalRequest);\n }\n }\n }\n\n return Promise.reject(error);\n },\n );\n\n return () => {\n customerClient.axios.interceptors.response.eject(interceptorId);\n };\n }, [tokens?.refreshToken, refreshAccessToken]);\n\n const login = useCallback(async (username: string, password: string) => {\n const response = await customerClient.auth.login({ username, password });\n\n console.log(\"🔐 Login response:\", response);\n console.log(\"🔐 accessToken:\", response.accessToken);\n console.log(\"🔐 refreshToken:\", response.refreshToken);\n console.log(\"🔐 encryptionKey:\", response.encryptionKey);\n\n const newTokens: AuthTokens = {\n accessToken: response.accessToken,\n refreshToken: response.refreshToken,\n idToken: response.idToken,\n encryptionKey: response.encryptionKey,\n expiresAt: response.expiresIn\n ? Date.now() + response.expiresIn * 1000\n : undefined,\n };\n\n console.log(\"🔐 newTokens object:\", newTokens);\n\n const newUser: User = {\n username,\n email: (response as any).email || (response as any).user?.email,\n };\n\n customerClient.setToken(newTokens.accessToken);\n setAuth(newUser, newTokens);\n\n console.log(\n \"🔐 Auth set complete, checking store:\",\n useAuthStore.getState().tokens,\n );\n }, []);\n\n const register = useCallback(\n async (username: string, email: string, password: string) => {\n await customerClient.auth.register({ username, email, password });\n },\n [],\n );\n\n const confirmEmail = useCallback(async (username: string, code: string) => {\n await customerClient.auth.confirm({ username, code });\n }, []);\n\n const forgotPassword = useCallback(async (username: string) => {\n await customerClient.auth.forgotPassword({ username });\n }, []);\n\n const resetPassword = useCallback(\n async (username: string, code: string, newPassword: string) => {\n await customerClient.auth.resetPassword({ username, code, newPassword });\n },\n [],\n );\n\n const logout = useCallback(() => {\n // Clear any scheduled refresh\n if (refreshTimeoutRef.current) {\n clearTimeout(refreshTimeoutRef.current);\n refreshTimeoutRef.current = null;\n }\n\n customerClient.setToken(null);\n clearAuth();\n }, [clearAuth]);\n\n return {\n user,\n token: tokens?.accessToken ?? null,\n tokens,\n isAuthenticated,\n api: customerClient,\n login,\n register,\n confirmEmail,\n forgotPassword,\n resetPassword,\n logout,\n refreshAccessToken,\n };\n}\n"
30
+ },
31
+ {
32
+ "path": "auth/auth-header-menu.tsx",
33
+ "type": "registry:component",
34
+ "target": "$modules$/auth/auth-header-menu.tsx",
35
+ "content": "import type { ReactNode } from \"react\";\nimport { Link } from \"react-router\";\nimport { User, LogOut } from \"lucide-react\";\nimport { useAuth } from \"@/modules/auth/use-auth\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { useTranslation } from \"react-i18next\";\nimport { toast } from \"sonner\";\n\ninterface AuthHeaderMenuProps {\n children?: ReactNode;\n variant: \"desktop\" | \"mobile\";\n onMenuClose?: () => void;\n}\n\nexport function AuthHeaderMenu({\n children,\n variant,\n onMenuClose,\n}: AuthHeaderMenuProps) {\n const { isAuthenticated, user, logout } = useAuth();\n const { t } = useTranslation(\"header\");\n\n const handleLogout = () => {\n logout();\n toast.success(t(\"logoutToastTitle\", \"Goodbye!\"), {\n description: t(\n \"logoutToastDesc\",\n \"You have been logged out successfully.\",\n ),\n });\n onMenuClose?.();\n };\n\n if (variant === \"desktop\") {\n if (isAuthenticated) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\n <User className=\"h-5 w-5\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-56\">\n <DropdownMenuLabel className=\"font-normal\">\n <div className=\"flex flex-col space-y-1\">\n <p className=\"text-sm font-medium\">{user?.username}</p>\n {user?.email && (\n <p className=\"text-xs text-muted-foreground\">{user.email}</p>\n )}\n </div>\n </DropdownMenuLabel>\n <DropdownMenuSeparator />\n {children}\n {children && <DropdownMenuSeparator />}\n <DropdownMenuItem\n onClick={handleLogout}\n className=\"text-red-600 focus:text-red-600 focus:bg-red-50 cursor-pointer\"\n >\n <LogOut className=\"mr-2 h-4 w-4\" />\n {t(\"logout\", \"Logout\")}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n }\n\n return (\n <Link to=\"/login\">\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\n <User className=\"h-5 w-5\" />\n </Button>\n </Link>\n );\n }\n\n // Mobile variant\n if (isAuthenticated) {\n return (\n <div className=\"space-y-3\">\n <div className=\"flex items-center space-x-3 p-3 bg-muted/50 rounded-lg\">\n <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center\">\n <User className=\"h-5 w-5 text-primary\" />\n </div>\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">{user?.username}</p>\n {user?.email && (\n <p className=\"text-xs text-muted-foreground truncate\">\n {user.email}\n </p>\n )}\n </div>\n </div>\n {children}\n <button\n onClick={handleLogout}\n className=\"flex items-center space-x-2 text-lg font-medium text-red-600 hover:text-red-700 transition-colors w-full\"\n >\n <LogOut className=\"h-5 w-5\" />\n <span>{t(\"logout\", \"Logout\")}</span>\n </button>\n </div>\n );\n }\n\n return (\n <Link\n to=\"/login\"\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\n onClick={onMenuClose}\n >\n <User className=\"h-5 w-5\" />\n <span>{t(\"login\", \"Login\")}</span>\n </Link>\n );\n}\n"
36
+ },
37
+ {
38
+ "path": "auth/login-page.tsx",
39
+ "type": "registry:page",
40
+ "target": "$modules$/auth/login-page.tsx",
41
+ "content": "import { useState, useEffect } from \"react\";\nimport { Link, useNavigate } from \"react-router\";\nimport { toast } from \"sonner\";\nimport { Layout } from \"@/components/Layout\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { useAuth } from \"@/modules/auth/use-auth\";\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { LogIn, Eye, EyeOff } from \"lucide-react\";\n\nexport default function LoginPage() {\n const { t } = useTranslation(\"login\");\n usePageTitle({ title: t(\"title\") });\n\n const navigate = useNavigate();\n const { login, isAuthenticated } = useAuth();\n\n const [formData, setFormData] = useState({\n username: \"\",\n password: \"\",\n });\n const [showPassword, setShowPassword] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n // Redirect when authenticated (works for both initial load and after login)\n useEffect(() => {\n if (isAuthenticated) {\n navigate(\"/\", { replace: true });\n }\n }, [isAuthenticated, navigate]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setError(null);\n\n try {\n await login(formData.username, formData.password);\n toast.success(t(\"toastSuccessTitle\", \"Welcome back!\"), {\n description: t(\"toastSuccessDesc\", \"You have successfully logged in.\"),\n });\n navigate(\"/\", { replace: true });\n } catch (err) {\n const errorMessage = getErrorMessage(err, t(\"errorGeneric\"));\n setError(errorMessage);\n toast.error(t(\"toastErrorTitle\", \"Login failed\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n {/* Hero Section */}\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\")}\n </h1>\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\n {t(\"description\")}\n </p>\n </div>\n\n <div className=\"max-w-md mx-auto\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <LogIn className=\"w-5 h-5 text-primary\" />\n {t(\"cardTitle\")}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n <div>\n <Label htmlFor=\"username\">{t(\"username\")} *</Label>\n <Input\n id=\"username\"\n name=\"username\"\n type=\"text\"\n value={formData.username}\n onChange={handleChange}\n placeholder={t(\"usernamePlaceholder\")}\n required\n className=\"mt-1\"\n autoComplete=\"username\"\n />\n </div>\n\n <div>\n <div className=\"flex items-center justify-between\">\n <Label htmlFor=\"password\">{t(\"password\")} *</Label>\n <Link\n to=\"/forgot-password\"\n className=\"text-sm text-primary hover:underline\"\n >\n {t(\"forgotPassword\", \"Forgot password?\")}\n </Link>\n </div>\n <div className=\"relative\">\n <Input\n id=\"password\"\n name=\"password\"\n type={showPassword ? \"text\" : \"password\"}\n value={formData.password}\n onChange={handleChange}\n placeholder={t(\"passwordPlaceholder\")}\n required\n className=\"mt-1 pr-10\"\n autoComplete=\"current-password\"\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n >\n {showPassword ? (\n <EyeOff className=\"w-4 h-4\" />\n ) : (\n <Eye className=\"w-4 h-4\" />\n )}\n </button>\n </div>\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\n <p className=\"text-red-800 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"submitting\")}\n </>\n ) : (\n t(\"submit\")\n )}\n </Button>\n\n <div className=\"text-center text-sm text-muted-foreground\">\n {t(\"noAccount\")}{\" \"}\n <Link\n to=\"/register\"\n className=\"text-primary hover:underline font-medium\"\n >\n {t(\"registerLink\")}\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n"
42
+ },
43
+ {
44
+ "path": "auth/register-page.tsx",
45
+ "type": "registry:page",
46
+ "target": "$modules$/auth/register-page.tsx",
47
+ "content": "import { useState, useEffect } from \"react\";\nimport { Link, useNavigate } from \"react-router\";\nimport { toast } from \"sonner\";\nimport { Layout } from \"@/components/Layout\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { useAuth } from \"@/modules/auth/use-auth\";\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { UserPlus, Eye, EyeOff, CheckCircle } from \"lucide-react\";\n\nexport default function RegisterPage() {\n const { t } = useTranslation(\"register\");\n usePageTitle({ title: t(\"title\") });\n\n const navigate = useNavigate();\n const { register, isAuthenticated } = useAuth();\n\n const [formData, setFormData] = useState({\n username: \"\",\n email: \"\",\n password: \"\",\n confirmPassword: \"\",\n });\n const [showPassword, setShowPassword] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState(false);\n\n // Redirect if already authenticated\n useEffect(() => {\n if (isAuthenticated) {\n navigate(\"/\", { replace: true });\n }\n }, [isAuthenticated, navigate]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setError(null);\n\n // Validate passwords match\n if (formData.password !== formData.confirmPassword) {\n setError(t(\"passwordMismatch\"));\n toast.error(t(\"toastErrorTitle\", \"Registration failed\"), {\n description: t(\"passwordMismatch\"),\n });\n setIsSubmitting(false);\n return;\n }\n\n try {\n await register(formData.username, formData.email, formData.password);\n setSuccess(true);\n toast.success(t(\"toastSuccessTitle\", \"Account created!\"), {\n description: t(\n \"toastSuccessDesc\",\n \"Please check your email to verify your account.\",\n ),\n });\n } catch (err) {\n const errorMessage = getErrorMessage(err, t(\"errorGeneric\"));\n setError(errorMessage);\n toast.error(t(\"toastErrorTitle\", \"Registration failed\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n if (success) {\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n <div className=\"max-w-md mx-auto\">\n <Card>\n <CardContent className=\"pt-6\">\n <div className=\"text-center space-y-4\">\n <CheckCircle className=\"w-16 h-16 text-green-500 mx-auto\" />\n <h2 className=\"text-2xl font-bold text-foreground\">\n {t(\"successTitle\")}\n </h2>\n <p className=\"text-muted-foreground\">\n {t(\"successMessage\")}\n </p>\n <Button asChild className=\"mt-4\">\n <Link to=\"/login\">{t(\"goToLogin\")}</Link>\n </Button>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n {/* Hero Section */}\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\")}\n </h1>\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\n {t(\"description\")}\n </p>\n </div>\n\n <div className=\"max-w-md mx-auto\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <UserPlus className=\"w-5 h-5 text-primary\" />\n {t(\"cardTitle\")}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n <div>\n <Label htmlFor=\"username\">{t(\"username\")} *</Label>\n <Input\n id=\"username\"\n name=\"username\"\n type=\"text\"\n value={formData.username}\n onChange={handleChange}\n placeholder={t(\"usernamePlaceholder\")}\n required\n className=\"mt-1\"\n autoComplete=\"username\"\n />\n </div>\n\n <div>\n <Label htmlFor=\"email\">{t(\"email\")} *</Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1\"\n autoComplete=\"email\"\n />\n </div>\n\n <div>\n <Label htmlFor=\"password\">{t(\"password\")} *</Label>\n <div className=\"relative\">\n <Input\n id=\"password\"\n name=\"password\"\n type={showPassword ? \"text\" : \"password\"}\n value={formData.password}\n onChange={handleChange}\n placeholder={t(\"passwordPlaceholder\")}\n required\n className=\"mt-1 pr-10\"\n autoComplete=\"new-password\"\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n >\n {showPassword ? (\n <EyeOff className=\"w-4 h-4\" />\n ) : (\n <Eye className=\"w-4 h-4\" />\n )}\n </button>\n </div>\n </div>\n\n <div>\n <Label htmlFor=\"confirmPassword\">\n {t(\"confirmPassword\")} *\n </Label>\n <Input\n id=\"confirmPassword\"\n name=\"confirmPassword\"\n type={showPassword ? \"text\" : \"password\"}\n value={formData.confirmPassword}\n onChange={handleChange}\n placeholder={t(\"confirmPasswordPlaceholder\")}\n required\n className=\"mt-1\"\n autoComplete=\"new-password\"\n />\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\n <p className=\"text-red-800 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"submitting\")}\n </>\n ) : (\n t(\"submit\")\n )}\n </Button>\n\n <div className=\"text-center text-sm text-muted-foreground\">\n {t(\"hasAccount\")}{\" \"}\n <Link\n to=\"/login\"\n className=\"text-primary hover:underline font-medium\"\n >\n {t(\"loginLink\")}\n </Link>\n </div>\n </form>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n"
48
+ },
49
+ {
50
+ "path": "auth/forgot-password-page.tsx",
51
+ "type": "registry:page",
52
+ "target": "$modules$/auth/forgot-password-page.tsx",
53
+ "content": "import { useState } from \"react\";\nimport { Link } from \"react-router\";\nimport { toast } from \"sonner\";\nimport { Layout } from \"@/components/Layout\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { useAuth } from \"@/modules/auth/use-auth\";\nimport { getErrorMessage } from \"@/modules/api/get-error-message\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n CardDescription,\n} from \"@/components/ui/card\";\nimport { KeyRound, ArrowLeft, Eye, EyeOff, CheckCircle2 } from \"lucide-react\";\n\ntype Step = \"request\" | \"reset\" | \"success\";\n\nexport default function ForgotPasswordPage() {\n const { t } = useTranslation(\"forgotPassword\");\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\n\n const { forgotPassword, resetPassword } = useAuth();\n\n const [step, setStep] = useState<Step>(\"request\");\n const [username, setUsername] = useState(\"\");\n const [code, setCode] = useState(\"\");\n const [newPassword, setNewPassword] = useState(\"\");\n const [confirmPassword, setConfirmPassword] = useState(\"\");\n const [showPassword, setShowPassword] = useState(false);\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const handleRequestCode = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setError(null);\n\n try {\n await forgotPassword(username);\n toast.success(t(\"codeSentTitle\", \"Code Sent!\"), {\n description: t(\n \"codeSentDesc\",\n \"A password reset code has been sent to your email.\",\n ),\n });\n setStep(\"reset\");\n } catch (err) {\n const errorMessage = getErrorMessage(\n err,\n t(\"errorGeneric\", \"Failed to send reset code. Please try again.\"),\n );\n setError(errorMessage);\n toast.error(t(\"errorTitle\", \"Error\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleResetPassword = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n\n // Validate passwords match\n if (newPassword !== confirmPassword) {\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\n return;\n }\n\n setIsSubmitting(true);\n\n try {\n await resetPassword(username, code, newPassword);\n toast.success(t(\"resetSuccessTitle\", \"Password Reset!\"), {\n description: t(\n \"resetSuccessDesc\",\n \"Your password has been successfully reset.\",\n ),\n });\n setStep(\"success\");\n } catch (err) {\n const errorMessage = getErrorMessage(\n err,\n t(\"errorResetGeneric\", \"Failed to reset password. Please try again.\"),\n );\n setError(errorMessage);\n toast.error(t(\"errorTitle\", \"Error\"), {\n description: errorMessage,\n });\n } finally {\n setIsSubmitting(false);\n }\n };\n\n // Success step\n if (step === \"success\") {\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n <div className=\"max-w-md mx-auto\">\n <Card>\n <CardContent className=\"pt-8 pb-8 text-center\">\n <CheckCircle2 className=\"w-16 h-16 text-green-500 mx-auto mb-4\" />\n <h1 className=\"text-2xl font-bold mb-2\">\n {t(\"successTitle\", \"Password Reset Successfully!\")}\n </h1>\n <p className=\"text-muted-foreground mb-6\">\n {t(\n \"successDescription\",\n \"Your password has been changed. You can now login with your new password.\",\n )}\n </p>\n <Button asChild className=\"w-full\">\n <Link to=\"/login\">{t(\"goToLogin\", \"Go to Login\")}</Link>\n </Button>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n {/* Hero Section */}\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\", \"Forgot Password\")}\n </h1>\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\n {step === \"request\"\n ? t(\n \"descriptionRequest\",\n \"Enter your username and we'll send you a code to reset your password.\",\n )\n : t(\n \"descriptionReset\",\n \"Enter the code sent to your email and your new password.\",\n )}\n </p>\n </div>\n\n <div className=\"max-w-md mx-auto\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <KeyRound className=\"w-5 h-5 text-primary\" />\n {step === \"request\"\n ? t(\"cardTitleRequest\", \"Request Reset Code\")\n : t(\"cardTitleReset\", \"Reset Password\")}\n </CardTitle>\n <CardDescription>\n {step === \"request\"\n ? t(\"cardDescRequest\", \"Step 1 of 2: Request a reset code\")\n : t(\n \"cardDescReset\",\n \"Step 2 of 2: Enter code and new password\",\n )}\n </CardDescription>\n </CardHeader>\n <CardContent>\n {step === \"request\" ? (\n // Step 1: Request Code\n <form onSubmit={handleRequestCode} className=\"space-y-6\">\n <div>\n <Label htmlFor=\"username\">\n {t(\"username\", \"Username\")} *\n </Label>\n <Input\n id=\"username\"\n type=\"text\"\n value={username}\n onChange={(e) => setUsername(e.target.value)}\n placeholder={t(\n \"usernamePlaceholder\",\n \"Enter your username\",\n )}\n required\n className=\"mt-1\"\n autoComplete=\"username\"\n />\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\n <p className=\"text-red-800 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"sending\", \"Sending...\")}\n </>\n ) : (\n t(\"sendCode\", \"Send Reset Code\")\n )}\n </Button>\n\n <div className=\"text-center\">\n <Link\n to=\"/login\"\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1\"\n >\n <ArrowLeft className=\"w-4 h-4\" />\n {t(\"backToLogin\", \"Back to Login\")}\n </Link>\n </div>\n </form>\n ) : (\n // Step 2: Reset Password\n <form onSubmit={handleResetPassword} className=\"space-y-6\">\n <div className=\"p-3 bg-muted rounded-lg text-sm\">\n <span className=\"text-muted-foreground\">\n {t(\"codeFor\", \"Reset code for:\")}{\" \"}\n </span>\n <span className=\"font-medium\">{username}</span>\n </div>\n\n <div>\n <Label htmlFor=\"code\">{t(\"code\", \"Reset Code\")} *</Label>\n <Input\n id=\"code\"\n type=\"text\"\n value={code}\n onChange={(e) => setCode(e.target.value)}\n placeholder={t(\"codePlaceholder\", \"Enter 6-digit code\")}\n required\n className=\"mt-1\"\n maxLength={6}\n />\n </div>\n\n <div>\n <Label htmlFor=\"newPassword\">\n {t(\"newPassword\", \"New Password\")} *\n </Label>\n <div className=\"relative\">\n <Input\n id=\"newPassword\"\n type={showPassword ? \"text\" : \"password\"}\n value={newPassword}\n onChange={(e) => setNewPassword(e.target.value)}\n placeholder={t(\n \"newPasswordPlaceholder\",\n \"Enter new password\",\n )}\n required\n className=\"mt-1 pr-10\"\n autoComplete=\"new-password\"\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n >\n {showPassword ? (\n <EyeOff className=\"w-4 h-4\" />\n ) : (\n <Eye className=\"w-4 h-4\" />\n )}\n </button>\n </div>\n <p className=\"text-xs text-muted-foreground mt-1\">\n {t(\n \"passwordRequirements\",\n \"At least 8 characters, 1 letter and 1 number\",\n )}\n </p>\n </div>\n\n <div>\n <Label htmlFor=\"confirmPassword\">\n {t(\"confirmPassword\", \"Confirm Password\")} *\n </Label>\n <Input\n id=\"confirmPassword\"\n type={showPassword ? \"text\" : \"password\"}\n value={confirmPassword}\n onChange={(e) => setConfirmPassword(e.target.value)}\n placeholder={t(\n \"confirmPasswordPlaceholder\",\n \"Confirm new password\",\n )}\n required\n className=\"mt-1\"\n autoComplete=\"new-password\"\n />\n </div>\n\n {error && (\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\n <p className=\"text-red-800 text-sm font-medium\">\n {error}\n </p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"resetting\", \"Resetting...\")}\n </>\n ) : (\n t(\"resetPassword\", \"Reset Password\")\n )}\n </Button>\n\n <div className=\"flex justify-between\">\n <button\n type=\"button\"\n onClick={() => {\n setStep(\"request\");\n setCode(\"\");\n setNewPassword(\"\");\n setConfirmPassword(\"\");\n setError(null);\n }}\n className=\"text-sm text-muted-foreground hover:text-primary\"\n >\n {t(\"changeUsername\", \"Change username\")}\n </button>\n <button\n type=\"button\"\n onClick={() =>\n handleRequestCode({\n preventDefault: () => {},\n } as React.FormEvent)\n }\n className=\"text-sm text-primary hover:underline\"\n disabled={isSubmitting}\n >\n {t(\"resendCode\", \"Resend code\")}\n </button>\n </div>\n </form>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n"
54
+ }
55
+ ],
56
+ "exports": {
57
+ "types": [
58
+ "AuthTokens",
59
+ "User"
60
+ ],
61
+ "variables": [
62
+ "AuthHeaderMenu",
63
+ "ForgotPasswordPage",
64
+ "LoginPage",
65
+ "RegisterPage",
66
+ "useAuth",
67
+ "useAuthStore"
68
+ ]
69
+ }
70
+ }
@@ -16,7 +16,7 @@
16
16
  "path": "bento-grid-section/bento-grid-section.tsx",
17
17
  "type": "registry:component",
18
18
  "target": "$modules$/bento-grid-section/bento-grid-section.tsx",
19
- "content": "import type { ReactNode } from \"react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\n// Bento Grid Components\r\nexport function BentoGrid({\r\n className,\r\n children,\r\n}: {\r\n className?: string;\r\n children?: ReactNode;\r\n}) {\r\n return (\r\n <div\r\n className={cn(\r\n \"grid md:auto-rows-[18rem] grid-cols-1 md:grid-cols-3 gap-4 max-w-7xl mx-auto\",\r\n className\r\n )}\r\n >\r\n {children}\r\n </div>\r\n );\r\n}\r\n\r\nexport function BentoGridItem({\r\n className,\r\n title,\r\n description,\r\n header,\r\n icon,\r\n}: {\r\n className?: string;\r\n title?: string | ReactNode;\r\n description?: string | ReactNode;\r\n header?: ReactNode;\r\n icon?: ReactNode;\r\n}) {\r\n return (\r\n <div\r\n className={cn(\r\n \"row-span-1 rounded-xl group/bento hover:shadow-xl transition duration-200 shadow-input dark:shadow-none p-4 dark:bg-black dark:border-white/[0.2] bg-white border border-transparent justify-between flex flex-col space-y-4\",\r\n className\r\n )}\r\n >\r\n {header}\r\n <div className=\"group-hover/bento:translate-x-2 transition duration-200\">\r\n {icon}\r\n <div className=\"font-sans font-bold text-neutral-600 dark:text-neutral-200 mb-2 mt-2\">\r\n {title}\r\n </div>\r\n <div className=\"font-sans font-normal text-neutral-600 text-xs dark:text-neutral-300\">\r\n {description}\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\n// Section Component\r\ninterface BentoGridSectionProps {\r\n title?: string;\r\n items: Array<{\r\n title: string;\r\n description: string;\r\n header?: ReactNode;\r\n icon?: ReactNode;\r\n className?: string;\r\n }>;\r\n className?: string;\r\n}\r\n\r\nexport function BentoGridSection({\r\n title,\r\n items,\r\n className,\r\n}: BentoGridSectionProps) {\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"container mx-auto px-4\">\r\n {title && (\r\n <h2 className=\"text-3xl md:text-4xl font-bold text-center mb-12\">\r\n {title}\r\n </h2>\r\n )}\r\n <BentoGrid>\r\n {items.map((item, i) => (\r\n <BentoGridItem\r\n key={i}\r\n title={item.title}\r\n description={item.description}\r\n header={item.header}\r\n icon={item.icon}\r\n className={item.className}\r\n />\r\n ))}\r\n </BentoGrid>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
19
+ "content": "import type { ReactNode } from \"react\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\n// Bento Grid Components\r\nexport function BentoGrid({\r\n className,\r\n children,\r\n}: {\r\n className?: string;\r\n children?: ReactNode;\r\n}) {\r\n return (\r\n <div\r\n className={cn(\r\n \"grid md:auto-rows-[18rem] grid-cols-1 md:grid-cols-3 gap-4 max-w-7xl mx-auto\",\r\n className\r\n )}\r\n >\r\n {children}\r\n </div>\r\n );\r\n}\r\n\r\nexport function BentoGridItem({\r\n className,\r\n title,\r\n description,\r\n header,\r\n icon,\r\n}: {\r\n className?: string;\r\n title?: string | ReactNode;\r\n description?: string | ReactNode;\r\n header?: ReactNode;\r\n icon?: ReactNode;\r\n}) {\r\n return (\r\n <div\r\n className={cn(\r\n \"row-span-1 rounded-xl group/bento hover:shadow-xl transition duration-200 shadow-input dark:shadow-none p-4 dark:bg-black dark:border-white/[0.2] bg-white border border-transparent justify-between flex flex-col space-y-4\",\r\n className\r\n )}\r\n >\r\n {header}\r\n <div className=\"group-hover/bento:translate-x-2 transition duration-200\">\r\n {icon}\r\n <div className=\"font-sans font-bold text-neutral-600 dark:text-neutral-200 mb-2 mt-2\">\r\n {title}\r\n </div>\r\n <div className=\"font-sans font-normal text-neutral-600 text-xs dark:text-neutral-300\">\r\n {description}\r\n </div>\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\n// Section Component\r\ninterface BentoGridSectionProps {\r\n title?: string;\r\n items: Array<{\r\n title: string;\r\n description: string;\r\n header?: ReactNode;\r\n icon?: ReactNode;\r\n className?: string;\r\n }>;\r\n className?: string;\r\n}\r\n\r\nexport function BentoGridSection({\r\n title,\r\n items,\r\n className,\r\n}: BentoGridSectionProps) {\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {title && (\r\n <h2 className=\"text-3xl md:text-4xl font-bold text-center mb-12\">\r\n {title}\r\n </h2>\r\n )}\r\n <BentoGrid>\r\n {items.map((item, i) => (\r\n <BentoGridItem\r\n key={i}\r\n title={item.title}\r\n description={item.description}\r\n header={item.header}\r\n icon={item.icon}\r\n className={item.className}\r\n />\r\n ))}\r\n </BentoGrid>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
20
20
  },
21
21
  {
22
22
  "path": "bento-grid-section/lang/en.json",
@@ -23,7 +23,7 @@
23
23
  "path": "blog-list-page/blog-list-page.tsx",
24
24
  "type": "registry:page",
25
25
  "target": "$modules$/blog-list-page/blog-list-page.tsx",
26
- "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=\"container 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=\"container 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=\"container 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"
26
+ "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"
27
27
  },
28
28
  {
29
29
  "path": "blog-list-page/lang/en.json",
@@ -19,7 +19,7 @@
19
19
  "path": "blog-section/blog-section.tsx",
20
20
  "type": "registry:component",
21
21
  "target": "$modules$/blog-section/blog-section.tsx",
22
- "content": "import { Link } from \"react-router\";\r\nimport { ArrowRight } from \"lucide-react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardFooter,\r\n CardHeader,\r\n} from \"@/components/ui/card\";\r\n\r\ninterface BlogSectionProps {\r\n className?: string;\r\n}\r\n\r\nexport function BlogSection({ className }: BlogSectionProps) {\r\n const { t } = useTranslation(\"blog-section\");\r\n\r\n const posts = [\r\n {\r\n id: \"1\",\r\n title: t(\"post1Title\", \"Getting Started with Modern Web Development\"),\r\n summary: t(\r\n \"post1Summary\",\r\n \"Learn the fundamentals of modern web development, from setting up your environment to deploying your first application.\"\r\n ),\r\n category: t(\"post1Category\", \"Tutorial\"),\r\n author: t(\"post1Author\", \"Sarah Chen\"),\r\n date: t(\"post1Date\", \"Jan 15, 2024\"),\r\n image: \"/images/placeholder.png\",\r\n slug: \"getting-started\",\r\n },\r\n {\r\n id: \"2\",\r\n title: t(\"post2Title\", \"Best Practices for Building Scalable Apps\"),\r\n summary: t(\r\n \"post2Summary\",\r\n \"Discover proven strategies and patterns for building applications that scale with your growing user base.\"\r\n ),\r\n category: t(\"post2Category\", \"Best Practices\"),\r\n author: t(\"post2Author\", \"Michael Park\"),\r\n date: t(\"post2Date\", \"Jan 10, 2024\"),\r\n image: \"/images/placeholder.png\",\r\n slug: \"scalable-apps\",\r\n },\r\n {\r\n id: \"3\",\r\n title: t(\"post3Title\", \"The Future of User Interface Design\"),\r\n summary: t(\r\n \"post3Summary\",\r\n \"Explore emerging trends in UI design and how they're shaping the way we build digital experiences.\"\r\n ),\r\n category: t(\"post3Category\", \"Design\"),\r\n author: t(\"post3Author\", \"Emily Davis\"),\r\n date: t(\"post3Date\", \"Jan 5, 2024\"),\r\n image: \"/images/placeholder.png\",\r\n slug: \"future-ui-design\",\r\n },\r\n ];\r\n\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"container mx-auto px-4\">\r\n {/* Header */}\r\n <div className=\"text-center mb-12\">\r\n <Badge variant=\"secondary\" className=\"mb-4\">\r\n {t(\"tagline\", \"Latest Updates\")}\r\n </Badge>\r\n <h2 className=\"text-3xl font-bold md:text-4xl lg:text-5xl mb-4\">\r\n {t(\"title\", \"From Our Blog\")}\r\n </h2>\r\n <p className=\"text-muted-foreground max-w-2xl mx-auto mb-6\">\r\n {t(\r\n \"subtitle\",\r\n \"Discover the latest trends, tips, and insights from our team of experts.\"\r\n )}\r\n </p>\r\n <Button variant=\"link\" asChild>\r\n <Link to=\"/blog\">\r\n {t(\"viewAll\", \"View all articles\")}\r\n <ArrowRight className=\"ml-2 h-4 w-4\" />\r\n </Link>\r\n </Button>\r\n </div>\r\n\r\n {/* Posts Grid */}\r\n <div className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto\">\r\n {posts.map((post) => (\r\n <Card key={post.id} className=\"overflow-hidden group p-0\">\r\n <div className=\"aspect-video overflow-hidden\">\r\n <Link to={`/blog/${post.slug}`}>\r\n <img\r\n src={post.image}\r\n alt={post.title}\r\n className=\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\"\r\n onError={(e) => {\r\n e.currentTarget.style.display = \"none\";\r\n }}\r\n />\r\n </Link>\r\n </div>\r\n <CardHeader className=\"pt-6 pb-2\">\r\n <div className=\"flex items-center gap-2 mb-2\">\r\n <Badge variant=\"outline\" className=\"text-xs\">\r\n {post.category}\r\n </Badge>\r\n <span className=\"text-xs text-muted-foreground\">\r\n {post.date}\r\n </span>\r\n </div>\r\n <Link to={`/blog/${post.slug}`}>\r\n <h3 className=\"text-lg font-semibold hover:text-primary transition-colors line-clamp-2\">\r\n {post.title}\r\n </h3>\r\n </Link>\r\n </CardHeader>\r\n <CardContent className=\"py-0\">\r\n <p className=\"text-sm text-muted-foreground line-clamp-2\">\r\n {post.summary}\r\n </p>\r\n </CardContent>\r\n <CardFooter className=\"pb-2\">\r\n <Link\r\n to={`/blog/${post.slug}`}\r\n className=\"text-sm font-medium text-primary hover:underline inline-flex items-center whitespace-nowrap\"\r\n >\r\n {t(\"readMore\", \"Read more\")}\r\n <ArrowRight className=\"ml-1 h-3 w-3 shrink-0\" />\r\n </Link>\r\n </CardFooter>\r\n </Card>\r\n ))}\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
22
+ "content": "import { Link } from \"react-router\";\r\nimport { ArrowRight } from \"lucide-react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardFooter,\r\n CardHeader,\r\n} from \"@/components/ui/card\";\r\nimport { useRecentPosts } from \"@/modules/blog-core\";\r\nimport type { Post } from \"@/modules/blog-core/types\";\r\n\r\ninterface BlogSectionProps {\r\n posts?: Post[];\r\n loading?: boolean;\r\n className?: string;\r\n}\r\n\r\nexport function BlogSection({\r\n posts: propPosts,\r\n loading: propLoading,\r\n className,\r\n}: BlogSectionProps) {\r\n const { t } = useTranslation(\"blog-section\");\r\n const { posts: hookPosts, loading: hookLoading } = useRecentPosts(3);\r\n\r\n const posts = propPosts ?? hookPosts;\r\n const loading = propLoading ?? hookLoading;\r\n\r\n return (\r\n <section className={cn(\"py-16 md:py-24\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Header */}\r\n <div className=\"text-center mb-12\">\r\n <Badge variant=\"secondary\" className=\"mb-4\">\r\n {t(\"tagline\", \"Latest Updates\")}\r\n </Badge>\r\n <h2 className=\"text-3xl font-bold md:text-4xl lg:text-5xl mb-4\">\r\n {t(\"title\", \"From Our Blog\")}\r\n </h2>\r\n <p className=\"text-muted-foreground max-w-2xl mx-auto mb-6\">\r\n {t(\r\n \"subtitle\",\r\n \"Discover the latest trends, tips, and insights from our team of experts.\"\r\n )}\r\n </p>\r\n <Button variant=\"link\" asChild>\r\n <Link to=\"/blog\">\r\n {t(\"viewAll\", \"View all articles\")}\r\n <ArrowRight className=\"ml-2 h-4 w-4\" />\r\n </Link>\r\n </Button>\r\n </div>\r\n\r\n {/* Posts Grid */}\r\n <div className=\"grid md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto\">\r\n {loading ? (\r\n [...Array(3)].map((_, i) => (\r\n <Card key={i} className=\"overflow-hidden p-0 animate-pulse\">\r\n <div className=\"aspect-video bg-muted\"></div>\r\n <CardHeader className=\"pt-6 pb-2\">\r\n <div className=\"flex items-center gap-2 mb-2\">\r\n <div className=\"h-5 w-16 bg-muted rounded\"></div>\r\n <div className=\"h-4 w-20 bg-muted rounded\"></div>\r\n </div>\r\n <div className=\"h-6 w-3/4 bg-muted rounded\"></div>\r\n </CardHeader>\r\n <CardContent className=\"py-0\">\r\n <div className=\"h-4 w-full bg-muted rounded mb-2\"></div>\r\n <div className=\"h-4 w-2/3 bg-muted rounded\"></div>\r\n </CardContent>\r\n <CardFooter className=\"pb-2\">\r\n <div className=\"h-4 w-24 bg-muted rounded\"></div>\r\n </CardFooter>\r\n </Card>\r\n ))\r\n ) : (\r\n posts.map((post) => (\r\n <Card key={post.id} className=\"overflow-hidden group p-0\">\r\n <div className=\"aspect-video overflow-hidden\">\r\n <Link to={`/blog/${post.slug}`}>\r\n <img\r\n src={post.featured_image || \"/images/placeholder.png\"}\r\n alt={post.title}\r\n className=\"w-full h-full object-cover transition-transform duration-300 group-hover:scale-105\"\r\n onError={(e) => {\r\n e.currentTarget.style.display = \"none\";\r\n }}\r\n />\r\n </Link>\r\n </div>\r\n <CardHeader className=\"pt-6 pb-2\">\r\n <div className=\"flex items-center gap-2 mb-2\">\r\n <Badge variant=\"outline\" className=\"text-xs\">\r\n {post.category_name || post.category}\r\n </Badge>\r\n <span className=\"text-xs text-muted-foreground\">\r\n {new Date(post.published_at).toLocaleDateString()}\r\n </span>\r\n </div>\r\n <Link to={`/blog/${post.slug}`}>\r\n <h3 className=\"text-lg font-semibold hover:text-primary transition-colors line-clamp-2\">\r\n {post.title}\r\n </h3>\r\n </Link>\r\n </CardHeader>\r\n <CardContent className=\"py-0\">\r\n <p className=\"text-sm text-muted-foreground line-clamp-2\">\r\n {post.excerpt}\r\n </p>\r\n </CardContent>\r\n <CardFooter className=\"pb-2\">\r\n <Link\r\n to={`/blog/${post.slug}`}\r\n className=\"text-sm font-medium text-primary hover:underline inline-flex items-center whitespace-nowrap\"\r\n >\r\n {t(\"readMore\", \"Read more\")}\r\n <ArrowRight className=\"ml-1 h-3 w-3 shrink-0\" />\r\n </Link>\r\n </CardFooter>\r\n </Card>\r\n ))\r\n )}\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
23
23
  },
24
24
  {
25
25
  "path": "blog-section/lang/en.json",
@@ -19,7 +19,7 @@
19
19
  "path": "cart-drawer/cart-drawer.tsx",
20
20
  "type": "registry:component",
21
21
  "target": "$modules$/cart-drawer/cart-drawer.tsx",
22
- "content": "import { Link } from \"react-router\";\r\nimport { ShoppingCart } 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\n\r\ninterface CartItem {\r\n id: string | number;\r\n name: string;\r\n href: string;\r\n color?: string;\r\n price: number;\r\n quantity: number;\r\n image: string;\r\n imageAlt?: string;\r\n}\r\n\r\ninterface CartDrawerProps {\r\n items: CartItem[];\r\n currency?: string;\r\n onRemove?: (id: string | number) => void;\r\n checkoutHref?: string;\r\n open?: boolean;\r\n onOpenChange?: (open: boolean) => void;\r\n}\r\n\r\nexport function CartDrawer({\r\n items,\r\n currency = \"$\",\r\n onRemove,\r\n checkoutHref = \"/checkout\",\r\n open,\r\n onOpenChange,\r\n}: CartDrawerProps) {\r\n const { t } = useTranslation(\"cart-drawer\");\r\n\r\n const subtotal = items.reduce(\r\n (sum, item) => sum + item.price * item.quantity,\r\n 0\r\n );\r\n\r\n return (\r\n <Sheet open={open} onOpenChange={onOpenChange}>\r\n <SheetTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"relative\">\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {items.length > 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 {items.length}\r\n </span>\r\n )}\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent className=\"w-full sm:max-w-md flex flex-col px-6\">\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.imageAlt || item.name}\r\n src={item.image}\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={item.href}>{item.name}</Link>\r\n </h3>\r\n <p className=\"ml-4\">\r\n {currency}{item.price.toFixed(2)}\r\n </p>\r\n </div>\r\n {item.color && (\r\n <p className=\"mt-1 text-sm text-muted-foreground\">\r\n {item.color}\r\n </p>\r\n )}\r\n </div>\r\n <div className=\"flex flex-1 items-end justify-between text-sm\">\r\n <p className=\"text-muted-foreground\">\r\n {t(\"qty\", \"Qty\")} {item.quantity}\r\n </p>\r\n\r\n <button\r\n type=\"button\"\r\n onClick={() => onRemove?.(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>{currency}{subtotal.toFixed(2)}</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\">\r\n <Link to={checkoutHref}>{t(\"checkout\", \"Checkout\")}</Link>\r\n </Button>\r\n </div>\r\n <div className=\"mt-6 flex justify-center text-center text-sm text-muted-foreground\">\r\n <p>\r\n {t(\"or\", \"or\")}{\" \"}\r\n <button\r\n type=\"button\"\r\n onClick={() => onOpenChange?.(false)}\r\n className=\"font-medium text-primary hover:text-primary/80\"\r\n >\r\n {t(\"continueShopping\", \"Continue Shopping\")}\r\n <span aria-hidden=\"true\"> &rarr;</span>\r\n </button>\r\n </p>\r\n </div>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n );\r\n}\r\n"
22
+ "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\n\r\ninterface CartItem {\r\n id: string | number;\r\n name: string;\r\n href: string;\r\n color?: string;\r\n price: number;\r\n quantity: number;\r\n image: string;\r\n imageAlt?: string;\r\n}\r\n\r\ninterface CartDrawerProps {\r\n items: CartItem[];\r\n currency?: string;\r\n onRemove?: (id: string | number) => void;\r\n onUpdateQuantity?: (id: string | number, quantity: number) => void;\r\n checkoutHref?: string;\r\n open?: boolean;\r\n onOpenChange?: (open: boolean) => void;\r\n /** Hide trigger button when using controlled mode (default: shows trigger only when open is undefined) */\r\n showTrigger?: boolean;\r\n}\r\n\r\nexport function CartDrawer({\r\n items,\r\n currency = \"$\",\r\n onRemove,\r\n onUpdateQuantity,\r\n checkoutHref = \"/checkout\",\r\n open,\r\n onOpenChange,\r\n showTrigger,\r\n}: CartDrawerProps) {\r\n // Hide trigger in controlled mode (when open is provided) unless explicitly shown\r\n const shouldShowTrigger = showTrigger ?? (open === undefined);\r\n const { t } = useTranslation(\"cart-drawer\");\r\n\r\n const subtotal = items.reduce(\r\n (sum, item) => sum + item.price * item.quantity,\r\n 0\r\n );\r\n\r\n return (\r\n <Sheet open={open} onOpenChange={onOpenChange}>\r\n {shouldShowTrigger && (\r\n <SheetTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"relative\">\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {items.length > 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 {items.length}\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.imageAlt || item.name}\r\n src={item.image}\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={item.href}>{item.name}</Link>\r\n </h3>\r\n <p className=\"ml-4\">\r\n {currency}{item.price.toFixed(2)}\r\n </p>\r\n </div>\r\n {item.color && (\r\n <p className=\"mt-1 text-sm text-muted-foreground\">\r\n {item.color}\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={() => onUpdateQuantity?.(item.id, item.quantity - 1)}\r\n >\r\n <Minus className=\"h-3 w-3\" />\r\n </Button>\r\n <span className=\"w-8 text-center text-sm\">{item.quantity}</span>\r\n <Button\r\n variant=\"outline\"\r\n size=\"icon\"\r\n className=\"h-6 w-6\"\r\n onClick={() => onUpdateQuantity?.(item.id, item.quantity + 1)}\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={() => onRemove?.(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>{currency}{subtotal.toFixed(2)}</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\">\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
23
  },
24
24
  {
25
25
  "path": "cart-drawer/lang/en.json",
@@ -4,7 +4,8 @@
4
4
  "title": "Cart Page",
5
5
  "description": "Shopping cart page with item list showing product image, name, price, and quantity controls (+/- buttons). Features order summary sidebar with subtotal, shipping estimate, tax calculation, and total. Includes empty cart state with CTA, remove item confirmation, quantity validation, and proceed to checkout button. Responsive layout with mobile-optimized summary.",
6
6
  "registryDependencies": [
7
- "ecommerce-core"
7
+ "ecommerce-core",
8
+ "animations"
8
9
  ],
9
10
  "route": {
10
11
  "path": "/cart",
@@ -22,7 +23,7 @@
22
23
  "path": "cart-page/cart-page.tsx",
23
24
  "type": "registry:page",
24
25
  "target": "$modules$/cart-page/cart-page.tsx",
25
- "content": "import { Link } from \"react-router\";\nimport { Trash2, Plus, Minus, ArrowLeft, ShoppingBag } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Input } from \"@/components/ui/input\";\nimport { useTranslation } from \"react-i18next\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn } from \"@/modules/animations\";\n\nexport function CartPage() {\n const { t } = useTranslation(\"cart-page\");\n const { state, removeItem, updateQuantity } = useCart();\n const { items, total } = state;\n\n const currency = constants.site.currency || \"USD\";\n const shipping = 0;\n const tax = 0;\n const freeShippingThreshold = 100;\n\n const getProductPrice = (product: { price: number; sale_price?: number; on_sale?: boolean }) => {\n return product.on_sale && product.sale_price ? product.sale_price : product.price;\n };\n\n const handleQuantityChange = (productId: number | string, newQuantity: number) => {\n if (newQuantity <= 0) {\n removeItem(productId);\n } else {\n updateQuantity(productId, newQuantity);\n }\n };\n\n const handleQuantityInputChange = (productId: number | string, value: string) => {\n const quantity = parseInt(value) || 1;\n handleQuantityChange(productId, quantity);\n };\n\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\n const finalTotal = total + shipping + tax;\n\n if (items.length === 0) {\n return (\n <Layout>\n <div className=\"container mx-auto px-4 py-8\">\n <div className=\"max-w-2xl mx-auto text-center\">\n <div className=\"mb-8\">\n <ShoppingBag className=\"h-24 w-24 mx-auto text-muted-foreground mb-4\" />\n <h1 className=\"text-3xl font-bold mb-4\">\n {t(\"empty\", \"Your Cart is Empty\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\"emptyDescription\", \"Looks like you haven't added any items to your cart yet.\")}\n </p>\n <Button asChild size=\"lg\">\n <Link to=\"/products\">\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"container mx-auto px-4 py-8\">\n <FadeIn className=\"flex items-center gap-4 mb-8\">\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <Link to=\"/products\">\n <ArrowLeft className=\"h-4 w-4\" />\n </Link>\n </Button>\n <div>\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Shopping Cart\")}</h1>\n <p className=\"text-muted-foreground\">\n {itemCount} {t(\"itemsInCart\", \"items in your cart\")}\n </p>\n </div>\n </FadeIn>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n <div className=\"lg:col-span-2 space-y-4\">\n {items.map((item) => (\n <Card key={item.id}>\n <CardContent className=\"p-6\">\n <div className=\"flex gap-4\">\n <div className=\"w-24 h-24 flex-shrink-0\">\n <img\n src={item.product.images[0] || \"/images/placeholder.png\"}\n alt={item.product.name}\n className=\"w-full h-full object-cover rounded-lg\"\n />\n </div>\n\n <div className=\"flex-1 space-y-2\">\n <div className=\"flex items-start justify-between\">\n <div>\n <h3 className=\"font-semibold\">{item.product.name}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {item.product.category_name ||\n item.product.categories?.[0]?.name ||\n item.product.category}\n </p>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeItem(item.product.id)}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => handleQuantityChange(item.product.id, item.quantity - 1)}\n >\n <Minus className=\"h-3 w-3\" />\n </Button>\n <Input\n type=\"number\"\n value={item.quantity}\n onChange={(e) => handleQuantityInputChange(item.product.id, e.target.value)}\n className=\"w-16 text-center\"\n min=\"1\"\n />\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => handleQuantityChange(item.product.id, item.quantity + 1)}\n >\n <Plus className=\"h-3 w-3\" />\n </Button>\n </div>\n\n <div className=\"text-right\">\n <p className=\"font-semibold\">\n {formatPrice(getProductPrice(item.product) * item.quantity, currency)}\n </p>\n {item.quantity > 1 && (\n <p className=\"text-sm text-muted-foreground\">\n {formatPrice(getProductPrice(item.product), currency)} {t(\"each\", \"each\")}\n </p>\n )}\n </div>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"flex justify-between\">\n <span>\n {t(\"subtotal\", \"Subtotal\")} ({itemCount} {t(\"items\", \"items\")})\n </span>\n <span>{formatPrice(total, currency)}</span>\n </div>\n\n <div className=\"flex justify-between\">\n <span>{t(\"shipping\", \"Shipping\")}</span>\n <span>\n {shipping === 0 ? t(\"free\", \"Free\") : formatPrice(shipping, currency)}\n </span>\n </div>\n\n <div className=\"flex justify-between\">\n <span>{t(\"tax\", \"Tax\")}</span>\n <span>{formatPrice(tax, currency)}</span>\n </div>\n\n <Separator />\n\n <div className=\"flex justify-between text-lg font-semibold\">\n <span>{t(\"total\", \"Total\")}</span>\n <span>{formatPrice(finalTotal, currency)}</span>\n </div>\n\n {shipping > 0 && freeShippingThreshold && freeShippingThreshold > total && (\n <div className=\"text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg\">\n {t(\"freeShippingMessage\", \"Add {{amount}} more for free shipping!\").replace(\n \"{{amount}}\",\n formatPrice(freeShippingThreshold - total, currency)\n )}\n </div>\n )}\n\n <Button asChild className=\"w-full\" size=\"lg\">\n <Link to=\"/checkout\">{t(\"proceedToCheckout\", \"Proceed to Checkout\")}</Link>\n </Button>\n\n <Button variant=\"outline\" asChild className=\"w-full\">\n <Link to=\"/products\">{t(\"continueShopping\", \"Continue Shopping\")}</Link>\n </Button>\n </CardContent>\n </Card>\n\n <Card>\n <CardContent className=\"p-4\">\n <div className=\"text-center space-y-2\">\n <div className=\"text-sm text-muted-foreground\">\n {t(\"secureCheckout\", \"Secure Checkout\")}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t(\"secureCheckoutDescription\", \"Your payment information is encrypted and secure\")}\n </p>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n"
26
+ "content": "import { Link } from \"react-router\";\nimport { Trash2, Plus, Minus, ArrowLeft, ShoppingBag } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Input } from \"@/components/ui/input\";\nimport { useTranslation } from \"react-i18next\";\nimport { useCart, formatPrice } from \"@/modules/ecommerce-core\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn } from \"@/modules/animations\";\n\nexport function CartPage() {\n const { t } = useTranslation(\"cart-page\");\n const { state, removeItem, updateQuantity } = useCart();\n const { items, total } = state;\n\n const currency = constants.site.currency || \"USD\";\n const shipping = 0;\n const tax = 0;\n const freeShippingThreshold = 100;\n\n const getProductPrice = (product: { price: number; sale_price?: number; on_sale?: boolean }) => {\n return product.on_sale && product.sale_price ? product.sale_price : product.price;\n };\n\n const handleQuantityChange = (productId: number | string, newQuantity: number) => {\n if (newQuantity <= 0) {\n removeItem(productId);\n } else {\n updateQuantity(productId, newQuantity);\n }\n };\n\n const handleQuantityInputChange = (productId: number | string, value: string) => {\n const quantity = parseInt(value) || 1;\n handleQuantityChange(productId, quantity);\n };\n\n const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);\n const finalTotal = total + shipping + tax;\n\n if (items.length === 0) {\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <div className=\"max-w-2xl mx-auto text-center py-16\">\n <div className=\"mb-8\">\n <ShoppingBag className=\"h-24 w-24 mx-auto text-muted-foreground mb-4\" />\n <h1 className=\"text-3xl font-bold mb-4\">\n {t(\"empty\", \"Your Cart is Empty\")}\n </h1>\n <p className=\"text-muted-foreground mb-8\">\n {t(\"emptyDescription\", \"Looks like you haven't added any items to your cart yet.\")}\n </p>\n <Button asChild size=\"lg\">\n <Link to=\"/products\">\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\n {t(\"continueShopping\", \"Continue Shopping\")}\n </Link>\n </Button>\n </div>\n </div>\n </div>\n </Layout>\n );\n }\n\n return (\n <Layout>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\n <FadeIn className=\"flex items-center gap-4 mb-8\">\n <Button variant=\"ghost\" size=\"icon\" asChild>\n <Link to=\"/products\">\n <ArrowLeft className=\"h-4 w-4\" />\n </Link>\n </Button>\n <div>\n <h1 className=\"text-3xl font-bold\">{t(\"title\", \"Shopping Cart\")}</h1>\n <p className=\"text-muted-foreground\">\n {itemCount} {t(\"itemsInCart\", \"items in your cart\")}\n </p>\n </div>\n </FadeIn>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8\">\n <div className=\"lg:col-span-2 space-y-4\">\n {items.map((item) => (\n <Card key={item.id}>\n <CardContent className=\"p-6\">\n <div className=\"flex gap-4\">\n <div className=\"w-24 h-24 flex-shrink-0\">\n <img\n src={item.product.images[0] || \"/images/placeholder.png\"}\n alt={item.product.name}\n className=\"w-full h-full object-cover rounded-lg\"\n />\n </div>\n\n <div className=\"flex-1 space-y-2\">\n <div className=\"flex items-start justify-between\">\n <div>\n <h3 className=\"font-semibold\">{item.product.name}</h3>\n <p className=\"text-sm text-muted-foreground\">\n {item.product.category_name ||\n item.product.categories?.[0]?.name ||\n item.product.category}\n </p>\n </div>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n onClick={() => removeItem(item.product.id)}\n className=\"text-destructive hover:text-destructive\"\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => handleQuantityChange(item.product.id, item.quantity - 1)}\n >\n <Minus className=\"h-3 w-3\" />\n </Button>\n <Input\n type=\"number\"\n value={item.quantity}\n onChange={(e) => handleQuantityInputChange(item.product.id, e.target.value)}\n className=\"w-16 text-center\"\n min=\"1\"\n />\n <Button\n variant=\"outline\"\n size=\"icon\"\n className=\"h-8 w-8\"\n onClick={() => handleQuantityChange(item.product.id, item.quantity + 1)}\n >\n <Plus className=\"h-3 w-3\" />\n </Button>\n </div>\n\n <div className=\"text-right\">\n <p className=\"font-semibold\">\n {formatPrice(getProductPrice(item.product) * item.quantity, currency)}\n </p>\n {item.quantity > 1 && (\n <p className=\"text-sm text-muted-foreground\">\n {formatPrice(getProductPrice(item.product), currency)} {t(\"each\", \"each\")}\n </p>\n )}\n </div>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>{t(\"orderSummary\", \"Order Summary\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"flex justify-between\">\n <span>\n {t(\"subtotal\", \"Subtotal\")} ({itemCount} {t(\"items\", \"items\")})\n </span>\n <span>{formatPrice(total, currency)}</span>\n </div>\n\n <div className=\"flex justify-between\">\n <span>{t(\"shipping\", \"Shipping\")}</span>\n <span>\n {shipping === 0 ? t(\"free\", \"Free\") : formatPrice(shipping, currency)}\n </span>\n </div>\n\n <div className=\"flex justify-between\">\n <span>{t(\"tax\", \"Tax\")}</span>\n <span>{formatPrice(tax, currency)}</span>\n </div>\n\n <Separator />\n\n <div className=\"flex justify-between text-lg font-semibold\">\n <span>{t(\"total\", \"Total\")}</span>\n <span>{formatPrice(finalTotal, currency)}</span>\n </div>\n\n {shipping > 0 && freeShippingThreshold && freeShippingThreshold > total && (\n <div className=\"text-sm text-muted-foreground bg-muted/50 p-3 rounded-lg\">\n {t(\"freeShippingMessage\", \"Add {{amount}} more for free shipping!\").replace(\n \"{{amount}}\",\n formatPrice(freeShippingThreshold - total, currency)\n )}\n </div>\n )}\n\n <Button asChild className=\"w-full\" size=\"lg\">\n <Link to=\"/checkout\">{t(\"proceedToCheckout\", \"Proceed to Checkout\")}</Link>\n </Button>\n\n <Button variant=\"outline\" asChild className=\"w-full\">\n <Link to=\"/products\">{t(\"continueShopping\", \"Continue Shopping\")}</Link>\n </Button>\n </CardContent>\n </Card>\n\n <Card>\n <CardContent className=\"p-4\">\n <div className=\"text-center space-y-2\">\n <div className=\"text-sm text-muted-foreground\">\n {t(\"secureCheckout\", \"Secure Checkout\")}\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t(\"secureCheckoutDescription\", \"Your payment information is encrypted and secure\")}\n </p>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n"
26
27
  },
27
28
  {
28
29
  "path": "cart-page/lang/en.json",