Skip to content
This repository has been archived by the owner on May 10, 2023. It is now read-only.

Commit

Permalink
fix: better differentiation for loading and error states (fixes #594) (
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelKohler committed Feb 12, 2022
1 parent db7fc21 commit 3b47a87
Show file tree
Hide file tree
Showing 14 changed files with 241 additions and 147 deletions.
62 changes: 53 additions & 9 deletions web/css/root.css
Expand Up @@ -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;
Expand Down Expand Up @@ -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%;
Expand All @@ -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;
Expand All @@ -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;
}
}
27 changes: 1 addition & 26 deletions 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;
}
}
7 changes: 3 additions & 4 deletions web/src/components/add/submit-form.test.tsx
Expand Up @@ -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(
<SubmitForm languages={languages} message={message} onSubmit={onSubmit} />
<SubmitForm languages={languages} duplicates={0} onSubmit={onSubmit} />
);
expect(screen.getByText(message)).toBeTruthy();
expect(screen.queryByText(/Submitted sentences./)).toBeTruthy();
});

test('should render error', async () => {
Expand Down
112 changes: 71 additions & 41 deletions web/src/components/add/submit-form.tsx
@@ -1,27 +1,19 @@
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';
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;
Expand All @@ -31,7 +23,7 @@ type SubmissionData = {
type Props = {
languages: Language[];
onSubmit: (data: SubmissionData) => void;
message?: string;
duplicates?: number;
error?: string;
sentenceSubmissionFailures?: SubmissionFailures;
languageFetchFailure?: boolean;
Expand All @@ -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<FormErrorDescription | Record<string, never>>({});
const [formFields, setFormFields] = useState<FormFields>({
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<HTMLInputElement> | React.ChangeEvent<HTMLTextAreaElement>
) => {
Expand All @@ -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<HTMLFormElement>) => {
Expand Down Expand Up @@ -129,16 +121,23 @@ export default function SubmitForm({
</Localized>

{languageFetchFailure && <Error translationKey="sc-languages-fetch-error" />}

{message && <section className="form-message">{message}</section>}
{formError && <section className="form-error">{formError}</section>}
{error && <section className="form-error">{error}</section>}
{typeof duplicates !== 'undefined' && (
<Success translationKey="sc-add-result" vars={{ duplicates }} />
)}
{error && <Error>{error}</Error>}

<section>
<Localized id="sc-submit-select-language" attrs={{ labelText: true }}>
<LanguageSelector languages={languages} labelText="" onChange={onLanguageSelect} />
</Localized>

{formErrors.language && (
<Localized id="sc-submit-err-select-lang">
<p className="error-message"></p>
</Localized>
)}
</section>

<section>
<Localized
id="sc-submit-add-sentences"
Expand All @@ -154,6 +153,13 @@ export default function SubmitForm({
>
<label htmlFor="sentences-input"></label>
</Localized>

{formErrors.sentences && (
<Localized id="sc-submit-err-add-sentences">
<p className="error-message"></p>
</Localized>
)}

<Localized id="sc-submit-ph-one-per-line" attrs={{ placeholder: true }}>
<textarea
id="sentences-input"
Expand All @@ -164,6 +170,7 @@ export default function SubmitForm({
/>
</Localized>
</section>

<section>
<Localized
id="sc-submit-from-where"
Expand All @@ -179,6 +186,13 @@ export default function SubmitForm({
>
<label htmlFor="source-input"></label>
</Localized>

{formErrors.source && (
<Localized id="sc-submit-err-add-source">
<p className="error-message"></p>
</Localized>
)}

<Localized id="sc-submit-ph-read-how-to" attrs={{ placeholder: true }}>
<input
id="source-input"
Expand All @@ -189,7 +203,14 @@ export default function SubmitForm({
/>
</Localized>
</section>

<section>
{formErrors.confirmed && (
<Localized id="sc-submit-err-confirm-pd">
<p className="error-message"></p>
</Localized>
)}

<input id="agree" type="checkbox" name="confirmed" onChange={handleInputChange} />
<Localized
id="sc-submit-confirm"
Expand Down Expand Up @@ -247,3 +268,12 @@ export default function SubmitForm({
</React.Fragment>
);
}

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;
}
25 changes: 19 additions & 6 deletions web/src/components/error.tsx
@@ -1,10 +1,23 @@
import React from 'react';
import { Localized } from '@fluent/react';

export default function Footer({ translationKey }: { translationKey: string }) {
return (
<Localized id={translationKey}>
<div className="error-box"></div>
</Localized>
);
type Props = {
translationKey?: string;
children?: React.ReactNode;
};

export default function Error({ translationKey, children }: Props) {
if (translationKey) {
return (
<Localized id={translationKey}>
<div className="error-box"></div>
</Localized>
);
}

if (children) {
return <div className="error-box">{children}</div>;
}

return null;
}

0 comments on commit 3b47a87

Please sign in to comment.