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": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"update:jambox:base": "node src/scripts/update-jambox-base.js"
"preview": "astro preview"
},
"dependencies": {
"@astrojs/node": "^9.5.1",

View File

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

View File

@@ -512,7 +512,7 @@ export default function InternetAddonsModal({
</div>
<a
href="/kontakt"
href="/kontakt#form"
class="btn btn-primary w-full mt-4"
onClick={() => saveOfferToLocalStorage()}
>
@@ -523,7 +523,6 @@ export default function InternetAddonsModal({
</SectionAccordion>
</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-circle" role="status" aria-label="Cena miesięczna">
<span class="f-floating-total-unit">Razem</span>

View File

@@ -10,6 +10,7 @@ function esc(str = "") {
function buildHtmlMail(form) {
const when = new Date().toLocaleString("pl-PL");
const offerText = (form.offerSummary || "").trim()
return `<!DOCTYPE html>
<html lang="pl">
@@ -79,6 +80,17 @@ function buildHtmlMail(form) {
${esc(form.message)}
</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;">
Wysłano: ${when}
</div>

View File

@@ -1,16 +1,17 @@
---
import path from "node:path";
import DefaultLayout from "../../layouts/DefaultLayout.astro";
import MapGoogle from "../../components/maps/MapGoogle.astro";
import { loadYamlFile } from "../../lib/loadYaml";
import yaml from "js-yaml";
import fs from "fs";
const data = yaml.load(
fs.readFileSync("./src/content/contact/contact.yaml", "utf8"),
type SeoYaml = any;
const seo = loadYamlFile<SeoYaml>(
path.join(process.cwd(), "src", "content", "contact", "seo.yaml"),
);
const seo = yaml.load(
fs.readFileSync("./src/content/internet-swiatlowodowy/seo.yaml", "utf8"),
type ContactData = any;
const data = loadYamlFile<ContactData>(
path.join(process.cwd(), "src", "content", "contact", "contact.yaml"),
);
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>
<div>
<div id="form">
<h2 class="f-section-title">{data.contactFormTitle}</h2>
<form id="contactForm" class="f-contact-form">
<div class="f-contact-form-inner">
<input
@@ -77,7 +79,20 @@ const form = data.form;
rows={form.message.rows}
placeholder={form.message.placeholder}
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">
<input type="checkbox" name="rodo" required />
@@ -117,8 +132,6 @@ const form = data.form;
</div>
<div id="toast" class="f-toast"></div>
<input type="hidden" name="offerConfig" id="offerConfig" />
</section>
<!-- ReCaptcha v3 -->
@@ -142,59 +155,32 @@ const form = data.form;
}}
>
document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("contactForm");
const formEl = document.getElementById("contactForm");
const toast = document.getElementById("toast");
if (!formEl) return;
if (!form) return;
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();
});
const LS_KEY = "fuz_offer_config_v1";
const SS_INJECTED_KEY = "fuz_offer_injected_v1";
function showToast(msg, type) {
if (!toast) return;
toast.innerHTML = `<div class="f-toast-msg ${type}">${msg}</div>`;
toast.classList.remove("visible");
void toast.offsetWidth;
toast.classList.add("visible");
setTimeout(() => toast.classList.remove("visible"), 3000);
}
// ----------
const LS_KEY = "fuz_offer_config_v1";
const SS_INJECTED_KEY = "fuz_offer_injected_v1";
function clearOfferDraft() {
try {
localStorage.removeItem(LS_KEY);
sessionStorage.removeItem(SS_INJECTED_KEY);
const hidden = document.getElementById("offerConfig");
if (hidden) hidden.value = "";
const wrap = document.getElementById("offerSummaryWrap");
if (wrap) wrap.classList.add("hidden");
const offerSummary = document.getElementById("offerSummary");
if (offerSummary) offerSummary.value = "";
} catch {}
}
@@ -207,9 +193,9 @@ const form = data.form;
const payload = JSON.parse(raw);
const msg = document.querySelector('textarea[name="message"]');
const subject = document.querySelector('input[name="subject"]');
const hidden = document.getElementById("offerConfig");
const subject = formEl.querySelector('input[name="subject"]');
const wrap = document.getElementById("offerSummaryWrap");
const offerSummary = document.getElementById("offerSummary");
const offerText =
typeof payload?.message === "string" && payload.message.trim()
@@ -218,19 +204,13 @@ const form = data.form;
if (!offerText) return;
if (wrap) wrap.classList.remove("hidden");
if (subject && !subject.value) {
subject.value = `Zapytanie: ${payload?.pkg?.name || "Oferta"}`;
}
if (msg) {
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;
if (offerSummary) offerSummary.value = offerText;
sessionStorage.setItem(SS_INJECTED_KEY, "1");
} catch {}
@@ -238,21 +218,43 @@ const form = data.form;
hydrateOfferIntoForm();
function onSubmitSuccessCleanup() {
clearOfferDraft();
}
window.addEventListener("pagehide", clearOfferDraft);
window.addEventListener("beforeunload", clearOfferDraft);
function onLeaveCleanup() {
clearOfferDraft();
}
formEl.addEventListener("submit", async (e) => {
if (!formEl.reportValidity()) return;
e.preventDefault();
window.addEventListener("pagehide", onLeaveCleanup);
window.addEventListener("beforeunload", onLeaveCleanup);
if (json.ok) {
form.reset();
onSubmitSuccessCleanup();
}
// ----------
try {
const data = Object.fromEntries(new FormData(formEl).entries());
data.rodo = formEl.rodo?.checked === true;
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>
</DefaultLayout>

View File

@@ -1,5 +1,5 @@
.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 {