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 (
-
+ 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;
`;