summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/Comment/Comment.tsx20
-rw-r--r--src/components/CommentForm/CommentForm.tsx150
-rw-r--r--src/components/Notice/Notice.tsx3
-rw-r--r--src/components/Spinner/Spinner.tsx11
-rw-r--r--src/i18n/en.json44
-rw-r--r--src/i18n/fr.json44
-rw-r--r--src/ts/types/app.ts2
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;