PageSourceSearch

https://nobuycart.com/nobuycart-engagement-core.mjs

js nobuycart.com collected 2026-10-10 02:42:53 UTC 4,719 bytes, 86 lines download raw bytes

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.