diff --git a/app/components/Article/FeedbackForm/feedbackForm.css b/app/components/Article/FeedbackForm/feedbackForm.css new file mode 100644 index 00000000..7e730da2 --- /dev/null +++ b/app/components/Article/FeedbackForm/feedbackForm.css @@ -0,0 +1,60 @@ +.feedback-container { + padding: var(--spacing-32); +} +.select-option { + height: var(--spacing-48); + padding: var(--spacing-8) var(--spacing-16); + margin: var(--spacing-8) 0; + cursor: pointer; + width: 100%; +} +.select-option.selected { + border: 1px solid var(--colors-teal-500); +} + +.feedback-form { + width: 100%; + position: relative; + color: var(--colors-cool-grey-600); + z-index: 2; +} + +.feedback-text { + width: calc(100% - var(--spacing-24)); + max-width: calc(100% - var(--spacing-24)); + height: 128px; + padding: var(--spacing-12); + margin: var(--spacing-40) 0; + box-sizing: content-box; +} +.feedback-text.no-options { + margin: var(--spacing-16) 0; +} +.submit-feedback.enabled { + cursor: pointer; + opacity: 1; +} + +.tooltip:hover::after { + display: block; +} + +.action-feedback-text { + display: none; + position: absolute; + left: 180px; + text-wrap: nowrap; + top: 13px; +} +.action-feedback-text.show { + display: block; +} +.composite-button > .feedback-form { + position: absolute; + left: 90px; + top: -342px; + display: none; +} +.composite-button > .feedback-form.show { + display: block; +} diff --git a/app/components/Article/FeedbackForm/index.tsx b/app/components/Article/FeedbackForm/index.tsx new file mode 100644 index 00000000..1434a8d4 --- /dev/null +++ b/app/components/Article/FeedbackForm/index.tsx @@ -0,0 +1,111 @@ +import React, {ChangeEvent} from 'react' +import Button from '~/components/Button' +import './feedbackForm.css' + +export interface FeedbackFormProps { + /** + * Article ID + */ + pageid: string + /** + * Class name + */ + className?: string + /** + * onBlur + */ + onBlur?: () => void + /** + * onFocus + */ + onFocus?: () => void + /** + * Has Options + */ + hasOptions?: boolean +} +const FeedbackForm = ({ + pageid, + className = 'feedback-form', + onBlur, + onFocus, + hasOptions = true, +}: FeedbackFormProps) => { + // to be implemented. + console.log(pageid) + const [selected, setSelected] = React.useState() + const options = [ + { + text: 'Making things up', + option: 'making_things_up', + }, + { + text: 'Being mean', + option: 'being_mean', + }, + { + text: 'Typos', + option: 'typos', + }, + ] + const [enabledSubmit, setEnabledSubmit] = React.useState(false) + const selectFeedback = (option: string) => { + setSelected(option) + + if (onFocus) { + onFocus() + } + setEnabledSubmit(true) + } + const handleBlur = React.useCallback( + (e: ChangeEvent) => { + const currentTarget = e.currentTarget + + // Give browser time to focus the next element + requestAnimationFrame(() => { + // Check if the new focused element is a child of the original container + if (!currentTarget.contains(document.activeElement)) { + if (onBlur) { + onBlur() + } + } + }) + }, + [onBlur] + ) + + const handleSubmit = () => {} + + return ( +
+
+ What was the problem? + {hasOptions + ? options.map((option, index) => ( + + )) + : null} + + + +
+
+ ) +} + +export default FeedbackForm diff --git a/app/components/Article/article.css b/app/components/Article/article.css index f7502423..dbe0a672 100644 --- a/app/components/Article/article.css +++ b/app/components/Article/article.css @@ -127,6 +127,8 @@ article .link-popup p { article .footer-comtainer { display: flex; align-items: center; + margin-top: var(--spacing-56); + margin-bottom: var(--spacing-24); } article .footer-comtainer > * { diff --git a/app/components/Article/index.tsx b/app/components/Article/index.tsx index d9531da1..63bce764 100644 --- a/app/components/Article/index.tsx +++ b/app/components/Article/index.tsx @@ -1,4 +1,4 @@ -import {useState} from 'react' +import React, {useState} from 'react' import {Link} from '@remix-run/react' import KeepGoing from '~/components/Article/KeepGoing' import CopyIcon from '~/components/icons-generated/Copy' @@ -9,10 +9,14 @@ import type {Glossary, Question} from '~/server-utils/stampy' import {tagUrl} from '~/routesMapper' import Contents from './Contents' import './article.css' +import FeedbackForm from '~/components/Article/FeedbackForm' const isLoading = ({text}: Question) => !text || text === 'Loading...' const ArticleFooter = (question: Question) => { + const [showFeedback, setShowFeedback] = useState(false) + const [showFeedbackForm, setShowFeedbackForm] = useState(false) + const [isFormFocused, setIsFormFocused] = useState(false) const date = question.updatedAt && new Date(question.updatedAt).toLocaleDateString('en-GB', { @@ -20,6 +24,18 @@ const ArticleFooter = (question: Question) => { month: 'short', }) + React.useEffect(() => { + // Hide the form after 10 seconds if the user hasn't interacted with it + const timeoutId = setInterval(() => { + if (!isFormFocused) { + setShowFeedbackForm(false) + } + }, 10000) + + // Clear the timeout to prevent it from running if the component unmounts + return () => clearInterval(timeoutId) + }, [showFeedbackForm, isFormFocused]) + return ( !isLoading(question) && (
@@ -36,8 +52,28 @@ const ArticleFooter = (question: Question) => { Was this page helpful? - - + setShowFeedback(true)} + /> + setShowFeedbackForm(true)} + /> + + Thanks for your feedback! + + setIsFormFocused(false)} + onFocus={() => setIsFormFocused(true)} + hasOptions={false} + />
) diff --git a/app/components/Button/button.css b/app/components/Button/button.css index 2e5d5a79..7a8a7a6a 100644 --- a/app/components/Button/button.css +++ b/app/components/Button/button.css @@ -75,7 +75,7 @@ border-right: 1px solid transparent; } -.composite-button > form:last-child .button, +.composite-button > form:last-of-type .button, .composite-button > .button:last-child { border-top-right-radius: var(--border-radius); border-bottom-right-radius: var(--border-radius); @@ -107,6 +107,13 @@ font-size: 16px; /* hard to set via classes, what with this being a pseudoclass */ } -.tooltip:hover::after { - display: block; +.button.full-width { + width: 100%; + height: 48px; + border: 0; +} +.button:disabled, +.button[disabled] { + opacity: 0.6; + cursor: inherit; } diff --git a/app/components/Button/index.tsx b/app/components/Button/index.tsx index 28cece8f..c5662adb 100644 --- a/app/components/Button/index.tsx +++ b/app/components/Button/index.tsx @@ -7,18 +7,28 @@ type ButtonProps = { children?: ReactNode className?: string tooltip?: string + disabled?: boolean } -const Button = ({children, action, tooltip, className}: ButtonProps) => { +const Button = ({children, action, tooltip, className, disabled = false}: ButtonProps) => { const classes = ['button', className, tooltip && 'tooltip'].filter((i) => i).join(' ') if (typeof action === 'string') { return ( - + { + if (disabled) { + e.preventDefault() + } + }} + > {children} ) } return ( - ) diff --git a/app/newRoot.css b/app/newRoot.css index 91196fcb..cb6a0895 100644 --- a/app/newRoot.css +++ b/app/newRoot.css @@ -30,12 +30,14 @@ /* spacing */ --border-radius: 6px; --spacing-4: 4px; + --spacing-6: 6px; --spacing-8: 8px; --spacing-12: 12px; --spacing-16: 16px; --spacing-24: 24px; --spacing-32: 32px; --spacing-40: 40px; + --spacing-48: 48px; --spacing-56: 56px; --spacing-80: 80px; --spacing-104: 104px; diff --git a/app/routes/questions.actions.tsx b/app/routes/questions.actions.tsx index 79a46e8d..5d4d76e4 100644 --- a/app/routes/questions.actions.tsx +++ b/app/routes/questions.actions.tsx @@ -94,8 +94,16 @@ type Props = { showText?: boolean children?: ReactNode | ReactNode[] [k: string]: unknown + onSuccess?: () => void } -export const Action = ({pageid, actionType, showText = true, children, ...props}: Props) => { +export const Action = ({ + pageid, + actionType, + showText = true, + children, + onSuccess, + ...props +}: Props) => { const [remixSearchParams] = useSearchParams() const [stateString] = useState(() => remixSearchParams.get('state') ?? '') const {Icon, title} = actions[actionType] @@ -139,6 +147,7 @@ export const Action = ({pageid, actionType, showText = true, children, ...props} const response = await fetch('/questions/actions', {method: 'POST', body: searchParams}) if (response.ok !== true) setActionTaken(!actionTaken) + else if (onSuccess) onSuccess() } const className = 'secondary icon-link' + (actionTaken ? ' focused' : '') diff --git a/stories/FeedbackForm.stories.tsx b/stories/FeedbackForm.stories.tsx new file mode 100644 index 00000000..feba8c94 --- /dev/null +++ b/stories/FeedbackForm.stories.tsx @@ -0,0 +1,14 @@ +import type {Meta, StoryObj} from '@storybook/react' +import FeedbackForm from '../app/components/Article/FeedbackForm' +const meta = { + title: 'Components/Article/FeedbackForm', + component: FeedbackForm, + tags: ['autodocs'], +} satisfies Meta +export default meta +type Story = StoryObj +export const Default: Story = { + args: { + pageid: 'NH50', + }, +}