From 732d0943f8041d76262222a092b014f2557085ef Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Mon, 2 May 2022 18:57:29 +0200 Subject: chore: add homepage --- src/utils/hooks/use-settings.tsx | 112 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/utils/hooks/use-settings.tsx (limited to 'src/utils/hooks/use-settings.tsx') diff --git a/src/utils/hooks/use-settings.tsx b/src/utils/hooks/use-settings.tsx new file mode 100644 index 0000000..a45e934 --- /dev/null +++ b/src/utils/hooks/use-settings.tsx @@ -0,0 +1,112 @@ +import photo from '@assets/images/armand-philippot.jpg'; +import { settings } from '@utils/config'; +import { useRouter } from 'next/router'; + +export type BlogSettings = { + /** + * The number of posts per page. + */ + postsPerPage: number; +}; + +export type CopyrightSettings = { + /** + * The copyright end year. + */ + end: string; + /** + * The copyright start year. + */ + start: string; +}; + +export type LocaleSettings = { + /** + * The default locale. + */ + default: string; + /** + * The supported locales. + */ + supported: string[]; +}; + +export type PictureSettings = { + /** + * The picture height. + */ + height: number; + /** + * The picture url. + */ + src: string; + /** + * The picture width. + */ + width: number; +}; + +export type WebsiteSettings = { + /** + * The website name. + */ + name: string; + /** + * The website baseline. + */ + baseline: string; + /** + * The website copyright dates. + */ + copyright: CopyrightSettings; + /** + * The website locales. + */ + locales: LocaleSettings; + /** + * A picture representing the website. + */ + picture: PictureSettings; + /** + * The website url. + */ + url: string; +}; + +export type UseSettingsReturn = { + blog: BlogSettings; + website: WebsiteSettings; +}; + +/** + * Retrieve the website and blog settings. + * + * @returns {UseSettingsReturn} - An object describing settings. + */ +const useSettings = (): UseSettingsReturn => { + const { baseline, copyright, locales, name, postsPerPage, url } = settings; + const router = useRouter(); + const locale = router.locale || locales.defaultLocale; + + return { + blog: { + postsPerPage, + }, + website: { + baseline: locale.startsWith('en') ? baseline.en : baseline.fr, + copyright: { + end: copyright.endYear, + start: copyright.startYear, + }, + locales: { + default: locales.defaultLocale, + supported: locales.supported, + }, + name, + picture: photo, + url, + }, + }; +}; + +export default useSettings; -- cgit v1.2.3 From 339c6957fe92c4ec1809159f09c55201d3794c18 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 6 May 2022 18:21:16 +0200 Subject: chore: add a Contact page --- src/components/atoms/layout/notice.stories.tsx | 13 ++ src/components/atoms/layout/notice.tsx | 14 +- .../organisms/forms/contact-form.stories.tsx | 2 +- .../organisms/forms/contact-form.test.tsx | 4 +- src/components/organisms/forms/contact-form.tsx | 18 +- src/pages/contact.tsx | 181 +++++++++++++++++++++ src/services/graphql/api.ts | 26 ++- src/services/graphql/contact.mutation.ts | 25 +++ src/services/graphql/contact.ts | 26 +++ src/styles/pages/contact.module.scss | 3 + src/utils/config.ts | 1 + src/utils/hooks/use-settings.tsx | 8 +- 12 files changed, 305 insertions(+), 16 deletions(-) create mode 100644 src/pages/contact.tsx create mode 100644 src/services/graphql/contact.mutation.ts create mode 100644 src/services/graphql/contact.ts create mode 100644 src/styles/pages/contact.module.scss (limited to 'src/utils/hooks/use-settings.tsx') diff --git a/src/components/atoms/layout/notice.stories.tsx b/src/components/atoms/layout/notice.stories.tsx index 62f4cba..dedf834 100644 --- a/src/components/atoms/layout/notice.stories.tsx +++ b/src/components/atoms/layout/notice.stories.tsx @@ -8,6 +8,19 @@ export default { title: 'Atoms/Layout/Notice', component: NoticeComponent, argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the notice wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, kind: { control: { type: 'select', diff --git a/src/components/atoms/layout/notice.tsx b/src/components/atoms/layout/notice.tsx index 115bd9c..a0d1d3e 100644 --- a/src/components/atoms/layout/notice.tsx +++ b/src/components/atoms/layout/notice.tsx @@ -4,6 +4,10 @@ import styles from './notice.module.scss'; export type NoticeKind = 'error' | 'info' | 'success' | 'warning'; export type NoticeProps = { + /** + * Set additional classnames to the notice wrapper. + */ + className?: string; /** * The notice kind. */ @@ -19,11 +23,15 @@ export type NoticeProps = { * * Render a colored message depending on notice kind. */ -const Notice: FC = ({ kind, message }) => { +const Notice: FC = ({ className = '', kind, message }) => { const kindClass = `wrapper--${kind}`; - return ( -
{message}
+ return message ? ( +
+ {message} +
+ ) : ( + <> ); }; diff --git a/src/components/organisms/forms/contact-form.stories.tsx b/src/components/organisms/forms/contact-form.stories.tsx index 9b936f9..39d0b71 100644 --- a/src/components/organisms/forms/contact-form.stories.tsx +++ b/src/components/organisms/forms/contact-form.stories.tsx @@ -64,7 +64,7 @@ const Template: ComponentStory = (args) => ( */ export const Contact = Template.bind({}); Contact.args = { - sendMail: (reset: () => void) => { + sendMail: async (_data, reset: () => void) => { reset(); }, }; diff --git a/src/components/organisms/forms/contact-form.test.tsx b/src/components/organisms/forms/contact-form.test.tsx index 744f147..6225fa9 100644 --- a/src/components/organisms/forms/contact-form.test.tsx +++ b/src/components/organisms/forms/contact-form.test.tsx @@ -2,7 +2,9 @@ import { render, screen } from '@test-utils'; import ContactForm from './contact-form'; const props = { - sendMail: () => null, + sendMail: async () => { + /** Do nothing. */ + }, }; describe('ContactForm', () => { diff --git a/src/components/organisms/forms/contact-form.tsx b/src/components/organisms/forms/contact-form.tsx index 4a6902b..912402c 100644 --- a/src/components/organisms/forms/contact-form.tsx +++ b/src/components/organisms/forms/contact-form.tsx @@ -6,6 +6,13 @@ import { FC, ReactNode, useState } from 'react'; import { useIntl } from 'react-intl'; import styles from './contact-form.module.scss'; +export type ContactFormData = { + email: string; + message: string; + name: string; + subject: string; +}; + export type ContactFormProps = { /** * Set additional classnames to the form wrapper. @@ -16,10 +23,9 @@ export type ContactFormProps = { */ Notice?: ReactNode; /** - * A callback function to send mail. It takes a function as parameter to - * reset the form. + * A callback function to send mail. */ - sendMail: (reset: () => void) => void; + sendMail: (data: ContactFormData, reset: () => void) => Promise; }; /** @@ -80,9 +86,11 @@ const ContactForm: FC = ({ id: 'yN5P+m', }); - const submitHandler = () => { + const submitHandler = async () => { setIsSubmitting(true); - sendMail(resetForm); + sendMail({ email, message, name, subject: object }, resetForm).then(() => + setIsSubmitting(false) + ); }; return ( diff --git a/src/pages/contact.tsx b/src/pages/contact.tsx new file mode 100644 index 0000000..d88a8a1 --- /dev/null +++ b/src/pages/contact.tsx @@ -0,0 +1,181 @@ +import Notice, { NoticeKind } from '@components/atoms/layout/notice'; +import { BreadcrumbItem } from '@components/molecules/nav/breadcrumb'; +import ContactForm, { + ContactFormProps, +} from '@components/organisms/forms/contact-form'; +import SocialMedia from '@components/organisms/widgets/social-media'; +import PageLayout from '@components/templates/page/page-layout'; +import { meta } from '@content/pages/contact.mdx'; +import styles from '@styles/pages/contact.module.scss'; +import { sendMail } from '@services/graphql/contact'; +import { loadTranslation } from '@utils/helpers/i18n'; +import useSettings from '@utils/hooks/use-settings'; +import { GetStaticProps, NextPage } from 'next'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import Script from 'next/script'; +import { useState } from 'react'; +import { useIntl } from 'react-intl'; +import { ContactPage as ContactPageSchema, Graph, WebPage } from 'schema-dts'; + +const ContactPage: NextPage = () => { + const { dates, intro, seo, title } = meta; + const intl = useIntl(); + const homeLabel = intl.formatMessage({ + defaultMessage: 'Home', + description: 'Breadcrumb: home label', + id: 'j5k9Fe', + }); + const breadcrumb: BreadcrumbItem[] = [ + { id: 'home', name: homeLabel, url: '/' }, + { id: 'contact', name: title, url: '/contact' }, + ]; + + const socialMediaTitle = intl.formatMessage({ + defaultMessage: 'Find me elsewhere', + description: 'ContactPage: social media widget title', + id: 'Qh2CwH', + }); + + const { website } = useSettings(); + const { asPath } = useRouter(); + const pageUrl = `${website.url}${asPath}`; + const pagePublicationDate = new Date(dates.publication); + const pageUpdateDate = new Date(dates.update); + + const webpageSchema: WebPage = { + '@id': `${pageUrl}`, + '@type': 'WebPage', + breadcrumb: { '@id': `${website.url}/#breadcrumb` }, + name: seo.title, + description: seo.description, + reviewedBy: { '@id': `${website.url}/#branding` }, + url: `${pageUrl}`, + isPartOf: { + '@id': `${website.url}`, + }, + }; + + const contactSchema: ContactPageSchema = { + '@id': `${website.url}/#contact`, + '@type': 'ContactPage', + name: title, + description: intro, + author: { '@id': `${website.url}/#branding` }, + creator: { '@id': `${website.url}/#branding` }, + dateCreated: pagePublicationDate.toISOString(), + dateModified: pageUpdateDate.toISOString(), + datePublished: pagePublicationDate.toISOString(), + editor: { '@id': `${website.url}/#branding` }, + inLanguage: website.locales.default, + license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr', + mainEntityOfPage: { '@id': `${pageUrl}` }, + }; + + const schemaJsonLd: Graph = { + '@context': 'https://schema.org', + '@graph': [webpageSchema, contactSchema], + }; + + const widgets = [ + , + ]; + + const [status, setStatus] = useState('info'); + const [statusMessage, setStatusMessage] = useState(''); + + const submitMail: ContactFormProps['sendMail'] = async (data, reset) => { + const { email, message, name, subject } = data; + const messageHTML = message.replace(/\r?\n/g, '
'); + const body = `Message received from ${name} <${email}> on ${website.url}.

${messageHTML}`; + const replyTo = `${name} <${email}>`; + const mailData = { + body, + clientMutationId: 'contact', + replyTo, + subject, + }; + const { message: mutationMessage, sent } = await sendMail(mailData); + + if (sent) { + setStatus('success'); + setStatusMessage( + intl.formatMessage({ + defaultMessage: + 'Thanks. Your message was successfully sent. I will answer it as soon as possible.', + description: 'Contact: success message', + id: '3Pipok', + }) + ); + reset(); + } else { + const errorPrefix = intl.formatMessage({ + defaultMessage: 'An error occurred:', + description: 'Contact: error message', + id: 'TpyFZ6', + }); + const error = `${errorPrefix} ${mutationMessage}`; + setStatus('error'); + setStatusMessage(error); + } + }; + + return ( + <> + + {`${seo.title} - ${website.name}`} + + + + + + +