@promakeai/cli 0.0.6 → 0.1.0
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/dist/index.js +2 -2
- package/dist/registry/about-page.json +4 -2
- package/dist/registry/about-section.json +2 -2
- package/dist/registry/announcement-bar.json +43 -0
- package/dist/registry/auth-core.json +43 -0
- package/dist/registry/auth.json +1 -1
- package/dist/registry/blog-list-page.json +3 -2
- package/dist/registry/blog-section.json +1 -1
- package/dist/registry/cart-page.json +3 -3
- package/dist/registry/case-study-page.json +48 -0
- package/dist/registry/checkout-page.json +6 -5
- package/dist/registry/coming-soon-page-minimal.json +45 -0
- package/dist/registry/coming-soon-page.json +47 -0
- package/dist/registry/contact-info-grid.json +1 -1
- package/dist/registry/contact-page-centered.json +2 -2
- package/dist/registry/contact-page-map-overlay.json +4 -3
- package/dist/registry/contact-page-map-split.json +4 -3
- package/dist/registry/contact-page-split.json +3 -3
- package/dist/registry/contact-page.json +5 -3
- package/dist/registry/cookie-consent.json +43 -0
- package/dist/registry/cookies-page.json +3 -1
- package/dist/registry/cta-section.json +1 -1
- package/dist/registry/db.json +1 -1
- package/dist/registry/docs/about-page.md +5 -0
- package/dist/registry/docs/announcement-bar.md +38 -0
- package/dist/registry/docs/auth-core.md +64 -0
- package/dist/registry/docs/blog-list-page.md +1 -0
- package/dist/registry/docs/case-study-page.md +39 -0
- package/dist/registry/docs/checkout-page.md +2 -1
- package/dist/registry/docs/coming-soon-page-minimal.md +32 -0
- package/dist/registry/docs/coming-soon-page.md +37 -0
- package/dist/registry/docs/contact-page-map-overlay.md +2 -2
- package/dist/registry/docs/contact-page-map-split.md +2 -2
- package/dist/registry/docs/contact-page.md +5 -0
- package/dist/registry/docs/cookie-consent.md +37 -0
- package/dist/registry/docs/cookies-page.md +5 -0
- package/dist/registry/docs/ecommerce-core.md +4 -3
- package/dist/registry/docs/forgot-password-page-split.md +45 -0
- package/dist/registry/docs/forgot-password-page.md +14 -5
- package/dist/registry/docs/header-ecommerce.md +1 -0
- package/dist/registry/docs/hero-carousel.md +37 -0
- package/dist/registry/docs/landing-page-app.md +43 -0
- package/dist/registry/docs/landing-page-saas.md +41 -0
- package/dist/registry/docs/login-page-split.md +13 -4
- package/dist/registry/docs/login-page.md +17 -4
- package/dist/registry/docs/logo-cloud.md +33 -0
- package/dist/registry/docs/masonry-grid.md +37 -0
- package/dist/registry/docs/my-orders-page.md +44 -0
- package/dist/registry/docs/order-confirmation-page.md +41 -0
- package/dist/registry/docs/portfolio-page.md +38 -0
- package/dist/registry/docs/pricing-page.md +38 -0
- package/dist/registry/docs/privacy-page.md +5 -0
- package/dist/registry/docs/product-quick-view.md +37 -0
- package/dist/registry/docs/reading-progress.md +31 -0
- package/dist/registry/docs/register-page-split.md +45 -0
- package/dist/registry/docs/register-page.md +14 -7
- package/dist/registry/docs/reset-password-page-split.md +45 -0
- package/dist/registry/docs/reset-password-page.md +36 -0
- package/dist/registry/docs/share-buttons.md +37 -0
- package/dist/registry/docs/team-page.md +38 -0
- package/dist/registry/docs/terms-page.md +5 -0
- package/dist/registry/docs/timeline-section.md +37 -0
- package/dist/registry/docs/video-hero.md +41 -0
- package/dist/registry/ecommerce-core.json +17 -1
- package/dist/registry/empty-page.json +1 -1
- package/dist/registry/faq-categorized.json +1 -1
- package/dist/registry/faq-simple.json +1 -1
- package/dist/registry/favorites-ecommerce-block.json +1 -1
- package/dist/registry/feature-section.json +2 -2
- package/dist/registry/footer.json +1 -1
- package/dist/registry/forgot-password-page-split.json +50 -0
- package/dist/registry/forgot-password-page.json +9 -7
- package/dist/registry/header-ecommerce.json +3 -2
- package/dist/registry/header-mega.json +1 -1
- package/dist/registry/hero-carousel.json +45 -0
- package/dist/registry/hero-cta.json +2 -2
- package/dist/registry/hero-gradient.json +1 -1
- package/dist/registry/hero.json +1 -1
- package/dist/registry/index.json +22 -2
- package/dist/registry/landing-page-app.json +47 -0
- package/dist/registry/landing-page-saas.json +47 -0
- package/dist/registry/login-page-split.json +11 -7
- package/dist/registry/login-page.json +4 -4
- package/dist/registry/logo-cloud.json +41 -0
- package/dist/registry/masonry-grid.json +43 -0
- package/dist/registry/my-orders-page.json +52 -0
- package/dist/registry/order-confirmation-page.json +49 -0
- package/dist/registry/portfolio-page.json +45 -0
- package/dist/registry/pricing-page.json +47 -0
- package/dist/registry/pricing-section.json +1 -1
- package/dist/registry/privacy-page.json +3 -1
- package/dist/registry/product-detail-block.json +1 -1
- package/dist/registry/product-quick-view.json +46 -0
- package/dist/registry/products-page.json +3 -3
- package/dist/registry/reading-progress.json +43 -0
- package/dist/registry/register-page-split.json +50 -0
- package/dist/registry/register-page.json +9 -7
- package/dist/registry/reset-password-page-split.json +50 -0
- package/dist/registry/reset-password-page.json +39 -0
- package/dist/registry/share-buttons.json +46 -0
- package/dist/registry/team-page.json +47 -0
- package/dist/registry/terms-page.json +3 -1
- package/dist/registry/testimonials-carousel.json +1 -1
- package/dist/registry/testimonials-grid.json +1 -1
- package/dist/registry/timeline-section.json +43 -0
- package/dist/registry/video-hero.json +42 -0
- package/package.json +1 -1
- package/template/index.html +5 -5
- package/template/src/App.tsx +4 -0
- package/template/src/components/GoogleAnalytics.tsx +34 -0
- package/template/src/components/Layout.tsx +1 -1
- package/template/src/components/ScriptInjector.tsx +62 -0
- package/template/src/constants/constants.json +8 -2
- package/template/src/pages/Index.tsx +5 -1
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
"path": "contact-page-map-split/index.ts",
|
|
25
25
|
"type": "registry:index",
|
|
26
26
|
"target": "$modules$/contact-page-map-split/index.ts",
|
|
27
|
-
"content": "export * from \"./contact-page-map-split\";\n"
|
|
27
|
+
"content": "export * from \"./contact-page-map-split\";\nexport { default } from \"./contact-page-map-split\";\n"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
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\";\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 { Label } from \"@/components/ui/label\";\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\";\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 });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [submitStatus, setSubmitStatus] = useState<\n \"idle\" | \"success\" | \"error\"\n >(\"idle\");\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.submitForm(\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 ],\n },\n currentLanguage\n );\n\n setSubmitStatus(\"success\");\n setFormData({\n name: \"\",\n email: \"\",\n phone: \"\",\n message: \"\",\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 <div>\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\n {t(\"fullName\")} *\n </Label>\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 </div>\n <div>\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\n {t(\"emailAddress\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1.5\"\n />\n </div>\n </div>\n\n <div>\n <Label htmlFor=\"phone\" className=\"text-sm font-medium\">\n {t(\"phoneNumber\")}\n </Label>\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 </div>\n\n <div>\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\n {t(\"message\")} *\n </Label>\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 </div>\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-auto lg:min-h-screen 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"
|
|
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 { Label } from \"@/components/ui/label\";\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\";\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 });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [submitStatus, setSubmitStatus] = useState<\n \"idle\" | \"success\" | \"error\"\n >(\"idle\");\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.submitForm(\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 ],\n },\n currentLanguage\n );\n\n setSubmitStatus(\"success\");\n setFormData({\n name: \"\",\n email: \"\",\n phone: \"\",\n message: \"\",\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 <div>\n <Label htmlFor=\"name\" className=\"text-sm font-medium\">\n {t(\"fullName\")} *\n </Label>\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 </div>\n <div>\n <Label htmlFor=\"email\" className=\"text-sm font-medium\">\n {t(\"emailAddress\")} *\n </Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1.5\"\n />\n </div>\n </div>\n\n <div>\n <Label htmlFor=\"phone\" className=\"text-sm font-medium\">\n {t(\"phoneNumber\")}\n </Label>\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 </div>\n\n <div>\n <Label htmlFor=\"message\" className=\"text-sm font-medium\">\n {t(\"message\")} *\n </Label>\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 </div>\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",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"exports": {
|
|
49
49
|
"types": [],
|
|
50
50
|
"variables": [
|
|
51
|
-
"ContactPageMapSplit"
|
|
51
|
+
"ContactPageMapSplit",
|
|
52
|
+
"default"
|
|
52
53
|
]
|
|
53
54
|
}
|
|
54
55
|
}
|
|
@@ -24,19 +24,19 @@
|
|
|
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, { useState } from \"react\";\r\nimport { useTranslation } from \"react-i18next\";\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\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 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 });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\"idle\" | \"success\" | \"error\">(\"idle\");\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.submitForm(\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 ],\r\n },\r\n constants.site.defaultLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({ name: \"\", email: \"\", phone: \"\", message: \"\" });\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-screen\", className)}>\r\n <div className=\"grid lg:grid-cols-2 min-h-screen\">\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\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 </Layout>\r\n );\r\n}\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 { Label } from \"@/components/ui/label\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport constants from \"@/constants/constants.json\";\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 });\r\n const [isSubmitting, setIsSubmitting] = useState(false);\r\n const [submitStatus, setSubmitStatus] = useState<\"idle\" | \"success\" | \"error\">(\"idle\");\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.submitForm(\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 ],\r\n },\r\n constants.site.defaultLanguage\r\n );\r\n\r\n setSubmitStatus(\"success\");\r\n setFormData({ name: \"\", email: \"\", phone: \"\", message: \"\" });\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\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",
|
|
31
31
|
"type": "registry:lang",
|
|
32
32
|
"target": "$modules$/contact-page-split/lang/en.json",
|
|
33
|
-
"content": "{\r\n \"title\": \"Let's Start a Conversation\",\r\n \"subtitle\": \"
|
|
33
|
+
"content": "{\r\n \"pageTitle\": \"Contact Us\",\r\n \"title\": \"Let's Start a Conversation\",\r\n \"subtitle\": \"Ask Promake to customize this subtitle based on your contact page purpose. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\",\r\n \"emailLabel\": \"Email\",\r\n \"phoneLabel\": \"Phone\",\r\n \"addressLabel\": \"Address\",\r\n \"hoursLabel\": \"Hours\",\r\n \"hours\": \"Mon - Fri: 9:00 AM - 6:00 PM\",\r\n \"formTitle\": \"Send us a message\",\r\n \"formSubtitle\": \"Fill out the form below and we'll get back to you as soon as possible.\",\r\n \"nameLabel\": \"Full Name\",\r\n \"namePlaceholder\": \"John Doe\",\r\n \"emailInputLabel\": \"Email\",\r\n \"emailPlaceholder\": \"john@example.com\",\r\n \"phoneInputLabel\": \"Phone\",\r\n \"phonePlaceholder\": \"+1 234 567 890\",\r\n \"messageLabel\": \"Message\",\r\n \"messagePlaceholder\": \"Tell us about your project...\",\r\n \"submit\": \"Send Message\",\r\n \"sending\": \"Sending...\",\r\n \"success\": \"Message sent! We'll be in touch soon.\",\r\n \"error\": \"Something went wrong. Please try again.\"\r\n}\r\n"
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
36
|
"path": "contact-page-split/lang/tr.json",
|
|
37
37
|
"type": "registry:lang",
|
|
38
38
|
"target": "$modules$/contact-page-split/lang/tr.json",
|
|
39
|
-
"content": "{\r\n \"title\": \"Bir Sohbet Başlatalım\",\r\n \"subtitle\": \"AI bu alt başlığı iletişim sayfası amacınıza göre özelleştirecektir. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\",\r\n \"emailLabel\": \"E-posta\",\r\n \"phoneLabel\": \"Telefon\",\r\n \"addressLabel\": \"Adres\",\r\n \"hoursLabel\": \"Çalışma Saatleri\",\r\n \"hours\": \"Pazartesi - Cuma: 09:00 - 18:00\",\r\n \"formTitle\": \"Bize mesaj gönderin\",\r\n \"formSubtitle\": \"Aşağıdaki formu doldurun, en kısa sürede size döneceğiz.\",\r\n \"nameLabel\": \"Ad Soyad\",\r\n \"namePlaceholder\": \"Ahmet Yılmaz\",\r\n \"emailInputLabel\": \"E-posta\",\r\n \"emailPlaceholder\": \"ahmet@ornek.com\",\r\n \"phoneInputLabel\": \"Telefon\",\r\n \"phonePlaceholder\": \"+90 532 123 4567\",\r\n \"messageLabel\": \"Mesaj\",\r\n \"messagePlaceholder\": \"Projeniz hakkında bize bilgi verin...\",\r\n \"submit\": \"Mesaj Gönder\",\r\n \"sending\": \"Gönderiliyor...\",\r\n \"success\": \"Mesaj gönderildi! En kısa sürede iletişime geçeceğiz.\",\r\n \"error\": \"Bir şeyler yanlış gitti. Lütfen tekrar deneyin.\"\r\n}\r\n"
|
|
39
|
+
"content": "{\r\n \"pageTitle\": \"İletişim\",\r\n \"title\": \"Bir Sohbet Başlatalım\",\r\n \"subtitle\": \"AI bu alt başlığı iletişim sayfası amacınıza göre özelleştirecektir. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\",\r\n \"emailLabel\": \"E-posta\",\r\n \"phoneLabel\": \"Telefon\",\r\n \"addressLabel\": \"Adres\",\r\n \"hoursLabel\": \"Çalışma Saatleri\",\r\n \"hours\": \"Pazartesi - Cuma: 09:00 - 18:00\",\r\n \"formTitle\": \"Bize mesaj gönderin\",\r\n \"formSubtitle\": \"Aşağıdaki formu doldurun, en kısa sürede size döneceğiz.\",\r\n \"nameLabel\": \"Ad Soyad\",\r\n \"namePlaceholder\": \"Ahmet Yılmaz\",\r\n \"emailInputLabel\": \"E-posta\",\r\n \"emailPlaceholder\": \"ahmet@ornek.com\",\r\n \"phoneInputLabel\": \"Telefon\",\r\n \"phonePlaceholder\": \"+90 532 123 4567\",\r\n \"messageLabel\": \"Mesaj\",\r\n \"messagePlaceholder\": \"Projeniz hakkında bize bilgi verin...\",\r\n \"submit\": \"Mesaj Gönder\",\r\n \"sending\": \"Gönderiliyor...\",\r\n \"success\": \"Mesaj gönderildi! En kısa sürede iletişime geçeceğiz.\",\r\n \"error\": \"Bir şeyler yanlış gitti. Lütfen tekrar deneyin.\"\r\n}\r\n"
|
|
40
40
|
}
|
|
41
41
|
],
|
|
42
42
|
"exports": {
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"type": "registry:page",
|
|
4
4
|
"title": "Contact Page",
|
|
5
5
|
"description": "Contact page with two-column layout: contact form (name, email, subject, message) on left, and contact information (address, phone, email, business hours, social links) on right. Includes embedded Google Map, form validation, and success/error toast notifications. Mobile-responsive with stacked layout.",
|
|
6
|
-
"registryDependencies": [
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"animations"
|
|
8
|
+
],
|
|
7
9
|
"usage": "import { ContactPage } from '@/modules/contact-page';\n\n<Route path=\"/contact\" element={<ContactPage />} />\n\n• Two-column: form left, info right\n• Form: name, email, subject, message\n• Info: address, phone, email, hours, map\n• Toast notifications on submit",
|
|
8
10
|
"route": {
|
|
9
11
|
"path": "/contact",
|
|
@@ -20,13 +22,13 @@
|
|
|
20
22
|
"path": "contact-page/contact-page.tsx",
|
|
21
23
|
"type": "registry:page",
|
|
22
24
|
"target": "$modules$/contact-page/contact-page.tsx",
|
|
23
|
-
"content": "import React, { useState, useMemo } 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 { Label } from \"@/components/ui/label\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Mail,\n Phone,\n MapPin,\n MessageSquare,\n Facebook,\n Twitter,\n Instagram,\n Linkedin,\n} from \"lucide-react\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn, SlideInLeft, SlideInRight } from \"@/modules/animations\";\n\nconst socialIcons: Record<string, React.ElementType> = {\n facebook: Facebook,\n twitter: Twitter,\n instagram: Instagram,\n linkedin: Linkedin,\n};\n\nexport function ContactPage() {\n const { t } = useTranslation(\"contact-page\");\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 subject: \"\",\n message: \"\",\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [submitStatus, setSubmitStatus] = useState<\n \"idle\" | \"success\" | \"error\"\n >(\"idle\");\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.submitForm(\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: \"subject\", required: false },\n { name: \"message\", required: true },\n ],\n },\n currentLanguage\n );\n\n setSubmitStatus(\"success\");\n setFormData({\n name: \"\",\n email: \"\",\n phone: \"\",\n subject: \"\",\n message: \"\",\n });\n\n setTimeout(() => {\n setSubmitStatus(\"idle\");\n }, 5000);\n } catch (error: unknown) {\n console.error(\"Form submission failed:\", error);\n setSubmitStatus(\"error\");\n\n setTimeout(() => {\n setSubmitStatus(\"idle\");\n }, 5000);\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (\n e: React.ChangeEvent<\n HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement\n >\n ) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 max-w-6xl\">\n {/* Hero Section */}\n <FadeIn className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\")}\n </h1>\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\n <p className=\"text-lg text-muted-foreground max-w-3xl mx-auto\">\n {t(\"description\")}\n </p>\n </FadeIn>\n\n <div className=\"grid lg:grid-cols-3 gap-8\">\n {/* Contact Information */}\n <SlideInLeft className=\"lg:col-span-1 space-y-6\">\n {/* Company Info */}\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MessageSquare className=\"w-5 h-5 text-primary\" />\n {t(\"getInTouch\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"flex items-start gap-3\">\n <Mail className=\"w-5 h-5 text-primary mt-1\" />\n <div>\n <p className=\"font-medium text-foreground\">\n {t(\"email\")}\n </p>\n <p className=\"text-muted-foreground\">{constants.email}</p>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {t(\"emailResponse\")}\n </p>\n </div>\n </div>\n\n <div className=\"flex items-start gap-3\">\n <Phone className=\"w-5 h-5 text-primary mt-1\" />\n <div>\n <p className=\"font-medium text-foreground\">\n {t(\"phone\")}\n </p>\n <p className=\"text-muted-foreground\">{constants.phone}</p>\n </div>\n </div>\n\n <div className=\"flex items-start gap-3\">\n <MapPin className=\"w-5 h-5 text-primary mt-1\" />\n <div>\n <p className=\"font-medium text-foreground\">\n {t(\"address\")}\n </p>\n <div className=\"text-muted-foreground\">\n <p>{constants.address.line1}</p>\n {constants.address.line2 && (\n <p>{constants.address.line2}</p>\n )}\n <p>\n {constants.address.city}, {constants.address.state}{\" \"}\n {constants.address.postalCode}\n </p>\n {constants.address.country && (\n <p>{constants.address.country}</p>\n )}\n </div>\n </div>\n </div>\n\n {/* Social Media Links */}\n {socialLinks.length > 0 && (\n <div className=\"pt-4 border-t\">\n <p className=\"font-medium text-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-md border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\n >\n <Icon className=\"h-5 w-5\" />\n </a>\n ))}\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n\n {/* Support Info */}\n <Card>\n <CardHeader>\n <CardTitle>{t(\"needSupport\")}</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-muted-foreground mb-4\">\n {t(\"supportDescription\")}\n </p>\n <div className=\"space-y-2\">\n <p className=\"font-medium text-foreground\">\n {t(\"supportEmail\")}\n </p>\n <p className=\"text-muted-foreground\">{constants.email}</p>\n </div>\n </CardContent>\n </Card>\n </SlideInLeft>\n\n {/* Contact Form */}\n <SlideInRight className=\"lg:col-span-2\">\n <Card>\n <CardHeader>\n <CardTitle>{t(\"sendMessage\")}</CardTitle>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Full Name */}\n <div>\n <Label htmlFor=\"name\">{t(\"fullName\")} *</Label>\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\"\n />\n </div>\n\n {/* Email */}\n <div>\n <Label htmlFor=\"email\">{t(\"emailAddress\")} *</Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1\"\n />\n </div>\n\n {/* Phone */}\n <div>\n <Label htmlFor=\"phone\">{t(\"phoneNumber\")}</Label>\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\"\n />\n </div>\n\n {/* Subject */}\n <div>\n <Label htmlFor=\"subject\">{t(\"subject\")}</Label>\n <Input\n id=\"subject\"\n name=\"subject\"\n type=\"text\"\n value={formData.subject}\n onChange={handleChange}\n placeholder={t(\"subjectPlaceholder\")}\n className=\"mt-1\"\n />\n </div>\n\n {/* Message */}\n <div>\n <Label htmlFor=\"message\">{t(\"message\")} *</Label>\n <Textarea\n id=\"message\"\n name=\"message\"\n value={formData.message}\n onChange={handleChange}\n placeholder={t(\"messagePlaceholder\")}\n required\n rows={6}\n className=\"mt-1 resize-none\"\n />\n </div>\n\n {submitStatus === \"success\" && (\n <div className=\"p-4 bg-green-500/10 dark:bg-green-500/20 border border-green-500/30 rounded-lg\">\n <p className=\"text-green-600 dark:text-green-400 text-sm font-medium\">\n {t(\"success\")}\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\")}\n </p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"sending\")}\n </>\n ) : (\n t(\"submit\")\n )}\n </Button>\n </form>\n </CardContent>\n </Card>\n </SlideInRight>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n"
|
|
25
|
+
"content": "import React, { useState, useMemo } 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 { Label } from \"@/components/ui/label\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Mail,\n Phone,\n MapPin,\n MessageSquare,\n Facebook,\n Twitter,\n Instagram,\n Linkedin,\n} from \"lucide-react\";\nimport constants from \"@/constants/constants.json\";\nimport { FadeIn, SlideInLeft, SlideInRight } from \"@/modules/animations\";\n\nconst socialIcons: Record<string, React.ElementType> = {\n facebook: Facebook,\n twitter: Twitter,\n instagram: Instagram,\n linkedin: Linkedin,\n};\n\nexport function ContactPage() {\n const { t } = useTranslation(\"contact-page\");\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 subject: \"\",\n message: \"\",\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [submitStatus, setSubmitStatus] = useState<\n \"idle\" | \"success\" | \"error\"\n >(\"idle\");\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.submitForm(\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: \"subject\", required: false },\n { name: \"message\", required: true },\n ],\n },\n currentLanguage\n );\n\n setSubmitStatus(\"success\");\n setFormData({\n name: \"\",\n email: \"\",\n phone: \"\",\n subject: \"\",\n message: \"\",\n });\n\n setTimeout(() => {\n setSubmitStatus(\"idle\");\n }, 5000);\n } catch (error: unknown) {\n console.error(\"Form submission failed:\", error);\n setSubmitStatus(\"error\");\n\n setTimeout(() => {\n setSubmitStatus(\"idle\");\n }, 5000);\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const handleChange = (\n e: React.ChangeEvent<\n HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement\n >\n ) => {\n setFormData((prev) => ({\n ...prev,\n [e.target.name]: e.target.value,\n }));\n };\n\n return (\n <Layout>\n <div className=\"min-h-screen bg-muted/30 py-12\">\n <div className=\"w-full max-w-[var(--container-max-width)] mx-auto px-4 max-w-6xl\">\n {/* Hero Section */}\n <FadeIn className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-foreground mb-4\">\n {t(\"title\")}\n </h1>\n <div className=\"w-16 h-1 bg-primary mx-auto mb-6\"></div>\n <p className=\"text-lg text-muted-foreground max-w-3xl mx-auto\">\n {t(\"description\")}\n </p>\n </FadeIn>\n\n <div className=\"grid lg:grid-cols-3 gap-8\">\n {/* Contact Information */}\n <SlideInLeft className=\"lg:col-span-1 space-y-6\">\n {/* Company Info */}\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MessageSquare className=\"w-5 h-5 text-primary\" />\n {t(\"getInTouch\")}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"flex items-start gap-3\">\n <Mail className=\"w-5 h-5 text-primary mt-1\" />\n <div>\n <p className=\"font-medium text-foreground\">\n {t(\"email\")}\n </p>\n <p className=\"text-muted-foreground\">{constants.email}</p>\n <p className=\"text-sm text-muted-foreground mt-1\">\n {t(\"emailResponse\")}\n </p>\n </div>\n </div>\n\n <div className=\"flex items-start gap-3\">\n <Phone className=\"w-5 h-5 text-primary mt-1\" />\n <div>\n <p className=\"font-medium text-foreground\">\n {t(\"phone\")}\n </p>\n <p className=\"text-muted-foreground\">{constants.phone}</p>\n </div>\n </div>\n\n <div className=\"flex items-start gap-3\">\n <MapPin className=\"w-5 h-5 text-primary mt-1\" />\n <div>\n <p className=\"font-medium text-foreground\">\n {t(\"address\")}\n </p>\n <div className=\"text-muted-foreground\">\n <p>{constants.address.line1}</p>\n {constants.address.line2 && (\n <p>{constants.address.line2}</p>\n )}\n <p>\n {constants.address.city}, {constants.address.state}{\" \"}\n {constants.address.postalCode}\n </p>\n {constants.address.country && (\n <p>{constants.address.country}</p>\n )}\n </div>\n </div>\n </div>\n\n {/* Social Media Links */}\n {socialLinks.length > 0 && (\n <div className=\"pt-4 border-t\">\n <p className=\"font-medium text-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-md border text-muted-foreground hover:text-primary hover:border-primary transition-colors\"\n >\n <Icon className=\"h-5 w-5\" />\n </a>\n ))}\n </div>\n </div>\n )}\n </CardContent>\n </Card>\n\n {/* Support Info */}\n <Card>\n <CardHeader>\n <CardTitle>{t(\"needSupport\")}</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-muted-foreground mb-4\">\n {t(\"supportDescription\")}\n </p>\n <div className=\"space-y-2\">\n <p className=\"font-medium text-foreground\">\n {t(\"supportEmail\")}\n </p>\n <p className=\"text-muted-foreground\">{constants.email}</p>\n </div>\n </CardContent>\n </Card>\n </SlideInLeft>\n\n {/* Contact Form */}\n <SlideInRight className=\"lg:col-span-2\">\n <Card>\n <CardHeader>\n <CardTitle>{t(\"sendMessage\")}</CardTitle>\n </CardHeader>\n <CardContent>\n <form onSubmit={handleSubmit} className=\"space-y-6\">\n {/* Full Name */}\n <div>\n <Label htmlFor=\"name\">{t(\"fullName\")} *</Label>\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\"\n />\n </div>\n\n {/* Email */}\n <div>\n <Label htmlFor=\"email\">{t(\"emailAddress\")} *</Label>\n <Input\n id=\"email\"\n name=\"email\"\n type=\"email\"\n value={formData.email}\n onChange={handleChange}\n placeholder={t(\"emailPlaceholder\")}\n required\n className=\"mt-1\"\n />\n </div>\n\n {/* Phone */}\n <div>\n <Label htmlFor=\"phone\">{t(\"phoneNumber\")}</Label>\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\"\n />\n </div>\n\n {/* Subject */}\n <div>\n <Label htmlFor=\"subject\">{t(\"subject\")}</Label>\n <Input\n id=\"subject\"\n name=\"subject\"\n type=\"text\"\n value={formData.subject}\n onChange={handleChange}\n placeholder={t(\"subjectPlaceholder\")}\n className=\"mt-1\"\n />\n </div>\n\n {/* Message */}\n <div>\n <Label htmlFor=\"message\">{t(\"message\")} *</Label>\n <Textarea\n id=\"message\"\n name=\"message\"\n value={formData.message}\n onChange={handleChange}\n placeholder={t(\"messagePlaceholder\")}\n required\n rows={6}\n className=\"mt-1 resize-none\"\n />\n </div>\n\n {submitStatus === \"success\" && (\n <div className=\"p-4 bg-green-500/10 dark:bg-green-500/20 border border-green-500/30 rounded-lg\">\n <p className=\"text-green-600 dark:text-green-400 text-sm font-medium\">\n {t(\"success\")}\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\")}\n </p>\n </div>\n )}\n\n <Button\n type=\"submit\"\n size=\"lg\"\n className=\"w-full\"\n disabled={isSubmitting}\n >\n {isSubmitting ? (\n <>\n <div className=\"w-4 h-4 border-2 border-primary-foreground border-t-transparent rounded-full animate-spin mr-2\" />\n {t(\"sending\")}\n </>\n ) : (\n t(\"submit\")\n )}\n </Button>\n </form>\n </CardContent>\n </Card>\n </SlideInRight>\n </div>\n </div>\n </div>\n </Layout>\n );\n}\n\nexport default ContactPage;\n"
|
|
24
26
|
},
|
|
25
27
|
{
|
|
26
28
|
"path": "contact-page/lang/en.json",
|
|
27
29
|
"type": "registry:lang",
|
|
28
30
|
"target": "$modules$/contact-page/lang/en.json",
|
|
29
|
-
"content": "{\r\n \"title\": \"Contact Us\",\r\n \"getInTouch\": \"Get in Touch\",\r\n \"emailUs\": \"Email Us\",\r\n \"callUs\": \"Call Us\",\r\n \"visitUs\": \"Visit Us\",\r\n \"businessHours\": \"Hours\",\r\n \"sendMessage\": \"Send us a Message\",\r\n \"formNotAvailable\": \"Form is not available at the moment.\",\r\n \"fullName\": \"Full Name\",\r\n \"emailAddress\": \"Email Address\",\r\n \"phoneNumber\": \"Phone Number\",\r\n \"subject\": \"Subject\",\r\n \"message\": \"Message\",\r\n \"submit\": \"Send Message\",\r\n \"sending\": \"Sending...\",\r\n \"success\": \"Thank you for your message! We will get back to you soon.\",\r\n \"error\": \"Failed to send message. Please try again later.\",\r\n \"description\": \"
|
|
31
|
+
"content": "{\r\n \"title\": \"Contact Us\",\r\n \"getInTouch\": \"Get in Touch\",\r\n \"emailUs\": \"Email Us\",\r\n \"callUs\": \"Call Us\",\r\n \"visitUs\": \"Visit Us\",\r\n \"businessHours\": \"Hours\",\r\n \"sendMessage\": \"Send us a Message\",\r\n \"formNotAvailable\": \"Form is not available at the moment.\",\r\n \"fullName\": \"Full Name\",\r\n \"emailAddress\": \"Email Address\",\r\n \"phoneNumber\": \"Phone Number\",\r\n \"subject\": \"Subject\",\r\n \"message\": \"Message\",\r\n \"submit\": \"Send Message\",\r\n \"sending\": \"Sending...\",\r\n \"success\": \"Thank you for your message! We will get back to you soon.\",\r\n \"error\": \"Failed to send message. Please try again later.\",\r\n \"description\": \"Ask Promake to customize this contact page description based on your site and support options. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\",\r\n \"email\": \"Email\",\r\n \"phone\": \"Phone\",\r\n \"address\": \"Address\",\r\n \"fullNamePlaceholder\": \"Your full name\",\r\n \"emailPlaceholder\": \"your@email.com\",\r\n \"phonePlaceholder\": \"+1 (555) 123-4567\",\r\n \"subjectPlaceholder\": \"What is this regarding?\",\r\n \"messagePlaceholder\": \"Tell us how we can help you...\",\r\n \"loading\": \"Loading contact information...\",\r\n \"emailResponse\": \"We typically respond within 24 hours\",\r\n \"needSupport\": \"Need Support?\",\r\n \"supportDescription\": \"For technical support or general inquiries, contact our dedicated support team:\",\r\n \"supportEmail\": \"Support Email\",\r\n \"monday_friday\": \"Monday - Friday\",\r\n \"saturday\": \"Saturday\",\r\n \"sunday\": \"Sunday\",\r\n \"closed\": \"Closed\",\r\n \"am\": \"AM\",\r\n \"pm\": \"PM\"\r\n}\r\n"
|
|
30
32
|
},
|
|
31
33
|
{
|
|
32
34
|
"path": "contact-page/lang/tr.json",
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cookie-consent",
|
|
3
|
+
"type": "registry:component",
|
|
4
|
+
"title": "Cookie Consent Banner",
|
|
5
|
+
"description": "GDPR-compliant cookie consent banner with Accept, Decline, and Customize buttons. Features slide-up animation, localStorage persistence, and customizable links to privacy/cookie policy pages.",
|
|
6
|
+
"dependencies": [
|
|
7
|
+
"motion"
|
|
8
|
+
],
|
|
9
|
+
"registryDependencies": [],
|
|
10
|
+
"usage": "import { CookieConsent } from '@/modules/cookie-consent';\n\n// Add to your App or Layout component\n<CookieConsent\n onAccept={() => console.log('Accepted')}\n onDecline={() => console.log('Declined')}\n onCustomize={() => openPreferencesModal()}\n/>\n\n• Auto-hides after user decision\n• localStorage persistence\n• Customizable policy URLs",
|
|
11
|
+
"files": [
|
|
12
|
+
{
|
|
13
|
+
"path": "cookie-consent/index.ts",
|
|
14
|
+
"type": "registry:index",
|
|
15
|
+
"target": "$modules$/cookie-consent/index.ts",
|
|
16
|
+
"content": "export { CookieConsent } from \"./cookie-consent\";\r\n"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "cookie-consent/cookie-consent.tsx",
|
|
20
|
+
"type": "registry:component",
|
|
21
|
+
"target": "$modules$/cookie-consent/cookie-consent.tsx",
|
|
22
|
+
"content": "import { useState, useEffect } from \"react\";\r\nimport { Link } from \"react-router\";\r\nimport { useTranslation } from \"react-i18next\";\r\nimport { X } from \"lucide-react\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport { motion, AnimatePresence } from \"motion/react\";\r\n\r\ninterface CookieConsentProps {\r\n className?: string;\r\n privacyPolicyUrl?: string;\r\n cookiePolicyUrl?: string;\r\n onAccept?: () => void;\r\n onDecline?: () => void;\r\n width?: \"full\" | \"compact\" | \"auto\";\r\n position?: \"left\" | \"right\" | \"center\";\r\n storageKey?: string;\r\n}\r\n\r\nconst DEFAULT_STORAGE_KEY = \"cookie-consent\";\r\n\r\nexport function CookieConsent({\r\n className,\r\n privacyPolicyUrl = \"/privacy\",\r\n cookiePolicyUrl = \"/cookies\",\r\n onAccept,\r\n onDecline,\r\n width = \"full\",\r\n position = \"center\",\r\n storageKey = DEFAULT_STORAGE_KEY,\r\n}: CookieConsentProps) {\r\n const { t } = useTranslation(\"cookie-consent\");\r\n const [isVisible, setIsVisible] = useState(false);\r\n const [isClosing, setIsClosing] = useState(false);\r\n\r\n useEffect(() => {\r\n const consent = localStorage.getItem(storageKey);\r\n if (!consent) {\r\n // Small delay for better UX\r\n const timer = setTimeout(() => setIsVisible(true), 1000);\r\n return () => clearTimeout(timer);\r\n }\r\n }, [storageKey]);\r\n\r\n const handleAccept = () => {\r\n localStorage.setItem(storageKey, \"accepted\");\r\n setIsClosing(true);\r\n setTimeout(() => {\r\n setIsVisible(false);\r\n onAccept?.();\r\n }, 300);\r\n };\r\n\r\n const handleDecline = () => {\r\n localStorage.setItem(storageKey, \"declined\");\r\n setIsClosing(true);\r\n setTimeout(() => {\r\n setIsVisible(false);\r\n onDecline?.();\r\n }, 300);\r\n };\r\n\r\n const handleClose = () => {\r\n setIsClosing(true);\r\n setTimeout(() => setIsVisible(false), 300);\r\n };\r\n\r\n if (!isVisible) return null;\r\n\r\n return (\r\n <AnimatePresence>\r\n {isVisible && (\r\n <motion.div\r\n initial={{ y: 100, opacity: 0 }}\r\n animate={{ y: isClosing ? 100 : 0, opacity: isClosing ? 0 : 1 }}\r\n exit={{ y: 100, opacity: 0 }}\r\n transition={{ type: \"spring\", damping: 25, stiffness: 300 }}\r\n className={cn(\r\n \"fixed bottom-0 z-50 p-4 md:p-6 left-0 right-0\",\r\n position === \"left\" && \"md:right-auto\",\r\n position === \"right\" && \"md:left-auto\",\r\n className\r\n )}\r\n >\r\n <div className={cn(\r\n \"w-full mx-auto\",\r\n width === \"full\" && \"max-w-[var(--container-max-width)]\",\r\n width === \"compact\" && \"md:max-w-md\",\r\n width === \"auto\" && \"md:w-auto\"\r\n )}>\r\n <div className=\"bg-card border border-border rounded-2xl shadow-2xl p-5 relative\">\r\n {/* Close button */}\r\n <button\r\n onClick={handleClose}\r\n className=\"absolute top-3 right-3 p-1 text-muted-foreground hover:text-foreground transition-colors\"\r\n aria-label={t(\"close\", \"Close\")}\r\n >\r\n <X className=\"h-4 w-4\" />\r\n </button>\r\n\r\n {/* Title & Description */}\r\n <div className=\"mb-4 pr-6\">\r\n <h3 className=\"text-base font-semibold text-foreground mb-1\">\r\n {t(\"title\", \"Cookie Notice\")}\r\n </h3>\r\n <p className=\"text-sm text-muted-foreground\">\r\n {t(\"description\", \"We use cookies to improve your experience.\")}{\" \"}\r\n <Link to={privacyPolicyUrl} className=\"text-primary hover:underline\">\r\n {t(\"privacyPolicy\", \"Privacy\")}\r\n </Link>\r\n {\" · \"}\r\n <Link to={cookiePolicyUrl} className=\"text-primary hover:underline\">\r\n {t(\"cookiePolicy\", \"Cookies\")}\r\n </Link>\r\n </p>\r\n </div>\r\n\r\n {/* Buttons */}\r\n <div className=\"flex flex-row gap-3\">\r\n <Button\r\n variant=\"outline\"\r\n size=\"sm\"\r\n onClick={handleDecline}\r\n className=\"flex-1\"\r\n >\r\n {t(\"decline\", \"Decline\")}\r\n </Button>\r\n <Button\r\n size=\"sm\"\r\n onClick={handleAccept}\r\n className=\"flex-1\"\r\n >\r\n {t(\"accept\", \"Accept\")}\r\n </Button>\r\n </div>\r\n </div>\r\n </div>\r\n </motion.div>\r\n )}\r\n </AnimatePresence>\r\n );\r\n}\r\n"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"path": "cookie-consent/lang/en.json",
|
|
26
|
+
"type": "registry:lang",
|
|
27
|
+
"target": "$modules$/cookie-consent/lang/en.json",
|
|
28
|
+
"content": "{\r\n \"title\": \"Cookie Notice\",\r\n \"description\": \"We use cookies to improve your experience.\",\r\n \"privacyPolicy\": \"Privacy\",\r\n \"cookiePolicy\": \"Cookies\",\r\n \"accept\": \"Accept\",\r\n \"decline\": \"Decline\",\r\n \"close\": \"Close\"\r\n}\r\n"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "cookie-consent/lang/tr.json",
|
|
32
|
+
"type": "registry:lang",
|
|
33
|
+
"target": "$modules$/cookie-consent/lang/tr.json",
|
|
34
|
+
"content": "{\r\n \"title\": \"Gizliliğinize değer veriyoruz\",\r\n \"description\": \"Göz atma deneyiminizi geliştirmek, kişiselleştirilmiş içerik sunmak ve trafiğimizi analiz etmek için çerezler kullanıyoruz. \\\"Tümünü Kabul Et\\\"e tıklayarak çerez kullanımımıza onay verirsiniz.\",\r\n \"privacyPolicy\": \"Gizlilik Politikası\",\r\n \"cookiePolicy\": \"Çerez Politikası\",\r\n \"accept\": \"Tümünü Kabul Et\",\r\n \"decline\": \"Reddet\",\r\n \"customize\": \"Özelleştir\",\r\n \"close\": \"Kapat\"\r\n}\r\n"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
"exports": {
|
|
38
|
+
"types": [],
|
|
39
|
+
"variables": [
|
|
40
|
+
"CookieConsent"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
"type": "registry:page",
|
|
4
4
|
"title": "Cookie Policy Page",
|
|
5
5
|
"description": "Cookie policy page explaining cookie types (essential, analytics, marketing), purposes, third-party cookies, and user controls. Includes cookie table with name, purpose, and expiration. GDPR/ePrivacy compliant formatting with clear explanations for non-technical users.",
|
|
6
|
-
"registryDependencies": [
|
|
6
|
+
"registryDependencies": [
|
|
7
|
+
"animations"
|
|
8
|
+
],
|
|
7
9
|
"usage": "import { CookiesPage } from '@/modules/cookies-page';\n\n<Route path=\"/cookies\" element={<CookiesPage />} />\n\n• Cookie types: essential, analytics, marketing\n• Cookie table with name, purpose, expiration\n• GDPR/ePrivacy compliant",
|
|
8
10
|
"route": {
|
|
9
11
|
"path": "/cookies",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"path": "cta-section/lang/en.json",
|
|
23
23
|
"type": "registry:lang",
|
|
24
24
|
"target": "$modules$/cta-section/lang/en.json",
|
|
25
|
-
"content": "{\r\n \"title\": \"Ready to Start Your Project?\",\r\n \"description\": \"
|
|
25
|
+
"content": "{\r\n \"title\": \"Ready to Start Your Project?\",\r\n \"description\": \"Ask Promake to customize this CTA description based on your site goals. Lorem ipsum dolor sit amet, consectetur adipiscing elit.\",\r\n \"primaryButton\": \"Get Free Quote\",\r\n \"secondaryButton\": \"Learn About Us\"\r\n}\r\n"
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
"path": "cta-section/lang/tr.json",
|
package/dist/registry/db.json
CHANGED
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"path": "db/adapters/SqliteAdapter.ts",
|
|
50
50
|
"type": "registry:lib",
|
|
51
51
|
"target": "$modules$/db/adapters/SqliteAdapter.ts",
|
|
52
|
-
"content": "import initSqlJs, { Database } from \"sql.js\";\nimport type { IDataAdapter } from \"./IDataAdapter\";\nimport type {\n QueryOptions,\n WhereCondition,\n WhereGroup,\n JoinClause,\n} from \"../core/types\";\n\n/**\n * SQLite Adapter\n * Loads database from file using sql.js\n * Supports complex queries: JOIN, WHERE groups, LIKE, IN, BETWEEN\n */\nexport class SqliteAdapter implements IDataAdapter {\n private db: Database | null = null;\n private dbPath: string;\n\n constructor(dbPath: string = \"/data/database.db\") {\n this.dbPath = dbPath;\n }\n\n // ============================================\n // CONNECTION\n // ============================================\n\n async connect(): Promise<void> {\n if (this.db) return;\n\n try {\n const SQL = await initSqlJs({\n locateFile: (file: string) => `https://sql.js.org/dist/${file}`,\n });\n\n const response = await fetch(this.dbPath);\n if (!response.ok) {\n throw new Error(`Database file not found: ${this.dbPath}`);\n }\n\n const buffer = await response.arrayBuffer();\n this.db = new SQL.Database(new Uint8Array(buffer));\n console.log(\"SQLite adapter connected\");\n } catch (error) {\n console.error(\"SQLite adapter connection failed:\", error);\n throw error;\n }\n }\n\n async disconnect(): Promise<void> {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n // ============================================\n // CRUD OPERATIONS\n // ============================================\n\n async findMany<T>(table: string, options?: QueryOptions): Promise<T[]> {\n await this.connect();\n if (!this.db) {\n console.warn(\"Database not connected\");\n return [];\n }\n try {\n const { sql, params } = this.buildSelectQuery(table, options);\n return this.executeQuery<T>(sql, params);\n } catch (error) {\n console.error(`Error querying table ${table}:`, error);\n return [];\n }\n }\n\n async findOne<T>(table: string, options: QueryOptions): Promise<T | null> {\n const results = await this.findMany<T>(table, { ...options, limit: 1 });\n return results[0] || null;\n }\n\n async findById<T>(table: string, id: number | string): Promise<T | null> {\n return this.findOne<T>(table, { where: { id } });\n }\n\n async create<T>(table: string, data: Partial<T>): Promise<T> {\n await this.connect();\n\n // Otomatik timestamp - sadece yoksa ekle\n const dataWithTimestamps: any = { ...data };\n\n if (!dataWithTimestamps.created_at) {\n dataWithTimestamps.created_at = new Date().toISOString();\n }\n\n if (!dataWithTimestamps.updated_at) {\n dataWithTimestamps.updated_at = new Date().toISOString();\n }\n\n const keys = Object.keys(dataWithTimestamps);\n const values = Object.values(dataWithTimestamps) as any[];\n const placeholders = keys.map(() => \"?\").join(\", \");\n\n const sql = `INSERT INTO ${table} (${keys.join(\", \")}) VALUES (${placeholders})`;\n this.db!.run(sql, values);\n\n const lastIdResult = this.db!.exec(\"SELECT last_insert_rowid() as id\");\n const lastId = lastIdResult[0]?.values[0]?.[0] as number;\n\n return this.findById<T>(table, lastId) as Promise<T>;\n }\n\n async update<T>(\n table: string,\n id: number | string,\n data: Partial<T>,\n ): Promise<T> {\n await this.connect();\n\n // Otomatik updated_at - her zaman güncelle\n const dataWithTimestamp = {\n ...data,\n updated_at: new Date().toISOString(),\n };\n\n const keys = Object.keys(dataWithTimestamp);\n const values = Object.values(dataWithTimestamp) as any[];\n const setClause = keys.map((key) => `${key} = ?`).join(\", \");\n\n const sql = `UPDATE ${table} SET ${setClause} WHERE id = ?`;\n this.db!.run(sql, [...values, id]);\n\n return this.findById<T>(table, id) as Promise<T>;\n }\n\n async delete(table: string, id: number | string): Promise<boolean> {\n await this.connect();\n const sql = `DELETE FROM ${table} WHERE id = ?`;\n this.db!.run(sql, [id]);\n return true;\n }\n\n async count(table: string, options?: QueryOptions): Promise<number> {\n await this.connect();\n const { sql, params } = this.buildCountQuery(table, options);\n const results = await this.executeQuery<{ count: number }>(sql, params);\n return results[0]?.count || 0;\n }\n\n // ============================================\n // RAW SQL QUERIES\n // ============================================\n\n async raw<T>(sql: string, params?: any[]): Promise<T[]> {\n await this.connect();\n return this.executeQuery<T>(sql, params);\n }\n\n async rawOne<T>(sql: string, params?: any[]): Promise<T | null> {\n const results = await this.raw<T>(sql, params);\n return results[0] || null;\n }\n\n // ============================================\n // PRIVATE: QUERY EXECUTION\n // ============================================\n\n private async executeQuery<T>(sql: string, params?: any[]): Promise<T[]> {\n if (!this.db) {\n console.warn(\"Database not connected\");\n return [];\n }\n try {\n const stmt = this.db.prepare(sql);\n if (params && params.length > 0) stmt.bind(params);\n\n const results: T[] = [];\n while (stmt.step()) {\n results.push(stmt.getAsObject() as T);\n }\n stmt.free();\n return results;\n } catch (error) {\n console.error(`Query error: ${sql}`, error);\n return [];\n }\n }\n\n // ============================================\n // PRIVATE: QUERY BUILDERS\n // ============================================\n\n private buildSelectQuery(\n table: string,\n options?: QueryOptions,\n ): { sql: string; params: any[] } {\n const params: any[] = [];\n\n // SELECT clause\n const selectFields = options?.select?.join(\", \") || \"*\";\n const distinct = options?.distinct ? \"DISTINCT \" : \"\";\n let sql = `SELECT ${distinct}${selectFields} FROM ${table}`;\n\n // JOIN clauses\n if (options?.joins && options.joins.length > 0) {\n sql += this.buildJoinClause(options.joins);\n }\n\n // WHERE clause\n const whereClause = this.buildWhereClause(options, params);\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n // GROUP BY clause\n if (options?.groupBy && options.groupBy.length > 0) {\n sql += ` GROUP BY ${options.groupBy.join(\", \")}`;\n }\n\n // HAVING clause\n if (options?.having) {\n const havingClause = this.buildWhereGroupClause(options.having, params);\n if (havingClause) {\n sql += ` HAVING ${havingClause}`;\n }\n }\n\n // ORDER BY clause\n if (options?.orderBy && options.orderBy.length > 0) {\n const orderClauses = options.orderBy.map(\n (o) => `${o.field} ${o.direction}`,\n );\n sql += ` ORDER BY ${orderClauses.join(\", \")}`;\n }\n\n // LIMIT & OFFSET\n if (options?.limit) {\n sql += ` LIMIT ?`;\n params.push(options.limit);\n }\n\n if (options?.offset) {\n sql += ` OFFSET ?`;\n params.push(options.offset);\n }\n\n return { sql, params };\n }\n\n private buildCountQuery(\n table: string,\n options?: QueryOptions,\n ): { sql: string; params: any[] } {\n const params: any[] = [];\n let sql = `SELECT COUNT(*) as count FROM ${table}`;\n\n // JOIN clauses\n if (options?.joins && options.joins.length > 0) {\n sql += this.buildJoinClause(options.joins);\n }\n\n // WHERE clause\n const whereClause = this.buildWhereClause(options, params);\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n return { sql, params };\n }\n\n private buildJoinClause(joins: JoinClause[]): string {\n return joins\n .map((join) => {\n const alias = join.alias ? ` ${join.alias}` : \"\";\n const leftField = join.on.leftField;\n const rightField = join.on.rightField;\n return ` ${join.type} JOIN ${join.table}${alias} ON ${leftField} = ${rightField}`;\n })\n .join(\"\");\n }\n\n private buildWhereClause(\n options: QueryOptions | undefined,\n params: any[],\n ): string {\n const clauses: string[] = [];\n\n // Simple where (backwards compatible)\n if (options?.where) {\n const simpleConditions = Object.entries(options.where).map(\n ([key, value]) => {\n params.push(value);\n return `${key} = ?`;\n },\n );\n if (simpleConditions.length > 0) {\n clauses.push(simpleConditions.join(\" AND \"));\n }\n }\n\n // Advanced where (new feature)\n if (options?.whereAdvanced) {\n const advancedClause = this.buildWhereGroupClause(\n options.whereAdvanced,\n params,\n );\n if (advancedClause) {\n clauses.push(advancedClause);\n }\n }\n\n return clauses.length > 0 ? clauses.join(\" AND \") : \"\";\n }\n\n private buildWhereGroupClause(group: WhereGroup, params: any[]): string {\n const conditions = group.conditions\n .map((condition) => {\n // Recursive: WhereGroup\n if (\"type\" in condition && \"conditions\" in condition) {\n return `(${this.buildWhereGroupClause(condition as WhereGroup, params)})`;\n }\n // WhereCondition\n return this.buildConditionClause(condition as WhereCondition, params);\n })\n .filter(Boolean);\n\n return conditions.join(` ${group.type} `);\n }\n\n private buildConditionClause(\n condition: WhereCondition,\n params: any[],\n ): string {\n const { field, operator, value } = condition;\n\n switch (operator) {\n case \"IS NULL\":\n return `${field} IS NULL`;\n\n case \"IS NOT NULL\":\n return `${field} IS NOT NULL`;\n\n case \"IN\":\n case \"NOT IN\":\n if (Array.isArray(value)) {\n const placeholders = value.map(() => \"?\").join(\", \");\n params.push(...value);\n return `${field} ${operator} (${placeholders})`;\n }\n return \"\";\n\n case \"BETWEEN\":\n case \"NOT BETWEEN\":\n if (Array.isArray(value) && value.length === 2) {\n params.push(value[0], value[1]);\n return `${field} ${operator} ? AND ?`;\n }\n return \"\";\n\n default:\n // =, !=, <>, >, <, >=, <=, LIKE, NOT LIKE\n params.push(value);\n return `${field} ${operator} ?`;\n }\n }\n}\n"
|
|
52
|
+
"content": "import initSqlJs from \"sql.js\";\nimport type { Database } from \"sql.js\";\nimport type { IDataAdapter } from \"./IDataAdapter\";\nimport type {\n QueryOptions,\n WhereCondition,\n WhereGroup,\n JoinClause,\n} from \"../core/types\";\n\n/**\n * SQLite Adapter\n * Loads database from file using sql.js\n * Supports complex queries: JOIN, WHERE groups, LIKE, IN, BETWEEN\n */\nexport class SqliteAdapter implements IDataAdapter {\n private db: Database | null = null;\n private dbPath: string;\n\n constructor(dbPath: string = \"/data/database.db\") {\n this.dbPath = dbPath;\n }\n\n // ============================================\n // CONNECTION\n // ============================================\n\n async connect(): Promise<void> {\n if (this.db) return;\n\n try {\n const SQL = await initSqlJs({\n locateFile: (file: string) => `https://sql.js.org/dist/${file}`,\n });\n\n const response = await fetch(this.dbPath);\n if (!response.ok) {\n throw new Error(`Database file not found: ${this.dbPath}`);\n }\n\n const buffer = await response.arrayBuffer();\n this.db = new SQL.Database(new Uint8Array(buffer));\n console.log(\"SQLite adapter connected\");\n } catch (error) {\n console.error(\"SQLite adapter connection failed:\", error);\n throw error;\n }\n }\n\n async disconnect(): Promise<void> {\n if (this.db) {\n this.db.close();\n this.db = null;\n }\n }\n\n // ============================================\n // CRUD OPERATIONS\n // ============================================\n\n async findMany<T>(table: string, options?: QueryOptions): Promise<T[]> {\n await this.connect();\n if (!this.db) {\n console.warn(\"Database not connected\");\n return [];\n }\n try {\n const { sql, params } = this.buildSelectQuery(table, options);\n return this.executeQuery<T>(sql, params);\n } catch (error) {\n console.error(`Error querying table ${table}:`, error);\n return [];\n }\n }\n\n async findOne<T>(table: string, options: QueryOptions): Promise<T | null> {\n const results = await this.findMany<T>(table, { ...options, limit: 1 });\n return results[0] || null;\n }\n\n async findById<T>(table: string, id: number | string): Promise<T | null> {\n return this.findOne<T>(table, { where: { id } });\n }\n\n async create<T>(table: string, data: Partial<T>): Promise<T> {\n await this.connect();\n\n // Otomatik timestamp - sadece yoksa ekle\n const dataWithTimestamps: any = { ...data };\n\n if (!dataWithTimestamps.created_at) {\n dataWithTimestamps.created_at = new Date().toISOString();\n }\n\n if (!dataWithTimestamps.updated_at) {\n dataWithTimestamps.updated_at = new Date().toISOString();\n }\n\n const keys = Object.keys(dataWithTimestamps);\n const values = Object.values(dataWithTimestamps) as any[];\n const placeholders = keys.map(() => \"?\").join(\", \");\n\n const sql = `INSERT INTO ${table} (${keys.join(\", \")}) VALUES (${placeholders})`;\n this.db!.run(sql, values);\n\n const lastIdResult = this.db!.exec(\"SELECT last_insert_rowid() as id\");\n const lastId = lastIdResult[0]?.values[0]?.[0] as number;\n\n return this.findById<T>(table, lastId) as Promise<T>;\n }\n\n async update<T>(\n table: string,\n id: number | string,\n data: Partial<T>,\n ): Promise<T> {\n await this.connect();\n\n // Otomatik updated_at - her zaman güncelle\n const dataWithTimestamp = {\n ...data,\n updated_at: new Date().toISOString(),\n };\n\n const keys = Object.keys(dataWithTimestamp);\n const values = Object.values(dataWithTimestamp) as any[];\n const setClause = keys.map((key) => `${key} = ?`).join(\", \");\n\n const sql = `UPDATE ${table} SET ${setClause} WHERE id = ?`;\n this.db!.run(sql, [...values, id]);\n\n return this.findById<T>(table, id) as Promise<T>;\n }\n\n async delete(table: string, id: number | string): Promise<boolean> {\n await this.connect();\n const sql = `DELETE FROM ${table} WHERE id = ?`;\n this.db!.run(sql, [id]);\n return true;\n }\n\n async count(table: string, options?: QueryOptions): Promise<number> {\n await this.connect();\n const { sql, params } = this.buildCountQuery(table, options);\n const results = await this.executeQuery<{ count: number }>(sql, params);\n return results[0]?.count || 0;\n }\n\n // ============================================\n // RAW SQL QUERIES\n // ============================================\n\n async raw<T>(sql: string, params?: any[]): Promise<T[]> {\n await this.connect();\n return this.executeQuery<T>(sql, params);\n }\n\n async rawOne<T>(sql: string, params?: any[]): Promise<T | null> {\n const results = await this.raw<T>(sql, params);\n return results[0] || null;\n }\n\n // ============================================\n // PRIVATE: QUERY EXECUTION\n // ============================================\n\n private async executeQuery<T>(sql: string, params?: any[]): Promise<T[]> {\n if (!this.db) {\n console.warn(\"Database not connected\");\n return [];\n }\n try {\n const stmt = this.db.prepare(sql);\n if (params && params.length > 0) stmt.bind(params);\n\n const results: T[] = [];\n while (stmt.step()) {\n results.push(stmt.getAsObject() as T);\n }\n stmt.free();\n return results;\n } catch (error) {\n console.error(`Query error: ${sql}`, error);\n return [];\n }\n }\n\n // ============================================\n // PRIVATE: QUERY BUILDERS\n // ============================================\n\n private buildSelectQuery(\n table: string,\n options?: QueryOptions,\n ): { sql: string; params: any[] } {\n const params: any[] = [];\n\n // SELECT clause\n const selectFields = options?.select?.join(\", \") || \"*\";\n const distinct = options?.distinct ? \"DISTINCT \" : \"\";\n let sql = `SELECT ${distinct}${selectFields} FROM ${table}`;\n\n // JOIN clauses\n if (options?.joins && options.joins.length > 0) {\n sql += this.buildJoinClause(options.joins);\n }\n\n // WHERE clause\n const whereClause = this.buildWhereClause(options, params);\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n // GROUP BY clause\n if (options?.groupBy && options.groupBy.length > 0) {\n sql += ` GROUP BY ${options.groupBy.join(\", \")}`;\n }\n\n // HAVING clause\n if (options?.having) {\n const havingClause = this.buildWhereGroupClause(options.having, params);\n if (havingClause) {\n sql += ` HAVING ${havingClause}`;\n }\n }\n\n // ORDER BY clause\n if (options?.orderBy && options.orderBy.length > 0) {\n const orderClauses = options.orderBy.map(\n (o) => `${o.field} ${o.direction}`,\n );\n sql += ` ORDER BY ${orderClauses.join(\", \")}`;\n }\n\n // LIMIT & OFFSET\n if (options?.limit) {\n sql += ` LIMIT ?`;\n params.push(options.limit);\n }\n\n if (options?.offset) {\n sql += ` OFFSET ?`;\n params.push(options.offset);\n }\n\n return { sql, params };\n }\n\n private buildCountQuery(\n table: string,\n options?: QueryOptions,\n ): { sql: string; params: any[] } {\n const params: any[] = [];\n let sql = `SELECT COUNT(*) as count FROM ${table}`;\n\n // JOIN clauses\n if (options?.joins && options.joins.length > 0) {\n sql += this.buildJoinClause(options.joins);\n }\n\n // WHERE clause\n const whereClause = this.buildWhereClause(options, params);\n if (whereClause) {\n sql += ` WHERE ${whereClause}`;\n }\n\n return { sql, params };\n }\n\n private buildJoinClause(joins: JoinClause[]): string {\n return joins\n .map((join) => {\n const alias = join.alias ? ` ${join.alias}` : \"\";\n const leftField = join.on.leftField;\n const rightField = join.on.rightField;\n return ` ${join.type} JOIN ${join.table}${alias} ON ${leftField} = ${rightField}`;\n })\n .join(\"\");\n }\n\n private buildWhereClause(\n options: QueryOptions | undefined,\n params: any[],\n ): string {\n const clauses: string[] = [];\n\n // Simple where (backwards compatible)\n if (options?.where) {\n const simpleConditions = Object.entries(options.where).map(\n ([key, value]) => {\n params.push(value);\n return `${key} = ?`;\n },\n );\n if (simpleConditions.length > 0) {\n clauses.push(simpleConditions.join(\" AND \"));\n }\n }\n\n // Advanced where (new feature)\n if (options?.whereAdvanced) {\n const advancedClause = this.buildWhereGroupClause(\n options.whereAdvanced,\n params,\n );\n if (advancedClause) {\n clauses.push(advancedClause);\n }\n }\n\n return clauses.length > 0 ? clauses.join(\" AND \") : \"\";\n }\n\n private buildWhereGroupClause(group: WhereGroup, params: any[]): string {\n const conditions = group.conditions\n .map((condition) => {\n // Recursive: WhereGroup\n if (\"type\" in condition && \"conditions\" in condition) {\n return `(${this.buildWhereGroupClause(condition as WhereGroup, params)})`;\n }\n // WhereCondition\n return this.buildConditionClause(condition as WhereCondition, params);\n })\n .filter(Boolean);\n\n return conditions.join(` ${group.type} `);\n }\n\n private buildConditionClause(\n condition: WhereCondition,\n params: any[],\n ): string {\n const { field, operator, value } = condition;\n\n switch (operator) {\n case \"IS NULL\":\n return `${field} IS NULL`;\n\n case \"IS NOT NULL\":\n return `${field} IS NOT NULL`;\n\n case \"IN\":\n case \"NOT IN\":\n if (Array.isArray(value)) {\n const placeholders = value.map(() => \"?\").join(\", \");\n params.push(...value);\n return `${field} ${operator} (${placeholders})`;\n }\n return \"\";\n\n case \"BETWEEN\":\n case \"NOT BETWEEN\":\n if (Array.isArray(value) && value.length === 2) {\n params.push(value[0], value[1]);\n return `${field} ${operator} ? AND ?`;\n }\n return \"\";\n\n default:\n // =, !=, <>, >, <, >=, <=, LIKE, NOT LIKE\n params.push(value);\n return `${field} ${operator} ?`;\n }\n }\n}\n"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"path": "db/react/index.ts",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Announcement Bar
|
|
2
|
+
|
|
3
|
+
Top notification bar with multiple style variants (default, primary, warning, success, gradient), dismissible with localStorage persistence, customizable icon, and link support. Slide animation.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| Target | Type |
|
|
8
|
+
|--------|------|
|
|
9
|
+
| `$modules$/announcement-bar/index.ts` | index |
|
|
10
|
+
| `$modules$/announcement-bar/announcement-bar.tsx` | component |
|
|
11
|
+
| `$modules$/announcement-bar/lang/en.json` | lang |
|
|
12
|
+
| `$modules$/announcement-bar/lang/tr.json` | lang |
|
|
13
|
+
|
|
14
|
+
## Exports
|
|
15
|
+
|
|
16
|
+
**Components/Functions:** `AnnouncementBar`
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { AnnouncementBar } from '@/modules/announcement-bar';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
import { AnnouncementBar } from '@/modules/announcement-bar';
|
|
26
|
+
|
|
27
|
+
<AnnouncementBar
|
|
28
|
+
message="New features available!"
|
|
29
|
+
linkText="Learn more"
|
|
30
|
+
linkUrl="/blog/new-features"
|
|
31
|
+
variant="gradient"
|
|
32
|
+
icon="sparkles"
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
• Variants: default, primary, warning, success, gradient
|
|
36
|
+
• Icons: sparkles, megaphone, gift, zap, none
|
|
37
|
+
• Dismissible with storage key
|
|
38
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Auth Core
|
|
2
|
+
|
|
3
|
+
Core authentication system with Zustand store, JWT token management with auto-refresh. No pages included - use separate page modules (login-page, register-page, etc.) for UI.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| Target | Type |
|
|
8
|
+
|--------|------|
|
|
9
|
+
| `$modules$/auth-core/index.ts` | index |
|
|
10
|
+
| `$modules$/auth-core/auth-store.ts` | store |
|
|
11
|
+
| `$modules$/auth-core/use-auth.ts` | hook |
|
|
12
|
+
|
|
13
|
+
## Exports
|
|
14
|
+
|
|
15
|
+
**Types:** `AuthTokens`, `User`
|
|
16
|
+
|
|
17
|
+
**Components/Functions:** `useAuth`, `useAuthStore`
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { useAuth, useAuthStore, AuthTokens, ... } from '@/modules/auth-core';
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
import { useAuth, useAuthStore } from '@/modules/auth-core';
|
|
27
|
+
|
|
28
|
+
// Use the hook for auth operations
|
|
29
|
+
const {
|
|
30
|
+
user,
|
|
31
|
+
tokens,
|
|
32
|
+
isAuthenticated,
|
|
33
|
+
login,
|
|
34
|
+
register,
|
|
35
|
+
confirmEmail,
|
|
36
|
+
forgotPassword,
|
|
37
|
+
resetPassword,
|
|
38
|
+
logout,
|
|
39
|
+
api,
|
|
40
|
+
} = useAuth();
|
|
41
|
+
|
|
42
|
+
// Auth operations
|
|
43
|
+
await login(username, password);
|
|
44
|
+
await register(username, email, password);
|
|
45
|
+
await confirmEmail(username, code);
|
|
46
|
+
await forgotPassword(username);
|
|
47
|
+
await resetPassword(username, code, newPassword);
|
|
48
|
+
logout();
|
|
49
|
+
|
|
50
|
+
// Direct store access for state management
|
|
51
|
+
const setAuth = useAuthStore((state) => state.setAuth);
|
|
52
|
+
const clearAuth = useAuthStore((state) => state.clearAuth);
|
|
53
|
+
|
|
54
|
+
• Installed at: src/modules/auth-core/
|
|
55
|
+
• Provides: useAuth hook, useAuthStore
|
|
56
|
+
• Auto token refresh before expiry
|
|
57
|
+
• 401 interceptor with automatic retry
|
|
58
|
+
• Uses customerClient from api module internally
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Dependencies
|
|
62
|
+
|
|
63
|
+
This component requires:
|
|
64
|
+
- `api`
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Case Study Page
|
|
2
|
+
|
|
3
|
+
Detailed project case study with challenge/solution/results sections. Includes hero image, gallery, project details sidebar, and share buttons.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| Target | Type |
|
|
8
|
+
|--------|------|
|
|
9
|
+
| `$modules$/case-study-page/case-study-page.tsx` | component |
|
|
10
|
+
| `$modules$/case-study-page/index.ts` | index |
|
|
11
|
+
| `$modules$/case-study-page/lang/en.json` | lang |
|
|
12
|
+
| `$modules$/case-study-page/lang/tr.json` | lang |
|
|
13
|
+
|
|
14
|
+
## Exports
|
|
15
|
+
|
|
16
|
+
**Components/Functions:** `CaseStudyPage`, `default`
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { CaseStudyPage, default } from '@/modules/case-study-page';
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
import { CaseStudyPage } from '@/modules/case-study-page';
|
|
26
|
+
|
|
27
|
+
<CaseStudyPage />
|
|
28
|
+
|
|
29
|
+
• Installed at: src/modules/case-study-page/
|
|
30
|
+
• Customize content: lang/en/case-study-page.json
|
|
31
|
+
• Props: title, heroImage, gallery[], challenge, solution, results, details
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Dependencies
|
|
35
|
+
|
|
36
|
+
This component requires:
|
|
37
|
+
- `button`
|
|
38
|
+
- `animations`
|
|
39
|
+
- `share-buttons`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Checkout Page
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Full-featured checkout page with customer information form, payment method selection (card, bank transfer, cash on delivery), and online payment provider selection (Stripe, iyzico). Integrates with payment API for creating checkout sessions and fetching bank transfer info. Features form validation, order summary sidebar, and skeleton loading states.
|
|
4
4
|
|
|
5
5
|
## Files
|
|
6
6
|
|
|
@@ -37,3 +37,4 @@ import CheckoutPage from '@/modules/checkout-page';
|
|
|
37
37
|
This component requires:
|
|
38
38
|
- `ecommerce-core`
|
|
39
39
|
- `animations`
|
|
40
|
+
- `api`
|