diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/Comment/Comment.tsx | 20 | ||||
| -rw-r--r-- | src/components/CommentForm/CommentForm.tsx | 150 | ||||
| -rw-r--r-- | src/components/Notice/Notice.tsx | 3 | ||||
| -rw-r--r-- | src/components/Spinner/Spinner.tsx | 11 | ||||
| -rw-r--r-- | src/i18n/en.json | 44 | ||||
| -rw-r--r-- | src/i18n/fr.json | 44 | ||||
| -rw-r--r-- | src/ts/types/app.ts | 2 |
7 files changed, 191 insertions, 83 deletions
diff --git a/src/components/Comment/Comment.tsx b/src/components/Comment/Comment.tsx index ab1dffc..a263771 100644 --- a/src/components/Comment/Comment.tsx +++ b/src/components/Comment/Comment.tsx @@ -24,7 +24,7 @@ const Comment = ({ const intl = useIntl(); const router = useRouter(); const locale = router.locale ? router.locale : settings.locales.defaultLocale; - const [isReply, setIsReply] = useState<boolean>(false); + const [shouldOpenForm, setShouldOpenForm] = useState<boolean>(false); const firstFieldRef = useRef<HTMLInputElement>(null); useEffect(() => { @@ -98,21 +98,25 @@ const Comment = ({ ></div> {!isNested && ( <footer className={styles.footer}> - <Button clickHandler={() => setIsReply((prev) => !prev)}> - {intl.formatMessage({ - defaultMessage: 'Reply', - description: 'Comment: reply button', - })} + <Button clickHandler={() => setShouldOpenForm((prev) => !prev)}> + {shouldOpenForm + ? intl.formatMessage({ + defaultMessage: 'Cancel reply', + description: 'Comment: reply button', + }) + : intl.formatMessage({ + defaultMessage: 'Reply', + description: 'Comment: reply button', + })} </Button> </footer> )} </article> - {isReply && ( + {shouldOpenForm && ( <CommentForm ref={firstFieldRef} articleId={articleId} parentId={comment.commentId} - isReply={isReply} /> )} {comment.replies.length > 0 && ( diff --git a/src/components/CommentForm/CommentForm.tsx b/src/components/CommentForm/CommentForm.tsx index 0ea3276..762bb75 100644 --- a/src/components/CommentForm/CommentForm.tsx +++ b/src/components/CommentForm/CommentForm.tsx @@ -1,68 +1,107 @@ import { ButtonSubmit } from '@components/Buttons'; import { Form, FormItem, Input, TextArea } from '@components/Form'; import Notice from '@components/Notice/Notice'; +import Spinner from '@components/Spinner/Spinner'; import { createComment } from '@services/graphql/mutations'; +import { NoticeType } from '@ts/types/app'; import { ForwardedRef, forwardRef, useState } from 'react'; import { useIntl } from 'react-intl'; import styles from './CommentForm.module.scss'; const CommentForm = ( - { - articleId, - parentId = 0, - isReply = false, - }: { - articleId: number; - parentId?: number; - isReply?: boolean; - }, + { articleId, parentId = 0 }: { articleId: number; parentId?: number }, ref: ForwardedRef<HTMLInputElement> ) => { const intl = useIntl(); - const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [website, setWebsite] = useState(''); - const [message, setMessage] = useState(''); - const [isSuccess, setIsSuccess] = useState(false); - const [isApproved, setIsApproved] = useState(false); + const [name, setName] = useState<string>(''); + const [email, setEmail] = useState<string>(''); + const [website, setWebsite] = useState<string>(''); + const [comment, setComment] = useState<string>(''); + const [isSubmitting, setIsSubmitting] = useState<boolean>(false); + const [notice, setNotice] = useState<string>(); + const [noticeType, setNoticeType] = useState<NoticeType>('success'); const resetForm = () => { setName(''); setEmail(''); setWebsite(''); - setMessage(''); + setComment(''); + setIsSubmitting(false); }; - const submitHandler = async (e: SubmitEvent) => { - e.preventDefault(); + const isEmptyString = (value: string): boolean => value.trim() === ''; + const areRequiredFieldsSet = (): boolean => + !isEmptyString(name) && !isEmptyString(email) && !isEmptyString(comment); - if (name && email && message && articleId) { - const data = { - author: name, - authorEmail: email, - authorUrl: website, - content: message, - parent: parentId, - commentOn: articleId, - mutationId: 'createComment', - }; - const createdComment = await createComment(data); + const sendComment = async () => { + const data = { + author: name, + authorEmail: email, + authorUrl: website, + content: comment, + parent: parentId, + commentOn: articleId, + mutationId: 'createComment', + }; - if (createdComment.success) setIsSuccess(true); - if (isSuccess) { - resetForm(); - if (createdComment.comment?.approved) setIsApproved(true); + const createdComment = await createComment(data); - setTimeout(() => { - setIsSuccess(false); - setIsApproved(false); - }, 8000); + if (createdComment.success) { + resetForm(); + setNoticeType('success'); + if (createdComment.comment?.approved) { + setNotice( + intl.formatMessage({ + defaultMessage: 'Thanks for your comment!', + description: 'CommentForm: success notice', + }) + ); + } else { + setNotice( + intl.formatMessage({ + defaultMessage: + 'Thanks for your comment! It is now awaiting moderation.', + description: 'CommentForm: success notice but awaiting moderation', + }) + ); } + + setTimeout(() => { + setNotice(undefined); + }, 10000); + } else { + setNoticeType('error'); + setNotice( + intl.formatMessage({ + defaultMessage: + 'An unexpected error happened. Comment cannot be submitted.', + description: 'CommentForm: error notice', + }) + ); + } + }; + + const submitHandler = async (e: SubmitEvent) => { + e.preventDefault(); + setNotice(undefined); + setIsSubmitting(true); + + if (areRequiredFieldsSet()) { + sendComment(); } else { - setIsSuccess(false); + setIsSubmitting(false); + setNoticeType('warning'); + setNotice( + intl.formatMessage({ + defaultMessage: + 'Some required fields are empty. Comment cannot be submitted.', + description: 'CommentForm: missing required fields', + }) + ); } }; + const isReply = parentId !== 0; const wrapperClasses = `${styles.wrapper} ${ isReply ? styles['wrapper--reply'] : '' }`; @@ -72,7 +111,7 @@ const CommentForm = ( <h2 className={styles.title}> {intl.formatMessage({ defaultMessage: 'Leave a comment', - description: 'CommentForm: form title', + description: 'CommentForm: Form title', })} </h2> <Form @@ -97,6 +136,7 @@ const CommentForm = ( <Input id="commenter-email" name="commenter-email" + type="email" label={intl.formatMessage({ defaultMessage: 'Email', description: 'CommentForm: Email field label', @@ -120,18 +160,24 @@ const CommentForm = ( </FormItem> <FormItem> <TextArea - id="commenter-message" - name="commenter-message" + id="commenter-comment" + name="commenter-comment" label={intl.formatMessage({ defaultMessage: 'Comment', description: 'CommentForm: Comment field label', })} - value={message} - setValue={setMessage} + value={comment} + setValue={setComment} required={true} /> </FormItem> <FormItem> + <noscript> + {intl.formatMessage({ + defaultMessage: 'Javascript is required to post a comment.', + description: 'CommentForm: noscript tag', + })} + </noscript> <ButtonSubmit> {intl.formatMessage({ defaultMessage: 'Send', @@ -139,16 +185,16 @@ const CommentForm = ( })} </ButtonSubmit> </FormItem> - {isSuccess && !isApproved && ( - <Notice type="success"> - {intl.formatMessage({ - defaultMessage: - 'Thanks for your comment! It is now awaiting moderation.', - description: 'CommentForm: Comment sent success message', - })} - </Notice> - )} </Form> + {isSubmitting && ( + <Spinner + message={intl.formatMessage({ + defaultMessage: 'Submitting...', + description: 'CommentForm: submitting message', + })} + /> + )} + {notice && <Notice type={noticeType}>{notice}</Notice>} </div> ); }; diff --git a/src/components/Notice/Notice.tsx b/src/components/Notice/Notice.tsx index 472efa5..02b1f12 100644 --- a/src/components/Notice/Notice.tsx +++ b/src/components/Notice/Notice.tsx @@ -1,8 +1,7 @@ +import { NoticeType } from '@ts/types/app'; import { ReactNode } from 'react'; import styles from './Notice.module.scss'; -type NoticeType = 'error' | 'info' | 'success' | 'warning'; - const Notice = ({ children, type, diff --git a/src/components/Spinner/Spinner.tsx b/src/components/Spinner/Spinner.tsx index 381fbb6..afe86f6 100644 --- a/src/components/Spinner/Spinner.tsx +++ b/src/components/Spinner/Spinner.tsx @@ -1,7 +1,7 @@ import { useIntl } from 'react-intl'; import styles from './Spinner.module.scss'; -const Spinner = () => { +const Spinner = ({ message }: { message?: string }) => { const intl = useIntl(); return ( @@ -10,10 +10,11 @@ const Spinner = () => { <div className={styles.ball}></div> <div className={styles.ball}></div> <div className={styles.text}> - {intl.formatMessage({ - defaultMessage: 'Loading...', - description: 'Spinner: loading text', - })} + {message || + intl.formatMessage({ + defaultMessage: 'Loading...', + description: 'Spinner: loading text', + })} </div> </div> ); diff --git a/src/i18n/en.json b/src/i18n/en.json index e6e7647..56f448f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -15,6 +15,10 @@ "defaultMessage": "Blog: development, open source - {websiteName}", "description": "BlogPage: SEO - Page title" }, + "+aHn7j": { + "defaultMessage": "Leave a comment", + "description": "CommentForm: Form title" + }, "/IirIt": { "defaultMessage": "Legal notice", "description": "LegalNoticePage: page title" @@ -95,6 +99,10 @@ "defaultMessage": "Contact", "description": "ContactPage: page title" }, + "AVUUgG": { + "defaultMessage": "Thanks for your comment!", + "description": "CommentForm: success notice" + }, "AnaPbu": { "defaultMessage": "Search", "description": "SearchForm: search button text" @@ -155,10 +163,6 @@ "defaultMessage": "Updated on:", "description": "PostMeta: update date label" }, - "G/SLvC": { - "defaultMessage": "Thanks for your comment! It is now awaiting moderation.", - "description": "CommentForm: Comment sent success message" - }, "GUfnQ4": { "defaultMessage": "Reading time:", "description": "Article meta" @@ -171,6 +175,10 @@ "defaultMessage": "Primary", "description": "MainNav: aria-label" }, + "HEJ3Gv": { + "defaultMessage": "Submitting...", + "description": "CommentForm: submitting message" + }, "HTdaZj": { "defaultMessage": "Footer", "description": "FooterNav: aria-label" @@ -303,6 +311,10 @@ "defaultMessage": "Javascript is required to use the table of contents.", "description": "ToC: noscript tag" }, + "Rle+UK": { + "defaultMessage": "Some required fields are empty. Comment cannot be submitted.", + "description": "CommentForm: missing required fields" + }, "SWjj4l": { "defaultMessage": "Github", "description": "SocialMedia: Github" @@ -335,6 +347,10 @@ "defaultMessage": "Light Theme 🌞", "description": "Prism: toggle light theme button text" }, + "Ul2NIl": { + "defaultMessage": "Thanks for your comment! It is now awaiting moderation.", + "description": "CommentForm: success notice but awaiting moderation" + }, "UsQske": { "defaultMessage": "Read more here:", "description": "Sharing: content link prefix" @@ -419,6 +435,10 @@ "defaultMessage": "Table of contents", "description": "ToC: widget title" }, + "Zlkww3": { + "defaultMessage": "Failed to load.", + "description": "CommentsList: failed to load" + }, "aA3hOT": { "defaultMessage": "{starsCount, plural, =0 {0 stars on Github} one {# star on Github} other {# stars on Github}}", "description": "ProjectSummary: technologies list label" @@ -451,6 +471,10 @@ "defaultMessage": "Contact", "description": "MainNav: contact link" }, + "cjK9Ad": { + "defaultMessage": "An unexpected error happened. Comment cannot be submitted.", + "description": "CommentForm: error notice" + }, "csCQQk": { "defaultMessage": "LinkedIn", "description": "Sharing: LinkedIn" @@ -467,6 +491,10 @@ "defaultMessage": "{articlesCount, plural, =0 {# loaded articles} one {# loaded article} other {# loaded articles}} out of a total of {total}", "description": "PaginationCursor: loaded articles count message" }, + "e1Forh": { + "defaultMessage": "Cancel reply", + "description": "Comment: reply button" + }, "e9L59q": { "defaultMessage": "No comments yet.", "description": "CommentsList: No comment message" @@ -479,10 +507,6 @@ "defaultMessage": "Seen on {domainName}:", "description": "Sharing: seen on text" }, - "ec3m6p": { - "defaultMessage": "Leave a comment", - "description": "CommentForm: form title" - }, "enwhNm": { "defaultMessage": "{count, plural, =0 {Technologies:} one {Technology:} other {Technologies:}}", "description": "ProjectSummary: technologies list label" @@ -499,6 +523,10 @@ "defaultMessage": "Failed to load.", "description": "SearchPage: failed to load text" }, + "g1cFCa": { + "defaultMessage": "Javascript is required to post a comment.", + "description": "CommentForm: noscript tag" + }, "g4DckL": { "defaultMessage": "Table of Contents", "description": "CVPage: ToC sidebar aria-label" diff --git a/src/i18n/fr.json b/src/i18n/fr.json index d59b6e5..6cfd591 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -15,6 +15,10 @@ "defaultMessage": "Blog : développement, libre et open-source - {websiteName}", "description": "BlogPage: SEO - Page title" }, + "+aHn7j": { + "defaultMessage": "Laisser un commentaire", + "description": "CommentForm: Form title" + }, "/IirIt": { "defaultMessage": "Mentions légales", "description": "LegalNoticePage: page title" @@ -95,6 +99,10 @@ "defaultMessage": "Contact", "description": "ContactPage: page title" }, + "AVUUgG": { + "defaultMessage": "Merci pour votre commentaire !", + "description": "CommentForm: success notice" + }, "AnaPbu": { "defaultMessage": "Rechercher", "description": "SearchForm: search button text" @@ -155,10 +163,6 @@ "defaultMessage": "Mis à jour le :", "description": "PostMeta: update date label" }, - "G/SLvC": { - "defaultMessage": "Merci pour votre commentaire ! Il est maintenant en attente de modération.", - "description": "CommentForm: Comment sent success message" - }, "GUfnQ4": { "defaultMessage": "Temps de lecture :", "description": "Article meta" @@ -171,6 +175,10 @@ "defaultMessage": "Principal", "description": "MainNav: aria-label" }, + "HEJ3Gv": { + "defaultMessage": "En cours d'envoi...", + "description": "CommentForm: submitting message" + }, "HTdaZj": { "defaultMessage": "Pied de page", "description": "FooterNav: aria-label" @@ -303,6 +311,10 @@ "defaultMessage": "Javascript est nécessaire pour utiliser la table des matières.", "description": "ToC: noscript tag" }, + "Rle+UK": { + "defaultMessage": "Certains champs requis sont vides. Le commentaire ne peut être envoyé.", + "description": "CommentForm: missing required fields" + }, "SWjj4l": { "defaultMessage": "Github", "description": "SocialMedia: Github" @@ -335,6 +347,10 @@ "defaultMessage": "Thème clair 🌞", "description": "Prism: toggle light theme button text" }, + "Ul2NIl": { + "defaultMessage": "Merci pour votre commentaire ! Il est maintenant en attente de modération.", + "description": "CommentForm: success notice but awaiting moderation" + }, "UsQske": { "defaultMessage": "En lire plus ici :", "description": "Sharing: content link prefix" @@ -419,6 +435,10 @@ "defaultMessage": "Table des matières", "description": "ToC: widget title" }, + "Zlkww3": { + "defaultMessage": "Échec du chargement.", + "description": "CommentsList: failed to load" + }, "aA3hOT": { "defaultMessage": "{starsCount, plural, =0 {0 étoile sur Github} one {# étoile sur Github} other {# étoiles sur Github}}", "description": "ProjectSummary: technologies list label" @@ -451,6 +471,10 @@ "defaultMessage": "Contact", "description": "MainNav: contact link" }, + "cjK9Ad": { + "defaultMessage": "Une erreur inattendue est survenue. Le commentaire ne peut pas être envoyé.", + "description": "CommentForm: error notice" + }, "csCQQk": { "defaultMessage": "LinkedIn", "description": "Sharing: LinkedIn" @@ -467,6 +491,10 @@ "defaultMessage": "{articlesCount, plural, =0 {# article chargé} one {# article chargé} other {# articles chargés}} sur un total de {total}", "description": "PaginationCursor: loaded articles count message" }, + "e1Forh": { + "defaultMessage": "Annuler la réponse", + "description": "Comment: reply button" + }, "e9L59q": { "defaultMessage": "Aucun commentaire.", "description": "CommentsList: No comment message" @@ -479,10 +507,6 @@ "defaultMessage": "Vu sur {domainName} :", "description": "Sharing: seen on text" }, - "ec3m6p": { - "defaultMessage": "Laisser un commentaire", - "description": "CommentForm: form title" - }, "enwhNm": { "defaultMessage": "{count, plural, =0 {Technologies :} one {Technologie :} other {Technologies :}}", "description": "ProjectSummary: technologies list label" @@ -499,6 +523,10 @@ "defaultMessage": "Échec du chargement.", "description": "SearchPage: failed to load text" }, + "g1cFCa": { + "defaultMessage": "Javascript est nécessaire pour poster un commentaire.", + "description": "CommentForm: noscript tag" + }, "g4DckL": { "defaultMessage": "Table des matières", "description": "CVPage: ToC sidebar aria-label" diff --git a/src/ts/types/app.ts b/src/ts/types/app.ts index 8e087fd..aeab2bc 100644 --- a/src/ts/types/app.ts +++ b/src/ts/types/app.ts @@ -98,6 +98,8 @@ export type Meta = { updatedOn: string; }; +export type NoticeType = 'error' | 'info' | 'success' | 'warning'; + export type PageInfo = { endCursor: string; hasNextPage: boolean; |
