Stylizacja strony dokumenty, czyszczenie informacji z wyboru opcji do kontaktu po wyslaniu, odswieżeniu

This commit is contained in:
dm
2025-12-15 14:16:33 +01:00
parent 3cc0887dca
commit 3713d50bca
7 changed files with 189 additions and 174 deletions

View File

@@ -1,14 +0,0 @@
title: "Promocja świąteczna"
visible: true
intro: Przykładowo gdybysmy dodali promocję do dokumentów
content: |
Jeśli kupujesz w sklepach internetowych, prawdopodobnie co pewien czas natykasz się na opisy, które nie zachęcają do zakupów.
Do najczęściej powtarzanych błędów opisów produktów należą:
- brak konkretów klient chce wiedzieć, z czego produkt jest wykonany, jakie ma wymiary czy funkcje, a nie tylko, że jest „wysokiej jakości”;
- zbyt techniczny język warto dostosować ton komunikacji do odbiorcy, unikając skomplikowanych terminów (i w drugą stronę jeśli sprzedajesz towar skierowany do profesjonalistów, nie trzeba w opisie ze szczegółami wyjaśniać, jak działa czy do czego służy);
- brak narracji storytelling w opisach produktów pomaga zbudować emocjonalne zaangażowanie klienta;
ignorowanie pytań klientów warto analizować najczęstsze pytania i uwzględniać odpowiedzi w opisach; jeśli np. często dostajesz zapytania dotyczące tego, czy produkt jest wodoodporny, lepiej napisać o tym od razu w opisie;
zbyt długie i skomplikowane opisy należy dbać o przejrzystość treści, używać krótkich akapitów i list wypunktowanych; klient poszukuje konkretów, a nie zawiłych opowieści, które trudno się czyta.

View File

@@ -1,14 +0,0 @@
title: "Promocja świąteczna"
visible: true
intro: Przykładowo gdybysmy dodali promocję do dokumentów
content: |
Jeśli kupujesz w sklepach internetowych, prawdopodobnie co pewien czas natykasz się na opisy, które nie zachęcają do zakupów.
Do najczęściej powtarzanych błędów opisów produktów należą:
- brak konkretów klient chce wiedzieć, z czego produkt jest wykonany, jakie ma wymiary czy funkcje, a nie tylko, że jest „wysokiej jakości”;
- zbyt techniczny język warto dostosować ton komunikacji do odbiorcy, unikając skomplikowanych terminów (i w drugą stronę jeśli sprzedajesz towar skierowany do profesjonalistów, nie trzeba w opisie ze szczegółami wyjaśniać, jak działa czy do czego służy);
- brak narracji storytelling w opisach produktów pomaga zbudować emocjonalne zaangażowanie klienta;
ignorowanie pytań klientów warto analizować najczęstsze pytania i uwzględniać odpowiedzi w opisach; jeśli np. często dostajesz zapytania dotyczące tego, czy produkt jest wodoodporny, lepiej napisać o tym od razu w opisie;
zbyt długie i skomplikowane opisy należy dbać o przejrzystość treści, używać krótkich akapitów i list wypunktowanych; klient poszukuje konkretów, a nie zawiłych opowieści, które trudno się czyta.

View File

@@ -145,7 +145,7 @@ ${form.message || ""}
subject, subject,
text, text,
html, html,
replyTo: form.email ? String(form.email) : undefined, // wygodne do "Odpowiedz" replyTo: form.email ? String(form.email) : undefined,
}); });
return new Response(JSON.stringify({ ok: true }), { return new Response(JSON.stringify({ ok: true }), {

View File

@@ -3,6 +3,7 @@ import DefaultLayout from "../../layouts/DefaultLayout.astro";
import Markdown from "../../islands/Markdown.jsx"; import Markdown from "../../islands/Markdown.jsx";
import { marked } from "marked"; import { marked } from "marked";
import { getDocumentBySlug } from "../../lib/documents"; import { getDocumentBySlug } from "../../lib/documents";
import "../../styles/document.css";
const { slug } = Astro.params; const { slug } = Astro.params;
@@ -17,7 +18,7 @@ const html = marked.parse(doc.content);
<DefaultLayout title={doc.title}> <DefaultLayout title={doc.title}>
<section class="f-section"> <section class="f-section">
<div class="f-section-grid-single"> <div class="f-section-grid-single">
<a href="/dokumenty" class="text-sm opacity-70 hover:opacity-100"> <a href="/dokumenty" class="f-document-link">
← Wróć do dokumentów ← Wróć do dokumentów
</a> </a>

View File

@@ -1,29 +1,28 @@
--- ---
import DefaultLayout from "../../layouts/DefaultLayout.astro"; import DefaultLayout from "../../layouts/DefaultLayout.astro";
import { listDocuments } from "../../lib/documents"; import { listDocuments } from "../../lib/documents";
import "../../styles/document.css";
const documents = listDocuments().filter((d) => d.visible === true); const documents = listDocuments().filter((d) => d.visible === true);
--- ---
<DefaultLayout title="Dokumenty"> <DefaultLayout title="Dokumenty">
<section class="max-w-6xl mx-auto px-4 py-10"> <section class="f-documents">
<h1 class="text-4xl md:text-5xl font-bold text-[--f-header]">Dokumenty</h1> <h1 class="f-documents-title">Dokumenty</h1>
<div class="mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div class="f-documents-grid">
{documents.map((doc) => ( {documents.map((doc) => (
<a <a
href={`/dokumenty/${doc.slug}`} href={`/dokumenty/${doc.slug}`}
class="group rounded-2xl border border-[--f-border-color] class="f-document-card"
bg-[--f-background] p-5 shadow-sm
transition hover:-translate-y-0.5 hover:shadow-md"
> >
<h3 class="text-xl font-bold group-hover:underline">{doc.title}</h3> <h3 class="f-document-title">{doc.title}</h3>
{doc.intro && ( {doc.intro && (
<p class="mt-3 text-sm opacity-80 leading-relaxed">{doc.intro}</p> <p class="f-document-intro">{doc.intro}</p>
)} )}
<span class="mt-4 inline-block text-sm opacity-70">Otwórz →</span> <span class="f-document-link">Otwórz →</span>
</a> </a>
))} ))}
</div> </div>

View File

@@ -18,104 +18,108 @@ const form = data.form;
--- ---
<DefaultLayout seo={seo}> <DefaultLayout seo={seo}>
<section class="f-section"> <section class="f-section">
<div class="f-section-grid md:grid-cols-2 gap-10 items-start"> <div class="f-section-grid md:grid-cols-2 gap-10 items-start">
<div> <div>
<h2 class="f-section-title">{data.title}</h2> <h2 class="f-section-title">{data.title}</h2>
<div class="f-contact-item" set:html={data.description} /> <div class="f-contact-item" set:html={data.description} />
</div>
<div>
<h2 class="f-section-title">{data.contactFormTitle}</h2>
<form id="contactForm" class="f-contact-form">
<div class="f-contact-form-inner">
<input
type="text"
name="firstName"
placeholder={form.firstName.placeholder}
class="f-input"
required
/>
<input
type="text"
name="lastName"
placeholder={form.lastName.placeholder}
class="f-input"
required
/>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<input
type="email"
name="email"
placeholder={form.email.placeholder}
class="f-input"
required
autocomplete="email"
/>
<input
type="tel"
name="phone"
placeholder={form.phone.placeholder}
class="f-input"
required
autocomplete="tel"
/>
</div>
<input
type="text"
name="subject"
placeholder={form.subject.placeholder}
class="f-input"
required
/>
<textarea
name="message"
rows={form.message.rows}
placeholder={form.message.placeholder}
class="f-input"
required></textarea>
<label class="f-rodo">
<input type="checkbox" name="rodo" required />
<span>
{form.rodo.label}
<a href={form.rodo.policyLink} title={form.rodo.policyTitle}>
{form.rodo.policyText}
</a>.
</span>
</label>
<button
type="submit"
class="btn btn-primary w-full py-3"
title={form.submit.title}
>
{form.submit.label}
</button>
</form>
</div>
</div> </div>
<div> <div class="mt-10">
<h2 class="f-section-title">{data.contactFormTitle}</h2> <div class="f-contact-map">
<form id="contactForm" class="f-contact-form"> <MapGoogle
<div class="f-contact-form-inner"> apiKey={apiKey}
<input lat={data.lat}
type="text" lon={data.lng}
name="firstName" zoom={16}
placeholder={form.firstName.placeholder} title={data.markerTitle}
class="f-input" description={data.markerAddress}
required showMarker={true}
/> mode="contact"
<input mapStyleId={data.maps.mapId}
type="text"
name="lastName"
placeholder={form.lastName.placeholder}
class="f-input"
required
/>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<input
type="email"
name="email"
placeholder={form.email.placeholder}
class="f-input"
required
autocomplete="email"
/>
<input
type="tel"
name="phone"
placeholder={form.phone.placeholder}
class="f-input"
required
autocomplete="tel"
/>
</div>
<input
type="text"
name="subject"
placeholder={form.subject.placeholder}
class="f-input"
required
/> />
</div>
<textarea
name="message"
rows={form.message.rows}
placeholder={form.message.placeholder}
class="f-input"
required></textarea>
<label class="f-rodo">
<input type="checkbox" name="rodo" required />
<span>
{form.rodo.label}
<a href={form.rodo.policyLink} title={form.rodo.policyTitle}>
{form.rodo.policyText}
</a>.
</span>
</label>
<button type="submit" class="btn btn-primary w-full py-3" title={form.submit.title}>
{form.submit.label}
</button>
</form>
</div> </div>
</div>
<div class="mt-10"> <div id="toast" class="f-toast"></div>
<div class="f-contact-map">
<MapGoogle
apiKey={apiKey}
lat={data.lat}
lon={data.lng}
zoom={16}
title={data.markerTitle}
description={data.markerAddress}
showMarker={true}
mode="contact"
mapStyleId={data.maps.mapId}
/>
</div>
</div>
<div id="toast" class="f-toast"></div> <input type="hidden" name="offerConfig" id="offerConfig" />
</section>
<input type="hidden" name="offerConfig" id="offerConfig" />
</section>
<!-- ReCaptcha v3 --> <!-- ReCaptcha v3 -->
<script <script
@@ -182,63 +186,72 @@ const form = data.form;
// ---------- // ----------
const LS_KEY = "fuz_offer_config_v1"; const LS_KEY = "fuz_offer_config_v1";
const SS_INJECTED_KEY = "fuz_offer_injected_v1";
function formatOfferSummary(o) { function clearOfferDraft() {
if (!o || !o.pkg) return ""; try {
localStorage.removeItem(LS_KEY);
sessionStorage.removeItem(SS_INJECTED_KEY);
const lines = []; const hidden = document.getElementById("offerConfig");
lines.push(`Wybrana oferta: ${o.pkg.name} (${o.totals?.base ?? 0} ${o.totals?.currencyLabel ?? ""})`); if (hidden) hidden.value = "";
} catch {}
}
if (o.decoder) lines.push(`Dekoder: ${o.decoder.name} (+${o.decoder.price} ${o.totals?.currencyLabel ?? ""})`); function hydrateOfferIntoForm() {
if (o.phone) lines.push(`Telefon: ${o.phone.name} (+${o.phone.price} ${o.totals?.currencyLabel ?? ""})`); try {
if (sessionStorage.getItem(SS_INJECTED_KEY) === "1") return;
if (Array.isArray(o.tvAddons) && o.tvAddons.length) { const raw = localStorage.getItem(LS_KEY);
lines.push("Pakiety TV:"); if (!raw) return;
o.tvAddons.forEach((x) => {
const term = x.term ? `, ${x.term}` : "";
lines.push(`- ${x.nazwa} x${x.qty} (${x.unit} ${o.totals?.currencyLabel ?? ""}${term})`);
});
}
if (Array.isArray(o.addons) && o.addons.length) { const payload = JSON.parse(raw);
lines.push("Dodatki:");
o.addons.forEach((x) =>
lines.push(`- ${x.nazwa} x${x.qty} (${x.unit} ${o.totals?.currencyLabel ?? ""})`),
);
}
lines.push(`RAZEM: ${o.totals?.total ?? 0} ${o.totals?.currencyLabel ?? ""}`); const msg = document.querySelector('textarea[name="message"]');
return lines.join("\n"); const subject = document.querySelector('input[name="subject"]');
} const hidden = document.getElementById("offerConfig");
function hydrateOfferIntoForm() { const offerText =
try { typeof payload?.message === "string" && payload.message.trim()
const raw = localStorage.getItem(LS_KEY); ? payload.message
if (!raw) return; : null;
const payload = JSON.parse(raw); if (!offerText) return;
const msg = document.querySelector('textarea[name="message"]');
const subject = document.querySelector('input[name="subject"]');
const offerText = if (subject && !subject.value) {
typeof payload?.message === "string" && payload.message.trim() subject.value = `Zapytanie: ${payload?.pkg?.name || "Oferta"}`;
? payload.message }
: null;
if (!offerText) return; 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 (subject && !subject.value) { if (hidden) hidden.value = offerText;
subject.value = `Zapytanie: ${payload?.pkg?.name || "Oferta"}`;
}
if (msg) { sessionStorage.setItem(SS_INJECTED_KEY, "1");
msg.value = (msg.value ? msg.value + "\n\n" : "") + offerText; } catch {}
} }
} catch {}
}
hydrateOfferIntoForm(); hydrateOfferIntoForm();
function onSubmitSuccessCleanup() {
clearOfferDraft();
}
function onLeaveCleanup() {
clearOfferDraft();
}
window.addEventListener("pagehide", onLeaveCleanup);
window.addEventListener("beforeunload", onLeaveCleanup);
if (json.ok) {
form.reset();
onSubmitSuccessCleanup();
}
// ---------- // ----------
}); });
</script> </script>

30
src/styles/document.css Normal file
View File

@@ -0,0 +1,30 @@
.f-documents {
@apply max-w-7xl mx-auto px-4 py-10;
}
.f-documents-title {
@apply text-4xl md:text-5xl font-bold text-[--f-header];
}
.f-documents-grid {
@apply mt-8 grid gap-6 sm:grid-cols-2 lg:grid-cols-3;
}
.f-document-card {
@apply border border-[--f-border-color] bg-[--f-background] text-[--f-text]
p-5 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md;
}
.f-document-title {
@apply text-2xl font-bold;
}
.f-document-intro {
@apply mt-3 text-xl opacity-80 leading-relaxed;
}
.f-document-link {
@apply mt-4 inline-block text-base opacity-70 text-[--f-link-text];
}