From 835f82dbcc26d8c33c249d167c4c329b6064e4a2 Mon Sep 17 00:00:00 2001 From: Jared Vu Date: Fri, 20 Oct 2023 16:30:22 -0700 Subject: [PATCH] Support Intercom (#92) * Add ChatIcon * FooterDesktop Help&Support * HelpDialog w/ triggers * Add other links * item gap css var passing * inject script * Add quotes to script * Close modal on select * Remove feedback form --- package.json | 1 + scripts/inject-intercom.js | 76 +++++++++++++++++ src/components/ComboboxDialogMenu.tsx | 52 ++++++++---- src/components/ComboboxMenu.tsx | 15 ++-- src/components/Dialog.tsx | 4 +- src/components/Icon.tsx | 3 + src/constants/menus.ts | 2 +- src/icons/chat-bubble.svg | 1 + src/icons/feedback.svg | 2 +- src/icons/index.ts | 1 + src/layout/Footer/FooterDesktop.tsx | 87 ++++++++++++++----- src/polyfills.ts | 2 + src/styles/constants.css | 9 +- src/styles/popoverMixins.ts | 3 +- src/views/dialogs/HelpDialog.tsx | 117 ++++++++++++++++---------- 15 files changed, 275 insertions(+), 100 deletions(-) create mode 100644 scripts/inject-intercom.js create mode 100644 src/icons/chat-bubble.svg diff --git a/package.json b/package.json index c1682d3..382986b 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "build:inject-app-deeplinks": "sh scripts/inject-app-deeplinks.sh", "build:inject-amplitude": "node scripts/inject-amplitude.js", "build:inject-bugsnag": "node scripts/inject-bugsnag.js", + "build:inject-intercom": "node scripts/inject-intercom.js", "build:inject-statuspage": "node scripts/inject-statuspage.js", "deploy:ipfs": "node scripts/upload-ipfs.js --verbose", "deploy:update-ipns": "node scripts/update-ipns.js", diff --git a/scripts/inject-intercom.js b/scripts/inject-intercom.js new file mode 100644 index 0000000..f344e92 --- /dev/null +++ b/scripts/inject-intercom.js @@ -0,0 +1,76 @@ +/* eslint-disable no-console */ +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const INTERCOM_APP_ID = process.env.INTERCOM_APP_ID; + +const currentPath = fileURLToPath(import.meta.url); +const projectRoot = path.dirname(currentPath); +const htmlFilePath = path.resolve(projectRoot, '../dist/index.html'); + +if (INTERCOM_APP_ID) { + try { + const html = await fs.readFile(htmlFilePath, 'utf-8'); + + const intercomScripts = ` + + + + + `; + + const injectedHtml = html.replace( + '
', + `
\n${intercomScripts}\n` + ); + + await fs.writeFile(htmlFilePath, injectedHtml, 'utf-8'); + + console.log('Intercom scripts successfully injected.'); + } catch (err) { + console.error('Error injecting Intercom scripts:', err); + } +} diff --git a/src/components/ComboboxDialogMenu.tsx b/src/components/ComboboxDialogMenu.tsx index 306eec3..f4e231c 100644 --- a/src/components/ComboboxDialogMenu.tsx +++ b/src/components/ComboboxDialogMenu.tsx @@ -3,31 +3,43 @@ import styled, { AnyStyledComponent } from 'styled-components'; import { type MenuConfig } from '@/constants/menus'; -import { Dialog, DialogPlacement } from '@/components/Dialog'; -import { ComboboxMenu } from '@/components/ComboboxMenu'; +import { Dialog, DialogPlacement, type DialogProps } from '@/components/Dialog'; +import { ComboboxMenu, type ComboboxMenuProps } from '@/components/ComboboxMenu'; type ElementProps = { - isOpen?: boolean; - setIsOpen?: (open: boolean) => void; title?: React.ReactNode; description?: React.ReactNode; - slotTrigger?: React.ReactNode; - slotHeaderInner?: React.ReactNode; - slotFooter?: React.ReactNode; children?: React.ReactNode; - items: MenuConfig; - onItemSelected?: () => void; - inputPlaceholder?: string; - slotEmpty?: React.ReactNode; }; type StyleProps = { - placement: DialogPlacement, className?: string; }; -export const ComboboxDialogMenu = ({ +type PickComboxMenuProps< + MenuItemValue extends string | number, + MenuGroupValue extends string | number +> = Pick< + ComboboxMenuProps, + 'inputPlaceholder' | 'onItemSelected' | 'slotEmpty' | 'withSearch' | 'withStickyLayout' +>; + +type PickDialogProps = Pick< + DialogProps, + | 'description' + | 'isOpen' + | 'placement' + | 'setIsOpen' + | 'slotHeaderInner' + | 'slotTrigger' + | 'slotFooter' +>; + +export const ComboboxDialogMenu = < + MenuItemValue extends string | number, + MenuGroupValue extends string | number +>({ isOpen = false, setIsOpen, title, @@ -40,11 +52,16 @@ export const ComboboxDialogMenu = & StyleProps) => ( +}: ElementProps & + PickComboxMenuProps & + PickDialogProps & + StyleProps) => ( // TODO: sub-menu state management @@ -64,7 +80,8 @@ export const ComboboxDialogMenu = {children} @@ -98,4 +115,5 @@ Styled.Dialog = styled(Dialog)` Styled.ComboboxMenu = styled(ComboboxMenu)` --comboboxMenu-backgroundColor: var(--comboboxDialogMenu-backgroundColor); + --comboboxMenu-item-gap: var(--comboxDialogMenu-item-gap, 0.5rem); `; diff --git a/src/components/ComboboxMenu.tsx b/src/components/ComboboxMenu.tsx index e8fa87d..9f5ce78 100644 --- a/src/components/ComboboxMenu.tsx +++ b/src/components/ComboboxMenu.tsx @@ -8,7 +8,7 @@ import { layoutMixins } from '@/styles/layoutMixins'; import { Tag } from '@/components/Tag'; -type ElementProps = { +type ElementProps = { items: MenuConfig; onItemSelected?: () => void; @@ -23,6 +23,11 @@ type StyleProps = { withStickyLayout?: boolean; }; +export type ComboboxMenuProps< + MenuItemValue extends string | number, + MenuGroupValue extends string | number +> = ElementProps & StyleProps; + export const ComboboxMenu = ({ items, onItemSelected, @@ -34,7 +39,7 @@ export const ComboboxMenu = & StyleProps) => { +}: ComboboxMenuProps) => { const [highlightedCommand, setHighlightedCommand] = useState(); const [searchValue, setSearchValue] = useState(''); // const inputRef = useRef(null); @@ -171,6 +176,7 @@ Styled.Command = styled(Command)<{ $withStickyLayout?: boolean }>` --comboboxMenu-item-highlighted-backgroundColor: var(--color-layer-3); --comboboxMenu-item-highlighted-textColor: var(--color-text-1); --comboboxMenu-item-backgroundColor: ; + --comboboxMenu-item-gap: 0.5rem; display: grid; align-content: start; @@ -269,18 +275,17 @@ Styled.List = styled(Command.List)<{ $withStickyLayout?: boolean }>` `; Styled.Item = styled(Command.Item)` + ${layoutMixins.scrollSnapItem} ${popoverMixins.item} --item-checked-backgroundColor: var(--comboboxMenu-item-checked-backgroundColor); --item-checked-textColor: var(--comboboxMenu-item-checked-textColor); --item-highlighted-textColor: var(--comboboxMenu-item-highlighted-textColor); - - ${layoutMixins.scrollSnapItem} + --item-gap: var(--comboboxMenu-item-gap); background-color: var(--comboboxMenu-backgroundColor, inherit); display: flex; align-items: center; - gap: 0.5rem; &[aria-disabled='true'] { opacity: 0.75; diff --git a/src/components/Dialog.tsx b/src/components/Dialog.tsx index 32cdf26..f64d89f 100644 --- a/src/components/Dialog.tsx +++ b/src/components/Dialog.tsx @@ -48,6 +48,8 @@ type StyleProps = { className?: string; }; +export type DialogProps = ElementProps & StyleProps; + const DialogPortal = ({ withPortal, container, @@ -82,7 +84,7 @@ export const Dialog = ({ hasHeaderBorder = false, children, className, -}: ElementProps & StyleProps) => { +}: DialogProps) => { const closeButtonRef = useRef(); const showOverlay = ![DialogPlacement.Inline, DialogPlacement.FullScreen].includes(placement); diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 5a36f1d..b7bf634 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -13,6 +13,7 @@ import { CaretIcon, CautionCircleStrokeIcon, CautionCircleIcon, + ChatIcon, CheckIcon, ChevronLeftIcon, ChevronRightIcon, @@ -80,6 +81,7 @@ export enum IconName { Caret = 'Caret', CautionCircle = 'CautionCircle', CautionCircleStroked = 'CautionCircleStroked', + Chat = 'Chat', Check = 'Check', ChevronLeft = 'ChevronLeft', ChevronRight = 'ChevronRight', @@ -148,6 +150,7 @@ const icons = { [IconName.Caret]: CaretIcon, [IconName.CautionCircle]: CautionCircleIcon, [IconName.CautionCircleStroked]: CautionCircleStrokeIcon, + [IconName.Chat]: ChatIcon, [IconName.Check]: CheckIcon, [IconName.ChevronLeft]: ChevronLeftIcon, [IconName.ChevronRight]: ChevronRightIcon, diff --git a/src/constants/menus.ts b/src/constants/menus.ts index b350873..da72dd6 100644 --- a/src/constants/menus.ts +++ b/src/constants/menus.ts @@ -21,7 +21,7 @@ export type MenuItem = { value: MenuItemValue; slotBefore?: React.ReactNode; - label: string; + label: React.ReactNode; labelRight?: React.ReactNode; tag?: React.ReactNode; slotAfter?: React.ReactNode; diff --git a/src/icons/chat-bubble.svg b/src/icons/chat-bubble.svg new file mode 100644 index 0000000..6a2f865 --- /dev/null +++ b/src/icons/chat-bubble.svg @@ -0,0 +1 @@ + diff --git a/src/icons/feedback.svg b/src/icons/feedback.svg index ed38c37..ae390f5 100644 --- a/src/icons/feedback.svg +++ b/src/icons/feedback.svg @@ -1 +1 @@ - + diff --git a/src/icons/index.ts b/src/icons/index.ts index a96402a..f840de8 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -8,6 +8,7 @@ export { default as CalculatorIcon } from './calculator.svg'; export { default as CaretIcon } from './caret-down.svg'; export { default as CautionCircleIcon } from './caution-circle.svg'; export { default as CautionCircleStrokeIcon } from './caution-circle-stroke.svg'; +export { default as ChatIcon } from './chat-bubble.svg'; export { default as CheckIcon } from './check.svg'; export { default as ChevronLeftIcon } from './chevron-left.svg'; export { default as ChevronRightIcon } from './chevron-right.svg'; diff --git a/src/layout/Footer/FooterDesktop.tsx b/src/layout/Footer/FooterDesktop.tsx index dcdbb40..226fab4 100644 --- a/src/layout/Footer/FooterDesktop.tsx +++ b/src/layout/Footer/FooterDesktop.tsx @@ -1,12 +1,16 @@ import styled, { type AnyStyledComponent, css } from 'styled-components'; import { AbacusApiStatus } from '@/constants/abacus'; +import { ButtonSize } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; +import { ENVIRONMENT_CONFIG_MAP } from '@/constants/networks'; -import { useApiState, useStringGetter } from '@/hooks'; +import { useApiState, useSelectedNetwork, useStringGetter } from '@/hooks'; +import { ChatIcon, LinkOutIcon } from '@/icons'; import { layoutMixins } from '@/styles/layoutMixins'; +import { Button } from '@/components/Button'; import { Details } from '@/components/Details'; import { Output, OutputType } from '@/components/Output'; import { WithTooltip } from '@/components/WithTooltip'; @@ -24,6 +28,8 @@ enum ExchangeStatus { export const FooterDesktop = () => { const stringGetter = useStringGetter(); const { height, indexerHeight, status, statusErrorMessage } = useApiState(); + const { selectedNetwork } = useSelectedNetwork(); + const { statusPage } = ENVIRONMENT_CONFIG_MAP[selectedNetwork].links; const { exchangeStatus, label } = !status || status === AbacusApiStatus.NORMAL @@ -38,20 +44,37 @@ export const FooterDesktop = () => { return ( - -
{statusErrorMessage}
- - ) - } - > - - - {label} - -
+ + +
{statusErrorMessage}
+ + ) + } + > + } + slotRight={statusPage && } + size={ButtonSize.XSmall} + state={{ isDisabled: !statusPage }} + > + {label} + +
+ + {globalThis?.Intercom && ( + } + size={ButtonSize.XSmall} + onClick={() => globalThis.Intercom('show')} + > + {stringGetter({ key: STRING_KEYS.HELP_AND_SUPPORT })} + + )} +
+ {import.meta.env.MODE !== 'production' && ( ` - width: 1em; - height: 1em; + width: 0.5rem; + height: 0.5rem; border-radius: 50%; + margin-right: 0.25rem; background-color: ${({ exchangeStatus }) => ({ @@ -104,12 +129,28 @@ Styled.StatusDot = styled.div<{ exchangeStatus: ExchangeStatus }>` }[exchangeStatus])}; `; +Styled.FooterButton = styled(Button)` + --button-height: 1.5rem; + --button-radius: 0.25rem; + --button-backgroundColor: var(--color-layer-2); + --button-border: none; + --button-textColor: var(--color-text-0); + + &:hover:not(:disabled) { + --button-backgroundColor: var(--color-layer-3); + --button-textColor: var(--color-text-1); + } + + &:disabled { + cursor: default; + } +`; + Styled.WarningOutput = styled(Output)` color: var(--color-warning); `; Styled.Details = styled(Details)` ${layoutMixins.scrollArea} - - padding: 0 0.5em; + font: var(--font-tiny-book); `; diff --git a/src/polyfills.ts b/src/polyfills.ts index 481da45..6784bea 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -13,4 +13,6 @@ declare global { 'abacus:connectNetwork': CustomEvent; } + + var Intercom: any; } diff --git a/src/styles/constants.css b/src/styles/constants.css index 7e0cd0c..213f442 100644 --- a/src/styles/constants.css +++ b/src/styles/constants.css @@ -36,11 +36,10 @@ --single-unit-height: 9.375rem; /* Modal constants */ - --sidebar-modal-width: 18.75rem; - --modal-small-width: 18.75rem; - --modal-medium-width: 20.5rem; - --modal-large-width: 26.25rem; - --modal-header-height: 4rem; + --dialog-small-width: 18.75rem; + --dialog-medium-width: 20.5rem; + --dialog-large-width: 26.25rem; + --dialog-header-height: 4rem; /* Market Selector constants */ --marketsDropdown-openWidth: 45rem; diff --git a/src/styles/popoverMixins.ts b/src/styles/popoverMixins.ts index 733e18f..90697ea 100644 --- a/src/styles/popoverMixins.ts +++ b/src/styles/popoverMixins.ts @@ -188,13 +188,14 @@ export const popoverMixins = { --item-highlighted-backgroundColor: var(--color-layer-2); --item-highlighted-textColor: var(--color-text-2); + --item-gap: 0.5em; --item-radius: 0px; --item-padding: 0.5em 1em; display: flex; justify-content: space-between; align-items: center; - gap: 0.5em; + gap: var(--item-gap); padding: var(--item-padding); border-radius: var(--item-radius); diff --git a/src/views/dialogs/HelpDialog.tsx b/src/views/dialogs/HelpDialog.tsx index ab71c09..e89f2c1 100644 --- a/src/views/dialogs/HelpDialog.tsx +++ b/src/views/dialogs/HelpDialog.tsx @@ -1,69 +1,94 @@ +import { useMemo } from 'react'; import styled, { AnyStyledComponent } from 'styled-components'; -import { useDispatch } from 'react-redux'; -import { Close } from '@radix-ui/react-dialog'; import { STRING_KEYS } from '@/constants/localization'; import { useStringGetter } from '@/hooks'; -import { layoutMixins } from '@/styles/layoutMixins'; +import { ChatIcon, FeedbackIcon, FileIcon, TerminalIcon } from '@/icons'; -import { Button } from '@/components/Button'; -import { Dialog } from '@/components/Dialog'; -import { ButtonAction, ButtonType } from '@/constants/buttons'; -import { Icon, IconName } from '@/components/Icon'; +import { ComboboxDialogMenu } from '@/components/ComboboxDialogMenu'; + +import { isTruthy } from '@/lib/isTruthy'; type ElementProps = { setIsOpen: (open: boolean) => void; }; -export const HELP_URL = `https://docs.google.com/forms/d/e/1FAIpQLSezLsWCKvAYDEb7L-2O4wOON1T56xxro9A2Azvl6IxXHP_15Q/viewform?usp=sf_link`; +const HELP_LINKS = { + apiDocumentation: 'https://v4-teacher.vercel.app/', + helpCenter: null, + feedback: null, +}; -/** - * HelpDialog - * - Will temporarily be used as a 'Give Feedback' dialog. - * - It will ask users to navigate to a Google Form in order to record feedback. - */ export const HelpDialog = ({ setIsOpen }: ElementProps) => { const stringGetter = useStringGetter(); + const HELP_ITEMS = useMemo( + () => [ + { + group: 'help-items', + items: [ + HELP_LINKS.helpCenter && { + value: 'help-center', + label: stringGetter({ key: STRING_KEYS.HELP_CENTER }), + description: stringGetter({ key: STRING_KEYS.HELP_CENTER_DESCRIPTION }), + onSelect: () => { + HELP_LINKS.helpCenter && globalThis.open(HELP_LINKS.helpCenter, '_blank'); + setIsOpen(false); + }, + slotBefore: , + }, + HELP_LINKS.apiDocumentation && { + value: 'api-documentation', + label: stringGetter({ key: STRING_KEYS.API_DOCUMENTATION }), + description: stringGetter({ key: STRING_KEYS.API_DOCUMENTATION_DESCRIPTION }), + onSelect: () => { + HELP_LINKS.apiDocumentation && globalThis.open(HELP_LINKS.apiDocumentation, '_blank'); + setIsOpen(false); + }, + slotBefore: , + }, + globalThis?.Intercom && { + value: 'live-chat', + label: stringGetter({ key: STRING_KEYS.LIVE_CHAT }), + description: stringGetter({ key: STRING_KEYS.LIVE_CHAT_DESCRIPTION }), + onSelect: () => { + globalThis.Intercom('show'); + setIsOpen(false); + }, + slotBefore: , + }, + HELP_LINKS.feedback && { + value: 'feedback', + label: stringGetter({ key: STRING_KEYS.PROVIDE_FEEDBACK }), + description: stringGetter({ key: STRING_KEYS.PROVIDE_FEEDBACK_DESCRIPTION }), + onSelect: () => { + HELP_LINKS.feedback && globalThis.open(HELP_LINKS.feedback, '_blank'); + setIsOpen(false); + }, + slotBefore: , + }, + ].filter(isTruthy), + }, + ], + [stringGetter] + ); + return ( - - -

- You will be navigated to a Google Form where you will be able to provide feedback. Thank - you for your contribution! -

- - - - - -
-
+ title={stringGetter({ key: STRING_KEYS.HELP })} + items={HELP_ITEMS} + /> ); }; const Styled: Record = {}; -Styled.ButtonRow = styled.div` - ${layoutMixins.row} - - gap: 0.5rem; - justify-content: end; -`; - -Styled.Content = styled.div` - ${layoutMixins.column} - gap: 1rem; +Styled.ComboboxDialogMenu = styled(ComboboxDialogMenu)` + --dialog-width: var(--dialog-small-width); + --dialog-content-paddingTop: 1rem; + --dialog-content-paddingBottom: 1rem; + --comboxDialogMenu-item-gap: 1rem; `;