PluginProbe ʕ •ᴥ•ʔ
Hostinger Reach – AI-Powered Email Marketing for WordPress / trunk
Hostinger Reach – AI-Powered Email Marketing for WordPress vtrunk
1.8.0 1.7.2 1.7.1 1.7.0 1.6.0 1.5.9 1.5.8 1.5.7 1.5.6 1.5.5 1.5.4 1.5.3 1.5.2 1.5.1 1.5.0 1.4.12 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 trunk 1.0.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6
hostinger-reach / frontend / vue / views / OverviewView.vue
hostinger-reach / frontend / vue / views Last commit date
OverviewView.vue 5 days ago WelcomeView.vue 1 month ago
OverviewView.vue
627 lines
1 <script setup lang="ts">
2 import { HIcon, HPopover } from '@hostinger/hcomponents';
3 import { computed, onMounted, watchEffect } from 'vue';
4
5 import reachOverviewBannerBackground from '@/assets/images/backgrounds/reach-features-banner.png';
6 import reachLogo from '@/assets/images/icons/reach-logo.svg';
7 import ActionButtonsSection from '@/components/ActionButtonsSection.vue';
8 import Banner from '@/components/Banner.vue';
9 import FAQ from '@/components/FAQ.vue';
10 import Integrations from '@/components/Integrations.vue';
11 import WooCommerce from '@/components/WooCommerce.vue';
12 import { useModal } from '@/composables';
13 import { useOverviewData } from '@/composables/useOverviewData';
14 import { useReachUrls } from '@/composables/useReachUrls';
15 import { useToast } from '@/composables/useToast';
16 import { overviewFaqData } from '@/data/faq';
17 import { ELEMENTOR_ID, HOSTINGER_REACH_ID, WOOCOMMERCE_ID } from '@/data/pluginData';
18 import { formsRepo } from '@/data/repositories/formsRepo';
19 import { reachRepo } from '@/data/repositories/reachRepo';
20 import { TABS_KEYS } from '@/data/tabs';
21 import { useBuilderFormsStore } from '@/stores/builderFormsStore';
22 import { useIntegrationsStore } from '@/stores/integrationsStore';
23 import type { Integration } from '@/types';
24 import { ModalName } from '@/types';
25 import type { Form } from '@/types/models';
26 import { translate } from '@/utils/translate';
27
28 const { status, loadOverviewData } = useOverviewData();
29 const {
30 reachDashboardLink,
31 reachYourPlanLink,
32 reachContactsLink,
33 reachSegmentsLink,
34 reachAutomationsLink,
35 reachFormsLink
36 } = useReachUrls();
37 const { showError } = useToast();
38
39 const { openModal } = useModal();
40 const integrationsStore = useIntegrationsStore();
41 const builderFormsStore = useBuilderFormsStore();
42
43 const actionButtons = computed(() => [
44 {
45 icon: 'ic-user-double-16',
46 text: translate('hostinger_reach_overview_contacts_button'),
47 url: reachContactsLink.value
48 },
49 {
50 icon: 'ic-user-ai-16',
51 text: translate('hostinger_reach_overview_segments_button'),
52 url: reachSegmentsLink.value
53 },
54 {
55 icon: 'ic-lightning-16',
56 text: translate('hostinger_reach_overview_your_plan_button'),
57 url: reachYourPlanLink.value
58 },
59 {
60 icon: 'ic-reach-16',
61 text: translate('hostinger_reach_header_go_to_reach_button'),
62 url: reachDashboardLink.value
63 }
64 ]);
65
66 const handlePluginGoTo = (id: string) => {
67 const integration = integrationsStore.integrations.find((i) => i.id === id);
68 if (!integration?.adminUrl) {
69 return;
70 }
71
72 window.open(integration.adminUrl, '_blank');
73 };
74
75 const handlePluginDisconnect = (id: string) => {
76 openModal(ModalName.CONFIRM_DISCONNECT_MODAL, {
77 data: { integration: id }
78 });
79 };
80
81 const handleFormToggleStatus = async (form: Form, isActive: boolean) => {
82 if (form.isLoading) {
83 return;
84 }
85
86 const integration = integrationsStore.integrations.find((i) => i.forms?.some((f) => f.formId === form.formId));
87 if (!integration || !integration.forms) {
88 return;
89 }
90
91 const formIndex = integration.forms.findIndex((f) => f.formId === form.formId);
92 if (formIndex !== -1) {
93 integration.forms[formIndex].isLoading = true;
94 }
95
96 const [, error] = await formsRepo.toggleFormStatus(form.formId, isActive, form.type);
97
98 if (formIndex !== -1) {
99 integration.forms[formIndex].isLoading = false;
100 }
101
102 if (error?.response?.data?.error) {
103 showError(error?.response?.data?.error);
104
105 return;
106 }
107
108 if (error) {
109 showError(translate('hostinger_reach_error_message'));
110
111 return;
112 }
113
114 if (formIndex !== -1) {
115 integration.forms[formIndex] = {
116 ...integration.forms[formIndex],
117 isActive
118 };
119
120 if (isActive && !isAutomation(form) && hasContactsToImport(integration)) {
121 openModal(ModalName.CONFIRM_SYNC_MODAL, { integration });
122 }
123 }
124 };
125
126 const handleViewForm = (form: Form) => {
127 if (form.formId === 'ai-theme-footer-form') {
128 window.open(hostinger_reach_reach_data.site_url, '_blank');
129 }
130
131 if (form.post?.guid) {
132 window.open(form.post.guid, '_blank');
133 }
134 };
135
136 const FORM_BUILDER_INTEGRATION_IDS = [ELEMENTOR_ID, HOSTINGER_REACH_ID];
137
138 const handleAddForm = (id: string) => {
139 const integration = integrationsStore.integrations.find((i) => i.id === id);
140 if (!integration?.addFormUrl) {
141 return;
142 }
143
144 if (!FORM_BUILDER_INTEGRATION_IDS.includes(integration.id)) {
145 window.open(integration.addFormUrl, '_blank');
146
147 return;
148 }
149
150 openModal(
151 ModalName.SELECT_FORM_MODAL,
152 {
153 data: {
154 onContinue: (formBuilderId: string) => addFormToIntegrationPage(integration, formBuilderId)
155 }
156 },
157 { hasCloseButton: true, isXXL: true, noContentPadding: true }
158 );
159 };
160
161 const addFormToIntegrationPage = (integration: Integration, formBuilderId: string) => {
162 if (!integration.addFormUrl) {
163 return;
164 }
165
166 const addBlockValue = formBuilderId || '1';
167 let url = integration.addFormUrl.replace(
168 'hostinger_reach_add_block=1',
169 `hostinger_reach_add_block=${encodeURIComponent(addBlockValue)}`
170 );
171
172 if (integration.id !== ELEMENTOR_ID) {
173 const addBlockNonce = hostinger_reach_reach_data?.add_block_nonce || '';
174 url += `&_wpnonce=${encodeURIComponent(addBlockNonce)}`;
175 }
176
177 window.open(url, '_blank');
178 };
179
180 const showAddFormModal = () => {
181 openModal(ModalName.SELECT_FORM_MODAL, {}, { hasCloseButton: true, isXXL: true, noContentPadding: true });
182 };
183
184 const handleConnectPluginButton = () => {
185 openModal(ModalName.CONNECT_PLUGIN_MODAL, {}, { hasCloseButton: true, isLG: true });
186 };
187
188 const hasContactsToImport = (integration: Integration): boolean =>
189 integration.importEnabled && integration.importStatus?.total > 0;
190
191 const handleSyncContactsButton = () => {
192 if (!integrationsStore.importAvailableIntegrations.length) {
193 return;
194 }
195
196 openModal(
197 ModalName.SYNC_CONTACTS_MODAL,
198 {
199 title: translate('hostinger_reach_contacts_modal_title'),
200 subtitle: translate('hostinger_reach_contacts_modal_subtitle'),
201 data: { integrations: integrationsStore.importAvailableIntegrations ?? [] }
202 },
203 { hasCloseButton: true }
204 );
205 };
206
207 const handleEditForm = (form: Form) => {
208 const integration = integrationsStore.integrations.find((i) => i.forms?.some((f) => f.formId === form.formId));
209
210 if (!integration?.editUrl) {
211 return;
212 }
213
214 let editUrl = integration.editUrl;
215
216 const placeholders: Record<string, string> = {
217 '{post_id}': form.post?.ID.toString() ?? '',
218 '{form_id}': form.formId,
219 '{post_name}': form.post?.postName.toString() ?? ''
220 };
221
222 for (const [token, value] of Object.entries(placeholders)) {
223 if (editUrl.includes(token)) {
224 editUrl = editUrl.replaceAll(token, value);
225 }
226 }
227
228 if (form.formId === 'ai-theme-footer-form') {
229 editUrl = 'site-editor.php?p=%2Fwp_template_part%2Fhostinger-ai-theme%2F%2Ffooter&canvas=edit';
230 }
231
232 if (!editUrl.startsWith('http') && !editUrl.startsWith('/')) {
233 editUrl = `/wp-admin/${editUrl}`;
234 }
235
236 window.open(editUrl, '_blank');
237 };
238
239 const hasFormsOrActiveIntegrations = computed(
240 () =>
241 integrationsStore?.activeIntegrations?.filter((integration) => integration.type === 'forms')?.length > 1 ||
242 integrationsStore?.hasAnyForms('forms')
243 );
244
245 const maybeOpenAddFormModal = () => {
246 const url = new URL(window.location.href);
247 if (url.searchParams.has('addForm')) {
248 showAddFormModal();
249 }
250 };
251
252 const maybeShowConnectionSuccessModal = async () => {
253 const [data] = await reachRepo.getConnectionSuccess();
254
255 if (data?.success) {
256 openModal(ModalName.CONNECTION_SUCCESS_MODAL, {}, { hasCloseButton: true, isXL: true, noContentPadding: true });
257 }
258 };
259
260 onMounted(() => {
261 loadOverviewData();
262 integrationsStore.loadIntegrations();
263 builderFormsStore.loadForms();
264 maybeOpenAddFormModal();
265 maybeShowConnectionSuccessModal();
266 });
267
268 watchEffect(() => {
269 if (status?.value === 403) {
270 window.location.reload();
271 }
272 });
273
274 const wooCommerceConnected = computed(
275 () => !!integrationsStore?.activeIntegrations.find((i) => i.id === WOOCOMMERCE_ID)
276 );
277
278 const isAutomation = (form: Form): boolean => form?.formId?.includes('.') ?? false;
279
280 const shouldShowConnect = computed(
281 () =>
282 integrationsStore?.activeIntegrations?.filter((integration) => integration.type === TABS_KEYS.OVERVIEW_TAB_FORMS)
283 ?.length > 1 || integrationsStore.hasAnyForms(TABS_KEYS.OVERVIEW_TAB_FORMS)
284 );
285 </script>
286
287 <template>
288 <div class="overview">
289 <header class="overview__header">
290 <div class="overview__header-content">
291 <div class="overview__header-brand">
292 <img :src="reachLogo" :alt="translate('hostinger_reach_header_logo_alt')" class="overview__header-logo" />
293 </div>
294 </div>
295 </header>
296
297 <div class="overview__content">
298 <div class="overview__section">
299 <div class="overview__section-content">
300 <ActionButtonsSection :buttons="actionButtons" />
301 </div>
302 </div>
303
304 <Banner
305 :title="translate('hostinger_reach_overview_banner_title')"
306 :description="translate('hostinger_reach_overview_banner_description')"
307 :label="translate('hostinger_reach_overview_banner_label')"
308 :button-text="translate('hostinger_reach_overview_banner_button_text')"
309 :button-to="reachFormsLink"
310 button-target="_blank"
311 align="left"
312 :background-image="reachOverviewBannerBackground as unknown as string"
313 />
314
315 <div class="overview__integrations">
316 <div class="overview__integrations-header">
317 <HText class="overview__tabs-header" as="h2" variant="heading-2">
318 {{ translate('hostinger_reach_forms_title') }}
319 </HText>
320 <div class="overview__integrations-tabs">
321 <div class="overview__integrations-buttons">
322 <HButton
323 v-if="integrationsStore.importAvailableIntegrations.length > 0"
324 variant="text"
325 color="primary"
326 size="small"
327 icon-prepend="ic-arrows-circle-16"
328 :is-loading="integrationsStore.isLoading"
329 @click="handleSyncContactsButton"
330 >
331 {{ translate('hostinger_reach_sync_contacts_button_text') }}
332 </HButton>
333 <HButton
334 v-if="shouldShowConnect"
335 variant="outline"
336 color="primary"
337 size="small"
338 icon-prepend="ic-link-16"
339 :is-loading="integrationsStore.isLoading"
340 @click="handleConnectPluginButton"
341 >
342 {{ translate('hostinger_reach_connect_plugin') }}
343 </HButton>
344 <HButton
345 v-if="hasFormsOrActiveIntegrations"
346 variant="outline"
347 color="primary"
348 size="small"
349 icon-prepend="ic-plus-16"
350 :is-loading="integrationsStore.isLoading"
351 @click="showAddFormModal"
352 >
353 {{ translate('hostinger_reach_add_form') }}
354 </HButton>
355 </div>
356 </div>
357 </div>
358
359 <Integrations
360 :type="TABS_KEYS.OVERVIEW_TAB_FORMS"
361 :on-banner-button-click="showAddFormModal"
362 @go-to-plugin="handlePluginGoTo"
363 @disconnect-plugin="handlePluginDisconnect"
364 @toggle-form-status="handleFormToggleStatus"
365 @view-form="handleViewForm"
366 @edit-form="handleEditForm"
367 @add-form="handleAddForm"
368 />
369 <div class="overview__integrations-header">
370 <HText class="overview__tabs-header" as="h2" variant="heading-2">WooCommerce</HText>
371 <div class="overview__integrations-tabs">
372 <div v-if="wooCommerceConnected" class="overview__integrations-buttons">
373 <HButton
374 variant="text"
375 color="primary"
376 size="small"
377 icon-append="ic-arrow-up-right-square-16"
378 target="_blank"
379 :to="reachAutomationsLink"
380 >
381 {{ translate('hostinger_reach_woocommerce_manage_automations') }}
382 </HButton>
383 <HPopover
384 placement="bottom-end"
385 :show-arrow="false"
386 background-color="neutral--0"
387 border-radius="12px"
388 :outside-click-enabled="true"
389 :close-other-popovers-on-open="true"
390 >
391 <template #trigger>
392 <HButton variant="outline" color="primary" size="small" icon-prepend="ic-gear-16">
393 {{ translate('hostinger_reach_woocommerce_manage_plugin') }}
394 </HButton>
395 </template>
396 <div class="overview__popover-menu">
397 <div class="overview__popover-menu-item" @click="handlePluginGoTo(WOOCOMMERCE_ID)">
398 <HIcon name="ic-blocks-plus-16" />
399 <span>{{ translate('hostinger_reach_plugin_entries_table_go_to_plugin') }}</span>
400 <HIcon name="ic-arrow-up-right-square-16" />
401 </div>
402 <div class="overview__popover-menu-item" @click="handlePluginDisconnect(WOOCOMMERCE_ID)">
403 <HIcon name="ic-cross-circle-16" />
404 <span>{{ translate('hostinger_reach_plugin_entries_table_disconnect_plugin') }}</span>
405 </div>
406 </div>
407 </HPopover>
408 </div>
409 </div>
410 </div>
411 <WooCommerce
412 @go-to-plugin="handlePluginGoTo"
413 @disconnect-plugin="handlePluginDisconnect"
414 @toggle-form-status="handleFormToggleStatus"
415 />
416
417 <FAQ :faq-data="overviewFaqData" />
418 </div>
419 </div>
420 </div>
421 </template>
422
423 <style scoped lang="scss">
424 .overview {
425 min-height: 100vh;
426 background-color: var(--neutral--50);
427
428 &__header {
429 width: 100%;
430 padding: 40px 0 20px 0;
431 @media (max-width: 768px) {
432 padding: 16px 12px;
433 }
434
435 @media (max-width: 480px) {
436 padding: 12px 8px;
437 }
438 }
439
440 &__header-content {
441 display: flex;
442 justify-content: flex-start;
443 align-items: center;
444 width: 860px;
445 margin: 0 auto;
446
447 @media (max-width: 1023px) {
448 width: 100%;
449 }
450 }
451
452 &__header-brand {
453 display: flex;
454 align-items: center;
455 gap: 12px;
456
457 @media (max-width: 480px) {
458 gap: 8px;
459 }
460 }
461
462 &__header-logo {
463 height: 28px;
464 width: auto;
465
466 @media (max-width: 768px) {
467 height: 24px;
468 }
469
470 @media (max-width: 480px) {
471 height: 20px;
472 }
473 }
474
475 &__content {
476 display: flex;
477 flex-direction: column;
478 align-items: flex-end;
479 gap: 32px;
480 padding: 20px 0;
481 width: 860px;
482 margin: 0 auto;
483 }
484
485 &__integrations {
486 width: 100%;
487 }
488
489 &__integrations-header {
490 width: 100%;
491 margin-bottom: 16px;
492 display: flex;
493 flex-direction: row;
494 justify-content: space-between;
495 align-items: center;
496 gap: 10px;
497 flex-wrap: wrap;
498
499 @media (max-width: 992px) {
500 justify-content: start;
501 }
502 }
503
504 &__integrations-tabs {
505 display: flex;
506 justify-content: flex-end;
507 align-items: center;
508 width: 100%;
509
510 @media (max-width: 992px) {
511 flex-wrap: wrap;
512 justify-content: start;
513 }
514 }
515
516 &__integrations-buttons {
517 display: flex;
518 gap: 8px;
519
520 @media (max-width: 992px) {
521 flex-wrap: wrap;
522 width: 100%;
523
524 > div {
525 width: 100%;
526 }
527 }
528 }
529
530 &__section {
531 display: flex;
532 flex-direction: column;
533 align-self: stretch;
534 gap: 20px;
535 }
536
537 &__title {
538 display: flex;
539 justify-content: space-between;
540 align-items: center;
541 align-self: stretch;
542 }
543
544 &__title-buttons {
545 display: flex;
546 align-items: center;
547 gap: 8px;
548 }
549
550 &__popover-menu {
551 padding: 4px;
552 min-width: 180px;
553 }
554
555 &__popover-menu-item {
556 display: flex;
557 align-items: center;
558 gap: 8px;
559 padding: 12px;
560 cursor: pointer;
561 border-radius: 8px;
562 font-weight: 500;
563 font-size: 14px;
564 color: var(--neutral--600);
565 transition: background-color 0.2s ease;
566
567 &:hover {
568 background-color: var(--neutral--50);
569 }
570
571 span {
572 flex: 1;
573 }
574 }
575
576 &__your-plan-button {
577 margin-right: 0;
578 }
579
580 &__upgrade-button {
581 background: var(--neutral--0);
582 border: 1px solid transparent;
583 background-image:
584 linear-gradient(var(--neutral--0), var(--neutral--0)),
585 linear-gradient(135deg, var(--primary--200) 0%, var(--primary--400) 47.45%, var(--primary--600) 100%);
586 background-origin: border-box;
587 background-clip: padding-box, border-box;
588 color: var(--neutral--600);
589 }
590
591 &__section-content {
592 display: flex;
593 flex-direction: column;
594 align-self: stretch;
595 gap: 16px;
596 }
597
598 &__tabs-header {
599 width: 100%;
600 }
601 }
602
603 @media (max-width: 1023px) {
604 .overview {
605 &__content {
606 width: 100%;
607 padding: 24px 16px;
608 }
609
610 &__title {
611 flex-direction: column;
612 align-items: flex-start;
613 gap: 12px;
614 }
615
616 &__title-buttons {
617 align-self: stretch;
618 justify-content: flex-end;
619 }
620
621 &__integrations_tabs {
622 flex-direction: column;
623 }
624 }
625 }
626 </style>
627