From 30c9054b2318c2d39f09c65d67774a783e9f11ca Mon Sep 17 00:00:00 2001 From: Michael Kohler Date: Sat, 12 Feb 2022 02:20:53 +0100 Subject: [PATCH] fix: better differentiation for loading and error states (fixes #594) --- web/css/root.css | 62 ++++++++-- web/css/spinner-button.css | 27 +---- web/src/components/add/submit-form.test.tsx | 7 +- web/src/components/add/submit-form.tsx | 112 +++++++++++------- web/src/components/error.tsx | 25 +++- web/src/components/pages/add.tsx | 8 +- .../components/pages/my-sentences-list.tsx | 16 +-- .../pages/rejected-sentences-list.tsx | 16 +-- web/src/components/pages/review.tsx | 43 +++---- web/src/components/pages/stats.tsx | 27 +++-- .../profile/add-language-section.tsx | 3 +- .../profile/personal-language-info.tsx | 2 +- web/src/components/profile/settings.tsx | 7 +- web/src/components/success.tsx | 33 ++++++ 14 files changed, 241 insertions(+), 147 deletions(-) create mode 100644 web/src/components/success.tsx diff --git a/web/css/root.css b/web/css/root.css index a1128ba1..097dca0d 100644 --- a/web/css/root.css +++ b/web/css/root.css @@ -10,11 +10,13 @@ --deactive-color: #d7d7db; --grey-color: #d7d7db; --dark-grey-color: #5e5c5b; - --error-color: #ff4f5e; + --error-font-color: #ff4f5e; --error-border-color: #ff7070; --error-background-color: #ffbfc5; --warning-border-color: #ffff00; --warning-background-color: #ffffb9; + --success-border-color: #46ff55; + --success-background-color: #71ffc4; --review-selected-color: #000000; --review-unselected-color: #ffffff; --base-font-size: 19px; @@ -192,13 +194,6 @@ form button:not(.standalone) { font-weight: 600; } -.form-error { - line-height: 1rem; - color: var(--error-color); - text-align: center; - font-weight: 600; -} - .loading-text { display: inline-block; font-size: 80%; @@ -213,13 +208,23 @@ form button:not(.standalone) { } .error-message { - color: red; + color: var(--error-font-color); + font-size: 0.9rem; + margin-top: 0.5rem; + margin-bottom: 0.5rem; } .small { font-size: 0.7rem; } +.success-box { + margin: 1.5rem 0; + padding: 1rem; + border: 2px solid var(--success-border-color); + background-color: var(--success-background-color); +} + .error-box { margin: 1.5rem 0; padding: 1rem; @@ -239,3 +244,42 @@ form button:not(.standalone) { flex-direction: row; align-items: center; } + +.loading-container { + position: relative; +} + +.spinning:before { + content: ''; + width: 0px; + height: 0px; + border-radius: 50%; + right: 6px; + top: 50%; + position: absolute; + border-right: 3px solid #2795ae; + animation: rotate360 0.5s infinite linear, exist 0.1s forwards ease; +} + +.loading-container .spinning:before { + right: unset; + left: 0px; +} + +.loading-container p { + margin-left: 30px; +} + +@keyframes rotate360 { + 100% { + transform: rotate(360deg); + } +} + +@keyframes exist { + 100% { + width: 15px; + height: 15px; + margin: -8px 5px 0 0; + } +} diff --git a/web/css/spinner-button.css b/web/css/spinner-button.css index 40793e74..0f18da8c 100644 --- a/web/css/spinner-button.css +++ b/web/css/spinner-button.css @@ -1,34 +1,9 @@ .spinnerButton { transition: padding-right 0.3s ease; + position: relative; } .spinnerButton.spinning { padding-right: 40px; cursor: not-allowed; } - -.spinnerButton.spinning:before { - content: ''; - width: 0px; - height: 0px; - border-radius: 50%; - right: 6px; - top: 50%; - position: absolute; - border-right: 3px solid #2795ae; - animation: rotate360 0.5s infinite linear, exist 0.1s forwards ease; -} - -@keyframes rotate360 { - 100% { - transform: rotate(360deg); - } -} - -@keyframes exist { - 100% { - width: 15px; - height: 15px; - margin: -8px 5px 0 0; - } -} diff --git a/web/src/components/add/submit-form.test.tsx b/web/src/components/add/submit-form.test.tsx index 81135dd5..3f2397f5 100644 --- a/web/src/components/add/submit-form.test.tsx +++ b/web/src/components/add/submit-form.test.tsx @@ -35,12 +35,11 @@ test('should render submit button', async () => { expect(screen.getByText('Submit')).toBeTruthy(); }); -test('should render message', async () => { - const message = 'Hi'; +test('should render success', async () => { await renderWithLocalization( - + ); - expect(screen.getByText(message)).toBeTruthy(); + expect(screen.queryByText(/Submitted sentences./)).toBeTruthy(); }); test('should render error', async () => { diff --git a/web/src/components/add/submit-form.tsx b/web/src/components/add/submit-form.tsx index 246517ec..61895b8d 100644 --- a/web/src/components/add/submit-form.tsx +++ b/web/src/components/add/submit-form.tsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; -import { Localized, useLocalization } from '@fluent/react'; +import { Localized } from '@fluent/react'; import { useLocaleUrl } from '../../urls'; import type { Language, SubmissionFailures } from '../../types'; @@ -8,20 +8,12 @@ import Error from '../error'; import LanguageSelector from '../language-selector'; import Sentence from '../sentence'; import SubmitButton from '../submit-button'; +import Success from '../success'; import { Prompt } from '../prompt'; const SPLIT_ON = '\n'; const TRANSLATION_KEY_PREFIX = 'TRANSLATION_KEY:'; -function parseSentences(sentenceText: string): string[] { - const sentences = sentenceText - .split(SPLIT_ON) - .map((s) => s.trim()) - .filter(Boolean); - const dedupedSentences = Array.from(new Set(sentences)); - return dedupedSentences; -} - type SubmissionData = { sentences: string[]; language: string; @@ -31,7 +23,7 @@ type SubmissionData = { type Props = { languages: Language[]; onSubmit: (data: SubmissionData) => void; - message?: string; + duplicates?: number; error?: string; sentenceSubmissionFailures?: SubmissionFailures; languageFetchFailure?: boolean; @@ -43,24 +35,36 @@ type FormFields = { confirmed: boolean; }; +type FormErrorDescription = { + language: boolean; + sentences: boolean; + source: boolean; + confirmed: boolean; +}; + export default function SubmitForm({ languages, onSubmit, - message, + duplicates, error, sentenceSubmissionFailures, languageFetchFailure = false, }: Props) { - const firstLanguage = languages.length === 1 && languages[0]; - const [formError, setError] = useState(''); + const [formErrors, setFormErrors] = useState>({}); const [formFields, setFormFields] = useState({ sentenceText: '', source: '', confirmed: false, }); - const [language, setLanguage] = useState(firstLanguage ? firstLanguage.id : ''); + const [language, setLanguage] = useState(''); const localizedHowToUrl = useLocaleUrl('/how-to'); + useEffect(() => { + if (languages.length === 1 && languages[0]) { + setLanguage(languages[0].id); + } + }, [languages]); + const handleInputChange = ( event: React.FormEvent | React.ChangeEvent ) => { @@ -77,30 +81,18 @@ export default function SubmitForm({ setLanguage(language); }; - const { l10n } = useLocalization(); - const validateForm = () => { - if (!language) { - setError(l10n.getString('sc-submit-err-select-lang')); - return false; - } - - if (!formFields.sentenceText) { - setError(l10n.getString('sc-submit-err-add-sentences')); - return false; - } - - if (!formFields.source) { - setError(l10n.getString('sc-submit-err-add-source')); - return false; - } + const errors = { + language: !language, + sentences: !formFields.sentenceText, + source: !formFields.source, + confirmed: !formFields.confirmed, + }; - if (!formFields.confirmed) { - setError(l10n.getString('sc-submit-err-confirm-pd')); - return false; - } + setFormErrors(errors); - return true; + const hasErrors = Object.values(errors).some((errorState) => errorState); + return !hasErrors; }; const onSentencesSubmit = (event: React.FormEvent) => { @@ -129,16 +121,23 @@ export default function SubmitForm({ {languageFetchFailure && } - - {message &&
{message}
} - {formError &&
{formError}
} - {error &&
{error}
} + {typeof duplicates !== 'undefined' && ( + + )} + {error && {error}}
+ + {formErrors.language && ( + +

+
+ )}
+
+ + {formErrors.sentences && ( + +

+
+ )} +