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

fix: better differentiation for loading and error states (fixes #594) #601

Merged
merged 1 commit into from
Feb 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 53 additions & 9 deletions web/css/root.css
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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;
}
Loading