@promakeai/cli 0.4.9 → 0.4.10

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 (63) hide show
  1. package/dist/index.js +183 -176
  2. package/dist/registry/blog-core.json +26 -7
  3. package/dist/registry/blog-list-page.json +2 -2
  4. package/dist/registry/blog-section.json +1 -1
  5. package/dist/registry/cart-drawer.json +1 -1
  6. package/dist/registry/cart-page.json +1 -1
  7. package/dist/registry/category-section.json +1 -1
  8. package/dist/registry/checkout-page.json +1 -1
  9. package/dist/registry/contact-page-centered.json +1 -1
  10. package/dist/registry/contact-page-map-overlay.json +1 -1
  11. package/dist/registry/contact-page-map-split.json +1 -1
  12. package/dist/registry/contact-page-split.json +1 -1
  13. package/dist/registry/contact-page.json +1 -1
  14. package/dist/registry/db.json +129 -0
  15. package/dist/registry/docs/blog-core.md +13 -12
  16. package/dist/registry/docs/blog-list-page.md +1 -1
  17. package/dist/registry/docs/ecommerce-core.md +13 -10
  18. package/dist/registry/docs/featured-products.md +1 -1
  19. package/dist/registry/docs/post-detail-page.md +2 -2
  20. package/dist/registry/docs/product-detail-page.md +2 -2
  21. package/dist/registry/docs/products-page.md +1 -1
  22. package/dist/registry/ecommerce-core.json +25 -5
  23. package/dist/registry/featured-products.json +2 -2
  24. package/dist/registry/forgot-password-page-split.json +1 -1
  25. package/dist/registry/forgot-password-page.json +1 -1
  26. package/dist/registry/header-centered-pill.json +1 -1
  27. package/dist/registry/header-ecommerce.json +1 -1
  28. package/dist/registry/index.json +1 -0
  29. package/dist/registry/login-page-split.json +1 -1
  30. package/dist/registry/login-page.json +1 -1
  31. package/dist/registry/newsletter-section.json +1 -1
  32. package/dist/registry/post-card.json +1 -1
  33. package/dist/registry/post-detail-block.json +1 -1
  34. package/dist/registry/post-detail-page.json +3 -3
  35. package/dist/registry/product-card-detailed.json +1 -1
  36. package/dist/registry/product-card.json +1 -1
  37. package/dist/registry/product-detail-block.json +1 -1
  38. package/dist/registry/product-detail-page.json +3 -3
  39. package/dist/registry/product-detail-section.json +1 -1
  40. package/dist/registry/product-quick-view.json +1 -1
  41. package/dist/registry/products-page.json +2 -2
  42. package/dist/registry/register-page-split.json +1 -1
  43. package/dist/registry/register-page.json +1 -1
  44. package/dist/registry/related-products-block.json +1 -1
  45. package/dist/registry/reset-password-page-split.json +1 -1
  46. package/package.json +2 -4
  47. package/template/README.md +58 -39
  48. package/template/eslint.config.js +37 -37
  49. package/template/package.json +3 -4
  50. package/template/public/data/database.db +0 -0
  51. package/template/scripts/init-db.ts +126 -13
  52. package/template/src/App.tsx +5 -8
  53. package/template/src/PasswordInput.tsx +61 -0
  54. package/template/src/components/FormField.tsx +11 -5
  55. package/template/src/lang/index.ts +86 -86
  56. package/README.md +0 -71
  57. package/template/public/data/database.db-shm +0 -0
  58. package/template/public/data/database.db-wal +0 -0
  59. package/template/src/db/index.ts +0 -20
  60. package/template/src/db/provider.tsx +0 -77
  61. package/template/src/db/schema.json +0 -259
  62. package/template/src/db/types.ts +0 -195
  63. package/template/src/hooks/use-debounced-value.ts +0 -12
@@ -23,7 +23,7 @@
23
23
  "path": "forgot-password-page/forgot-password-page.tsx",
24
24
  "type": "registry:page",
25
25
  "target": "$modules$/forgot-password-page/forgot-password-page.tsx",
26
- "content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardHeader,\r\n CardTitle,\r\n CardDescription,\r\n} from \"@/components/ui/card\";\r\nimport { KeyRound, ArrowLeft, Eye, EyeOff, CheckCircle2 } from \"lucide-react\";\r\n\r\ntype Step = \"request\" | \"reset\" | \"success\";\r\n\r\nexport function ForgotPasswordPage() {\r\n const { t } = useTranslation(\"forgot-password-page\");\r\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\r\n\r\n const { forgotPassword, resetPassword } = useAuth();\r\n\r\n const [step, setStep] = useState<Step>(\"request\");\r\n const [username, setUsername] = useState(\"\");\r\n const [code, setCode] = useState(\"\");\r\n const [newPassword, setNewPassword] = useState(\"\");\r\n const [confirmPassword, setConfirmPassword] = useState(\"\");\r\n const [showPassword, setShowPassword] = useState(false);\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const handleRequestCode = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setError(null);\r\n\r\n try {\r\n await forgotPassword(username);\r\n toast.success(t(\"codeSentTitle\", \"Code Sent!\"), {\r\n description: t(\r\n \"codeSentDesc\",\r\n \"A password reset code has been sent to your email.\",\r\n ),\r\n });\r\n setStep(\"reset\");\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"errorGeneric\", \"Failed to send reset code. Please try again.\"),\r\n );\r\n setError(errorMessage);\r\n toast.error(t(\"errorTitle\", \"Error\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleResetPassword = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n\r\n // Validate passwords match\r\n if (newPassword !== confirmPassword) {\r\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\r\n return;\r\n }\r\n\r\n setIsSubmitting(true);\r\n\r\n try {\r\n await resetPassword(username, code, newPassword);\r\n toast.success(t(\"resetSuccessTitle\", \"Password Reset!\"), {\r\n description: t(\r\n \"resetSuccessDesc\",\r\n \"Your password has been successfully reset.\",\r\n ),\r\n });\r\n setStep(\"success\");\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"errorResetGeneric\", \"Failed to reset password. Please try again.\"),\r\n );\r\n setError(errorMessage);\r\n toast.error(t(\"errorTitle\", \"Error\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n // Success step\r\n if (step === \"success\") {\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardContent className=\"pt-8 pb-8 text-center\">\r\n <CheckCircle2 className=\"w-16 h-16 text-green-500 mx-auto mb-4\" />\r\n <h1 className=\"text-2xl font-bold mb-2\">\r\n {t(\"successTitle\", \"Password Reset Successfully!\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-6\">\r\n {t(\r\n \"successDescription\",\r\n \"Your password has been changed. You can now login with your new password.\",\r\n )}\r\n </p>\r\n <Button asChild className=\"w-full\">\r\n <Link to=\"/login\">{t(\"goToLogin\", \"Go to Login\")}</Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Hero Section */}\r\n <div className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\r\n {t(\"title\", \"Forgot Password\")}\r\n </h1>\r\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\r\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\r\n {step === \"request\"\r\n ? t(\r\n \"descriptionRequest\",\r\n \"Enter your username and we'll send you a code to reset your password.\",\r\n )\r\n : t(\r\n \"descriptionReset\",\r\n \"Enter the code sent to your email and your new password.\",\r\n )}\r\n </p>\r\n </div>\r\n\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <KeyRound className=\"w-5 h-5 text-primary\" />\r\n {step === \"request\"\r\n ? t(\"cardTitleRequest\", \"Request Reset Code\")\r\n : t(\"cardTitleReset\", \"Reset Password\")}\r\n </CardTitle>\r\n <CardDescription>\r\n {step === \"request\"\r\n ? t(\"cardDescRequest\", \"Step 1 of 2: Request a reset code\")\r\n : t(\r\n \"cardDescReset\",\r\n \"Step 2 of 2: Enter code and new password\",\r\n )}\r\n </CardDescription>\r\n </CardHeader>\r\n <CardContent>\r\n {step === \"request\" ? (\r\n // Step 1: Request Code\r\n <form onSubmit={handleRequestCode} className=\"space-y-6\">\r\n <div>\r\n <Label htmlFor=\"username\">\r\n {t(\"username\", \"Username\")} *\r\n </Label>\r\n <Input\r\n id=\"username\"\r\n type=\"text\"\r\n value={username}\r\n onChange={(e) => setUsername(e.target.value)}\r\n placeholder={t(\r\n \"usernamePlaceholder\",\r\n \"Enter your username\",\r\n )}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"username\"\r\n />\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\r\n <p className=\"text-red-800 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\", \"Sending...\")}\r\n </>\r\n ) : (\r\n t(\"sendCode\", \"Send Reset Code\")\r\n )}\r\n </Button>\r\n\r\n <div className=\"text-center\">\r\n <Link\r\n to=\"/login\"\r\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to Login\")}\r\n </Link>\r\n </div>\r\n </form>\r\n ) : (\r\n // Step 2: Reset Password\r\n <form onSubmit={handleResetPassword} className=\"space-y-6\">\r\n <div className=\"p-3 bg-muted rounded-lg text-sm\">\r\n <span className=\"text-muted-foreground\">\r\n {t(\"codeFor\", \"Reset code for:\")}{\" \"}\r\n </span>\r\n <span className=\"font-medium\">{username}</span>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"code\">{t(\"code\", \"Reset Code\")} *</Label>\r\n <Input\r\n id=\"code\"\r\n type=\"text\"\r\n value={code}\r\n onChange={(e) => setCode(e.target.value)}\r\n placeholder={t(\"codePlaceholder\", \"Enter 6-digit code\")}\r\n required\r\n className=\"mt-1\"\r\n maxLength={6}\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"newPassword\">\r\n {t(\"newPassword\", \"New Password\")} *\r\n </Label>\r\n <div className=\"relative\">\r\n <Input\r\n id=\"newPassword\"\r\n type={showPassword ? \"text\" : \"password\"}\r\n value={newPassword}\r\n onChange={(e) => setNewPassword(e.target.value)}\r\n placeholder={t(\r\n \"newPasswordPlaceholder\",\r\n \"Enter new password\",\r\n )}\r\n required\r\n className=\"mt-1 pr-10\"\r\n autoComplete=\"new-password\"\r\n />\r\n <button\r\n type=\"button\"\r\n onClick={() => setShowPassword(!showPassword)}\r\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\r\n >\r\n {showPassword ? (\r\n <EyeOff className=\"w-4 h-4\" />\r\n ) : (\r\n <Eye className=\"w-4 h-4\" />\r\n )}\r\n </button>\r\n </div>\r\n <p className=\"text-xs text-muted-foreground mt-1\">\r\n {t(\r\n \"passwordRequirements\",\r\n \"At least 8 characters, 1 letter and 1 number\",\r\n )}\r\n </p>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"confirmPassword\">\r\n {t(\"confirmPassword\", \"Confirm Password\")} *\r\n </Label>\r\n <Input\r\n id=\"confirmPassword\"\r\n type={showPassword ? \"text\" : \"password\"}\r\n value={confirmPassword}\r\n onChange={(e) => setConfirmPassword(e.target.value)}\r\n placeholder={t(\r\n \"confirmPasswordPlaceholder\",\r\n \"Confirm new password\",\r\n )}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"new-password\"\r\n />\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\r\n <p className=\"text-red-800 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"resetting\", \"Resetting...\")}\r\n </>\r\n ) : (\r\n t(\"resetPassword\", \"Reset Password\")\r\n )}\r\n </Button>\r\n\r\n <div className=\"flex justify-between\">\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n setStep(\"request\");\r\n setCode(\"\");\r\n setNewPassword(\"\");\r\n setConfirmPassword(\"\");\r\n setError(null);\r\n }}\r\n className=\"text-sm text-muted-foreground hover:text-primary\"\r\n >\r\n {t(\"changeUsername\", \"Change username\")}\r\n </button>\r\n <button\r\n type=\"button\"\r\n onClick={() =>\r\n handleRequestCode({\r\n preventDefault: () => {},\r\n } as React.FormEvent)\r\n }\r\n className=\"text-sm text-primary hover:underline\"\r\n disabled={isSubmitting}\r\n >\r\n {t(\"resendCode\", \"Resend code\")}\r\n </button>\r\n </div>\r\n </form>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ForgotPasswordPage;\r\n"
26
+ "content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardHeader,\r\n CardTitle,\r\n CardDescription,\r\n} from \"@/components/ui/card\";\r\nimport { KeyRound, ArrowLeft, CheckCircle2 } from \"lucide-react\";\r\nimport { FormField } from \"@/components/FormField\";\r\nimport { PasswordInput } from \"@/components/PasswordInput\";\r\n\r\ntype Step = \"request\" | \"reset\" | \"success\";\r\n\r\nexport function ForgotPasswordPage() {\r\n const { t } = useTranslation(\"forgot-password-page\");\r\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\r\n\r\n const { forgotPassword, resetPassword } = useAuth();\r\n\r\n const [step, setStep] = useState<Step>(\"request\");\r\n const [username, setUsername] = useState(\"\");\r\n const [code, setCode] = useState(\"\");\r\n const [newPassword, setNewPassword] = useState(\"\");\r\n const [confirmPassword, setConfirmPassword] = useState(\"\");\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const handleRequestCode = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setError(null);\r\n\r\n try {\r\n await forgotPassword(username);\r\n toast.success(t(\"codeSentTitle\", \"Code Sent!\"), {\r\n description: t(\r\n \"codeSentDesc\",\r\n \"A password reset code has been sent to your email.\",\r\n ),\r\n });\r\n setStep(\"reset\");\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"errorGeneric\", \"Failed to send reset code. Please try again.\"),\r\n );\r\n setError(errorMessage);\r\n toast.error(t(\"errorTitle\", \"Error\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleResetPassword = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n\r\n // Validate passwords match\r\n if (newPassword !== confirmPassword) {\r\n setError(t(\"passwordMismatch\", \"Passwords do not match\"));\r\n return;\r\n }\r\n\r\n setIsSubmitting(true);\r\n\r\n try {\r\n await resetPassword(username, code, newPassword);\r\n toast.success(t(\"resetSuccessTitle\", \"Password Reset!\"), {\r\n description: t(\r\n \"resetSuccessDesc\",\r\n \"Your password has been successfully reset.\",\r\n ),\r\n });\r\n setStep(\"success\");\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"errorResetGeneric\", \"Failed to reset password. Please try again.\"),\r\n );\r\n setError(errorMessage);\r\n toast.error(t(\"errorTitle\", \"Error\"), {\r\n description: errorMessage,\r\n });\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n // Success step\r\n if (step === \"success\") {\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardContent className=\"pt-8 pb-8 text-center\">\r\n <CheckCircle2 className=\"w-16 h-16 text-green-500 mx-auto mb-4\" />\r\n <h1 className=\"text-2xl font-bold mb-2\">\r\n {t(\"successTitle\", \"Password Reset Successfully!\")}\r\n </h1>\r\n <p className=\"text-muted-foreground mb-6\">\r\n {t(\r\n \"successDescription\",\r\n \"Your password has been changed. You can now login with your new password.\",\r\n )}\r\n </p>\r\n <Button asChild className=\"w-full\">\r\n <Link to=\"/login\">{t(\"goToLogin\", \"Go to Login\")}</Link>\r\n </Button>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen bg-muted/30 py-12\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n {/* Hero Section */}\r\n <div className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\r\n {t(\"title\", \"Forgot Password\")}\r\n </h1>\r\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\r\n <p className=\"text-lg text-muted-foreground max-w-xl mx-auto\">\r\n {step === \"request\"\r\n ? t(\r\n \"descriptionRequest\",\r\n \"Enter your username and we'll send you a code to reset your password.\",\r\n )\r\n : t(\r\n \"descriptionReset\",\r\n \"Enter the code sent to your email and your new password.\",\r\n )}\r\n </p>\r\n </div>\r\n\r\n <div className=\"max-w-md mx-auto\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <KeyRound className=\"w-5 h-5 text-primary\" />\r\n {step === \"request\"\r\n ? t(\"cardTitleRequest\", \"Request Reset Code\")\r\n : t(\"cardTitleReset\", \"Reset Password\")}\r\n </CardTitle>\r\n <CardDescription>\r\n {step === \"request\"\r\n ? t(\"cardDescRequest\", \"Step 1 of 2: Request a reset code\")\r\n : t(\r\n \"cardDescReset\",\r\n \"Step 2 of 2: Enter code and new password\",\r\n )}\r\n </CardDescription>\r\n </CardHeader>\r\n <CardContent>\r\n {step === \"request\" ? (\r\n // Step 1: Request Code\r\n <form onSubmit={handleRequestCode} className=\"space-y-6\">\r\n <FormField label={t(\"username\", \"Username\")} htmlFor=\"username\" required>\r\n <Input\r\n id=\"username\"\r\n type=\"text\"\r\n value={username}\r\n onChange={(e) => setUsername(e.target.value)}\r\n placeholder={t(\"usernamePlaceholder\", \"Enter your username\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"username\"\r\n />\r\n </FormField>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\r\n <p className=\"text-red-800 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\", \"Sending...\")}\r\n </>\r\n ) : (\r\n t(\"sendCode\", \"Send Reset Code\")\r\n )}\r\n </Button>\r\n\r\n <div className=\"text-center\">\r\n <Link\r\n to=\"/login\"\r\n className=\"text-sm text-muted-foreground hover:text-primary inline-flex items-center gap-1\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to Login\")}\r\n </Link>\r\n </div>\r\n </form>\r\n ) : (\r\n // Step 2: Reset Password\r\n <form onSubmit={handleResetPassword} className=\"space-y-6\">\r\n <div className=\"p-3 bg-muted rounded-lg text-sm\">\r\n <span className=\"text-muted-foreground\">\r\n {t(\"codeFor\", \"Reset code for:\")}{\" \"}\r\n </span>\r\n <span className=\"font-medium\">{username}</span>\r\n </div>\r\n <FormField label={t(\"code\", \"Reset Code\")} htmlFor=\"code\" required>\r\n <Input\r\n id=\"code\"\r\n type=\"text\"\r\n value={code}\r\n onChange={(e) => setCode(e.target.value)}\r\n placeholder={t(\"codePlaceholder\", \"Enter 6-digit code\")}\r\n required\r\n className=\"mt-1\"\r\n maxLength={6}\r\n />\r\n </FormField>\r\n <FormField label={t(\"newPassword\", \"New Password\")} htmlFor=\"new-password\" required>\r\n <PasswordInput\r\n id=\"new-password\"\r\n name=\"new-password\"\r\n value={newPassword}\r\n onChange={(e) => setNewPassword(e.target.value)}\r\n placeholder={t(\"newPasswordPlaceholder\", \"Enter new password\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"new-password\"\r\n />\r\n </FormField>\r\n <div>\r\n <p className=\"text-xs text-muted-foreground mt-1\">\r\n {t(\r\n \"passwordRequirements\",\r\n \"At least 8 characters, 1 letter and 1 number\",\r\n )}\r\n </p>\r\n </div>\r\n\r\n <FormField label={t(\"confirmPassword\", \"Confirm Password\")} htmlFor=\"confirm-password\" required>\r\n <PasswordInput\r\n id=\"confirm-password\"\r\n value={confirmPassword}\r\n name=\"confirm-password\"\r\n onChange={(e) => setConfirmPassword(e.target.value)}\r\n placeholder={t(\"confirmPasswordPlaceholder\", \"Confirm new password\")}\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"new-password\"\r\n />\r\n </FormField>\r\n\r\n {error && (\r\n <div className=\"p-4 bg-red-50 border border-red-200 rounded-lg\">\r\n <p className=\"text-red-800 text-sm font-medium\">\r\n {error}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"resetting\", \"Resetting...\")}\r\n </>\r\n ) : (\r\n t(\"resetPassword\", \"Reset Password\")\r\n )}\r\n </Button>\r\n\r\n <div className=\"flex justify-between\">\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n setStep(\"request\");\r\n setCode(\"\");\r\n setNewPassword(\"\");\r\n setConfirmPassword(\"\");\r\n setError(null);\r\n }}\r\n className=\"text-sm text-muted-foreground hover:text-primary\"\r\n >\r\n {t(\"changeUsername\", \"Change username\")}\r\n </button>\r\n <button\r\n type=\"button\"\r\n onClick={() =>\r\n handleRequestCode({\r\n preventDefault: () => { },\r\n } as React.FormEvent)\r\n }\r\n className=\"text-sm text-primary hover:underline\"\r\n disabled={isSubmitting}\r\n >\r\n {t(\"resendCode\", \"Resend code\")}\r\n </button>\r\n </div>\r\n </form>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ForgotPasswordPage;\r\n"
27
27
  },
28
28
  {
29
29
  "path": "forgot-password-page/lang/en.json",
@@ -21,7 +21,7 @@
21
21
  "path": "header-centered-pill/header-centered-pill.tsx",
22
22
  "type": "registry:component",
23
23
  "target": "$modules$/header-centered-pill/header-centered-pill.tsx",
24
- "content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { Menu } from \"lucide-react\";\r\nimport { Button, buttonVariants } from \"@/components/ui/button\";\r\nimport {\r\n Sheet,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetContent,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useTranslation } from \"react-i18next\";\r\n\r\nexport function HeaderCenteredPill() {\r\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\r\n const { t } = useTranslation(\"header-centered-pill\");\r\n\r\n const navigation = [\r\n { name: t(\"features\", \"Features\"), href: \"/features\" },\r\n { name: t(\"pricing\", \"Pricing\"), href: \"/pricing\" },\r\n { name: t(\"blog\", \"Blog\"), href: \"/blog\" },\r\n { name: t(\"company\", \"Company\"), href: \"/about\" },\r\n { name: t(\"signIn\", \"Sign in\"), href: \"/login\" },\r\n ];\r\n\r\n return (\r\n <header className=\"p-4\">\r\n <div className=\"mx-auto flex max-w-2xl items-center justify-between space-x-4 rounded-full border bg-background p-1.5 pl-4\">\r\n <Logo size=\"sm\" />\r\n\r\n {/* Desktop Navigation */}\r\n <div className=\"hidden md:inline-flex\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className={buttonVariants({ variant: \"ghost\", size: \"sm\" })}\r\n >\r\n {item.name}\r\n </Link>\r\n ))}\r\n </div>\r\n\r\n {/* Desktop CTA */}\r\n <div className=\"hidden md:inline-flex\">\r\n <Link\r\n to=\"/register\"\r\n className={cn(buttonVariants({ size: \"sm\" }), \"rounded-full\")}\r\n >\r\n {t(\"getStarted\", \"Get Started\")}\r\n </Link>\r\n </div>\r\n\r\n {/* Mobile Menu */}\r\n <div className=\"md:hidden\">\r\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\r\n <SheetTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"rounded-full\">\r\n <Menu className=\"h-5 w-5\" />\r\n <span className=\"sr-only\">{t(\"menu\", \"Menu\")}</span>\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent side=\"right\" className=\"w-[300px] px-6\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"menu\", \"Menu\")}</SheetTitle>\r\n </SheetHeader>\r\n <div className=\"flex flex-col space-y-4 mt-8\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n {item.name}\r\n </Link>\r\n ))}\r\n <Link\r\n to=\"/register\"\r\n className={cn(buttonVariants(), \"rounded-full mt-4\")}\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n {t(\"getStarted\", \"Get Started\")}\r\n </Link>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n </div>\r\n </div>\r\n </header>\r\n );\r\n}\r\n"
24
+ "content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { Menu } from \"lucide-react\";\r\nimport { Button, buttonVariants } from \"@/components/ui/button\";\r\nimport {\r\n Sheet,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetContent,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useTranslation } from \"react-i18next\";\r\n\r\nexport function HeaderCenteredPill() {\r\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\r\n const { t } = useTranslation(\"header-centered-pill\");\r\n\r\n const navigation = [\r\n { name: t(\"features\", \"Features\"), href: \"/features\" },\r\n { name: t(\"pricing\", \"Pricing\"), href: \"/pricing\" },\r\n { name: t(\"blog\", \"Blog\"), href: \"/blog\" },\r\n { name: t(\"company\", \"Company\"), href: \"/about\" },\r\n { name: t(\"signIn\", \"Sign in\"), href: \"/login\" },\r\n ];\r\n\r\n return (\r\n <header className=\"p-4\">\r\n <div className=\"mx-auto flex max-w-2xl items-center justify-between space-x-4 rounded-full border bg-background p-1.5 pl-4\">\r\n <Link to=\"/\">\r\n <Logo size=\"sm\" />\r\n </Link>\r\n\r\n {/* Desktop Navigation */}\r\n <div className=\"hidden md:inline-flex\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className={buttonVariants({ variant: \"ghost\", size: \"sm\" })}\r\n >\r\n {item.name}\r\n </Link>\r\n ))}\r\n </div>\r\n\r\n {/* Desktop CTA */}\r\n <div className=\"hidden md:inline-flex\">\r\n <Link\r\n to=\"/register\"\r\n className={cn(buttonVariants({ size: \"sm\" }), \"rounded-full\")}\r\n >\r\n {t(\"getStarted\", \"Get Started\")}\r\n </Link>\r\n </div>\r\n\r\n {/* Mobile Menu */}\r\n <div className=\"md:hidden\">\r\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\r\n <SheetTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"rounded-full\">\r\n <Menu className=\"h-5 w-5\" />\r\n <span className=\"sr-only\">{t(\"menu\", \"Menu\")}</span>\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent side=\"right\" className=\"w-[300px] px-6\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"menu\", \"Menu\")}</SheetTitle>\r\n </SheetHeader>\r\n <div className=\"flex flex-col space-y-4 mt-8\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n {item.name}\r\n </Link>\r\n ))}\r\n <Link\r\n to=\"/register\"\r\n className={cn(buttonVariants(), \"rounded-full mt-4\")}\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n {t(\"getStarted\", \"Get Started\")}\r\n </Link>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n </div>\r\n </div>\r\n </header>\r\n );\r\n}\r\n"
25
25
  },
26
26
  {
27
27
  "path": "header-centered-pill/lang/en.json",
@@ -20,7 +20,7 @@
20
20
  "path": "header-ecommerce/header-ecommerce.tsx",
21
21
  "type": "registry:component",
22
22
  "target": "$modules$/header-ecommerce/header-ecommerce.tsx",
23
- "content": "import { useState, useMemo } from \"react\";\r\nimport { useDebouncedValue } from \"@/hooks/use-debounced-value\";\r\nimport { Link, useNavigate } from \"react-router\";\r\nimport { ShoppingCart, Menu, Search, Heart, Package, User, LogOut } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport {\r\n Sheet,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetContent,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport {\r\n Dialog,\r\n DialogContent,\r\n DialogHeader,\r\n DialogTitle,\r\n DialogTrigger,\r\n} from \"@/components/ui/dialog\";\r\nimport {\r\n DropdownMenu,\r\n DropdownMenuContent,\r\n DropdownMenuItem,\r\n DropdownMenuLabel,\r\n DropdownMenuSeparator,\r\n DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { CartDrawer } from \"@/modules/cart-drawer\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport type { Product, Category } from \"@/modules/ecommerce-core/types\";\r\nimport {\r\n useCart,\r\n useFavorites,\r\n formatPrice,\r\n} from \"@/modules/ecommerce-core\";\r\nimport { useDbList } from \"@/db\";\r\n\r\nexport function HeaderEcommerce() {\r\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\r\n const [mobileSearchOpen, setMobileSearchOpen] = useState(false);\r\n const [desktopSearchOpen, setDesktopSearchOpen] = useState(false);\r\n const [showResults, setShowResults] = useState(false);\r\n const { itemCount, state } = useCart();\r\n const { favoriteCount } = useFavorites();\r\n const { isAuthenticated, user, logout } = useAuth();\r\n const navigate = useNavigate();\r\n const { t } = useTranslation(\"header-ecommerce\");\r\n\r\n const handleLogout = () => {\r\n logout();\r\n toast.success(t(\"logoutToastTitle\", \"Goodbye!\"), {\r\n description: t(\"logoutToastDesc\", \"You have been logged out successfully.\"),\r\n });\r\n };\r\n\r\n const [searchTerm, setSearchTerm] = useState(\"\");\r\n const debouncedTerm = useDebouncedValue(searchTerm, 300);\r\n const { data: productCategories = [] } = useDbList<Category>(\"product_categories\");\r\n const categoryMap = useMemo(() => new Map(productCategories.map(c => [c.id, c])), [productCategories]);\r\n\r\n const { data: searchResults = [] } = useDbList<Product>(\"products\", {\r\n where: debouncedTerm ? { name: { $like: `%${debouncedTerm}%` } } : {},\r\n limit: 20,\r\n enabled: debouncedTerm.length > 0,\r\n });\r\n\r\n const clearSearch = () => { setSearchTerm(\"\"); };\r\n\r\n const handleSearchSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n if (searchTerm.trim()) {\r\n navigate(`/products?search=${encodeURIComponent(searchTerm)}`);\r\n setShowResults(false);\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }\r\n };\r\n\r\n const handleSearchFocus = () => {\r\n setShowResults(true);\r\n };\r\n\r\n const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n setSearchTerm(e.target.value);\r\n setShowResults(true);\r\n };\r\n\r\n const navigation = [\r\n { name: t(\"home\"), href: \"/\" },\r\n { name: t(\"products\"), href: \"/products\" },\r\n { name: t(\"about\"), href: \"/about\" },\r\n { name: t(\"contact\"), href: \"/contact\" },\r\n ];\r\n\r\n return (\r\n <header className=\"sticky top-0 z-50 w-full border-b border-border/20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\r\n <div className=\"flex h-14 sm:h-16 md:h-20 items-center justify-between gap-2\">\r\n {/* Logo */}\r\n <div className=\"flex-shrink-0 min-w-0\">\r\n <Logo size=\"sm\" className=\"text-base sm:text-xl lg:text-2xl\" />\r\n </div>\r\n\r\n {/* Desktop Navigation - Centered */}\r\n <nav className=\"hidden lg:flex items-center space-x-12 absolute left-1/2 transform -translate-x-1/2\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-base font-medium transition-colors hover:text-primary relative group py-2\"\r\n >\r\n {item.name}\r\n <span className=\"absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full\"></span>\r\n </Link>\r\n ))}\r\n </nav>\r\n\r\n {/* Search & Actions - Right Aligned */}\r\n <div className=\"flex items-center space-x-1 sm:space-x-2 lg:space-x-4 flex-shrink-0\">\r\n {/* Desktop Search - Modal */}\r\n <Dialog\r\n open={desktopSearchOpen}\r\n onOpenChange={setDesktopSearchOpen}\r\n >\r\n <DialogTrigger asChild>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"hidden lg:flex h-10 w-10\"\r\n >\r\n <Search className=\"h-5 w-5\" />\r\n </Button>\r\n </DialogTrigger>\r\n <DialogContent className=\"sm:max-w-2xl\">\r\n <DialogHeader>\r\n <DialogTitle>\r\n {t(\"searchProducts\", \"Search Products\")}\r\n </DialogTitle>\r\n </DialogHeader>\r\n <div className=\"space-y-4\">\r\n <form onSubmit={handleSearchSubmit}>\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\r\n \"searchPlaceholder\",\r\n \"Search for products...\"\r\n )}\r\n value={searchTerm}\r\n onChange={handleSearchChange}\r\n className=\"pl-11 h-12 text-base\"\r\n autoFocus\r\n />\r\n </div>\r\n </form>\r\n\r\n {/* Desktop Search Results */}\r\n {searchTerm.trim() && (\r\n <div className=\"max-h-[400px] overflow-y-auto rounded-lg border bg-card\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"divide-y\">\r\n <div className=\"px-4 py-3 bg-muted/50\">\r\n <p className=\"text-sm font-medium text-muted-foreground\">\r\n {searchResults.length}{\" \"}\r\n {searchResults.length === 1\r\n ? \"result\"\r\n : \"results\"}{\" \"}\r\n found\r\n </p>\r\n </div>\r\n {searchResults.slice(0, 8).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"flex items-center gap-4 p-4 hover:bg-muted/50 transition-colors\"\r\n >\r\n <img\r\n src={\r\n product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-16 h-16 object-cover rounded flex-shrink-0\"\r\n />\r\n <div className=\"flex-1 min-w-0\">\r\n <h4 className=\"font-medium text-base line-clamp-1\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-sm text-muted-foreground capitalize\">\r\n {categoryMap.get(product.categories?.[0] as number)?.name}\r\n </p>\r\n <p className=\"text-base font-semibold text-primary mt-1\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n {searchResults.length > 8 && (\r\n <div className=\"px-4 py-3 bg-muted/30 text-center\">\r\n <button\r\n onClick={() => {\r\n navigate(\r\n `/products?search=${encodeURIComponent(\r\n searchTerm\r\n )}`\r\n );\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"text-sm font-medium text-primary hover:underline\"\r\n >\r\n {t(\r\n \"viewAllResults\",\r\n `View all ${searchResults.length} results`\r\n )}\r\n </button>\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"p-8 text-center\">\r\n <Search className=\"h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50\" />\r\n <p className=\"text-base text-muted-foreground\">\r\n {t(\"noResults\", \"No products found\")}\r\n </p>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\r\n \"tryDifferentKeywords\",\r\n \"Try different keywords\"\r\n )}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </DialogContent>\r\n </Dialog>\r\n\r\n {/* Search - Mobile (Hidden - moved to hamburger menu) */}\r\n <Dialog open={mobileSearchOpen} onOpenChange={setMobileSearchOpen}>\r\n <DialogTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"hidden\">\r\n <Search className=\"h-4 w-4 sm:h-5 sm:w-5\" />\r\n </Button>\r\n </DialogTrigger>\r\n <DialogContent className=\"sm:max-w-md\">\r\n <DialogHeader>\r\n <DialogTitle>{t(\"searchProducts\")}</DialogTitle>\r\n </DialogHeader>\r\n <form\r\n onSubmit={(e) => {\r\n e.preventDefault();\r\n if (searchTerm.trim()) {\r\n navigate(\r\n `/products?search=${encodeURIComponent(searchTerm)}`\r\n );\r\n setMobileSearchOpen(false);\r\n clearSearch();\r\n }\r\n }}\r\n className=\"space-y-4\"\r\n >\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={(e) => setSearchTerm(e.target.value)}\r\n className=\"pl-10\"\r\n autoFocus\r\n />\r\n </div>\r\n <div className=\"flex gap-2\">\r\n <Button type=\"submit\" className=\"flex-1\">\r\n {t(\"searchButton\", \"Search\")}\r\n </Button>\r\n <Button\r\n type=\"button\"\r\n variant=\"outline\"\r\n onClick={() => {\r\n clearSearch();\r\n setMobileSearchOpen(false);\r\n }}\r\n >\r\n {t(\"cancel\", \"Cancel\")}\r\n </Button>\r\n </div>\r\n </form>\r\n\r\n {/* Mobile Search Results */}\r\n {searchTerm.trim() && (\r\n <div className=\"mt-4 max-h-64 overflow-y-auto\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"space-y-2\">\r\n <p className=\"text-sm text-muted-foreground mb-2\">\r\n {searchResults.length} result\r\n {searchResults.length !== 1 ? \"s\" : \"\"} found\r\n </p>\r\n {searchResults.slice(0, 5).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setMobileSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"block p-2 rounded hover:bg-muted/50 transition-colors\"\r\n >\r\n <div className=\"flex items-center gap-3\">\r\n <img\r\n src={\r\n product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-10 h-10 object-cover rounded\"\r\n />\r\n <div className=\"flex-1\">\r\n <h4 className=\"font-medium text-sm\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {categoryMap.get(product.categories?.[0] as number)?.name}\r\n </p>\r\n <p className=\"text-sm font-medium\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n </div>\r\n ) : (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"noResults\")}\r\n </p>\r\n )}\r\n </div>\r\n )}\r\n </DialogContent>\r\n </Dialog>\r\n\r\n {/* Wishlist - Desktop Only */}\r\n <Link to=\"/favorites\" className=\"hidden lg:block\">\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"relative h-10 w-10\"\r\n >\r\n <Heart className=\"h-5 w-5\" />\r\n {favoriteCount > 0 && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\r\n >\r\n {favoriteCount}\r\n </Badge>\r\n )}\r\n </Button>\r\n </Link>\r\n\r\n {/* Cart - Desktop Only (Goes to Cart Page) */}\r\n <Link to=\"/cart\" className=\"hidden lg:block\">\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"relative h-10 w-10\"\r\n >\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {itemCount > 0 && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\r\n >\r\n {itemCount}\r\n </Badge>\r\n )}\r\n </Button>\r\n </Link>\r\n\r\n {/* Auth - Desktop Only */}\r\n <div className=\"hidden lg:flex\">\r\n {isAuthenticated ? (\r\n <DropdownMenu>\r\n <DropdownMenuTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\r\n <User className=\"h-5 w-5\" />\r\n </Button>\r\n </DropdownMenuTrigger>\r\n <DropdownMenuContent align=\"end\" className=\"w-56\">\r\n <DropdownMenuLabel className=\"font-normal\">\r\n <div className=\"flex flex-col space-y-1\">\r\n <p className=\"text-sm font-medium\">{user?.username}</p>\r\n {user?.email && (\r\n <p className=\"text-xs text-muted-foreground\">{user.email}</p>\r\n )}\r\n </div>\r\n </DropdownMenuLabel>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuItem asChild className=\"cursor-pointer\">\r\n <Link to=\"/my-orders\" className=\"flex items-center\">\r\n <Package className=\"mr-2 h-4 w-4\" />\r\n {t(\"myOrders\", \"My Orders\")}\r\n </Link>\r\n </DropdownMenuItem>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuItem\r\n onClick={handleLogout}\r\n className=\"text-red-600 focus:text-red-600 focus:bg-red-50 cursor-pointer\"\r\n >\r\n <LogOut className=\"mr-2 h-4 w-4\" />\r\n {t(\"logout\", \"Logout\")}\r\n </DropdownMenuItem>\r\n </DropdownMenuContent>\r\n </DropdownMenu>\r\n ) : (\r\n <Link to=\"/login\">\r\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\r\n <User className=\"h-5 w-5\" />\r\n </Button>\r\n </Link>\r\n )}\r\n </div>\r\n\r\n {/* Mobile Menu */}\r\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\r\n <SheetTrigger asChild>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"lg:hidden h-8 w-8 sm:h-10 sm:w-10\"\r\n >\r\n <Menu className=\"h-4 w-4 sm:h-5 sm:w-5\" />\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent side=\"right\" className=\"w-[300px] sm:w-[400px] px-6\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"menu\")}</SheetTitle>\r\n </SheetHeader>\r\n\r\n {/* Mobile Search in Hamburger */}\r\n <div className=\"mt-6 pb-4 border-b\">\r\n <form onSubmit={handleSearchSubmit}>\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={handleSearchChange}\r\n onFocus={handleSearchFocus}\r\n className=\"pl-10 h-11\"\r\n />\r\n </div>\r\n </form>\r\n\r\n {/* Search Results in Hamburger */}\r\n {showResults && searchTerm && (\r\n <div className=\"mt-3 max-h-[300px] overflow-y-auto rounded-lg border bg-card\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"divide-y\">\r\n <div className=\"px-3 py-2 bg-muted/50\">\r\n <p className=\"text-xs font-medium text-muted-foreground\">\r\n {searchResults.length}{\" \"}\r\n {searchResults.length === 1\r\n ? \"result\"\r\n : \"results\"}\r\n </p>\r\n </div>\r\n {searchResults.slice(0, 5).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setMobileMenuOpen(false);\r\n clearSearch();\r\n setShowResults(false);\r\n }}\r\n className=\"flex items-center gap-3 p-3 hover:bg-muted/50 transition-colors\"\r\n >\r\n <img\r\n src={\r\n product.images?.length ? product.images?.[0] : \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-14 h-14 object-cover rounded flex-shrink-0\"\r\n />\r\n <div className=\"flex-1 min-w-0\">\r\n <h4 className=\"font-medium text-sm line-clamp-1\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-xs text-muted-foreground capitalize\">\r\n {categoryMap.get(product.categories?.[0] as number)?.name}\r\n </p>\r\n <p className=\"text-sm font-semibold text-primary mt-1\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n {searchResults.length > 5 && (\r\n <div className=\"px-3 py-2 bg-muted/30 text-center\">\r\n <button\r\n onClick={() => {\r\n navigate(\r\n `/products?search=${encodeURIComponent(\r\n searchTerm\r\n )}`\r\n );\r\n setMobileMenuOpen(false);\r\n clearSearch();\r\n setShowResults(false);\r\n }}\r\n className=\"text-xs font-medium text-primary hover:underline\"\r\n >\r\n {t(\r\n \"viewAllResults\",\r\n `View all ${searchResults.length} results`\r\n )}\r\n </button>\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"p-6 text-center\">\r\n <Search className=\"h-8 w-8 text-muted-foreground mx-auto mb-2 opacity-50\" />\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"noResults\", \"No results found\")}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n\r\n <div className=\"flex flex-col space-y-4 mt-6\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n {item.name}\r\n </Link>\r\n ))}\r\n <div className=\"border-t pt-4 space-y-4\">\r\n <Link\r\n to=\"/favorites\"\r\n className=\"flex items-center justify-between text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <div className=\"flex items-center space-x-2\">\r\n <Heart className=\"h-5 w-5\" />\r\n <span>{t(\"favorites\")}</span>\r\n </div>\r\n <Badge variant=\"secondary\">{favoriteCount}</Badge>\r\n </Link>\r\n <Link\r\n to=\"/cart\"\r\n className=\"flex items-center justify-between w-full text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <div className=\"flex items-center space-x-2\">\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n <span>{t(\"cart\")}</span>\r\n </div>\r\n <div className=\"flex flex-col items-end\">\r\n <Badge variant=\"secondary\">{itemCount}</Badge>\r\n <span className=\"text-xs text-muted-foreground\">\r\n {formatPrice(state.total, constants.site.currency)}\r\n </span>\r\n </div>\r\n </Link>\r\n\r\n {/* Auth - Mobile */}\r\n {isAuthenticated ? (\r\n <div className=\"space-y-3\">\r\n <div className=\"flex items-center space-x-3 p-3 bg-muted/50 rounded-lg\">\r\n <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <User className=\"h-5 w-5 text-primary\" />\r\n </div>\r\n <div className=\"flex-1 min-w-0\">\r\n <p className=\"text-sm font-medium truncate\">{user?.username}</p>\r\n {user?.email && (\r\n <p className=\"text-xs text-muted-foreground truncate\">{user.email}</p>\r\n )}\r\n </div>\r\n </div>\r\n <Link\r\n to=\"/my-orders\"\r\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <Package className=\"h-5 w-5\" />\r\n <span>{t(\"myOrders\", \"My Orders\")}</span>\r\n </Link>\r\n <button\r\n onClick={() => {\r\n handleLogout();\r\n setMobileMenuOpen(false);\r\n }}\r\n className=\"flex items-center space-x-2 text-lg font-medium text-red-600 hover:text-red-700 transition-colors w-full\"\r\n >\r\n <LogOut className=\"h-5 w-5\" />\r\n <span>{t(\"logout\", \"Logout\")}</span>\r\n </button>\r\n </div>\r\n ) : (\r\n <Link\r\n to=\"/login\"\r\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <User className=\"h-5 w-5\" />\r\n <span>{t(\"login\", \"Login\")}</span>\r\n </Link>\r\n )}\r\n </div>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n </div>\r\n </div>\r\n </div>\r\n {/* Cart Drawer */}\r\n <CartDrawer showTrigger={false} />\r\n </header>\r\n );\r\n}\r\n"
23
+ "content": "import { useState } from \"react\";\r\nimport { Link, useNavigate } from \"react-router\";\r\nimport { ShoppingCart, Menu, Search, Heart, Package, User, LogOut } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport {\r\n Sheet,\r\n SheetHeader,\r\n SheetTitle,\r\n SheetContent,\r\n SheetTrigger,\r\n} from \"@/components/ui/sheet\";\r\nimport {\r\n Dialog,\r\n DialogContent,\r\n DialogHeader,\r\n DialogTitle,\r\n DialogTrigger,\r\n} from \"@/components/ui/dialog\";\r\nimport {\r\n DropdownMenu,\r\n DropdownMenuContent,\r\n DropdownMenuItem,\r\n DropdownMenuLabel,\r\n DropdownMenuSeparator,\r\n DropdownMenuTrigger,\r\n} from \"@/components/ui/dropdown-menu\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { CartDrawer } from \"@/modules/cart-drawer\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport type { Product } from \"@/modules/ecommerce-core/types\";\r\nimport {\r\n useCart,\r\n useFavorites,\r\n useDbSearch,\r\n formatPrice,\r\n} from \"@/modules/ecommerce-core\";\r\n\r\nexport function HeaderEcommerce() {\r\n const [mobileMenuOpen, setMobileMenuOpen] = useState(false);\r\n const [mobileSearchOpen, setMobileSearchOpen] = useState(false);\r\n const [desktopSearchOpen, setDesktopSearchOpen] = useState(false);\r\n const [showResults, setShowResults] = useState(false);\r\n const { itemCount, state } = useCart();\r\n const { favoriteCount } = useFavorites();\r\n const { isAuthenticated, user, logout } = useAuth();\r\n const navigate = useNavigate();\r\n const { t } = useTranslation(\"header-ecommerce\");\r\n\r\n const handleLogout = () => {\r\n logout();\r\n toast.success(t(\"logoutToastTitle\", \"Goodbye!\"), {\r\n description: t(\"logoutToastDesc\", \"You have been logged out successfully.\"),\r\n });\r\n };\r\n\r\n const {\r\n searchTerm,\r\n setSearchTerm,\r\n results: searchResults,\r\n clearSearch,\r\n } = useDbSearch();\r\n\r\n const handleSearchSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n if (searchTerm.trim()) {\r\n navigate(`/products?search=${encodeURIComponent(searchTerm)}`);\r\n setShowResults(false);\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }\r\n };\r\n\r\n const handleSearchFocus = () => {\r\n setShowResults(true);\r\n };\r\n\r\n const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n setSearchTerm(e.target.value);\r\n setShowResults(true);\r\n };\r\n\r\n const navigation = [\r\n { name: t(\"home\"), href: \"/\" },\r\n { name: t(\"products\"), href: \"/products\" },\r\n { name: t(\"about\"), href: \"/about\" },\r\n { name: t(\"contact\"), href: \"/contact\" },\r\n ];\r\n\r\n return (\r\n <header className=\"sticky top-0 z-50 w-full border-b border-border/20 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\r\n <div className=\"flex h-14 sm:h-16 md:h-20 items-center justify-between gap-2\">\r\n {/* Logo */}\r\n <div className=\"flex-shrink-0 min-w-0\">\r\n <Logo size=\"sm\" className=\"text-base sm:text-xl lg:text-2xl\" />\r\n </div>\r\n\r\n {/* Desktop Navigation - Centered */}\r\n <nav className=\"hidden lg:flex items-center space-x-12 absolute left-1/2 transform -translate-x-1/2\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-base font-medium transition-colors hover:text-primary relative group py-2\"\r\n >\r\n {item.name}\r\n <span className=\"absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full\"></span>\r\n </Link>\r\n ))}\r\n </nav>\r\n\r\n {/* Search & Actions - Right Aligned */}\r\n <div className=\"flex items-center space-x-1 sm:space-x-2 lg:space-x-4 flex-shrink-0\">\r\n {/* Desktop Search - Modal */}\r\n <Dialog\r\n open={desktopSearchOpen}\r\n onOpenChange={setDesktopSearchOpen}\r\n >\r\n <DialogTrigger asChild>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"hidden lg:flex h-10 w-10\"\r\n >\r\n <Search className=\"h-5 w-5\" />\r\n </Button>\r\n </DialogTrigger>\r\n <DialogContent className=\"sm:max-w-2xl\">\r\n <DialogHeader>\r\n <DialogTitle>\r\n {t(\"searchProducts\", \"Search Products\")}\r\n </DialogTitle>\r\n </DialogHeader>\r\n <div className=\"space-y-4\">\r\n <form onSubmit={handleSearchSubmit}>\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\r\n \"searchPlaceholder\",\r\n \"Search for products...\"\r\n )}\r\n value={searchTerm}\r\n onChange={handleSearchChange}\r\n className=\"pl-11 h-12 text-base\"\r\n autoFocus\r\n />\r\n </div>\r\n </form>\r\n\r\n {/* Desktop Search Results */}\r\n {searchTerm.trim() && (\r\n <div className=\"max-h-[400px] overflow-y-auto rounded-lg border bg-card\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"divide-y\">\r\n <div className=\"px-4 py-3 bg-muted/50\">\r\n <p className=\"text-sm font-medium text-muted-foreground\">\r\n {searchResults.length}{\" \"}\r\n {searchResults.length === 1\r\n ? \"result\"\r\n : \"results\"}{\" \"}\r\n found\r\n </p>\r\n </div>\r\n {searchResults.slice(0, 8).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"flex items-center gap-4 p-4 hover:bg-muted/50 transition-colors\"\r\n >\r\n <img\r\n src={\r\n product.images[0] || \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-16 h-16 object-cover rounded flex-shrink-0\"\r\n />\r\n <div className=\"flex-1 min-w-0\">\r\n <h4 className=\"font-medium text-base line-clamp-1\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-sm text-muted-foreground capitalize\">\r\n {product.category}\r\n </p>\r\n <p className=\"text-base font-semibold text-primary mt-1\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n {searchResults.length > 8 && (\r\n <div className=\"px-4 py-3 bg-muted/30 text-center\">\r\n <button\r\n onClick={() => {\r\n navigate(\r\n `/products?search=${encodeURIComponent(\r\n searchTerm\r\n )}`\r\n );\r\n setDesktopSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"text-sm font-medium text-primary hover:underline\"\r\n >\r\n {t(\r\n \"viewAllResults\",\r\n `View all ${searchResults.length} results`\r\n )}\r\n </button>\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"p-8 text-center\">\r\n <Search className=\"h-12 w-12 text-muted-foreground mx-auto mb-3 opacity-50\" />\r\n <p className=\"text-base text-muted-foreground\">\r\n {t(\"noResults\", \"No products found\")}\r\n </p>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\r\n \"tryDifferentKeywords\",\r\n \"Try different keywords\"\r\n )}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </DialogContent>\r\n </Dialog>\r\n\r\n {/* Search - Mobile (Hidden - moved to hamburger menu) */}\r\n <Dialog open={mobileSearchOpen} onOpenChange={setMobileSearchOpen}>\r\n <DialogTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"hidden\">\r\n <Search className=\"h-4 w-4 sm:h-5 sm:w-5\" />\r\n </Button>\r\n </DialogTrigger>\r\n <DialogContent className=\"sm:max-w-md\">\r\n <DialogHeader>\r\n <DialogTitle>{t(\"searchProducts\")}</DialogTitle>\r\n </DialogHeader>\r\n <form\r\n onSubmit={(e) => {\r\n e.preventDefault();\r\n if (searchTerm.trim()) {\r\n navigate(\r\n `/products?search=${encodeURIComponent(searchTerm)}`\r\n );\r\n setMobileSearchOpen(false);\r\n clearSearch();\r\n }\r\n }}\r\n className=\"space-y-4\"\r\n >\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={(e) => setSearchTerm(e.target.value)}\r\n className=\"pl-10\"\r\n autoFocus\r\n />\r\n </div>\r\n <div className=\"flex gap-2\">\r\n <Button type=\"submit\" className=\"flex-1\">\r\n {t(\"searchButton\", \"Search\")}\r\n </Button>\r\n <Button\r\n type=\"button\"\r\n variant=\"outline\"\r\n onClick={() => {\r\n clearSearch();\r\n setMobileSearchOpen(false);\r\n }}\r\n >\r\n {t(\"cancel\", \"Cancel\")}\r\n </Button>\r\n </div>\r\n </form>\r\n\r\n {/* Mobile Search Results */}\r\n {searchTerm.trim() && (\r\n <div className=\"mt-4 max-h-64 overflow-y-auto\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"space-y-2\">\r\n <p className=\"text-sm text-muted-foreground mb-2\">\r\n {searchResults.length} result\r\n {searchResults.length !== 1 ? \"s\" : \"\"} found\r\n </p>\r\n {searchResults.slice(0, 5).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setMobileSearchOpen(false);\r\n clearSearch();\r\n }}\r\n className=\"block p-2 rounded hover:bg-muted/50 transition-colors\"\r\n >\r\n <div className=\"flex items-center gap-3\">\r\n <img\r\n src={\r\n product.images[0] || \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-10 h-10 object-cover rounded\"\r\n />\r\n <div className=\"flex-1\">\r\n <h4 className=\"font-medium text-sm\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-xs text-muted-foreground\">\r\n {product.category}\r\n </p>\r\n <p className=\"text-sm font-medium\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n </div>\r\n ) : (\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"noResults\")}\r\n </p>\r\n )}\r\n </div>\r\n )}\r\n </DialogContent>\r\n </Dialog>\r\n\r\n {/* Wishlist - Desktop Only */}\r\n <Link to=\"/favorites\" className=\"hidden lg:block\">\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"relative h-10 w-10\"\r\n >\r\n <Heart className=\"h-5 w-5\" />\r\n {favoriteCount > 0 && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\r\n >\r\n {favoriteCount}\r\n </Badge>\r\n )}\r\n </Button>\r\n </Link>\r\n\r\n {/* Cart - Desktop Only (Goes to Cart Page) */}\r\n <Link to=\"/cart\" className=\"hidden lg:block\">\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"relative h-10 w-10\"\r\n >\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n {itemCount > 0 && (\r\n <Badge\r\n variant=\"destructive\"\r\n className=\"absolute -top-1 -right-1 h-4 w-4 flex items-center justify-center p-0 text-[10px]\"\r\n >\r\n {itemCount}\r\n </Badge>\r\n )}\r\n </Button>\r\n </Link>\r\n\r\n {/* Auth - Desktop Only */}\r\n <div className=\"hidden lg:flex\">\r\n {isAuthenticated ? (\r\n <DropdownMenu>\r\n <DropdownMenuTrigger asChild>\r\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\r\n <User className=\"h-5 w-5\" />\r\n </Button>\r\n </DropdownMenuTrigger>\r\n <DropdownMenuContent align=\"end\" className=\"w-56\">\r\n <DropdownMenuLabel className=\"font-normal\">\r\n <div className=\"flex flex-col space-y-1\">\r\n <p className=\"text-sm font-medium\">{user?.username}</p>\r\n {user?.email && (\r\n <p className=\"text-xs text-muted-foreground\">{user.email}</p>\r\n )}\r\n </div>\r\n </DropdownMenuLabel>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuItem asChild className=\"cursor-pointer\">\r\n <Link to=\"/my-orders\" className=\"flex items-center\">\r\n <Package className=\"mr-2 h-4 w-4\" />\r\n {t(\"myOrders\", \"My Orders\")}\r\n </Link>\r\n </DropdownMenuItem>\r\n <DropdownMenuSeparator />\r\n <DropdownMenuItem\r\n onClick={handleLogout}\r\n className=\"text-red-600 focus:text-red-600 focus:bg-red-50 cursor-pointer\"\r\n >\r\n <LogOut className=\"mr-2 h-4 w-4\" />\r\n {t(\"logout\", \"Logout\")}\r\n </DropdownMenuItem>\r\n </DropdownMenuContent>\r\n </DropdownMenu>\r\n ) : (\r\n <Link to=\"/login\">\r\n <Button variant=\"ghost\" size=\"icon\" className=\"h-10 w-10\">\r\n <User className=\"h-5 w-5\" />\r\n </Button>\r\n </Link>\r\n )}\r\n </div>\r\n\r\n {/* Mobile Menu */}\r\n <Sheet open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>\r\n <SheetTrigger asChild>\r\n <Button\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"lg:hidden h-8 w-8 sm:h-10 sm:w-10\"\r\n >\r\n <Menu className=\"h-4 w-4 sm:h-5 sm:w-5\" />\r\n </Button>\r\n </SheetTrigger>\r\n <SheetContent side=\"right\" className=\"w-[300px] sm:w-[400px] px-6\">\r\n <SheetHeader>\r\n <SheetTitle>{t(\"menu\")}</SheetTitle>\r\n </SheetHeader>\r\n\r\n {/* Mobile Search in Hamburger */}\r\n <div className=\"mt-6 pb-4 border-b\">\r\n <form onSubmit={handleSearchSubmit}>\r\n <div className=\"relative\">\r\n <Search className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4\" />\r\n <Input\r\n type=\"search\"\r\n placeholder={t(\"searchPlaceholder\")}\r\n value={searchTerm}\r\n onChange={handleSearchChange}\r\n onFocus={handleSearchFocus}\r\n className=\"pl-10 h-11\"\r\n />\r\n </div>\r\n </form>\r\n\r\n {/* Search Results in Hamburger */}\r\n {showResults && searchTerm && (\r\n <div className=\"mt-3 max-h-[300px] overflow-y-auto rounded-lg border bg-card\">\r\n {searchResults.length > 0 ? (\r\n <div className=\"divide-y\">\r\n <div className=\"px-3 py-2 bg-muted/50\">\r\n <p className=\"text-xs font-medium text-muted-foreground\">\r\n {searchResults.length}{\" \"}\r\n {searchResults.length === 1\r\n ? \"result\"\r\n : \"results\"}\r\n </p>\r\n </div>\r\n {searchResults.slice(0, 5).map((product: Product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <Link\r\n to={`/products/${product.slug}`}\r\n onClick={() => {\r\n setMobileMenuOpen(false);\r\n clearSearch();\r\n setShowResults(false);\r\n }}\r\n className=\"flex items-center gap-3 p-3 hover:bg-muted/50 transition-colors\"\r\n >\r\n <img\r\n src={\r\n product.images[0] || \"/images/placeholder.png\"\r\n }\r\n alt={product.name}\r\n className=\"w-14 h-14 object-cover rounded flex-shrink-0\"\r\n />\r\n <div className=\"flex-1 min-w-0\">\r\n <h4 className=\"font-medium text-sm line-clamp-1\">\r\n {product.name}\r\n </h4>\r\n <p className=\"text-xs text-muted-foreground capitalize\">\r\n {product.category}\r\n </p>\r\n <p className=\"text-sm font-semibold text-primary mt-1\">\r\n {formatPrice(\r\n product.price,\r\n constants.site.currency\r\n )}\r\n </p>\r\n </div>\r\n </Link>\r\n </div>\r\n ))}\r\n {searchResults.length > 5 && (\r\n <div className=\"px-3 py-2 bg-muted/30 text-center\">\r\n <button\r\n onClick={() => {\r\n navigate(\r\n `/products?search=${encodeURIComponent(\r\n searchTerm\r\n )}`\r\n );\r\n setMobileMenuOpen(false);\r\n clearSearch();\r\n setShowResults(false);\r\n }}\r\n className=\"text-xs font-medium text-primary hover:underline\"\r\n >\r\n {t(\r\n \"viewAllResults\",\r\n `View all ${searchResults.length} results`\r\n )}\r\n </button>\r\n </div>\r\n )}\r\n </div>\r\n ) : (\r\n <div className=\"p-6 text-center\">\r\n <Search className=\"h-8 w-8 text-muted-foreground mx-auto mb-2 opacity-50\" />\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"noResults\", \"No results found\")}\r\n </p>\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n\r\n <div className=\"flex flex-col space-y-4 mt-6\">\r\n {navigation.map((item) => (\r\n <Link\r\n key={item.name}\r\n to={item.href}\r\n className=\"text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n {item.name}\r\n </Link>\r\n ))}\r\n <div className=\"border-t pt-4 space-y-4\">\r\n <Link\r\n to=\"/favorites\"\r\n className=\"flex items-center justify-between text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <div className=\"flex items-center space-x-2\">\r\n <Heart className=\"h-5 w-5\" />\r\n <span>{t(\"favorites\")}</span>\r\n </div>\r\n <Badge variant=\"secondary\">{favoriteCount}</Badge>\r\n </Link>\r\n <Link\r\n to=\"/cart\"\r\n className=\"flex items-center justify-between w-full text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <div className=\"flex items-center space-x-2\">\r\n <ShoppingCart className=\"h-5 w-5\" />\r\n <span>{t(\"cart\")}</span>\r\n </div>\r\n <div className=\"flex flex-col items-end\">\r\n <Badge variant=\"secondary\">{itemCount}</Badge>\r\n <span className=\"text-xs text-muted-foreground\">\r\n {formatPrice(state.total, constants.site.currency)}\r\n </span>\r\n </div>\r\n </Link>\r\n\r\n {/* Auth - Mobile */}\r\n {isAuthenticated ? (\r\n <div className=\"space-y-3\">\r\n <div className=\"flex items-center space-x-3 p-3 bg-muted/50 rounded-lg\">\r\n <div className=\"h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <User className=\"h-5 w-5 text-primary\" />\r\n </div>\r\n <div className=\"flex-1 min-w-0\">\r\n <p className=\"text-sm font-medium truncate\">{user?.username}</p>\r\n {user?.email && (\r\n <p className=\"text-xs text-muted-foreground truncate\">{user.email}</p>\r\n )}\r\n </div>\r\n </div>\r\n <Link\r\n to=\"/my-orders\"\r\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <Package className=\"h-5 w-5\" />\r\n <span>{t(\"myOrders\", \"My Orders\")}</span>\r\n </Link>\r\n <button\r\n onClick={() => {\r\n handleLogout();\r\n setMobileMenuOpen(false);\r\n }}\r\n className=\"flex items-center space-x-2 text-lg font-medium text-red-600 hover:text-red-700 transition-colors w-full\"\r\n >\r\n <LogOut className=\"h-5 w-5\" />\r\n <span>{t(\"logout\", \"Logout\")}</span>\r\n </button>\r\n </div>\r\n ) : (\r\n <Link\r\n to=\"/login\"\r\n className=\"flex items-center space-x-2 text-lg font-medium hover:text-primary transition-colors\"\r\n onClick={() => setMobileMenuOpen(false)}\r\n >\r\n <User className=\"h-5 w-5\" />\r\n <span>{t(\"login\", \"Login\")}</span>\r\n </Link>\r\n )}\r\n </div>\r\n </div>\r\n </SheetContent>\r\n </Sheet>\r\n </div>\r\n </div>\r\n </div>\r\n {/* Cart Drawer */}\r\n <CartDrawer showTrigger={false} />\r\n </header>\r\n );\r\n}\r\n"
24
24
  },
25
25
  {
26
26
  "path": "header-ecommerce/lang/en.json",
@@ -27,6 +27,7 @@
27
27
  "cookie-consent",
28
28
  "cookies-page",
29
29
  "cta-section",
30
+ "db",
30
31
  "ecommerce-core",
31
32
  "empty-page",
32
33
  "faq-categorized",
@@ -23,7 +23,7 @@
23
23
  "path": "login-page-split/login-page-split.tsx",
24
24
  "type": "registry:page",
25
25
  "target": "$modules$/login-page-split/login-page-split.tsx",
26
- "content": "import { useState } from \"react\";\r\nimport { Link, useNavigate, useLocation } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\n\r\ninterface LoginPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function LoginPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: LoginPageSplitProps) {\r\n const { t } = useTranslation(\"login-page-split\");\r\n const navigate = useNavigate();\r\n const location = useLocation();\r\n const { login } = useAuth();\r\n\r\n const [username, setUsername] = useState(\"\");\r\n const [password, setPassword] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n // Get redirect URL from location state or default to home\r\n const from = (location.state as { from?: string })?.from || \"/\";\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n setIsLoading(true);\r\n\r\n try {\r\n await login(username, password);\r\n toast.success(t(\"loginSuccess\", \"Login successful!\"));\r\n navigate(from, { replace: true });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"loginError\", \"Login failed. Please check your credentials.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6\">\r\n <Logo />\r\n <hr />\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Login\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"subtitle\", \"Enter your details below to login\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <form onSubmit={handleSubmit} className=\"grid gap-4\">\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"username-split\">{t(\"username\", \"Username\")}</Label>\r\n <Input\r\n required\r\n id=\"username-split\"\r\n type=\"text\"\r\n autoComplete=\"username\"\r\n placeholder={t(\"usernamePlaceholder\", \"Enter your username\")}\r\n value={username}\r\n onChange={(e) => setUsername(e.target.value)}\r\n disabled={isLoading}\r\n />\r\n </div>\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"password-split\">{t(\"password\", \"Password\")}</Label>\r\n <Input\r\n required\r\n id=\"password-split\"\r\n type=\"password\"\r\n placeholder=\"••••••••••\"\r\n autoComplete=\"current-password\"\r\n value={password}\r\n onChange={(e) => setPassword(e.target.value)}\r\n disabled={isLoading}\r\n />\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"loggingIn\", \"Logging in...\")}\r\n </>\r\n ) : (\r\n t(\"login\", \"Login\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <div className=\"flex flex-col gap-4 text-sm\">\r\n <p>\r\n {t(\"noAccount\", \"Don't have an account?\")}{\" \"}\r\n <Link to=\"/register\" className=\"underline\">\r\n {t(\"signUp\", \"Sign up\")}\r\n </Link>\r\n </p>\r\n <Link to=\"/forgot-password\" className=\"underline\">\r\n {t(\"forgotPassword\", \"Forgot your password?\")}\r\n </Link>\r\n </div>\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Login background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default LoginPageSplit;\r\n"
26
+ "content": "import { useState } from \"react\";\r\nimport { Link, useNavigate, useLocation } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { FormField } from \"@/components/FormField\";\r\nimport { PasswordInput } from \"@/components/PasswordInput\";\r\n\r\ninterface LoginPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function LoginPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: LoginPageSplitProps) {\r\n const { t } = useTranslation(\"login-page-split\");\r\n const navigate = useNavigate();\r\n const location = useLocation();\r\n const { login } = useAuth();\r\n\r\n const [username, setUsername] = useState(\"\");\r\n const [password, setPassword] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n // Get redirect URL from location state or default to home\r\n const from = (location.state as { from?: string })?.from || \"/\";\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n setIsLoading(true);\r\n\r\n try {\r\n await login(username, password);\r\n toast.success(t(\"loginSuccess\", \"Login successful!\"));\r\n navigate(from, { replace: true });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"loginError\", \"Login failed. Please check your credentials.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6\">\r\n <Logo />\r\n <hr />\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Login\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"subtitle\", \"Enter your details below to login\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <form onSubmit={handleSubmit} className=\"grid gap-4\">\r\n <div className=\"grid gap-2\">\r\n <FormField label={t(\"username\", \"Username\")} htmlFor=\"username-split\">\r\n <Input\r\n required\r\n id=\"username-split\"\r\n type=\"text\"\r\n autoComplete=\"username\"\r\n placeholder={t(\"usernamePlaceholder\", \"Enter your username\")}\r\n value={username}\r\n onChange={(e) => setUsername(e.target.value)}\r\n disabled={isLoading}\r\n />\r\n </FormField>\r\n </div>\r\n <div className=\"grid gap-2\">\r\n <FormField label={t(\"password\", \"Password\")} htmlFor=\"password-split\">\r\n <PasswordInput\r\n id=\"password-split\"\r\n name=\"password\"\r\n value={password}\r\n disabled={isLoading}\r\n onChange={(e) => setPassword(e.target.value)}\r\n placeholder=\"••••••••••\"\r\n required\r\n className=\"mt-1\"\r\n autoComplete=\"current-password\"\r\n />\r\n </FormField>\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"loggingIn\", \"Logging in...\")}\r\n </>\r\n ) : (\r\n t(\"login\", \"Login\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <div className=\"flex flex-col gap-4 text-sm\">\r\n <p>\r\n {t(\"noAccount\", \"Don't have an account?\")}{\" \"}\r\n <Link to=\"/register\" className=\"underline\">\r\n {t(\"signUp\", \"Sign up\")}\r\n </Link>\r\n </p>\r\n <Link to=\"/forgot-password\" className=\"underline\">\r\n {t(\"forgotPassword\", \"Forgot your password?\")}\r\n </Link>\r\n </div>\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Login background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default LoginPageSplit;\r\n"
27
27
  },
28
28
  {
29
29
  "path": "login-page-split/lang/en.json",
@@ -23,7 +23,7 @@
23
23
  "path": "login-page/login-page.tsx",
24
24
  "type": "registry:page",
25
25
  "target": "$modules$/login-page/login-page.tsx",
26
- "content": "import { useState } from \"react\";\r\nimport { Link, useNavigate, useLocation } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { toast } from \"sonner\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\n\r\ninterface LoginPageProps {\r\n className?: string;\r\n}\r\n\r\nexport function LoginPage({ className }: LoginPageProps) {\r\n const { t } = useTranslation(\"login-page\");\r\n usePageTitle({ title: t(\"title\", \"Sign In\") });\r\n const navigate = useNavigate();\r\n const location = useLocation();\r\n const { login } = useAuth();\r\n\r\n const [username, setUsername] = useState(\"\");\r\n const [password, setPassword] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const from = (location.state as { from?: string })?.from || \"/\";\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n setIsLoading(true);\r\n\r\n try {\r\n await login(username, password);\r\n toast.success(t(\"loginSuccess\", \"Login successful!\"));\r\n navigate(from, { replace: true });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"loginError\", \"Login failed. Please check your credentials.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n return (\r\n <section\r\n className={cn(\"flex min-h-screen bg-muted/30 px-4 py-16 md:py-32\", className)}\r\n >\r\n <form\r\n onSubmit={handleSubmit}\r\n className=\"bg-muted m-auto h-fit w-full max-w-sm overflow-hidden rounded-xl border shadow-md\"\r\n >\r\n <div className=\"bg-card -m-px rounded-xl border p-8 pb-6\">\r\n <div className=\"text-center\">\r\n <div className=\"mx-auto block w-fit\">\r\n <Logo size=\"sm\" />\r\n </div>\r\n <h1 className=\"mb-1 mt-4 text-xl font-semibold\">\r\n {t(\"title\", \"Sign In\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"subtitle\", \"Welcome back! Sign in to continue\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"mt-4 p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <div className=\"mt-6 space-y-6\">\r\n <div className=\"space-y-2\">\r\n <Label htmlFor=\"username\" className=\"block text-sm\">\r\n {t(\"username\", \"Username\")}\r\n </Label>\r\n <Input\r\n type=\"text\"\r\n required\r\n name=\"username\"\r\n id=\"username\"\r\n autoComplete=\"username\"\r\n value={username}\r\n onChange={(e) => setUsername(e.target.value)}\r\n placeholder={t(\"usernamePlaceholder\", \"Enter your username\")}\r\n disabled={isLoading}\r\n />\r\n </div>\r\n\r\n <div className=\"space-y-2\">\r\n <div className=\"flex items-center justify-between\">\r\n <Label htmlFor=\"password\" className=\"text-sm\">\r\n {t(\"password\", \"Password\")}\r\n </Label>\r\n <Button asChild variant=\"link\" size=\"sm\" className=\"h-auto p-0\">\r\n <Link to=\"/forgot-password\" className=\"text-xs\">\r\n {t(\"forgotPassword\", \"Forgot password?\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n <Input\r\n type=\"password\"\r\n required\r\n name=\"password\"\r\n id=\"password\"\r\n value={password}\r\n onChange={(e) => setPassword(e.target.value)}\r\n placeholder=\"••••••••\"\r\n disabled={isLoading}\r\n />\r\n </div>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"signingIn\", \"Signing in...\")}\r\n </>\r\n ) : (\r\n t(\"signIn\", \"Sign In\")\r\n )}\r\n </Button>\r\n </div>\r\n </div>\r\n\r\n <div className=\"p-3\">\r\n <p className=\"text-center text-sm text-muted-foreground\">\r\n {t(\"noAccount\", \"Don't have an account?\")}\r\n <Button asChild variant=\"link\" className=\"px-2\">\r\n <Link to=\"/register\">{t(\"createAccount\", \"Create account\")}</Link>\r\n </Button>\r\n </p>\r\n </div>\r\n </form>\r\n </section>\r\n );\r\n}\r\n\r\nexport default LoginPage;\r\n"
26
+ "content": "import { useState } from \"react\";\r\nimport { Link, useNavigate, useLocation } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { toast } from \"sonner\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { FormField } from \"@/components/FormField\";\r\nimport { PasswordInput } from \"@/components/PasswordInput\";\r\n\r\ninterface LoginPageProps {\r\n className?: string;\r\n}\r\n\r\nexport function LoginPage({ className }: LoginPageProps) {\r\n const { t } = useTranslation(\"login-page\");\r\n usePageTitle({ title: t(\"title\", \"Sign In\") });\r\n const navigate = useNavigate();\r\n const location = useLocation();\r\n const { login } = useAuth();\r\n\r\n const [username, setUsername] = useState(\"\");\r\n const [password, setPassword] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n\r\n const from = (location.state as { from?: string })?.from || \"/\";\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setError(null);\r\n setIsLoading(true);\r\n\r\n try {\r\n await login(username, password);\r\n toast.success(t(\"loginSuccess\", \"Login successful!\"));\r\n navigate(from, { replace: true });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"loginError\", \"Login failed. Please check your credentials.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n return (\r\n <section\r\n className={cn(\"flex min-h-screen bg-muted/30 px-4 py-16 md:py-32\", className)}\r\n >\r\n <form\r\n onSubmit={handleSubmit}\r\n className=\"bg-muted m-auto h-fit w-full max-w-sm overflow-hidden rounded-xl border shadow-md\"\r\n >\r\n <div className=\"bg-card -m-px rounded-xl border p-8 pb-6\">\r\n <div className=\"text-center\">\r\n <Link to=\"/\" aria-label=\"go home\" className=\"mx-auto block w-fit\">\r\n <Logo size=\"sm\" />\r\n </Link>\r\n <h1 className=\"mb-1 mt-4 text-xl font-semibold\">\r\n {t(\"title\", \"Sign In\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"subtitle\", \"Welcome back! Sign in to continue\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"mt-4 p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <div className=\"mt-6 space-y-6\">\r\n <FormField label={t(\"username\", \"Username\")} htmlFor=\"username\">\r\n <Input\r\n type=\"text\"\r\n required\r\n name=\"username\"\r\n id=\"username\"\r\n autoComplete=\"username\"\r\n value={username}\r\n onChange={(e) => setUsername(e.target.value)}\r\n placeholder={t(\"usernamePlaceholder\", \"Enter your username\")}\r\n disabled={isLoading}\r\n />\r\n </FormField>\r\n <FormField\r\n label={t(\"password\", \"Password\")}\r\n htmlFor=\"password\"\r\n labelAction={\r\n <Button asChild variant=\"link\" size=\"sm\" className=\"h-auto p-0\">\r\n <Link to=\"/forgot-password\" className=\"text-xs\">\r\n {t(\"forgotPassword\", \"Forgot password?\")}\r\n </Link>\r\n </Button>\r\n }\r\n >\r\n <PasswordInput\r\n id=\"password\"\r\n name=\"password\"\r\n value={password}\r\n onChange={(e) => setPassword(e.target.value)}\r\n placeholder=\"••••••••\"\r\n disabled={isLoading}\r\n required\r\n />\r\n </FormField>\r\n\r\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\r\n {isLoading ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"signingIn\", \"Signing in...\")}\r\n </>\r\n ) : (\r\n t(\"signIn\", \"Sign In\")\r\n )}\r\n </Button>\r\n </div>\r\n </div>\r\n\r\n <div className=\"p-3\">\r\n <p className=\"text-center text-sm text-muted-foreground\">\r\n {t(\"noAccount\", \"Don't have an account?\")}\r\n <Button asChild variant=\"link\" className=\"px-2\">\r\n <Link to=\"/register\">{t(\"createAccount\", \"Create account\")}</Link>\r\n </Button>\r\n </p>\r\n </div>\r\n </form>\r\n </section>\r\n );\r\n}\r\n\r\nexport default LoginPage;\r\n"
27
27
  },
28
28
  {
29
29
  "path": "login-page/lang/en.json",
@@ -20,7 +20,7 @@
20
20
  "path": "newsletter-section/newsletter-section.tsx",
21
21
  "type": "registry:component",
22
22
  "target": "$modules$/newsletter-section/newsletter-section.tsx",
23
- "content": "import { Button } from \"@/components/ui/button\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { useTranslation } from \"react-i18next\";\r\n\r\ninterface NewsletterSectionProps {\r\n image?: string;\r\n className?: string;\r\n}\r\n\r\nexport function NewsletterSection({\r\n image = \"/images/placeholder.png\",\r\n className,\r\n}: NewsletterSectionProps) {\r\n const { t } = useTranslation(\"newsletter-section\");\r\n\r\n const handleSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n // Handle newsletter subscription\r\n };\r\n\r\n return (\r\n <section className={`px-4 py-20 bg-accent ${className || \"\"}`}>\r\n <div className=\"mx-auto max-w-6xl\">\r\n <Card className=\"overflow-hidden shadow-none p-0\">\r\n <CardContent className=\"p-0\">\r\n <div className=\"grid md:grid-cols-2 items-center\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n src={image}\r\n alt={t(\"imageAlt\", \"Newsletter\")}\r\n width={1200}\r\n height={800}\r\n className=\"h-64 w-full object-cover md:h-full\"\r\n />\r\n <form\r\n onSubmit={handleSubmit}\r\n className=\"flex flex-col gap-4 p-8 max-w-md w-full mx-auto\"\r\n >\r\n <div className=\"space-y-1 text-center\">\r\n <h2 className=\"text-2xl font-bold tracking-tight\">\r\n {t(\"title\", \"Get the inside scoop\")}\r\n </h2>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"description\", \"Join our newsletter to stay ahead.\")}\r\n </p>\r\n </div>\r\n <div className=\"grid gap-2 text-left\">\r\n <Label htmlFor=\"newsletter-email\">\r\n {t(\"emailLabel\", \"Email\")}\r\n </Label>\r\n <Input\r\n id=\"newsletter-email\"\r\n type=\"email\"\r\n placeholder={t(\"emailPlaceholder\", \"Enter your email\")}\r\n required\r\n />\r\n </div>\r\n <Button type=\"submit\">{t(\"subscribe\", \"Subscribe\")}</Button>\r\n </form>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
23
+ "content": "import { FormField } from \"@/components/FormField\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Card, CardContent } from \"@/components/ui/card\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { useTranslation } from \"react-i18next\";\r\n\r\ninterface NewsletterSectionProps {\r\n image?: string;\r\n className?: string;\r\n}\r\n\r\nexport function NewsletterSection({\r\n image = \"/images/placeholder.png\",\r\n className,\r\n}: NewsletterSectionProps) {\r\n const { t } = useTranslation(\"newsletter-section\");\r\n\r\n const handleSubmit = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n // Handle newsletter subscription\r\n };\r\n\r\n return (\r\n <section className={`px-4 py-20 bg-accent ${className || \"\"}`}>\r\n <div className=\"mx-auto max-w-6xl\">\r\n <Card className=\"overflow-hidden shadow-none p-0\">\r\n <CardContent className=\"p-0\">\r\n <div className=\"grid md:grid-cols-2 items-center\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n src={image}\r\n alt={t(\"imageAlt\", \"Newsletter\")}\r\n width={1200}\r\n height={800}\r\n className=\"h-64 w-full object-cover md:h-full\"\r\n />\r\n <form\r\n onSubmit={handleSubmit}\r\n className=\"flex flex-col gap-4 p-8 max-w-md w-full mx-auto\"\r\n >\r\n <div className=\"space-y-1 text-center\">\r\n <h2 className=\"text-2xl font-bold tracking-tight\">\r\n {t(\"title\", \"Get the inside scoop\")}\r\n </h2>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"description\", \"Join our newsletter to stay ahead.\")}\r\n </p>\r\n </div>\r\n <div className=\"grid gap-2 text-left\">\r\n <FormField label={t(\"emailLabel\", \"Email\")} htmlFor=\"newsletter-email\" required>\r\n <Input\r\n id=\"newsletter-email\"\r\n type=\"email\"\r\n placeholder={t(\"emailPlaceholder\", \"Enter your email\")}\r\n required\r\n />\r\n </FormField>\r\n \r\n </div>\r\n <Button type=\"submit\">{t(\"subscribe\", \"Subscribe\")}</Button>\r\n </form>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
24
24
  },
25
25
  {
26
26
  "path": "newsletter-section/lang/en.json",
@@ -18,7 +18,7 @@
18
18
  "path": "post-card/post-card.tsx",
19
19
  "type": "registry:component",
20
20
  "target": "$modules$/post-card/post-card.tsx",
21
- "content": "import { useMemo } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { Calendar, Eye, Clock, ArrowRight } from \"lucide-react\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardFooter,\r\n CardHeader,\r\n} from \"@/components/ui/card\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useDbList } from \"@/db\";\r\nimport type { Post, BlogCategory } from \"@/modules/blog-core/types\";\r\n\r\ninterface CardWrapperProps {\r\n children: React.ReactNode;\r\n className?: string;\r\n}\r\n\r\nfunction CardWrapper({ children, className = \"\" }: CardWrapperProps) {\r\n return (\r\n <Card\r\n className={`group overflow-hidden border-0 p-0 shadow-sm hover:shadow-lg transition-all duration-300 h-full ${className}`}\r\n >\r\n {children}\r\n </Card>\r\n );\r\n}\r\n\r\ninterface PostCardProps {\r\n post: Post;\r\n layout?: \"grid\" | \"list\";\r\n showExcerpt?: boolean;\r\n className?: string;\r\n}\r\n\r\nexport function PostCard({\r\n post,\r\n layout = \"grid\",\r\n showExcerpt = true,\r\n className = \"\",\r\n}: PostCardProps) {\r\n const { t } = useTranslation(\"post-card\");\r\n const { data: blogCategories = [] } = useDbList<BlogCategory>(\"blog_categories\");\r\n const categoryMap = useMemo(() => new Map(blogCategories.map(c => [c.id, c])), [blogCategories]);\r\n\r\n const formatDate = (dateString: string) => {\r\n return new Date(dateString).toLocaleDateString(\"en-US\", {\r\n year: \"numeric\",\r\n month: \"short\",\r\n day: \"numeric\",\r\n });\r\n };\r\n\r\n if (layout === \"list\") {\r\n return (\r\n <CardWrapper className={className}>\r\n <div className=\"flex flex-col md:flex-row\">\r\n {post.featured_image && (\r\n <div className=\"md:w-1/3 flex-shrink-0\">\r\n <Link to={`/blog/${post.slug}`}>\r\n <img\r\n src={post.featured_image}\r\n alt={post.title}\r\n className=\"w-full h-48 md:h-full object-cover\"\r\n onError={(e) => {\r\n e.currentTarget.src = \"/images/placeholder.png\";\r\n }}\r\n />\r\n </Link>\r\n </div>\r\n )}\r\n\r\n <div className=\"flex-1 flex flex-col\">\r\n <CardHeader className=\"pt-6 pb-3\">\r\n {post.categories && post.categories.length > 0 && (\r\n <div className=\"flex flex-wrap gap-1 mb-2\">\r\n {post.categories.slice(0, 2).map((catId) => {\r\n const category = categoryMap.get(catId);\r\n if (!category) return null;\r\n return (\r\n <div key={catId} className=\"contents\" data-db-table=\"blog_categories\" data-db-id={catId}>\r\n <Badge\r\n variant=\"secondary\"\r\n className=\"text-xs\"\r\n >\r\n <Link to={`/blog?categories=${category.slug}`}>\r\n {category.name}\r\n </Link>\r\n </Badge>\r\n </div>\r\n );\r\n })}\r\n </div>\r\n )}\r\n\r\n <h3 className=\"text-xl font-semibold line-clamp-2 group-hover:text-primary transition-colors\">\r\n <Link to={`/blog/${post.slug}`}>{post.title}</Link>\r\n </h3>\r\n </CardHeader>\r\n\r\n <CardContent className=\"flex-1 pb-3\">\r\n {showExcerpt && post.excerpt && (\r\n <p className=\"text-muted-foreground text-sm line-clamp-3 mb-4\">\r\n {post.excerpt}\r\n </p>\r\n )}\r\n\r\n <div className=\"flex flex-wrap items-center gap-4 text-xs text-muted-foreground\">\r\n <div className=\"flex items-center gap-1\">\r\n <Calendar className=\"h-3 w-3\" />\r\n <span>{formatDate(post.published_at)}</span>\r\n </div>\r\n\r\n {post.read_time > 0 && (\r\n <div className=\"flex items-center gap-1\">\r\n <Clock className=\"h-3 w-3\" />\r\n <span>\r\n {post.read_time} {t(\"minRead\", \"min\")}\r\n </span>\r\n </div>\r\n )}\r\n\r\n {post.view_count > 0 && (\r\n <div className=\"flex items-center gap-1\">\r\n <Eye className=\"h-3 w-3\" />\r\n <span>{post.view_count.toLocaleString()}</span>\r\n </div>\r\n )}\r\n </div>\r\n </CardContent>\r\n\r\n <CardFooter className=\"pt-0 pb-6\">\r\n <Button variant=\"ghost\" size=\"sm\" asChild>\r\n <Link to={`/blog/${post.slug}`}>\r\n {t(\"readMore\", \"Read More\")}\r\n <ArrowRight className=\"h-3 w-3 ml-1\" />\r\n </Link>\r\n </Button>\r\n </CardFooter>\r\n </div>\r\n </div>\r\n </CardWrapper>\r\n );\r\n }\r\n\r\n return (\r\n <CardWrapper className={className}>\r\n <div className=\"flex flex-col h-full\">\r\n {post.featured_image && (\r\n <div className=\"aspect-video overflow-hidden\">\r\n <Link to={`/blog/${post.slug}`}>\r\n <img\r\n src={post.featured_image}\r\n alt={post.title}\r\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300\"\r\n onError={(e) => {\r\n e.currentTarget.src = \"/images/placeholder.png\";\r\n }}\r\n />\r\n </Link>\r\n </div>\r\n )}\r\n\r\n <div className=\"flex flex-col flex-1\">\r\n <CardHeader className=\"pt-6 pb-3\">\r\n {post.categories && post.categories.length > 0 && (\r\n <div className=\"flex flex-wrap gap-1 mb-2\">\r\n {post.categories.slice(0, 2).map((catId) => {\r\n const category = categoryMap.get(catId);\r\n if (!category) return null;\r\n return (\r\n <Badge\r\n key={catId}\r\n variant=\"secondary\"\r\n className=\"text-xs\"\r\n >\r\n <Link to={`/blog?categories=${category.slug}`}>\r\n {category.name}\r\n </Link>\r\n </Badge>\r\n );\r\n })}\r\n </div>\r\n )}\r\n\r\n <h3 className=\"text-lg font-semibold line-clamp-2 group-hover:text-primary transition-colors\">\r\n <Link to={`/blog/${post.slug}`}>{post.title}</Link>\r\n </h3>\r\n </CardHeader>\r\n\r\n <CardContent className=\"flex-1 pb-3\">\r\n {showExcerpt && post.excerpt && (\r\n <p className=\"text-muted-foreground text-sm line-clamp-3 mb-4\">\r\n {post.excerpt}\r\n </p>\r\n )}\r\n\r\n <div className=\"flex flex-wrap items-center gap-3 text-xs text-muted-foreground\">\r\n <div className=\"flex items-center gap-1\">\r\n <Calendar className=\"h-3 w-3\" />\r\n <span>{formatDate(post.published_at)}</span>\r\n </div>\r\n\r\n {post.read_time > 0 && (\r\n <div className=\"flex items-center gap-1\">\r\n <Clock className=\"h-3 w-3\" />\r\n <span>\r\n {post.read_time} {t(\"minRead\", \"min\")}\r\n </span>\r\n </div>\r\n )}\r\n </div>\r\n </CardContent>\r\n\r\n <CardFooter className=\"pt-0 pb-6 mt-auto\">\r\n <div className=\"flex items-center justify-between w-full\">\r\n <Button variant=\"ghost\" size=\"sm\" asChild>\r\n <Link to={`/blog/${post.slug}`}>\r\n {t(\"readMore\", \"Read More\")}\r\n <ArrowRight className=\"h-3 w-3 ml-1\" />\r\n </Link>\r\n </Button>\r\n\r\n {post.view_count > 0 && (\r\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground\">\r\n <Eye className=\"h-3 w-3\" />\r\n <span>{post.view_count.toLocaleString()}</span>\r\n </div>\r\n )}\r\n </div>\r\n </CardFooter>\r\n </div>\r\n </div>\r\n </CardWrapper>\r\n );\r\n}\r\n"
21
+ "content": "import { Link } from \"react-router\";\r\nimport { Calendar, Eye, Clock, ArrowRight } from \"lucide-react\";\r\nimport {\r\n Card,\r\n CardContent,\r\n CardFooter,\r\n CardHeader,\r\n} from \"@/components/ui/card\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport type { Post } from \"@/modules/blog-core/types\";\r\n\r\ninterface CardWrapperProps {\r\n children: React.ReactNode;\r\n className?: string;\r\n}\r\n\r\nfunction CardWrapper({ children, className = \"\" }: CardWrapperProps) {\r\n return (\r\n <Card\r\n className={`group overflow-hidden border-0 p-0 shadow-sm hover:shadow-lg transition-all duration-300 h-full ${className}`}\r\n >\r\n {children}\r\n </Card>\r\n );\r\n}\r\n\r\ninterface PostCardProps {\r\n post: Post;\r\n layout?: \"grid\" | \"list\";\r\n showExcerpt?: boolean;\r\n className?: string;\r\n}\r\n\r\nexport function PostCard({\r\n post,\r\n layout = \"grid\",\r\n showExcerpt = true,\r\n className = \"\",\r\n}: PostCardProps) {\r\n const { t } = useTranslation(\"post-card\");\r\n\r\n const formatDate = (dateString: string) => {\r\n return new Date(dateString).toLocaleDateString(\"en-US\", {\r\n year: \"numeric\",\r\n month: \"short\",\r\n day: \"numeric\",\r\n });\r\n };\r\n\r\n if (layout === \"list\") {\r\n return (\r\n <CardWrapper className={className}>\r\n <div className=\"flex flex-col md:flex-row\">\r\n {post.featured_image && (\r\n <div className=\"md:w-1/3 flex-shrink-0\">\r\n <Link to={`/blog/${post.slug}`}>\r\n <img\r\n src={post.featured_image}\r\n alt={post.title}\r\n className=\"w-full h-48 md:h-full object-cover\"\r\n onError={(e) => {\r\n e.currentTarget.src = \"/images/placeholder.png\";\r\n }}\r\n />\r\n </Link>\r\n </div>\r\n )}\r\n\r\n <div className=\"flex-1 flex flex-col\">\r\n <CardHeader className=\"pt-6 pb-3\">\r\n {post.categories && post.categories.length > 0 && (\r\n <div className=\"flex flex-wrap gap-1 mb-2\">\r\n {post.categories.slice(0, 2).map((category) => (\r\n <div key={category.slug} className=\"contents\" data-db-table=\"blog_categories\" data-db-id={category.slug}>\r\n <Badge\r\n variant=\"secondary\"\r\n className=\"text-xs\"\r\n >\r\n <Link to={`/blog?categories=${category.slug}`}>\r\n {category.name}\r\n </Link>\r\n </Badge>\r\n </div>\r\n ))}\r\n </div>\r\n )}\r\n\r\n <h3 className=\"text-xl font-semibold line-clamp-2 group-hover:text-primary transition-colors\">\r\n <Link to={`/blog/${post.slug}`}>{post.title}</Link>\r\n </h3>\r\n </CardHeader>\r\n\r\n <CardContent className=\"flex-1 pb-3\">\r\n {showExcerpt && post.excerpt && (\r\n <p className=\"text-muted-foreground text-sm line-clamp-3 mb-4\">\r\n {post.excerpt}\r\n </p>\r\n )}\r\n\r\n <div className=\"flex flex-wrap items-center gap-4 text-xs text-muted-foreground\">\r\n <div className=\"flex items-center gap-1\">\r\n <Calendar className=\"h-3 w-3\" />\r\n <span>{formatDate(post.published_at)}</span>\r\n </div>\r\n\r\n {post.read_time > 0 && (\r\n <div className=\"flex items-center gap-1\">\r\n <Clock className=\"h-3 w-3\" />\r\n <span>\r\n {post.read_time} {t(\"minRead\", \"min\")}\r\n </span>\r\n </div>\r\n )}\r\n\r\n {post.view_count > 0 && (\r\n <div className=\"flex items-center gap-1\">\r\n <Eye className=\"h-3 w-3\" />\r\n <span>{post.view_count.toLocaleString()}</span>\r\n </div>\r\n )}\r\n </div>\r\n </CardContent>\r\n\r\n <CardFooter className=\"pt-0 pb-6\">\r\n <Button variant=\"ghost\" size=\"sm\" asChild>\r\n <Link to={`/blog/${post.slug}`}>\r\n {t(\"readMore\", \"Read More\")}\r\n <ArrowRight className=\"h-3 w-3 ml-1\" />\r\n </Link>\r\n </Button>\r\n </CardFooter>\r\n </div>\r\n </div>\r\n </CardWrapper>\r\n );\r\n }\r\n\r\n return (\r\n <CardWrapper className={className}>\r\n <div className=\"flex flex-col h-full\">\r\n {post.featured_image && (\r\n <div className=\"aspect-video overflow-hidden\">\r\n <Link to={`/blog/${post.slug}`}>\r\n <img\r\n src={post.featured_image}\r\n alt={post.title}\r\n className=\"w-full h-full object-cover group-hover:scale-105 transition-transform duration-300\"\r\n onError={(e) => {\r\n e.currentTarget.src = \"/images/placeholder.png\";\r\n }}\r\n />\r\n </Link>\r\n </div>\r\n )}\r\n\r\n <div className=\"flex flex-col flex-1\">\r\n <CardHeader className=\"pt-6 pb-3\">\r\n {post.categories && post.categories.length > 0 && (\r\n <div className=\"flex flex-wrap gap-1 mb-2\">\r\n {post.categories.slice(0, 2).map((category) => (\r\n <Badge\r\n key={category.slug}\r\n variant=\"secondary\"\r\n className=\"text-xs\"\r\n >\r\n <Link to={`/blog?categories=${category.slug}`}>\r\n {category.name}\r\n </Link>\r\n </Badge>\r\n ))}\r\n </div>\r\n )}\r\n\r\n <h3 className=\"text-lg font-semibold line-clamp-2 group-hover:text-primary transition-colors\">\r\n <Link to={`/blog/${post.slug}`}>{post.title}</Link>\r\n </h3>\r\n </CardHeader>\r\n\r\n <CardContent className=\"flex-1 pb-3\">\r\n {showExcerpt && post.excerpt && (\r\n <p className=\"text-muted-foreground text-sm line-clamp-3 mb-4\">\r\n {post.excerpt}\r\n </p>\r\n )}\r\n\r\n <div className=\"flex flex-wrap items-center gap-3 text-xs text-muted-foreground\">\r\n <div className=\"flex items-center gap-1\">\r\n <Calendar className=\"h-3 w-3\" />\r\n <span>{formatDate(post.published_at)}</span>\r\n </div>\r\n\r\n {post.read_time > 0 && (\r\n <div className=\"flex items-center gap-1\">\r\n <Clock className=\"h-3 w-3\" />\r\n <span>\r\n {post.read_time} {t(\"minRead\", \"min\")}\r\n </span>\r\n </div>\r\n )}\r\n </div>\r\n </CardContent>\r\n\r\n <CardFooter className=\"pt-0 pb-6 mt-auto\">\r\n <div className=\"flex items-center justify-between w-full\">\r\n <Button variant=\"ghost\" size=\"sm\" asChild>\r\n <Link to={`/blog/${post.slug}`}>\r\n {t(\"readMore\", \"Read More\")}\r\n <ArrowRight className=\"h-3 w-3 ml-1\" />\r\n </Link>\r\n </Button>\r\n\r\n {post.view_count > 0 && (\r\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground\">\r\n <Eye className=\"h-3 w-3\" />\r\n <span>{post.view_count.toLocaleString()}</span>\r\n </div>\r\n )}\r\n </div>\r\n </CardFooter>\r\n </div>\r\n </div>\r\n </CardWrapper>\r\n );\r\n}\r\n"
22
22
  },
23
23
  {
24
24
  "path": "post-card/lang/en.json",
@@ -18,7 +18,7 @@
18
18
  "path": "post-detail-block/post-detail-block.tsx",
19
19
  "type": "registry:block",
20
20
  "target": "$modules$/post-detail-block/post-detail-block.tsx",
21
- "content": "import { useMemo } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport {\r\n Calendar,\r\n User,\r\n Eye,\r\n Clock,\r\n ArrowLeft,\r\n Share2,\r\n Heart,\r\n} from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useDbList } from \"@/db\";\r\nimport type { Post, BlogCategory } from \"@/modules/blog-core/types\";\r\n\r\ninterface PostDetailBlockProps {\r\n post: Post;\r\n onShare?: () => void;\r\n onAddToFavorites?: () => void;\r\n isFavorite?: boolean;\r\n}\r\n\r\nexport function PostDetailBlock({\r\n post,\r\n onShare,\r\n onAddToFavorites,\r\n isFavorite = false,\r\n}: PostDetailBlockProps) {\r\n const { t } = useTranslation(\"post-detail-block\");\r\n const { data: blogCategories = [] } = useDbList<BlogCategory>(\"blog_categories\");\r\n const categoryMap = useMemo(() => new Map(blogCategories.map(c => [c.id, c])), [blogCategories]);\r\n\r\n const formatDate = (dateString?: string) => {\r\n if (!dateString) return \"\";\r\n return new Date(dateString).toLocaleDateString(\"en-US\", {\r\n year: \"numeric\",\r\n month: \"long\",\r\n day: \"numeric\",\r\n });\r\n };\r\n\r\n return (\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"max-w-4xl mx-auto\">\r\n {/* Back Button */}\r\n <div className=\"mb-6\">\r\n <Button variant=\"ghost\" asChild>\r\n <Link to=\"/blog\">\r\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\r\n {t(\"backToBlog\", \"Back to Blog\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n\r\n {/* Post Header */}\r\n <div className=\"mb-8\">\r\n {/* Category Badge */}\r\n {categoryMap.get(post.categories?.[0] as number)?.name && (\r\n <Badge variant=\"secondary\" className=\"mb-4\">\r\n {categoryMap.get(post.categories?.[0] as number)?.name}\r\n </Badge>\r\n )}\r\n\r\n {/* Title */}\r\n <h1 className=\"text-4xl md:text-5xl font-bold mb-6\">{post.title}</h1>\r\n\r\n {/* Meta Information */}\r\n <div className=\"flex flex-wrap items-center gap-4 text-muted-foreground mb-6\">\r\n {post.author && (\r\n <div className=\"flex items-center gap-2\">\r\n {post.author_avatar ? (\r\n <Avatar className=\"w-8 h-8\">\r\n <AvatarImage src={post.author_avatar} alt={post.author} />\r\n <AvatarFallback>\r\n {post.author\r\n .split(\" \")\r\n .map((n) => n[0])\r\n .join(\"\")}\r\n </AvatarFallback>\r\n </Avatar>\r\n ) : (\r\n <User className=\"w-4 h-4\" />\r\n )}\r\n <span className=\"font-medium\">{post.author}</span>\r\n </div>\r\n )}\r\n\r\n {post.published_at && (\r\n <div className=\"flex items-center gap-1\">\r\n <Calendar className=\"w-4 h-4\" />\r\n <span>{formatDate(post.published_at)}</span>\r\n </div>\r\n )}\r\n\r\n {post.read_time && (\r\n <div className=\"flex items-center gap-1\">\r\n <Clock className=\"w-4 h-4\" />\r\n <span>\r\n {post.read_time} {t(\"minRead\", \"min read\")}\r\n </span>\r\n </div>\r\n )}\r\n\r\n {post.view_count !== undefined && (\r\n <div className=\"flex items-center gap-1\">\r\n <Eye className=\"w-4 h-4\" />\r\n <span>\r\n {post.view_count} {t(\"views\", \"views\")}\r\n </span>\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Actions */}\r\n <div className=\"flex gap-2\">\r\n {onShare && (\r\n <Button variant=\"outline\" size=\"sm\" onClick={onShare}>\r\n <Share2 className=\"w-4 h-4 mr-2\" />\r\n {t(\"share\", \"Share\")}\r\n </Button>\r\n )}\r\n {onAddToFavorites && (\r\n <Button\r\n variant=\"outline\"\r\n size=\"sm\"\r\n onClick={onAddToFavorites}\r\n className={isFavorite ? \"text-red-500\" : \"\"}\r\n >\r\n <Heart\r\n className={`w-4 h-4 mr-2 ${isFavorite ? \"fill-current\" : \"\"}`}\r\n />\r\n {isFavorite\r\n ? t(\"removeFromFavorites\", \"Remove\")\r\n : t(\"addToFavorites\", \"Add to Favorites\")}\r\n </Button>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Featured Image */}\r\n {post.featured_image && (\r\n <div className=\"mb-8\">\r\n <img\r\n src={post.featured_image}\r\n alt={post.title}\r\n className=\"w-full h-auto rounded-lg\"\r\n />\r\n </div>\r\n )}\r\n\r\n <Separator className=\"my-8\" />\r\n\r\n {/* Post Content */}\r\n <article className=\"prose prose-lg dark:prose-invert max-w-none\">\r\n <div\r\n dangerouslySetInnerHTML={{ __html: post.content }}\r\n className=\"leading-relaxed\"\r\n />\r\n </article>\r\n\r\n {/* Tags */}\r\n {post.tags && post.tags.length > 0 && (\r\n <div className=\"mt-8\">\r\n <Separator className=\"mb-4\" />\r\n <div className=\"flex items-center gap-2 flex-wrap\">\r\n <span className=\"text-sm font-medium text-muted-foreground\">\r\n {t(\"tags\", \"Tags\")}:\r\n </span>\r\n {post.tags.map((tag, index) => (\r\n <Badge key={index} variant=\"outline\">\r\n {tag}\r\n </Badge>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Author Box */}\r\n {post.author && (\r\n <div className=\"mt-12 p-6 bg-muted/30 rounded-lg\">\r\n <div className=\"flex items-start gap-4\">\r\n {post.author_avatar && (\r\n <Avatar className=\"w-16 h-16\">\r\n <AvatarImage src={post.author_avatar} alt={post.author} />\r\n <AvatarFallback>\r\n {post.author\r\n .split(\" \")\r\n .map((n) => n[0])\r\n .join(\"\")}\r\n </AvatarFallback>\r\n </Avatar>\r\n )}\r\n <div>\r\n <h3 className=\"font-semibold text-lg mb-1\">{post.author}</h3>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"authorDescription\", \"Author of this post\")}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
21
+ "content": "import { Link } from \"react-router\";\r\nimport {\r\n Calendar,\r\n User,\r\n Eye,\r\n Clock,\r\n ArrowLeft,\r\n Share2,\r\n Heart,\r\n} from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Badge } from \"@/components/ui/badge\";\r\nimport { Separator } from \"@/components/ui/separator\";\r\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport type { Post } from \"@/modules/blog-core/types\";\r\n\r\ninterface PostDetailBlockProps {\r\n post: Post;\r\n onShare?: () => void;\r\n onAddToFavorites?: () => void;\r\n isFavorite?: boolean;\r\n}\r\n\r\nexport function PostDetailBlock({\r\n post,\r\n onShare,\r\n onAddToFavorites,\r\n isFavorite = false,\r\n}: PostDetailBlockProps) {\r\n const { t } = useTranslation(\"post-detail-block\");\r\n\r\n const formatDate = (dateString?: string) => {\r\n if (!dateString) return \"\";\r\n return new Date(dateString).toLocaleDateString(\"en-US\", {\r\n year: \"numeric\",\r\n month: \"long\",\r\n day: \"numeric\",\r\n });\r\n };\r\n\r\n return (\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"max-w-4xl mx-auto\">\r\n {/* Back Button */}\r\n <div className=\"mb-6\">\r\n <Button variant=\"ghost\" asChild>\r\n <Link to=\"/blog\">\r\n <ArrowLeft className=\"w-4 h-4 mr-2\" />\r\n {t(\"backToBlog\", \"Back to Blog\")}\r\n </Link>\r\n </Button>\r\n </div>\r\n\r\n {/* Post Header */}\r\n <div className=\"mb-8\">\r\n {/* Category Badge */}\r\n {post.category && (\r\n <Badge variant=\"secondary\" className=\"mb-4\">\r\n {post.category}\r\n </Badge>\r\n )}\r\n\r\n {/* Title */}\r\n <h1 className=\"text-4xl md:text-5xl font-bold mb-6\">{post.title}</h1>\r\n\r\n {/* Meta Information */}\r\n <div className=\"flex flex-wrap items-center gap-4 text-muted-foreground mb-6\">\r\n {post.author && (\r\n <div className=\"flex items-center gap-2\">\r\n {post.author_avatar ? (\r\n <Avatar className=\"w-8 h-8\">\r\n <AvatarImage src={post.author_avatar} alt={post.author} />\r\n <AvatarFallback>\r\n {post.author\r\n .split(\" \")\r\n .map((n) => n[0])\r\n .join(\"\")}\r\n </AvatarFallback>\r\n </Avatar>\r\n ) : (\r\n <User className=\"w-4 h-4\" />\r\n )}\r\n <span className=\"font-medium\">{post.author}</span>\r\n </div>\r\n )}\r\n\r\n {post.published_at && (\r\n <div className=\"flex items-center gap-1\">\r\n <Calendar className=\"w-4 h-4\" />\r\n <span>{formatDate(post.published_at)}</span>\r\n </div>\r\n )}\r\n\r\n {post.read_time && (\r\n <div className=\"flex items-center gap-1\">\r\n <Clock className=\"w-4 h-4\" />\r\n <span>\r\n {post.read_time} {t(\"minRead\", \"min read\")}\r\n </span>\r\n </div>\r\n )}\r\n\r\n {post.view_count !== undefined && (\r\n <div className=\"flex items-center gap-1\">\r\n <Eye className=\"w-4 h-4\" />\r\n <span>\r\n {post.view_count} {t(\"views\", \"views\")}\r\n </span>\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Actions */}\r\n <div className=\"flex gap-2\">\r\n {onShare && (\r\n <Button variant=\"outline\" size=\"sm\" onClick={onShare}>\r\n <Share2 className=\"w-4 h-4 mr-2\" />\r\n {t(\"share\", \"Share\")}\r\n </Button>\r\n )}\r\n {onAddToFavorites && (\r\n <Button\r\n variant=\"outline\"\r\n size=\"sm\"\r\n onClick={onAddToFavorites}\r\n className={isFavorite ? \"text-red-500\" : \"\"}\r\n >\r\n <Heart\r\n className={`w-4 h-4 mr-2 ${isFavorite ? \"fill-current\" : \"\"}`}\r\n />\r\n {isFavorite\r\n ? t(\"removeFromFavorites\", \"Remove\")\r\n : t(\"addToFavorites\", \"Add to Favorites\")}\r\n </Button>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Featured Image */}\r\n {post.featured_image && (\r\n <div className=\"mb-8\">\r\n <img\r\n src={post.featured_image}\r\n alt={post.title}\r\n className=\"w-full h-auto rounded-lg\"\r\n />\r\n </div>\r\n )}\r\n\r\n <Separator className=\"my-8\" />\r\n\r\n {/* Post Content */}\r\n <article className=\"prose prose-lg dark:prose-invert max-w-none\">\r\n <div\r\n dangerouslySetInnerHTML={{ __html: post.content }}\r\n className=\"leading-relaxed\"\r\n />\r\n </article>\r\n\r\n {/* Tags */}\r\n {post.tags && post.tags.length > 0 && (\r\n <div className=\"mt-8\">\r\n <Separator className=\"mb-4\" />\r\n <div className=\"flex items-center gap-2 flex-wrap\">\r\n <span className=\"text-sm font-medium text-muted-foreground\">\r\n {t(\"tags\", \"Tags\")}:\r\n </span>\r\n {post.tags.map((tag, index) => (\r\n <Badge key={index} variant=\"outline\">\r\n {tag}\r\n </Badge>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Author Box */}\r\n {post.author && (\r\n <div className=\"mt-12 p-6 bg-muted/30 rounded-lg\">\r\n <div className=\"flex items-start gap-4\">\r\n {post.author_avatar && (\r\n <Avatar className=\"w-16 h-16\">\r\n <AvatarImage src={post.author_avatar} alt={post.author} />\r\n <AvatarFallback>\r\n {post.author\r\n .split(\" \")\r\n .map((n) => n[0])\r\n .join(\"\")}\r\n </AvatarFallback>\r\n </Avatar>\r\n )}\r\n <div>\r\n <h3 className=\"font-semibold text-lg mb-1\">{post.author}</h3>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"authorDescription\", \"Author of this post\")}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
22
22
  },
23
23
  {
24
24
  "path": "post-detail-block/lang/en.json",
@@ -2,12 +2,12 @@
2
2
  "name": "post-detail-page",
3
3
  "type": "registry:page",
4
4
  "title": "Post Detail Page",
5
- "description": "Blog post detail page that fetches post data by slug from URL params. Uses useDbGet from @/db and renders PostDetailBlock. Includes loading skeleton, error handling for not found posts, and automatic page title.",
5
+ "description": "Blog post detail page that fetches post data by slug from URL params. Uses useDbPostBySlug hook from blog-core and renders PostDetailBlock. Includes loading skeleton, error handling for not found posts, and automatic page title.",
6
6
  "registryDependencies": [
7
7
  "blog-core",
8
8
  "post-detail-block"
9
9
  ],
10
- "usage": "import { PostDetailPage } from '@/modules/post-detail-page';\n\n<Route path=\"/blog/:slug\" element={<PostDetailPage />} />\n\n• Uses useDbGet() from @/db to fetch post by slug\n• Fetches post by slug from URL params\n• Shows loading skeleton while fetching\n• Handles post not found state",
10
+ "usage": "import { PostDetailPage } from '@/modules/post-detail-page';\n\n<Route path=\"/blog/:slug\" element={<PostDetailPage />} />\n\n• Uses useDbPostBySlug() from blog-core\n• Fetches post by slug from URL params\n• Shows loading skeleton while fetching\n• Handles post not found state",
11
11
  "route": {
12
12
  "path": "/blog/:slug",
13
13
  "componentName": "PostDetailPage"
@@ -23,7 +23,7 @@
23
23
  "path": "post-detail-page/post-detail-page.tsx",
24
24
  "type": "registry:page",
25
25
  "target": "$modules$/post-detail-page/post-detail-page.tsx",
26
- "content": "import { useParams } from \"react-router\";\r\nimport { useDbGet } from \"@/db\";\r\nimport type { Post } from \"@/modules/blog-core/types\";\r\nimport { PostDetailBlock } from \"@/modules/post-detail-block\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\n\r\nexport function PostDetailPage() {\r\n const { t } = useTranslation(\"post-detail-page\");\r\n const { slug } = useParams<{ slug: string }>();\r\n const { data: post, isLoading: loading, error } = useDbGet<Post>(\"posts\", {\r\n where: { slug: slug || \"\" },\r\n enabled: !!slug,\r\n });\r\n\r\n usePageTitle({ title: post?.title || t(\"loading\", \"Loading...\") });\r\n\r\n if (loading) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"animate-pulse space-y-4\">\r\n <div className=\"h-8 bg-muted rounded w-1/3\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/4\"></div>\r\n <div className=\"h-64 bg-muted rounded\"></div>\r\n <div className=\"space-y-2\">\r\n <div className=\"h-4 bg-muted rounded\"></div>\r\n <div className=\"h-4 bg-muted rounded\"></div>\r\n <div className=\"h-4 bg-muted rounded w-3/4\"></div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n if (error || !post) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8 text-center\">\r\n <h1 className=\"text-2xl font-bold mb-4\">{t(\"notFound\", \"Post Not Found\")}</h1>\r\n <p className=\"text-muted-foreground\">{t(\"notFoundDescription\", \"The post you're looking for doesn't exist or has been removed.\")}</p>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <PostDetailBlock post={post} />\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default PostDetailPage;\r\n"
26
+ "content": "import { useParams } from \"react-router\";\r\nimport { useDbPostBySlug } from \"@/modules/blog-core\";\r\nimport { PostDetailBlock } from \"@/modules/post-detail-block\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\n\r\nexport function PostDetailPage() {\r\n const { t } = useTranslation(\"post-detail-page\");\r\n const { slug } = useParams<{ slug: string }>();\r\n const { post, loading, error } = useDbPostBySlug(slug || \"\");\r\n\r\n usePageTitle({ title: post?.title || t(\"loading\", \"Loading...\") });\r\n\r\n if (loading) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8\">\r\n <div className=\"animate-pulse space-y-4\">\r\n <div className=\"h-8 bg-muted rounded w-1/3\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/4\"></div>\r\n <div className=\"h-64 bg-muted rounded\"></div>\r\n <div className=\"space-y-2\">\r\n <div className=\"h-4 bg-muted rounded\"></div>\r\n <div className=\"h-4 bg-muted rounded\"></div>\r\n <div className=\"h-4 bg-muted rounded w-3/4\"></div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n if (error || !post) {\r\n return (\r\n <Layout>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 py-8 text-center\">\r\n <h1 className=\"text-2xl font-bold mb-4\">{t(\"notFound\", \"Post Not Found\")}</h1>\r\n <p className=\"text-muted-foreground\">{t(\"notFoundDescription\", \"The post you're looking for doesn't exist or has been removed.\")}</p>\r\n </div>\r\n </Layout>\r\n );\r\n }\r\n\r\n return (\r\n <Layout>\r\n <PostDetailBlock post={post} />\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default PostDetailPage;\r\n"
27
27
  },
28
28
  {
29
29
  "path": "post-detail-page/lang/en.json",