PageSourceSearch

https://member.life/includes/vue/support.js?version=6

js member.life collected 2026-10-02 15:50:35 UTC 19,636 bytes, 502 lines download raw bytes

1
2Vue.component('SupportList', {
3    template: `
4    <div class="support-container">
5        <Loading v-if="loading" :show="loading" />
6        <PopConfirm v-if="confirm.show" :show="confirm.show" :title="confirm.title" :prompt="confirm.prompt" :cancel="confirm.cancel" :okay="confirm.okay" @close="confirm.show=false" @decline="confirm.action.negative" @confirm="confirm.action.positive" :negative="confirm.red" />
7        <PopToast :toasts="toasts" @remove="removeToast($event)" />
8
9        <PopSupport v-if="showGuide" :guide="showGuide" :editMode="editMode" @edit="clickEdit($event)" @close="showGuide=false" />
10
11        <!-- Modify Guide Modal -->
12        <ModifyGuide
13            v-if="showEditGuide"
14            :guide="showEditGuide"
15            :levelTypes="levelTypes"
16            @close="showEditGuide=false"
17            @save="clickSave($event)"
18            @delete="clickDelete($event)"
19        />
20
21        <!-- Search Bar -->
22        <div style="position:relative;">
23            <button v-if="isAdmin && editMode" class="btn-green" style="position:absolute;right:10px;" @click="clickNew">{{ $t('points.btn.add').toUpperCase() }}</button>
24            <div class="vid-search">
25                <div class="tbl">
26                <div class="tbl-row">
27                    <div class="tbl-cell">
28                        <input type="text" v-model="searchTerm" :placeholder="$t('support.search.placeholder')" autofocus />
29                    </div>
30                </div>
31                </div>
32            </div>
33        </div>
34
35        <div class="none" v-if="filteredGuides.length === 0 && !loading">
36            {{ $t('common.empty.no_guides') }}
37        </div>
38
39        <!-- Videos Grid -->
40        <div class="vid-grid">
41            <div
42                v-for="(guide, index) in filteredGuides"
43                :key="guide.id"
44                class="vid"
45                :style="guide.png_url ? {backgroundImage: 'url('+guide.png_url+')'} : {}"
46                @click="clickGuide(guide)"
47            >
48                <div class="vid-label">{{ guide.title }}</div>
49            </div>
50
51            <div v-if="isAdmin && editMode" class="vid vid-create" @click="clickNew">
52                <div class="vid-plus">+</div>
53                <div class="vid-label">{{ $t('support.faq.create_new').toUpperCase() }}</div>
54            </div>
55        </div>
56    </div>
57    `,
58    mixins: [Utils, Core, I18n],
59    props: {
60        editMode: {
61            type: Boolean,
62            default: false
63        }
64    },
65    data() {
66        return {
67            loading: false,
68            guides: [],
69            searchTerm: '',
70            showGuide: false,
71            showEditGuide: false,
72            levelTypes: {}
73        }
74    },
75    computed: {
76        filteredGuides() {
77            if (!this.searchTerm) {
78                return this.guides;
79            }
80            const term = this.searchTerm.toLowerCase().trim();
81            return this.guides.filter(guide =>
82                guide.title.toLowerCase().includes(term) ||
83                (guide.content && guide.content.toLowerCase().includes(term))
84            );
85        }
86    },
87    mounted() {
88        this.refresh();
89        // Check URL hash for direct video link
90        const hash = window.location.hash.replace('#', '');
91        if (hash) {
92            this.$nextTick(() => {
93                const guide = this.guides.find(g =>
94                    this.hashify(g.title) === hash
95                );
96                if (guide) {
97                    this.clickGuide(guide);
98                }
99            });
100        }
101    },
102    methods: {
103        refresh() {
104            this.requestGetGuides();
105        },
106        hashify(title) {
107            return title.replace(/[^a-zA-Z0-9\-]+/g, '').replace(/ /g, '-').toLowerCase();
108        },
109        clickGuide(guide) {
110            this.showGuide = guide;
111            // Update URL hash
112            window.location.hash = this.hashify(guide.title);
113        },
114        clickNew() {
115            this.showEditGuide = {};
116        },
117        clickEdit(guide) {
118            this.showGuide = false;
119            this.showEditGuide = guide;
120        },
121        clickSave(guide) {
122            if (guide.id) {
123                this.requestEditGuide(guide);
124            } else {
125                this.requestAddGuide(guide);
126            }
127        },
128        clickDelete(guide) {
129            this.showConfirm(this.$t('confirm.delete_guide.title'), this.$t('confirm.delete_item.prompt', {item: guide.title}), this.$t('common.btn.delete'), this.$t('common.btn.cancel'), true,
130                () => {
131                    this.showEditGuide = false;
132                    this.requestDeleteGuide(guide.id);
133                },
134                () => {});
135        },
136        requestGetGuides() {
137            this.loading = true;
138
139            this.postAPI('get-videos', {
140                    name_short: this.org ? this.org.vanity : '',
141                    org_id: 0  // Always fetch global support guides
142                },
143                result => {
144                    this.loading = false;
145                    if (!result.success) {
146                        this.addToast('get-guides-' + Math.floor(Math.random() * 1000000), result.reason, result.success, 15000);
147                        console.error('Failed to get guides:', result.reason);
148                        return;
149                    }
150                    this.guides = result.data;
151                    // Retrieve level types for the form
152                    if (this.constant && this.constant.levelTypes) {
153                        this.levelTypes = this.constant.levelTypes[-1] || {};
154                    }
155                    // Update page count
156                    if ($('.page-amount').length) {
157                        $('.page-amount').text(this.guides.length);
158                    }
159                });
160        },
161        requestAddGuide(guideForm) {
162            this.loading = true;
163
164            // Create FormData for file upload
165            const formData = new FormData();
166            formData.append('access_id', this.access.id);
167            formData.append('access_token', this.access.token);
168            formData.append('name_short', this.org ? this.org.vanity : '');
169            formData.append('access', guideForm.access);
170            formData.append('title', guideForm.title);
171            formData.append('content', guideForm.content || '');
172            formData.append('code', guideForm.code || '');
173            if (guideForm.png_file) {
174                formData.append('png_img', guideForm.png_file);
175            }
176            if (guideForm.gif_file) {
177                formData.append('gif_img', guideForm.gif_file);
178            }
179
180            console.log('Params add-video',Object.fromEntries(formData.entries()))
181            fetch(this.access.api_url + 'add-video', {
182                method: 'POST',
183                body: formData
184            })
185            .then(response => response.json())
186            .then(result => {
187                console.log('add-video',result)
188                this.loading = false;
189                this.addToast('add-guide-' + Math.floor(Math.random() * 1000000), result.reason, result.success, 15000);
190
191                if (!result.success) {
192                    console.error('Failed to add guide:', result.reason);
193                    return;
194                }
195
196                this.showEditGuide = false;
197                this.refresh();
198            })
199            .catch(error => {
200                this.loading = false;
201                console.error('Add guide error:', error);
202                this.addToast('add-guide-error', 'Failed to add guide'+result.reason, false, 15000);
203            });
204        },
205        requestEditGuide(guideForm) {
206            this.loading = true;
207
208            // Create FormData for file upload
209            const formData = new FormData();
210            formData.append('access_id', this.access.id);
211            formData.append('access_token', this.access.token);
212            formData.append('name_short', this.org ? this.org.vanity : '');
213            formData.append('video_id', guideForm.id);
214            formData.append('access', guideForm.access);
215            formData.append('title', guideForm.title);
216            formData.append('content', guideForm.content || '');
217            formData.append('code', guideForm.code || '');
218            if (guideForm.png_file) {
219                formData.append('png_img', guideForm.png_file);
220            }
221            if (guideForm.gif_file) {
222                formData.append('gif_img', guideForm.gif_file);
223            }
224
225            console.log('Params edit-video',Object.fromEntries(formData.entries()))
226            fetch(this.access.api_url + 'edit-video', {
227                method: 'POST',
228                body: formData
229            })
230            .then(response => response.json())
231            .then(result => {
232                console.log('edit-video',result)
233                this.loading = false;
234                this.addToast('edit-guide-' + Math.floor(Math.random() * 1000000), result.reason, result.success, 15000);
235
236                if (!result.success) {
237                    this.addToast('edit-guide-error', 'Failed to edit guide: '+result.reason, false, 15000);
238                    return;
239                }
240
241                this.showEditGuide = false;
242                this.refresh();
243            })
244            .catch(error => {
245                this.loading = false;
246                this.addToast('edit-guide-error', 'Failed to edit guide: '+result.reason, false, 15000);
247            });
248        },
249        requestDeleteGuide(guide_id) {
250            this.loading = true;
251
252            this.postAPI('delete-video', {
253                    name_short: this.org ? this.org.vanity : '',
254                    video_id: guide_id
255                },
256                result => {
257                    this.loading = false;
258                    this.addToast('delete-guide-'+Math.floor(Math.random() * 1000000), result.reason, result.success, 15000);
259
260                    if (!result.success) {
261                        this.addToast('delete-guide-error-'+Math.floor(Math.random() * 1000000), 'Failed to delete guide: '+result.reason, false, 15000);
262                        return;
263                    }
264
265                    this.refresh();
266                });
267        }
268    }
269});
270Vue.component('PopSupport', {
271    template: `
272    <div>
273        <!-- Video Player Modal -->
274        <div v-if="showPlayer" class="chooser-fade" style="z-index:80;" @mousedown="closePlayer">
275            <iframe
276                ref="videoPlayer"
277                class="player-frame"
278                type="text/html"
279                :width="playerWidth"
280                :height="playerHeight"
281                :src="'https://www.youtube.com/embed/'+guide.code+'?enablejsapi=1&autoplay=1&controls=1&modestbranding=1&rel=0'"
282                frameborder="0"
283                allow="autoplay; encrypted-media"
284                allowfullscreen
285                @mousedown.stop>
286            </iframe>
287        </div>
288
289        <!-- Video Detail Modal -->
290        <div class="chooser-fade" @mousedown="$emit('close')">
291            <div class="box chooser chooser-auto" @mousedown.stop>
292                <div class="title">
293                    <div class="close" @click="$emit('close')">&times;</div>
294                    <span v-if="isAdmin && editMode" @click="$emit('edit',guide)" class="edit hint--top-left hint--no-animate hint--rounded" data-hint="Edit">
295                        <img :src="config.CDN_HOST+'/images/icon-edit.png'" />
296                    </span>
297                    <span class="vid-title">{{ guide ? guide.title : '' }}</span>
298                </div>
299
300                <img v-if="guide && guide.gif_url" class="vid-img" :src="guide.gif_url" @click="playVideo(guide.code)" :style="{cursor:guide.code ? 'pointer' : 'auto'}" />
301                <img v-else-if="guide && guide.png_url" class="vid-img" :src="guide.png_url" @click="playVideo(guide.code)" :style="{cursor:guide.code ? 'pointer' : 'auto'}" />
302
303                <div v-if="guide && guide.code" class="vid-how">
304                    {{ $t('support.faq.click_to_watch') }}
305                </div>
306
307                <div class="vid-text" v-html="formatContent(guide)"></div>
308            </div>
309        </div>
310    </div>
311    `,
312    mixins: [Utils, Core, I18n],
313    props: ['guide','editMode'],
314    data() {
315        return {
316            showPlayer: false,
317            playerWidth: 0,
318            palyerHeight: 0,
319        }
320    },
321    methods: {
322        playVideo() {
323            if (this.guide && this.guide.code) {
324                this.resizeVideo();
325                this.showPlayer = true;
326            }
327        },
328        resizeVideo() {
329			this.playerWidth = window.innerWidth * 0.7;
330			if(this.playerWidth > 1280)
331				this.playerWidth = 1280;
332			if(this.playerWidth < 500)
333				this.playerWidth = window.innerWidth;
334			this.playerHeight = this.playerWidth * .5625// aspect ratio
335        },
336        closePlayer() {
337            this.showPlayer = false;
338        },
339        formatContent(guide) {
340            if (!guide || !guide.content) return '';
341
342            let content = guide.content;
343            // Replace studio short name placeholder
344            if (this.org) {
345                content = content.replace(/\{\{studio_short\}\}/g, this.org.vanity);
346            }
347            // Replace files CDN placeholder
348            content = content.replace(/\{\{files\}\}/g, this.config.CDN_HOST);
349            // Add click handler to links
350            content = content.replace(/<a /g, '<a onclick="event.stopPropagation();" ');
351
352            // Convert newlines to step divs
353            const steps = content.split(/\r\n|\r|\n/);
354            return steps.map(step => '<div class="vid-step">' + step + '</div>').join('');
355        }
356    }
357})
358Vue.component('ModifyGuide', {
359    template: `
360    <div class="chooser-fade" @click="$emit('close')">
361        <PopConfirm v-if="confirm.show" :show="confirm.show" :title="confirm.title" :prompt="confirm.prompt" :cancel="confirm.cancel" :okay="confirm.okay" @close="confirm.show=false" @decline="confirm.action.negative" @confirm="confirm.action.positive" :negative="confirm.red" />
362
363        <div class="box chooser" @click.stop style="max-width: 520px;">
364            <div class="title">
365                <span>{{ guide ? $t('support.guide.edit_title').toUpperCase() : $t('support.guide.add_title').toUpperCase() }}</span>
366                <span class="close" @click="$emit('close')">&times;</span>
367            </div>
368
369            <div class="label">{{ $t('support.guide.label_access') }}</div>
370            <select v-model="guideForm.access" required>
371                <option value="" disabled>{{ $t('support.guide.placeholder_access') }}</option>
372                <option v-for="(label, key) in levelTypes" :key="key" :value="key">{{ label }}</option>
373            </select>
374
375            <div class="label">{{ $t('support.guide.label_title') }}</div>
376            <input type="text" v-model="guideForm.title" :placeholder="$t('support.guide.placeholder_title')" required />
377
378            <div class="label">{{ $t('support.guide.label_content') }}</div>
379            <textarea v-model="guideForm.content" :placeholder="$t('support.guide.placeholder_content')" rows="5"></textarea>
380
381            <div class="label">{{ $t('support.guide.label_cover') }}</div>
382            <input type="file" @change="handlePngFile" accept="image/png" />
383            <img v-if="guideForm.png_url && !guideForm.png_file" :src="guideForm.png_url" style="max-width:100px;margin-top:5px;" />
384
385            <div class="label">{{ $t('support.guide.label_gif') }}</div>
386            <input type="file" @change="handleGifFile" accept="image/gif" />
387            <img v-if="guideForm.gif_url && !guideForm.gif_file" :src="guideForm.gif_url" style="max-width:100px;margin-top:5px;" />
388
389            <div class="label">{{ $t('support.guide.label_youtube') }}</div>
390            <input type="text" v-model="guideForm.code" :placeholder="$t('support.guide.placeholder_youtube')" />
391
392            <button class="btn-long btn-green" @click="clickSave" :disabled="!isValid">{{ guide.id ? $t('support.guide.btn_update').toUpperCase() : $t('support.guide.btn_create').toUpperCase() }}</button>
393            <button v-if="guide" class="btn-long btn-red" @click="clickDelete">{{ $t('common.btn.delete').toUpperCase() }}</button>
394        </div>
395    </div>`,
396    mixins: [Utils, Core, I18n],
397    props: ['guide', 'levelTypes'],
398    data() {
399        return {
400            guideForm: {
401                id: this.guide ? this.guide.id : null,
402                access: this.guide ? this.guide.access : '',
403                title: this.guide ? this.guide.title : '',
404                content: this.guide ? this.guide.content : '',
405                code: this.guide ? this.guide.code : '',
406                png_url: this.guide ? this.guide.png_url : '',
407                gif_url: this.guide ? this.guide.gif_url : '',
408                png_file: null,
409                gif_file: null
410            }
411        };
412    },
413    computed: {
414        isValid() {
415            return this.guideForm.access && this.guideForm.title;
416        }
417    },
418    methods: {
419        handlePngFile(event) {
420            const file = event.target.files[0];
421            if (file) {
422                this.guideForm.png_file = file;
423            }
424        },
425        handleGifFile(event) {
426            const file = event.target.files[0];
427            if (file) {
428                this.guideForm.gif_file = file;
429            }
430        },
431        clickSave() {
432            if (this.isValid) {
433                this.$emit('save', this.guideForm);
434            }
435        },
436        clickDelete() {
437            this.$emit('delete', this.guideForm);
438        }
439    }
440});
441
442/**
443 * Standalone Support Guide Popup Component
444 * Can be embedded on any page to show a specific support guide by ID.
445 *
446 * Usage:
447 * 1. Add to page: <div id="support-guide-popup"><support-guide-popup></support-guide-popup></div>
448 * 2. Initialize: window.vmSupportGuide = new Vue({ el: '#support-guide-popup' });
449 * 3. Trigger: showSupportGuide(guideId) or vmSupportGuide.$emit('show-support', guideId);
450 */
451Vue.component('SupportGuidePopup', {
452    template: `
453    <div>
454        <PopSupport v-if="show" :guide="guide" @close="close" />
455    </div>
456    `,
457    mixins: [Utils, Core, I18n],
458    data() {
459        return {
460            show: false,
461            loading: false,
462            guide: null,
463            showPlayer: false,
464            error: null
465        }
466    },
467    mounted() {
468        // Listen for global event to open guide
469        this.$root.$on('show-support', (guideId) => this.open(guideId));
470    },
471    beforeDestroy() {
472        this.$root.$off('show-support');
473    },
474    methods: {
475        open(guideId) {
476            this.show = true;
477            this.loading = true;
478            this.error = null;
479            this.guide = null;
480            this.fetchGuide(guideId);
481        },
482        close() {
483            this.show = false;
484            this.guide = null;
485            this.showPlayer = false;
486            this.error = null;
487        },
488        fetchGuide(guideId) {
489            this.postAPI('get-video', { org_id: 0, video_id: guideId }, result => {
490                this.loading = false;
491                if (!result.success) {
492                    this.error = result.reason || 'Failed to load guide';
493                    return;
494                }
495                this.guide = result.data;
496                if (!this.guide) {
497                    this.error = 'Guide not found';
498                }
499            });
500        }
501    }
502});

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.