PageSourceSearch

https://www.planyourroom.com/js/list-status.js

js planyourroom.com collected 2026-09-26 04:43:43 UTC 5,935 bytes, 157 lines download raw bytes

1// /js/list-status.js
2// Header heart icon + offcanvas slide-out for quick list access.
3// Hidden until the user has at least one list.
4// Loaded as a regular deferred script (not a module) so it registers
5// with Alpine before Alpine processes the DOM.
6
7document.addEventListener('alpine:init', () => {
8    // Shared store so both mobile and desktop icons can read visibility/count
9    Alpine.store('listStatus', {
10        visible: false,
11        totalItemCount: 0,
12        defaultListId: 0
13    });
14
15    Alpine.data('listStatus', () => ({
16        lists: [],
17        activeList: null,
18        items: [],
19        loaded: false,
20        slideOutOpen: false,
21        loadingItems: false,
22        shareCopyMessage: '',
23        copyingShareLink: false,
24
25        get visible() {
26            return Alpine.store('listStatus').visible;
27        },
28
29        get totalItemCount() {
30            return Alpine.store('listStatus').totalItemCount;
31        },
32
33        get defaultListId() {
34            return Alpine.store('listStatus').defaultListId;
35        },
36
37        async init() {
38            await this._loadLists();
39
40            // Favorites keeps the shared cache in sync before firing this.
41            document.addEventListener('list-status-update', () => this._loadLists());
42
43            const el = document.getElementById('listStatusSlideOut');
44            if (el) {
45                el.addEventListener('show.bs.offcanvas', () => {
46                    this._loadLists({ force: true }).then(() => this._loadItems());
47                });
48                el.addEventListener('shown.bs.offcanvas', () => { this.slideOutOpen = true; });
49                el.addEventListener('hidden.bs.offcanvas', () => { this.slideOutOpen = false; });
50            }
51        },
52
53        async _loadLists({ force = false } = {}) {
54            try {
55                const { getListsOnce } = await import(Utils.baseUrl + 'js/quicklist/lists-cache.js');
56                const data = await getListsOnce({ force });
57                const defaultListId = Number(data?.defaultListId || 0);
58                this.lists = (data?.lists || []).map(l => ({
59                    id: Number(l.id),
60                    title: l.title ?? '',
61                    isPrimary: !!l.isPrimary,
62                    itemCount: l.itemCount,
63                    isPublic: !!l.isPublic,
64                    publicLinkUrl: l.publicLinkUrl ?? ''
65                }));
66                this.loaded = true;
67
68                Alpine.store('listStatus').defaultListId = defaultListId;
69
70                const defaultList = defaultListId > 0
71                    ? this.lists.find(l => l.id === defaultListId)
72                    : this.lists.find(l => l.isPrimary);
73
74                if (defaultList) {
75                    this.activeList = defaultList;
76                } else if (this.lists.length && !this.activeList) {
77                    this.activeList = this.lists[0];
78                } else if (this.activeList) {
79                    const updated = this.lists.find(l => l.id === this.activeList.id);
80                    this.activeList = updated || this.lists[0] || null;
81                }
82
83                Alpine.store('listStatus').visible = this.lists.length > 0;
84                Alpine.store('listStatus').totalItemCount = this.activeList?.itemCount ?? 0;
85            } catch (e) {
86                // Silently fail — icon stays hidden
87            }
88        },
89
90        async _loadItems() {
91            if (!this.activeList) { this.items = []; return; }
92            this.loadingItems = true;
93            try {
94                const res = await fetch(Utils.baseUrl + 'quickList/getlistitems?listId=' + this.activeList.id + '&max=10', { credentials: 'include' });
95                if (!res.ok) { this.items = []; return; }
96                const data = await res.json();
97                this.items = data?.items || [];
98            } catch (e) {
99                this.items = [];
100            } finally {
101                this.loadingItems = false;
102            }
103        },
104
105        async selectList(list) {
106            this.activeList = list;
107            Alpine.store('listStatus').totalItemCount = list?.itemCount ?? 0;
108            await this._loadItems();
109        },
110
111        async copyShareLink() {
112            if (!this.activeList?.publicLinkUrl || this.copyingShareLink) return;
113
114            this.copyingShareLink = true;
115
116            try {
117                await navigator.clipboard.writeText(this.activeList.publicLinkUrl);
118                this.shareCopyMessage = 'Link copied.';
119                window.setTimeout(() => {
120                    this.shareCopyMessage = '';
121                }, 2000);
122            } catch (e) {
123                this.shareCopyMessage = 'Unable to copy the link.';
124            } finally {
125                this.copyingShareLink = false;
126            }
127        },
128
129        toggleSlideOut() {
130            const el = document.getElementById('listStatusSlideOut');
131            if (!el) return;
132            const instance = bootstrap.Offcanvas.getOrCreateInstance(el);
133            if (this.slideOutOpen) {
134                instance.hide();
135            } else {
136                this._loadLists({ force: true }).then(() => this._loadItems());
137                instance.show();
138            }
139        },
140
141        viewAllListsUrl() {
142            return Utils.baseUrl + 'quicklist/list';
143        },
144
145        viewListUrl(list) {
146            return Utils.baseUrl + 'quicklist/index/' + (list?.id || this.activeList?.id);
147        }
148    }));
149});
150
151// Global function for triggers outside the Alpine component scope
152window.toggleListStatus = function() {
153    const el = document.getElementById('listStatusSlideOut');
154    if (!el) return;
155    const instance = bootstrap.Offcanvas.getOrCreateInstance(el);
156    instance.toggle();
157};

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.