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