1import { MISSIONS, hash, readJson, writeJson } from './nobuycart-retention.mjs'; 2 3export const ENGAGEMENT_KEY = 'nobuycart.engagement.v1'; 4 5const object = (value) => value && typeof value === 'object' && !Array.isArray(value); 6export const localDay = (date = new Date()) => { 7 const value = date instanceof Date ? date : new Date(date); 8 return Number.isNaN(value.valueOf()) ? new Date().toISOString().slice(0, 10) : `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`; 9}; 10 11export const STREAK_STAGES = [ 12 'Window Shopper', 13 'Cart Curator', 14 'Pause Collector', 15 '$0 Checkout Expert', 16 'Fake Mall Manager', 17 'Chief Non-Purchasing Officer', 18]; 19 20export function defaultEngagement() { 21 return { version: 1, eventIds: [], eventDays: [], lastEventDay: null, currentStreak: 0, longestStreak: 0, considerations: [] }; 22} 23 24export function readEngagement(storage) { 25 const saved = readJson(storage, ENGAGEMENT_KEY, null); 26 if (!object(saved)) return defaultEngagement(); 27 const validDay = (value) => typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value); 28 return { 29 ...defaultEngagement(), 30 eventIds: Array.isArray(saved.eventIds) ? [...new Set(saved.eventIds.filter((value) => typeof value === 'string' && value.length <= 100))].slice(-500) : [], 31 eventDays: Array.isArray(saved.eventDays) ? [...new Set(saved.eventDays.filter(validDay))].sort().slice(-366) : [], 32 lastEventDay: validDay(saved.lastEventDay) ? saved.lastEventDay : null, 33 currentStreak: Number.isInteger(saved.currentStreak) && saved.currentStreak >= 0 ? Math.min(saved.currentStreak, 366) : 0, 34 longestStreak: Number.isInteger(saved.longestStreak) && saved.longestStreak >= 0 ? Math.min(saved.longestStreak, 366) : 0, 35 considerations: Array.isArray(saved.considerations) ? saved.considerations.filter((item) => object(item) && typeof item.id === 'string' && typeof item.item === 'string' && item.item.length <= 80 && typeof item.savedAt === 'string').slice(0, 50) : [], 36 }; 37} 38 39const previousDay = (value) => { 40 const date = new Date(`${value}T12:00:00`); 41 date.setDate(date.getDate() - 1); 42 return localDay(date); 43}; 44 45export function recordEngagement(storage, type, date = new Date()) { 46 const current = readEngagement(storage), eventDay = localDay(date), eventId = `${eventDay}:${String(type).slice(0, 64)}`; 47 if (current.eventIds.includes(eventId)) return { state: current, credited: false, duplicate: true }; 48 const newDay = !current.eventDays.includes(eventDay); 49 const currentStreak = newDay ? (current.lastEventDay === previousDay(eventDay) ? current.currentStreak + 1 : 1) : current.currentStreak; 50 const next = { 51 ...current, 52 eventIds: [...current.eventIds, eventId].slice(-500), 53 eventDays: newDay ? [...current.eventDays, eventDay].sort().slice(-366) : current.eventDays, 54 lastEventDay: newDay ? eventDay : current.lastEventDay, 55 currentStreak, 56 longestStreak: Math.max(current.longestStreak, currentStreak), 57 }; 58 writeJson(storage, ENGAGEMENT_KEY, next); 59 return { state: next, credited: newDay, duplicate: false }; 60} 61 62export function saveConsideration(storage, item, price) { 63 const current = readEngagement(storage), label = String(item || '').trim().slice(0, 80); 64 if (!label) return current; 65 const savedAt = new Date().toISOString(), id = `consideration:${hash(`${label}:${savedAt}`)}`; 66 const amount = Number(price); 67 const next = { 68 ...current, 69 considerations: [{ id, item: label, price: Number.isFinite(amount) && amount >= 0 ? amount : null, savedAt }, ...current.considerations].slice(0, 50), 70 }; 71 writeJson(storage, ENGAGEMENT_KEY, next); 72 return next; 73} 74 75export function rouletteMission(seed = `${Date.now()}`) { 76 return MISSIONS[hash(seed) % MISSIONS.length]; 77} 78 79export function shouldIBuyResult(answers) { 80 const safe = object(answers) ? answers : {}; 81 const score = Number(Boolean(safe.noDuplicate)) + Number(Boolean(safe.notSaleLed)) + Number(Boolean(safe.wantedLonger)) + Number(Boolean(safe.useThisMonth)) + Number(Boolean(safe.wantTomorrow)); 82 if (score <= 1) return { label: 'Give it 48 hours', copy: 'A little space could make the next look clearer. Save the thought locally and see what tomorrow-you thinks.' }; 83 if (score === 2) return { label: 'Sleep on it', copy: 'There is no rush here. Leave room for a calmer second look before deciding anything.' }; 84 if (score === 3) return { label: 'Wish later', copy: 'Your answers are mi
84xed. Keep the idea in this browser and revisit it when the moment feels less immediate.' }; 85 return { label: 'You seem pretty sure', copy: 'Your answers sound considered. NoBuyCart still leaves the decision with you, not a score.' }; 86}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.