The repo for Purrform's main BigCommerce store.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Fix/diet-builder-improvements (#48)

authored by

Rogerio Romao and committed by
GitHub
631ddcc3 01cd7546

+156 -3
+132 -3
assets/js/theme/custom/diet-builder.js
··· 190 190 ); 191 191 } 192 192 193 + /** 194 + * Selects up to `max` products from `products` for the Klaviyo email payload. 195 + * 196 + * Selection order: 197 + * 1. The product with the lowest pricePerDay. 198 + * 2. The product with the highest pricePerDay. 199 + * 3-4. Products that introduce the most ingredient variety not yet covered 200 + * by the already-selected products; falls back to a random pick when 201 + * no new ingredients can be added. 202 + */ 203 + function selectProductsForKlaviyo(products, max = 4) { 204 + if (products.length <= max) return [...products]; 205 + 206 + const selected = []; 207 + const remaining = [...products]; 208 + 209 + const getIngredients = (p) => p.customFields?.Ingredients ?? []; 210 + 211 + // 1. Cheapest product 212 + const minIdx = remaining.reduce( 213 + (minI, p, i) => 214 + p.pricePerDay < remaining[minI].pricePerDay ? i : minI, 215 + 0, 216 + ); 217 + selected.push(remaining.splice(minIdx, 1)[0]); 218 + 219 + if (selected.length >= max || remaining.length === 0) return selected; 220 + 221 + // 2. Most expensive product 222 + const maxIdx = remaining.reduce( 223 + (maxI, p, i) => 224 + p.pricePerDay > remaining[maxI].pricePerDay ? i : maxI, 225 + 0, 226 + ); 227 + selected.push(remaining.splice(maxIdx, 1)[0]); 228 + 229 + // 3-4. Ingredient variety, then random fallback 230 + while (selected.length < max && remaining.length > 0) { 231 + const coveredIngredients = new Set( 232 + selected.flatMap((p) => getIngredients(p)), 233 + ); 234 + 235 + const withNewIngredients = remaining.filter((p) => 236 + getIngredients(p).some((i) => !coveredIngredients.has(i)), 237 + ); 238 + 239 + let pick; 240 + if (withNewIngredients.length > 0) { 241 + pick = withNewIngredients.reduce((best, p) => { 242 + const newCount = getIngredients(p).filter( 243 + (i) => !coveredIngredients.has(i), 244 + ).length; 245 + const bestCount = getIngredients(best).filter( 246 + (i) => !coveredIngredients.has(i), 247 + ).length; 248 + return newCount > bestCount ? p : best; 249 + }); 250 + } else { 251 + pick = remaining[Math.floor(Math.random() * remaining.length)]; 252 + } 253 + 254 + selected.push(pick); 255 + remaining.splice(remaining.indexOf(pick), 1); 256 + } 257 + 258 + return selected; 259 + } 260 + 193 261 // ── DietBuilder Class ──────────────────────────────────────────────── 194 262 195 263 export default class DietBuilder extends PageManager { ··· 395 463 this.container.innerHTML = ''; 396 464 } 397 465 398 - renderStep(heading, content) { 466 + renderStep(heading, content, subHeading = null) { 399 467 this.clearContainer(); 400 468 const wrapper = el('div', { className: 'diet-builder-step' }); 401 469 ··· 406 474 heading, 407 475 ); 408 476 wrapper.appendChild(headingEl); 477 + } 478 + 479 + if (subHeading) { 480 + const subHeadingEl = el( 481 + 'p', 482 + { className: 'diet-builder-step__sub-heading' }, 483 + subHeading, 484 + ); 485 + wrapper.appendChild(subHeadingEl); 409 486 } 410 487 411 488 wrapper.appendChild(content); ··· 486 563 const dob = new Date(year, month - 1, day); 487 564 const ageInMs = Date.now() - dob.getTime(); 488 565 const ageInMonths = ageInMs / (1000 * 60 * 60 * 24 * 30.44); 566 + 567 + if (ageInMonths < 4) { 568 + let errorMsg = document.getElementById('diet-builder-age-error'); 569 + if (!errorMsg) { 570 + errorMsg = el( 571 + 'p', 572 + { 573 + id: 'diet-builder-age-error', 574 + className: 'diet-builder-age-form__error', 575 + }, 576 + 'Our diet builder is designed for cats aged 4 months and older.', 577 + ); 578 + document 579 + .querySelector('.diet-builder-age-form') 580 + .appendChild(errorMsg); 581 + } 582 + return; 583 + } 584 + 585 + document.getElementById('diet-builder-age-error')?.remove(); 489 586 490 587 let ageKey; 491 588 if (ageInMonths < 2) ageKey = 1; ··· 878 975 // ── Step 5: Health Conditions ──────────────────────────────────── 879 976 880 977 renderHealthStep(flow) { 978 + // Restore products to pre-health-filter state when re-entering this step 979 + if (this.state.productsBeforeHealthFilter) { 980 + this.state.recommendedProducts = [ 981 + ...this.state.productsBeforeHealthFilter, 982 + ]; 983 + this.state.productsBeforeHealthFilter = null; 984 + } 985 + 881 986 const content = el('div', { className: 'diet-builder-health' }); 882 987 883 988 const grid = el('div', { ··· 1020 1125 1021 1126 content.appendChild(infoSection); 1022 1127 1023 - this.renderStep('Does your cat have any health conditions?', content); 1128 + this.renderStep( 1129 + 'Does your cat have any health conditions?', 1130 + content, 1131 + 'Just click next if the cat is healthy. Otherwise click to select up to 2 conditions that apply to your cat and we will tailor the product recommendations accordingly.', 1132 + ); 1024 1133 } 1025 1134 1026 1135 submitHealth(flow) { 1136 + // Snapshot products before health filter so we can restore on back navigation 1137 + this.state.productsBeforeHealthFilter = [ 1138 + ...this.state.recommendedProducts, 1139 + ]; 1140 + 1027 1141 this.state.recommendedProducts = this.state.recommendedProducts.filter( 1028 1142 (product) => 1029 1143 this.state.healthConditions.every((condition) => ··· 1101 1215 } 1102 1216 } 1103 1217 1218 + this.state.productsForKlaviyo = selectProductsForKlaviyo( 1219 + this.state.recommendedProducts, 1220 + ); 1221 + 1104 1222 this.renderResultsStep(); 1105 1223 } 1106 1224 ··· 1108 1226 const previewPayload = { 1109 1227 catName: this.state.catName, 1110 1228 calculatedRDA: this.state.calculatedRDA, 1229 + recommendedProductsCount: this.state.recommendedProducts.length, 1230 + productsForKlaviyoCount: this.state.productsForKlaviyo.length, 1111 1231 recommendedProducts: this.state.recommendedProducts.map((p) => ({ 1112 1232 name: p.name, 1113 1233 image: p.image, ··· 1115 1235 path: `https://www.purrform.co.uk${p.path}`, 1116 1236 gramsPerDay: p.gramsPerDay, 1117 1237 pricePerDay: p.pricePerDay, 1238 + })), 1239 + productsForKlaviyo: this.state.productsForKlaviyo.map((p) => ({ 1240 + name: p.name, 1241 + image: p.image, 1242 + price: p.price, 1243 + path: `https://www.purrform.co.uk${p.path}`, 1244 + gramsPerDay: p.gramsPerDay, 1245 + pricePerDay: p.pricePerDay, 1246 + ingredients: p.customFields?.Ingredients ?? [], 1118 1247 })), 1119 1248 }; 1120 1249 ··· 1183 1312 email, 1184 1313 catName: this.state.catName, 1185 1314 calculatedRDA: this.state.calculatedRDA, 1186 - recommendedProducts: this.state.recommendedProducts.map((p) => ({ 1315 + recommendedProducts: this.state.productsForKlaviyo.map((p) => ({ 1187 1316 name: p.name, 1188 1317 image: p.image, 1189 1318 price: p.price,
+23
assets/scss/custom/pages/_diet-builder.scss
··· 44 44 z-index: 1; 45 45 animation: dbFadeDown 0.6s ease both; 46 46 47 + em { 48 + color: var(--db-text-muted); 49 + font-size: 0.9rem; 50 + } 51 + 47 52 .diet-builder-heading__icon { 48 53 font-size: 2.4rem; 49 54 margin-bottom: 8px; ··· 99 104 color: var(--db-green); 100 105 font-size: 1.5rem; 101 106 text-align: center; 107 + margin-bottom: 0.5rem; 108 + } 109 + 110 + .diet-builder-step__sub-heading { 111 + color: var(--db-green); 112 + font-size: 0.95rem; 113 + text-align: center; 102 114 margin-bottom: 1.5rem; 115 + width: 100%; 116 + max-width: 80ch; 117 + margin-inline: auto; 118 + text-wrap: balance; 103 119 } 104 120 105 121 // ── Buttons ────────────────────────────────────────────────── ··· 271 287 272 288 .diet-builder-btn--primary { 273 289 margin: 1.5rem auto 0; 290 + } 291 + 292 + &__error { 293 + color: #c0392b; 294 + font-size: 0.875rem; 295 + text-align: center; 296 + margin-top: 0.5rem; 274 297 } 275 298 } 276 299
+1
templates/pages/custom/page/diet-builder.html
··· 13 13 personalized feeding plan to help your cat maintain a healthy weight 14 14 and overall well-being. 15 15 </p> 16 + <p><em>Please note: The recommendations are based on our own products. Different brands may have different nutritional values. This diet builder is for cats 4 months and older.</em></p> 16 17 </div> 17 18 18 19 <div id="diet-builder" class="diet-builder">