Skip to content

Commit

Permalink
feat(nps): update UI (#1370)
Browse files Browse the repository at this point in the history
* feat(feedbak): add initial new modal

* feat(feedbackmodal): shift ui

* feat(rating): add new componentn

* refactor(feedbackmodal): update to use new component

* chore(feedbackmodal): update ui

* feat(types): add types

* feat(logincontext): add usertype

* feat(feedbackmodal): submit to be

* docs(usefeedbackdisclosure): update doc
  • Loading branch information
seaerchin authored Aug 3, 2023
1 parent 0d0bcea commit bf341f8
Show file tree
Hide file tree
Showing 10 changed files with 254 additions and 52 deletions.
124 changes: 74 additions & 50 deletions src/components/FeedbackModal/FeedbackModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,25 @@ import {
ModalContent,
ModalHeader,
ModalBody,
Heading,
HStack,
ModalFooter,
VStack,
ButtonGroup,
FormControl,
} from "@chakra-ui/react"
import { ModalCloseButton } from "@opengovsg/design-system-react"

const FeedbackForm = () => {
return (
<>
<div
style={{
fontFamily: "sans-serif",
fontSize: "15px",
color: "#000",
opacity: "0.9",
paddingTop: "5px",
paddingBottom: "8px",
}}
>
If the form below is not loaded, you can also fill it in{" "}
<a href="https://form.gov.sg/64b7a11823e54700118bad90">here</a>.
</div>
import {
ModalCloseButton,
Button,
Textarea,
FormLabel,
} from "@opengovsg/design-system-react"
import { Rating } from "components/Rating/Rating"
import { useState } from "react"
import { useForm } from "react-hook-form"

<iframe
title="FormSG Feedback form for Isomer"
id="iframe"
src="https://form.gov.sg/64b7a11823e54700118bad90"
width="100%"
height="650px"
/>
import { useLoginContext } from "contexts/LoginContext"

<div
style={{
fontFamily: "sans-serif",
fontSize: "12px",
color: "#999",
opacity: "0.5",
paddingTop: "5px",
}}
>
Powered by{" "}
<a href="https://form.gov.sg" color="#999">
Form
</a>
</div>
</>
)
}
import { useSubmitFeedback } from "hooks/useSubmitFeedback"

export interface FeedbackModalProps {
isOpen: boolean
Expand All @@ -59,17 +32,68 @@ export const FeedbackModal = ({
isOpen,
onClose,
}: FeedbackModalProps): JSX.Element => {
const [shouldShowTextArea, setShouldShowTextArea] = useState(false)
const [activeIdx, setActiveIdx] = useState<number>(-1)
const { register, handleSubmit } = useForm<{ feedback: string }>()
const { userType, email } = useLoginContext()
const { mutateAsync: submitFeedback, isLoading } = useSubmitFeedback()

return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Heading as="h4">Help make Isomer better!</Heading>
</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
<FeedbackForm />
</ModalBody>
<form
onSubmit={handleSubmit(async ({ feedback }) => {
submitFeedback({ feedback, email, userType, rating: activeIdx })
onClose()
})}
>
<ModalHeader>How satisfied are you with Isomer?</ModalHeader>
<ModalCloseButton />
<ModalBody pb={shouldShowTextArea ? "auto" : "1.5rem"}>
<VStack spacing="1.5rem" align="flex-start">
<Rating
onClick={(idx) => {
setShouldShowTextArea(true)
setActiveIdx(idx)
}}
activeIdx={activeIdx}
/>
{shouldShowTextArea && (
<FormControl>
<FormLabel>Share why you gave us this rating</FormLabel>
<Textarea
mt="0.25rem"
placeholder="Leave us some feedback"
{...register("feedback")}
/>
</FormControl>
)}
</VStack>
</ModalBody>
{shouldShowTextArea && (
<ModalFooter>
<HStack spacing="1rem">
<ButtonGroup>
<Button
colorScheme="neutral"
variant="clear"
onClick={onClose}
>
Cancel
</Button>
<Button
colorScheme="main"
type="submit"
isLoading={isLoading}
>
Done
</Button>
</ButtonGroup>
</HStack>
</ModalFooter>
)}
</form>
</ModalContent>
</Modal>
)
Expand Down
78 changes: 78 additions & 0 deletions src/components/Rating/Rating.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
Button as ChakraButton,
useMultiStyleConfig,
chakra,
Text,
Flex,
Box,
Spacer,
} from "@chakra-ui/react"
import { PropsWithChildren } from "react"

const RATING_SCALE = 11

interface RatingButtonProps {
onClick: () => void
isActive: boolean
}
const RatingButton = ({
onClick,
isActive,
children,
}: PropsWithChildren<RatingButtonProps>) => {
const styles = useMultiStyleConfig("Rating")
const paginationStyles = useMultiStyleConfig("Pagination")

return (
<>
<ChakraButton
variant="unstyled"
sx={{
// NOTE: We need to use the pagination styles
// because the figma uses the pagination button for rating...
...paginationStyles.button,
...styles.button,
}}
onClick={onClick}
isActive={isActive}
>
{children}
</ChakraButton>
</>
)
}

export interface RatingProps {
onClick: (idx: number) => void
activeIdx: number
}

export const Rating = ({ onClick, activeIdx }: RatingProps) => {
const styles = useMultiStyleConfig("Rating")

return (
<Box>
<chakra.ul __css={styles.container}>
{Array(RATING_SCALE)
.fill(null)
.map((_, idx) => {
return (
<chakra.li>
<RatingButton
onClick={() => onClick(idx)}
isActive={activeIdx === idx}
>
{idx}
</RatingButton>
</chakra.li>
)
})}
</chakra.ul>
<Flex w="full">
<Text textStyle="caption-1">Not satisfied</Text>
<Spacer />
<Text textStyle="caption-1">Very satisfied</Text>
</Flex>
</Box>
)
}
4 changes: 3 additions & 1 deletion src/contexts/LoginContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import { LOCAL_STORAGE_KEYS } from "constants/localStorage"

import { useLocalStorage } from "hooks/useLocalStorage"

import { LoggedInUser } from "types/user"
import { LoggedInUser, UserType, UserTypes } from "types/user"

const { REACT_APP_BACKEND_URL_V2: BACKEND_URL } = process.env

interface LoginContextProps extends LoggedInUser {
isLoading: boolean
logout: () => Promise<void>
verifyLoginAndGetUserDetails: () => Promise<void>
userType: UserType
}

const LoginContext = createContext<null | LoginContextProps>(null)
Expand Down Expand Up @@ -92,6 +93,7 @@ const LoginProvider = ({
displayedName: `${storedUserId ? "@" : ""}${
storedUserId || storedUserEmail
}`,
userType: storedUserId ? UserTypes.Github : UserTypes.Email,
}

return (
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useFeedbackDisclosure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export const useFeedbackDisclosure = (): UseFeedbackDisclosureReturn => {

const [lastSeen, setLastSeen] = useFeedbackStorage()
// NOTE: Either this is the first time the user has ever seen the survey
// or that the user has seen the survey but it has been more than 3 weeks.
// or that the user has seen the survey but it has been more a week (greater than duration of survey).
// Because we toggle the survey on every month, this indicates that they should
// do the survey if the toggle is on + sufficient time has elapsed
const isSurveyRequired =
Expand Down
28 changes: 28 additions & 0 deletions src/hooks/useSubmitFeedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { AxiosError } from "axios"
import { useMutation, UseMutationResult } from "react-query"

import * as FeedbackService from "services/FeedbackService"

import { FeedbackDto } from "types/feedback"
import { useSuccessToast } from "utils"

export const useSubmitFeedback = (): UseMutationResult<
void,
AxiosError,
FeedbackDto
> => {
const successToast = useSuccessToast()
return useMutation<void, AxiosError, FeedbackDto>(
async (userFeedback) => {
FeedbackService.submitFeedback(userFeedback)
},
{
onSuccess: () => {
successToast({
id: "submit-feedback-success",
description: "Thanks for your feedback!",
})
},
}
)
}
8 changes: 8 additions & 0 deletions src/services/FeedbackService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { FeedbackDto } from "types/feedback"

import { apiService } from "./ApiService"

export const submitFeedback = (userFeedback: FeedbackDto) => {
const endpoint = "/metrics/feedback"
return apiService.post(endpoint, userFeedback)
}
45 changes: 45 additions & 0 deletions src/theme/components/Rating.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createMultiStyleConfigHelpers } from "@chakra-ui/react"
import { anatomy } from "@chakra-ui/theme-tools"

import { textStyles } from "theme/textStyles"

const parts = anatomy("rating").parts("button", "container")

const {
definePartsStyle,
defineMultiStyleConfig,
} = createMultiStyleConfigHelpers(parts.keys)

const baseStyle = definePartsStyle({
container: {
display: "flex",
flexFlow: "row nowrap",
listStyleType: "none",
alignItems: "flex-start",
gap: "1.5rem",
w: "full",
alignSelf: "center",
px: "1.16rem",
py: "0.5rem",
},
button: {
...textStyles["subhead-2"],
minH: "auto",
minW: "auto",
_active: {
bg: "interaction.support.selected",
color: "base.content.inverse",
_hover: {
bg: "interaction.support.selected",
},
_disabled: {
bg: "interaction.support.disabled",
color: "interaction.support.disabled-content",
},
},
},
})

export const Rating = defineMultiStyleConfig({
baseStyle,
})
2 changes: 2 additions & 0 deletions src/theme/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { Breadcrumb } from "./Breadcrumb"
import { Card, CARD_THEME_KEY } from "./Card"
import { DISPLAY_CARD_THEME_KEY, DisplayCard } from "./DisplayCard"
import { Infobox } from "./Infobox"
import { Rating } from "./Rating"

// eslint-disable-next-line import/prefer-default-export
export const components = {
[CARD_THEME_KEY]: Card,
[DISPLAY_CARD_THEME_KEY]: DisplayCard,
Breadcrumb,
Infobox,
Rating,
}
8 changes: 8 additions & 0 deletions src/types/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { UserType } from "./user"

export interface FeedbackDto {
rating: number
feedback: string
email: string
userType: UserType
}
7 changes: 7 additions & 0 deletions src/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@ export interface LoggedInUser {
contactNumber: string
displayedName: string
}

export const UserTypes = {
Email: "email",
Github: "github",
} as const

export type UserType = typeof UserTypes[keyof typeof UserTypes]

0 comments on commit bf341f8

Please sign in to comment.