feat(support): виджет онлайн-чата JivoSite (спит без ключа)
- JivoWidget.vue: подгружает скрипт Jivo при заданном VITE_JIVO_WIDGET_ID (по образцу SmartCaptchaWidget); ключ пуст → ничего не грузит, виджет спит. - Смонтирован в AppLayout (клиентский портал). +VITE_JIVO_WIDGET_ID в vite-env.d.ts. - TDD: JivoWidget.spec (грузит при ключе / спит без) GREEN; AppLayout.spec 16/16; eslint 0. - Активируется вставкой VITE_JIVO_WIDGET_ID (твой ID канала Jivo). На прод не выкачено. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Виджет онлайн-чата JivoSite (G7-A). Когда задан VITE_JIVO_WIDGET_ID —
|
||||
* на странице подгружается скрипт Jivo и появляется плавающая кнопка чата
|
||||
* (Jivo рисует её сам). Ключ пуст (dev/CI/нет аккаунта) → ничего не грузим,
|
||||
* виджет спит. По образцу SmartCaptchaWidget (условная подгрузка скрипта).
|
||||
*/
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const widgetId = import.meta.env.VITE_JIVO_WIDGET_ID ?? '';
|
||||
const SCRIPT_ID = 'jivosite-widget-js';
|
||||
|
||||
function injectJivo(): void {
|
||||
if (widgetId === '' || document.getElementById(SCRIPT_ID) !== null) {
|
||||
return;
|
||||
}
|
||||
const script = document.createElement('script');
|
||||
script.id = SCRIPT_ID;
|
||||
script.src = `//code.jivosite.com/widget/${widgetId}`;
|
||||
script.async = true;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
onMounted(injectJivo);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Jivo рисует плавающую кнопку чата сам (в body); здесь — скрытый якорь. -->
|
||||
<span class="jivo-widget-anchor" aria-hidden="true" style="display: none" />
|
||||
</template>
|
||||
@@ -20,6 +20,7 @@ import AppSidebar from '../components/layout/AppSidebar.vue';
|
||||
import AppTopbar from '../components/layout/AppTopbar.vue';
|
||||
import DevIndexBadge from '../components/DevIndexBadge.vue';
|
||||
import CommandPalette from '../components/layout/CommandPalette.vue';
|
||||
import JivoWidget from '../components/support/JivoWidget.vue';
|
||||
import BalanceFrozenBanner from '../components/billing/BalanceFrozenBanner.vue';
|
||||
import ImpersonationSessionBanner from '../components/admin/ImpersonationSessionBanner.vue';
|
||||
|
||||
@@ -88,6 +89,7 @@ usePolling(loadBalanceStatus, { intervalMs: POLLING_REMINDERS_INTERVAL_MS, enabl
|
||||
</v-main>
|
||||
<DevIndexBadge :index="route.meta.devIndex" :label="route.meta.devLabel" />
|
||||
<CommandPalette />
|
||||
<JivoWidget />
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
|
||||
Vendored
+1
@@ -2,6 +2,7 @@
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_YANDEX_SMARTCAPTCHA_SITEKEY?: string;
|
||||
readonly VITE_JIVO_WIDGET_ID?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import JivoWidget from '../../resources/js/components/support/JivoWidget.vue';
|
||||
|
||||
beforeEach(() => {
|
||||
document.querySelectorAll('#jivosite-widget-js').forEach((n) => n.remove());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
describe('JivoWidget', () => {
|
||||
it('подгружает скрипт Jivo, когда задан VITE_JIVO_WIDGET_ID', () => {
|
||||
vi.stubEnv('VITE_JIVO_WIDGET_ID', 'abc123');
|
||||
mount(JivoWidget);
|
||||
const script = document.getElementById('jivosite-widget-js') as HTMLScriptElement | null;
|
||||
expect(script).not.toBeNull();
|
||||
expect(script?.src).toContain('code.jivosite.com/widget/abc123');
|
||||
});
|
||||
|
||||
it('ничего не грузит, когда ключ пуст (виджет спит)', () => {
|
||||
vi.stubEnv('VITE_JIVO_WIDGET_ID', '');
|
||||
mount(JivoWidget);
|
||||
expect(document.getElementById('jivosite-widget-js')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user