From d6d4905337540779aa0381002df5d6858ab52e1e Mon Sep 17 00:00:00 2001 From: Matheus Cristian Date: Tue, 24 Sep 2024 17:28:44 -0300 Subject: [PATCH] feat(commerce): adds mvp navigation mocked --- src/api/solutions.ts | 73 +++++++ src/components/Drawer.vue | 6 +- src/components/solutions/DrawerSolution.vue | 11 + src/components/solutions/SolutionsGroup.vue | 103 ++++++++++ .../SolutionsGroupSkeletonLoading.vue | 48 +++++ src/components/solutions/SolutionsList.vue | 93 +++++++++ src/locales/messages.json | 192 ++++-------------- src/router/index.ts | 37 ++++ src/stores/Solutions.ts | 142 +++++++++++++ src/views/Discovery.vue | 134 ++---------- src/views/HomeView.vue | 18 +- src/views/IntegratedSolutions.vue | 140 ++++--------- 12 files changed, 620 insertions(+), 377 deletions(-) create mode 100644 src/api/solutions.ts create mode 100644 src/components/solutions/SolutionsGroupSkeletonLoading.vue create mode 100644 src/components/solutions/SolutionsList.vue create mode 100644 src/stores/Solutions.ts diff --git a/src/api/solutions.ts b/src/api/solutions.ts new file mode 100644 index 0000000..8162507 --- /dev/null +++ b/src/api/solutions.ts @@ -0,0 +1,73 @@ +import { i18n } from '@/locales'; + +function sleep(timeInMs) { + return new Promise((resolve) => { + setTimeout(resolve, timeInMs); + }); +} + +export default { + async listIntegratedByCategory({ category }) { + await sleep(500); + + return [ + // { + // id: 'abandoned_cart', + // title: i18n.global.t('solutions.abandoned_cart.title'), + // description: i18n.global.t( + // 'solutions.abandoned_cart.description', + // ), + // }, + ]; + }, + + async listByCategory({ category }) { + await sleep(500); + + if (category === 'active') { + return [ + { + id: 'abandoned_cart', + title: i18n.global.t('solutions.abandoned_cart.title'), + description: i18n.global.t('solutions.abandoned_cart.description'), + }, + { + id: 'order_status', + title: i18n.global.t('solutions.order_status.title'), + description: i18n.global.t('solutions.order_status.description'), + }, + { + id: 'recurring_purchase', + title: i18n.global.t('solutions.recurring_purchase.title'), + description: i18n.global.t( + 'solutions.recurring_purchase.description', + ), + }, + ]; + } else if (category === 'passive') { + return [ + { + id: 'order_status_passive', + title: i18n.global.t('solutions.order_status_passive.title'), + description: i18n.global.t( + 'solutions.order_status_passive.description', + ), + }, + { + id: 'refer_and_win', + title: i18n.global.t('solutions.refer_and_win.title'), + description: i18n.global.t('solutions.refer_and_win.description'), + globals: [ + 'VTEX API App Key', + 'VTEX API App Token', + 'VTEX API App Key MD', + 'VTEX API App Token MD', + 'URL API Vtex', + ], + }, + ]; + } else { + return []; + } + }, +}; diff --git a/src/components/Drawer.vue b/src/components/Drawer.vue index a1f8e05..e363a55 100644 --- a/src/components/Drawer.vue +++ b/src/components/Drawer.vue @@ -16,8 +16,8 @@
@@ -53,6 +53,8 @@ import { onUnmounted, watch } from 'vue'; const props = defineProps<{ isOpen: boolean; title: string; + icon: string; + iconScheme: string; }>(); const emit = defineEmits<{ diff --git a/src/components/solutions/DrawerSolution.vue b/src/components/solutions/DrawerSolution.vue index 0d119d1..2aa891c 100644 --- a/src/components/solutions/DrawerSolution.vue +++ b/src/components/solutions/DrawerSolution.vue @@ -71,12 +71,19 @@ import SelectSmart from '@/components/SelectSmart.vue'; import { ref, useTemplateRef } from 'vue'; import { useI18n } from 'vue-i18n'; import { useAlertStore } from '@/stores/Alert'; +import { useSolutionsStore } from '@/stores/Solutions'; import { useRouter } from 'vue-router'; +const props = defineProps<{ + id: string; + category: 'activeNotifications' | 'passiveService'; +}>(); + const { t } = useI18n(); const router = useRouter(); const alertStore = useAlertStore(); +const solutionsStore = useSolutionsStore(); const drawerRef = useTemplateRef('drawer'); @@ -90,6 +97,10 @@ function close() { function save() { close(); + solutionsStore.integrate({ + id: props.id, + }); + alertStore.add({ type: 'success', text: t('solutions.integrate.status.created'), diff --git a/src/components/solutions/SolutionsGroup.vue b/src/components/solutions/SolutionsGroup.vue index d10b92c..f657124 100644 --- a/src/components/solutions/SolutionsGroup.vue +++ b/src/components/solutions/SolutionsGroup.vue @@ -12,6 +12,7 @@ :key="index" :title="solution.title" :description="solution.description" + :options="getOptionsBySolution(solution)" @add="openIntegrateSolutionModal(solution)" /> @@ -25,9 +26,49 @@ /> + + + + + {{ + $t('solutions.disable.confirmation.description.0', { + name: solutionToDisintegrate.solution?.title, + }) + }} + + + @@ -38,9 +79,12 @@ import Header from '@/components/Header.vue'; import SolutionCard from '@/components/solutions/SolutionCard.vue'; import ModalIntegrate from '@/components/solutions/ModalIntegrate.vue'; import DrawerSolution from '@/components/solutions/DrawerSolution.vue'; +import { useSolutionsStore } from '@/stores/Solutions'; const { t } = useI18n(); +const solutionsStore = useSolutionsStore(); + const isOpen = ref(false); type Solution = { @@ -53,12 +97,25 @@ defineProps<{ title: string; icon: string; iconScheme: string; + category: 'activeNotifications' | 'passiveService'; solutions: Solution[]; }>(); +const solutionToDisintegrate = reactive<{ + isOpen: boolean; + solution: null | { + id: string; + title: string; + }; +}>({ + isOpen: false, + solution: null, +}); + const solutionToIntegrate = reactive<{ isOpen: boolean; solution: null | { + id: string; title: string; description: string; tip: string; @@ -68,15 +125,61 @@ const solutionToIntegrate = reactive<{ solution: null, }); +function getOptionsBySolution(solution) { + const integrated = [ + solutionsStore.integrated.activeNotifications.data, + solutionsStore.integrated.passiveService.data, + ].flat(); + + if (integrated.some((integrated) => integrated.id === solution.id)) { + return [ + { + icon: 'visibility', + title: t('solutions.actions.see_details'), + }, + { + icon: 'settings', + title: t('solutions.actions.settings'), + }, + { + type: 'separator', + }, + { + icon: 'do_not_disturb_on', + title: t('solutions.actions.disable_solution'), + scheme: 'aux-red-500', + onClick: openDisable.bind(this, solution), + }, + ]; + } else { + return undefined; + } +} + function openIntegrateSolutionModal(solution: Solution) { solutionToIntegrate.isOpen = true; solutionToIntegrate.solution = { + id: solution.id, title: t(`solutions.${solution.id}.title`), description: t(`solutions.${solution.id}.description`), tip: t(`solutions.${solution.id}.tip`), }; } + +function openDisable(solution) { + solutionToDisintegrate.isOpen = true; + + solutionToDisintegrate.solution = solution; +} + +function disintegrate() { + solutionToDisintegrate.isOpen = false; + + solutionsStore.disintegrate({ id: solutionToDisintegrate.solution?.id }); + + solutionToDisintegrate.solution = null; +} diff --git a/src/components/solutions/SolutionsList.vue b/src/components/solutions/SolutionsList.vue new file mode 100644 index 0000000..d7c1b94 --- /dev/null +++ b/src/components/solutions/SolutionsList.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/src/locales/messages.json b/src/locales/messages.json index a94fd05..25c64b6 100644 --- a/src/locales/messages.json +++ b/src/locales/messages.json @@ -185,22 +185,16 @@ "es": "Carrito abandonado" }, - "short_description": { - "pt-br": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "en-us": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "es": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat." - }, - "description": { - "pt-br": "Reengaje automaticamente seus clientes enviando lembretes personalizados quando eles deixarem produtos no carrinho sem finalizar a compra.", - "en-us": "Automatically re-engage your customers by sending personalized reminders when they leave products in the cart without completing the purchase.", - "es": "Vuelva a captar automáticamente a sus clientes enviándoles recordatorios personalizados cuando dejen productos en la cesta sin finalizar la compra." + "pt-br": "Recupere vendas lembrando clientes de itens esquecidos no carrinho.", + "en-us": "Recover sales by reminding customers of items forgotten in the cart.", + "es": "Recuperar ventas recordando a los clientes los artículos olvidados en el carrito." }, "tip": { - "pt-br": "Com essa solução, você aumenta suas chances de conversão, mantendo seu cliente próximo e incentivando a conclusão do pedido.", - "en-us": "With this solution, you increase your chances of conversion by keeping your customer close and encouraging them to complete their order.", - "es": "Con esta solución, aumentará sus posibilidades de conversión manteniendo a su cliente cerca y animándole a completar su pedido." + "pt-br": "Com essa solução, é possível aumentar suas chances de conversão, mantendo seu cliente próximo e incentivando a conclusão do pedido.", + "en-us": "With this solution, you can increase your chances of conversion by keeping your customer close and encouraging them to complete their order.", + "es": "Con esta solución, puede aumentar sus posibilidades de conversión manteniendo a su cliente cerca y animándole a completar su pedido." } }, @@ -211,178 +205,76 @@ "es": "Estado del pedido" }, - "short_description": { - "pt-br": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "en-us": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "es": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat." - }, - - "description": { - "pt-br": "", - "en-us": "", - "es": "" - }, - - "tip": { - "pt-br": "", - "en-us": "", - "es": "" - } - }, - - "order_tracking": { - "title": { - "pt-br": "Tracking de pedido", - "en-us": "Order tracking", - "es": "Seguimiento del pedido" - }, - - "short_description": { - "pt-br": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "en-us": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "es": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat." - }, - - "description": { - "pt-br": "", - "en-us": "", - "es": "" - }, - - "tip": { - "pt-br": "", - "en-us": "", - "es": "" - } - }, - - "product_replacement": { - "title": { - "pt-br": "Substituição do produto", - "en-us": "Product replacement", - "es": "Sustitución del producto" - }, - - "short_description": { - "pt-br": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "en-us": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "es": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat." - }, - "description": { - "pt-br": "", - "en-us": "", - "es": "" + "pt-br": "Informe seus clientes sobre o andamento dos pedidos em tempo real.", + "en-us": "Inform your customers about the progress of their orders in real time.", + "es": "Informe a sus clientes sobre el progreso de sus pedidos en tiempo real." }, "tip": { - "pt-br": "", - "en-us": "", - "es": "" + "pt-br": "Com essa solução, é possível melhorar a comunicação com o cliente e reduzir a incerteza sobre o processo de compra, aumentando a satisfação e a lealdade ao longo do tempo.", + "en-us": "With this solution, it is possible to improve communication with the customer and reduce uncertainty about the purchasing process, increasing satisfaction and loyalty over time.", + "es": "Con esta solución, es posible mejorar la comunicación con el cliente y reducir la incertidumbre sobre el proceso de compra, aumentando la satisfacción y la fidelidad a lo largo del tiempo." } }, - "product_exchange_and_return": { + "recurring_purchase": { "title": { - "pt-br": "Troca e devolução de produto", - "en-us": "Product exchange and return", - "es": "Cambio y devolución del producto" - }, - - "short_description": { - "pt-br": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "en-us": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "es": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat." + "pt-br": "Compra recorrente", + "en-us": "Recurring purchase", + "es": "Compra periódica" }, "description": { - "pt-br": "", - "en-us": "", - "es": "" + "pt-br": "Facilite a compra automática de produtos com uma solução de assinatura.", + "en-us": "Facilitate the automatic purchase of products with a subscription solution.", + "es": "Facilite la compra automática de productos con una solución de suscripción." }, "tip": { - "pt-br": "", - "en-us": "", - "es": "" + "pt-br": "Com essa solução, é possível aumentar a retenção de clientes e gerar vendas recorrentes, proporcionando uma experiência de compra sem esforço para o consumidor.", + "en-us": "With this solution, you can increase customer retention and generate recurring sales, providing an effortless shopping experience for the consumer.", + "es": "Con esta solución, puede aumentar la retención de clientes y generar ventas recurrentes, proporcionando una experiencia de compra sin esfuerzo para el consumidor." } }, - "FAQ": { + "order_status_passive": { "title": { - "pt-br": "FAQ", - "en-us": "FAQ", - "es": "FAQ" - }, - - "short_description": { - "pt-br": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "en-us": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "es": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat." + "pt-br": "Status do pedido (Passivo)", + "en-us": "Order status (Passive)", + "es": "Estado del pedido (Pasivo)" }, "description": { - "pt-br": "", - "en-us": "", - "es": "" + "pt-br": "Ofereça aos clientes uma forma prática de consultar o status do pedido.", + "en-us": "Offer customers a practical way to check the status of their order.", + "es": "Ofrezca a los clientes una forma práctica de comprobar el estado de su pedido." }, "tip": { - "pt-br": "", - "en-us": "", - "es": "" + "pt-br": "Com essa solução, você reduz o volume de consultas manuais e melhora a transparência, permitindo que o cliente se sinta sempre informado e no controle do processo de compra.", + "en-us": "With this solution, you reduce the volume of manual queries and improve transparency, allowing the customer to always feel informed and in control of the purchasing process.", + "es": "Con esta solución, se reduce el volumen de consultas manuales y se mejora la transparencia, lo que permite al cliente sentirse siempre informado y en control del proceso de compra." } }, - "order_invoice_inquiry": { + "refer_and_win": { "title": { - "pt-br": "Consulta de pedido / nota fiscal", - "en-us": "Order / invoice inquiry", - "es": "Solicitud de pedido / factura" - }, - - "short_description": { - "pt-br": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "en-us": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "es": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat." - }, - - "description": { - "pt-br": "", - "en-us": "", - "es": "" - }, - - "tip": { - "pt-br": "", - "en-us": "", - "es": "" - } - }, - - "transfer_to_human_care": { - "title": { - "pt-br": "Transferência para atendimento humano", - "en-us": "Transfer to human care", - "es": "Transferir a servicios humanos" - }, - - "short_description": { - "pt-br": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "en-us": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat.", - "es": "Velit assumenda culpa sunt repellat omnis deserunt. Velit assumenda culpa sunt repellat." + "pt-br": "Indique e ganhe", + "en-us": "Refer and win", + "es": "Recomiéndanos y gana" }, "description": { - "pt-br": "", - "en-us": "", - "es": "" + "pt-br": "Incentive os clientes a recomendar sua loja para amigos e familiares com um programa de recompensas.", + "en-us": "Encourage customers to recommend your store to friends and family with a rewards program.", + "es": "Anime a los clientes a recomendar su tienda a amigos y familiares con un programa de recompensas." }, "tip": { - "pt-br": "", - "en-us": "", - "es": "" + "pt-br": "Com essa solução, é possível ampliar a base de clientes de forma orgânica e aumentar o engajamento, transformando consumidores satisfeitos em promotores ativos da sua marca.", + "en-us": "With this solution, you can expand your customer base organically and increase engagement, turning satisfied consumers into active promoters of your brand.", + "es": "Con esta solución, puede ampliar su base de clientes de forma orgánica y aumentar el compromiso, convirtiendo a los consumidores satisfechos en promotores activos de su marca." } } } diff --git a/src/router/index.ts b/src/router/index.ts index 5f7d007..a7aff21 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -2,10 +2,36 @@ import { createRouter, createWebHistory } from 'vue-router'; import HomeView from '../views/HomeView.vue'; import Discovery from '@/views/Discovery.vue'; import IntegratedSolutions from '@/views/IntegratedSolutions.vue'; +import { i18n } from '@/locales'; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ + { + path: '/loginexternal/:token/', + name: 'externalLogin', + component: {}, + beforeEnter: async (to, from, next) => { + const { token } = to.params; + + // to.query.org_uuid + // to.query.project_uuid + // to.query.locale + + i18n.global.locale = + { + en: 'en-us', + }[to.query.locale] || to.query.locale; + + const nextPath = to.query.next; + + if (nextPath) { + next({ path: nextPath, replace: true }); + } else { + next({ path: '/', replace: true }); + } + }, + }, { path: '/', name: 'home', @@ -27,4 +53,15 @@ const router = createRouter({ ], }); +router.afterEach(() => { + window.parent.postMessage( + { + event: 'changePathname', + pathname: window.location.pathname, + moduleName: 'commerce', + }, + '*', + ); +}); + export default router; diff --git a/src/stores/Solutions.ts b/src/stores/Solutions.ts new file mode 100644 index 0000000..fc2eb35 --- /dev/null +++ b/src/stores/Solutions.ts @@ -0,0 +1,142 @@ +import { defineStore } from 'pinia'; +import { computed, ref } from 'vue'; +import APISolutions from '@/api/solutions'; + +type Solution = { + id: string; + title: string; + description: string; +}; + +function makeS({ request, category }) { + const status = ref(null); + const alreadyCalledLoad = ref(false); + const data = ref([]); + + const isFirstLoading = computed( + () => !alreadyCalledLoad.value && status.value === 'loading', + ); + + async function load() { + if (status.value !== null) { + return; + } + + try { + status.value = 'loading'; + + data.value = await request({ category }); + + status.value = 'complete'; + } catch { + status.value = 'error'; + } finally { + alreadyCalledLoad.value = true; + } + } + + function add(solution: Solution) { + data.value.push(solution); + } + + function remove(solution: Solution) { + data.value.splice( + data.value.findIndex(({ id }) => id === solution.id), + 1, + ); + } + + return { status, isFirstLoading, data, load, add, remove }; +} + +export const useSolutionsStore = defineStore('solutions', () => { + const integratedActiveNotifications = makeS({ + request: APISolutions.listIntegratedByCategory, + category: 'active', + }); + + const integratedPassiveService = makeS({ + request: APISolutions.listIntegratedByCategory, + category: 'passive', + }); + + const integratedIds = computed(() => + [ + integratedActiveNotifications.data.value, + integratedPassiveService.data.value, + ] + .flat() + .map(({ id }) => id), + ); + + const activeNotifications = makeS({ + request: APISolutions.listByCategory, + category: 'active', + }); + + const passiveService = makeS({ + request: APISolutions.listByCategory, + category: 'passive', + }); + + function findSolution({ id }) { + const allSolutions = computed(() => + [ + activeNotifications.data.value.map((item) => ({ + ...item, + parent: integratedActiveNotifications, + })), + + passiveService.data.value.map((item) => ({ + ...item, + parent: integratedPassiveService, + })), + ].flat(), + ); + + return allSolutions.value.find((solution) => solution.id === id); + } + + function integrate({ id }: { id: string }) { + const solutionToIntegrate = findSolution({ id }); + + solutionToIntegrate?.parent.add({ + id: solutionToIntegrate.id, + title: solutionToIntegrate.title, + description: solutionToIntegrate.description, + }); + } + + function disintegrate({ id }: { id: string }) { + const solutionToDisintegrate = findSolution({ id }); + + solutionToDisintegrate?.parent.remove({ id: id }); + } + + const availableActiveNotifications = computed(() => + activeNotifications.data.value.filter( + (solution) => !integratedIds.value.includes(solution.id), + ), + ); + + const availablePassiveService = computed(() => + passiveService.data.value.filter( + (solution) => !integratedIds.value.includes(solution.id), + ), + ); + + return { + activeNotifications, + passiveService, + available: { + activeNotifications: availableActiveNotifications, + passiveService: availablePassiveService, + }, + integrated: { + activeNotifications: integratedActiveNotifications, + passiveService: integratedPassiveService, + }, + integrate, + disintegrate, + }; +}); diff --git a/src/views/Discovery.vue b/src/views/Discovery.vue index 198ec16..1c8ee47 100644 --- a/src/views/Discovery.vue +++ b/src/views/Discovery.vue @@ -1,127 +1,25 @@ - + diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 9836170..a4490e1 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -28,8 +28,24 @@ +