aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-09-29 11:47:06 +0200
committerArmand Philippot <git@armandphilippot.com>2023-10-24 12:25:00 +0200
commit81b1e0e05919eb368a66aef47adcf7738af76f29 (patch)
tree758577d3f58d7025f84bca5bac9bc0da8432deb7 /src
parent3272ac336da52364ace5ed76d8f609d4088ffc06 (diff)
refactor(components): rewrite Spinner component
* Message should be set as children * Default message is no longer available (depending on use case, the consumer might prefer aria-label instead) * It is now possible to define the message position
Diffstat (limited to 'src')
-rw-r--r--src/components/atoms/images/icons/svg-paths/icons-paths/arrow-icon-paths.tsx3
-rw-r--r--src/components/atoms/loaders/spinner.module.scss48
-rw-r--r--src/components/atoms/loaders/spinner.test.tsx15
-rw-r--r--src/components/atoms/loaders/spinner.tsx35
-rw-r--r--src/components/atoms/loaders/spinner/index.ts1
-rw-r--r--src/components/atoms/loaders/spinner/spinner.module.scss69
-rw-r--r--src/components/atoms/loaders/spinner/spinner.stories.tsx (renamed from src/components/atoms/loaders/spinner.stories.tsx)17
-rw-r--r--src/components/atoms/loaders/spinner/spinner.test.tsx53
-rw-r--r--src/components/atoms/loaders/spinner/spinner.tsx42
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.tsx16
-rw-r--r--src/components/organisms/forms/contact-form/contact-form.tsx115
-rw-r--r--src/components/organisms/layout/posts-list.tsx10
-rw-r--r--src/i18n/en.json20
-rw-r--r--src/i18n/fr.json20
-rw-r--r--src/pages/article/[slug].tsx7
-rw-r--r--src/pages/projets/[slug].tsx8
-rw-r--r--src/pages/recherche/index.tsx7
-rw-r--r--src/types/app.ts16
18 files changed, 312 insertions, 190 deletions
diff --git a/src/components/atoms/images/icons/svg-paths/icons-paths/arrow-icon-paths.tsx b/src/components/atoms/images/icons/svg-paths/icons-paths/arrow-icon-paths.tsx
index ee29d5d..0dc701a 100644
--- a/src/components/atoms/images/icons/svg-paths/icons-paths/arrow-icon-paths.tsx
+++ b/src/components/atoms/images/icons/svg-paths/icons-paths/arrow-icon-paths.tsx
@@ -1,7 +1,8 @@
/* eslint-disable react/jsx-no-literals */
import type { FC } from 'react';
+import type { Position } from '../../../../../types';
-export type ArrowOrientation = 'top' | 'right' | 'bottom' | 'left';
+export type ArrowOrientation = Exclude<Position, 'center'>;
const getArrowBarPathFrom = (orientation: ArrowOrientation) => {
switch (orientation) {
diff --git a/src/components/atoms/loaders/spinner.module.scss b/src/components/atoms/loaders/spinner.module.scss
deleted file mode 100644
index 3e05cb3..0000000
--- a/src/components/atoms/loaders/spinner.module.scss
+++ /dev/null
@@ -1,48 +0,0 @@
-@use "../../../styles/abstracts/functions" as fun;
-
-.wrapper {
- display: flex;
- flex-flow: row wrap;
- align-items: center;
- justify-content: center;
- gap: var(--spacing-2xs);
- margin: var(--spacing-md) 0;
-}
-
-.ball {
- width: fun.convert-px(8);
- height: fun.convert-px(8);
- background: linear-gradient(
- to right,
- var(--color-primary-light) 0%,
- var(--color-primary-lighter) 100%
- );
- border-radius: 50%;
- animation: spinner 1.4s infinite ease-in-out both;
-
- &:first-child {
- animation-delay: -0.32s;
- }
-
- &:nth-child(2) {
- animation-delay: -0.16s;
- }
-}
-
-.text {
- margin-left: var(--spacing-xs);
- color: var(--color-primary-darker);
- text-align: center;
-}
-
-@keyframes spinner {
- 0%,
- 80%,
- 100% {
- transform: scale(0);
- }
-
- 40% {
- transform: scale(1);
- }
-}
diff --git a/src/components/atoms/loaders/spinner.test.tsx b/src/components/atoms/loaders/spinner.test.tsx
deleted file mode 100644
index 553c3ef..0000000
--- a/src/components/atoms/loaders/spinner.test.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
-import { Spinner } from './spinner';
-
-describe('Spinner', () => {
- it('renders a spinner loader', () => {
- render(<Spinner />);
- expect(screen.getByText('Loading...')).toBeInTheDocument();
- });
-
- it('renders a spinner loader with a custom message', () => {
- render(<Spinner message="Submitting" />);
- expect(screen.getByText('Submitting')).toBeInTheDocument();
- });
-});
diff --git a/src/components/atoms/loaders/spinner.tsx b/src/components/atoms/loaders/spinner.tsx
deleted file mode 100644
index 968290b..0000000
--- a/src/components/atoms/loaders/spinner.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { FC } from 'react';
-import { useIntl } from 'react-intl';
-import styles from './spinner.module.scss';
-
-export type SpinnerProps = {
- /**
- * The loading message. Default: "Loading...".
- */
- message?: string;
-};
-
-/**
- * Spinner component
- *
- * Render a loading message with animation.
- */
-export const Spinner: FC<SpinnerProps> = ({ message }) => {
- const intl = useIntl();
-
- return (
- <div className={styles.wrapper}>
- <div className={styles.ball}></div>
- <div className={styles.ball}></div>
- <div className={styles.ball}></div>
- <div className={styles.text}>
- {message ??
- intl.formatMessage({
- defaultMessage: 'Loading...',
- description: 'Spinner: loading text',
- id: 'q9cJQe',
- })}
- </div>
- </div>
- );
-};
diff --git a/src/components/atoms/loaders/spinner/index.ts b/src/components/atoms/loaders/spinner/index.ts
new file mode 100644
index 0000000..cd17217
--- /dev/null
+++ b/src/components/atoms/loaders/spinner/index.ts
@@ -0,0 +1 @@
+export * from './spinner';
diff --git a/src/components/atoms/loaders/spinner/spinner.module.scss b/src/components/atoms/loaders/spinner/spinner.module.scss
new file mode 100644
index 0000000..97882a4
--- /dev/null
+++ b/src/components/atoms/loaders/spinner/spinner.module.scss
@@ -0,0 +1,69 @@
+@use "../../../../styles/abstracts/functions" as fun;
+
+.wrapper {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-xs);
+ width: fit-content;
+
+ &--left {
+ flex-flow: row-reverse wrap;
+ }
+
+ &--right {
+ flex-flow: row wrap;
+ }
+
+ &--bottom {
+ flex-flow: column nowrap;
+ }
+
+ &--top {
+ flex-flow: column-reverse nowrap;
+ }
+}
+
+.icon {
+ --ball-size: #{fun.convert-px(8)};
+
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: space-between;
+ width: calc((var(--ball-size) * 3) + var(--spacing-xs));
+
+ &__ball {
+ width: var(--ball-size);
+ height: var(--ball-size);
+ background: linear-gradient(
+ to right,
+ var(--color-primary-light) 0%,
+ var(--color-primary-lighter) 100%
+ );
+ border-radius: 50%;
+ animation: spinner 1.4s infinite ease-in-out both;
+
+ &:first-child {
+ animation-delay: -0.32s;
+ }
+
+ &:nth-child(2) {
+ animation-delay: -0.16s;
+ }
+ }
+}
+
+.body {
+ color: var(--color-primary-darker);
+}
+
+@keyframes spinner {
+ 0%,
+ 80%,
+ 100% {
+ transform: scale(0);
+ }
+
+ 40% {
+ transform: scale(1);
+ }
+}
diff --git a/src/components/atoms/loaders/spinner.stories.tsx b/src/components/atoms/loaders/spinner/spinner.stories.tsx
index 197d06c..e9dfae4 100644
--- a/src/components/atoms/loaders/spinner.stories.tsx
+++ b/src/components/atoms/loaders/spinner/spinner.stories.tsx
@@ -1,14 +1,14 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { Spinner as SpinnerComponent } from './spinner';
/**
* Spinner - Storybook Meta
*/
export default {
- title: 'Atoms/Loaders/Spinner',
+ title: 'Atoms/Loaders',
component: SpinnerComponent,
argTypes: {
- message: {
+ children: {
control: {
type: 'text',
},
@@ -29,14 +29,9 @@ const Template: ComponentStory<typeof SpinnerComponent> = (args) => (
);
/**
- * Loaders Stories - Default Spinner
+ * Loaders Stories - Spinner
*/
export const Spinner = Template.bind({});
-
-/**
- * Loaders Stories - Spinner with custom message
- */
-export const SpinnerCustomMessage = Template.bind({});
-SpinnerCustomMessage.args = {
- message: 'Submitting...',
+Spinner.args = {
+ children: 'Submitting...',
};
diff --git a/src/components/atoms/loaders/spinner/spinner.test.tsx b/src/components/atoms/loaders/spinner/spinner.test.tsx
new file mode 100644
index 0000000..733648b
--- /dev/null
+++ b/src/components/atoms/loaders/spinner/spinner.test.tsx
@@ -0,0 +1,53 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { Spinner } from './spinner';
+
+describe('Spinner', () => {
+ it('renders a spinner', () => {
+ const { container } = render(<Spinner />);
+ expect(container).toBeInTheDocument();
+ });
+
+ it('can render a spinner with a custom message', () => {
+ const customMsg = 'Submitting';
+
+ render(<Spinner>{customMsg}</Spinner>);
+ expect(rtlScreen.getByText(customMsg)).toBeInTheDocument();
+ });
+
+ it('can render a spinner with a custom message at the bottom', () => {
+ const customMsg = 'necessitatibus';
+
+ render(<Spinner position="bottom">{customMsg}</Spinner>);
+ expect(rtlScreen.getByText(customMsg).parentElement).toHaveClass(
+ 'wrapper--bottom'
+ );
+ });
+
+ it('can render a spinner with a custom message on the left', () => {
+ const customMsg = 'eos';
+
+ render(<Spinner position="left">{customMsg}</Spinner>);
+ expect(rtlScreen.getByText(customMsg).parentElement).toHaveClass(
+ 'wrapper--left'
+ );
+ });
+
+ it('can render a spinner with a custom message on the right', () => {
+ const customMsg = 'neque';
+
+ render(<Spinner position="right">{customMsg}</Spinner>);
+ expect(rtlScreen.getByText(customMsg).parentElement).toHaveClass(
+ 'wrapper--right'
+ );
+ });
+
+ it('can render a spinner with a custom message on the top', () => {
+ const customMsg = 'vero';
+
+ render(<Spinner position="top">{customMsg}</Spinner>);
+ expect(rtlScreen.getByText(customMsg).parentElement).toHaveClass(
+ 'wrapper--top'
+ );
+ });
+});
diff --git a/src/components/atoms/loaders/spinner/spinner.tsx b/src/components/atoms/loaders/spinner/spinner.tsx
new file mode 100644
index 0000000..6c6c23c
--- /dev/null
+++ b/src/components/atoms/loaders/spinner/spinner.tsx
@@ -0,0 +1,42 @@
+import type { FC, HTMLAttributes, ReactNode } from 'react';
+import type { Position } from '../../../../types';
+import styles from './spinner.module.scss';
+
+export type SpinnerProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
+ /**
+ * The loading message.
+ */
+ children?: ReactNode;
+ /**
+ * Define the position of the loading message if any.
+ *
+ * @default 'right'
+ */
+ position?: Exclude<Position, 'center'>;
+};
+
+/**
+ * Spinner component
+ *
+ * Render a loading message with animation.
+ */
+export const Spinner: FC<SpinnerProps> = ({
+ children,
+ className = '',
+ position = 'right',
+ ...props
+}) => {
+ const positionClass = styles[`wrapper--${position}`];
+ const wrapperClass = `${styles.wrapper} ${positionClass} ${className}`;
+
+ return (
+ <div {...props} className={wrapperClass}>
+ <div aria-hidden className={styles.icon}>
+ <div className={styles.icon__ball} />
+ <div className={styles.icon__ball} />
+ <div className={styles.icon__ball} />
+ </div>
+ <div className={styles.body}>{children}</div>
+ </div>
+ );
+};
diff --git a/src/components/organisms/forms/comment-form/comment-form.tsx b/src/components/organisms/forms/comment-form/comment-form.tsx
index e645ede..b5f2d64 100644
--- a/src/components/organisms/forms/comment-form/comment-form.tsx
+++ b/src/components/organisms/forms/comment-form/comment-form.tsx
@@ -117,6 +117,12 @@ export const CommentForm: FC<CommentFormProps> = ({
id: 'dz2kDV',
});
+ const loadingMsg = intl.formatMessage({
+ defaultMessage: 'Submitting...',
+ description: 'CommentForm: spinner message on submit',
+ id: 'IY5ew6',
+ });
+
const formAriaLabel = title ? undefined : formTitle;
const formId = useId();
const formLabelledBy = title ? formId : undefined;
@@ -246,15 +252,7 @@ export const CommentForm: FC<CommentFormProps> = ({
id: 'OL0Yzx',
})}
</Button>
- {isSubmitting ? (
- <Spinner
- message={intl.formatMessage({
- defaultMessage: 'Submitting...',
- description: 'CommentForm: spinner message on submit',
- id: 'IY5ew6',
- })}
- />
- ) : null}
+ {isSubmitting ? <Spinner>{loadingMsg}</Spinner> : null}
{Notice}
</Form>
);
diff --git a/src/components/organisms/forms/contact-form/contact-form.tsx b/src/components/organisms/forms/contact-form/contact-form.tsx
index 6208b94..89fd331 100644
--- a/src/components/organisms/forms/contact-form/contact-form.tsx
+++ b/src/components/organisms/forms/contact-form/contact-form.tsx
@@ -1,4 +1,13 @@
-import { ChangeEvent, FC, FormEvent, ReactNode, useState } from 'react';
+/* eslint-disable max-statements */
+import {
+ type ChangeEvent,
+ type FC,
+ type FormEvent,
+ type ReactNode,
+ useState,
+ useCallback,
+ useMemo,
+} from 'react';
import { useIntl } from 'react-intl';
import { Button, Form, Input, Label, Spinner, TextArea } from '../../../atoms';
import { LabelledField } from '../../../molecules';
@@ -38,51 +47,54 @@ export const ContactForm: FC<ContactFormProps> = ({
}) => {
const formClass = `${styles.form} ${className}`;
const intl = useIntl();
- const emptyForm: ContactFormData = {
- email: '',
- message: '',
- name: '',
- object: '',
- };
+ const emptyForm: ContactFormData = useMemo(() => {
+ return {
+ email: '',
+ message: '',
+ name: '',
+ object: '',
+ };
+ }, []);
const [data, setData] = useState(emptyForm);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
/**
* Reset all the form fields.
*/
- const resetForm = () => {
+ const resetForm = useCallback(() => {
setData(emptyForm);
setIsSubmitting(false);
- };
+ }, [emptyForm]);
- const updateForm = (
- e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
- ) => {
- switch (e.target.name) {
- case 'email':
- setData((prevData) => {
- return { ...prevData, email: e.target.value };
- });
- break;
- case 'message':
- setData((prevData) => {
- return { ...prevData, message: e.target.value };
- });
- break;
- case 'name':
- setData((prevData) => {
- return { ...prevData, name: e.target.value };
- });
- break;
- case 'object':
- setData((prevData) => {
- return { ...prevData, object: e.target.value };
- });
- break;
- default:
- break;
- }
- };
+ const updateForm = useCallback(
+ (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+ switch (e.target.name) {
+ case 'email':
+ setData((prevData) => {
+ return { ...prevData, email: e.target.value };
+ });
+ break;
+ case 'message':
+ setData((prevData) => {
+ return { ...prevData, message: e.target.value };
+ });
+ break;
+ case 'name':
+ setData((prevData) => {
+ return { ...prevData, name: e.target.value };
+ });
+ break;
+ case 'object':
+ setData((prevData) => {
+ return { ...prevData, object: e.target.value };
+ });
+ break;
+ default:
+ break;
+ }
+ },
+ []
+ );
const formName = intl.formatMessage({
defaultMessage: 'Contact form',
@@ -114,11 +126,20 @@ export const ContactForm: FC<ContactFormProps> = ({
id: 'yN5P+m',
});
- const submitHandler = async (e: FormEvent) => {
- e.preventDefault();
- setIsSubmitting(true);
- sendMail(data, resetForm).then(() => setIsSubmitting(false));
- };
+ const loadingMsg = intl.formatMessage({
+ defaultMessage: 'Sending mail...',
+ description: 'ContactForm: spinner message on submit',
+ id: 'xaqaYQ',
+ });
+
+ const submitHandler = useCallback(
+ async (e: FormEvent) => {
+ e.preventDefault();
+ setIsSubmitting(true);
+ await sendMail(data, resetForm).then(() => setIsSubmitting(false));
+ },
+ [data, resetForm, sendMail]
+ );
return (
<Form aria-label={formName} className={formClass} onSubmit={submitHandler}>
@@ -195,15 +216,7 @@ export const ContactForm: FC<ContactFormProps> = ({
id: 'VkAnvv',
})}
</Button>
- {isSubmitting && (
- <Spinner
- message={intl.formatMessage({
- defaultMessage: 'Sending mail...',
- description: 'ContactForm: spinner message on submit',
- id: 'xaqaYQ',
- })}
- />
- )}
+ {isSubmitting ? <Spinner>{loadingMsg}</Spinner> : null}
{Notice}
</Form>
);
diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx
index f04ba74..5401ed1 100644
--- a/src/components/organisms/layout/posts-list.tsx
+++ b/src/components/organisms/layout/posts-list.tsx
@@ -165,8 +165,14 @@ export const PostsList: FC<PostsListProps> = ({
const loadMoreBody = intl.formatMessage({
defaultMessage: 'Load more articles?',
- id: 'uaqd5F',
description: 'PostsList: load more button',
+ id: 'uaqd5F',
+ });
+
+ const loadingMoreArticles = intl.formatMessage({
+ defaultMessage: 'Loading more articles...',
+ description: 'PostsList: loading more articles message',
+ id: 'xYemkP',
});
/**
@@ -224,7 +230,7 @@ export const PostsList: FC<PostsListProps> = ({
return (
<>
{getPosts()}
- {isLoading ? <Spinner /> : null}
+ {isLoading ? <Spinner>{loadingMoreArticles}</Spinner> : null}
{isMounted ? getProgressBar() : getPagination()}
</>
);
diff --git a/src/i18n/en.json b/src/i18n/en.json
index d8a982d..2faedfa 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -83,6 +83,10 @@
"defaultMessage": "Page not found.",
"description": "404Page: SEO - Meta description"
},
+ "4iYISO": {
+ "defaultMessage": "Loading the requested article...",
+ "description": "ArticlePage: loading article message"
+ },
"50xc4o": {
"defaultMessage": "Read more articles about:",
"description": "ArticlePage: footer topics list label"
@@ -187,6 +191,10 @@
"defaultMessage": "Reading time:",
"description": "Meta: reading time label"
},
+ "EeCqAE": {
+ "defaultMessage": "Loading the search results...",
+ "description": "SearchPage: loading search results message"
+ },
"Es52wh": {
"defaultMessage": "Blog",
"description": "Breadcrumb: blog label"
@@ -307,6 +315,10 @@
"defaultMessage": "CV",
"description": "Layout: main nav - cv link"
},
+ "RwI3B9": {
+ "defaultMessage": "Loading the repository popularity...",
+ "description": "ProjectsPage: loading repository popularity"
+ },
"T4YA64": {
"defaultMessage": "Subscribe",
"description": "HomePage: RSS feed subscription text"
@@ -535,10 +547,6 @@
"defaultMessage": "Share",
"description": "Sharing: widget title"
},
- "q9cJQe": {
- "defaultMessage": "Loading...",
- "description": "Spinner: loading text"
- },
"qnwsWV": {
"defaultMessage": "Projects",
"description": "Layout: main nav - projects link"
@@ -619,6 +627,10 @@
"defaultMessage": "Settings form",
"description": "SettingsModal: an accessible form name"
},
+ "xYemkP": {
+ "defaultMessage": "Loading more articles...",
+ "description": "PostsList: loading more articles message"
+ },
"xaqaYQ": {
"defaultMessage": "Sending mail...",
"description": "ContactForm: spinner message on submit"
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index d64c930..0f79416 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -83,6 +83,10 @@
"defaultMessage": "Page non trouvée.",
"description": "404Page: SEO - Meta description"
},
+ "4iYISO": {
+ "defaultMessage": "Chargement de l'article demandé…",
+ "description": "ArticlePage: loading article message"
+ },
"50xc4o": {
"defaultMessage": "Lire plus d’articles à propos de :",
"description": "ArticlePage: footer topics list label"
@@ -187,6 +191,10 @@
"defaultMessage": "Temps de lecture :",
"description": "Meta: reading time label"
},
+ "EeCqAE": {
+ "defaultMessage": "Chargement des résultats…",
+ "description": "SearchPage: loading search results message"
+ },
"Es52wh": {
"defaultMessage": "Blog",
"description": "Breadcrumb: blog label"
@@ -307,6 +315,10 @@
"defaultMessage": "CV",
"description": "Layout: main nav - cv link"
},
+ "RwI3B9": {
+ "defaultMessage": "Chargement de la popularité du dépôt…",
+ "description": "ProjectsPage: loading repository popularity"
+ },
"T4YA64": {
"defaultMessage": "Vous abonner",
"description": "HomePage: RSS feed subscription text"
@@ -535,10 +547,6 @@
"defaultMessage": "Partager",
"description": "Sharing: widget title"
},
- "q9cJQe": {
- "defaultMessage": "Chargement…",
- "description": "Spinner: loading text"
- },
"qnwsWV": {
"defaultMessage": "Projets",
"description": "Layout: main nav - projects link"
@@ -619,6 +627,10 @@
"defaultMessage": "Formulaire des réglages",
"description": "SettingsModal: an accessible form name"
},
+ "xYemkP": {
+ "defaultMessage": "Chargement des articles précédents…",
+ "description": "PostsList: loading more articles message"
+ },
"xaqaYQ": {
"defaultMessage": "Mail en cours d’envoi…",
"description": "ContactForm: spinner message on submit"
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index 3e4c38f..523e21d 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -71,8 +71,13 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({
const { website } = useSettings();
const prismPlugins: OptionalPrismPlugin[] = ['command-line', 'line-numbers'];
const { attributes, className } = usePrism({ plugins: prismPlugins });
+ const loadingArticle = intl.formatMessage({
+ defaultMessage: 'Loading the requested article...',
+ description: 'ArticlePage: loading article message',
+ id: '4iYISO',
+ });
- if (isFallback || !article) return <Spinner />;
+ if (isFallback || !article) return <Spinner>{loadingArticle}</Spinner>;
const { content, id, intro, meta, title } = article;
const { author, commentsCount, cover, dates, seo, thematics, topics } = meta;
diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx
index afcf060..717ae13 100644
--- a/src/pages/projets/[slug].tsx
+++ b/src/pages/projets/[slug].tsx
@@ -170,6 +170,12 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
return links;
};
+ const loadingRepoPopularity = intl.formatMessage({
+ defaultMessage: 'Loading the repository popularity...',
+ description: 'ProjectsPage: loading repository popularity',
+ id: 'RwI3B9',
+ });
+
const { isError, isLoading, data } = useGithubApi(
/*
* Repo should be defined for each project so for now it is safe for my
@@ -182,7 +188,7 @@ const ProjectPage: NextPageWithLayout<ProjectPageProps> = ({ project }) => {
);
if (isError) return 'Error';
- if (isLoading || !data) return <Spinner />;
+ if (isLoading || !data) return <Spinner aria-label={loadingRepoPopularity} />;
const getRepoPopularity = (repo: string) => {
const stars = intl.formatMessage(
diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx
index 971d04a..5acf352 100644
--- a/src/pages/recherche/index.tsx
+++ b/src/pages/recherche/index.tsx
@@ -151,6 +151,11 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({
id: 'N804XO',
});
const postsListBaseUrl = `${ROUTES.SEARCH}/page/`;
+ const loadingResults = intl.formatMessage({
+ defaultMessage: 'Loading the search results...',
+ description: 'SearchPage: loading search results message',
+ id: 'EeCqAE',
+ });
return (
<>
@@ -211,7 +216,7 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({
total={totalArticles ?? 0}
/>
) : (
- <Spinner />
+ <Spinner>{loadingResults}</Spinner>
)}
{error ? (
<Notice
diff --git a/src/types/app.ts b/src/types/app.ts
index 64bb3af..e237560 100644
--- a/src/types/app.ts
+++ b/src/types/app.ts
@@ -1,7 +1,7 @@
-import { NextPage } from 'next';
-import { AppProps as NextAppProps } from 'next/app';
-import { ReactElement, ReactNode } from 'react';
-import { MessageFormatElement } from 'react-intl';
+import type { NextPage } from 'next';
+import type { AppProps as NextAppProps } from 'next/app';
+import type { ReactElement, ReactNode } from 'react';
+import type { MessageFormatElement } from 'react-intl';
export type NextPageWithLayoutOptions = {
withExtraPadding?: boolean;
@@ -9,15 +9,15 @@ export type NextPageWithLayoutOptions = {
useGrid?: boolean;
};
-export type NextPageWithLayout<T = {}> = NextPage<T> & {
+export type NextPageWithLayout<T = object> = NextPage<T> & {
getLayout?: (
page: ReactElement,
options: NextPageWithLayoutOptions
) => ReactNode;
};
-// modified version - allows for custom pageProps type, falling back to 'any'
-type AppProps<P = any> = {
+// modified version - allows custom pageProps type, falling back to 'unknown'
+type AppProps<P = unknown> = {
pageProps: P;
} & Omit<NextAppProps<P>, 'pageProps'>;
@@ -130,3 +130,5 @@ export type Topic = Page<'topic'>;
export type Slug = {
slug: string;
};
+
+export type Position = 'bottom' | 'center' | 'left' | 'right' | 'top';