From 77f3c4ddbdc668841637365581d057e9072997a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=A9o=20GUILBERT?= Date: Tue, 2 Jun 2026 19:32:50 +0200 Subject: [PATCH] add BlogCTASection and BlogSubForm components with translations for blog subscription --- .../blog/_components/BlogCTASection.tsx | 70 ++++++++ app/[locale]/blog/page.tsx | 14 ++ components/ui/BlogSubFormController.tsx | 9 + components/ui/BlogSubFormModal.tsx | 158 ++++++++++++++++++ messages/en.json | 5 + messages/fr.json | 5 + 6 files changed, 261 insertions(+) create mode 100644 app/[locale]/blog/_components/BlogCTASection.tsx create mode 100644 components/ui/BlogSubFormController.tsx create mode 100644 components/ui/BlogSubFormModal.tsx diff --git a/app/[locale]/blog/_components/BlogCTASection.tsx b/app/[locale]/blog/_components/BlogCTASection.tsx new file mode 100644 index 0000000..ecb44f0 --- /dev/null +++ b/app/[locale]/blog/_components/BlogCTASection.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { useRef, useEffect } from 'react'; +import Link from 'next/link'; + +type Props = { + locale: string; + title: string; + text: string; + cta: string; +}; + +export function BlogCTASection({ locale, title, text, cta }: Props) { + const sectionRef = useRef(null); + + useEffect(() => { + const section = sectionRef.current; + if (!section) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.querySelectorAll('.reveal').forEach((el, i) => { + setTimeout(() => el.classList.add('visible'), i * 120); + }); + observer.unobserve(entry.target); + } + }); + }, + { threshold: 0.1 } + ); + + observer.observe(section); + return () => observer.disconnect(); + }, []); + + return ( +
+
+
+

+ {title} +

+

+ {text} +

+
+ +
+ + {cta} + + +
+
+
+ ); +} diff --git a/app/[locale]/blog/page.tsx b/app/[locale]/blog/page.tsx index 93192da..afc0583 100644 --- a/app/[locale]/blog/page.tsx +++ b/app/[locale]/blog/page.tsx @@ -1,6 +1,9 @@ import { getAllArticles } from "@/lib/blog/get-articles"; import { BlogHero } from "@/app/[locale]/blog/_components/BlogHero"; +import { BlogCTASection } from "@/app/[locale]/blog/_components/BlogCTASection"; import { ArticleList } from "@/app/[locale]/blog/[slug]/_components/ArticleList"; +import { getTranslations } from "next-intl/server"; +import { BlogSubFormController } from '@/components/ui/BlogSubFormController'; type props = { params: Promise<{ locale: string }>; @@ -8,11 +11,22 @@ type props = { export default async function BlogPage({ params }: props) { const { locale } = await params; + const t = await getTranslations('blogPage.blogSubscription'); const articles = getAllArticles(locale); return (
+ + + + +
); diff --git a/components/ui/BlogSubFormController.tsx b/components/ui/BlogSubFormController.tsx new file mode 100644 index 0000000..df6a0f2 --- /dev/null +++ b/components/ui/BlogSubFormController.tsx @@ -0,0 +1,9 @@ +'use client'; + +import { useState } from 'react'; +import { BlogSubFormModal } from '@/components/ui/BlogSubFormModal'; + +export function BlogSubFormController() { + const [subOpen, setSubOpen] = useState(false); + return setSubOpen(false)} />; +} diff --git a/components/ui/BlogSubFormModal.tsx b/components/ui/BlogSubFormModal.tsx new file mode 100644 index 0000000..ce802ae --- /dev/null +++ b/components/ui/BlogSubFormModal.tsx @@ -0,0 +1,158 @@ +'use client'; + +import { useEffect, useRef, useState, useActionState } from 'react'; +import { createPortal } from 'react-dom'; +import { sendContactMail, type ContactFormState } from '@/app/actions/sendContactMail'; + +type Props = { + isOpen: boolean; + onClose: () => void; +}; + +type FieldProps = { + label: string; + name: string; + type?: string; + required?: boolean; + textarea?: boolean; + inputRef?: React.RefObject; +}; + +function Field({ label, name, type = 'text', required, textarea, inputRef }: FieldProps) { + const base = + 'w-full font-sans text-sm text-brand-brown bg-white/60 border border-brand-brown/12 rounded-lg px-4 py-3 placeholder:text-brand-brown/25 focus:outline-none focus:border-brand-brown/35 focus:bg-white/80 transition-colors duration-150'; + + return ( +
+ + {textarea ? ( +