popup-subscription into main #22

Open
davish wants to merge 2 commits from popup-subscription into main
13 changed files with 246 additions and 9 deletions

13
package-lock.json generated
View File

@ -4268,6 +4268,19 @@
"url": "https://dotenvx.com"
}
},
"node_modules/@prisma/config/node_modules/magicast": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz",
"integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@babel/parser": "^7.25.4",
"@babel/types": "^7.25.4",
"source-map-js": "^1.2.0"
}
},
"node_modules/@prisma/config/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",

View File

@ -17,3 +17,13 @@ model Contact {
@@index([email, createdAt])
}
model NewsletterSubscriber {
id Int @id @default(autoincrement())
email String
locale String?
createdAt DateTime @default(now())
@@unique([email, locale])
@@index([email])
}

View File

@ -0,0 +1,114 @@
<script setup>
import { ref, onMounted } from "vue";
import { Icon } from "@iconify/vue";
import { createTranslator } from "../i18n";
const props = defineProps({
locale: String,
});
const tl = createTranslator(props.locale);
const modalRef = ref(null);
const email = ref("");
const loading = ref(false);
const submitted = ref(false);
const STORAGE_KEY = "newsletter_popup_shown";
const closeModal = () => {
modalRef.value?.close();
};
const handleSubmit = async (e) => {
e.preventDefault();
loading.value = true;
try {
const response = await fetch("/api/newsletter/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.value, locale: props.locale }),
});
if (!response.ok) throw new Error();
submitted.value = true;
setTimeout(() => closeModal(), 1500);
} catch {
// silently ignore popup is non-critical
} finally {
loading.value = false;
}
};
onMounted(() => {
if (localStorage.getItem(STORAGE_KEY)) return;
localStorage.setItem(STORAGE_KEY, "1");
setTimeout(() => {
modalRef.value?.showModal();
}, 2000);
});
</script>
<template>
<dialog ref="modalRef" class="modal modal-bottom sm:modal-middle p-8">
<div class="modal-box bg-[#EBE6D2] text-[#22523F] rounded-none p-8 max-w-md">
<button
type="button"
@click="closeModal"
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
>
<Icon icon="ph:x" class="text-2xl" />
</button>
<template v-if="!submitted">
<h3 class="text-xl font-bold lg:text-2xl font-secondary text-center uppercase mb-4">
{{ tl("newsletter.popup.title") }}
</h3>
<p class="text-base font-primary text-center mb-6">
{{ tl("newsletter.popup.text") }}
</p>
<form @submit="handleSubmit" class="flex flex-col gap-3 items-center">
<label class="input rounded-none bg-white w-full">
<Icon icon="ph:envelope" class="text-xl text-[#22523F]" />
<input
v-model="email"
name="email"
type="email"
:placeholder="tl('newsletter.popup.placeholder')"
class="w-full rounded-none"
required
/>
</label>
<button
type="submit"
:disabled="loading"
class="btn w-full mt-2 bg-[#22523F] text-[#EBE6D2] border-0 hover:bg-[#1a3f30] hover:text-[#EBE6D2] uppercase rounded-none"
>
{{ loading ? tl("newsletter.popup.sending") : tl("newsletter.popup.button") }}
</button>
</form>
</template>
<template v-else>
<div class="flex flex-col items-center gap-4 py-4">
<Icon icon="ph:check-circle" class="text-5xl text-[#22523F]" />
<p class="text-lg font-bold font-secondary text-center uppercase">
{{ tl("newsletter.popup.success") }}
</p>
</div>
</template>
</div>
<form method="dialog" class="modal-backdrop">
<button @click="closeModal">close</button>
</form>
</dialog>
</template>

View File

@ -203,6 +203,14 @@
"footer.form.name": "Name and Lastname",
"footer.form.mesagge": "Write a message",
"footer.form.button": "Send",
"footer.reserved": "©2026 All Rights Reserved. Kingdom of Peace and Justice Center"
"footer.reserved": "©2026 All Rights Reserved. Kingdom of Peace and Justice Center",
"newsletter.popup.title": "Stay informed",
"newsletter.popup.text": "Subscribe to receive the latest news and updates from the Kingdom of Peace and Justice Center.",
"newsletter.popup.placeholder": "Your email address",
"newsletter.popup.button": "Subscribe",
"newsletter.popup.sending": "Sending...",
"newsletter.popup.success": "Thank you for subscribing!",
"newsletter.popup.close": "Close"
}

View File

@ -208,5 +208,13 @@
"footer.form.name": "Nombre y Apellido",
"footer.form.mesagge": "Escriba su mensaje",
"footer.form.button": "Enviar",
"footer.reserved": "©2026. Todos los Derechos Reservados. Centro del Reino de Paz y Justicia"
"footer.reserved": "©2026. Todos los Derechos Reservados. Centro del Reino de Paz y Justicia",
"newsletter.popup.title": "Mantente informado",
"newsletter.popup.text": "Suscríbete para recibir las últimas noticias y novedades del Centro del Reino de Paz y Justicia.",
"newsletter.popup.placeholder": "Tu correo electrónico",
"newsletter.popup.button": "Suscribirme",
"newsletter.popup.sending": "Enviando...",
"newsletter.popup.success": "¡Gracias por suscribirte!",
"newsletter.popup.close": "Cerrar"
}

View File

@ -4,5 +4,12 @@
"nav.about": "À propos",
"nav.news": "Nouvelles",
"nav.programs": "Programmes",
"nav.contact": "Contactez"
"nav.contact": "Contactez",
"newsletter.popup.title": "Restez informé",
"newsletter.popup.text": "Abonnez-vous pour recevoir les dernières nouvelles et mises à jour du Centre du Royaume de Paix et de Justice.",
"newsletter.popup.placeholder": "Votre adresse e-mail",
"newsletter.popup.button": "S'abonner",
"newsletter.popup.sending": "Envoi en cours...",
"newsletter.popup.success": "Merci pour votre abonnement !",
"newsletter.popup.close": "Fermer"
}

View File

@ -203,7 +203,14 @@
"footer.form.name": "שם מלא",
"footer.form.mesagge": "כתיבת הודעה",
"footer.form.button": "שליחה",
"footer.reserved": "כל הזכויות שמורות 2026 © Centro del Reino de Paz y Justicia"
"footer.reserved": "כל הזכויות שמורות 2026 © Centro del Reino de Paz y Justicia",
"newsletter.popup.title": "הישארו מעודכנים",
"newsletter.popup.text": "הירשמו לקבלת החדשות והעדכונים האחרונים ממרכז הממלכה לשלום וצדק.",
"newsletter.popup.placeholder": "כתובת הדוא\"ל שלכם",
"newsletter.popup.button": "הרשמה",
"newsletter.popup.sending": "שולח...",
"newsletter.popup.success": "תודה שנרשמתם!",
"newsletter.popup.close": "סגור"
}

View File

@ -4,5 +4,12 @@
"nav.about": "Somos",
"nav.news": "Noticias",
"nav.programs": "Programas",
"nav.contact": "Contacto"
"nav.contact": "Contacto",
"newsletter.popup.title": "Fique informado",
"newsletter.popup.text": "Inscreva-se para receber as últimas notícias e atualizações do Centro do Reino da Paz e da Justiça.",
"newsletter.popup.placeholder": "Seu endereço de e-mail",
"newsletter.popup.button": "Inscrever-se",
"newsletter.popup.sending": "Enviando...",
"newsletter.popup.success": "Obrigado por se inscrever!",
"newsletter.popup.close": "Fechar"
}

View File

@ -203,7 +203,14 @@
"footer.form.name": "Имя и фамилия",
"footer.form.mesagge": "Напишите свое сообщение",
"footer.form.button": "Отправить",
"footer.reserved": "© 2026. Все права защищены. Центр Царства мира и справедливости"
"footer.reserved": "© 2026. Все права защищены. Центр Царства мира и справедливости",
"newsletter.popup.title": "Будьте в курсе",
"newsletter.popup.text": "Подпишитесь, чтобы получать последние новости и обновления от Центра Царства мира и справедливости.",
"newsletter.popup.placeholder": "Ваш адрес электронной почты",
"newsletter.popup.button": "Подписаться",
"newsletter.popup.sending": "Отправка...",
"newsletter.popup.success": "Спасибо за подписку!",
"newsletter.popup.close": "Закрыть"
}

View File

@ -208,5 +208,12 @@
"footer.form.name": "Amazina yombi",
"footer.form.mesagge": "Andika ubutumwa bwawe",
"footer.form.button": "Ohereza",
"footer.reserved": "© 2026. Uburenganzira bwose bufitwe na Centro del Reino de Paz y Justicia(Ihuriro ryUbwami bwAmahoro nUbutabera)"
"footer.reserved": "© 2026. Uburenganzira bwose bufitwe na Centro del Reino de Paz y Justicia(Ihuriro ryUbwami bwAmahoro nUbutabera)",
"newsletter.popup.title": "Komeza uhabwe amakuru",
"newsletter.popup.text": "Iyandikishe kugira ngo ubone amakuru mashya namakuru yIhuriro ryUbwami bwAmahoro nUbutabera.",
"newsletter.popup.placeholder": "Aderesi yawe ya imeyili",
"newsletter.popup.button": "Iyandikishe",
"newsletter.popup.sending": "Kohereza...",
"newsletter.popup.success": "Urakoze kwiyandikisha!",
"newsletter.popup.close": "Funga"
}

View File

@ -4,5 +4,12 @@
"nav.about": "Somos",
"nav.news": "Noticias",
"nav.programs": "Programas",
"nav.contact": "Contacto"
"nav.contact": "Contacto",
"newsletter.popup.title": "Будьте в курсі",
"newsletter.popup.text": "Підпишіться, щоб отримувати останні новини та оновлення від Центру Царства Миру та Справедливості.",
"newsletter.popup.placeholder": "Ваша електронна адреса",
"newsletter.popup.button": "Підписатися",
"newsletter.popup.sending": "Надсилання...",
"newsletter.popup.success": "Дякуємо за підписку!",
"newsletter.popup.close": "Закрити"
}

View File

@ -8,6 +8,7 @@ import "@fontsource/poppins/500.css";
import "@fontsource/poppins/700.css";
import "@fontsource-variable/kameron";
import ShareSticky from "../components/ShareSticky.vue";
import NewsletterPopup from "../components/NewsletterPopup.vue";
const {
title,
description,
@ -38,6 +39,7 @@ const direction = currentLocale === 'he' ? 'rtl' : 'ltr';
{Astro.url.pathname.includes('/news/') && (
<ShareSticky client:only url={Astro.url.href} />
)}
<NewsletterPopup client:only locale={currentLocale} />
<slot />
<Footer />
</body>

View File

@ -0,0 +1,40 @@
import type { APIRoute } from "astro";
import { prisma } from "../lib/prisma";
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
try {
const body = await request.json();
const email = body.email?.toString().trim().toLowerCase();
const locale = body.locale?.toString().trim() || "";
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return new Response(JSON.stringify({ error: "Email inválido" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
const existing = await prisma.newsletterSubscriber.findUnique({
where: { email_locale: { email, locale } },
});
if (!existing) {
await prisma.newsletterSubscriber.create({ data: { email, locale } });
}else{
console.log(`Email ${email} já está inscrito para a locale ${locale}`);
}
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error(error);
return new Response(JSON.stringify({ error: "Error interno" }), {
status: 500,
headers: { "Content-Type": "application/json" },
});
}
};