Podmiana image w hero, Kontakt pole readonly dla danych z ofert dodanie do tresci maila

This commit is contained in:
dm
2025-12-16 05:00:33 +01:00
parent f213b74caf
commit 6c91584fe1
6 changed files with 91 additions and 79 deletions

View File

@@ -5,8 +5,7 @@
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"build": "astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview"
"update:jambox:base": "node src/scripts/update-jambox-base.js"
}, },
"dependencies": { "dependencies": {
"@astrojs/node": "^9.5.1", "@astrojs/node": "^9.5.1",

View File

@@ -8,7 +8,7 @@ subtitle:
description: | description: |
imageUrl: "fiber.webp" imageUrl: "section-tv.webp"
ctas: ctas:
- label: "Zobacz ofertę Internetu" - label: "Zobacz ofertę Internetu"
href: "/internet-swiatlowodowy" href: "/internet-swiatlowodowy"

View File

@@ -512,7 +512,7 @@ export default function InternetAddonsModal({
</div> </div>
<a <a
href="/kontakt" href="/kontakt#form"
class="btn btn-primary w-full mt-4" class="btn btn-primary w-full mt-4"
onClick={() => saveOfferToLocalStorage()} onClick={() => saveOfferToLocalStorage()}
> >
@@ -523,7 +523,6 @@ export default function InternetAddonsModal({
</SectionAccordion> </SectionAccordion>
</div> </div>
{/* ✅ pływająca suma (ten sam styl co w Jambox, jeśli już masz CSS) */}
<div class="f-floating-total" onClick={(e) => e.stopPropagation()}> <div class="f-floating-total" onClick={(e) => e.stopPropagation()}>
<div class="f-floating-total-circle" role="status" aria-label="Cena miesięczna"> <div class="f-floating-total-circle" role="status" aria-label="Cena miesięczna">
<span class="f-floating-total-unit">Razem</span> <span class="f-floating-total-unit">Razem</span>

View File

@@ -10,6 +10,7 @@ function esc(str = "") {
function buildHtmlMail(form) { function buildHtmlMail(form) {
const when = new Date().toLocaleString("pl-PL"); const when = new Date().toLocaleString("pl-PL");
const offerText = (form.offerSummary || "").trim()
return `<!DOCTYPE html> return `<!DOCTYPE html>
<html lang="pl"> <html lang="pl">
@@ -79,6 +80,17 @@ function buildHtmlMail(form) {
${esc(form.message)} ${esc(form.message)}
</div> </div>
${offerText ? `
<div style="font-size:14px;color:#111827;margin:8px 0;font-weight:700;">
Wybrana oferta:
</div>
<div style="font-size:14px;color:#111827;white-space:pre-line;line-height:1.6;
background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:14px;">
${esc(offerText)}
</div>
` : ``}
<div style="margin-top:16px;font-size:12px;color:#6b7280;"> <div style="margin-top:16px;font-size:12px;color:#6b7280;">
Wysłano: ${when} Wysłano: ${when}
</div> </div>

View File

@@ -1,16 +1,17 @@
--- ---
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro"; import DefaultLayout from "../../layouts/DefaultLayout.astro";
import MapGoogle from "../../components/maps/MapGoogle.astro"; import MapGoogle from "../../components/maps/MapGoogle.astro";
import { loadYamlFile } from "../../lib/loadYaml";
import yaml from "js-yaml"; type SeoYaml = any;
import fs from "fs"; const seo = loadYamlFile<SeoYaml>(
path.join(process.cwd(), "src", "content", "contact", "seo.yaml"),
const data = yaml.load(
fs.readFileSync("./src/content/contact/contact.yaml", "utf8"),
); );
const seo = yaml.load( type ContactData = any;
fs.readFileSync("./src/content/internet-swiatlowodowy/seo.yaml", "utf8"), const data = loadYamlFile<ContactData>(
path.join(process.cwd(), "src", "content", "contact", "contact.yaml"),
); );
const apiKey = import.meta.env.PUBLIC_GOOGLE_MAPS_KEY; const apiKey = import.meta.env.PUBLIC_GOOGLE_MAPS_KEY;
@@ -25,8 +26,9 @@ const form = data.form;
<div class="f-contact-item" set:html={data.description} /> <div class="f-contact-item" set:html={data.description} />
</div> </div>
<div> <div id="form">
<h2 class="f-section-title">{data.contactFormTitle}</h2> <h2 class="f-section-title">{data.contactFormTitle}</h2>
<form id="contactForm" class="f-contact-form"> <form id="contactForm" class="f-contact-form">
<div class="f-contact-form-inner"> <div class="f-contact-form-inner">
<input <input
@@ -77,7 +79,20 @@ const form = data.form;
rows={form.message.rows} rows={form.message.rows}
placeholder={form.message.placeholder} placeholder={form.message.placeholder}
class="f-input" class="f-input"
required></textarea> required
></textarea>
<!-- widoczne tylko gdy jest oferta -->
<div id="offerSummaryWrap" class="hidden">
<textarea
id="offerSummary"
name="offerSummary"
rows="6"
class="f-input"
readonly
placeholder="Wybrana oferta pojawi się tutaj."
></textarea>
</div>
<label class="f-rodo"> <label class="f-rodo">
<input type="checkbox" name="rodo" required /> <input type="checkbox" name="rodo" required />
@@ -117,8 +132,6 @@ const form = data.form;
</div> </div>
<div id="toast" class="f-toast"></div> <div id="toast" class="f-toast"></div>
<input type="hidden" name="offerConfig" id="offerConfig" />
</section> </section>
<!-- ReCaptcha v3 --> <!-- ReCaptcha v3 -->
@@ -142,59 +155,32 @@ const form = data.form;
}} }}
> >
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("contactForm"); const formEl = document.getElementById("contactForm");
const toast = document.getElementById("toast"); const toast = document.getElementById("toast");
if (!formEl) return;
if (!form) return; const LS_KEY = "fuz_offer_config_v1";
const SS_INJECTED_KEY = "fuz_offer_injected_v1";
form.addEventListener("submit", async (e) => {
if (!form.reportValidity()) return;
e.preventDefault();
const data = Object.fromEntries(new FormData(form).entries());
data.rodo = form.rodo.checked;
const token = await grecaptcha.execute(window.FUZ_RECAPTCHA_KEY, {
action: "submit",
});
data.recaptcha = token;
const resp = await fetch("/api/contact/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const json = await resp.json();
showToast(
json.ok ? successMsg : errorMsg,
json.ok ? "success" : "error",
);
if (json.ok) form.reset();
});
function showToast(msg, type) { function showToast(msg, type) {
if (!toast) return;
toast.innerHTML = `<div class="f-toast-msg ${type}">${msg}</div>`; toast.innerHTML = `<div class="f-toast-msg ${type}">${msg}</div>`;
toast.classList.remove("visible"); toast.classList.remove("visible");
void toast.offsetWidth; void toast.offsetWidth;
toast.classList.add("visible"); toast.classList.add("visible");
setTimeout(() => toast.classList.remove("visible"), 3000); setTimeout(() => toast.classList.remove("visible"), 3000);
} }
// ----------
const LS_KEY = "fuz_offer_config_v1";
const SS_INJECTED_KEY = "fuz_offer_injected_v1";
function clearOfferDraft() { function clearOfferDraft() {
try { try {
localStorage.removeItem(LS_KEY); localStorage.removeItem(LS_KEY);
sessionStorage.removeItem(SS_INJECTED_KEY); sessionStorage.removeItem(SS_INJECTED_KEY);
const hidden = document.getElementById("offerConfig"); const wrap = document.getElementById("offerSummaryWrap");
if (hidden) hidden.value = ""; if (wrap) wrap.classList.add("hidden");
const offerSummary = document.getElementById("offerSummary");
if (offerSummary) offerSummary.value = "";
} catch {} } catch {}
} }
@@ -207,9 +193,9 @@ const form = data.form;
const payload = JSON.parse(raw); const payload = JSON.parse(raw);
const msg = document.querySelector('textarea[name="message"]'); const subject = formEl.querySelector('input[name="subject"]');
const subject = document.querySelector('input[name="subject"]'); const wrap = document.getElementById("offerSummaryWrap");
const hidden = document.getElementById("offerConfig"); const offerSummary = document.getElementById("offerSummary");
const offerText = const offerText =
typeof payload?.message === "string" && payload.message.trim() typeof payload?.message === "string" && payload.message.trim()
@@ -218,19 +204,13 @@ const form = data.form;
if (!offerText) return; if (!offerText) return;
if (wrap) wrap.classList.remove("hidden");
if (subject && !subject.value) { if (subject && !subject.value) {
subject.value = `Zapytanie: ${payload?.pkg?.name || "Oferta"}`; subject.value = `Zapytanie: ${payload?.pkg?.name || "Oferta"}`;
} }
if (msg) { if (offerSummary) offerSummary.value = offerText;
const marker = "Wybrana oferta:";
const alreadyHas = msg.value && msg.value.includes(marker);
if (!alreadyHas) {
msg.value = (msg.value ? msg.value + "\n\n" : "") + offerText;
}
}
if (hidden) hidden.value = offerText;
sessionStorage.setItem(SS_INJECTED_KEY, "1"); sessionStorage.setItem(SS_INJECTED_KEY, "1");
} catch {} } catch {}
@@ -238,21 +218,43 @@ const form = data.form;
hydrateOfferIntoForm(); hydrateOfferIntoForm();
function onSubmitSuccessCleanup() { window.addEventListener("pagehide", clearOfferDraft);
clearOfferDraft(); window.addEventListener("beforeunload", clearOfferDraft);
}
function onLeaveCleanup() { formEl.addEventListener("submit", async (e) => {
clearOfferDraft(); if (!formEl.reportValidity()) return;
} e.preventDefault();
window.addEventListener("pagehide", onLeaveCleanup); try {
window.addEventListener("beforeunload", onLeaveCleanup); const data = Object.fromEntries(new FormData(formEl).entries());
if (json.ok) { data.rodo = formEl.rodo?.checked === true;
form.reset();
onSubmitSuccessCleanup(); const token = await grecaptcha.execute(window.FUZ_RECAPTCHA_KEY, {
} action: "submit",
// ---------- });
data.recaptcha = token;
const resp = await fetch("/api/contact/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
const json = await resp.json().catch(() => ({}));
showToast(
json?.ok ? successMsg : errorMsg,
json?.ok ? "success" : "error",
);
if (json?.ok) {
formEl.reset();
clearOfferDraft();
}
} catch {
showToast(errorMsg, "error");
}
});
}); });
</script> </script>
</DefaultLayout> </DefaultLayout>

View File

@@ -1,5 +1,5 @@
.btn { .btn {
@apply inline-flex items-center justify-center gap-2 font-semibold rounded-lg px-6 py-3 text-base transition-all duration-200 cursor-pointer select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-[--f-header] focus-visible:ring-offset-2 focus-visible:ring-offset-[--f-background]; @apply inline-flex items-center justify-center gap-2 font-semibold rounded-lg px-6 py-3 mt-3 text-base transition-all duration-200 cursor-pointer select-none focus:outline-none focus-visible:ring-2 focus-visible:ring-[--f-header] focus-visible:ring-offset-2 focus-visible:ring-offset-[--f-background];
} }
.btn-primary { .btn-primary {