@promakeai/cli 0.9.9 → 0.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -111
- package/dist/index.js +142 -142
- package/dist/registry/about-page.json +3 -3
- package/dist/registry/about-section.json +4 -4
- package/dist/registry/animations.json +2 -2
- package/dist/registry/announcement-bar.json +4 -4
- package/dist/registry/api.json +1 -1
- package/dist/registry/auth-core.json +2 -2
- package/dist/registry/bento-grid-section.json +4 -4
- package/dist/registry/blog-core.json +5 -5
- package/dist/registry/blog-list-page.json +4 -4
- package/dist/registry/blog-section.json +4 -4
- package/dist/registry/cards-carousel-section.json +4 -4
- package/dist/registry/cart-drawer.json +4 -4
- package/dist/registry/cart-page.json +4 -4
- package/dist/registry/case-study-page.json +3 -3
- package/dist/registry/category-section.json +4 -4
- package/dist/registry/checkout-page.json +4 -4
- package/dist/registry/coming-soon-page-minimal.json +4 -4
- package/dist/registry/coming-soon-page.json +4 -4
- package/dist/registry/contact-info-grid.json +4 -4
- package/dist/registry/contact-page-centered.json +4 -4
- package/dist/registry/contact-page-map-overlay.json +3 -3
- package/dist/registry/contact-page-map-split.json +3 -3
- package/dist/registry/contact-page-split.json +4 -4
- package/dist/registry/contact-page.json +4 -4
- package/dist/registry/content-section.json +4 -4
- package/dist/registry/cookie-consent.json +4 -4
- package/dist/registry/cookies-page.json +3 -3
- package/dist/registry/cta-section.json +3 -3
- package/dist/registry/ecommerce-core.json +8 -8
- package/dist/registry/empty-page.json +3 -3
- package/dist/registry/faq-categorized.json +4 -4
- package/dist/registry/faq-simple.json +4 -4
- package/dist/registry/favorites-blog-block.json +1 -1
- package/dist/registry/favorites-blog-page.json +4 -4
- package/dist/registry/favorites-ecommerce-block.json +1 -1
- package/dist/registry/favorites-ecommerce-page.json +4 -4
- package/dist/registry/feature-section.json +3 -3
- package/dist/registry/featured-products.json +4 -4
- package/dist/registry/footer-detailed.json +4 -4
- package/dist/registry/footer-minimal.json +3 -3
- package/dist/registry/footer.json +3 -3
- package/dist/registry/forgot-password-page-split.json +4 -4
- package/dist/registry/forgot-password-page.json +4 -4
- package/dist/registry/google-adsense.json +4 -4
- package/dist/registry/google-map.json +2 -2
- package/dist/registry/header-centered-pill.json +4 -4
- package/dist/registry/header-ecommerce.json +4 -4
- package/dist/registry/header-mega.json +4 -4
- package/dist/registry/header-minimal.json +4 -4
- package/dist/registry/header-simple.json +3 -3
- package/dist/registry/hero-carousel.json +3 -3
- package/dist/registry/hero-cta.json +4 -4
- package/dist/registry/hero-gradient.json +4 -4
- package/dist/registry/hero-grid.json +4 -4
- package/dist/registry/hero-profile.json +3 -3
- package/dist/registry/hero.json +3 -3
- package/dist/registry/index.json +103 -103
- package/dist/registry/landing-page-app.json +3 -3
- package/dist/registry/landing-page-saas.json +3 -3
- package/dist/registry/login-page-split.json +4 -4
- package/dist/registry/login-page.json +4 -4
- package/dist/registry/logo-cloud.json +4 -4
- package/dist/registry/masonry-grid.json +3 -3
- package/dist/registry/my-orders-page.json +4 -4
- package/dist/registry/newsletter-section.json +4 -4
- package/dist/registry/order-card-compact.json +3 -3
- package/dist/registry/order-confirmation-page.json +4 -4
- package/dist/registry/order-detail-block.json +1 -1
- package/dist/registry/orders-list-block.json +1 -1
- package/dist/registry/payment-success-block.json +2 -2
- package/dist/registry/portfolio-page.json +4 -4
- package/dist/registry/post-card.json +4 -4
- package/dist/registry/post-detail-block.json +4 -4
- package/dist/registry/post-detail-page.json +4 -4
- package/dist/registry/pricing-card.json +3 -3
- package/dist/registry/pricing-page.json +4 -4
- package/dist/registry/pricing-section.json +4 -4
- package/dist/registry/privacy-page.json +3 -3
- package/dist/registry/product-card-detailed.json +4 -4
- package/dist/registry/product-card-hover.json +4 -4
- package/dist/registry/product-card.json +4 -4
- package/dist/registry/product-detail-block.json +2 -2
- package/dist/registry/product-detail-page.json +4 -4
- package/dist/registry/product-detail-section.json +4 -4
- package/dist/registry/product-quick-view.json +4 -4
- package/dist/registry/products-page.json +4 -4
- package/dist/registry/reading-progress.json +4 -4
- package/dist/registry/register-page-split.json +4 -4
- package/dist/registry/register-page.json +4 -4
- package/dist/registry/related-posts-block.json +1 -1
- package/dist/registry/related-products-block.json +2 -2
- package/dist/registry/reset-password-page-split.json +4 -4
- package/dist/registry/reset-password-page.json +4 -4
- package/dist/registry/service-card.json +1 -1
- package/dist/registry/share-buttons.json +4 -4
- package/dist/registry/skill-card.json +1 -1
- package/dist/registry/team-page.json +4 -4
- package/dist/registry/terms-page.json +3 -3
- package/dist/registry/testimonials-carousel.json +4 -4
- package/dist/registry/testimonials-grid.json +4 -4
- package/dist/registry/timeline-section.json +4 -4
- package/dist/registry/verify-email-page.json +4 -4
- package/dist/registry/video-hero.json +4 -4
- package/dist/registry/youtube-embed.json +4 -4
- package/package.json +1 -1
- package/template/.env +5 -5
- package/template/README.md +54 -54
- package/template/eslint.config.js +41 -41
- package/template/index.html +237 -237
- package/template/package.json +96 -96
- package/template/public/_redirects +1 -1
- package/template/public/robots.txt +14 -14
- package/template/scripts/init-db.ts +18 -18
- package/template/src/App.tsx +21 -21
- package/template/src/components/FormField.tsx +48 -48
- package/template/src/components/FormFileInput.tsx +75 -75
- package/template/src/components/GoogleAnalytics.tsx +34 -34
- package/template/src/components/LanguageSwitcher.tsx +53 -53
- package/template/src/components/MetriaAnalytics.tsx +68 -68
- package/template/src/components/PasswordInput.tsx +60 -60
- package/template/src/components/ScriptInjector.tsx +62 -62
- package/template/src/components/Stack.tsx +39 -39
- package/template/src/constants/constants.json +71 -71
- package/template/src/db/index.ts +21 -21
- package/template/src/db/provider.tsx +106 -106
- package/template/src/db/schema.json +278 -278
- package/template/src/db/types.ts +195 -195
- package/template/src/hooks/use-debounced-value.ts +12 -12
- package/template/src/hooks/use-page-title.ts +55 -55
- package/template/src/lang/index.ts +90 -90
- package/template/src/lib/api.ts +345 -345
- package/template/src/lib/env.ts +19 -19
- package/template/src/router.tsx +14 -14
- package/template/src/vite-env.d.ts +1 -1
- package/template/vite.config.ts +194 -199
|
@@ -14,25 +14,25 @@
|
|
|
14
14
|
"path": "coming-soon-page-minimal/index.ts",
|
|
15
15
|
"type": "registry:index",
|
|
16
16
|
"target": "$modules$/coming-soon-page-minimal/index.ts",
|
|
17
|
-
"content": "export * from \"./coming-soon-page-minimal\";\
|
|
17
|
+
"content": "export * from \"./coming-soon-page-minimal\";\nexport { default } from \"./coming-soon-page-minimal\";\n"
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"path": "coming-soon-page-minimal/coming-soon-page-minimal.tsx",
|
|
21
21
|
"type": "registry:page",
|
|
22
22
|
"target": "$modules$/coming-soon-page-minimal/coming-soon-page-minimal.tsx",
|
|
23
|
-
"content": "import { useState, useEffect, useMemo } from \"react\";\
|
|
23
|
+
"content": "import { useState, useEffect, useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { ArrowRight } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ComingSoonPageMinimalProps {\n launchDate?: Date;\n className?: string;\n}\n\n// Default launch date: 30 days from initial load\nconst DEFAULT_LAUNCH_DATE = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);\n\nexport function ComingSoonPageMinimal({\n launchDate: launchDateProp,\n className\n}: ComingSoonPageMinimalProps) {\n const { t } = useTranslation(\"coming-soon-page-minimal\");\n usePageTitle({ title: t(\"title\", \"Coming Soon\") });\n\n const launchDate = useMemo(() => launchDateProp ?? DEFAULT_LAUNCH_DATE, [launchDateProp]);\n\n const [email, setEmail] = useState(\"\");\n const [isSubmitted, setIsSubmitted] = useState(false);\n const [days, setDays] = useState(0);\n\n useEffect(() => {\n const calculateDays = () => {\n const difference = launchDate.getTime() - new Date().getTime();\n if (difference > 0) {\n setDays(Math.floor(difference / (1000 * 60 * 60 * 24)));\n }\n };\n calculateDays();\n const timer = setInterval(calculateDays, 60000);\n return () => clearInterval(timer);\n }, [launchDate]);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitted(true);\n setEmail(\"\");\n };\n\n return (\n <div className={cn(\n \"min-h-screen flex flex-col items-center justify-center bg-background px-4\",\n className\n )}>\n <div className=\"max-w-3xl mx-auto text-center\">\n {/* Large countdown number */}\n <div className=\"mb-8\">\n <span className=\"text-[12rem] md:text-[16rem] lg:text-[20rem] font-bold leading-none text-primary/10\">\n {days}\n </span>\n </div>\n\n {/* Heading overlaid */}\n <div className=\"-mt-32 md:-mt-48 lg:-mt-64 relative z-10 mb-12\">\n <p className=\"text-sm uppercase tracking-[0.3em] text-muted-foreground mb-4\">\n {t(\"daysLeft\", \"days until launch\")}\n </p>\n <h1 className=\"text-4xl md:text-6xl lg:text-7xl font-bold text-foreground mb-6\">\n {t(\"heading\", \"Coming Soon\")}\n </h1>\n <p className=\"text-lg text-muted-foreground max-w-lg mx-auto\">\n {t(\"description\", \"We're crafting something beautiful. Leave your email to get notified.\")}\n </p>\n </div>\n\n {/* Email signup */}\n <div className=\"max-w-sm mx-auto\">\n {!isSubmitted ? (\n <form onSubmit={handleSubmit} className=\"relative\">\n <Input\n type=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={t(\"emailPlaceholder\", \"your@email.com\")}\n required\n className=\"h-14 pr-14 text-base\"\n />\n <Button\n type=\"submit\"\n size=\"icon\"\n className=\"absolute right-2 top-1/2 -translate-y-1/2 h-10 w-10 rounded-full\"\n >\n <ArrowRight className=\"h-5 w-5\" />\n </Button>\n </form>\n ) : (\n <p className=\"text-primary font-medium py-4\">\n {t(\"success\", \"We'll be in touch!\")}\n </p>\n )}\n </div>\n </div>\n\n {/* Bottom decoration line */}\n <div className=\"absolute bottom-0 left-0 right-0 h-1 bg-gradient-to-r from-transparent via-primary/50 to-transparent\" />\n </div>\n );\n}\n\nexport default ComingSoonPageMinimal;\n"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "coming-soon-page-minimal/lang/en.json",
|
|
27
27
|
"type": "registry:lang",
|
|
28
28
|
"target": "$modules$/coming-soon-page-minimal/lang/en.json",
|
|
29
|
-
"content": "{\
|
|
29
|
+
"content": "{\n \"title\": \"Coming Soon\",\n \"daysLeft\": \"days until launch\",\n \"heading\": \"Coming Soon\",\n \"description\": \"We're crafting something beautiful. Stay tuned!\",\n \"emailPlaceholder\": \"your@email.com\",\n \"success\": \"We'll be in touch!\"\n}"
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
"path": "coming-soon-page-minimal/lang/tr.json",
|
|
33
33
|
"type": "registry:lang",
|
|
34
34
|
"target": "$modules$/coming-soon-page-minimal/lang/tr.json",
|
|
35
|
-
"content": "{\
|
|
35
|
+
"content": "{\n \"title\": \"Yakında\",\n \"daysLeft\": \"gün kaldı\",\n \"heading\": \"Yakında\",\n \"description\": \"Güzel bir şey hazırlıyoruz. Haberdar olmak için e-postanızı bırakın.\",\n \"emailPlaceholder\": \"ornek@email.com\",\n \"success\": \"Sizinle iletişime geçeceğiz!\"\n}\n"
|
|
36
36
|
}
|
|
37
37
|
],
|
|
38
38
|
"exports": {
|
|
@@ -16,25 +16,25 @@
|
|
|
16
16
|
"path": "coming-soon-page/index.ts",
|
|
17
17
|
"type": "registry:index",
|
|
18
18
|
"target": "$modules$/coming-soon-page/index.ts",
|
|
19
|
-
"content": "export * from \"./coming-soon-page\";\
|
|
19
|
+
"content": "export * from \"./coming-soon-page\";\nexport { default } from \"./coming-soon-page\";\n"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "coming-soon-page/coming-soon-page.tsx",
|
|
23
23
|
"type": "registry:page",
|
|
24
24
|
"target": "$modules$/coming-soon-page/coming-soon-page.tsx",
|
|
25
|
-
"content": "import { useState, useEffect, useMemo } from \"react\";\
|
|
25
|
+
"content": "import { useState, useEffect, useMemo } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Mail, Twitter, Instagram, Github, Linkedin } from \"lucide-react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport { FadeIn, ScaleUp } from \"@/modules/animations\";\n\ninterface ComingSoonPageProps {\n launchDate?: Date;\n className?: string;\n}\n\ninterface TimeLeft {\n days: number;\n hours: number;\n minutes: number;\n seconds: number;\n}\n\n// Default launch date: 30 days from initial load\nconst DEFAULT_LAUNCH_DATE = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);\n\nexport function ComingSoonPage({\n launchDate: launchDateProp,\n className\n}: ComingSoonPageProps) {\n const { t } = useTranslation(\"coming-soon-page\");\n usePageTitle({ title: t(\"title\", \"Coming Soon\") });\n\n const launchDate = useMemo(() => launchDateProp ?? DEFAULT_LAUNCH_DATE, [launchDateProp]);\n\n const [email, setEmail] = useState(\"\");\n const [isSubmitted, setIsSubmitted] = useState(false);\n const [timeLeft, setTimeLeft] = useState<TimeLeft>({ days: 0, hours: 0, minutes: 0, seconds: 0 });\n\n useEffect(() => {\n const calculateTimeLeft = () => {\n const difference = launchDate.getTime() - new Date().getTime();\n\n if (difference > 0) {\n setTimeLeft({\n days: Math.floor(difference / (1000 * 60 * 60 * 24)),\n hours: Math.floor((difference / (1000 * 60 * 60)) % 24),\n minutes: Math.floor((difference / 1000 / 60) % 60),\n seconds: Math.floor((difference / 1000) % 60),\n });\n }\n };\n\n calculateTimeLeft();\n const timer = setInterval(calculateTimeLeft, 1000);\n return () => clearInterval(timer);\n }, [launchDate]);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n // Handle email submission\n setIsSubmitted(true);\n setEmail(\"\");\n };\n\n const socialLinks = [\n { icon: Twitter, href: \"#\", label: \"Twitter\" },\n { icon: Instagram, href: \"#\", label: \"Instagram\" },\n { icon: Github, href: \"#\", label: \"GitHub\" },\n { icon: Linkedin, href: \"#\", label: \"LinkedIn\" },\n ];\n\n const timeUnits = [\n { value: timeLeft.days, label: t(\"days\", \"Days\") },\n { value: timeLeft.hours, label: t(\"hours\", \"Hours\") },\n { value: timeLeft.minutes, label: t(\"minutes\", \"Minutes\") },\n { value: timeLeft.seconds, label: t(\"seconds\", \"Seconds\") },\n ];\n\n return (\n <div className={cn(\n \"min-h-screen flex flex-col items-center justify-center relative overflow-hidden\",\n \"bg-gradient-to-br from-background via-muted/50 to-background\",\n className\n )}>\n {/* Background decoration */}\n <div className=\"absolute inset-0 overflow-hidden pointer-events-none\">\n <div className=\"absolute -top-1/2 -left-1/2 w-full h-full bg-primary/5 rounded-full blur-3xl\" />\n <div className=\"absolute -bottom-1/2 -right-1/2 w-full h-full bg-primary/10 rounded-full blur-3xl\" />\n </div>\n\n <div className=\"relative z-10 w-full max-w-[var(--container-max-width)] mx-auto px-4 py-12 text-center\">\n {/* Heading */}\n <FadeIn className=\"mb-6\">\n <h1 className=\"text-4xl md:text-5xl lg:text-6xl font-bold text-foreground mb-4\">\n {t(\"heading\", \"Something Amazing is Coming\")}\n </h1>\n <p className=\"text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto\">\n {t(\"description\", \"We're working hard to bring you something special. Stay tuned and be the first to know when we launch.\")}\n </p>\n </FadeIn>\n\n {/* Countdown */}\n <ScaleUp delay={0.1} className=\"mb-12\">\n <div className=\"flex justify-center gap-4 md:gap-8\">\n {timeUnits.map((unit, index) => (\n <div key={index} className=\"flex flex-col items-center\">\n <div className=\"w-16 h-16 md:w-24 md:h-24 bg-card border border-border rounded-2xl flex items-center justify-center shadow-lg\">\n <span className=\"text-2xl md:text-4xl font-bold text-foreground\">\n {String(unit.value).padStart(2, \"0\")}\n </span>\n </div>\n <span className=\"mt-2 text-xs md:text-sm text-muted-foreground uppercase tracking-wide\">\n {unit.label}\n </span>\n </div>\n ))}\n </div>\n </ScaleUp>\n\n {/* Email signup */}\n <FadeIn delay={0.2} className=\"mb-12\">\n {!isSubmitted ? (\n <form onSubmit={handleSubmit} className=\"flex flex-col sm:flex-row gap-3 max-w-md mx-auto\">\n <div className=\"relative flex-1\">\n <Mail className=\"absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-muted-foreground\" />\n <Input\n type=\"email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder={t(\"emailPlaceholder\", \"Enter your email\")}\n required\n className=\"pl-10 h-12\"\n />\n </div>\n <Button type=\"submit\" size=\"lg\" className=\"h-12\">\n {t(\"notify\", \"Notify Me\")}\n </Button>\n </form>\n ) : (\n <div className=\"bg-primary/10 border border-primary/20 rounded-lg p-4 max-w-md mx-auto\">\n <p className=\"text-primary font-medium\">\n {t(\"success\", \"Thank you! We'll notify you when we launch.\")}\n </p>\n </div>\n )}\n </FadeIn>\n\n {/* Social links */}\n <FadeIn delay={0.3}>\n <p className=\"text-sm text-muted-foreground mb-4\">\n {t(\"followUs\", \"Follow us for updates\")}\n </p>\n <div className=\"flex justify-center gap-4\">\n {socialLinks.map((social, index) => (\n <a\n key={index}\n href={social.href}\n aria-label={social.label}\n className=\"w-10 h-10 rounded-full bg-muted hover:bg-primary/10 flex items-center justify-center transition-colors\"\n >\n <social.icon className=\"h-5 w-5 text-muted-foreground hover:text-primary transition-colors\" />\n </a>\n ))}\n </div>\n </FadeIn>\n </div>\n </div>\n );\n}\n\nexport default ComingSoonPage;\n"
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
"path": "coming-soon-page/lang/en.json",
|
|
29
29
|
"type": "registry:lang",
|
|
30
30
|
"target": "$modules$/coming-soon-page/lang/en.json",
|
|
31
|
-
"content": "{\
|
|
31
|
+
"content": "{\n \"title\": \"Coming Soon\",\n \"heading\": \"Something Amazing is Coming\",\n \"description\": \"Build anticipation for your launch. Explain what's coming and why it's exciting.\",\n \"days\": \"Days\",\n \"hours\": \"Hours\",\n \"minutes\": \"Minutes\",\n \"seconds\": \"Seconds\",\n \"emailPlaceholder\": \"Enter your email\",\n \"notify\": \"Notify Me\",\n \"success\": \"Thank you! We'll notify you when we launch.\",\n \"followUs\": \"Follow us for updates\"\n}"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"path": "coming-soon-page/lang/tr.json",
|
|
35
35
|
"type": "registry:lang",
|
|
36
36
|
"target": "$modules$/coming-soon-page/lang/tr.json",
|
|
37
|
-
"content": "{\
|
|
37
|
+
"content": "{\n \"title\": \"Yakında\",\n \"heading\": \"Harika Bir Şey Geliyor\",\n \"description\": \"Lansmanınız için heyecan yaratın. Nelerin geleceğini ve neden heyecan verici olduğunu anlatın.\",\n \"days\": \"Gün\",\n \"hours\": \"Saat\",\n \"minutes\": \"Dakika\",\n \"seconds\": \"Saniye\",\n \"emailPlaceholder\": \"E-posta adresiniz\",\n \"notify\": \"Beni Bilgilendir\",\n \"success\": \"Teşekkürler! Lansman yapıldığında sizi bilgilendireceğiz.\",\n \"followUs\": \"Güncellemeler için bizi takip edin\"\n}"
|
|
38
38
|
}
|
|
39
39
|
],
|
|
40
40
|
"exports": {
|
|
@@ -10,25 +10,25 @@
|
|
|
10
10
|
"path": "contact-info-grid/index.ts",
|
|
11
11
|
"type": "registry:index",
|
|
12
12
|
"target": "$modules$/contact-info-grid/index.ts",
|
|
13
|
-
"content": "export * from './contact-info-grid';\
|
|
13
|
+
"content": "export * from './contact-info-grid';\n"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"path": "contact-info-grid/contact-info-grid.tsx",
|
|
17
17
|
"type": "registry:component",
|
|
18
18
|
"target": "$modules$/contact-info-grid/contact-info-grid.tsx",
|
|
19
|
-
"content": "import { Mail, MapPin, MessageCircle, Phone } from \"lucide-react\";\
|
|
19
|
+
"content": "import { Mail, MapPin, MessageCircle, Phone } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\nimport { cn } from \"@/lib/utils\";\nimport constants from \"@/constants/constants.json\";\n\ninterface ContactInfoGridProps {\n className?: string;\n}\n\nexport function ContactInfoGrid({ className }: ContactInfoGridProps) {\n const { t } = useTranslation(\"contact-info-grid\");\n\n const contactItems = [\n {\n icon: Mail,\n label: t(\"emailLabel\", \"Email\"),\n description: t(\"emailDesc\", \"We respond to all emails within 24 hours.\"),\n value: constants.email || \"hello@example.com\",\n href: `mailto:${constants.email || \"hello@example.com\"}`,\n },\n {\n icon: MapPin,\n label: t(\"officeLabel\", \"Office\"),\n description: t(\"officeDesc\", \"Drop by our office for a chat.\"),\n value: `${constants.address?.line1 || \"\"}, ${constants.address?.city || \"\"}`,\n href: \"#\",\n },\n {\n icon: Phone,\n label: t(\"phoneLabel\", \"Phone\"),\n description: t(\"phoneDesc\", \"We're available Mon-Fri, 9am-5pm.\"),\n value: constants.phone || \"+1 234 567 890\",\n href: `tel:${constants.phone || \"+1234567890\"}`,\n },\n {\n icon: MessageCircle,\n label: t(\"chatLabel\", \"Live Chat\"),\n description: t(\"chatDesc\", \"Get instant help from our support team.\"),\n value: t(\"startChat\", \"Start Chat\"),\n href: \"#\",\n },\n ];\n\n return (\n <section className={cn(\"py-16 md:py-24\", className)}>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4\">\n <div className=\"mb-12\">\n <h2 className=\"text-3xl font-bold md:text-4xl mb-4\">\n {t(\"title\", \"Get in Touch\")}\n </h2>\n <p className=\"text-lg text-muted-foreground max-w-xl\">\n {t(\"subtitle\", \"Have questions? We'd love to hear from you. Choose your preferred way to reach us.\")}\n </p>\n </div>\n\n <div className=\"grid sm:grid-cols-2 lg:grid-cols-4 gap-6\">\n {contactItems.map((item, index) => (\n <div\n key={index}\n className=\"rounded-xl bg-muted/50 p-6 hover:bg-muted transition-colors\"\n >\n <span className=\"mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10\">\n <item.icon className=\"h-6 w-6 text-primary\" />\n </span>\n <h3 className=\"mb-2 text-lg font-semibold\">{item.label}</h3>\n <p className=\"mb-3 text-sm text-muted-foreground\">\n {item.description}\n </p>\n <a\n href={item.href}\n className=\"text-sm font-semibold text-primary hover:underline\"\n >\n {item.value}\n </a>\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "contact-info-grid/lang/en.json",
|
|
23
23
|
"type": "registry:lang",
|
|
24
24
|
"target": "$modules$/contact-info-grid/lang/en.json",
|
|
25
|
-
"content": "{\
|
|
25
|
+
"content": "{\n \"title\": \"Get in Touch\",\n \"subtitle\": \"This text introduces your contact information grid. Explain the different ways visitors can reach you and any important details about your contact preferences or availability. Use Promake to provide clear, helpful guidance for getting in touch.\",\n \"emailLabel\": \"Email\",\n \"emailDesc\": \"We respond to all emails within 24 hours.\",\n \"officeLabel\": \"Office\",\n \"officeDesc\": \"Drop by our office for a chat.\",\n \"phoneLabel\": \"Phone\",\n \"phoneDesc\": \"We're available Mon-Fri, 9am-5pm.\",\n \"chatLabel\": \"Live Chat\",\n \"chatDesc\": \"Get instant help from our support team.\",\n \"startChat\": \"Start Chat\"\n}\n"
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
"path": "contact-info-grid/lang/tr.json",
|
|
29
29
|
"type": "registry:lang",
|
|
30
30
|
"target": "$modules$/contact-info-grid/lang/tr.json",
|
|
31
|
-
"content": "{\
|
|
31
|
+
"content": "{\n \"title\": \"İletişime Geçin\",\n \"subtitle\": \"Bu metin iletişim bilgileri tablosunu tanıtır. Ziyaretçilerin size ulaşabilecekleri farklı yolları ve iletişim tercihleriniz veya müsaitliğinizle ilgili önemli detayları açıklayın. Promake ile iletişime geçmek için açık ve faydalı rehberlik sağlayın.\",\n \"emailLabel\": \"E-posta\",\n \"emailDesc\": \"Tüm e-postalara 24 saat içinde yanıt veriyoruz.\",\n \"officeLabel\": \"Ofis\",\n \"officeDesc\": \"Sohbet etmek için ofisimize uğrayın.\",\n \"phoneLabel\": \"Telefon\",\n \"phoneDesc\": \"Pazartesi-Cuma, 9:00-17:00 arası müsaitiz.\",\n \"chatLabel\": \"Canlı Sohbet\",\n \"chatDesc\": \"Destek ekibimizden anında yardım alın.\",\n \"startChat\": \"Sohbet Başlat\"\n}\n"
|
|
32
32
|
}
|
|
33
33
|
],
|
|
34
34
|
"exports": {
|
|
@@ -19,25 +19,25 @@
|
|
|
19
19
|
"path": "contact-page-centered/index.ts",
|
|
20
20
|
"type": "registry:index",
|
|
21
21
|
"target": "$modules$/contact-page-centered/index.ts",
|
|
22
|
-
"content": "export * from './contact-page-centered';\
|
|
22
|
+
"content": "export * from './contact-page-centered';\nexport { ContactPageCentered as default } from './contact-page-centered';\n"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "contact-page-centered/contact-page-centered.tsx",
|
|
26
26
|
"type": "registry:component",
|
|
27
27
|
"target": "$modules$/contact-page-centered/contact-page-centered.tsx",
|
|
28
|
-
"content": "import React, { useState } from \"react\";\
|
|
28
|
+
"content": "import React, { useState } from \"react\";\nimport { useTranslation } from \"react-i18next\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { Mail, Phone, MapPin } from \"lucide-react\";\nimport { Layout } from \"@/components/Layout\";\nimport { useApiService } from \"@/lib/api\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { cn } from \"@/lib/utils\";\nimport constants from \"@/constants/constants.json\";\nimport { FormFileInput } from \"@/components/FormFileInput\";\nimport { FormField } from \"@/components/FormField\";\n\ninterface ContactPageCenteredProps {\n className?: string;\n}\n\nexport function ContactPageCentered({ className }: ContactPageCenteredProps) {\n const { t } = useTranslation(\"contact-page-centered\");\n usePageTitle({ title: t(\"title\", \"Contact Us\") });\n const apiService = useApiService();\n const fileMaxFiles = constants.file?.maxFiles || 5;\n\n const [formData, setFormData] = useState({\n name: \"\",\n email: \"\",\n message: \"\",\n attachments: [] as File[]\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [submitStatus, setSubmitStatus] = useState<\"idle\" | \"success\" | \"error\">(\"idle\");\n\n const contactCards = [\n {\n icon: Mail,\n title: t(\"emailTitle\", \"Email\"),\n value: constants.email || \"hello@example.com\",\n href: `mailto:${constants.email || \"hello@example.com\"}`,\n },\n {\n icon: Phone,\n title: t(\"phoneTitle\", \"Phone\"),\n value: constants.phone || \"+1 234 567 890\",\n href: `tel:${constants.phone || \"+1234567890\"}`,\n },\n {\n icon: MapPin,\n title: t(\"addressTitle\", \"Address\"),\n value: constants.address?.city || \"New York, USA\",\n href: \"#\",\n },\n ];\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const selectedFiles = Array.from(e.target.files || []) as File[];\n\n const remainingSlots = fileMaxFiles - formData.attachments.length;\n\n // If the limit is exceeded, alert and do not add any files\n if (selectedFiles.length > remainingSlots) {\n alert(t(\"maxFilesLimit\", { max: fileMaxFiles }));\n e.target.value = ''; // Clear the input\n return;\n }\n\n setFormData(prev => ({\n ...prev,\n attachments: [...prev.attachments, ...selectedFiles]\n }));\n\n e.target.value = ''; // Clear the input\n }\n const handleRemoveFile = (index: number) => {\n setFormData(prev => ({\n ...prev,\n attachments: prev.attachments.filter((_, i) => i !== index)\n }));\n };\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setSubmitStatus(\"idle\");\n\n try {\n await apiService.submitFormWithFile(\n formData,\n {\n email_subject1: \"Thank you for contacting us\",\n email_subject2: \"New Contact Form Submission\",\n fields: [\n { name: \"name\", required: true },\n { name: \"email\", required: true },\n { name: \"message\", required: true },\n { name: \"attachments\", required: false },\n ],\n },\n constants.site.defaultLanguage\n );\n\n setSubmitStatus(\"success\");\n setFormData({ name: \"\", email: \"\", message: \"\", attachments: [] });\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\n } catch {\n setSubmitStatus(\"error\");\n setTimeout(() => setSubmitStatus(\"idle\"), 5000);\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));\n };\n\n return (\n <Layout>\n <div className={cn(\"min-h-screen bg-muted/30 py-16 md:py-24\", className)}>\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 max-w-4xl\">\n {/* Header */}\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold mb-4\">{t(\"title\", \"Contact Us\")}</h1>\n <p className=\"text-lg text-muted-foreground max-w-2xl mx-auto\">\n {t(\"subtitle\", \"We'd love to hear from you. Send us a message and we'll respond as soon as possible.\")}\n </p>\n </div>\n\n {/* Contact Cards */}\n <div className=\"grid sm:grid-cols-3 gap-4 mb-12\">\n {contactCards.map((card, index) => (\n <Card key={index} className=\"text-center\">\n <CardContent className=\"pt-6\">\n <div className=\"mx-auto w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mb-4\">\n <card.icon className=\"h-6 w-6 text-primary\" />\n </div>\n <h3 className=\"font-semibold mb-1\">{card.title}</h3>\n <a\n href={card.href}\n className=\"text-sm text-muted-foreground hover:text-primary transition-colors\"\n >\n {card.value}\n </a>\n </CardContent>\n </Card>\n ))}\n </div>\n\n {/* Form */}\n <Card>\n <CardContent className=\"pt-6\">\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n <div className=\"grid sm:grid-cols-2 gap-4\">\n <FormField label={t(\"nameLabel\", \"Name\")} htmlFor=\"name\" required>\n <Input\n id=\"name\"\n name=\"name\"\n value={formData.name}\n onChange={handleChange}\n placeholder={t(\"namePlaceholder\", \"Your name\")}\n required\n className=\"mt-1\"\n />\n </FormField>\n <FormField label={t(\"emailLabel\", \"Email\")} htmlFor=\"email\" required>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\", \"your@email.com\")}\n required\n className=\"mt-1\"\n />\n </FormField>\n </div>\n <FormField label={t(\"messageLabel\", \"Message\")} htmlFor=\"message\" required>\n <Textarea\n id=\"message\"\n name=\"message\"\n value={formData.message}\n onChange={handleChange}\n placeholder={t(\"messagePlaceholder\", \"How can we help you?\")}\n required\n rows={6}\n className=\"mt-1 resize-none\"\n />\n </FormField>\n <FormFileInput\n files={formData.attachments}\n onFilesChange={handleFileUploadChange}\n handleRemoveFile={handleRemoveFile}\n maxFiles={constants.file?.maxFiles || 5}\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\n disabled={isSubmitting}\n uploadButtonText={t(\"addFiles\")}\n maxFilesReachedText={t(\"maxFilesReached\")}\n />\n {submitStatus === \"success\" && (\n <div className=\"p-4 bg-green-500/10 border border-green-500/30 rounded-lg\">\n <p className=\"text-green-600 dark:text-green-400 text-sm font-medium\">\n {t(\"success\", \"Message sent successfully! We'll get back to you soon.\")}\n </p>\n </div>\n )}\n\n {submitStatus === \"error\" && (\n <div className=\"p-4 bg-destructive/10 border border-destructive/30 rounded-lg\">\n <p className=\"text-destructive text-sm font-medium\">\n {t(\"error\", \"Something went wrong. Please try again.\")}\n </p>\n </div>\n )}\n\n <Button type=\"submit\" size=\"lg\" className=\"w-full\" disabled={isSubmitting}>\n {isSubmitting ? t(\"sending\", \"Sending...\") : t(\"submit\", \"Send Message\")}\n </Button>\n </form>\n </CardContent>\n </Card>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ContactPageCentered;\n"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
"path": "contact-page-centered/lang/en.json",
|
|
32
32
|
"type": "registry:lang",
|
|
33
33
|
"target": "$modules$/contact-page-centered/lang/en.json",
|
|
34
|
-
"content": "{\
|
|
34
|
+
"content": "{\n \"title\": \"Contact Us\",\n \"subtitle\": \"This subtitle appears below your contact page heading. Use it to encourage visitors to reach out, explain your availability, or highlight your commitment to customer service. Customize with Promake to reflect your brand's communication style.\",\n \"emailTitle\": \"Email\",\n \"phoneTitle\": \"Phone\",\n \"addressTitle\": \"Address\",\n \"nameLabel\": \"Name\",\n \"namePlaceholder\": \"Your name\",\n \"emailLabel\": \"Email\",\n \"emailPlaceholder\": \"your@email.com\",\n \"messageLabel\": \"Message\",\n \"messagePlaceholder\": \"How can we help you?\",\n \"submit\": \"Send Message\",\n \"sending\": \"Sending...\",\n \"success\": \"Message sent successfully! We'll get back to you soon.\",\n \"error\": \"Something went wrong. Please try again.\",\n \"addFiles\": \"Add Files\",\n \"maxFilesReached\": \"Maximum files reached\",\n \"maxFilesLimit\": \"You can add up to {{max}} files\"\n}\n"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"path": "contact-page-centered/lang/tr.json",
|
|
38
38
|
"type": "registry:lang",
|
|
39
39
|
"target": "$modules$/contact-page-centered/lang/tr.json",
|
|
40
|
-
"content": "{\
|
|
40
|
+
"content": "{\n \"title\": \"İletişim\",\n \"subtitle\": \"Bu alt başlık iletişim sayfası başlığınızın altında görünür. Ziyaretçileri iletişime geçmeye teşvik etmek, müsaitliğinizi açıklamak veya müşteri hizmetlerine olan bağlılığınızı vurgulamak için kullanın. Promake ile markanızın iletişim tarzını yansıtacak şekilde özelleştirin.\",\n \"emailTitle\": \"E-posta\",\n \"phoneTitle\": \"Telefon\",\n \"addressTitle\": \"Adres\",\n \"nameLabel\": \"İsim\",\n \"namePlaceholder\": \"Adınız\",\n \"emailLabel\": \"E-posta\",\n \"emailPlaceholder\": \"email@adresiniz.com\",\n \"messageLabel\": \"Mesaj\",\n \"messagePlaceholder\": \"Size nasıl yardımcı olabiliriz?\",\n \"submit\": \"Mesaj Gönder\",\n \"sending\": \"Gönderiliyor...\",\n \"success\": \"Mesajınız başarıyla gönderildi! En kısa sürede size döneceğiz.\",\n \"error\": \"Bir şeyler yanlış gitti. Lütfen tekrar deneyin.\",\n \"addFiles\": \"Dosya Ekle\",\n \"maxFilesReached\": \"Maksimum dosya sayısına ulaşıldı\",\n \"maxFilesLimit\": \"En fazla {{max}} kadar dosya ekleyebilirsiniz\"\n}\n"
|
|
41
41
|
}
|
|
42
42
|
],
|
|
43
43
|
"exports": {
|
|
@@ -30,19 +30,19 @@
|
|
|
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 } 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"
|
|
33
|
+
"content": "import React, { useState, useMemo, useEffect } from \"react\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { Layout } from \"@/components/Layout\";\nimport { useApiService } from \"@/lib/api\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Mail,\n Phone,\n MapPin,\n Facebook,\n Twitter,\n Instagram,\n Linkedin,\n Send,\n ExternalLink,\n} from \"lucide-react\";\nimport constants from \"@/constants/constants.json\";\nimport { GoogleMap } from \"@/modules/google-map\";\nimport { FormFileInput } from \"@/components/FormFileInput\";\nimport { FormField } from \"@/components/FormField\";\n\nconst socialIcons: Record<string, React.ElementType> = {\n facebook: Facebook,\n twitter: Twitter,\n instagram: Instagram,\n linkedin: Linkedin,\n};\n\nexport function ContactPageMapOverlay() {\n const { t } = useTranslation(\"contact-page-map-overlay\");\n usePageTitle({ title: t(\"title\") });\n\n const apiService = useApiService();\n\n const socialLinks = useMemo(() => {\n const socialMedia = constants.socialMedia as\n | Record<string, string>\n | undefined;\n if (!socialMedia) return [];\n return Object.entries(socialMedia)\n .filter(([platform, url]) => url && socialIcons[platform])\n .map(([platform, url]) => ({\n platform,\n url,\n Icon: socialIcons[platform],\n }));\n }, []);\n\n const [formData, setFormData] = useState({\n name: \"\",\n email: \"\",\n message: \"\",\n attachments: [] as File[]\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [submitStatus, setSubmitStatus] = useState<\n \"idle\" | \"success\" | \"error\"\n >(\"idle\");\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const selectedFiles = Array.from(e.target.files || []) as File[];\n const maxFiles = constants.file?.maxFiles || 5;\n\n const remainingSlots = maxFiles - formData.attachments.length;\n\n // If the limit is exceeded, alert and do not add any files\n if (selectedFiles.length > remainingSlots) {\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\n e.target.value = ''; // Clear the input\n return;\n }\n\n setFormData(prev => ({\n ...prev,\n attachments: [...prev.attachments, ...selectedFiles]\n }));\n\n e.target.value = ''; // Clear the input\n }\n\n const handleRemoveFile = (index: number) => {\n setFormData(prev => ({\n ...prev,\n attachments: prev.attachments.filter((_, i) => i !== index)\n }));\n };\n\n // Auto-reset status after 5 seconds with proper cleanup\n useEffect(() => {\n if (submitStatus === \"idle\") return;\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\n return () => clearTimeout(timer);\n }, [submitStatus]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setSubmitStatus(\"idle\");\n try {\n const currentLanguage = constants.site.defaultLanguage;\n\n await apiService.submitFormWithFile(\n formData,\n {\n email_subject1: \"Thank you for contacting us\",\n email_subject2: \"New Contact Form Submission\",\n fields: [\n { name: \"name\", required: true },\n { name: \"email\", required: true },\n { name: \"message\", required: true },\n { name: \"attachments\", required: false },\n ],\n },\n currentLanguage\n );\n\n setSubmitStatus(\"success\");\n setFormData({\n name: \"\",\n email: \"\",\n message: \"\",\n attachments: []\n });\n } catch (error: unknown) {\n console.error(\"Form submission failed:\", error);\n setSubmitStatus(\"error\");\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n // Default coordinates (can be customized via constants)\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\n\n return (\n <Layout>\n <div className=\"relative min-h-[calc(100vh-4rem)]\">\n {/* Full-screen Map Background */}\n <div className=\"absolute inset-0\">\n <GoogleMap\n latitude={mapLatitude}\n longitude={mapLongitude}\n zoom={14}\n height=\"100%\"\n className=\"rounded-none border-0 h-full\"\n title={t(\"mapTitle\")}\n />\n {/* Dark overlay for better readability */}\n <div className=\"absolute inset-0 bg-black/30 pointer-events-none\" />\n </div>\n\n {/* Content Overlay */}\n <div className=\"relative z-10 min-h-[calc(100vh-4rem)] flex items-center py-12 px-4\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto\">\n <div className=\"grid lg:grid-cols-2 gap-8 items-stretch\">\n {/* Form Card - Glassmorphism */}\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\n <CardHeader>\n <CardTitle className=\"text-2xl lg:text-3xl\">\n {t(\"title\")}\n </CardTitle>\n <p className=\"text-muted-foreground mt-2\">\n {t(\"description\")}\n </p>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <FormField label={t(\"fullName\")} htmlFor=\"name\" required>\n <Input\n id=\"name\"\n name=\"name\"\n type=\"text\"\n value={formData.name}\n onChange={handleChange}\n placeholder={t(\"fullNamePlaceholder\")}\n required\n className=\"mt-1.5 bg-background/50\"\n />\n </FormField>\n <FormField label={t(\"emailAddress\")} htmlFor=\"email\" required>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1.5 bg-background/50\"\n />\n </FormField>\n </div>\n\n <FormField label={t(\"message\")} htmlFor=\"message\" required>\n <Textarea\n id=\"message\"\n name=\"message\"\n value={formData.message}\n onChange={handleChange}\n placeholder={t(\"messagePlaceholder\")}\n required\n rows={4}\n className=\"mt-1.5 resize-none bg-background/50\"\n />\n </FormField>\n <FormFileInput\n files={formData.attachments}\n onFilesChange={handleFileUploadChange}\n handleRemoveFile={handleRemoveFile}\n maxFiles={constants.file?.maxFiles || 5}\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\n disabled={isSubmitting}\n uploadButtonText={t(\"addFiles\")}\n maxFilesReachedText={t(\"maxFilesReached\")}\n />\n {submitStatus === \"success\" && (\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\n {t(\"success\")}\n </p>\n </div>\n )}\n\n {submitStatus === \"error\" && (\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"sending\")}\n </>\n ) : (\n <>\n <Send className=\"w-4 h-4 mr-2\" />\n {t(\"submit\")}\n </>\n )}\n </Button>\n </form>\n </CardContent>\n </Card>\n\n {/* Contact Info Card - Glassmorphism */}\n <Card className=\"backdrop-blur-xl bg-background/85 dark:bg-background/90 border-white/20 shadow-2xl\">\n <CardHeader>\n <CardTitle>{t(\"contactInfo\")}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <Mail className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"email\")}</p>\n <a\n href={`mailto:${constants.email}`}\n className=\"font-medium hover:text-primary transition-colors\"\n >\n {constants.email}\n </a>\n </div>\n </div>\n\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <Phone className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"phone\")}</p>\n <a\n href={`tel:${constants.phone}`}\n className=\"font-medium hover:text-primary transition-colors\"\n >\n {constants.phone}\n </a>\n </div>\n </div>\n\n <div className=\"flex items-start gap-4\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <MapPin className=\"w-5 h-5 text-primary\" />\n </div>\n <div>\n <p className=\"text-sm text-muted-foreground\">{t(\"address\")}</p>\n <p className=\"font-medium\">\n {constants.address.line1}\n <br />\n {constants.address.city}, {constants.address.state} {constants.address.postalCode}\n </p>\n </div>\n </div>\n\n {/* Open in Maps Link */}\n <a\n href={`https://www.google.com/maps?q=${mapLatitude},${mapLongitude}`}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex items-center gap-2 text-sm text-primary hover:underline mt-2\"\n >\n <ExternalLink className=\"w-4 h-4\" />\n {t(\"openInMaps\")}\n </a>\n\n {/* Social Links */}\n {socialLinks.length > 0 && (\n <div className=\"pt-4 border-t\">\n <p className=\"text-sm text-muted-foreground mb-3\">\n {t(\"followUs\")}\n </p>\n <div className=\"flex gap-2\">\n {socialLinks.map(({ platform, url, Icon }) => (\n <a\n key={platform}\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\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\"\n >\n <Icon className=\"h-4 w-4\" />\n </a>\n ))}\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ContactPageMapOverlay;\n"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"path": "contact-page-map-overlay/lang/en.json",
|
|
37
37
|
"type": "registry:lang",
|
|
38
38
|
"target": "$modules$/contact-page-map-overlay/lang/en.json",
|
|
39
|
-
"content": "{\
|
|
39
|
+
"content": "{\n \"title\": \"Contact Us\",\n \"description\": \"We'd love to hear from you. Send us a message and we'll respond as soon as possible.\",\n \"contactInfo\": \"Contact Information\",\n \"email\": \"Email\",\n \"phone\": \"Phone\",\n \"address\": \"Address\",\n \"fullName\": \"Full Name\",\n \"fullNamePlaceholder\": \"Your name\",\n \"emailAddress\": \"Email Address\",\n \"emailPlaceholder\": \"your@email.com\",\n \"message\": \"Message\",\n \"messagePlaceholder\": \"How can we help you?\",\n \"submit\": \"Send Message\",\n \"sending\": \"Sending...\",\n \"success\": \"Message sent successfully! We'll get back to you soon.\",\n \"error\": \"Something went wrong. Please try again.\",\n \"followUs\": \"Follow us\",\n \"mapTitle\": \"Our Location\",\n \"openInMaps\": \"Open in Google Maps\",\n \"addFiles\": \"Add Files\",\n \"maxFilesReached\": \"Maximum files reached\",\n \"maxFilesLimit\": \"You can add up to {{max}} files\"\n}\n"
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"path": "contact-page-map-overlay/lang/tr.json",
|
|
43
43
|
"type": "registry:lang",
|
|
44
44
|
"target": "$modules$/contact-page-map-overlay/lang/tr.json",
|
|
45
|
-
"content": "{\
|
|
45
|
+
"content": "{\n \"title\": \"Bize Ulaşın\",\n \"description\": \"Sizden haber almak isteriz. Bize mesaj gönderin, en kısa sürede yanıtlayacağız.\",\n \"contactInfo\": \"İletişim Bilgileri\",\n \"email\": \"E-posta\",\n \"phone\": \"Telefon\",\n \"address\": \"Adres\",\n \"fullName\": \"Ad Soyad\",\n \"fullNamePlaceholder\": \"Adınız\",\n \"emailAddress\": \"E-posta Adresi\",\n \"emailPlaceholder\": \"email@adresiniz.com\",\n \"message\": \"Mesaj\",\n \"messagePlaceholder\": \"Size nasıl yardımcı olabiliriz?\",\n \"submit\": \"Mesaj Gönder\",\n \"sending\": \"Gönderiliyor...\",\n \"success\": \"Mesaj başarıyla gönderildi! En kısa sürede yanıtlayacağız.\",\n \"error\": \"Bir hata oluştu. Lütfen tekrar deneyin.\",\n \"followUs\": \"Bizi takip edin\",\n \"mapTitle\": \"Konumumuz\",\n \"openInMaps\": \"Google Maps'te Aç\",\n \"addFiles\": \"Dosya Ekle\",\n \"maxFilesReached\": \"Maksimum dosya sayısına ulaşıldı\",\n \"maxFilesLimit\": \"En fazla {{max}} kadar dosya ekleyebilirsiniz\"\n}\n"
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
48
|
"exports": {
|
|
@@ -30,19 +30,19 @@
|
|
|
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 } 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"
|
|
33
|
+
"content": "import React, { useState, useMemo, useEffect } from \"react\";\nimport { usePageTitle } from \"@/hooks/use-page-title\";\nimport { useTranslation } from \"react-i18next\";\nimport { Layout } from \"@/components/Layout\";\nimport { useApiService } from \"@/lib/api\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { \n Mail,\n Phone,\n MapPin,\n Facebook,\n Twitter,\n Instagram,\n Linkedin,\n Send,\n} from \"lucide-react\";\nimport constants from \"@/constants/constants.json\";\nimport { GoogleMap } from \"@/modules/google-map\";\nimport { FormFileInput } from \"@/components/FormFileInput\";\nimport { FormField } from \"@/components/FormField\";\n\nconst socialIcons: Record<string, React.ElementType> = {\n facebook: Facebook,\n twitter: Twitter,\n instagram: Instagram,\n linkedin: Linkedin,\n};\n\nexport function ContactPageMapSplit() {\n const { t } = useTranslation(\"contact-page-map-split\");\n usePageTitle({ title: t(\"title\") });\n\n const apiService = useApiService();\n\n const socialLinks = useMemo(() => {\n const socialMedia = constants.socialMedia as\n | Record<string, string>\n | undefined;\n if (!socialMedia) return [];\n return Object.entries(socialMedia)\n .filter(([platform, url]) => url && socialIcons[platform])\n .map(([platform, url]) => ({\n platform,\n url,\n Icon: socialIcons[platform],\n }));\n }, []);\n\n const [formData, setFormData] = useState({\n name: \"\",\n email: \"\",\n phone: \"\",\n message: \"\",\n attachments: [] as File[]\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [submitStatus, setSubmitStatus] = useState<\n \"idle\" | \"success\" | \"error\"\n >(\"idle\");\n\n\n const handleFileUploadChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const selectedFiles = Array.from(e.target.files || []) as File[];\n const maxFiles = constants.file?.maxFiles || 5;\n\n const remainingSlots = maxFiles - formData.attachments.length;\n\n // If the limit is exceeded, alert and do not add any files\n if (selectedFiles.length > remainingSlots) {\n alert(t(\"maxFilesLimit\", { max: maxFiles }));\n e.target.value = ''; // Clear the input\n return;\n }\n\n setFormData(prev => ({\n ...prev,\n attachments: [...prev.attachments, ...selectedFiles]\n }));\n\n e.target.value = ''; // Clear the input\n }\n\n const handleRemoveFile = (index: number) => {\n setFormData(prev => ({\n ...prev,\n attachments: prev.attachments.filter((_, i) => i !== index)\n }));\n };\n\n // Auto-reset status after 5 seconds with proper cleanup\n useEffect(() => {\n if (submitStatus === \"idle\") return;\n const timer = setTimeout(() => setSubmitStatus(\"idle\"), 5000);\n return () => clearTimeout(timer);\n }, [submitStatus]);\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setIsSubmitting(true);\n setSubmitStatus(\"idle\");\n\n try {\n const currentLanguage = constants.site.defaultLanguage;\n\n await apiService.submitFormWithFile(\n formData,\n {\n email_subject1: \"Thank you for contacting us\",\n email_subject2: \"New Contact Form Submission\",\n fields: [\n { name: \"name\", required: true },\n { name: \"email\", required: true },\n { name: \"phone\", required: false },\n { name: \"message\", required: true },\n { name: \"attachments\", required: false },\n ],\n },\n currentLanguage\n );\n\n setSubmitStatus(\"success\");\n setFormData({\n name: \"\",\n email: \"\",\n phone: \"\",\n message: \"\",\n attachments: []\n });\n } catch (error: unknown) {\n console.error(\"Form submission failed:\", error);\n setSubmitStatus(\"error\");\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (\n e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>\n ) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n // Default coordinates (can be customized via constants)\n const mapLatitude = (constants as any).location?.latitude || 41.0082;\n const mapLongitude = (constants as any).location?.longitude || 28.9784;\n\n return (\n <Layout>\n <div className=\"min-h-screen flex flex-col lg:flex-row\">\n {/* Left Side - Form & Info */}\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\">\n <div className=\"max-w-lg mx-auto w-full\">\n {/* Header */}\n <div className=\"mb-10\">\n <h1 className=\"text-3xl lg:text-4xl font-bold text-foreground mb-3\">\n {t(\"title\")}\n </h1>\n <p className=\"text-muted-foreground\">\n {t(\"description\")}\n </p>\n </div>\n\n {/* Contact Info Cards */}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4 mb-10\">\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\n <Mail className=\"w-5 h-5 text-primary\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-muted-foreground\">{t(\"email\")}</p>\n <p className=\"text-sm font-medium truncate\">{constants.email}</p>\n </div>\n </div>\n\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center\">\n <Phone className=\"w-5 h-5 text-primary\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-muted-foreground\">{t(\"phone\")}</p>\n <p className=\"text-sm font-medium truncate\">{constants.phone}</p>\n </div>\n </div>\n\n <div className=\"flex items-center gap-3 p-4 rounded-xl bg-muted/50 sm:col-span-2\">\n <div className=\"w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0\">\n <MapPin className=\"w-5 h-5 text-primary\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs text-muted-foreground\">{t(\"address\")}</p>\n <p className=\"text-sm font-medium\">\n {constants.address.line1}, {constants.address.city}\n </p>\n </div>\n </div>\n </div>\n\n {/* Contact Form */}\n <form onSubmit={handleSubmit} className=\"space-y-5\">\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4\">\n <FormField label={t(\"fullName\")} htmlFor=\"name\" required>\n <Input\n id=\"name\"\n name=\"name\"\n type=\"text\"\n value={formData.name}\n onChange={handleChange}\n placeholder={t(\"fullNamePlaceholder\")}\n required\n className=\"mt-1.5\"\n />\n </FormField>\n <FormField label={t(\"emailAddress\")} htmlFor=\"email\" required>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1.5\"\n />\n </FormField>\n </div>\n\n <FormField label={t(\"phoneNumber\")} htmlFor=\"phone\">\n <Input\n id=\"phone\"\n name=\"phone\"\n type=\"tel\"\n value={formData.phone}\n onChange={handleChange}\n placeholder={t(\"phonePlaceholder\")}\n className=\"mt-1.5\"\n />\n </FormField>\n <FormField label={t(\"message\")} htmlFor=\"message\" required>\n <Textarea\n id=\"message\"\n name=\"message\"\n value={formData.message}\n onChange={handleChange}\n placeholder={t(\"messagePlaceholder\")}\n required\n rows={4}\n className=\"mt-1.5 resize-none\"\n />\n </FormField>\n <FormFileInput\n files={formData.attachments}\n onFilesChange={handleFileUploadChange}\n handleRemoveFile={handleRemoveFile}\n maxFiles={constants.file?.maxFiles || 5}\n accept={constants.file?.accept || \".pdf,.doc,.docx,.jpg,.jpeg,.png\"}\n disabled={isSubmitting}\n uploadButtonText={t(\"addFiles\")}\n maxFilesReachedText={t(\"maxFilesReached\")}\n />\n {submitStatus === \"success\" && (\n <div className=\"p-3 bg-green-500/10 border border-green-500/30 rounded-lg\">\n <p className=\"text-green-600 dark:text-green-400 text-sm\">\n {t(\"success\")}\n </p>\n </div>\n )}\n\n {submitStatus === \"error\" && (\n <div className=\"p-3 bg-destructive/10 border border-destructive/30 rounded-lg\">\n <p className=\"text-destructive text-sm\">{t(\"error\")}</p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"sending\")}\n </>\n ) : (\n <>\n <Send className=\"w-4 h-4 mr-2\" />\n {t(\"submit\")}\n </>\n )}\n </Button>\n </form>\n\n {/* Social Links */}\n {socialLinks.length > 0 && (\n <div className=\"mt-10 pt-6 border-t\">\n <p className=\"text-sm text-muted-foreground mb-3\">\n {t(\"followUs\")}\n </p>\n <div className=\"flex gap-2\">\n {socialLinks.map(({ platform, url, Icon }) => (\n <a\n key={platform}\n href={url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\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\"\n >\n <Icon className=\"h-4 w-4\" />\n </a>\n ))}\n </div>\n </div>\n )}\n </div>\n </div>\n\n {/* Right Side - Map */}\n <div className=\"w-full lg:w-1/2 h-[400px] lg:h-[calc(100vh-4rem)] relative\">\n <GoogleMap\n latitude={mapLatitude}\n longitude={mapLongitude}\n zoom={14}\n height=\"100%\"\n className=\"rounded-none border-0 h-full\"\n title={t(\"mapTitle\")}\n />\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ContactPageMapSplit;\n"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"path": "contact-page-map-split/lang/en.json",
|
|
37
37
|
"type": "registry:lang",
|
|
38
38
|
"target": "$modules$/contact-page-map-split/lang/en.json",
|
|
39
|
-
"content": "{\
|
|
39
|
+
"content": "{\n \"title\": \"Get in Touch\",\n \"description\": \"Have a question or want to work together? Fill out the form below and we'll get back to you as soon as possible.\",\n \"email\": \"Email\",\n \"phone\": \"Phone\",\n \"address\": \"Address\",\n \"fullName\": \"Full Name\",\n \"fullNamePlaceholder\": \"Your name\",\n \"emailAddress\": \"Email Address\",\n \"emailPlaceholder\": \"your@email.com\",\n \"phoneNumber\": \"Phone Number\",\n \"phonePlaceholder\": \"+1 (555) 000-0000\",\n \"message\": \"Message\",\n \"messagePlaceholder\": \"How can we help you?\",\n \"submit\": \"Send Message\",\n \"sending\": \"Sending...\",\n \"success\": \"Your message has been sent successfully! We'll get back to you soon.\",\n \"error\": \"Something went wrong. Please try again later.\",\n \"followUs\": \"Follow us on social media\",\n \"mapTitle\": \"Our Location\",\n \"addFiles\": \"Add Files\",\n \"maxFilesReached\": \"Maximum files reached\",\n \"maxFilesLimit\": \"You can add up to {{max}} files\"\n}\n"
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"path": "contact-page-map-split/lang/tr.json",
|
|
43
43
|
"type": "registry:lang",
|
|
44
44
|
"target": "$modules$/contact-page-map-split/lang/tr.json",
|
|
45
|
-
"content": "{\
|
|
45
|
+
"content": "{\n \"title\": \"Bize Ulaşın\",\n \"description\": \"Sorunuz mu var veya birlikte çalışmak mı istiyorsunuz? Aşağıdaki formu doldurun, en kısa sürede size döneceğiz.\",\n \"email\": \"E-posta\",\n \"phone\": \"Telefon\",\n \"address\": \"Adres\",\n \"fullName\": \"Ad Soyad\",\n \"fullNamePlaceholder\": \"Adınız\",\n \"emailAddress\": \"E-posta Adresi\",\n \"emailPlaceholder\": \"email@adresiniz.com\",\n \"phoneNumber\": \"Telefon Numarası\",\n \"phonePlaceholder\": \"+90 (555) 000 00 00\",\n \"message\": \"Mesaj\",\n \"messagePlaceholder\": \"Size nasıl yardımcı olabiliriz?\",\n \"submit\": \"Mesaj Gönder\",\n \"sending\": \"Gönderiliyor...\",\n \"success\": \"Mesajınız başarıyla gönderildi! En kısa sürede size döneceğiz.\",\n \"error\": \"Bir hata oluştu. Lütfen daha sonra tekrar deneyin.\",\n \"followUs\": \"Sosyal medyada bizi takip edin\",\n \"mapTitle\": \"Konumumuz\",\n \"addFiles\": \"Dosya Ekle\",\n \"maxFilesReached\": \"Maksimum dosya sayısına ulaşıldı\",\n \"maxFilesLimit\": \"En fazla {{max}} kadar dosya ekleyebilirsiniz\"\n}\n"
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
48
|
"exports": {
|