Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"use client";

import { useState } from "react";

import type { SupportedLanguage } from "@/locales/.generated/types";
import {
aiNewsletterCta,
aiNewsletterDisclaimer,
aiNewsletterError,
aiNewsletterHeading,
aiNewsletterLanguageEn,
aiNewsletterLanguageId,
aiNewsletterLanguageLabel,
aiNewsletterLanguageSu,
aiNewsletterPlaceholder,
aiNewsletterSubheading,
aiNewsletterSuccess
} from "@/locales/.generated/strings";

type NewsletterSectionProps = {
lang: SupportedLanguage;
};

export function NewsletterSection({ lang }: NewsletterSectionProps) {
const [email, setEmail] = useState("");
const [digestLang, setDigestLang] = useState<"en" | "id" | "su">(
lang === "id" ? "id" : "en"
);
const [status, setStatus] = useState<
"idle" | "loading" | "success" | "error"
>("idle");

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const trimmed = email.trim();
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!trimmed || !emailRegex.test(trimmed)) {
setStatus("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 })
});
if (res.ok) {
setStatus("success");
} else {
setStatus("error");
}
} catch {
setStatus("error");
}
};

return (
<section className="bg-[#F6F8F9] py-8 md:py-16">
<div className="text-hyperjump-black mx-auto flex w-full max-w-5xl flex-col items-center px-4 text-center md:px-20 xl:px-0">
<h2 className="mb-3 text-[34px] font-medium md:text-4xl">
{aiNewsletterHeading(lang)}
</h2>
<p className="mb-6 max-w-xl text-lg leading-relaxed text-gray-600">
{aiNewsletterSubheading(lang)}
</p>

{/* Language dropdown */}
<div className="mb-4 flex items-center gap-2 text-sm text-gray-500">
<label htmlFor="digest-lang">{aiNewsletterLanguageLabel(lang)}</label>
<select
id="digest-lang"
value={digestLang}
onChange={(e) =>
setDigestLang(e.target.value as "en" | "id" | "su")
}
className="rounded border border-gray-300 px-2 py-1 text-sm text-gray-700 focus:border-blue-500 focus:ring-1 focus:ring-blue-200 focus:outline-none">
<option value="en">{aiNewsletterLanguageEn(lang)}</option>
<option value="id">{aiNewsletterLanguageId(lang)}</option>
<option value="su">{aiNewsletterLanguageSu(lang)}</option>
</select>
</div>

<form
onSubmit={handleSubmit}
className="flex w-full max-w-xl flex-col gap-3 sm:flex-row">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={aiNewsletterPlaceholder(lang)}
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"
/>
<button
type="submit"
disabled={status === "loading" || status === "success"}
className="rounded-lg bg-blue-600 px-6 py-3 text-base font-semibold text-white shadow-sm transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60">
{status === "loading" ? "..." : aiNewsletterCta(lang)}
</button>
</form>

{status === "success" && (
<p className="mt-4 text-sm font-medium text-green-600">
{aiNewsletterSuccess(lang)}
</p>
)}
{status === "error" && (
<p className="mt-4 text-sm font-medium text-red-500">
{aiNewsletterError(lang)}
</p>
)}

<p className="mt-4 text-sm text-gray-400">
{aiNewsletterDisclaimer(lang)}
</p>
</div>
</section>
);
}
2 changes: 2 additions & 0 deletions app/[lang]/(hyperjump)/services/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -107,6 +108,7 @@ export default async function ServiceDetail({ params }: ServiceDetailProps) {
<HowItWorks lang={lang} service={service} />
<WhatYouGet lang={lang} service={service} />
<WhyUs lang={lang} service={service} />
{slug === ServiceSlug.InferenceAI && <NewsletterSection lang={lang} />}
<Faqs lang={lang} service={service} />
<CaseStudies caseStudies={service.caseStudies} lang={lang} />
<CallToAction lang={lang} service={service} />
Expand Down
48 changes: 48 additions & 0 deletions app/api/subscribe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
const body = await req.json();
const { email, language } = body;

// Basic validation server-side
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!email || !emailRegex.test(email.trim())) {
return NextResponse.json({ error: "Invalid email" }, { status: 422 });
}

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 });
}
13 changes: 13 additions & 0 deletions locales/en/ai.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@
"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": "You're in! We'll be in touch soon.",
"error": "Something went wrong. Please try again.",
"languageLabel": "Digest language:",
"languageEn": "English",
"languageId": "Bahasa Indonesia",
"languageSu": "Basa Sunda"
},
"faq": {
"heading": "Frequently asked questions",
"desc": "Everything you need to go from concept to fully deployed Al agent-done for you, end to end.",
Expand Down
13 changes: 13 additions & 0 deletions locales/id/ai.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@
"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": "Berhasil! Kami akan segera menghubungi kamu.",
"error": "Terjadi kesalahan. Silakan coba lagi.",
"languageLabel": "Bahasa digest:",
"languageEn": "English",
"languageId": "Bahasa Indonesia",
"languageSu": "Basa Sunda"
},
"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.",
Expand Down
Loading