@promakeai/cli 0.4.5 → 0.4.7

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 (38) hide show
  1. package/dist/index.js +113 -113
  2. package/dist/registry/blog-core.json +1 -1
  3. package/dist/registry/blog-list-page.json +1 -1
  4. package/dist/registry/cart-drawer.json +1 -1
  5. package/dist/registry/cart-page.json +1 -1
  6. package/dist/registry/category-section.json +1 -1
  7. package/dist/registry/checkout-page.json +1 -1
  8. package/dist/registry/contact-page-centered.json +1 -1
  9. package/dist/registry/contact-page-map-overlay.json +1 -1
  10. package/dist/registry/contact-page-map-split.json +1 -1
  11. package/dist/registry/contact-page-split.json +1 -1
  12. package/dist/registry/contact-page.json +1 -1
  13. package/dist/registry/db.json +6 -6
  14. package/dist/registry/featured-products.json +1 -1
  15. package/dist/registry/forgot-password-page-split.json +1 -1
  16. package/dist/registry/forgot-password-page.json +1 -1
  17. package/dist/registry/header-ecommerce.json +1 -1
  18. package/dist/registry/index.json +104 -104
  19. package/dist/registry/login-page-split.json +1 -1
  20. package/dist/registry/login-page.json +1 -1
  21. package/dist/registry/newsletter-section.json +1 -1
  22. package/dist/registry/post-card.json +1 -1
  23. package/dist/registry/post-detail-block.json +1 -1
  24. package/dist/registry/product-card.json +1 -1
  25. package/dist/registry/product-detail-block.json +1 -1
  26. package/dist/registry/products-page.json +1 -1
  27. package/dist/registry/register-page-split.json +1 -1
  28. package/dist/registry/register-page.json +1 -1
  29. package/dist/registry/related-products-block.json +1 -1
  30. package/dist/registry/reset-password-page-split.json +1 -1
  31. package/package.json +1 -1
  32. package/template/README.md +73 -73
  33. package/template/package.json +93 -93
  34. package/template/scripts/init-db.ts +131 -131
  35. package/template/src/App.tsx +16 -16
  36. package/template/src/PasswordInput.tsx +61 -0
  37. package/template/src/components/FormField.tsx +11 -5
  38. package/dist/registry/auth.json +0 -70
@@ -30,7 +30,7 @@
30
30
  "path": "contact-page-map-overlay/contact-page-map-overlay.tsx",
31
31
  "type": "registry:component",
32
32
  "target": "$modules$/contact-page-map-overlay/contact-page-map-overlay.tsx",
33
- "content": "import React, { useState, useMemo, useEffect, useRef } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport {\r\n Mail,\r\n Phone,\r\n MapPin,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Send,\r\n ExternalLink,\r\n Upload,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { GoogleMap } from \"@/modules/google-map\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n};\r\n\r\nexport function ContactPageMapOverlay() {\r\n const { t } = useTranslation(\"contact-page-map-overlay\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n const fileInputRef = useRef<HTMLInputElement>(null);\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const maxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = maxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n // Auto-reset status after 5 seconds with proper cleanup\r\n useEffect(() => {\r\n if (submitStatus === \"idle\") return;\r\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n return () => clearTimeout(timer);\r\n }, [submitStatus]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: []\r\n });\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n // Default coordinates (can be customized via constants)\r\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\r\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\r\n\r\n return (\r\n <Layout>\r\n <div className=\"relative min-h-[calc(100vh-4rem)]\">\r\n {/* Full-screen Map Background */}\r\n <div className=\"absolute inset-0\">\r\n <GoogleMap\r\n latitude={mapLatitude}\r\n longitude={mapLongitude}\r\n zoom={14}\r\n height=\"100%\"\r\n className=\"rounded-none border-0 h-full\"\r\n title={t(\"mapTitle\")}\r\n />\r\n {/* Dark overlay for better readability */}\r\n <div className=\"absolute inset-0 bg-black/30 pointer-events-none\" />\r\n </div>\r\n\r\n {/* Content Overlay */}\r\n <div className=\"relative z-10 min-h-[calc(100vh-4rem)] flex items-center py-12 px-4\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto\">\r\n <div className=\"grid lg:grid-cols-2 gap-8 items-stretch\">\r\n {/* Form Card - Glassmorphism */}\r\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\r\n <CardHeader>\r\n <CardTitle className=\"text-2xl lg:text-3xl\">\r\n {t(\"title\")}\r\n </CardTitle>\r\n <p className=\"text-muted-foreground mt-2\">\r\n {t(\"description\")}\r\n </p>\r\n </CardHeader>\r\n <CardContent>\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\r\n <div>\r\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\r\n {t(\"fullName\")} *\r\n </Label>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1.5 bg-background/50\"\r\n />\r\n </div>\r\n <div>\r\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\r\n {t(\"emailAddress\")} *\r\n </Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1.5 bg-background/50\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\r\n {t(\"message\")} *\r\n </Label>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={4}\r\n className=\"mt-1.5 resize-none bg-background/50\"\r\n />\r\n </div>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n <>\r\n <Send className=\"w-4 h-4 mr-2\" />\r\n {t(\"submit\")}\r\n </>\r\n )}\r\n </Button>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Contact Info Card - Glassmorphism */}\r\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\r\n <CardHeader>\r\n <CardTitle>{t(\"contactInfo\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-5\">\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <Mail className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"email\")}</p>\r\n <a\r\n href={`mailto:${constants.email}`}\r\n className=\"font-medium hover:text-primary transition-colors\"\r\n >\r\n {constants.email}\r\n </a>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <Phone className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"phone\")}</p>\r\n <a\r\n href={`tel:${constants.phone}`}\r\n className=\"font-medium hover:text-primary transition-colors\"\r\n >\r\n {constants.phone}\r\n </a>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"address\")}</p>\r\n <p className=\"font-medium\">\r\n {constants.address.line1}\r\n <br />\r\n {constants.address.city}, {constants.address.state} {constants.address.postalCode}\r\n </p>\r\n </div>\r\n </div>\r\n\r\n {/* Open in Maps Link */}\r\n <a\r\n href={`https://www.google.com/maps?q=${mapLatitude},${mapLongitude}`}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"inline-flex items-center gap-2 text-sm text-primary hover:underline mt-2\"\r\n >\r\n <ExternalLink className=\"w-4 h-4\" />\r\n {t(\"openInMaps\")}\r\n </a>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"pt-4 border-t\">\r\n <p className=\"text-sm text-muted-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-4 w-4\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageMapOverlay;\r\n"
33
+ "content": "import React, { useState, useMemo, useEffect } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport {\r\n Mail,\r\n Phone,\r\n MapPin,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Send,\r\n ExternalLink,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { GoogleMap } from \"@/modules/google-map\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n};\r\n\r\nexport function ContactPageMapOverlay() {\r\n const { t } = useTranslation(\"contact-page-map-overlay\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const maxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = maxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n // Auto-reset status after 5 seconds with proper cleanup\r\n useEffect(() => {\r\n if (submitStatus === \"idle\") return;\r\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n return () => clearTimeout(timer);\r\n }, [submitStatus]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n message: \"\",\r\n attachments: []\r\n });\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n // Default coordinates (can be customized via constants)\r\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\r\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\r\n\r\n return (\r\n <Layout>\r\n <div className=\"relative min-h-[calc(100vh-4rem)]\">\r\n {/* Full-screen Map Background */}\r\n <div className=\"absolute inset-0\">\r\n <GoogleMap\r\n latitude={mapLatitude}\r\n longitude={mapLongitude}\r\n zoom={14}\r\n height=\"100%\"\r\n className=\"rounded-none border-0 h-full\"\r\n title={t(\"mapTitle\")}\r\n />\r\n {/* Dark overlay for better readability */}\r\n <div className=\"absolute inset-0 bg-black/30 pointer-events-none\" />\r\n </div>\r\n\r\n {/* Content Overlay */}\r\n <div className=\"relative z-10 min-h-[calc(100vh-4rem)] flex items-center py-12 px-4\">\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto\">\r\n <div className=\"grid lg:grid-cols-2 gap-8 items-stretch\">\r\n {/* Form Card - Glassmorphism */}\r\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\r\n <CardHeader>\r\n <CardTitle className=\"text-2xl lg:text-3xl\">\r\n {t(\"title\")}\r\n </CardTitle>\r\n <p className=\"text-muted-foreground mt-2\">\r\n {t(\"description\")}\r\n </p>\r\n </CardHeader>\r\n <CardContent>\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\r\n <FormField label={t(\"fullName\")} htmlFor=\"name\" required>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1.5 bg-background/50\"\r\n />\r\n </FormField>\r\n <FormField label={t(\"emailAddress\")} htmlFor=\"email\" required>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1.5 bg-background/50\"\r\n />\r\n </FormField>\r\n </div>\r\n\r\n <FormField label={t(\"message\")} htmlFor=\"message\" required>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={4}\r\n className=\"mt-1.5 resize-none bg-background/50\"\r\n />\r\n </FormField>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n <>\r\n <Send className=\"w-4 h-4 mr-2\" />\r\n {t(\"submit\")}\r\n </>\r\n )}\r\n </Button>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Contact Info Card - Glassmorphism */}\r\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\r\n <CardHeader>\r\n <CardTitle>{t(\"contactInfo\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-5\">\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <Mail className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"email\")}</p>\r\n <a\r\n href={`mailto:${constants.email}`}\r\n className=\"font-medium hover:text-primary transition-colors\"\r\n >\r\n {constants.email}\r\n </a>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <Phone className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"phone\")}</p>\r\n <a\r\n href={`tel:${constants.phone}`}\r\n className=\"font-medium hover:text-primary transition-colors\"\r\n >\r\n {constants.phone}\r\n </a>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div>\r\n <p className=\"text-sm text-muted-foreground\">{t(\"address\")}</p>\r\n <p className=\"font-medium\">\r\n {constants.address.line1}\r\n <br />\r\n {constants.address.city}, {constants.address.state} {constants.address.postalCode}\r\n </p>\r\n </div>\r\n </div>\r\n\r\n {/* Open in Maps Link */}\r\n <a\r\n href={`https://www.google.com/maps?q=${mapLatitude},${mapLongitude}`}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"inline-flex items-center gap-2 text-sm text-primary hover:underline mt-2\"\r\n >\r\n <ExternalLink className=\"w-4 h-4\" />\r\n {t(\"openInMaps\")}\r\n </a>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"pt-4 border-t\">\r\n <p className=\"text-sm text-muted-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-4 w-4\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageMapOverlay;\r\n"
34
34
  },
35
35
  {
36
36
  "path": "contact-page-map-overlay/lang/en.json",
@@ -30,7 +30,7 @@
30
30
  "path": "contact-page-map-split/contact-page-map-split.tsx",
31
31
  "type": "registry:component",
32
32
  "target": "$modules$/contact-page-map-split/contact-page-map-split.tsx",
33
- "content": "import React, { useState, useMemo, useEffect, useRef } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport {\r\n Mail,\r\n Phone,\r\n MapPin,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Send,\r\n Upload,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { GoogleMap } from \"@/modules/google-map\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n};\r\n\r\nexport function ContactPageMapSplit() {\r\n const { t } = useTranslation(\"contact-page-map-split\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n const fileInputRef = useRef<HTMLInputElement>(null);\r\n\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const maxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = maxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n // Auto-reset status after 5 seconds with proper cleanup\r\n useEffect(() => {\r\n if (submitStatus === \"idle\") return;\r\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n return () => clearTimeout(timer);\r\n }, [submitStatus]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"phone\", required: false },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: []\r\n });\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n // Default coordinates (can be customized via constants)\r\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\r\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\r\n\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen flex flex-col lg:flex-row\">\r\n {/* Left Side - Form & Info */}\r\n <div className=\"w-full lg:w-1/2 bg-background py-12 lg:py-16 px-6 lg:px-12 flex flex-col justify-center\">\r\n <div className=\"max-w-lg mx-auto w-full\">\r\n {/* Header */}\r\n <div className=\"mb-10\">\r\n <h1 className=\"text-3xl lg:text-4xl font-bold text-foreground mb-3\">\r\n {t(\"title\")}\r\n </h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"description\")}\r\n </p>\r\n </div>\r\n\r\n {/* Contact Info Cards */}\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4 mb-10\">\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <Mail className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"email\")}</p>\r\n <p className=\"text-sm font-medium truncate\">{constants.email}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <Phone className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"phone\")}</p>\r\n <p className=\"text-sm font-medium truncate\">{constants.phone}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50 sm:col-span-2\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"address\")}</p>\r\n <p className=\"text-sm font-medium\">\r\n {constants.address.line1}, {constants.address.city}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Contact Form */}\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\r\n <div>\r\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\r\n {t(\"fullName\")} *\r\n </Label>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1.5\"\r\n />\r\n </div>\r\n <div>\r\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\r\n {t(\"emailAddress\")} *\r\n </Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1.5\"\r\n />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"phone\" className=\"text-sm font-medium\">\r\n {t(\"phoneNumber\")}\r\n </Label>\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleChange}\r\n placeholder={t(\"phonePlaceholder\")}\r\n className=\"mt-1.5\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\r\n {t(\"message\")} *\r\n </Label>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={4}\r\n className=\"mt-1.5 resize-none\"\r\n />\r\n </div>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n <>\r\n <Send className=\"w-4 h-4 mr-2\" />\r\n {t(\"submit\")}\r\n </>\r\n )}\r\n </Button>\r\n </form>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"mt-10 pt-6 border-t\">\r\n <p className=\"text-sm text-muted-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-4 w-4\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Right Side - Map */}\r\n <div className=\"w-full lg:w-1/2 h-[400px] lg:h-[calc(100vh-4rem)] relative\">\r\n <GoogleMap\r\n latitude={mapLatitude}\r\n longitude={mapLongitude}\r\n zoom={14}\r\n height=\"100%\"\r\n className=\"rounded-none border-0 h-full\"\r\n title={t(\"mapTitle\")}\r\n />\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageMapSplit;\r\n"
33
+ "content": "import React, { useState, useMemo, useEffect } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { \r\n Mail,\r\n Phone,\r\n MapPin,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Send,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { GoogleMap } from \"@/modules/google-map\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n};\r\n\r\nexport function ContactPageMapSplit() {\r\n const { t } = useTranslation(\"contact-page-map-split\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n\r\n\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const maxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = maxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n // Auto-reset status after 5 seconds with proper cleanup\r\n useEffect(() => {\r\n if (submitStatus === \"idle\") return;\r\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n return () => clearTimeout(timer);\r\n }, [submitStatus]);\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"phone\", required: false },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: []\r\n });\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (\r\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n // Default coordinates (can be customized via constants)\r\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\r\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\r\n\r\n return (\r\n <Layout>\r\n <div className=\"min-h-screen flex flex-col lg:flex-row\">\r\n {/* Left Side - Form & Info */}\r\n <div className=\"w-full lg:w-1/2 bg-background py-12 lg:py-16 px-6 lg:px-12 flex flex-col justify-center\">\r\n <div className=\"max-w-lg mx-auto w-full\">\r\n {/* Header */}\r\n <div className=\"mb-10\">\r\n <h1 className=\"text-3xl lg:text-4xl font-bold text-foreground mb-3\">\r\n {t(\"title\")}\r\n </h1>\r\n <p className=\"text-muted-foreground\">\r\n {t(\"description\")}\r\n </p>\r\n </div>\r\n\r\n {/* Contact Info Cards */}\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4 mb-10\">\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <Mail className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"email\")}</p>\r\n <p className=\"text-sm font-medium truncate\">{constants.email}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\r\n <Phone className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"phone\")}</p>\r\n <p className=\"text-sm font-medium truncate\">{constants.phone}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50 sm:col-span-2\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"w-5 h-5 text-primary\" />\r\n </div>\r\n <div className=\"min-w-0\">\r\n <p className=\"text-xs text-muted-foreground\">{t(\"address\")}</p>\r\n <p className=\"text-sm font-medium\">\r\n {constants.address.line1}, {constants.address.city}\r\n </p>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Contact Form */}\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\r\n <FormField label={t(\"fullName\")} htmlFor=\"name\" required>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1.5\"\r\n />\r\n </FormField>\r\n <FormField label={t(\"emailAddress\")} htmlFor=\"email\" required>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1.5\"\r\n />\r\n </FormField>\r\n </div>\r\n\r\n <FormField label={t(\"phoneNumber\")} htmlFor=\"phone\">\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleChange}\r\n placeholder={t(\"phonePlaceholder\")}\r\n className=\"mt-1.5\"\r\n />\r\n </FormField>\r\n <FormField label={t(\"message\")} htmlFor=\"message\" required>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={4}\r\n className=\"mt-1.5 resize-none\"\r\n />\r\n </FormField>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\r\n </div>\r\n )}\r\n\r\n <Button\r\n type=\"submit\"\r\n size=\"lg\"\r\n className=\"w-full\"\r\n disabled={isSubmitting}\r\n >\r\n {isSubmitting ? (\r\n <>\r\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n <>\r\n <Send className=\"w-4 h-4 mr-2\" />\r\n {t(\"submit\")}\r\n </>\r\n )}\r\n </Button>\r\n </form>\r\n\r\n {/* Social Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"mt-10 pt-6 border-t\">\r\n <p className=\"text-sm text-muted-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-full border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-4 w-4\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n\r\n {/* Right Side - Map */}\r\n <div className=\"w-full lg:w-1/2 h-[400px] lg:h-[calc(100vh-4rem)] relative\">\r\n <GoogleMap\r\n latitude={mapLatitude}\r\n longitude={mapLongitude}\r\n zoom={14}\r\n height=\"100%\"\r\n className=\"rounded-none border-0 h-full\"\r\n title={t(\"mapTitle\")}\r\n />\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageMapSplit;\r\n"
34
34
  },
35
35
  {
36
36
  "path": "contact-page-map-split/lang/en.json",
@@ -24,7 +24,7 @@
24
24
  "path": "contact-page-split/contact-page-split.tsx",
25
25
  "type": "registry:component",
26
26
  "target": "$modules$/contact-page-split/contact-page-split.tsx",
27
- "content": "import React, { useRef, useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Mail, Phone, MapPin, Clock } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\n\r\ninterface ContactPageSplitProps {\r\n className?: string;\r\n}\r\n\r\nexport function ContactPageSplit({ className }: ContactPageSplitProps) {\r\n const { t } = useTranslation(\"contact-page-split\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Contact Us\") });\r\n const apiService = useApiService();\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\"idle\" | \"success\" | \"error\">(\"idle\");\r\n\r\n\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const fileMaxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = fileMaxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: fileMaxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n\r\n try {\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"phone\", required: false },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n constants.site.defaultLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({ name: \"\", email: \"\", phone: \"\", message: \"\", attachments: [] });\r\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n } catch {\r\n setSubmitStatus(\"error\");\r\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {\r\n setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));\r\n };\r\n\r\n return (\r\n <Layout>\r\n <div className={cn(\"min-h-[calc(100vh-200px)] py-8 md:py-12\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"grid lg:grid-cols-2 h-full rounded-xl overflow-hidden shadow-lg\">\r\n {/* Left Side - Info & Image */}\r\n <div className=\"relative bg-primary text-primary-foreground p-8 lg:p-12 flex flex-col justify-center\">\r\n {/* Background Pattern */}\r\n <div className=\"absolute inset-0 bg-[linear-gradient(to_right,rgba(255,255,255,0.1)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.1)_1px,transparent_1px)] bg-[size:4rem_4rem]\" />\r\n\r\n <div className=\"relative z-10 max-w-lg\">\r\n <h1 className=\"text-3xl lg:text-4xl font-bold mb-4\">\r\n {t(\"title\", \"Let's Start a Conversation\")}\r\n </h1>\r\n <p className=\"text-primary-foreground/80 mb-8\">\r\n {t(\"subtitle\", \"Have a project in mind? We'd love to hear about it. Get in touch and let's create something amazing together.\")}\r\n </p>\r\n\r\n <div className=\"space-y-6\">\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary-foreground/10 flex items-center justify-center flex-shrink-0\">\r\n <Mail className=\"h-5 w-5\" />\r\n </div>\r\n <div>\r\n <p className=\"font-semibold\">{t(\"emailLabel\", \"Email\")}</p>\r\n <p className=\"text-primary-foreground/70\">{constants.email || \"hello@example.com\"}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary-foreground/10 flex items-center justify-center flex-shrink-0\">\r\n <Phone className=\"h-5 w-5\" />\r\n </div>\r\n <div>\r\n <p className=\"font-semibold\">{t(\"phoneLabel\", \"Phone\")}</p>\r\n <p className=\"text-primary-foreground/70\">{constants.phone || \"+1 234 567 890\"}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary-foreground/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"h-5 w-5\" />\r\n </div>\r\n <div>\r\n <p className=\"font-semibold\">{t(\"addressLabel\", \"Address\")}</p>\r\n <p className=\"text-primary-foreground/70\">\r\n {constants.address?.line1 || \"123 Main Street\"}<br />\r\n {constants.address?.city || \"New York\"}, {constants.address?.country || \"USA\"}\r\n </p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary-foreground/10 flex items-center justify-center flex-shrink-0\">\r\n <Clock className=\"h-5 w-5\" />\r\n </div>\r\n <div>\r\n <p className=\"font-semibold\">{t(\"hoursLabel\", \"Business Hours\")}</p>\r\n <p className=\"text-primary-foreground/70\">{t(\"hours\", \"Mon - Fri: 9:00 AM - 6:00 PM\")}</p>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Right Side - Form */}\r\n <div className=\"p-8 lg:p-12 flex items-center justify-center bg-background\">\r\n <div className=\"w-full max-w-md\">\r\n <h2 className=\"text-2xl font-bold mb-2\">{t(\"formTitle\", \"Send us a message\")}</h2>\r\n <p className=\"text-muted-foreground mb-8\">\r\n {t(\"formSubtitle\", \"Fill out the form below and we'll get back to you as soon as possible.\")}\r\n </p>\r\n\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <div>\r\n <Label htmlFor=\"name\">{t(\"nameLabel\", \"Full Name\")} *</Label>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"namePlaceholder\", \"John Doe\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"email\">{t(\"emailInputLabel\", \"Email\")} *</Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\", \"john@example.com\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"phone\">{t(\"phoneInputLabel\", \"Phone\")}</Label>\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleChange}\r\n placeholder={t(\"phonePlaceholder\", \"+1 234 567 890\")}\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n\r\n <div>\r\n <Label htmlFor=\"message\">{t(\"messageLabel\", \"Message\")} *</Label>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\", \"Tell us about your project...\")}\r\n required\r\n rows={5}\r\n className=\"mt-1 resize-none\"\r\n />\r\n </div>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-4 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm font-medium\">\r\n {t(\"success\", \"Message sent! We'll be in touch soon.\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-4 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm font-medium\">\r\n {t(\"error\", \"Something went wrong. Please try again.\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button type=\"submit\" size=\"lg\" className=\"w-full\" disabled={isSubmitting}>\r\n {isSubmitting ? t(\"sending\", \"Sending...\") : t(\"submit\", \"Send Message\")}\r\n </Button>\r\n </form>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageSplit;\r\n"
27
+ "content": "import React, { useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Mail, Phone, MapPin, Clock } from \"lucide-react\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\ninterface ContactPageSplitProps {\r\n className?: string;\r\n}\r\n\r\nexport function ContactPageSplit({ className }: ContactPageSplitProps) {\r\n const { t } = useTranslation(\"contact-page-split\");\r\n usePageTitle({ title: t(\"pageTitle\", \"Contact Us\") });\r\n const apiService = useApiService();\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n message: \"\",\r\n attachments: [] as File[]\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\"idle\" | \"success\" | \"error\">(\"idle\");\r\n\r\n\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const fileMaxFiles = constants.file?.maxFiles || 5;\r\n\r\n const remainingSlots = fileMaxFiles - formData.attachments.length;\r\n\r\n // If the limit is exceeded, alert and do not add any files\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: fileMaxFiles }));\r\n e.target.value = ''; // Clear the input\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n\r\n try {\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"phone\", required: false },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n constants.site.defaultLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({ name: \"\", email: \"\", phone: \"\", message: \"\", attachments: [] });\r\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n } catch {\r\n setSubmitStatus(\"error\");\r\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n\r\n const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {\r\n setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));\r\n };\r\n\r\n return (\r\n <Layout>\r\n <div className={cn(\"min-h-[calc(100vh-200px)] py-8 md:py-12\", className)}>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\r\n <div className=\"grid lg:grid-cols-2 h-full rounded-xl overflow-hidden shadow-lg\">\r\n {/* Left Side - Info & Image */}\r\n <div className=\"relative bg-primary text-primary-foreground p-8 lg:p-12 flex flex-col justify-center\">\r\n {/* Background Pattern */}\r\n <div className=\"absolute inset-0 bg-[linear-gradient(to_right,rgba(255,255,255,0.1)_1px,transparent_1px),linear-gradient(to_bottom,rgba(255,255,255,0.1)_1px,transparent_1px)] bg-[size:4rem_4rem]\" />\r\n\r\n <div className=\"relative z-10 max-w-lg\">\r\n <h1 className=\"text-3xl lg:text-4xl font-bold mb-4\">\r\n {t(\"title\", \"Let's Start a Conversation\")}\r\n </h1>\r\n <p className=\"text-primary-foreground/80 mb-8\">\r\n {t(\"subtitle\", \"Have a project in mind? We'd love to hear about it. Get in touch and let's create something amazing together.\")}\r\n </p>\r\n\r\n <div className=\"space-y-6\">\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary-foreground/10 flex items-center justify-center flex-shrink-0\">\r\n <Mail className=\"h-5 w-5\" />\r\n </div>\r\n <div>\r\n <p className=\"font-semibold\">{t(\"emailLabel\", \"Email\")}</p>\r\n <p className=\"text-primary-foreground/70\">{constants.email || \"hello@example.com\"}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary-foreground/10 flex items-center justify-center flex-shrink-0\">\r\n <Phone className=\"h-5 w-5\" />\r\n </div>\r\n <div>\r\n <p className=\"font-semibold\">{t(\"phoneLabel\", \"Phone\")}</p>\r\n <p className=\"text-primary-foreground/70\">{constants.phone || \"+1 234 567 890\"}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary-foreground/10 flex items-center justify-center flex-shrink-0\">\r\n <MapPin className=\"h-5 w-5\" />\r\n </div>\r\n <div>\r\n <p className=\"font-semibold\">{t(\"addressLabel\", \"Address\")}</p>\r\n <p className=\"text-primary-foreground/70\">\r\n {constants.address?.line1 || \"123 Main Street\"}<br />\r\n {constants.address?.city || \"New York\"}, {constants.address?.country || \"USA\"}\r\n </p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-4\">\r\n <div className=\"w-10 h-10 rounded-full bg-primary-foreground/10 flex items-center justify-center flex-shrink-0\">\r\n <Clock className=\"h-5 w-5\" />\r\n </div>\r\n <div>\r\n <p className=\"font-semibold\">{t(\"hoursLabel\", \"Business Hours\")}</p>\r\n <p className=\"text-primary-foreground/70\">{t(\"hours\", \"Mon - Fri: 9:00 AM - 6:00 PM\")}</p>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Right Side - Form */}\r\n <div className=\"p-8 lg:p-12 flex items-center justify-center bg-background\">\r\n <div className=\"w-full max-w-md\">\r\n <h2 className=\"text-2xl font-bold mb-2\">{t(\"formTitle\", \"Send us a message\")}</h2>\r\n <p className=\"text-muted-foreground mb-8\">\r\n {t(\"formSubtitle\", \"Fill out the form below and we'll get back to you as soon as possible.\")}\r\n </p>\r\n\r\n <form onSubmit={handleSubmit} className=\"space-y-5\">\r\n <FormField label={t(\"nameLabel\", \"Full Name\")} htmlFor=\"name\" required>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"namePlaceholder\", \"John Doe\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </FormField>\r\n\r\n <FormField label={t(\"emailInputLabel\", \"Email\")} htmlFor=\"email\" required>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\", \"john@example.com\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </FormField>\r\n\r\n <FormField label={t(\"phoneInputLabel\", \"Phone\")} htmlFor=\"phone\">\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleChange}\r\n placeholder={t(\"phonePlaceholder\", \"+1 234 567 890\")}\r\n className=\"mt-1\"\r\n />\r\n </FormField>\r\n <FormField label={t(\"messageLabel\", \"Message\")} htmlFor=\"message\" required>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\", \"Tell us about your project...\")}\r\n required\r\n rows={5}\r\n className=\"mt-1 resize-none\"\r\n />\r\n </FormField>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-4 bg-green-500/10 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm font-medium\">\r\n {t(\"success\", \"Message sent! We'll be in touch soon.\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-4 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm font-medium\">\r\n {t(\"error\", \"Something went wrong. Please try again.\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n <Button type=\"submit\" size=\"lg\" className=\"w-full\" disabled={isSubmitting}>\r\n {isSubmitting ? t(\"sending\", \"Sending...\") : t(\"submit\", \"Send Message\")}\r\n </Button>\r\n </form>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPageSplit;\r\n"
28
28
  },
29
29
  {
30
30
  "path": "contact-page-split/lang/en.json",
@@ -22,7 +22,7 @@
22
22
  "path": "contact-page/contact-page.tsx",
23
23
  "type": "registry:page",
24
24
  "target": "$modules$/contact-page/contact-page.tsx",
25
- "content": "import React, { useState, useMemo, useRef } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport {\r\n Mail,\r\n Phone,\r\n MapPin,\r\n MessageSquare,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n Upload,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FadeIn, SlideInLeft, SlideInRight } from \"@/modules/animations\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n};\r\n\r\nexport function ContactPage() {\r\n const { t } = useTranslation(\"contact-page\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n subject: \"\",\r\n message: \"\",\r\n attachments: [] as File[],\r\n });\r\n const fileInputRef = useRef<HTMLInputElement>(null);\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n const fileAcceptTypes = constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\";\r\n const fileMaxFiles = constants.file?.maxFiles || 5;\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n setIsSubmitting(false);\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"phone\", required: false },\r\n { name: \"subject\", required: false },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n subject: \"\",\r\n message: \"\",\r\n attachments: [],\r\n });\r\n\r\n setTimeout(() => {\r\n setSubmitStatus(\"idle\");\r\n }, 5000);\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n\r\n setTimeout(() => {\r\n setSubmitStatus(\"idle\");\r\n }, 5000);\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const remainingSlots = fileMaxFiles - formData.attachments.length;\r\n\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: fileMaxFiles }));\r\n e.target.value = ''; // Clear the input value\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n const handleChange = (\r\n e: React.ChangeEvent<\r\n HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement\r\n >\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n 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 max-w-6xl\">\r\n {/* Hero Section */}\r\n <FadeIn className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\r\n {t(\"title\")}\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-3xl mx-auto\">\r\n {t(\"description\")}\r\n </p>\r\n </FadeIn>\r\n\r\n <div className=\"grid lg:grid-cols-3 gap-8\">\r\n {/* Contact Information */}\r\n <SlideInLeft className=\"lg:col-span-1 space-y-6\">\r\n {/* Company Info */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <MessageSquare className=\"w-5 h-5 text-primary\" />\r\n {t(\"getInTouch\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"flex items-start gap-3\">\r\n <Mail className=\"w-5 h-5 text-primary mt-1\" />\r\n <div>\r\n <p className=\"font-medium text-foreground\">\r\n {t(\"email\")}\r\n </p>\r\n <p className=\"text-muted-foreground\">{constants.email}</p>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"emailResponse\")}\r\n </p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-3\">\r\n <Phone className=\"w-5 h-5 text-primary mt-1\" />\r\n <div>\r\n <p className=\"font-medium text-foreground\">\r\n {t(\"phone\")}\r\n </p>\r\n <p className=\"text-muted-foreground\">{constants.phone}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-3\">\r\n <MapPin className=\"w-5 h-5 text-primary mt-1\" />\r\n <div>\r\n <p className=\"font-medium text-foreground\">\r\n {t(\"address\")}\r\n </p>\r\n <div className=\"text-muted-foreground\">\r\n <p>{constants.address.line1}</p>\r\n {constants.address.line2 && (\r\n <p>{constants.address.line2}</p>\r\n )}\r\n <p>\r\n {constants.address.city}, {constants.address.state}{\" \"}\r\n {constants.address.postalCode}\r\n </p>\r\n {constants.address.country && (\r\n <p>{constants.address.country}</p>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Social Media Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"pt-4 border-t\">\r\n <p className=\"font-medium text-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-md border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-5 w-5\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Support Info */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"needSupport\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <p className=\"text-muted-foreground mb-4\">\r\n {t(\"supportDescription\")}\r\n </p>\r\n <div className=\"space-y-2\">\r\n <p className=\"font-medium text-foreground\">\r\n {t(\"supportEmail\")}\r\n </p>\r\n <p className=\"text-muted-foreground\">{constants.email}</p>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </SlideInLeft>\r\n\r\n {/* Contact Form */}\r\n <SlideInRight className=\"lg:col-span-2\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"sendMessage\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <form onSubmit={handleSubmit} className=\"space-y-6\">\r\n {/* Full Name */}\r\n <div>\r\n <Label htmlFor=\"name\">{t(\"fullName\")} *</Label>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n\r\n {/* Email */}\r\n <div>\r\n <Label htmlFor=\"email\">{t(\"emailAddress\")} *</Label>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n\r\n {/* Phone */}\r\n <div>\r\n <Label htmlFor=\"phone\">{t(\"phoneNumber\")}</Label>\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleChange}\r\n placeholder={t(\"phonePlaceholder\")}\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n\r\n {/* Subject */}\r\n <div>\r\n <Label htmlFor=\"subject\">{t(\"subject\")}</Label>\r\n <Input\r\n id=\"subject\"\r\n name=\"subject\"\r\n type=\"text\"\r\n value={formData.subject}\r\n onChange={handleChange}\r\n placeholder={t(\"subjectPlaceholder\")}\r\n className=\"mt-1\"\r\n />\r\n </div>\r\n\r\n {/* Message */}\r\n <div>\r\n <Label htmlFor=\"message\">{t(\"message\")} *</Label>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={6}\r\n className=\"mt-1 resize-none\"\r\n />\r\n </div>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-4 bg-green-500/10 dark:bg-green-500/20 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm font-medium\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-4 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm font-medium\">\r\n {t(\"error\")}\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-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n t(\"submit\")\r\n )}\r\n </Button>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n </SlideInRight>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPage;\r\n"
25
+ "content": "import React, { useState, useMemo } from \"react\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { Layout } from \"@/components/Layout\";\r\nimport { useApiService } from \"@/lib/api\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\r\nimport {\r\n Mail,\r\n Phone,\r\n MapPin,\r\n MessageSquare,\r\n Facebook,\r\n Twitter,\r\n Instagram,\r\n Linkedin,\r\n} from \"lucide-react\";\r\nimport constants from \"@/constants/constants.json\";\r\nimport { FadeIn, SlideInLeft, SlideInRight } from \"@/modules/animations\";\r\nimport { FormFileInput } from \"@/components/FormFileInput\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\nconst socialIcons: Record<string, React.ElementType> = {\r\n facebook: Facebook,\r\n twitter: Twitter,\r\n instagram: Instagram,\r\n linkedin: Linkedin,\r\n};\r\n\r\nexport function ContactPage() {\r\n const { t } = useTranslation(\"contact-page\");\r\n usePageTitle({ title: t(\"title\") });\r\n\r\n const apiService = useApiService();\r\n\r\n const socialLinks = useMemo(() => {\r\n const socialMedia = constants.socialMedia as\r\n | Record<string, string>\r\n | undefined;\r\n if (!socialMedia) return [];\r\n return Object.entries(socialMedia)\r\n .filter(([platform, url]) => url && socialIcons[platform])\r\n .map(([platform, url]) => ({\r\n platform,\r\n url,\r\n Icon: socialIcons[platform],\r\n }));\r\n }, []);\r\n\r\n const [formData, setFormData] = useState({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n subject: \"\",\r\n message: \"\",\r\n attachments: [] as File[],\r\n });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\r\n \"idle\" | \"success\" | \"error\"\r\n >(\"idle\");\r\n const fileMaxFiles = constants.file?.maxFiles || 5;\r\n\r\n const handleSubmit = async (e: React.FormEvent) => {\r\n e.preventDefault();\r\n setIsSubmitting(true);\r\n setSubmitStatus(\"idle\");\r\n try {\r\n const currentLanguage = constants.site.defaultLanguage;\r\n\r\n await apiService.submitFormWithFile(\r\n formData,\r\n {\r\n email_subject1: \"Thank you for contacting us\",\r\n email_subject2: \"New Contact Form Submission\",\r\n fields: [\r\n { name: \"name\", required: true },\r\n { name: \"email\", required: true },\r\n { name: \"phone\", required: false },\r\n { name: \"subject\", required: false },\r\n { name: \"message\", required: true },\r\n { name: \"attachments\", required: false },\r\n ],\r\n },\r\n currentLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({\r\n name: \"\",\r\n email: \"\",\r\n phone: \"\",\r\n subject: \"\",\r\n message: \"\",\r\n attachments: [],\r\n });\r\n\r\n setTimeout(() => {\r\n setSubmitStatus(\"idle\");\r\n }, 5000);\r\n } catch (error: unknown) {\r\n console.error(\"Form submission failed:\", error);\r\n setSubmitStatus(\"error\");\r\n\r\n setTimeout(() => {\r\n setSubmitStatus(\"idle\");\r\n }, 5000);\r\n } finally {\r\n setIsSubmitting(false);\r\n }\r\n };\r\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\r\n const selectedFiles = Array.from(e.target.files || []) as File[];\r\n const remainingSlots = fileMaxFiles - formData.attachments.length;\r\n\r\n if (selectedFiles.length > remainingSlots) {\r\n alert(t(\"maxFilesLimit\", { max: fileMaxFiles }));\r\n e.target.value = ''; // Clear the input value\r\n return;\r\n }\r\n\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: [...prev.attachments, ...selectedFiles]\r\n }));\r\n\r\n e.target.value = ''; // Clear the input\r\n }\r\n const handleRemoveFile = (index: number) => {\r\n setFormData(prev => ({\r\n ...prev,\r\n attachments: prev.attachments.filter((_, i) => i !== index)\r\n }));\r\n };\r\n const handleChange = (\r\n e: React.ChangeEvent<\r\n HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement\r\n >\r\n ) => {\r\n setFormData((prev) => ({\r\n ...prev,\r\n [e.target.name]: e.target.value,\r\n }));\r\n };\r\n\r\n 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 max-w-6xl\">\r\n {/* Hero Section */}\r\n <FadeIn className=\"text-center mb-12\">\r\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\r\n {t(\"title\")}\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-3xl mx-auto\">\r\n {t(\"description\")}\r\n </p>\r\n </FadeIn>\r\n\r\n <div className=\"grid lg:grid-cols-3 gap-8\">\r\n {/* Contact Information */}\r\n <SlideInLeft className=\"lg:col-span-1 space-y-6\">\r\n {/* Company Info */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle className=\"flex items-center gap-2\">\r\n <MessageSquare className=\"w-5 h-5 text-primary\" />\r\n {t(\"getInTouch\")}\r\n </CardTitle>\r\n </CardHeader>\r\n <CardContent className=\"space-y-4\">\r\n <div className=\"flex items-start gap-3\">\r\n <Mail className=\"w-5 h-5 text-primary mt-1\" />\r\n <div>\r\n <p className=\"font-medium text-foreground\">\r\n {t(\"email\")}\r\n </p>\r\n <p className=\"text-muted-foreground\">{constants.email}</p>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"emailResponse\")}\r\n </p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-3\">\r\n <Phone className=\"w-5 h-5 text-primary mt-1\" />\r\n <div>\r\n <p className=\"font-medium text-foreground\">\r\n {t(\"phone\")}\r\n </p>\r\n <p className=\"text-muted-foreground\">{constants.phone}</p>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-start gap-3\">\r\n <MapPin className=\"w-5 h-5 text-primary mt-1\" />\r\n <div>\r\n <p className=\"font-medium text-foreground\">\r\n {t(\"address\")}\r\n </p>\r\n <div className=\"text-muted-foreground\">\r\n <p>{constants.address.line1}</p>\r\n {constants.address.line2 && (\r\n <p>{constants.address.line2}</p>\r\n )}\r\n <p>\r\n {constants.address.city}, {constants.address.state}{\" \"}\r\n {constants.address.postalCode}\r\n </p>\r\n {constants.address.country && (\r\n <p>{constants.address.country}</p>\r\n )}\r\n </div>\r\n </div>\r\n </div>\r\n\r\n {/* Social Media Links */}\r\n {socialLinks.length > 0 && (\r\n <div className=\"pt-4 border-t\">\r\n <p className=\"font-medium text-foreground mb-3\">\r\n {t(\"followUs\")}\r\n </p>\r\n <div className=\"flex gap-2\">\r\n {socialLinks.map(({ platform, url, Icon }) => (\r\n <a\r\n key={platform}\r\n href={url}\r\n target=\"_blank\"\r\n rel=\"noopener noreferrer\"\r\n className=\"h-10 w-10 flex items-center justify-center rounded-md border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\r\n >\r\n <Icon className=\"h-5 w-5\" />\r\n </a>\r\n ))}\r\n </div>\r\n </div>\r\n )}\r\n </CardContent>\r\n </Card>\r\n\r\n {/* Support Info */}\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"needSupport\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <p className=\"text-muted-foreground mb-4\">\r\n {t(\"supportDescription\")}\r\n </p>\r\n <div className=\"space-y-2\">\r\n <p className=\"font-medium text-foreground\">\r\n {t(\"supportEmail\")}\r\n </p>\r\n <p className=\"text-muted-foreground\">{constants.email}</p>\r\n </div>\r\n </CardContent>\r\n </Card>\r\n </SlideInLeft>\r\n\r\n {/* Contact Form */}\r\n <SlideInRight className=\"lg:col-span-2\">\r\n <Card>\r\n <CardHeader>\r\n <CardTitle>{t(\"sendMessage\")}</CardTitle>\r\n </CardHeader>\r\n <CardContent>\r\n <form onSubmit={handleSubmit} className=\"space-y-6\">\r\n {/* Full Name */}\r\n <FormField label={t(\"fullName\")} htmlFor=\"name\" required>\r\n <Input\r\n id=\"name\"\r\n name=\"name\"\r\n type=\"text\"\r\n value={formData.name}\r\n onChange={handleChange}\r\n placeholder={t(\"fullNamePlaceholder\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </FormField>\r\n {/* Email */}\r\n <FormField label={t(\"emailAddress\")} htmlFor=\"email\" required>\r\n <Input\r\n id=\"email\"\r\n name=\"email\"\r\n type=\"email\"\r\n value={formData.email}\r\n onChange={handleChange}\r\n placeholder={t(\"emailPlaceholder\")}\r\n required\r\n className=\"mt-1\"\r\n />\r\n </FormField>\r\n {/* Phone */}\r\n <FormField label={t(\"phoneNumber\")} htmlFor=\"phone\">\r\n <Input\r\n id=\"phone\"\r\n name=\"phone\"\r\n type=\"tel\"\r\n value={formData.phone}\r\n onChange={handleChange}\r\n placeholder={t(\"phonePlaceholder\")}\r\n className=\"mt-1\"\r\n />\r\n </FormField>\r\n {/* Subject */}\r\n <FormField label={t(\"subject\")} htmlFor=\"subject\" >\r\n <Input\r\n id=\"subject\"\r\n name=\"subject\"\r\n type=\"text\"\r\n value={formData.subject}\r\n onChange={handleChange}\r\n placeholder={t(\"subjectPlaceholder\")}\r\n className=\"mt-1\"\r\n />\r\n </FormField>\r\n {/* Message */}\r\n <FormField label={t(\"message\")} htmlFor=\"message\" required>\r\n <Textarea\r\n id=\"message\"\r\n name=\"message\"\r\n value={formData.message}\r\n onChange={handleChange}\r\n placeholder={t(\"messagePlaceholder\")}\r\n required\r\n rows={6}\r\n className=\"mt-1 resize-none\"\r\n />\r\n </FormField>\r\n <FormFileInput\r\n files={formData.attachments}\r\n onFilesChange={handleFileUploadChange}\r\n handleRemoveFile={handleRemoveFile}\r\n maxFiles={constants.file?.maxFiles || 5}\r\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\r\n disabled={isSubmitting}\r\n uploadButtonText={t(\"addFiles\")}\r\n maxFilesReachedText={t(\"maxFilesReached\")}\r\n />\r\n\r\n {submitStatus === \"success\" && (\r\n <div className=\"p-4 bg-green-500/10 dark:bg-green-500/20 border border-green-500/30 rounded-lg\">\r\n <p className=\"text-green-600 dark:text-green-400 text-sm font-medium\">\r\n {t(\"success\")}\r\n </p>\r\n </div>\r\n )}\r\n\r\n {submitStatus === \"error\" && (\r\n <div className=\"p-4 bg-destructive/10 border border-destructive/30 rounded-lg\">\r\n <p className=\"text-destructive text-sm font-medium\">\r\n {t(\"error\")}\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-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\r\n {t(\"sending\")}\r\n </>\r\n ) : (\r\n t(\"submit\")\r\n )}\r\n </Button>\r\n </form>\r\n </CardContent>\r\n </Card>\r\n </SlideInRight>\r\n </div>\r\n </div>\r\n </div>\r\n </Layout>\r\n );\r\n}\r\n\r\nexport default ContactPage;\r\n"
26
26
  },
27
27
  {
28
28
  "path": "contact-page/lang/en.json",
@@ -19,7 +19,7 @@
19
19
  "path": "db/config.ts",
20
20
  "type": "registry:lib",
21
21
  "target": "$modules$/db/config.ts",
22
- "content": "import type { IDataAdapter } from \"./adapters/IDataAdapter\";\nimport { SqliteAdapter } from \"./adapters/SqliteAdapter\";\n\nexport type AdapterType = \"sqlite\";\n\nexport interface DataConfig {\n adapter: AdapterType;\n options?: {\n dbPath?: string;\n };\n}\n\n/**\n * Get configuration from environment variables\n */\nfunction getConfig(): DataConfig {\n const adapter =\n (import.meta.env.VITE_DATA_ADAPTER as AdapterType) || \"sqlite\";\n\n return {\n adapter,\n options: {\n dbPath: import.meta.env.VITE_DB_PATH || \"/data/database.db\",\n },\n };\n}\n\n/**\n * Create adapter instance based on configuration\n */\nexport function createAdapter(config: DataConfig): IDataAdapter {\n switch (config.adapter) {\n case \"sqlite\":\n return new SqliteAdapter(config.options?.dbPath);\n\n default:\n throw new Error(`Unknown adapter: ${config.adapter}`);\n }\n}\n\n/**\n * Get the configured adapter instance (singleton)\n */\nlet adapterInstance: IDataAdapter | null = null;\n\nexport function getAdapter(): IDataAdapter {\n if (!adapterInstance) {\n const config = getConfig();\n adapterInstance = createAdapter(config);\n }\n return adapterInstance;\n}\n\n/**\n * Reset adapter instance (useful for testing or switching adapters)\n */\nexport function resetAdapter(): void {\n adapterInstance = null;\n}\n"
22
+ "content": "import type { IDataAdapter } from \"./adapters/IDataAdapter\";\r\nimport { SqliteAdapter } from \"./adapters/SqliteAdapter\";\r\n\r\nexport type AdapterType = \"sqlite\";\r\n\r\nexport interface DataConfig {\r\n adapter: AdapterType;\r\n options?: {\r\n dbPath?: string;\r\n };\r\n}\r\n\r\n/**\r\n * Get configuration from environment variables\r\n */\r\nfunction getConfig(): DataConfig {\r\n const adapter =\r\n (import.meta.env.VITE_DATA_ADAPTER as AdapterType) || \"sqlite\";\r\n\r\n return {\r\n adapter,\r\n options: {\r\n dbPath: import.meta.env.VITE_DB_PATH || \"/data/database.db\",\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Create adapter instance based on configuration\r\n */\r\nexport function createAdapter(config: DataConfig): IDataAdapter {\r\n switch (config.adapter) {\r\n case \"sqlite\":\r\n return new SqliteAdapter(config.options?.dbPath);\r\n\r\n default:\r\n throw new Error(`Unknown adapter: ${config.adapter}`);\r\n }\r\n}\r\n\r\n/**\r\n * Get the configured adapter instance (singleton)\r\n */\r\nlet adapterInstance: IDataAdapter | null = null;\r\n\r\nexport function getAdapter(): IDataAdapter {\r\n if (!adapterInstance) {\r\n const config = getConfig();\r\n adapterInstance = createAdapter(config);\r\n }\r\n return adapterInstance;\r\n}\r\n\r\n/**\r\n * Reset adapter instance (useful for testing or switching adapters)\r\n */\r\nexport function resetAdapter(): void {\r\n adapterInstance = null;\r\n}\r\n"
23
23
  },
24
24
  {
25
25
  "path": "db/core/DataManager.ts",
@@ -43,7 +43,7 @@
43
43
  "path": "db/adapters/index.ts",
44
44
  "type": "registry:index",
45
45
  "target": "$modules$/db/adapters/index.ts",
46
- "content": "export type { IDataAdapter } from \"./IDataAdapter\";\nexport { SqliteAdapter } from \"./SqliteAdapter\";\n"
46
+ "content": "export type { IDataAdapter } from \"./IDataAdapter\";\r\nexport { SqliteAdapter } from \"./SqliteAdapter\";\r\n"
47
47
  },
48
48
  {
49
49
  "path": "db/adapters/SqliteAdapter.ts",
@@ -55,19 +55,19 @@
55
55
  "path": "db/react/index.ts",
56
56
  "type": "registry:index",
57
57
  "target": "$modules$/db/react/index.ts",
58
- "content": "// Provider\nexport { DBQueryProvider } from \"./QueryProvider\";\n\n// Query client and utilities\nexport { queryClient, queryKeys, cacheUtils } from \"./queryClient\";\n\n// Generic repository hooks\nexport {\n useRepositoryQuery,\n useRepositoryQueryOne,\n useRepositoryQueryById,\n useRepositoryPagination,\n useRepositoryInfiniteQuery,\n useRepositoryCreate,\n useRepositoryUpdate,\n useRepositoryDelete,\n // Raw SQL hooks\n useRawQuery,\n useRawQueryOne,\n} from \"./useRepository\";\n\n// Types\nexport type { RepositoryQueryOptions } from \"./useRepository\";\n"
58
+ "content": "// Provider\r\nexport { DBQueryProvider } from \"./QueryProvider\";\r\n\r\n// Query client and utilities\r\nexport { queryClient, queryKeys, cacheUtils } from \"./queryClient\";\r\n\r\n// Generic repository hooks\r\nexport {\r\n useRepositoryQuery,\r\n useRepositoryQueryOne,\r\n useRepositoryQueryById,\r\n useRepositoryPagination,\r\n useRepositoryInfiniteQuery,\r\n useRepositoryCreate,\r\n useRepositoryUpdate,\r\n useRepositoryDelete,\r\n // Raw SQL hooks\r\n useRawQuery,\r\n useRawQueryOne,\r\n} from \"./useRepository\";\r\n\r\n// Types\r\nexport type { RepositoryQueryOptions } from \"./useRepository\";\r\n"
59
59
  },
60
60
  {
61
61
  "path": "db/react/queryClient.ts",
62
62
  "type": "registry:lib",
63
63
  "target": "$modules$/db/react/queryClient.ts",
64
- "content": "import { QueryClient } from \"@tanstack/react-query\";\n\n/**\n * React Query handles ALL caching, refetching, and invalidation\n * No custom cache needed!\n */\nexport const queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 30 * 1000, // 30 seconds fresh\n gcTime: 5 * 60 * 1000, // 5 minutes in cache (was cacheTime)\n retry: 1,\n refetchOnWindowFocus: false, // Don't auto-refetch on window focus\n refetchOnReconnect: false, // Don't refetch on reconnect\n refetchOnMount: false, // Don't refetch on component mount (prevent loops)\n },\n mutations: {\n retry: 0,\n },\n },\n});\n\n/**\n * Query key factory - for cache management\n * React Query uses these keys to cache and invalidate queries\n */\nexport const queryKeys = {\n all: (table: string) => [table] as const,\n lists: (table: string) => [table, \"list\"] as const,\n list: (table: string, options?: any) => [table, \"list\", options] as const,\n details: (table: string) => [table, \"detail\"] as const,\n detail: (table: string, id: number | string) =>\n [table, \"detail\", id] as const,\n paginated: (table: string, page: number, limit: number, options?: any) =>\n [table, \"paginated\", page, limit, options] as const,\n infinite: (table: string, limit: number, options?: any) =>\n [table, \"infinite\", limit, options] as const,\n count: (table: string, options?: any) => [table, \"count\", options] as const,\n};\n\n/**\n * Manual cache utilities (rarely needed)\n */\nexport const cacheUtils = {\n // Invalidate all queries for a table\n invalidateTable: (table: string) => {\n return queryClient.invalidateQueries({ queryKey: queryKeys.all(table) });\n },\n\n // Clear all cache\n clearAll: () => {\n return queryClient.clear();\n },\n\n // Get cached data\n getCachedData: <T>(queryKey: any[]) => {\n return queryClient.getQueryData<T>(queryKey);\n },\n\n // Set cached data manually\n setCachedData: <T>(queryKey: any[], data: T) => {\n return queryClient.setQueryData<T>(queryKey, data);\n },\n};\n"
64
+ "content": "import { QueryClient } from \"@tanstack/react-query\";\r\n\r\n/**\r\n * React Query handles ALL caching, refetching, and invalidation\r\n * No custom cache needed!\r\n */\r\nexport const queryClient = new QueryClient({\r\n defaultOptions: {\r\n queries: {\r\n staleTime: 30 * 1000, // 30 seconds fresh\r\n gcTime: 5 * 60 * 1000, // 5 minutes in cache (was cacheTime)\r\n retry: 1,\r\n refetchOnWindowFocus: false, // Don't auto-refetch on window focus\r\n refetchOnReconnect: false, // Don't refetch on reconnect\r\n refetchOnMount: false, // Don't refetch on component mount (prevent loops)\r\n },\r\n mutations: {\r\n retry: 0,\r\n },\r\n },\r\n});\r\n\r\n/**\r\n * Query key factory - for cache management\r\n * React Query uses these keys to cache and invalidate queries\r\n */\r\nexport const queryKeys = {\r\n all: (table: string) => [table] as const,\r\n lists: (table: string) => [table, \"list\"] as const,\r\n list: (table: string, options?: any) => [table, \"list\", options] as const,\r\n details: (table: string) => [table, \"detail\"] as const,\r\n detail: (table: string, id: number | string) =>\r\n [table, \"detail\", id] as const,\r\n paginated: (table: string, page: number, limit: number, options?: any) =>\r\n [table, \"paginated\", page, limit, options] as const,\r\n infinite: (table: string, limit: number, options?: any) =>\r\n [table, \"infinite\", limit, options] as const,\r\n count: (table: string, options?: any) => [table, \"count\", options] as const,\r\n};\r\n\r\n/**\r\n * Manual cache utilities (rarely needed)\r\n */\r\nexport const cacheUtils = {\r\n // Invalidate all queries for a table\r\n invalidateTable: (table: string) => {\r\n return queryClient.invalidateQueries({ queryKey: queryKeys.all(table) });\r\n },\r\n\r\n // Clear all cache\r\n clearAll: () => {\r\n return queryClient.clear();\r\n },\r\n\r\n // Get cached data\r\n getCachedData: <T>(queryKey: any[]) => {\r\n return queryClient.getQueryData<T>(queryKey);\r\n },\r\n\r\n // Set cached data manually\r\n setCachedData: <T>(queryKey: any[], data: T) => {\r\n return queryClient.setQueryData<T>(queryKey, data);\r\n },\r\n};\r\n"
65
65
  },
66
66
  {
67
67
  "path": "db/react/QueryProvider.tsx",
68
68
  "type": "registry:component",
69
69
  "target": "$modules$/db/react/QueryProvider.tsx",
70
- "content": "import { QueryClientProvider } from \"@tanstack/react-query\";\nimport { queryClient } from \"./queryClient\";\n\ninterface DBQueryProviderProps {\n children: React.ReactNode;\n}\n\n/**\n * DBQueryProvider - DB module's React Query provider\n * Wraps components that use db module hooks\n */\nexport function DBQueryProvider({ children }: DBQueryProviderProps) {\n return (\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\n );\n}\n"
70
+ "content": "import { QueryClientProvider } from \"@tanstack/react-query\";\r\nimport { queryClient } from \"./queryClient\";\r\n\r\ninterface DBQueryProviderProps {\r\n children: React.ReactNode;\r\n}\r\n\r\n/**\r\n * DBQueryProvider - DB module's React Query provider\r\n * Wraps components that use db module hooks\r\n */\r\nexport function DBQueryProvider({ children }: DBQueryProviderProps) {\r\n return (\r\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>\r\n );\r\n}\r\n"
71
71
  },
72
72
  {
73
73
  "path": "db/react/useRepository.ts",
@@ -79,7 +79,7 @@
79
79
  "path": "db/utils/parsers.ts",
80
80
  "type": "registry:lib",
81
81
  "target": "$modules$/db/utils/parsers.ts",
82
- "content": "/**\n * Database field parsers - Client-side utilities\n * NO automatic parsing - client decides what to parse and when\n */\n\n/**\n * Parse comma-separated string to array\n * @example \"tag1,tag2,tag3\" -> [\"tag1\", \"tag2\", \"tag3\"]\n */\nexport const parseCommaSeparatedString = (value: string): string[] => {\n if (!value || typeof value !== \"string\") return [];\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n};\n\n/**\n * Parse JSON string to array\n * @example '[\"img1.jpg\",\"img2.jpg\"]' -> [\"img1.jpg\", \"img2.jpg\"]\n */\nexport const parseJSONStringToArray = (value: string): string[] => {\n if (!value || typeof value !== \"string\") return [];\n try {\n const parsed = JSON.parse(value);\n return Array.isArray(parsed) ? parsed : [];\n } catch (e) {\n console.warn(\"Failed to parse JSON array:\", value);\n return [];\n }\n};\n\n/**\n * Smart array parser - tries JSON first, falls back to comma-separated\n * @example '[\"a\",\"b\"]' -> [\"a\", \"b\"] OR \"a,b\" -> [\"a\", \"b\"]\n */\nexport const parseStringToArray = (value: any): string[] => {\n if (!value) return [];\n if (Array.isArray(value)) return value;\n if (typeof value !== \"string\") return [];\n\n // Try JSON first\n if (value.trim().startsWith(\"[\")) {\n const jsonResult = parseJSONStringToArray(value);\n if (jsonResult.length > 0) return jsonResult;\n }\n\n // Fall back to comma-separated\n return parseCommaSeparatedString(value);\n};\n\n/**\n * Parse JSON string to object\n * @example '{\"key\":\"value\"}' -> {key: \"value\"}\n */\nexport const parseJSONString = <T = any>(\n value: any,\n defaultValue: T | null = null,\n): T | null => {\n if (!value) return defaultValue;\n if (typeof value === \"object\") return value; // Already parsed\n if (typeof value !== \"string\") return defaultValue;\n\n try {\n return JSON.parse(value);\n } catch (e) {\n console.warn(\"Failed to parse JSON:\", value);\n return defaultValue;\n }\n};\n\n/**\n * Parse SQLite boolean (0/1) to JavaScript boolean\n * @example 1 -> true, 0 -> false\n */\nexport const parseSQLiteBoolean = (value: any): boolean => {\n if (typeof value === \"boolean\") return value;\n if (typeof value === \"number\") return value !== 0;\n if (typeof value === \"string\") {\n const lower = value.toLowerCase();\n return lower === \"true\" || lower === \"1\" || lower === \"yes\";\n }\n return Boolean(value);\n};\n\n/**\n * Parse number safely with default fallback\n * @example \"123\" -> 123, \"invalid\" -> 0 (or provided default)\n */\nexport const parseNumberSafe = (\n value: any,\n defaultValue: number = 0,\n): number => {\n const num = Number(value);\n return isNaN(num) ? defaultValue : num;\n};\n"
82
+ "content": "/**\r\n * Database field parsers - Client-side utilities\r\n * NO automatic parsing - client decides what to parse and when\r\n */\r\n\r\n/**\r\n * Parse comma-separated string to array\r\n * @example \"tag1,tag2,tag3\" -> [\"tag1\", \"tag2\", \"tag3\"]\r\n */\r\nexport const parseCommaSeparatedString = (value: string): string[] => {\r\n if (!value || typeof value !== \"string\") return [];\r\n return value\r\n .split(\",\")\r\n .map((item) => item.trim())\r\n .filter(Boolean);\r\n};\r\n\r\n/**\r\n * Parse JSON string to array\r\n * @example '[\"img1.jpg\",\"img2.jpg\"]' -> [\"img1.jpg\", \"img2.jpg\"]\r\n */\r\nexport const parseJSONStringToArray = (value: string): string[] => {\r\n if (!value || typeof value !== \"string\") return [];\r\n try {\r\n const parsed = JSON.parse(value);\r\n return Array.isArray(parsed) ? parsed : [];\r\n } catch (e) {\r\n console.warn(\"Failed to parse JSON array:\", value);\r\n return [];\r\n }\r\n};\r\n\r\n/**\r\n * Smart array parser - tries JSON first, falls back to comma-separated\r\n * @example '[\"a\",\"b\"]' -> [\"a\", \"b\"] OR \"a,b\" -> [\"a\", \"b\"]\r\n */\r\nexport const parseStringToArray = (value: any): string[] => {\r\n if (!value) return [];\r\n if (Array.isArray(value)) return value;\r\n if (typeof value !== \"string\") return [];\r\n\r\n // Try JSON first\r\n if (value.trim().startsWith(\"[\")) {\r\n const jsonResult = parseJSONStringToArray(value);\r\n if (jsonResult.length > 0) return jsonResult;\r\n }\r\n\r\n // Fall back to comma-separated\r\n return parseCommaSeparatedString(value);\r\n};\r\n\r\n/**\r\n * Parse JSON string to object\r\n * @example '{\"key\":\"value\"}' -> {key: \"value\"}\r\n */\r\nexport const parseJSONString = <T = any>(\r\n value: any,\r\n defaultValue: T | null = null,\r\n): T | null => {\r\n if (!value) return defaultValue;\r\n if (typeof value === \"object\") return value; // Already parsed\r\n if (typeof value !== \"string\") return defaultValue;\r\n\r\n try {\r\n return JSON.parse(value);\r\n } catch (e) {\r\n console.warn(\"Failed to parse JSON:\", value);\r\n return defaultValue;\r\n }\r\n};\r\n\r\n/**\r\n * Parse SQLite boolean (0/1) to JavaScript boolean\r\n * @example 1 -> true, 0 -> false\r\n */\r\nexport const parseSQLiteBoolean = (value: any): boolean => {\r\n if (typeof value === \"boolean\") return value;\r\n if (typeof value === \"number\") return value !== 0;\r\n if (typeof value === \"string\") {\r\n const lower = value.toLowerCase();\r\n return lower === \"true\" || lower === \"1\" || lower === \"yes\";\r\n }\r\n return Boolean(value);\r\n};\r\n\r\n/**\r\n * Parse number safely with default fallback\r\n * @example \"123\" -> 123, \"invalid\" -> 0 (or provided default)\r\n */\r\nexport const parseNumberSafe = (\r\n value: any,\r\n defaultValue: number = 0,\r\n): number => {\r\n const num = Number(value);\r\n return isNaN(num) ? defaultValue : num;\r\n};\r\n"
83
83
  }
84
84
  ],
85
85
  "exports": {
@@ -19,7 +19,7 @@
19
19
  "path": "featured-products/featured-products.tsx",
20
20
  "type": "registry:component",
21
21
  "target": "$modules$/featured-products/featured-products.tsx",
22
- "content": "import { Link } from \"react-router\";\nimport { ArrowRight } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { ProductCard } from \"@/modules/product-card/product-card\";\nimport { useTranslation } from \"react-i18next\";\nimport { useDbFeaturedProducts } from \"@/modules/ecommerce-core\";\nimport type { Product } from \"@/modules/ecommerce-core/types\";\n\ninterface FeaturedProductsProps {\n products?: Product[];\n loading?: boolean;\n}\n\nexport function FeaturedProducts({\n products: propProducts,\n loading: propLoading,\n}: FeaturedProductsProps) {\n const { t } = useTranslation(\"featured-products\");\n const { products: hookProducts, loading: hookLoading } = useDbFeaturedProducts();\n\n const products = propProducts ?? hookProducts;\n const loading = propLoading ?? hookLoading;\n\n return (\n <section className=\"py-8 sm:py-12 md:py-16 lg:py-20 bg-background border-t border-border/20 relative\">\n <div className=\"absolute top-0 left-1/2 transform -translate-x-1/2 w-16 sm:w-24 h-px bg-primary/30\"></div>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\n <div className=\"text-center mb-6 sm:mb-8 md:mb-12 lg:mb-16 px-2\">\n <h2 className=\"text-xl sm:text-2xl md:text-3xl lg:text-4xl xl:text-5xl font-bold mb-2 sm:mb-3 md:mb-4 bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent leading-normal pb-1\">\n {t('title', 'Featured Products')}\n </h2>\n <div className=\"w-12 sm:w-16 md:w-20 h-1 bg-gradient-to-r from-primary/50 to-primary/20 mx-auto mb-3 sm:mb-4 md:mb-6 rounded-full\"></div>\n <p className=\"text-xs sm:text-sm md:text-base lg:text-lg xl:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed\">\n {t('subtitle', 'Hand-picked favorites from our collection')}\n </p>\n </div>\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-8 xl:gap-10\">\n {loading ? (\n [...Array(3)].map((_, i) => (\n <div key={i} className=\"animate-pulse group\">\n <div className=\"aspect-square bg-gradient-to-br from-muted to-muted/50 rounded-2xl mb-6\"></div>\n <div className=\"space-y-3\">\n <div className=\"h-6 bg-muted rounded-lg w-3/4\"></div>\n <div className=\"h-4 bg-muted rounded w-1/2\"></div>\n <div className=\"h-5 bg-muted rounded w-2/3\"></div>\n </div>\n </div>\n ))\n ) : (\n products.map((product) => (\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\n <ProductCard\n product={product}\n variant=\"featured\"\n />\n </div>\n ))\n )}\n </div>\n\n <div className=\"text-center mt-8 sm:mt-12 lg:mt-16\">\n <Button size=\"lg\" asChild className=\"px-6 sm:px-8 py-3 sm:py-4 text-base sm:text-lg\">\n <Link to=\"/products\">\n {t('viewAll', 'View All Products')}\n <ArrowRight className=\"w-4 h-4 sm:w-5 sm:h-5 ml-2\" />\n </Link>\n </Button>\n </div>\n </div>\n </section>\n );\n}\n"
22
+ "content": "import { Link } from \"react-router\";\r\nimport { ArrowRight } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { ProductCard } from \"@/modules/product-card/product-card\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { useDbFeaturedProducts } from \"@/modules/ecommerce-core\";\r\nimport type { Product } from \"@/modules/ecommerce-core/types\";\r\n\r\ninterface FeaturedProductsProps {\r\n products?: Product[];\r\n loading?: boolean;\r\n}\r\n\r\nexport function FeaturedProducts({\r\n products: propProducts,\r\n loading: propLoading,\r\n}: FeaturedProductsProps) {\r\n const { t } = useTranslation(\"featured-products\");\r\n const { products: hookProducts, loading: hookLoading } = useDbFeaturedProducts();\r\n\r\n const products = propProducts ?? hookProducts;\r\n const loading = propLoading ?? hookLoading;\r\n\r\n return (\r\n <section className=\"py-8 sm:py-12 md:py-16 lg:py-20 bg-background border-t border-border/20 relative\">\r\n <div className=\"absolute top-0 left-1/2 transform -translate-x-1/2 w-16 sm:w-24 h-px bg-primary/30\"></div>\r\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-3 sm:px-4 lg:px-8\">\r\n <div className=\"text-center mb-6 sm:mb-8 md:mb-12 lg:mb-16 px-2\">\r\n <h2 className=\"text-xl sm:text-2xl md:text-3xl lg:text-4xl xl:text-5xl font-bold mb-2 sm:mb-3 md:mb-4 bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent leading-normal pb-1\">\r\n {t('title', 'Featured Products')}\r\n </h2>\r\n <div className=\"w-12 sm:w-16 md:w-20 h-1 bg-gradient-to-r from-primary/50 to-primary/20 mx-auto mb-3 sm:mb-4 md:mb-6 rounded-full\"></div>\r\n <p className=\"text-xs sm:text-sm md:text-base lg:text-lg xl:text-xl text-muted-foreground max-w-2xl mx-auto leading-relaxed\">\r\n {t('subtitle', 'Hand-picked favorites from our collection')}\r\n </p>\r\n </div>\r\n\r\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 sm:gap-6 lg:gap-8 xl:gap-10\">\r\n {loading ? (\r\n [...Array(3)].map((_, i) => (\r\n <div key={i} className=\"animate-pulse group\">\r\n <div className=\"aspect-square bg-gradient-to-br from-muted to-muted/50 rounded-2xl mb-6\"></div>\r\n <div className=\"space-y-3\">\r\n <div className=\"h-6 bg-muted rounded-lg w-3/4\"></div>\r\n <div className=\"h-4 bg-muted rounded w-1/2\"></div>\r\n <div className=\"h-5 bg-muted rounded w-2/3\"></div>\r\n </div>\r\n </div>\r\n ))\r\n ) : (\r\n products.map((product) => (\r\n <div key={product.id} className=\"contents\" data-db-table=\"products\" data-db-id={product.id}>\r\n <ProductCard\r\n product={product}\r\n variant=\"featured\"\r\n />\r\n </div>\r\n ))\r\n )}\r\n </div>\r\n\r\n <div className=\"text-center mt-8 sm:mt-12 lg:mt-16\">\r\n <Button size=\"lg\" asChild className=\"px-6 sm:px-8 py-3 sm:py-4 text-base sm:text-lg\">\r\n <Link to=\"/products\">\r\n {t('viewAll', 'View All Products')}\r\n <ArrowRight className=\"w-4 h-4 sm:w-5 sm:h-5 ml-2\" />\r\n </Link>\r\n </Button>\r\n </div>\r\n </div>\r\n </section>\r\n );\r\n}\r\n"
23
23
  },
24
24
  {
25
25
  "path": "featured-products/lang/en.json",
@@ -23,7 +23,7 @@
23
23
  "path": "forgot-password-page-split/forgot-password-page-split.tsx",
24
24
  "type": "registry:page",
25
25
  "target": "$modules$/forgot-password-page-split/forgot-password-page-split.tsx",
26
- "content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { Mail, ArrowLeft, CheckCircle } from \"lucide-react\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\n\r\ninterface ForgotPasswordPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function ForgotPasswordPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: ForgotPasswordPageSplitProps) {\r\n const { t } = useTranslation(\"forgot-password-page-split\");\r\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\r\n const { forgotPassword } = useAuth();\r\n\r\n const [username, setUsername] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [isEmailSent, setIsEmailSent] = useState(false);\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 forgotPassword(username);\r\n\r\n setIsEmailSent(true);\r\n toast.success(t(\"emailSent\", \"Reset link sent!\"), {\r\n description: t(\"checkInbox\", \"Please check your email inbox.\"),\r\n });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"forgotPasswordError\", \"Failed to send reset link. Please try again.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n // Success state - email sent\r\n if (isEmailSent) {\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6 text-center\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center\">\r\n <CheckCircle className=\"w-8 h-8 text-green-600 dark:text-green-400\" />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"checkYourEmail\", \"Check your email\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-2\">\r\n {t(\"emailSentTo\", \"We've sent a password reset link to\")}\r\n </p>\r\n <p className=\"text-sm font-medium mt-1\">{username}</p>\r\n </div>\r\n\r\n <div className=\"text-sm text-muted-foreground\">\r\n <p>{t(\"didntReceive\", \"Didn't receive the email?\")}</p>\r\n <Button\r\n variant=\"link\"\r\n className=\"p-0 h-auto\"\r\n onClick={() => {\r\n setIsEmailSent(false);\r\n setError(null);\r\n }}\r\n >\r\n {t(\"tryAgain\", \"Click here to try again\")}\r\n </Button>\r\n </div>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Forgot password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n }\r\n\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center\">\r\n <Mail className=\"w-6 h-6 text-primary\" />\r\n </div>\r\n </div>\r\n\r\n <div className=\"text-center\">\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Forgot Password\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"subtitle\", \"Enter your username to reset your password\")}\r\n </p>\r\n </div>\r\n\r\n {error && (\r\n <div className=\"p-3 text-sm text-red-600 bg-red-50 dark:bg-red-950 dark:text-red-400 rounded-md\">\r\n {error}\r\n </div>\r\n )}\r\n\r\n <form onSubmit={handleSubmit} className=\"grid gap-4\">\r\n <div className=\"grid gap-2\">\r\n <Label htmlFor=\"username\">{t(\"username\", \"Username\")}</Label>\r\n <Input\r\n required\r\n id=\"username\"\r\n type=\"text\"\r\n autoComplete=\"username\"\r\n placeholder={t(\"usernamePlaceholder\", \"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\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(\"sending\", \"Sending...\")}\r\n </>\r\n ) : (\r\n t(\"sendLink\", \"Send Reset Link\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Forgot password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default ForgotPasswordPageSplit;\r\n"
26
+ "content": "import { useState } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { toast } from \"sonner\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { usePageTitle } from \"@/hooks/use-page-title\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Logo } from \"@/components/Logo\";\r\nimport { Mail, ArrowLeft, CheckCircle } from \"lucide-react\";\r\nimport { useAuth } from \"@/modules/auth-core\";\r\nimport { getErrorMessage } from \"@/modules/api\";\r\nimport { FormField } from \"@/components/FormField\";\r\n\r\ninterface ForgotPasswordPageSplitProps {\r\n image?: string;\r\n}\r\n\r\nexport function ForgotPasswordPageSplit({\r\n image = \"/images/placeholder.png\",\r\n}: ForgotPasswordPageSplitProps) {\r\n const { t } = useTranslation(\"forgot-password-page-split\");\r\n usePageTitle({ title: t(\"title\", \"Forgot Password\") });\r\n const { forgotPassword } = useAuth();\r\n\r\n const [username, setUsername] = useState(\"\");\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<string | null>(null);\r\n const [isEmailSent, setIsEmailSent] = useState(false);\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 forgotPassword(username);\r\n\r\n setIsEmailSent(true);\r\n toast.success(t(\"emailSent\", \"Reset link sent!\"), {\r\n description: t(\"checkInbox\", \"Please check your email inbox.\"),\r\n });\r\n } catch (err) {\r\n const errorMessage = getErrorMessage(\r\n err,\r\n t(\"forgotPasswordError\", \"Failed to send reset link. Please try again.\")\r\n );\r\n setError(errorMessage);\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n };\r\n\r\n // Success state - email sent\r\n if (isEmailSent) {\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6 text-center\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center\">\r\n <CheckCircle className=\"w-8 h-8 text-green-600 dark:text-green-400\" />\r\n </div>\r\n </div>\r\n\r\n <div>\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"checkYourEmail\", \"Check your email\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-2\">\r\n {t(\"emailSentTo\", \"We've sent a password reset link to\")}\r\n </p>\r\n <p className=\"text-sm font-medium mt-1\">{username}</p>\r\n </div>\r\n\r\n <div className=\"text-sm text-muted-foreground\">\r\n <p>{t(\"didntReceive\", \"Didn't receive the email?\")}</p>\r\n <Button\r\n variant=\"link\"\r\n className=\"p-0 h-auto\"\r\n onClick={() => {\r\n setIsEmailSent(false);\r\n setError(null);\r\n }}\r\n >\r\n {t(\"tryAgain\", \"Click here to try again\")}\r\n </Button>\r\n </div>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Forgot password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n }\r\n\r\n return (\r\n <section className=\"w-full md:grid md:min-h-screen md:grid-cols-2\">\r\n <div className=\"flex items-center justify-center px-4 py-12\">\r\n <div className=\"mx-auto grid w-full max-w-sm gap-6\">\r\n <Logo />\r\n <hr />\r\n\r\n <div className=\"flex justify-center\">\r\n <div className=\"w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center\">\r\n <Mail className=\"w-6 h-6 text-primary\" />\r\n </div>\r\n </div>\r\n\r\n <div className=\"text-center\">\r\n <h1 className=\"text-xl font-bold tracking-tight\">\r\n {t(\"title\", \"Forgot Password\")}\r\n </h1>\r\n <p className=\"text-sm text-muted-foreground mt-1\">\r\n {t(\"subtitle\", \"Enter your username to reset your password\")}\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\" required>\r\n <Input\r\n required\r\n id=\"username\"\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\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(\"sending\", \"Sending...\")}\r\n </>\r\n ) : (\r\n t(\"sendLink\", \"Send Reset Link\")\r\n )}\r\n </Button>\r\n </form>\r\n\r\n <Link\r\n to=\"/login\"\r\n className=\"inline-flex items-center justify-center gap-2 text-sm text-muted-foreground hover:text-foreground\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n {t(\"backToLogin\", \"Back to login\")}\r\n </Link>\r\n\r\n <hr />\r\n <p className=\"text-sm text-muted-foreground\">\r\n © {new Date().getFullYear()} {t(\"copyright\", \"All rights reserved.\")}\r\n </p>\r\n </div>\r\n </div>\r\n <div className=\"hidden p-4 md:block\">\r\n <img\r\n loading=\"lazy\"\r\n decoding=\"async\"\r\n width=\"1920\"\r\n height=\"1080\"\r\n alt={t(\"imageAlt\", \"Forgot password background\")}\r\n src={image}\r\n className=\"size-full rounded-lg border bg-muted object-cover object-center\"\r\n />\r\n </div>\r\n </section>\r\n );\r\n}\r\n\r\nexport default ForgotPasswordPageSplit;\r\n"
27
27
  },
28
28
  {
29
29
  "path": "forgot-password-page-split/lang/en.json",