ShoppingPad

Slows down impulse buying. Accumulate, consider, decide.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         ShoppingPad
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Slows down impulse buying. Accumulate, consider, decide.
// @author       anrinion
// @match        https://www.amazon.com/*
// @match        https://www.amazon.de/*
// @match        https://www.amazon.co.uk/*
// @match        https://www.amazon.fr/*
// @match        https://www.amazon.it/*
// @match        https://www.amazon.es/*
// @match        https://www.amazon.ca/*
// @match        https://www.amazon.com.mx/*
// @match        https://www.amazon.com.br/*
// @match        https://www.amazon.in/*
// @match        https://www.amazon.co.jp/*
// @match        https://www.amazon.com.au/*
// @match        https://www.amazon.nl/*
// @match        https://www.amazon.pl/*
// @match        https://www.amazon.se/*
// @match        https://www.amazon.sa/*
// @match        https://www.amazon.eg/*
// @match        https://www.amazon.ae/*
// @match        https://www.amazon.tr/*
// @match        https://www.amazon.be/*
// @match        https://www.amazon.sg/*
// @match        https://www.amazon.co.za/*
// @grant        GM_addStyle
// @run-at       document-start
// @license      MIT 
// ==/UserScript==

(function () {
    'use strict';

    /* ---------- CONFIG ---------- */
    const DEFAULT_MAX_VISITS = 3;
    const SESSION_DURATION = 3600000; // 1 hour in milliseconds

    /* ---------- STORAGE ---------- */
    const SK = {
        COUNT: 'ab_visit_count',
        WEEK: 'ab_week_number',
        LIST: 'ab_shopping_list',
        MAX: 'ab_max_visits',
        THEME: 'ab_theme',
        SESSION_START: 'ab_session_start'
    };

    /* ---------- WEEK ---------- */
    // Returns the ISO date of the current week's Monday (e.g., "2026-03-30")
    function getWeekKey() {
        const now = new Date();
        const day = now.getDay();

        // Days back to Monday: Sun(0)->6, Mon(1)->0, Tue(2)->1, ..., Sat(6)->5
        const diff = (day + 6) % 7;

        const monday = new Date(now);
        monday.setDate(now.getDate() - diff);
        monday.setHours(0, 0, 0, 0);

        return monday.toISOString().split('T')[0];
    }

    function resetIfNewWeek() {
        try {
            const cur = getWeekKey();
            if (localStorage.getItem(SK.WEEK) !== cur) {
                localStorage.setItem(SK.COUNT, '0');
                localStorage.setItem(SK.WEEK, cur);
            }
        } catch (e) {
            console.error('Storage error', e);
        }
    }

    // Returns days until next Monday (1-7)
    function daysUntilMonday() {
        const day = new Date().getDay();
        // (8 - day) % 7 results: Sun(0)->1, Mon(1)->0, ..., Sat(6)->2
        // || 7 converts the 0 (Monday) to 7
        return (8 - day) % 7 || 7;
    }

    /* ---------- STATE ---------- */
    const getMax = () => parseInt(localStorage.getItem(SK.MAX) || DEFAULT_MAX_VISITS, 10);
    const setMax = n => localStorage.setItem(SK.MAX, n);
    const getCount = () => parseInt(localStorage.getItem(SK.COUNT) || '0', 10);
    const getRemaining = () => Math.max(0, getMax() - getCount());
    const isBlocked = () => getRemaining() === 0;
    const consume = () => localStorage.setItem(SK.COUNT, getCount() + 1);

    /* ---------- SESSION ---------- */
    function startSession() {
        localStorage.setItem(SK.SESSION_START, Date.now().toString());
    }
    function isSessionActive() {
        const start = localStorage.getItem(SK.SESSION_START);
        if (!start) return false;
        const now = Date.now();
        return (now - parseInt(start, 10)) < SESSION_DURATION;
    }

    /* ---------- LIST ---------- */
    const getList = () => JSON.parse(localStorage.getItem(SK.LIST) || '[]');
    const saveList = l => localStorage.setItem(SK.LIST, JSON.stringify(l));
    function addItem(text) {
        if (!text.trim()) return false;
        const l = getList();
        l.push({ text: text.trim(), added: new Date().toISOString(), checked: false });
        saveList(l);
        return true;
    }
    function removeItem(i) {
        const l = getList(); l.splice(i, 1); saveList(l);
    }
    function toggleItemCheck(i) {
        const l = getList();
        l[i].checked = !l[i].checked;
        saveList(l);
    }
    const fmtDate = iso => new Date(iso).toLocaleDateString(undefined, { day: '2-digit', month: 'short' });

    /* ---------- THEME ---------- */
    const getTheme = () => localStorage.getItem(SK.THEME) || 'light';
    const setTheme = t => localStorage.setItem(SK.THEME, t);
    function applyTheme() {
        document.documentElement.setAttribute('data-ab-theme', getTheme());
    }

    function toggleTheme() {
        const next = getTheme() === 'light' ? 'dark' : 'light';
        setTheme(next);
        applyTheme();
        document.querySelectorAll('.ab-pill').forEach(btn => {
            btn.textContent = next === 'light' ? '☽ Dark' : '☀ Light';
        });
        // Update any existing inputs to reflect new background
        document.querySelectorAll('.ab-add input').forEach(input => {
            input.style.backgroundColor = 'var(--input)';
        });
    }

    /* ---------- CSS ---------- */
    GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Inter:400;500;600;700&display=swap');

/* Hide body until overlay is ready – prevents flicker */
body.ab-hide-content {
    visibility: hidden !important;
}

:root[data-ab-theme="light"] {
    --page: #e4e4e9;
    --surface: #fff;
    --surface2: #f4f4f8;
    --border: #dddde6;
    --text-hi: #111114;
    --text-mid: #70707a;
    --text-lo: #b0b0bc;
    --accent: #0066cc;
    --accent-lo: #e6f0fd;
    --danger: #c62828;
    --input: #ffffff;
    --shadow: 0 24px 60px rgba(0, 0, 0, .14), 0 4px 12px rgba(0, 0, 0, .06);
}

:root[data-ab-theme="dark"] {
    --page: #09090b;
    --surface: #18181b;
    --surface2: #27272a;
    --border: #3f3f46;
    --text-hi: #fafafa;
    --text-mid: #a1a1aa;
    --text-lo: #52525b;
    --accent: #60a5fa;
    --accent-lo: #172554;
    --danger: #f87171;
    --input: #27272a;
    --shadow: 0 24px 60px rgba(0, 0, 0, .5), 0 4px 12px rgba(0, 0, 0, .3);
}

#ab-overlay {
    position: fixed;
    inset: 0;
    z-index: 999999;
    background: var(--page);
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: Inter, system-ui;
}

.ab-card {
    width: 100%;
    max-width: 480px;
    background: var(--surface);
    border-radius: 18px;
    box-shadow: var(--shadow);
    overflow: hidden;
}

.ab-top {
    display: flex;
    justify-content: space-between;
    padding: 14px 20px;
    background: var(--surface2);
    border-bottom: 1px solid var(--border);
}

.ab-word {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    color: var(--text-lo);
}

.ab-pill {
    padding: 4px 12px;
    border-radius: 999px;
    border: 1px solid var(--border);
    cursor: pointer;
    background: var(--surface);
    color: var(--text-mid);
}

.ab-body {
    padding: 28px;
}

.ab-eyebrow {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    color: var(--accent);
}

.ab-heading {
    font-size: 28px;
    font-weight: 700;
    margin: 10px 0;
    color: var(--text-hi);
}

.ab-heading.blocked {
    color: var(--danger);
}

.ab-sub {
    font-size: 14px;
    color: var(--text-mid);
    margin-bottom: 20px;
    line-height: 1.5;
}

.ab-chip {
    display: inline-flex;
    gap: 8px;
    background: var(--accent-lo);
    padding: 6px 14px;
    border-radius: 999px;
    margin-bottom: 20px;
    color: var(--text-mid);
}

.ab-chip b {
    color: var(--accent);
}

.ab-btn {
    width: 100%;
    padding: 12px;
    border-radius: 10px;
    border: 1px solid var(--border);
    background: transparent;
    cursor: pointer;
    margin-bottom: 20px;
    color: var(--text-mid);
}

.ab-btn:hover {
    background: var(--surface2);
    border-color: var(--text-lo);
    color: var(--text-hi);
}

.ab-sec {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    color: var(--text-lo);
    margin-bottom: 8px;
}

.ab-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.ab-item {
    display: flex;
    gap: 8px;
    padding: 8px;
    background: var(--surface2);
    border: 1px solid var(--border);
    border-radius: 10px;
    color: var(--text-hi);
}

.ab-item span:first-child {
    flex: 1;
}

.ab-item .item-text {
    flex: 1;
}

.ab-item button {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-lo);
    width: 20px;
    height: 20px;
    line-height: 1;
}

.ab-item button:hover {
    color: var(--danger);
}

.ab-item.checked .item-text {
    text-decoration: line-through;
    color: var(--text-lo);
}

.ab-add {
    display: flex;
    gap: 6px;
    margin-top: 6px;
}

.ab-add input {
    flex: 1;
    padding: 10px;
    border-radius: 8px;
    border: 1px solid var(--border);
    background: var(--input);
    color: var(--text-hi);
    outline: none;
    font-family: inherit;
    transition: border-color 0.15s, box-shadow 0.15s;
}

.ab-add input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-lo);
}

.ab-add button {
    padding: 0 16px;
    border: none;
    border-radius: 8px;
    background: var(--accent);
    color: #fff;
    cursor: pointer;
    font-weight: 600;
}

#ab-widget-icon {
    position: fixed;
    bottom: 18px;
    right: 18px;
    width: 44px;
    height: 44px;
    border-radius: 14px;
    background: var(--surface);
    border: 1px solid var(--border);
    box-shadow: var(--shadow);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 999999;
    color: var(--text-mid);
    font-size: 18px;
}

#ab-widget-panel {
    position: fixed;
    bottom: 72px;
    right: 18px;
    width: 320px;
    display: none;
    z-index: 999999;
}
`);

    /* ---------- SHARED CARD BUILDER ---------- */
    function buildCard(blocked, compact = false) {
        const card = document.createElement('div');
        card.className = 'ab-card';

        const top = document.createElement('div');
        top.className = 'ab-top';
        top.innerHTML = `
<span class="ab-word">ShoppingPad</span>
<button class="ab-pill">${getTheme() === 'light' ? '☽ Dark' : '☀ Light'}</button>
`;

        const body = document.createElement('div');
        body.className = 'ab-body';

        if (!compact) {
            body.innerHTML += `
        <div class="ab-eyebrow">${blocked ? 'Access restricted' : 'Checkpoint'}</div>
        <div class="ab-heading ${blocked ? 'blocked' : ''}">
        ${blocked ? 'Done for the week.' : 'Start a shopping session?'}
        </div>
        <div class="ab-sub">
        ${blocked
                    ? `You've used your ${getMax()} shopping sessions this week. You can still put things down here and buy them when your sessions reset.`
                    : `You have ${getRemaining()} sessions left this week. You can browse now, or just leave items on the pad to buy everything at once later.`}
        </div>
        <div class="ab-chip">
        <span>${blocked ? 'Resets in' : 'Sessions left'}</span>
        <b>${blocked ? daysUntilMonday() + ' days' : getRemaining() + ' of ' + getMax()}</b>
        </div>
        ${!blocked ? '<button class="ab-btn">Start 1-hour session</button>' : ''}
        <div class="ab-sec">Your pad</div>
        `;
        } else {
            body.innerHTML += `<div class="ab-sec">Shopping list</div>`;
        }

        const listWrap = document.createElement('div');
        const inputElement = buildList(listWrap, compact);
        body.appendChild(listWrap);

        card.append(top, body);

        const themeBtn = top.querySelector('button');
        themeBtn.onclick = (e) => {
            e.preventDefault();
            toggleTheme();
        };

        if (!blocked && !compact) {
            const startBtn = body.querySelector('.ab-btn');
            if (startBtn) {
                startBtn.onclick = () => {
                    consume();
                    startSession();
                    document.getElementById('ab-overlay')?.remove();
                    createWidget();
                };
            }
        }

        // For compact view, focus the input if present
        if (compact && inputElement) {
            setTimeout(() => inputElement.focus(), 0);
        }

        return card;
    }

    /* ---------- LIST BUILDER ---------- */
    // Returns the input element so that it can be focused later
    function buildList(container, compact = false) {
        container.innerHTML = '';
        const list = getList();

        const wrap = document.createElement('div');
        wrap.className = 'ab-list';

        if (!list.length) {
            wrap.innerHTML = '<div style="text-align:center;color:var(--text-lo);padding:10px;border:1px dashed var(--border);border-radius:10px">Nothing here yet.</div>';
        } else {
            list.forEach((item, i) => {
                const row = document.createElement('div');
                row.className = 'ab-item';

                if (item.checked) {
                    row.classList.add('checked');
                }

                let checkboxHtml = '';
                if (compact) {
                    checkboxHtml = `<input type="checkbox" class="ab-checkbox" style="cursor:pointer;" ${item.checked ? 'checked' : ''}>`;
                }

                row.innerHTML = `
${checkboxHtml}
<span class="item-text">${escapeHtml(item.text)}</span>
<span style="font-size:11px;color:var(--text-lo)">${fmtDate(item.added)}</span>
<button>×</button>
`;

                if (compact) {
                    const cb = row.querySelector('.ab-checkbox');
                    cb.addEventListener('change', () => {
                        toggleItemCheck(i);
                        if (cb.checked) row.classList.add('checked');
                        else row.classList.remove('checked');
                    });
                }

                row.querySelector('button').onclick = () => {
                    removeItem(i);
                    buildList(container, compact);
                };
                wrap.appendChild(row);
            });
        }

        container.appendChild(wrap);

        const add = document.createElement('div');
        add.className = 'ab-add';

        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = 'Add to list…';
        input.style.backgroundColor = 'var(--input)';

        const btn = document.createElement('button');
        btn.textContent = 'Add';

        const handleAdd = () => {
            if (addItem(input.value)) {
                input.value = '';
                // Rebuild the list, then focus the new input
                const newInput = buildList(container, compact);
                if (newInput) {
                    newInput.focus();
                }
            }
        };

        btn.onclick = (e) => {
            e.preventDefault();
            handleAdd();
        };

        input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                handleAdd();
            }
        });

        add.append(input, btn);
        container.appendChild(add);

        return input; // Return input so caller can focus if needed
    }

    function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
    }

    /* ---------- OVERLAY ---------- */
    function showOverlay(blocked) {
        // Remove any existing overlay
        const existingOverlay = document.getElementById('ab-overlay');
        if (existingOverlay) {
            existingOverlay.remove();
        }

        const o = document.createElement('div');
        o.id = 'ab-overlay';
        o.appendChild(buildCard(blocked, false));

        // Hide body content before adding overlay (prevent flicker)
        document.body.classList.add('ab-hide-content');

        document.body.appendChild(o);

        // Force a reflow to ensure overlay is rendered before showing body
        o.offsetHeight;

        // Remove the hiding class after overlay is in place
        document.body.classList.remove('ab-hide-content');
    }

    /* ---------- WIDGET ---------- */
    let panel;

    function createWidget() {
        if (document.getElementById('ab-widget-icon')) return;

        const icon = document.createElement('div');
        icon.id = 'ab-widget-icon';
        icon.textContent = '⊟';
        icon.onclick = togglePanel;

        panel = document.createElement('div');
        panel.id = 'ab-widget-panel';

        document.body.append(icon, panel);
    }

    function togglePanel() {
        if (panel.style.display === 'block') {
            panel.style.display = 'none';
        } else {
            panel.innerHTML = '';
            panel.appendChild(buildCard(false, true));
            panel.style.display = 'block';
        }
    }

    /* ---------- INIT ---------- */
    function run() {
        resetIfNewWeek();
        applyTheme();

        // Check if there's an active session (not expired)
        if (isSessionActive()) {
            // Session active: ensure widget exists and don't show overlay
            createWidget();
        } else if (isBlocked()) {
            // No active session and limit reached: show blocked overlay
            showOverlay(true);
        } else {
            // No active session and limit not reached: show regular overlay
            showOverlay(false);
        }
    }

    // Apply hide-body style as early as possible to prevent flicker
    const style = document.createElement('style');
    style.textContent = `body { visibility: hidden !important; }`;
    document.documentElement.appendChild(style);

    // Wait for DOM ready to run the main logic and then remove the hiding style
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            style.remove();
            run();
        });
    } else {
        style.remove();
        run();
    }
})();