diff options
21 files changed, 295 insertions, 238 deletions
diff --git a/src/components/organisms/forms/search-form/search-form.module.scss b/src/components/organisms/forms/search-form/search-form.module.scss index 1315fde..db247a2 100644 --- a/src/components/organisms/forms/search-form/search-form.module.scss +++ b/src/components/organisms/forms/search-form/search-form.module.scss @@ -1,57 +1,60 @@ @use "../../../../styles/abstracts/functions" as fun; -@use "../../../../styles/abstracts/mixins" as mix; -.wrapper { - display: flex; - align-items: center; - position: relative; +.input { + border-right: none; +} - @include mix.media("screen") { - @include mix.dimensions("sm") { - max-width: 35ch; - } - } +.icon { + transform: scale(0.85); + transition: all 0.3s ease-in-out 0s; } .btn { - align-self: stretch; background: var(--color-bg-tertiary); border: fun.convert-px(2) solid var(--color-border); - border-left: none; box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 var(--color-shadow); transition: all 0.25s linear 0s; - &__icon { - transform: scale(0.85); - transition: all 0.3s ease-in-out 0s; + &:hover, + &:focus { + .icon { + transform: scale(0.85) rotate(20deg) translateX(#{fun.convert-px(2)}) + translateY(#{fun.convert-px(3)}); + } } &:focus { - outline: var(--color-primary-light) solid fun.convert-px(3); + outline: none; + border-color: var(--color-primary); + box-shadow: fun.convert-px(3) fun.convert-px(3) 0 0 + var(--color-primary-dark); } &:active { - outline: none; + .icon { + transform: scale(0.7); + } } +} + +.wrapper { + display: flex; - &:hover &, - &:focus & { - &__icon { - transform: scale(0.85) rotate(20deg) translateY(#{fun.convert-px(3)}); + &--no-label { + .btn { + align-self: stretch; } } - &:active & { - &__icon { - transform: scale(0.7); + &--has-label { + .btn { + align-self: flex-end; + height: fun.convert-px(52); } } } .field { - min-width: 0; - width: 100%; - &:focus-within ~ .btn { background: var(--color-bg); border-color: var(--color-primary); @@ -66,5 +69,10 @@ box-shadow: fun.convert-px(5) fun.convert-px(5) 0 fun.convert-px(1) var(--color-shadow); transform: translate(fun.convert-px(-3), fun.convert-px(-3)); + + &:focus { + box-shadow: fun.convert-px(5) fun.convert-px(5) 0 fun.convert-px(1) + var(--color-primary-dark); + } } } diff --git a/src/components/organisms/forms/search-form/search-form.stories.tsx b/src/components/organisms/forms/search-form/search-form.stories.tsx index 971a8ee..d8e4339 100644 --- a/src/components/organisms/forms/search-form/search-form.stories.tsx +++ b/src/components/organisms/forms/search-form/search-form.stories.tsx @@ -5,26 +5,9 @@ import { SearchForm } from './search-form'; * SearchForm - Storybook Meta */ export default { - title: 'Organisms/Forms', + title: 'Organisms/Forms/Search', component: SearchForm, - args: { - isLabelHidden: false, - searchPage: '#', - }, argTypes: { - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the form wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, isLabelHidden: { control: { type: 'boolean', @@ -39,16 +22,6 @@ export default { required: false, }, }, - searchPage: { - control: { - type: 'text', - }, - description: 'The search results page url.', - type: { - name: 'string', - required: true, - }, - }, }, } as ComponentMeta<typeof SearchForm>; @@ -57,9 +30,17 @@ const Template: ComponentStory<typeof SearchForm> = (args) => ( ); /** - * Forms Stories - Search + * SearchForm Stories - Default + */ +export const Default = Template.bind({}); +Default.args = { + isLabelHidden: false, +}; + +/** + * SearchForm Stories - With hidden label */ -export const Search = Template.bind({}); -Search.args = { +export const WithHiddenLabel = Template.bind({}); +WithHiddenLabel.args = { isLabelHidden: true, }; diff --git a/src/components/organisms/forms/search-form/search-form.test.tsx b/src/components/organisms/forms/search-form/search-form.test.tsx index 908a8eb..56ba0d7 100644 --- a/src/components/organisms/forms/search-form/search-form.test.tsx +++ b/src/components/organisms/forms/search-form/search-form.test.tsx @@ -1,19 +1,39 @@ import { describe, expect, it } from '@jest/globals'; +import { userEvent } from '@testing-library/user-event'; import { render, screen as rtlScreen } from '../../../../../tests/utils'; import { SearchForm } from './search-form'; describe('SearchForm', () => { - it('renders a search input', () => { - render(<SearchForm searchPage="#" />); + it('renders a search input with a submit button', () => { + render(<SearchForm />); + expect( rtlScreen.getByRole('searchbox', { name: 'Search for:' }) ).toBeInTheDocument(); - }); - - it('renders a submit button', () => { - render(<SearchForm searchPage="#" />); expect( rtlScreen.getByRole('button', { name: 'Search' }) ).toBeInTheDocument(); }); + + it('can submit the form', async () => { + const onSubmit = jest.fn((_search: { query?: string }) => undefined); + const user = userEvent.setup(); + const query = 'autem voluptatum eos'; + + render(<SearchForm onSubmit={onSubmit} />); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(3); + + expect(onSubmit).not.toHaveBeenCalled(); + + await user.type( + rtlScreen.getByRole('searchbox', { name: 'Search for:' }), + query + ); + await user.click(rtlScreen.getByRole('button', { name: 'Search' })); + + expect(onSubmit).toHaveBeenCalledTimes(1); + expect(onSubmit).toHaveBeenCalledWith({ query }); + }); }); diff --git a/src/components/organisms/forms/search-form/search-form.tsx b/src/components/organisms/forms/search-form/search-form.tsx index 5c685c0..3f16ad0 100644 --- a/src/components/organisms/forms/search-form/search-form.tsx +++ b/src/components/organisms/forms/search-form/search-form.tsx @@ -1,20 +1,22 @@ -import { useRouter } from 'next/router'; -import { - type ChangeEvent, - type FormEvent, - forwardRef, - type ForwardRefRenderFunction, - useId, - useState, - useCallback, -} from 'react'; +import { forwardRef, type ForwardRefRenderFunction, useId } from 'react'; import { useIntl } from 'react-intl'; -import { Button, Form, Icon, Input, Label } from '../../../atoms'; +import { type FormSubmitHandler, useForm } from '../../../../utils/hooks'; +import { + Button, + Form, + type FormProps, + Icon, + Input, + Label, +} from '../../../atoms'; import { LabelledField } from '../../../molecules'; import styles from './search-form.module.scss'; -export type SearchFormProps = { - className?: string; +export type SearchFormData = { query: string }; + +export type SearchFormSubmit = FormSubmitHandler<SearchFormData>; + +export type SearchFormProps = Omit<FormProps, 'children' | 'onSubmit'> & { /** * Should the label be visually hidden? * @@ -22,75 +24,80 @@ export type SearchFormProps = { */ isLabelHidden?: boolean; /** - * The search page url. + * A callback function to handle search form submit. */ - searchPage: string; + onSubmit?: SearchFormSubmit; }; const SearchFormWithRef: ForwardRefRenderFunction< HTMLInputElement, SearchFormProps -> = ({ className = '', isLabelHidden = false, searchPage }, ref) => { +> = ({ className = '', isLabelHidden = false, onSubmit, ...props }, ref) => { const intl = useIntl(); - const fieldLabel = intl.formatMessage({ - defaultMessage: 'Search for:', - description: 'SearchForm: field accessible label', - id: 'X8oujO', - }); - const buttonLabel = intl.formatMessage({ - defaultMessage: 'Search', - description: 'SearchForm: button accessible name', - id: 'WMqQrv', + const { values, submit, submitStatus, update } = useForm<SearchFormData>({ + initialValues: { query: '' }, + submitHandler: onSubmit, }); - - const router = useRouter(); - const [value, setValue] = useState<string>(''); - - const submitHandler = useCallback( - (e: FormEvent) => { - e.preventDefault(); - router.push({ pathname: searchPage, query: { s: value } }); - setValue(''); - }, - [router, searchPage, value] - ); - - const updateForm = useCallback((e: ChangeEvent<HTMLInputElement>) => { - setValue(e.target.value); - }, []); - const id = useId(); - const formClass = `${styles.wrapper} ${className}`; + const formClass = [ + styles.wrapper, + styles[isLabelHidden ? 'wrapper--no-label' : 'wrapper--has-label'], + className, + ].join(' '); + const labels = { + button: intl.formatMessage({ + defaultMessage: 'Search', + description: 'SearchForm: button accessible name', + id: 'WMqQrv', + }), + field: intl.formatMessage({ + defaultMessage: 'Search for:', + description: 'SearchForm: field accessible label', + id: 'X8oujO', + }), + }; return ( - <Form className={formClass} onSubmit={submitHandler}> + <Form {...props} className={formClass} onSubmit={submit}> <LabelledField className={styles.field} field={ <Input - className={styles.field} - id={`search-form-${id}`} - name="search-form" - onChange={updateForm} + className={styles.input} + id={id} + // eslint-disable-next-line react/jsx-no-literals + name="query" + onChange={update} ref={ref} + // eslint-disable-next-line react/jsx-no-literals type="search" - value={value} + value={values.query} /> } label={ - <Label htmlFor={`search-form-${id}`} isHidden={isLabelHidden}> - {fieldLabel} + <Label htmlFor={id} isHidden={isLabelHidden}> + {labels.field} </Label> } /> <Button - aria-label={buttonLabel} + aria-label={labels.button} className={styles.btn} + isLoading={submitStatus === 'PENDING'} + // eslint-disable-next-line react/jsx-no-literals kind="neutral" + // eslint-disable-next-line react/jsx-no-literals shape="initial" type="submit" > - <Icon className={styles.btn__icon} shape="magnifying-glass" /> + <Icon + aria-hidden + className={styles.icon} + // eslint-disable-next-line react/jsx-no-literals + shape="magnifying-glass" + // eslint-disable-next-line react/jsx-no-literals + size="lg" + /> </Button> </Form> ); diff --git a/src/components/organisms/layout/no-results.stories.tsx b/src/components/organisms/layout/no-results.stories.tsx index b157572..cfcee83 100644 --- a/src/components/organisms/layout/no-results.stories.tsx +++ b/src/components/organisms/layout/no-results.stories.tsx @@ -1,21 +1,10 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { NoResults as NoResultsComponent } from './no-results'; export default { title: 'Organisms/Layout', component: NoResultsComponent, - argTypes: { - searchPage: { - control: { - type: 'text', - }, - description: 'The search results page.', - type: { - name: 'string', - required: true, - }, - }, - }, + argTypes: {}, } as ComponentMeta<typeof NoResultsComponent>; const Template: ComponentStory<typeof NoResultsComponent> = (args) => ( @@ -23,6 +12,4 @@ const Template: ComponentStory<typeof NoResultsComponent> = (args) => ( ); export const NoResults = Template.bind({}); -NoResults.args = { - searchPage: '#', -}; +NoResults.args = {}; diff --git a/src/components/organisms/layout/no-results.test.tsx b/src/components/organisms/layout/no-results.test.tsx index 85f60cf..fdd86f7 100644 --- a/src/components/organisms/layout/no-results.test.tsx +++ b/src/components/organisms/layout/no-results.test.tsx @@ -1,15 +1,12 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; import { NoResults } from './no-results'; describe('NoResults', () => { - it('renders a no results text', () => { - render(<NoResults searchPage="#" />); - expect(screen.getByText(/No results/i)).toBeInTheDocument(); - }); + it('renders a text with a form', () => { + render(<NoResults />); - it('renders a search form', () => { - render(<NoResults searchPage="#" />); - expect(screen.getByRole('searchbox')).toBeInTheDocument(); + expect(rtlScreen.getByText(/No results/i)).toBeInTheDocument(); + expect(rtlScreen.getByRole('searchbox')).toBeInTheDocument(); }); }); diff --git a/src/components/organisms/layout/no-results.tsx b/src/components/organisms/layout/no-results.tsx index b2acf12..f760616 100644 --- a/src/components/organisms/layout/no-results.tsx +++ b/src/components/organisms/layout/no-results.tsx @@ -1,16 +1,37 @@ -import { FC } from 'react'; +import { useRouter } from 'next/router'; +import { type FC, useCallback } from 'react'; import { useIntl } from 'react-intl'; -import { SearchForm, type SearchFormProps } from '../forms'; - -export type NoResultsProps = Pick<SearchFormProps, 'searchPage'>; +import { ROUTES } from '../../../utils/constants'; +import { SearchForm, type SearchFormSubmit } from '../forms'; /** * NoResults component * * Renders a no results text with a search form. */ -export const NoResults: FC<NoResultsProps> = ({ searchPage }) => { +export const NoResults: FC = () => { const intl = useIntl(); + const router = useRouter(); + const searchSubmitHandler: SearchFormSubmit = useCallback( + ({ query }) => { + if (!query) + return { + messages: { + error: intl.formatMessage({ + defaultMessage: 'Query must be longer than one character.', + description: 'NoResults: invalid query message', + id: 'VkfO7t', + }), + }, + validator: (value) => value.query.length > 1, + }; + + router.push({ pathname: ROUTES.SEARCH, query: { s: query } }); + + return undefined; + }, + [intl, router] + ); return ( <> @@ -28,7 +49,7 @@ export const NoResults: FC<NoResultsProps> = ({ searchPage }) => { id: 'DVBwfu', })} </p> - <SearchForm isLabelHidden searchPage={searchPage} /> + <SearchForm isLabelHidden onSubmit={searchSubmitHandler} /> </> ); }; diff --git a/src/components/organisms/layout/posts-list.fixture.ts b/src/components/organisms/layout/posts-list.fixture.ts index 6109411..dfb0d97 100644 --- a/src/components/organisms/layout/posts-list.fixture.ts +++ b/src/components/organisms/layout/posts-list.fixture.ts @@ -59,5 +59,3 @@ export const posts: Post[] = [ url: '#', }, ]; - -export const searchPage = '#'; diff --git a/src/components/organisms/layout/posts-list.stories.tsx b/src/components/organisms/layout/posts-list.stories.tsx index d56c7e6..b5af1d3 100644 --- a/src/components/organisms/layout/posts-list.stories.tsx +++ b/src/components/organisms/layout/posts-list.stories.tsx @@ -1,6 +1,6 @@ -import { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { PostsList } from './posts-list'; -import { posts, searchPage } from './posts-list.fixture'; +import { posts } from './posts-list.fixture'; /** * PostsList - Storybook Meta @@ -161,7 +161,6 @@ const Template: ComponentStory<typeof PostsList> = (args) => ( export const Default = Template.bind({}); Default.args = { posts, - searchPage, total: posts.length, }; @@ -172,7 +171,6 @@ export const ByYears = Template.bind({}); ByYears.args = { posts, byYear: true, - searchPage, total: posts.length, }; ByYears.decorators = [ @@ -189,6 +187,5 @@ ByYears.decorators = [ export const NoResults = Template.bind({}); NoResults.args = { posts: [], - searchPage, total: posts.length, }; diff --git a/src/components/organisms/layout/posts-list.test.tsx b/src/components/organisms/layout/posts-list.test.tsx index d5273fc..fabf31f 100644 --- a/src/components/organisms/layout/posts-list.test.tsx +++ b/src/components/organisms/layout/posts-list.test.tsx @@ -1,47 +1,31 @@ import { describe, expect, it } from '@jest/globals'; -import { render, screen } from '../../../../tests/utils'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; import { PostsList } from './posts-list'; -import { posts, searchPage } from './posts-list.fixture'; +import { posts } from './posts-list.fixture'; describe('PostsList', () => { it('renders the correct number of posts', () => { - render( - <PostsList posts={posts} total={posts.length} searchPage={searchPage} /> - ); - expect(screen.getAllByRole('article')).toHaveLength(posts.length); + render(<PostsList posts={posts} total={posts.length} />); + expect(rtlScreen.getAllByRole('article')).toHaveLength(posts.length); }); it('renders the number of loaded posts', () => { - render( - <PostsList posts={posts} total={posts.length} searchPage={searchPage} /> - ); + render(<PostsList posts={posts} total={posts.length} />); const info = `${posts.length} loaded articles out of a total of ${posts.length}`; - expect(screen.getByText(info)).toBeInTheDocument(); + expect(rtlScreen.getByText(info)).toBeInTheDocument(); }); it('renders a load more button', () => { render( - <PostsList - posts={posts} - total={posts.length} - showLoadMoreBtn={true} - searchPage={searchPage} - /> + <PostsList posts={posts} total={posts.length} showLoadMoreBtn={true} /> ); expect( - screen.getByRole('button', { name: /Load more/i }) + rtlScreen.getByRole('button', { name: /Load more/i }) ).toBeInTheDocument(); }); it('renders a search form if no results', () => { - render( - <PostsList - posts={[]} - total={0} - showLoadMoreBtn={true} - searchPage={searchPage} - /> - ); - expect(screen.getByRole('searchbox')).toBeInTheDocument(); + render(<PostsList posts={[]} total={0} showLoadMoreBtn={true} />); + expect(rtlScreen.getByRole('searchbox')).toBeInTheDocument(); }); }); diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx index 30beb50..36d3c87 100644 --- a/src/components/organisms/layout/posts-list.tsx +++ b/src/components/organisms/layout/posts-list.tsx @@ -17,7 +17,7 @@ import { type RenderPaginationItemAriaLabel, type RenderPaginationLink, } from '../nav'; -import { NoResults, type NoResultsProps } from './no-results'; +import { NoResults } from './no-results'; import styles from './posts-list.module.scss'; import { Summary, type SummaryProps } from './summary'; @@ -30,45 +30,44 @@ export type Post = Omit<SummaryProps, 'titleLevel'> & { export type YearCollection = Record<string, Post[]>; -export type PostsListProps = Pick<PaginationProps, 'siblings'> & - Pick<NoResultsProps, 'searchPage'> & { - /** - * The pagination base url. - */ - baseUrl?: string; - /** - * True to display the posts by year. Default: false. - */ - byYear?: boolean; - /** - * Determine if the data is loading. - */ - isLoading?: boolean; - /** - * Load more button handler. - */ - loadMore?: () => void; - /** - * The current page number. Default: 1. - */ - pageNumber?: number; - /** - * The posts data. - */ - posts: Post[]; - /** - * Determine if the load more button should be visible. - */ - showLoadMoreBtn?: boolean; - /** - * The posts heading level (hn). - */ - titleLevel?: HeadingLevel; - /** - * The total posts number. - */ - total: number; - }; +export type PostsListProps = Pick<PaginationProps, 'siblings'> & { + /** + * The pagination base url. + */ + baseUrl?: string; + /** + * True to display the posts by year. Default: false. + */ + byYear?: boolean; + /** + * Determine if the data is loading. + */ + isLoading?: boolean; + /** + * Load more button handler. + */ + loadMore?: () => void; + /** + * The current page number. Default: 1. + */ + pageNumber?: number; + /** + * The posts data. + */ + posts: Post[]; + /** + * Determine if the load more button should be visible. + */ + showLoadMoreBtn?: boolean; + /** + * The posts heading level (hn). + */ + titleLevel?: HeadingLevel; + /** + * The total posts number. + */ + total: number; +}; /** * Create a collection of posts sorted by year. @@ -101,7 +100,6 @@ export const PostsList: FC<PostsListProps> = ({ loadMore, pageNumber = 1, posts, - searchPage, showLoadMoreBtn = false, siblings, titleLevel, @@ -305,7 +303,7 @@ export const PostsList: FC<PostsListProps> = ({ ); }; - if (posts.length === 0) return <NoResults searchPage={searchPage} />; + if (posts.length === 0) return <NoResults />; return ( <> diff --git a/src/components/templates/layout/layout.tsx b/src/components/templates/layout/layout.tsx index cdbb414..8332ba4 100644 --- a/src/components/templates/layout/layout.tsx +++ b/src/components/templates/layout/layout.tsx @@ -1,5 +1,6 @@ /* eslint-disable max-statements */ import NextImage from 'next/image'; +import { useRouter } from 'next/router'; import Script from 'next/script'; import { type FC, @@ -46,6 +47,7 @@ import { SearchForm, SettingsForm, type NavbarItems, + type SearchFormSubmit, } from '../../organisms'; import styles from './layout.module.scss'; @@ -85,6 +87,7 @@ export const Layout: FC<LayoutProps> = ({ isHome, useGrid = false, }) => { + const router = useRouter(); const intl = useIntl(); const { website } = useSettings(); const { baseline, copyright, locales, name, url } = website; @@ -249,6 +252,26 @@ export const Layout: FC<LayoutProps> = ({ condition: () => isSearchOpen, delay: 360, }); + const searchSubmitHandler: SearchFormSubmit = useCallback( + ({ query }) => { + if (!query) + return { + messages: { + error: intl.formatMessage({ + defaultMessage: 'Query must be longer than one character.', + description: 'Layout: invalid query message', + id: 'C2YcUJ', + }), + }, + validator: (value) => value.query.length > 1, + }; + + router.push({ pathname: ROUTES.SEARCH, query: { s: query } }); + + return undefined; + }, + [intl, router] + ); useRouteChange(deactivateSearch); @@ -268,8 +291,8 @@ export const Layout: FC<LayoutProps> = ({ <SearchForm className={styles.search} isLabelHidden + onSubmit={searchSubmitHandler} ref={searchInputRef} - searchPage={ROUTES.SEARCH} /> ), icon: 'magnifying-glass', diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx index 7977382..05b47da 100644 --- a/src/components/templates/page/page-layout.stories.tsx +++ b/src/components/templates/page/page-layout.stories.tsx @@ -373,14 +373,7 @@ Blog.args = { breadcrumb: postsListBreadcrumb, title: 'Blog', headerMeta: [{ id: 'total', label: 'Total:', value: `${posts.length}` }], - children: ( - <PostsList - posts={posts} - byYear={true} - total={posts.length} - searchPage="#" - /> - ), + children: <PostsList posts={posts} byYear={true} total={posts.length} />, widgets: [ <LinksListWidget heading={ diff --git a/src/i18n/en.json b/src/i18n/en.json index 4b7e756..4af656b 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -195,6 +195,14 @@ "defaultMessage": "Failed to load.", "description": "BlogPage: failed to load text" }, + "C2YcUJ": { + "defaultMessage": "Query must be longer than one character.", + "description": "Layout: invalid query message" + }, + "C6oK7h": { + "defaultMessage": "Query must be longer than one character.", + "description": "404Page: invalid query message" + }, "CvOqoh": { "defaultMessage": "Thematics:", "description": "ArticlePage: thematics meta label" @@ -411,6 +419,10 @@ "defaultMessage": "Send", "description": "ContactForm: send button" }, + "VkfO7t": { + "defaultMessage": "Query must be longer than one character.", + "description": "NoResults: invalid query message" + }, "Vmj5cw": { "defaultMessage": "It is now awaiting moderation.", "description": "PageLayout: comment awaiting moderation" diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 0934548..054068e 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -195,6 +195,14 @@ "defaultMessage": "Échec du chargement.", "description": "BlogPage: failed to load text" }, + "C2YcUJ": { + "defaultMessage": "Les mots-clés doivent être plus longs qu'un caractère.", + "description": "Layout: invalid query message" + }, + "C6oK7h": { + "defaultMessage": "Les mots-clés doivent être plus longs qu'un caractère.", + "description": "404Page: invalid query message" + }, "CvOqoh": { "defaultMessage": "Thématiques :", "description": "ArticlePage: thematics meta label" @@ -411,6 +419,10 @@ "defaultMessage": "Envoyer", "description": "ContactForm: send button" }, + "VkfO7t": { + "defaultMessage": "Les mots-clés doivent être plus longs qu'un caractère.", + "description": "NoResults: invalid query message" + }, "Vmj5cw": { "defaultMessage": "Il est maintenant en attente de modération.", "description": "PageLayout: comment awaiting moderation" diff --git a/src/pages/404.tsx b/src/pages/404.tsx index ae6eac5..00e2d5a 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -1,6 +1,8 @@ +/* eslint-disable max-statements */ import type { GetStaticProps } from 'next'; import Head from 'next/head'; -import type { ReactNode } from 'react'; +import { useRouter } from 'next/router'; +import { useCallback, type ReactNode } from 'react'; import { useIntl } from 'react-intl'; import { getLayout, @@ -9,6 +11,7 @@ import { LinksListWidget, PageLayout, SearchForm, + type SearchFormSubmit, } from '../components'; import { getThematicsPreview, @@ -39,6 +42,7 @@ const Error404Page: NextPageWithLayout<Error404PageProps> = ({ thematicsList, topicsList, }) => { + const router = useRouter(); const intl = useIntl(); const { website } = useSettings(); const title = intl.formatMessage({ @@ -85,6 +89,26 @@ const Error404Page: NextPageWithLayout<Error404PageProps> = ({ description: 'Error404Page: topics list widget title', id: 'GVpTIl', }); + const searchSubmitHandler: SearchFormSubmit = useCallback( + ({ query }) => { + if (!query) + return { + messages: { + error: intl.formatMessage({ + defaultMessage: 'Query must be longer than one character.', + description: '404Page: invalid query message', + id: 'C6oK7h', + }), + }, + validator: (value) => value.query.length > 1, + }; + + router.push({ pathname: ROUTES.SEARCH, query: { s: query } }); + + return undefined; + }, + [intl, router] + ); return ( <> @@ -134,7 +158,7 @@ const Error404Page: NextPageWithLayout<Error404PageProps> = ({ id: 'XKy7rx', })} </p> - <SearchForm isLabelHidden searchPage={ROUTES.SEARCH} /> + <SearchForm isLabelHidden onSubmit={searchSubmitHandler} /> </PageLayout> </> ); diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx index 5c64e6d..accd314 100644 --- a/src/pages/blog/index.tsx +++ b/src/pages/blog/index.tsx @@ -217,7 +217,6 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ isLoading={isLoadingMore ?? isLoadingInitialData} loadMore={loadMore} posts={getPostsList(data)} - searchPage={ROUTES.SEARCH} showLoadMoreBtn={hasNextPage} total={totalArticles} /> diff --git a/src/pages/blog/page/[number].tsx b/src/pages/blog/page/[number].tsx index 58cf7b9..1c723f1 100644 --- a/src/pages/blog/page/[number].tsx +++ b/src/pages/blog/page/[number].tsx @@ -213,7 +213,6 @@ const BlogPage: NextPageWithLayout<BlogPageProps> = ({ byYear={true} pageNumber={pageNumber} posts={getPostsList([articles])} - searchPage={ROUTES.SEARCH} total={totalArticles} /> </PageLayout> diff --git a/src/pages/recherche/index.tsx b/src/pages/recherche/index.tsx index 32312ec..12a482d 100644 --- a/src/pages/recherche/index.tsx +++ b/src/pages/recherche/index.tsx @@ -241,7 +241,6 @@ const SearchPage: NextPageWithLayout<SearchPageProps> = ({ isLoading={isLoadingMore ?? isLoadingInitialData} loadMore={loadMore} posts={getPostsList(data)} - searchPage={ROUTES.SEARCH} showLoadMoreBtn={hasNextPage} total={totalArticles ?? 0} /> diff --git a/src/pages/sujet/[slug].tsx b/src/pages/sujet/[slug].tsx index 87c3340..9094703 100644 --- a/src/pages/sujet/[slug].tsx +++ b/src/pages/sujet/[slug].tsx @@ -228,7 +228,6 @@ const TopicPage: NextPageWithLayout<TopicPageProps> = ({ baseUrl={postsListBaseUrl} byYear={true} posts={getPostsWithUrl(articles)} - searchPage={ROUTES.SEARCH} titleLevel={3} total={articles.length} /> diff --git a/src/pages/thematique/[slug].tsx b/src/pages/thematique/[slug].tsx index 8e21ff6..bb97f47 100644 --- a/src/pages/thematique/[slug].tsx +++ b/src/pages/thematique/[slug].tsx @@ -200,7 +200,6 @@ const ThematicPage: NextPageWithLayout<ThematicPageProps> = ({ baseUrl={postsListBaseUrl} byYear={true} posts={getPostsWithUrl(articles)} - searchPage={ROUTES.SEARCH} titleLevel={3} total={articles.length} /> |
