Repository navigation
Expand file tree
/
Copy pathscript.js
More file actions
264 lines (235 loc) · 10.6 KB
/
Copy pathscript.js
File metadata and controls
264 lines (235 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
/*
* WordPress 接入提示:
* 1. 可把 products 替换为 wp_localize_script() 输出的数据。
* 2. sceneImage/sampleImage 可改成 WordPress 媒体库图片 URL。
* 3. 下一步按钮事件中可接 Contact Form 7、Gravity Forms 或自定义表单。
*/
const products = [
{ id: 1, category: "decking", name: "Anthracite Grey Decking Board", tone: "#525a59", accent: "#a9b1ad" },
{ id: 2, category: "decking", name: "Slate Grey Grooved Decking", price: "From £5.27 per metre", tone: "#747d7d", accent: "#c7cfca" },
{ id: 3, category: "decking", name: "Wood Grain Grey Decking Board", price: "Only £5.27 per metre", tone: "#8b918f", accent: "#d8ddd8" },
{ id: 4, category: "decking", name: "Teak Capped Composite Board", tone: "#8d6641", accent: "#d1af7d" },
{ id: 5, category: "decking", name: "Graphite Grey Capped Board", tone: "#3f4547", accent: "#a7acad" },
{ id: 6, category: "decking", name: "Black Decking Board", price: "Only £4.55 per metre", tone: "#303537", accent: "#8f9696" },
{ id: 7, category: "decking", name: "Light Oak Decking Board", tone: "#aa7d52", accent: "#dec59d" },
{ id: 8, category: "decking", name: "Light Grey Decking Board", tone: "#9b9e99", accent: "#d5d6ce" },
{ id: 9, category: "decking", name: "Ash Grey Decking Board", tone: "#6e716d", accent: "#bec1ba" },
{ id: 10, category: "cladding", name: "Coastal Grey Slatted Cladding", tone: "#7c8584", accent: "#c6ceca" },
{ id: 11, category: "cladding", name: "Natural Cedar Wall Cladding", tone: "#9a6745", accent: "#d4a77c" },
{ id: 12, category: "cladding", name: "Charcoal Shadow Gap Cladding", tone: "#353b3c", accent: "#909897" },
{ id: 13, category: "cladding", name: "Warm Oak Exterior Cladding", tone: "#9d7450", accent: "#d2b18e" },
{ id: 14, category: "cladding", name: "Silver Grey Composite Cladding", tone: "#8b908e", accent: "#d0d3cf" },
{ id: 15, category: "cladding", name: "Deep Walnut Slatted Cladding", tone: "#594334", accent: "#a47c5d" },
{ id: 16, category: "fencing", name: "Graphite Composite Fence Panel", tone: "#424849", accent: "#a4abaa" },
{ id: 17, category: "fencing", name: "Natural Oak Garden Fencing", tone: "#9c744d", accent: "#d7b388" },
{ id: 18, category: "fencing", name: "Slate Grey Privacy Screen", tone: "#697171", accent: "#bec5c1" },
{ id: 19, category: "fencing", name: "Walnut Decorative Fence Panel", tone: "#6f4b35", accent: "#bf8f64" },
{ id: 20, category: "fencing", name: "Black Aluminium Fence Set", tone: "#292f30", accent: "#7e8785" }
].map(product => {
const fileNumber = String(product.id).padStart(2, "0");
return {
...product,
sceneImage: `images/products/product-${fileNumber}-scene.svg`,
sampleImage: `images/products/product-${fileNumber}-sample.svg`
};
});
let activeCategory = "decking";
let selectedIds = [2, 3];
let toastTimer;
const grid = document.querySelector("#product-grid");
const desktopPanel = document.querySelector("#desktop-panel");
const mobilePanel = document.querySelector("#mobile-panel");
const drawer = document.querySelector("#drawer");
const toast = document.querySelector("#toast");
const catalog = document.querySelector("#products");
const detailsStep = document.querySelector("#details-step");
const successStep = document.querySelector("#success-step");
const sampleForm = document.querySelector("#sample-form");
const mobileBar = document.querySelector("#mobile-bar");
function productImage(product, type) {
const source = type === "scene" ? product.sceneImage : product.sampleImage;
const description = type === "scene" ? "scene" : "sample";
return `<img src="${source}" alt="${product.name} ${description}" loading="lazy">`;
}
function renderProducts() {
grid.innerHTML = products
.filter(product => product.category === activeCategory)
.map(product => {
const selected = selectedIds.includes(product.id);
return `
<article class="product-card ${selected ? "selected" : ""}" data-id="${product.id}">
<button class="product-image" data-action="toggle" aria-pressed="${selected}">
<span class="scene-image">${productImage(product, "scene")}</span>
<span class="sample-image">${productImage(product, "sample")}</span>
<span class="hover-label">Hover to view sample</span>
</button>
<div class="product-info">
<div><h3>${product.name}</h3>${product.price ? `<p>${product.price}</p>` : ""}</div>
<button class="add-button" data-action="toggle" aria-label="${selected ? "Remove" : "Add"} ${product.name}">${selected ? "✓" : "+"}</button>
</div>
</article>`;
}).join("");
}
function panelMarkup() {
const selected = selectedIds.map(id => products.find(product => product.id === id)).filter(Boolean);
const slots = [0, 1, 2, 3].map(index => {
const product = selected[index];
return product ? `
<div class="selected-sample">
<button class="remove-button" data-remove="${product.id}" aria-label="Remove ${product.name}">×</button>
<div class="sample-mini">${productImage(product, "sample")}</div>
<span>${product.name}</span>
</div>` : `
<button class="empty-slot" data-empty>
<span class="empty-icon">◇</span>
<span>Add Free Sample ${index + 1}</span>
</button>`;
}).join("");
return `
<div class="panel-heading">
<span class="panel-icon">◇</span>
<div><h2>Your Selected Samples</h2><p><strong>${selectedIds.length}</strong> of 4 selected</p></div>
</div>
<div class="sample-slots">${slots}</div>
<button class="next-button" ${selectedIds.length ? "" : "disabled"}>Next Step: Order Samples →</button>`;
}
function renderPanels() {
const markup = panelMarkup();
desktopPanel.innerHTML = markup;
mobilePanel.innerHTML = markup;
document.querySelector("#mobile-count").textContent = selectedIds.length;
}
function toggleProduct(id) {
if (selectedIds.includes(id)) {
selectedIds = selectedIds.filter(item => item !== id);
} else if (selectedIds.length < 4) {
selectedIds.push(id);
} else {
showToast("You can select up to 4 samples.");
return;
}
renderProducts();
renderPanels();
}
function showToast(message) {
clearTimeout(toastTimer);
toast.textContent = message;
toast.style.display = "block";
toastTimer = setTimeout(() => { toast.style.display = "none"; }, 2200);
}
function selectedProducts() {
return selectedIds.map(id => products.find(product => product.id === id)).filter(Boolean);
}
function setActiveStep(step) {
document.querySelectorAll("[data-step]").forEach(item => {
const itemStep = Number(item.dataset.step);
item.classList.toggle("active", itemStep === step);
item.classList.toggle("complete", itemStep < step);
if (itemStep === step) {
item.setAttribute("aria-current", "step");
} else {
item.removeAttribute("aria-current");
}
});
document.querySelectorAll("[data-after-step]").forEach(arrow => {
arrow.classList.toggle("complete", Number(arrow.dataset.afterStep) < step);
});
}
function renderFormSummary() {
const chosen = selectedProducts();
document.querySelector("#selected-samples-input").value = JSON.stringify(
chosen.map(({ id, name, category }) => ({ id, name, category }))
);
document.querySelector("#form-summary").innerHTML = chosen.map(product => `
<div class="summary-item">
<div class="summary-thumb">${productImage(product, "sample")}</div>
<strong>${product.name}</strong>
</div>
`).join("");
}
function openDetails() {
if (!selectedIds.length) {
showToast("Please select at least one sample.");
return;
}
drawer.classList.remove("open");
catalog.hidden = true;
detailsStep.hidden = false;
successStep.hidden = true;
mobileBar.hidden = true;
renderFormSummary();
setActiveStep(2);
detailsStep.scrollIntoView({ behavior: "smooth", block: "start" });
}
function openProducts() {
detailsStep.hidden = true;
successStep.hidden = true;
catalog.hidden = false;
mobileBar.hidden = false;
setActiveStep(1);
catalog.scrollIntoView({ behavior: "smooth", block: "start" });
}
document.querySelector(".category-tabs").addEventListener("click", event => {
const button = event.target.closest("[data-category]");
if (!button) return;
activeCategory = button.dataset.category;
document.querySelectorAll("[data-category]").forEach(tab => tab.classList.toggle("active", tab === button));
renderProducts();
});
grid.addEventListener("click", event => {
const button = event.target.closest("[data-action='toggle']");
if (!button) return;
toggleProduct(Number(button.closest(".product-card").dataset.id));
});
document.addEventListener("click", event => {
const remove = event.target.closest("[data-remove]");
if (remove) toggleProduct(Number(remove.dataset.remove));
if (event.target.closest("[data-empty]")) {
drawer.classList.remove("open");
document.querySelector("#products").scrollIntoView({ behavior: "smooth" });
}
if (event.target.closest(".next-button")) {
openDetails();
}
});
document.querySelector("#mobile-bar").addEventListener("click", () => {
drawer.classList.add("open");
drawer.setAttribute("aria-hidden", "false");
});
document.querySelector("#drawer-close").addEventListener("click", () => drawer.classList.remove("open"));
drawer.addEventListener("click", event => {
if (event.target === drawer) drawer.classList.remove("open");
});
document.querySelector("#back-to-products").addEventListener("click", openProducts);
document.querySelector("#edit-samples").addEventListener("click", openProducts);
sampleForm.addEventListener("submit", event => {
event.preventDefault();
if (!sampleForm.reportValidity()) return;
const formData = new FormData(sampleForm);
const chosen = selectedProducts();
/*
* WordPress 接入点:
* 在这里使用 fetch() 提交到 admin-ajax.php、REST API,
* 或删除此监听器,让表单按 action/method 正常提交。
*/
console.log("Sample request:", Object.fromEntries(formData.entries()));
document.querySelector("#success-name").textContent = formData.get("name");
document.querySelector("#success-email").textContent = formData.get("email");
document.querySelector("#success-samples").innerHTML = chosen.map(product => `
<div class="success-sample">${productImage(product, "sample")}<span>${product.name}</span></div>
`).join("");
detailsStep.hidden = true;
successStep.hidden = false;
setActiveStep(3);
successStep.scrollIntoView({ behavior: "smooth", block: "start" });
});
document.querySelector("#start-again").addEventListener("click", () => {
sampleForm.reset();
selectedIds = [];
renderProducts();
renderPanels();
openProducts();
});
renderProducts();
renderPanels();
setActiveStep(1);