diff --git a/.env.example b/.env.example index e86e5a879..ae3d4b9f0 100644 --- a/.env.example +++ b/.env.example @@ -3,4 +3,8 @@ NEXT_PUBLIC_LANDING_GET_CHATS_WEBHOOK= NEXT_PUBLIC_LANDING_GET_FOLLOWUP_WEBHOOK= NEXT_PUBLIC_INFERENCEAI_POST_CHATS_WEBHOOK= NEXT_PUBLIC_INFERENCEAI_GET_CHATS_WEBHOOK= -NEXT_PUBLIC_GA_TRACKING_ID= \ No newline at end of file +NEXT_PUBLIC_GA_TRACKING_ID= +FRONTIERNOTES_API_URL= +SUBSCRIBE_API_KEY= +NEXT_PUBLIC_TURNSTILE_SITE_KEY= +TURNSTILE_SECRET_KEY= \ No newline at end of file diff --git a/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx b/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx new file mode 100644 index 000000000..09f0931a1 --- /dev/null +++ b/app/[lang]/(hyperjump)/services/[slug]/components/newsletter-section.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { Turnstile, type TurnstileInstance } from "@marsidev/react-turnstile"; +import { useRef, useState } from "react"; + +import type { SupportedLanguage } from "@/locales/.generated/types"; + +type NewsletterSectionProps = { + lang: SupportedLanguage; +}; + +type DigestLang = "en" | "id" | "su" | "de"; + +type NewsletterCopy = { + heading: string; + subheading: string; + placeholder: string; + cta: string; + disclaimer: string; + success: string; + error: string; + captchaError: string; + languageLabel: string; + languageEn: string; + languageId: string; + languageSu: string; + languageDe: string; +}; + +const newsletterCopyByDigestLang: Record< + "en" | "id" | "de" | "su", + NewsletterCopy +> = { + en: { + heading: "Stay Ahead of the Curve", + subheading: + "Get practical AI insights, agent architecture breakdowns, and real-world implementation stories — straight to your inbox.", + placeholder: "Enter your email", + cta: "Subscribe", + disclaimer: "No spam. Unsubscribe anytime.", + success: + "Almost there! Check your inbox to confirm your subscription.", + error: "Something went wrong. Please try again.", + captchaError: "Please complete the CAPTCHA verification.", + languageLabel: "Digest language:", + languageEn: "English", + languageId: "Bahasa Indonesia", + languageSu: "Basa Sunda", + languageDe: "Deutsch" + }, + id: { + heading: "Selangkah Lebih Maju", + subheading: + "Dapatkan insight AI praktis, breakdown arsitektur agent, dan kisah implementasi nyata — langsung ke kotak masukmu.", + placeholder: "Masukkan email kamu", + cta: "Langganan", + disclaimer: "Tanpa spam. Bisa berhenti berlangganan kapan saja.", + success: "Hampir selesai! Cek email kamu untuk konfirmasi langganan.", + error: "Terjadi kesalahan. Silakan coba lagi.", + captchaError: "Mohon selesaikan verifikasi CAPTCHA terlebih dahulu.", + languageLabel: "Bahasa digest:", + languageEn: "English", + languageId: "Bahasa Indonesia", + languageSu: "Basa Sunda", + languageDe: "Deutsch" + }, + de: { + heading: "Bleiben Sie immer einen Schritt voraus", + subheading: + "Erhalte praxisnahe KI-Einblicke, detaillierte Agent-Architekturen und Berichte aus echten Implementierungen — direkt in dein Postfach.", + placeholder: "Gib deine E-Mail-Adresse ein", + cta: "Abonnieren", + disclaimer: "Kein Spam. Jederzeit abbestellbar.", + success: + "Fast geschafft! Bestätige dein Abonnement über den Link in deinem Postfach.", + error: "Etwas ist schiefgelaufen. Bitte versuche es erneut.", + captchaError: "Bitte schließe die CAPTCHA-Verifizierung ab.", + languageLabel: "Digest-Sprache:", + languageEn: "English", + languageId: "Bahasa Indonesia", + languageSu: "Basa Sunda", + languageDe: "Deutsch" + }, + su: { + heading: "Salangkung Leuwih Maju", + subheading: + "Kenging insight AI anu praktis, bedah arsitektur agent, sareng carita implementasi nyata — langsung ka kotak surel anjeun.", + placeholder: "Lebetkeun email anjeun", + cta: "Langganan", + disclaimer: "Tanpa spam. Tiasa eureun langganan iraha wae.", + success: "Ampir rengse! Pariksa email anjeun kanggo konfirmasi langganan.", + error: "Aya kasalahan. Mangga cobian deui.", + captchaError: "Mangga lengkepan verifikasi CAPTCHA heula.", + languageLabel: "Basa digest:", + languageEn: "English", + languageId: "Bahasa Indonesia", + languageSu: "Basa Sunda", + languageDe: "Deutsch" + } + }; + +export function NewsletterSection({ lang }: NewsletterSectionProps) { + const [email, setEmail] = useState(""); + const [digestLang, setDigestLang] = useState( + lang === "id" ? "id" : "en" + ); + const [status, setStatus] = useState< + "idle" | "loading" | "success" | "error" | "captcha-error" + >("idle"); + const [captchaToken, setCaptchaToken] = useState(null); + const turnstileRef = useRef(null); + + const copy = newsletterCopyByDigestLang[digestLang] ?? + newsletterCopyByDigestLang.en; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const trimmed = email.trim(); + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!trimmed || !emailRegex.test(trimmed)) { + setStatus("error"); + return; + } + + if (!captchaToken) { + setStatus("captcha-error"); + return; + } + + setStatus("loading"); + try { + const res = await fetch("/api/subscribe", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: trimmed, + language: digestLang, + turnstileToken: captchaToken + }) + }); + if (res.ok) { + setStatus("success"); + } else { + setStatus("error"); + } + } catch { + setStatus("error"); + } finally { + setCaptchaToken(null); + turnstileRef.current?.reset(); + } + }; + + return ( +
+
+

+ {copy.heading} +

+

+ {copy.subheading} +

+ + {/* Language dropdown */} +
+ + +
+ +
+
+ setEmail(e.target.value)} + placeholder={copy.placeholder} + disabled={status === "loading" || status === "success"} + className="text-hyperjump-black flex-1 rounded-lg border border-gray-200 bg-white px-4 py-3 text-base placeholder-gray-400 shadow-sm outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-200 disabled:opacity-50" + /> + +
+ +
+ setCaptchaToken(token)} + onExpire={() => setCaptchaToken(null)} + onError={() => setCaptchaToken(null)} + /> +
+
+ + {status === "success" && ( +

{copy.success}

+ )} + {status === "error" && ( +

{copy.error}

+ )} + {status === "captcha-error" && ( +

+ {copy.captchaError} +

+ )} + +

{copy.disclaimer}

+
+
+ ); +} diff --git a/app/[lang]/(hyperjump)/services/[slug]/page.tsx b/app/[lang]/(hyperjump)/services/[slug]/page.tsx index 878a6a5a0..2add8e21b 100644 --- a/app/[lang]/(hyperjump)/services/[slug]/page.tsx +++ b/app/[lang]/(hyperjump)/services/[slug]/page.tsx @@ -35,6 +35,7 @@ import { mainServicesLabel } from "@/locales/.generated/strings"; +import { NewsletterSection } from "./components/newsletter-section"; import { AnimatedLines } from "../../components/animated-lines"; import { CaseStudyCard } from "../../components/case-study-card"; import { SectionReveal } from "../../components/motion-wrappers"; @@ -107,6 +108,7 @@ export default async function ServiceDetail({ params }: ServiceDetailProps) { + {slug === ServiceSlug.InferenceAI && } diff --git a/app/api/subscribe/route.ts b/app/api/subscribe/route.ts new file mode 100644 index 000000000..d693fa4b4 --- /dev/null +++ b/app/api/subscribe/route.ts @@ -0,0 +1,82 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(req: NextRequest) { + const body = await req.json(); + const { email, language, turnstileToken } = body; + + // Basic validation server-side + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!email || !emailRegex.test(email.trim())) { + return NextResponse.json({ error: "Invalid email" }, { status: 422 }); + } + + if (!turnstileToken) { + return NextResponse.json( + { error: "Missing CAPTCHA verification" }, + { status: 422 } + ); + } + + const turnstileVerifyRes = await fetch( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + secret: process.env.TURNSTILE_SECRET_KEY, + response: turnstileToken, + remoteip: req.headers.get("x-forwarded-for") ?? undefined + }) + } + ); + + let turnstileData: { success?: boolean } = {}; + try { + turnstileData = await turnstileVerifyRes.json(); + } catch { + // Cloudflare returned non-JSON, treat as verification failure below + } + + if (turnstileData.success !== true) { + return NextResponse.json( + { error: "CAPTCHA verification failed" }, + { status: 403 } + ); + } + + const apiKey = process.env.SUBSCRIBE_API_KEY; + const apiUrl = + process.env.FRONTIERNOTES_API_URL ?? "https://tech-monitor.fly.dev"; + + if (!apiKey) { + return NextResponse.json({ error: "Service unavailable" }, { status: 503 }); + } + + const res = await fetch(`${apiUrl}/api/subscribe`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}` + }, + body: JSON.stringify({ + email: email.trim().toLowerCase(), + language: language ?? "en" + }) + }); + + let data: { status?: string } = {}; + try { + data = await res.json(); + } catch { + // FrontierNotes returned non-JSON (e.g. plain text error) + } + + if (res.status === 201 || res.status === 200) { + return NextResponse.json( + { status: data.status ?? "subscribed" }, + { status: 200 } + ); + } + + return NextResponse.json({ error: "Subscription failed" }, { status: 500 }); +} diff --git a/bun.lock b/bun.lock index c7e1a12c1..75d68fea0 100644 --- a/bun.lock +++ b/bun.lock @@ -1,10 +1,10 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "hyperjump.tech", "dependencies": { + "@marsidev/react-turnstile": "^1.5.3", "@mdx-js/loader": "3.1.1", "@mdx-js/react": "3.1.1", "@n8n/chat": "0.61.0", @@ -213,6 +213,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@marsidev/react-turnstile": ["@marsidev/react-turnstile@1.5.3", "", { "peerDependencies": { "react": "^17.0.2 || ^18.0.0 || ^19.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0" } }, "sha512-8Dij2jiNGNczq1U4EKpO4do2XepcTPxSMc2ZzvHndO+gcp68tvMULm27z2P99rGkdB89hc3452NZeu2Rti4g6A=="], + "@mdx-js/loader": ["@mdx-js/loader@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "source-map": "^0.7.0" }, "peerDependencies": { "webpack": ">=5" }, "optionalPeers": ["webpack"] }, "sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ=="], "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], diff --git a/locales/en/ai.json b/locales/en/ai.json index 2ac7d917b..32980f6f4 100644 --- a/locales/en/ai.json +++ b/locales/en/ai.json @@ -103,6 +103,21 @@ "title": "Our Products", "description": "Innovative products tailored to support your goals and scale with your needs." }, + "newsletter": { + "heading": "Stay Ahead of the Curve", + "subheading": "Get practical AI insights, agent architecture breakdowns, and real-world implementation stories — straight to your inbox.", + "placeholder": "Enter your email", + "cta": "Subscribe", + "disclaimer": "No spam. Unsubscribe anytime.", + "success": "Almost there! Check your inbox to confirm your subscription.", + "error": "Something went wrong. Please try again.", + "captchaError": "Please complete the CAPTCHA verification.", + "languageLabel": "Digest language:", + "languageEn": "English", + "languageId": "Bahasa Indonesia", + "languageSu": "Basa Sunda", + "languageDe": "Deutsch" + }, "faq": { "heading": "Frequently asked questions", "desc": "Everything you need to go from concept to fully deployed Al agent-done for you, end to end.", diff --git a/locales/id/ai.json b/locales/id/ai.json index 266f81935..f67ddc47f 100644 --- a/locales/id/ai.json +++ b/locales/id/ai.json @@ -103,6 +103,21 @@ "title": "Produk Kami", "description": "Produk inovatif yang dirancang untuk mendukung tujuan Anda dan berkembang sesuai kebutuhan Anda." }, + "newsletter": { + "heading": "Selangkah Lebih Maju", + "subheading": "Dapatkan insight AI praktis, breakdown arsitektur agent, dan kisah implementasi nyata — langsung ke kotak masukmu.", + "placeholder": "Masukkan email kamu", + "cta": "Langganan", + "disclaimer": "Tanpa spam. Bisa berhenti berlangganan kapan saja.", + "success": "Hampir selesai! Cek email kamu untuk konfirmasi langganan.", + "error": "Terjadi kesalahan. Silakan coba lagi.", + "captchaError": "Mohon selesaikan verifikasi CAPTCHA terlebih dahulu.", + "languageLabel": "Bahasa digest:", + "languageEn": "English", + "languageId": "Bahasa Indonesia", + "languageSu": "Basa Sunda", + "languageDe": "Deutsch" + }, "faq": { "heading": "Pertanyaan yang Sering Diajukan", "desc": "Segala hal yang Anda butuhkan untuk mewujudkan agen AI dari konsep hingga implementasi penuh dikerjakan secara menyeluruh oleh kami.", diff --git a/package.json b/package.json index 07798ea6d..dbe3fd4fe 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "test:e2e:report": "playwright show-report" }, "dependencies": { + "@marsidev/react-turnstile": "^1.5.3", "@mdx-js/loader": "3.1.1", "@mdx-js/react": "3.1.1", "@n8n/chat": "0.61.0",