Stylizacja strony dokumenty, czyszczenie informacji z wyboru opcji do kontaktu po wyslaniu, odswieżeniu
This commit is contained in:
@@ -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.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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 }), {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
30
src/styles/document.css
Normal 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];
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user