diff --git a/app/(dashboard)/dashboard/protocols/_components/DeleteProtocolsDialog.tsx b/app/(dashboard)/dashboard/protocols/_components/DeleteProtocolsDialog.tsx index 56c59ee85..ba4360d0e 100644 --- a/app/(dashboard)/dashboard/protocols/_components/DeleteProtocolsDialog.tsx +++ b/app/(dashboard)/dashboard/protocols/_components/DeleteProtocolsDialog.tsx @@ -14,6 +14,8 @@ import type { ProtocolWithInterviews } from '~/shared/types'; import { useEffect, useState } from 'react'; import type { Dispatch, SetStateAction } from 'react'; import { api } from '~/trpc/client'; +import { clientRevalidateTag } from '~/utils/clientRevalidate'; +import { useRouter } from 'next/navigation'; interface DeleteProtocolsDialogProps { open: boolean; @@ -26,6 +28,8 @@ export const DeleteProtocolsDialog = ({ setOpen, protocolsToDelete, }: DeleteProtocolsDialogProps) => { + const router = useRouter(); + const [protocolsInfo, setProtocolsInfo] = useState<{ hasInterviews: boolean; hasUnexportedInterviews: boolean; @@ -50,10 +54,10 @@ export const DeleteProtocolsDialog = ({ }, }); - const utils = api.useUtils(); const handleConfirm = async () => { await deleteProtocols(protocolsToDelete.map((d) => d.hash)); - await utils.protocol.get.all.refetch(); + await clientRevalidateTag('protocols.get.all'); + router.refresh(); setOpen(false); }; diff --git a/app/(interview)/interview/[interviewId]/page.tsx b/app/(interview)/interview/[interviewId]/page.tsx index 4abe54477..247854bba 100644 --- a/app/(interview)/interview/[interviewId]/page.tsx +++ b/app/(interview)/interview/[interviewId]/page.tsx @@ -6,7 +6,6 @@ import Link from 'next/link'; import { api } from '~/trpc/server'; import InterviewShell from '../_components/InterviewShell'; import NoSSRWrapper from '~/utils/NoSSRWrapper'; -import Test from '../_components/Test'; export const dynamic = 'force-dynamic'; @@ -38,7 +37,7 @@ export default async function Page({ protocol={interviewProtocol} > - + ); diff --git a/app/(interview)/interview/_components/InterviewShell.tsx b/app/(interview)/interview/_components/InterviewShell.tsx index f88bdc102..45165c97e 100644 --- a/app/(interview)/interview/_components/InterviewShell.tsx +++ b/app/(interview)/interview/_components/InterviewShell.tsx @@ -1,10 +1,8 @@ 'use client'; -import { motion } from 'framer-motion'; import { useRef } from 'react'; import { Provider } from 'react-redux'; import DialogManager from '~/lib/interviewer/components/DialogManager'; -import { SettingsMenu } from '~/lib/interviewer/components/SettingsMenu'; import ProtocolScreen from '~/lib/interviewer/containers/ProtocolScreen'; import configureAppStore from '~/lib/interviewer/store'; import { useInterview } from '~/providers/InterviewProvider'; @@ -18,13 +16,10 @@ const InterviewShell = () => { ); return ( - - - - - - - + + + + ); }; diff --git a/app/(interview)/interview/_components/Stage.tsx b/app/(interview)/interview/_components/Stage.tsx deleted file mode 100644 index bc34dabac..000000000 --- a/app/(interview)/interview/_components/Stage.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client'; - -import { useInterview } from '~/providers/InterviewProvider'; -import { - entityAttributesProperty, - entityPrimaryKeyProperty, -} from '@codaco/shared-consts'; -import { Button } from '~/components/ui/Button'; -import { v4 as uuid } from 'uuid'; - -const Stage = () => { - const { network, addNode, stageConfig } = useInterview(); - - /** - * This is where the we render stages from the existing app, using the stage config - * and the network context. - * - * We need to use the stageConfig.type to fetch the correct stage component and - * render it. - * - * For now this component just outputs the current stageConfig and the network, - * and shows an example of using the book to add a new node. - */ - - return ( - <> -
-
-          {JSON.stringify(stageConfig, null, 2)}
-        
-
-          {JSON.stringify(network, null, 2)}
-        
-
-
- -
- - ); -}; - -export default Stage; diff --git a/app/(interview)/interview/layout.tsx b/app/(interview)/interview/layout.tsx index bb61f3df8..eacaae16f 100644 --- a/app/(interview)/interview/layout.tsx +++ b/app/(interview)/interview/layout.tsx @@ -6,11 +6,7 @@ export const metadata = { }; function RootLayout({ children }: { children: React.ReactNode }) { - return ( -
- {children} -
- ); + return
{children}
; } export default RootLayout; diff --git a/lib/interviewer/components/Navigation.tsx b/lib/interviewer/components/Navigation.tsx new file mode 100644 index 000000000..b531ec27b --- /dev/null +++ b/lib/interviewer/components/Navigation.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import ProgressBar from '~/lib/ui/components/ProgressBar'; +import useReadyForNextStage from '~/lib/interviewer/hooks/useReadyForNextStage'; +import { ChevronDown, ChevronUp, SettingsIcon } from 'lucide-react'; +import { useInterview } from '~/providers/InterviewProvider'; +import { cn } from '~/utils/shadcn'; + +const NavigationButton = ({ + disabled, + onClick, + className, + children, +}: { + disabled?: boolean; + onClick?: React.MouseEventHandler; + className?: string; + children: React.ReactNode; +}) => { + return ( +
+ {children} +
+ ); +}; + +const Navigation = () => { + const [isReadyForNextStage] = useReadyForNextStage(); + const { progress, hasNextPage, hasPreviousPage, nextPage, previousPage } = + useInterview(); + + return ( +
+ + + + + + +
+ +
+ + + +
+ ); +}; + +export default Navigation; diff --git a/lib/interviewer/components/SessionPanel/SessionNavigation.js b/lib/interviewer/components/SessionPanel/SessionNavigation.js deleted file mode 100644 index 049da22c6..000000000 --- a/lib/interviewer/components/SessionPanel/SessionNavigation.js +++ /dev/null @@ -1,107 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; -import ProgressBar from '~/lib/ui/components/ProgressBar'; -import Icon from '~/lib/ui/components/Icon'; -import { motion } from 'framer-motion'; -import useReadyForNextStage from '~/lib/interviewer/hooks/useReadyForNextStage'; - -const SessionNavigation = ({ - onClickBack, - onClickNext, - percentProgress, - setExpanded, - setShowSubMenu, -}) => { - const [isReadyForNextStage] = useReadyForNextStage(); - - const variants = { - normal: { - opacity: 1, - transition: { - delay: 0.5, - duration: 0.25, - }, - }, - expanded: { - opacity: 0, - transition: { - duration: 0.05, - }, - }, - }; - - return ( - - { - setShowSubMenu(true); - setExpanded(true); - }} - > - - - { - if (e) { - e.stopPropagation(); - e.preventDefault(); - } - onClickBack(e); - }} - > - - - { setShowSubMenu(false); setExpanded(true); }} - > - - - { - if (e) { - e.stopPropagation(); - e.preventDefault(); - } - onClickNext(e); - }} - > - - - - ); -}; - -SessionNavigation.propTypes = { - onClickNext: PropTypes.func.isRequired, - onClickBack: PropTypes.func.isRequired, - percentProgress: PropTypes.number.isRequired, - setExpanded: PropTypes.func.isRequired, - setShowSubMenu: PropTypes.func.isRequired, -}; - -export default SessionNavigation; diff --git a/lib/interviewer/components/SessionPanel/SessionPanel.js b/lib/interviewer/components/SessionPanel/SessionPanel.js deleted file mode 100644 index f72875f46..000000000 --- a/lib/interviewer/components/SessionPanel/SessionPanel.js +++ /dev/null @@ -1,77 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { compose } from 'recompose'; -import { motion, AnimatePresence } from 'framer-motion'; -import { DropObstacle } from '../../behaviours/DragAndDrop'; -import StagesMenu from '../StagesMenu/StagesMenu'; -import SubMenu from './SubMenu'; -import BackgroundDimmer from '../BackgroundDimmer'; -import SessionNavigation from './SessionNavigation'; -import CloseButton from '../CloseButton'; - -const choiceVariants = { - show: { - opacity: 1, - translateY: '0%', - transition: { - type: 'spring', damping: 15, stiffness: 200, delay: 0.25, - }, - }, - hide: { opacity: 0, translateY: '100%', transition: { duration: 0.3 } }, -}; - -const SessionPanel = React.forwardRef((props, ref) => { - const [expanded, setExpanded] = useState(false); - const [showSubMenu, setShowSubMenu] = useState(false); - - const resetMenuState = () => { - setExpanded(false); - setShowSubMenu(false); - }; - - const menuContent = showSubMenu - ? - : ; - - return ( - <> - - {expanded && ( setExpanded(false)} className="close-button-wrapper" />)} - -
- - - {expanded ? menuContent : ( - - )} - - - - ); -}); - -SessionPanel.displayName = 'SessionPanel'; - -SessionPanel.propTypes = { - onStageSelect: PropTypes.func.isRequired, - onClickNext: PropTypes.func.isRequired, - onClickBack: PropTypes.func.isRequired, - percentProgress: PropTypes.number.isRequired, -}; - -export { SessionPanel }; - -export default compose( - DropObstacle, -)(SessionPanel); diff --git a/lib/interviewer/components/SessionPanel/SubMenu.js b/lib/interviewer/components/SessionPanel/SubMenu.js deleted file mode 100644 index 38421ece3..000000000 --- a/lib/interviewer/components/SessionPanel/SubMenu.js +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Icon from '~/lib/ui/components/Icon'; -import { connect } from 'react-redux'; -import { motion } from 'framer-motion'; -import { compose } from 'recompose'; -import { actionCreators as uiActions } from '../../ducks/modules/ui'; - -const SubMenu = (props) => { - const { - setShowSubMenu, - setExpanded, - openSettingsMenu, - endSession, - } = props; - - const variants = { - show: { - opacity: 1, - transition: { staggerChildren: 0.07, delayChildren: 0.2 }, - }, - hide: { - opacity: 0, - transition: { staggerChildren: 0.05, staggerDirection: -1 }, - }, - }; - - const itemVariants = { - show: { - y: 0, - opacity: 1, - transition: { - y: { stiffness: 1000, velocity: -100 }, - }, - }, - hide: { - y: '20%', - opacity: 0, - transition: { - y: { stiffness: 1000 }, - }, - }, - }; - - return ( - - -

Menu

-
- { openSettingsMenu(); setExpanded(false); }} - variants={itemVariants} - > - - Device Settings - - setShowSubMenu(false)} - variants={itemVariants} - > - - Interview Stages - -
- -
- - Exit Interview -
-
-
- -
- ); -}; - -const mapDispatchToProps = (dispatch) => ({ - endSession: () => console.log('end session lib/interviewer/components/SessionPanel/SubMenu.js'), - openSettingsMenu: () => dispatch(uiActions.update({ settingsMenuOpen: true })), -}); - -SubMenu.propTypes = { - setShowSubMenu: PropTypes.func.isRequired, - setExpanded: PropTypes.func.isRequired, - openSettingsMenu: PropTypes.func.isRequired, - endSession: PropTypes.func.isRequired, -}; - -export default compose( - connect(null, mapDispatchToProps), -)(SubMenu); diff --git a/lib/interviewer/components/SessionPanel/__tests__/SessionNavigation.test.js b/lib/interviewer/components/SessionPanel/__tests__/SessionNavigation.test.js deleted file mode 100644 index f363c1651..000000000 --- a/lib/interviewer/components/SessionPanel/__tests__/SessionNavigation.test.js +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-env jest */ -/* eslint-disable @codaco/spellcheck/spell-checker */ - -import React from 'react'; -import { mount } from 'enzyme'; -import * as framer from 'framer-motion'; -import { createStore } from 'redux'; -import { Provider } from 'react-redux'; -import SessionNavigation from '../SessionNavigation'; - -describe('Session Navigation Component', () => { - const showSubMenuMock = jest.fn(); - const setExpandedMock = jest.fn(); - const backMock = jest.fn(); - const nextMock = jest.fn(); - - framer.useInvertedScale = jest.fn(() => ({ scaleX: 1, scaleY: 1 })); - - let component = null; - - const mockStore = createStore(() => ({ - ui: { - FORM_IS_READY: false, - }, - })); - - beforeEach(() => { - component = mount(( - - - - )); - }); - - it('toggles menu on timeline click', () => { - expect(showSubMenuMock.mock.calls.length).toBe(0); - component.find('.progress-bar').simulate('click'); - expect(showSubMenuMock.mock.calls.length).toBe(1); - }); - - it('calls back function on clicking back button', () => { - expect(backMock.mock.calls.length).toBe(0); - component.find('div.session-navigation__button--back').simulate('click'); - expect(backMock.mock.calls.length).toBe(1); - }); - - it('calls next function on clicking next button', () => { - expect(nextMock.mock.calls.length).toBe(0); - component.find('div.session-navigation__button--next').simulate('click'); - expect(nextMock.mock.calls.length).toBe(1); - }); -}); diff --git a/lib/interviewer/components/SettingsMenu/Sections/VisualPreferences.js b/lib/interviewer/components/SettingsMenu/Sections/VisualPreferences.js deleted file mode 100644 index 249e245f6..000000000 --- a/lib/interviewer/components/SettingsMenu/Sections/VisualPreferences.js +++ /dev/null @@ -1,117 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { compose } from 'recompose'; -import { motion } from 'framer-motion'; -import Toggle from '~/lib/ui/components/Fields/Toggle'; -import { actionCreators as deviceSettingsActions } from '../../../ducks/modules/deviceSettings'; - -const VisualPreferences = (props) => { - const { - useDynamicScaling, - useFullScreenForms, - showGettingStarted, - toggleSetting, - setInterfaceScale, - interfaceScale, - } = props; - - return ( - <> - - toggleSetting('showGettingStarted'), - }} - /> -
-

Show "welcome" card?

-

- This card provides links to useful resources when you open the app for the first - time. -

-
-
- -
-
- -
-
-
-

Interface Scale

-

- This setting allows you to control the size of the Interviewer user - interface. Increasing the interface size may limit the amount of information - visible on each screen. -

-
-
- - toggleSetting('useDynamicScaling'), - }} - /> -
-

Use dynamic scaling?

-

- Dynamic scaling lets Interviewer resize the user interface proportionally to - the size of the window. Turning it off will use a fixed size. -

-
-
- - toggleSetting('useFullScreenForms'), - }} - /> -
-

Use fullscreen forms?

-

- The full screen node form is optimized for smaller devices, or devices with - no physical keyboard. -

-
-
- - ); -}; - -const mapDispatchToProps = (dispatch) => ({ - toggleSetting: (settingName) => dispatch(deviceSettingsActions.toggleSetting(settingName)), - setInterfaceScale: (scale) => dispatch(deviceSettingsActions.setInterfaceScale(scale)), -}); - -const mapStateToProps = (state) => ({ - useFullScreenForms: state.deviceSettings.useFullScreenForms, - useDynamicScaling: state.deviceSettings.useDynamicScaling, - startFullScreen: state.deviceSettings.startFullScreen, - showGettingStarted: state.deviceSettings.showGettingStarted, - interfaceScale: state.deviceSettings.interfaceScale, -}); - -export default compose( - connect(mapStateToProps, mapDispatchToProps), -)(VisualPreferences); diff --git a/lib/interviewer/components/SettingsMenu/SettingsMenu.js b/lib/interviewer/components/SettingsMenu/SettingsMenu.js deleted file mode 100644 index 3594d7e4f..000000000 --- a/lib/interviewer/components/SettingsMenu/SettingsMenu.js +++ /dev/null @@ -1,215 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { motion, AnimatePresence } from 'framer-motion'; -import { connect } from 'react-redux'; -import { compose } from 'recompose'; -import Scroller from '~/lib/ui/components/Scroller'; -import { getCSSVariableAsNumber, getCSSVariableAsString } from '~/lib/ui/utils/CSSVariables'; -import { actionCreators as uiActions } from '../../ducks/modules/ui'; -import VisualPreferences from './Sections/VisualPreferences'; -import CloseButton from '../CloseButton'; - -const SettingsMenu = (props) => { - const { - closeMenu, - settingsMenuOpen, - } = props; - - const getAnimationDuration = (variable) => getCSSVariableAsNumber(variable) / 1000; - const baseAnimationEasing = getCSSVariableAsString('--animation-easing-json'); - - const tabs = { - 'Visual Preferences': VisualPreferences, - }; - - const baseVariants = { - show: { - opacity: 1, - transition: { - when: 'beforeChildren', - staggerChildren: 0.1, - duration: getAnimationDuration('--animation-duration-very-fast-ms'), - easing: baseAnimationEasing, - }, - }, - hide: { - opacity: 0, - transition: { - when: 'afterChildren', - duration: getAnimationDuration('--animation-duration-very-fast-ms'), - easing: baseAnimationEasing, - }, - }, - }; - - const variants = { - show: { - x: '0%', - transition: { - when: 'beforeChildren', - staggerChildren: 0.1, - duration: getAnimationDuration('--animation-duration-standard-ms'), - easing: baseAnimationEasing, - }, - }, - hide: { - x: '-100%', - transition: { - when: 'afterChildren', - staggerChildren: 0.05, - staggerDirection: -1, - duration: getAnimationDuration('--animation-duration-standard-ms'), - easing: baseAnimationEasing, - }, - }, - }; - - const navVariants = { - show: { - y: '0%', - opacity: 1, - transition: { - duration: getAnimationDuration('--animation-duration-very-fast-ms'), - easing: baseAnimationEasing, - }, - }, - hide: { - y: '-10%', - opacity: 0, - transition: { - duration: getAnimationDuration('--animation-duration-very-fast-ms'), - easing: baseAnimationEasing, - }, - }, - }; - - const contentVariants = { - show: { - opacity: 1, - transition: { - when: 'beforeChildren', - duration: getAnimationDuration('--animation-duration-very-fast-ms'), - delay: getAnimationDuration('--animation-duration-standard-ms'), - easing: baseAnimationEasing, - }, - }, - hide: { - opacity: 0, - transition: { - when: 'afterChildren', - duration: getAnimationDuration('--animation-duration-standard-ms'), - easing: baseAnimationEasing, - }, - }, - }; - - const tabVariants = { - hidden: { - opacity: 0, - transition: { - staggerChildren: 0.1, - staggerDirection: -1, - duration: getAnimationDuration('--animation-duration-very-fast-ms'), - }, - }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.1, - delayChildren: 0.2, - duration: getAnimationDuration('--animation-duration-very-fast-ms'), - }, - }, - }; - - const [activeTab, setActiveTab] = useState('Visual Preferences'); - - const renderNavigation = Object.keys(tabs).map((tabName) => ( - setActiveTab(tabName)} - className={activeTab === tabName ? 'active' : ''} - variants={navVariants} - > - {tabName} - - )); - - const renderTabs = Object.keys(tabs).map((tabName) => { - const TabComponent = tabs[tabName]; - const isActive = activeTab === tabName; - if (!isActive) { return ''; } - return ( - - - - - - ); - }); - - return ( - - {settingsMenuOpen && ( - - - -

Settings

-
    - {renderNavigation} -
-
- - - {/* The presence animation is temporarily disabled because it breaks - the tests (possible bug) */} - {/* */} - {renderTabs} - {/* */} - -
-
- )} -
- - ); -}; - -const mapStateToProps = (state) => ({ - settingsMenuOpen: state.ui.settingsMenuOpen, -}); - -const mapDispatchToProps = (dispatch) => ({ - closeMenu: () => dispatch(uiActions.update({ settingsMenuOpen: false })), -}); - -SettingsMenu.propTypes = { - closeMenu: PropTypes.func.isRequired, - settingsMenuOpen: PropTypes.bool.isRequired, -}; - -export default compose( - connect(mapStateToProps, mapDispatchToProps), -)(SettingsMenu); diff --git a/lib/interviewer/components/SettingsMenu/SettingsMenuButton.js b/lib/interviewer/components/SettingsMenu/SettingsMenuButton.js deleted file mode 100644 index 01bec0ffb..000000000 --- a/lib/interviewer/components/SettingsMenu/SettingsMenuButton.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { motion } from 'framer-motion'; -import Icon from '~/lib/ui/components/Icon'; -import { connect } from 'react-redux'; -import { compose } from 'recompose'; -import { actionCreators as uiActions } from '../../ducks/modules/ui'; - -const SettingsMenuButton = ({ openSettingsMenu }) => ( - - -

Settings

-
-); - -const mapDispatchToProps = (dispatch) => ({ - openSettingsMenu: () => dispatch(uiActions.update({ settingsMenuOpen: true })), -}); - -export default compose( - connect(null, mapDispatchToProps), -)(SettingsMenuButton); diff --git a/lib/interviewer/components/SettingsMenu/index.js b/lib/interviewer/components/SettingsMenu/index.js deleted file mode 100644 index 12167f352..000000000 --- a/lib/interviewer/components/SettingsMenu/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as SettingsMenu } from './SettingsMenu'; -export { default as SettingsMenuButton } from './SettingsMenuButton'; diff --git a/lib/interviewer/components/StagesMenu/StagePreview.js b/lib/interviewer/components/StagesMenu/StagePreview.js deleted file mode 100644 index 5d644ce3c..000000000 --- a/lib/interviewer/components/StagesMenu/StagePreview.js +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; -import { withHandlers, compose } from 'recompose'; -import PropTypes from 'prop-types'; -import cx from 'classnames'; -import { motion } from 'framer-motion'; -import { getCSSVariableAsNumber } from '~/lib/ui/utils/CSSVariables'; -import Image from 'next/image'; - -const StagePreview = ({ - item: { - type, label, index, id, - }, - handleOpenStage, - active, -}) => { - const classes = cx('stage-preview', { - 'stage-preview--current': active, - }); - - const baseAnimationDuration = getCSSVariableAsNumber('--animation-duration-standard-ms') / 1000; - - const timelineVariants = { - expanded: { - y: 0, - opacity: 1, - height: '100%', - transition: { - duration: baseAnimationDuration / 3, - }, - }, - normal: { - y: '-100%', - opacity: 0, - height: 0, - transition: { - duration: baseAnimationDuration / 3, - }, - }, - }; - - return ( -
- -
- NameGenerator Interface -
-
- {index + 1} - . - {' '} - {label} -
-
- ); -}; - -const stagePreviewHandlers = withHandlers({ - handleOpenStage: (props) => () => { - const { - item: { index: stageIndex }, - onStageSelect, - setExpanded, - } = props; - - onStageSelect(stageIndex); - setExpanded(false); - }, -}); - -StagePreview.propTypes = { - item: PropTypes.object.isRequired, - active: PropTypes.bool.isRequired, - handleOpenStage: PropTypes.func.isRequired, -}; - -export default compose( - stagePreviewHandlers, -)(StagePreview); diff --git a/lib/interviewer/components/StagesMenu/StagesMenu.js b/lib/interviewer/components/StagesMenu/StagesMenu.js deleted file mode 100644 index 856493680..000000000 --- a/lib/interviewer/components/StagesMenu/StagesMenu.js +++ /dev/null @@ -1,224 +0,0 @@ -import React, { useState, useRef, useLayoutEffect } from 'react'; -import Search from '~/lib/ui/components/Fields/Search'; -import { connect } from 'react-redux'; -import { motion, AnimatePresence } from 'framer-motion'; -import { compose } from 'recompose'; -import Scroller from '~/lib/ui/components/Scroller'; -import { getCSSVariableAsNumber, getCSSVariableAsString } from '~/lib/ui/utils/CSSVariables'; -import { getProtocolStages } from '../../selectors/protocol'; -import StagePreview from './StagePreview'; -import { useInterview } from '~/providers/InterviewProvider'; - -const StagesMenu = (props) => { - const { - stages, - setExpanded, - onStageSelect, - } = props; - - const { currentStageIndex } = useInterview(); - - const [filter, setFilter] = useState(''); - - const scrollerRef = useRef(null); - - const baseAnimationDuration = getCSSVariableAsNumber('--animation-duration-standard-ms') / 1000; - const baseAnimationEasing = getCSSVariableAsString('--animation-easing-json'); - - const containerVariants = { - normal: { - opacity: 0, - transition: { - duration: baseAnimationDuration, - easing: baseAnimationEasing, - }, - }, - expanded: { - opacity: 1, - transition: { - when: 'beforeChildren', - duration: baseAnimationDuration, - easing: baseAnimationEasing, - }, - }, - }; - - const calculateDelay = (currentItemIndex) => { - /** - * Purpose of this function is to ensure we animate only the items - * that are currently visible. - * */ - - const delayScale = 0.1; - - // Active index 0, current index less than 8: animate first 8 items. - if ( - currentStageIndex === 0 - && currentItemIndex < 8 - ) { - return currentItemIndex * delayScale; - } - - // Active index non-zero: resequence index to 8 following current. - if ( - currentStageIndex > 0 - && currentItemIndex < (currentStageIndex + 8) - && (stages.length - currentStageIndex) > 8 - ) { - const resequencedIndex = currentItemIndex - currentStageIndex; - return resequencedIndex * delayScale; - } - - // Active index within 8 of end: resequence to last 8 - if ( - (stages.length - currentStageIndex) < 8 - && stages.length - currentItemIndex < 8 - ) { - const resequencedIndex = (stages.length - currentItemIndex - 8) * -1; - return resequencedIndex * delayScale; - } - - // Don't animate - return 0; - }; - - const itemVariants = { - expanded: (currentItemIndex) => ({ - x: 0, - opacity: 1, - transition: { - delay: calculateDelay(currentItemIndex) + 0.5, - duration: baseAnimationDuration, - easing: baseAnimationEasing, - }, - }), - normal: (currentItemIndex) => ({ - x: '-5rem', - opacity: 0, - transition: { - delay: calculateDelay(currentItemIndex), - duration: baseAnimationDuration, - easing: baseAnimationEasing, - }, - }), - filtered: () => ({ - opacity: 0, - transition: { - duration: baseAnimationDuration, - easing: baseAnimationEasing, - }, - }), - }; - - const scrollToLocation = (amount) => { - if (scrollerRef && scrollerRef.current) { - scrollerRef.current.scrollTo(0, amount); - } - }; - - const onFilterChange = (event) => setFilter(event.target.value || ''); - - const filteredStageList = stages.filter( - (stage) => stage.label.toLowerCase().includes(filter.toLowerCase()), - ); - - const renderMenuItems = filteredStageList.map((item, index) => { - const isActive = currentStageIndex === item.index; - - return ( - - - - ); - }); - - useLayoutEffect(() => { - // This effect is designed to scroll to the active stage - // - // This can't use a reference to a dom element because of the staggered - // animation (ref won't exist until the element has rendered). Instead, - // scroll the container to where the element will be when it finishes - // animating, based on the calculated height of each element * number - // of elements. - // - // NOTE: because element height changes after the image is loaded, and - // because the image does not load immediately, we need to tie this - // effect to the imageLoaded state, which is itself updated as images - // load. - // - // This is not at all graceful. Please implement a better way if you - // know of one! - - if (!filter) { - const itemHeight = document.getElementsByClassName('stages-menu__preview-wrapper')[0].clientHeight; - scrollToLocation(currentStageIndex * itemHeight, 0.2); - } - }, []); - - return ( - - - {renderMenuItems.length > 0 ? ( - - - {renderMenuItems} - - - ) : ( -

No stages match your filter.

- )} -
- - - -
- ); -}; - -function mapStateToProps(state) { - const currentStages = getProtocolStages(state); - const withIndex = currentStages.map((stage, index) => ({ ...stage, index })); - - return { - stages: withIndex, - }; -} - -export default compose( - connect(mapStateToProps, null), -)(StagesMenu); diff --git a/lib/interviewer/containers/HyperList/HyperList.js b/lib/interviewer/containers/HyperList/HyperList.js index bf1c457ed..b77e07628 100644 --- a/lib/interviewer/containers/HyperList/HyperList.js +++ b/lib/interviewer/containers/HyperList/HyperList.js @@ -160,7 +160,9 @@ const HyperList = ({ document.body.appendChild(newHiddenSizingEl); newHiddenSizingEl.innerHTML = simpleRenderToString(); - const height = newHiddenSizingEl.clientHeight; + // const height = newHiddenSizingEl.clientHeight; + + const height = 200; document.body.removeChild(newHiddenSizingEl); return height + GUTTER_SIZE; diff --git a/lib/interviewer/containers/ProtocolScreen.js b/lib/interviewer/containers/ProtocolScreen.js index a271975d0..2970569a9 100644 --- a/lib/interviewer/containers/ProtocolScreen.js +++ b/lib/interviewer/containers/ProtocolScreen.js @@ -1,13 +1,12 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; -import Timeline from '../components/SessionPanel/SessionPanel'; +import React from 'react'; import Stage from './Stage'; import { AnimatePresence, motion } from 'framer-motion'; import { useInterview } from '~/providers/InterviewProvider'; import { getProtocolStages } from '../selectors/protocol'; import { getSessionProgress } from '../selectors/session'; import { getSkipMap } from '../selectors/skip-logic'; -import { useDispatch, useSelector } from 'react-redux'; -import { actionCreators as dialogActions } from '../ducks/modules/dialogs'; +import { useSelector } from 'react-redux'; +import Navigation from '../components/Navigation'; const ProtocolScreen = () => { @@ -16,126 +15,32 @@ const ProtocolScreen = () => { previousPage, nextPage, goToPage, + registerBeforeNext, + onComplete, } = useInterview(); // "mapStateToProps" const protocolStages = useSelector(getProtocolStages); const stage = protocolStages[currentStageIndex] || {}; const { percentProgress, currentPrompt: promptId } = useSelector(getSessionProgress); - const skipMap = useSelector(getSkipMap); + const skipMap = useSelector(getSkipMap); // TODO: move this to useInterview const isSkipped = (index) => skipMap[index].isSkipped; - // "mapDispatchToProps" - const dispatch = useDispatch(); - const openDialog = (dialog) => dispatch(dialogActions.openDialog(dialog)); - - // Ref used to hold beforeNext functions for any stage that registered one - const beforeNext = useRef({}); - - // Called by a stage to force navigation after a beforeNext function - const onComplete = (_directionOverride) => { - // const { pendingStage, pendingDirection } = navigationState; - // const nextDirection = directionOverride || pendingDirection; - - // const navigate = (pendingStage === -1) - // ? () => goToNext(nextDirection) - // : () => goToPage(pendingStage); - // setNavigationState( - // { ...initialNavigationState }, - // navigate, - // ); - - debugger; - } - - // Stages call this to register a beforeNext function - const registerBeforeNext = (beforeNextFn, stageIndex) => { - if (beforeNextFn === null) { - delete beforeNext.current[stageIndex]; - return; - } - - beforeNext.current[stageIndex] = beforeNextFn; - }; - - const go = (direction) => { - const goFn = (direction === 'backwards') ? previousPage : nextPage; - - if (!beforeNext.current[currentStageIndex]) { - goFn(); - return; - } - - beforeNext.current[currentStageIndex](); - }; - - const handleClickBack = () => go('backwards'); - const handleClickNext = () => go('forwards'); - - const goToStage = (index) => { - if (isSkipped(index)) { - openDialog({ - type: 'Warning', - title: 'Show this stage?', - confirmLabel: 'Show Stage', - onConfirm: () => goToPage(index), - message: ( -

- Your skip logic settings would normally prevent this stage from being shown in this - interview. Do you want to show it anyway? -

- ), - }); - } else { - goToPage(index + 1); - } - } - - const handleStageSelect = (index) => { - if (!beforeNext.current[index]) { - goToStage(index); - return; - } - - beforeNext.current[index](); - } + console.log('progress', percentProgress); return ( - - + + - - - + stage={stage} + promptId={promptId} + stageIndex={currentStageIndex} + registerBeforeNext={registerBeforeNext} + onComplete={onComplete} + /> ) diff --git a/lib/interviewer/containers/Stage.js b/lib/interviewer/containers/Stage.js index 11cfdbd10..6fe3a825a 100644 --- a/lib/interviewer/containers/Stage.js +++ b/lib/interviewer/containers/Stage.js @@ -1,58 +1,55 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { useCallback, useLayoutEffect } from 'react'; import getInterface from './Interfaces'; import StageErrorBoundary from '../components/StageErrorBoundary'; - -/** - * Render a protocol interface based on protocol info and id - * @extends Component - */ -class Stage extends Component { - componentWillUnmount() { - const { registerBeforeNext, stage: { id } } = this.props; - registerBeforeNext(null, id); - } - - registerBeforeNext = (beforeNext) => { - const { registerBeforeNext, stage: { id } } = this.props; - registerBeforeNext( - beforeNext, - id, - ); - } - - render() { - const { stage, registerBeforeNext, ...props } = this.props; - - const CurrentInterface = getInterface(stage.type); - - return ( -
-
- - {CurrentInterface - && ( - - )} - -
-
- ); - } +import { motion } from 'framer-motion'; + + +const Stage = (props) => { + const { stage, registerBeforeNext: register, ...rest } = props; + + const registerBeforeNext = useCallback((beforeNext) => register( + beforeNext, + stage.id, + ), [register, stage.id]); + + useLayoutEffect(() => { + return () => { + register(null, stage.id) + } + }, [register, stage.id]) + + const CurrentInterface = getInterface(stage.type); + + return ( + + + {CurrentInterface + && ( + + )} + + + ); } -Stage.propTypes = { - stage: PropTypes.object.isRequired, - promptId: PropTypes.number, -}; - -Stage.defaultProps = { - promptId: 0, -}; - export default Stage; diff --git a/lib/interviewer/hooks/useExternalData.js b/lib/interviewer/hooks/useExternalData.js index 4091cba4e..20bf2157b 100644 --- a/lib/interviewer/hooks/useExternalData.js +++ b/lib/interviewer/hooks/useExternalData.js @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import { useSelector } from 'react-redux'; import objectHash from 'object-hash'; -import { mapValues, mapKeys } from 'lodash'; +import { mapKeys } from 'lodash'; import { entityAttributesProperty, entityPrimaryKeyProperty } from '@codaco/shared-consts'; import loadExternalData from '../utils/loadExternalData'; import getParentKeyByNameValue from '../utils/getParentKeyByNameValue'; @@ -62,14 +62,6 @@ const useExternalData = (dataSource, subject) => { protocolCodebook, } = useSelector(getSessionMeta); - console.log('useExternalData', { - dataSource, - subject, - protocolUID, - assetManifest, - protocolCodebook, - }); - const [externalData, setExternalData] = useState(null); const [status, setStatus] = useState({ isLoading: false, error: null }); const updateStatus = (newStatus) => setStatus((s) => ({ ...s, ...newStatus })); @@ -85,10 +77,6 @@ const useExternalData = (dataSource, subject) => { const variableUUIDReplacer = makeVariableUUIDReplacer(protocolCodebook, subject); loadExternalData(name, url) - .then((data) => { - console.log('result', data); - return data; - }) .then(({ nodes }) => Promise.all(nodes.map(variableUUIDReplacer))) .then((uuidData) => getVariableTypeReplacements( name, uuidData, protocolCodebook, subject, @@ -96,6 +84,7 @@ const useExternalData = (dataSource, subject) => { .then((formattedData) => setExternalData(formattedData)) .then(() => updateStatus({ isLoading: false })) .catch((e) => { + // eslint-disable-next-line no-console console.error(e); updateStatus({ isLoading: false, error: e }) }); diff --git a/lib/interviewer/selectors/session.js b/lib/interviewer/selectors/session.js index ff3d33133..e08051817 100644 --- a/lib/interviewer/selectors/session.js +++ b/lib/interviewer/selectors/session.js @@ -81,6 +81,7 @@ export const getSessionProgress = createSelector( const promptProgress = promptCount ? currentPrompt / promptCount : 0; // This can go over 100% when finish screen is not present, // so it needs to be clamped + const percentProgress = clamp( (stageProgress + (promptProgress / (screenCount - 1))) * 100, 0, diff --git a/lib/interviewer/store.ts b/lib/interviewer/store.ts index 631d1b2b0..96d0ce8d1 100644 --- a/lib/interviewer/store.ts +++ b/lib/interviewer/store.ts @@ -19,7 +19,6 @@ type InitialData = { }; export default function configureAppStore(initialData: InitialData) { - // debugger; const store = configureStore({ reducer: { form, diff --git a/lib/interviewer/styles/components/_all.scss b/lib/interviewer/styles/components/_all.scss index 47a56ae35..ea5eafa38 100644 --- a/lib/interviewer/styles/components/_all.scss +++ b/lib/interviewer/styles/components/_all.scss @@ -37,15 +37,6 @@ @import 'case-id-form'; @import 'stack-button'; @import 'simulation-panel'; - -// Session Panel -@import './session-panel/all'; - -// Settings Menu -@import './settings-menu/all'; - -// Stages Menu -@import './stages-menu/all'; @import 'categorical-list'; @import 'categorical-item'; @import 'overlay'; diff --git a/lib/interviewer/styles/components/_information-interface.scss b/lib/interviewer/styles/components/_information-interface.scss index c8f4098fe..163b731de 100644 --- a/lib/interviewer/styles/components/_information-interface.scss +++ b/lib/interviewer/styles/components/_information-interface.scss @@ -27,9 +27,7 @@ $asset-size-default: 17vh; } &__item { - // sass-lint:disable-block class-name-format margin-top: spacing('medium'); - height: $asset-size-small; overflow: hidden; video { @@ -59,23 +57,8 @@ $asset-size-default: 17vh; } &-text { - overflow-x: hidden; - overflow-y: auto; height: auto; - max-height: $asset-size-small; margin-top: 0; // Text paragraphs have their own margin - - &.information-interface__item--size-SMALL { - max-height: $asset-size-small; - } - - &.information-interface__item--size-MEDIUM { - max-height: $asset-size-medium; - } - - &.information-interface__item--size-LARGE { - max-height: $asset-size-large; - } } } } diff --git a/lib/interviewer/styles/components/_protocol.scss b/lib/interviewer/styles/components/_protocol.scss index 61db2d5e2..222d687ac 100644 --- a/lib/interviewer/styles/components/_protocol.scss +++ b/lib/interviewer/styles/components/_protocol.scss @@ -1,11 +1,3 @@ .protocol { display: flex; - width: 100vw; - height: 100%; - - &__content { - background-color: palette('background'); - flex: 1 1 auto; - height: 100%; - } } diff --git a/lib/interviewer/styles/components/_search.scss b/lib/interviewer/styles/components/_search.scss index 98cab0fff..80b592402 100644 --- a/lib/interviewer/styles/components/_search.scss +++ b/lib/interviewer/styles/components/_search.scss @@ -17,9 +17,9 @@ $results-height: calc(100vh - 23rem); &__loading { position: absolute; - width: calc(100% - var(--session-panel-width)) !important; // sass-lint:disable-line no-important + width: 100%; bottom: 0; - left: var(--session-panel-width); + left: 0; overflow: hidden; } diff --git a/lib/interviewer/styles/components/_stage.scss b/lib/interviewer/styles/components/_stage.scss index 2ba3fe131..414c51c20 100644 --- a/lib/interviewer/styles/components/_stage.scss +++ b/lib/interviewer/styles/components/_stage.scss @@ -1,12 +1,5 @@ .stage { - height: 100%; - width: 100vw; - - &__interface { - flex: 1 auto; - height: 100%; - position: relative; - } + flex: 1 auto; &--transition { @include group-transition-with-delay(fade, var(--animation-duration-slow), var(--animation-duration-slow)); diff --git a/lib/interviewer/styles/components/session-panel/_all.scss b/lib/interviewer/styles/components/session-panel/_all.scss deleted file mode 100644 index 16c545eb3..000000000 --- a/lib/interviewer/styles/components/session-panel/_all.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'session-panel'; -@import 'session-navigation'; -@import 'sub-menu'; diff --git a/lib/interviewer/styles/components/session-panel/_session-navigation.scss b/lib/interviewer/styles/components/session-panel/_session-navigation.scss deleted file mode 100644 index 8702f3859..000000000 --- a/lib/interviewer/styles/components/session-panel/_session-navigation.scss +++ /dev/null @@ -1,138 +0,0 @@ -$session-panel-button-size: 6rem; - -.session-navigation { - display: flex; - flex-direction: column; - flex: 1 0 auto; - justify-content: center; - align-content: center; - align-items: center; - height: 100%; - padding: units.unit(2); - width: var(--session-panel-width); - - @keyframes pulse { - // sass-lint:disable-block no-color-literals - 0% { - box-shadow: 0 0 0 0 rgb(var(--color-sea-green---rgb) 0.8); - } - - 70% { - box-shadow: 0 0 0 1rem rgb(var(--color-sea-green---rgb) 0); - } - - 100% { - box-shadow: 0 0 4rem 1rem rgb(var(--color-sea-green---rgb) 0); - } - } - - svg { - height: 2rem; - width: 2rem; - } - - &__progress-bar { - @include platform-not-ios { - &:hover { - cursor: pointer; - - .progress-bar { - animation: pulse 2s infinite; - } - } - } - - display: flex; - flex-grow: 1; - padding: 2rem; - border-radius: 5rem; - margin: 1rem 0; - transition: box-shadow 0.3s ease-in-out; - box-shadow: 0 0 0 1rem transparent; - - .progress-bar { - height: unset; - } - - } - - &__button { - align-items: center; - border-radius: 100%; - cursor: pointer; - display: flex; - justify-content: center; - height: $session-panel-button-size; - width: $session-panel-button-size; - transition: background var(--animation-duration-fast) var(--animation-easing); - - &--next { - background: var(--light-background); - - svg { - height: 2.5rem; - width: 2.5rem; - } - - &::after { - content: ''; - opacity: 0; - transition: transform var(--animation-duration-fast) var(--animation-easing), opacity var(--animation-duration-fast) var(--animation-easing); - display: block; - pointer-events: none; - position: absolute; - height: 9rem; - width: 9rem; - border-radius: 50%; - z-index: -1; - background: var(--primary); - } - - &:active { - background: var(--primary); - - &::after { - transform: scale(.5); - opacity: 1; - transition: 0s; - } - } - } - - &--back { - .cls-2 { - fill: var(--transparent-light); - } - - &::after { - content: ''; - opacity: 0; - transition: transform var(--animation-duration-fast) var(--animation-easing), opacity var(--animation-duration-fast) var(--animation-easing); - display: block; - pointer-events: none; - position: absolute; - height: $session-panel-button-size * 2; - width: $session-panel-button-size * 2; - border-radius: 50%; - z-index: -1; - background: var(--light-background); - } - - &:active { - background: var(--light-background); - - &::after { - transform: scale(.5); - opacity: 1; - transition: 0s; - - } - } - } - - &--nudge { - animation: pulse 2s infinite; - background: var(--primary); - } - } -} diff --git a/lib/interviewer/styles/components/session-panel/_session-panel.scss b/lib/interviewer/styles/components/session-panel/_session-panel.scss deleted file mode 100644 index 4df022284..000000000 --- a/lib/interviewer/styles/components/session-panel/_session-panel.scss +++ /dev/null @@ -1,105 +0,0 @@ -// This is needed because .session-panel can't have a width - it animates -// its width automatically based on content. Without a width, -// it cannot function as a drop obstacle, so we create this dummy -// element. -.session-panel-drop-obstacle { - position: absolute; - top: 0; - bottom: 0; - width: var(--session-panel-width); -} - -.session-panel { - --light-background: #4a4677; // sass-lint:disable-line no-color-literals - - align-items: flex-end; - display: flex; - flex-direction: column; - position: absolute; - top: 0; - bottom: 0; - flex: 1 0 auto; - height: 100%; - z-index: var(--z-global-ui); - background: var(--panel-bg-muted); -} - -.session-info-panel { - --base-node-size: 5rem; - - background: var(--color-platinum); - color: var(--color-navy-taupe); - position: absolute; - min-width: 30rem; - max-width: 30rem; - max-height: 40rem; - bottom: units.unit(4); - right: units.unit(4); - padding: units.unit(2) units.unit(4); - z-index: var(--z-global-ui); - border-radius: var(--border-radius); - display: flex; - - .scrollable { - padding: units.unit(1) 0; - } - - section { - padding: units.unit(2) 0; - border-bottom: 0.1rem solid var(--divider); - - &:last-of-type { - border: 0; - } - } - - .case-id { - svg { - height: 1.5rem; - width: 1.5rem; - margin-left: 1rem; - vertical-align: middle; - } - } - - h4 { - font-weight: 400; - font-size: 0.6rem; - letter-spacing: 0.15em; - text-transform: uppercase; - margin: 0; - } - - h2 { - margin-bottom: 0; - margin-top: 0.2rem; - display: flex; - align-items: center; - justify-content: space-between; - } - - .session-duration { - margin: 0; - font-size: 3.5rem; - } - - .entity-summary { - display: inline-flex; - align-items: center; - justify-content: center; - flex-direction: column; - width: 6rem; - text-align: center; - margin: units.unit(1) 0; - - svg { - margin-bottom: units.unit(1); - } - - h6 { - margin: 0; - } - - - } -} diff --git a/lib/interviewer/styles/components/session-panel/_sub-menu.scss b/lib/interviewer/styles/components/session-panel/_sub-menu.scss deleted file mode 100644 index 7092f6ae7..000000000 --- a/lib/interviewer/styles/components/session-panel/_sub-menu.scss +++ /dev/null @@ -1,59 +0,0 @@ -.sub-menu { - z-index: 999; - display: flex; - flex-direction: column; - height: 100%; - flex: 1 1 auto; - width: 22rem; - - &__wrapper { - display: flex; - overflow: hidden; - flex-direction: column; - height: 100%; - - h1 { - padding: units.unit(2) units.unit(4); - } - } - - &__items { - flex-grow: 1; - } - - .item { - padding: units.unit(4); - transition: background var(--animation-duration-fast) var(--animation-easing), font-weight var(--animation-duration-fast) var(--animation-easing); - cursor: pointer; - font-weight: 700; - font-size: 0.8rem; - letter-spacing: 0.15em; - text-transform: uppercase; - display: flex; - align-items: center; - - svg { - &.icon { - height: 1.5rem !important; // sass-lint:disable-line no-important - width: 1.5rem !important; // sass-lint:disable-line no-important - margin-right: 1rem; - } - } - - @include platform-not-ios { - &:hover { - background: var(--color-slate-blue--dark); - } - } - } - - &__items, - &__exit { - display: flex; - flex-direction: column; - } - - &__exit { - background: var(--color-neon-coral); - } -} diff --git a/lib/interviewer/styles/components/settings-menu/_all.scss b/lib/interviewer/styles/components/settings-menu/_all.scss deleted file mode 100644 index 7bb4b5e9b..000000000 --- a/lib/interviewer/styles/components/settings-menu/_all.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import 'select'; -@import 'settings-menu'; -@import 'settings-menu-button'; diff --git a/lib/interviewer/styles/components/settings-menu/_select.scss b/lib/interviewer/styles/components/settings-menu/_select.scss deleted file mode 100644 index a1592bfd8..000000000 --- a/lib/interviewer/styles/components/settings-menu/_select.scss +++ /dev/null @@ -1,34 +0,0 @@ -.select-css { - display: block; - font-size: var(--font-size); - font-family: var(--body-font-family); - padding: units.unit(1); - width: 100%; - max-width: 100%; - height: 3.5rem; - box-sizing: border-box; - margin: 0; - border: 0; - border-radius: var(--border-radius); - appearance: none; - background-color: var(--color-platinum); - background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%236D6F76%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); - background-repeat: no-repeat, repeat; - background-position: right .7em top 50%, 0 0; - background-size: .65em auto, 100%; - min-width: 6rem; - - &:hover { - border-color: var(--color-platinum--dark); - } - - &:focus { - border-color: var(--color-platinum--dark); - } - - & option { - font-weight: normal; - } -} - - diff --git a/lib/interviewer/styles/components/settings-menu/_settings-menu-button.scss b/lib/interviewer/styles/components/settings-menu/_settings-menu-button.scss deleted file mode 100644 index 1d548da19..000000000 --- a/lib/interviewer/styles/components/settings-menu/_settings-menu-button.scss +++ /dev/null @@ -1,21 +0,0 @@ -.settings-menu-button { - display: flex; - align-items: center; - cursor: pointer; - - .icon { - &[name='settings'] { - height: units.unit(4); - width: units.unit(4); - } - } - - h4 { - font-weight: 700; - font-size: 0.8rem; - letter-spacing: 0.15em; - text-transform: uppercase; - margin: 0; - padding-left: units.unit(2); - } -} diff --git a/lib/interviewer/styles/components/settings-menu/_settings-menu.scss b/lib/interviewer/styles/components/settings-menu/_settings-menu.scss deleted file mode 100644 index fda9c1a6f..000000000 --- a/lib/interviewer/styles/components/settings-menu/_settings-menu.scss +++ /dev/null @@ -1,129 +0,0 @@ -.settings-menu { - display: flex; - flex-flow: column; - width: 100%; - height: 100%; - position: absolute; - z-index: var(--z-modal); - - &__wrapper { - display: flex; - height: 100%; - overflow: hidden; - background: var(--modal-overlay); - - nav { - background: var(--panel-bg-muted); - flex: 0 0 22rem; // Match to sub-menu component width - display: flex; - flex-direction: column; - overflow: hidden; - position: relative; - z-index: var(--z-modal); - - h1 { - padding: units.unit(2) units.unit(4); - } - - ul { - padding: 0; - } - - li { - @include platform-not-ios { - &:hover { - &:not(.active) { - background: var(--color-slate-blue--dark); - } - } - } - - padding: units.unit(4); - list-style: none; - transition: background var(--animation-duration-fast) var(--animation-easing), font-weight var(--animation-duration-fast) var(--animation-easing); - cursor: pointer; - backface-visibility: hidden; - font-weight: 700; - font-size: 0.8rem; - letter-spacing: 0.15em; - text-transform: uppercase; - - &.active { - background: var(--color-slate-blue); - font-weight: 700; - } - } - - } - - section { - width: 1000%; - display: flex; - background: var(--color-cyber-grape); - - .close-button-wrapper { - position: absolute; - top: units.unit(4); - right: units.unit(4); - z-index: var(--z-modal); - } - - .tab-content { - display: flex; - flex-direction: column; - - .scrollable { - padding-top: 2rem; - padding-bottom: 4rem; - } - } - - .settings-element { - display: flex; - margin: 0 units.unit(8); - - .form-field { - button { - width: 12rem; - } - } - - >.form-field-container { - .form-field-toggle { - margin-left: 2rem; - } - - .form-field { - padding: 0 2rem 0 0; - background: transparent; - width: auto; - } - } - - &--wide { - display: flex; - flex-direction: column; - margin: 0 units.unit(8); - } - - &--sub-item { - margin: 0 units.unit(8); - - .form-field-container { - margin: 0; - display: flex; - justify-content: flex-end; - align-items: flex-start; - flex-direction: column; - - .form-field-text { - width: 100%; - } - } - - } - - } - } - } -} diff --git a/lib/interviewer/styles/components/stages-menu/_all.scss b/lib/interviewer/styles/components/stages-menu/_all.scss deleted file mode 100644 index c6ad33efc..000000000 --- a/lib/interviewer/styles/components/stages-menu/_all.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import 'stages-menu'; -@import 'stage-preview'; diff --git a/lib/interviewer/styles/components/stages-menu/_stage-preview.scss b/lib/interviewer/styles/components/stages-menu/_stage-preview.scss deleted file mode 100644 index c7b8843bb..000000000 --- a/lib/interviewer/styles/components/stages-menu/_stage-preview.scss +++ /dev/null @@ -1,123 +0,0 @@ -:root { - --timeline-color: var(--color-neon-coral); -} - -.stage-preview { - @include platform-not-ios { - &:hover { - &:not(.stage-preview--current) { - cursor: pointer; - background: var(--color-slate-blue); - } - } - } - - display: flex; - align-items: center; - padding: 0 units.unit(4); - height: 8rem; - transition: background var(--animation-easing) var(--animation-duration-fast); - - &--current { - background: var(--primary); - } - - &__image { - flex: 0 0 8rem; - margin-right: units.unit(2); - - img { - max-width: 100%; - border-radius: var(--border-radius); - padding: 0.5rem; - background: var(--background); - } - } - - &__label { - display: flex; - align-items: center; - font-weight: 700; - font-size: 0.8rem; - letter-spacing: 0.15em; - text-transform: uppercase; - } -} - -.dimmer { - position: absolute; - inset: 0; - background: var(--modal-overlay); - z-index: var(--z-background); - padding: units.unit(4); - text-align: right; - - .close-button-wrapper { - position: absolute; - top: units.unit(4); - right: units.unit(4); - } -} - -.stages-menu__preview-wrapper { - display: flex; - - .stage-preview__notch { - height: 100%; - flex: 0 0 1.5rem; - position: relative; - margin-right: units.unit(2); - overflow: hidden; - - - &::before { - content: ''; - height: 100%; - display: block; - left: 50%; - position: absolute; - border-left: var(--timeline-color) .2rem solid; - transition: height var(--animation-easing) var(--animation-duration-fast); - transform: translateX(-.1rem); - } - - &::after { - content: ''; - background: var(--timeline-color); - height: 1rem; - width: 1rem; - border-radius: 50%; - display: block; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-.5rem, -.5rem); - } - - } - - &:first-child { - .stage-preview__notch { - &::before { - height: 50%; - top: 50%; - } - } - } - - &:last-child { - .stage-preview__notch { - &::before { - height: 50%; - } - } - } - - &:only-child { - .stage-preview__notch { - &::before { - display: none; - } - } - } -} diff --git a/lib/interviewer/styles/components/stages-menu/_stages-menu.scss b/lib/interviewer/styles/components/stages-menu/_stages-menu.scss deleted file mode 100644 index b9f45aae5..000000000 --- a/lib/interviewer/styles/components/stages-menu/_stages-menu.scss +++ /dev/null @@ -1,38 +0,0 @@ -.stages-menu { - display: flex; - flex-direction: column; - height: 100%; - flex: 1 1 auto; - overflow: hidden; - width: 40rem; - - &__wrapper { - display: flex; - overflow: hidden; - flex-direction: column; - height: 100%; - justify-content: center; - align-items: center; - - .scrollable { - padding: units.unit(4) 0; - } - - } - - footer { - display: flex; - padding: units.unit(2) units.unit(4); - flex-direction: column; - flex: 0 0 auto; - background: var(--light-background); - - .form-field-container { - margin: 0; - - .form-field-text__input { - margin: 0; - } - } - } -} diff --git a/lib/interviewer/styles/containers/_ego-form-interface.scss b/lib/interviewer/styles/containers/_ego-form-interface.scss index bbc30e04b..7724895cd 100644 --- a/lib/interviewer/styles/containers/_ego-form-interface.scss +++ b/lib/interviewer/styles/containers/_ego-form-interface.scss @@ -1,5 +1,5 @@ .ego-form { - @include interface-centering(false); + @include interface-centering; padding: 0; height: 100%; diff --git a/lib/interviewer/styles/containers/_interfaces.scss b/lib/interviewer/styles/containers/_interfaces.scss index 313110b0d..696fb3447 100644 --- a/lib/interviewer/styles/containers/_interfaces.scss +++ b/lib/interviewer/styles/containers/_interfaces.scss @@ -1,30 +1,18 @@ @use '~~/lib/ui/styles/global/core/units'; @mixin interface-prompt { - // flex: 0 0 var(--interface-prompt-flex-basis); flex: 0 0 auto; display: flex; align-items: center; justify-content: center; } -@mixin interface-centering($margins: true) { - padding: units.unit(2) 0; - - /* stylelint-disable-next-line length-zero-no-unit */ - $margin: if($margins, units.unit(4), 0px); - - margin-left: calc(#{$margin} + var(--session-panel-width)); - margin-right: $margin; - position: relative; // so that absolutely positioned child elements are correctly centered -} - // Trying this with padding instead of margins, so that absolutely // positioned child elements aren't clipped by overflow hidden. -@mixin interface-centering-padding { +@mixin interface-centering { display: flex; height: 100%; overflow: hidden; - padding: units.unit(2) units.unit(4) units.unit(2) calc(#{units.unit(4)} + var(--session-panel-width)); + padding: units.unit(2) units.unit(4); position: relative; } diff --git a/lib/interviewer/styles/containers/_name-generator-auto-complete-interface.scss b/lib/interviewer/styles/containers/_name-generator-auto-complete-interface.scss index f58f96221..f79462555 100644 --- a/lib/interviewer/styles/containers/_name-generator-auto-complete-interface.scss +++ b/lib/interviewer/styles/containers/_name-generator-auto-complete-interface.scss @@ -17,12 +17,12 @@ $search-z-index: var(--z-global-ui); &__search { .search { --right-collapsed-js: 3rem; // align with 'core' of button - --right-offset: var(--session-panel-width); // margin equal to left menu width + --right-offset: 0; // Position collapsed search element around the 'core' of the icon bottom: $search-icon-offset-y; right: var(--right-offset); - width: calc(100% - calc(var(--right-offset) * 1.2) - var(--session-panel-width)); + width: calc(100% - calc(var(--right-offset) * 1.2)); z-index: $search-z-index; } } diff --git a/lib/interviewer/styles/containers/_name-generator-interface.scss b/lib/interviewer/styles/containers/_name-generator-interface.scss index 8bc780d54..00a10916c 100644 --- a/lib/interviewer/styles/containers/_name-generator-interface.scss +++ b/lib/interviewer/styles/containers/_name-generator-interface.scss @@ -1,7 +1,7 @@ @use '~~/lib/ui/styles/global/core/units'; .name-generator-interface { - @include interface-centering-padding; + @include interface-centering; flex-direction: column; diff --git a/lib/interviewer/styles/containers/_narrative-interface.scss b/lib/interviewer/styles/containers/_narrative-interface.scss index 4eeeb065d..c7119fe9f 100644 --- a/lib/interviewer/styles/containers/_narrative-interface.scss +++ b/lib/interviewer/styles/containers/_narrative-interface.scss @@ -1,5 +1,5 @@ .narrative-interface { - @include interface-centering(false); + @include interface-centering; height: 100%; diff --git a/lib/interviewer/styles/containers/_sociogram-interface.scss b/lib/interviewer/styles/containers/_sociogram-interface.scss index f368da896..054f74705 100644 --- a/lib/interviewer/styles/containers/_sociogram-interface.scss +++ b/lib/interviewer/styles/containers/_sociogram-interface.scss @@ -1,5 +1,5 @@ .sociogram-interface { - @include interface-centering(false); + @include interface-centering; align-items: center; display: flex; diff --git a/lib/interviewer/styles/main.scss b/lib/interviewer/styles/main.scss index 58c012382..01f22e50d 100644 --- a/lib/interviewer/styles/main.scss +++ b/lib/interviewer/styles/main.scss @@ -21,9 +21,8 @@ $image-path: '~~/lib/ui/assets/images'; :root { --body-font-family: #{$netcanvas-font-stack}; // override Network Canvas UI --code-font: courier; - --session-panel-width: 9rem; // Does *not* change actual timeline width (which is automatically calculated). Used for drop obstacle and stage margins --interface-prompt-flex-basis: 8rem; - --base-font-size: 1.75vmin; + --base-font-size: 18px; --border-radius: .75rem; --padding-unit: 1rem; --form-intro-panel-background: var(--color-cyber-grape); @@ -32,16 +31,8 @@ $image-path: '~~/lib/ui/assets/images'; html, body { cursor: default; - overflow: hidden; color: palette('text'); background: palette('background'); - width: 100%; - height: 100vh; -} - -a { - color: palette('link'); - text-decoration: none; } // Override UI toast style for now (make wider) diff --git a/lib/ui/components/Boolean/BooleanOption.js b/lib/ui/components/Boolean/BooleanOption.js index 17596bb5e..0785bc357 100644 --- a/lib/ui/components/Boolean/BooleanOption.js +++ b/lib/ui/components/Boolean/BooleanOption.js @@ -6,8 +6,8 @@ import RoundCheckbox from './RoundCheckbox'; import Markdown from '../Fields/Markdown'; const BooleanOption = ({ - classes, - selected, + classes = '', + selected = false, label, onClick, customIcon, @@ -34,7 +34,7 @@ const BooleanOption = ({ return (
{resizeListener} - { customIcon || } + {customIcon || } {renderLabel()}
); @@ -55,7 +55,7 @@ BooleanOption.propTypes = { BooleanOption.defaultProps = { classes: null, selected: false, - onClick: () => {}, + onClick: () => { }, customIcon: null, negative: false, }; diff --git a/lib/ui/components/Boolean/RoundCheckbox.js b/lib/ui/components/Boolean/RoundCheckbox.js index f2c6f78fb..ece357678 100644 --- a/lib/ui/components/Boolean/RoundCheckbox.js +++ b/lib/ui/components/Boolean/RoundCheckbox.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import cx from 'classnames'; import Icon from '../Icon'; -const RoundCheckbox = ({ checked, negative }) => { +const RoundCheckbox = ({ checked = false, negative = false }) => { const classes = cx( 'round-checkbox', { 'round-checkbox--checked': checked }, @@ -22,9 +22,4 @@ RoundCheckbox.propTypes = { negative: PropTypes.bool, }; -RoundCheckbox.defaultProps = { - checked: false, - negative: false, -}; - export default RoundCheckbox; diff --git a/lib/ui/components/Cards/DataCard.js b/lib/ui/components/Cards/DataCard.js index 7cf832212..4cf17a964 100644 --- a/lib/ui/components/Cards/DataCard.js +++ b/lib/ui/components/Cards/DataCard.js @@ -5,9 +5,9 @@ import { noop } from 'lodash'; const DataCard = ({ label, - data, - onClick, - allowDrag, + data = {}, + onClick = noop, + allowDrag = false, }) => { const classes = cx( 'data-card', @@ -25,7 +25,7 @@ const DataCard = ({

{label}

- { data && Object.keys(data).length > 0 && ( + {data && Object.keys(data).length > 0 && (
{Object.keys(data).map((dataLabel) => (
@@ -39,11 +39,7 @@ const DataCard = ({ ); }; -DataCard.defaultProps = { - data: {}, - onClick: noop, - allowDrag: false, -}; + DataCard.propTypes = { data: PropTypes.object, diff --git a/providers/InterviewProvider.tsx b/providers/InterviewProvider.tsx index 4d65dd5f9..e78554222 100644 --- a/providers/InterviewProvider.tsx +++ b/providers/InterviewProvider.tsx @@ -75,7 +75,9 @@ function InterviewProvider({ 'stage', parseAsInteger.withDefault(1), ); + const [initialized, setInitialized] = useState(false); + const { network, networkHandlers } = useNetwork(initialNetwork); const { mutate: updateNetwork } = api.interview.updateNetwork.useMutation({ onError: (error) => { @@ -135,6 +137,18 @@ function InterviewProvider({ ); } +const registerBeforeNext = () => { + // eslint-disable-next-line no-console + console.log('NOT IMMPLEMENTED: registerBeforeNext'); + return; +}; + +const onComplete = () => { + // eslint-disable-next-line no-console + console.log('NOT IMMPLEMENTED: onComplete'); + return; +}; + const useInterview = () => { const { protocol, @@ -154,6 +168,8 @@ const useInterview = () => { progress, ...networkHandlers, ...navigationHandlers, + registerBeforeNext, + onComplete, }; }; diff --git a/tailwind.config.ts b/tailwind.config.ts index 26342560a..fbfcf6d1b 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -1,7 +1,11 @@ import { type Config } from 'tailwindcss'; export default { - content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'], + content: [ + './app/**/*.{js,ts,jsx,tsx}', + './lib/interviewer/**/*.{js,ts,jsx,tsx}', + './components/**/*.{js,ts,jsx,tsx}', + ], theme: { container: { center: true, diff --git a/utils/simpleRenderToString.ts b/utils/simpleRenderToString.ts index f96b04618..90d66240e 100644 --- a/utils/simpleRenderToString.ts +++ b/utils/simpleRenderToString.ts @@ -1,14 +1,16 @@ import type { ReactElement } from 'react'; import { createRoot } from 'react-dom/client'; -export async function simpleRenderToString(element: ReactElement) { +export function simpleRenderToString(element: ReactElement) { const container = document.createElement('div'); const root = createRoot(container); root.render(element); - return new Promise((resolve) => { - setTimeout(() => { - resolve(container.innerHTML); - }, 1); - }); + return container.innerHTML; + + // return new Promise((resolve) => { + // setTimeout(() => { + // resolve(container.innerHTML); + // }, 0); + // }); }