From b80843756768399e436ac43704873beef3c97081 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Fri, 5 Aug 2022 21:45:03 +0300 Subject: [PATCH] feat: contact form submission --- .prettierrc.json | 2 +- src/components/Button/Button.scss | 15 ++- src/components/Button/Button.tsx | 19 +-- src/components/ContactForm/ContactForm.scss | 37 ++++-- src/components/ContactForm/ContactForm.tsx | 123 +++++++++++++++++--- src/components/ContactForm/Error.tsx | 7 ++ src/components/ContactForm/Submitted.tsx | 7 ++ src/components/ContactForm/validations.ts | 3 + src/components/Spinner/Spinner.scss | 26 +++++ src/components/Spinner/Spinner.tsx | 11 ++ src/components/Spinner/index.ts | 1 + src/hooks/index.ts | 1 + src/hooks/useForm.ts | 80 +++++++++++++ src/i18n/en/ui.ts | 21 ++++ src/i18n/ru/ui.ts | 21 ++++ src/pages/404.astro | 2 +- src/pages/about.astro | 2 +- src/pages/articles.astro | 2 +- src/pages/contacts.astro | 2 +- src/pages/index.astro | 2 +- src/pages/projects.astro | 2 +- src/styles/global.scss | 11 +- src/styles/theme.scss | 12 +- src/templates/Contacts.astro | 6 +- src/types/main.d.ts | 10 +- src/utils/string.ts | 6 +- src/utils/url.ts | 5 + tsconfig.json | 4 +- 28 files changed, 387 insertions(+), 53 deletions(-) create mode 100644 src/components/ContactForm/Error.tsx create mode 100644 src/components/ContactForm/Submitted.tsx create mode 100644 src/components/ContactForm/validations.ts create mode 100644 src/components/Spinner/Spinner.scss create mode 100644 src/components/Spinner/Spinner.tsx create mode 100644 src/components/Spinner/index.ts create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useForm.ts create mode 100644 src/i18n/en/ui.ts create mode 100644 src/i18n/ru/ui.ts create mode 100644 src/utils/url.ts diff --git a/.prettierrc.json b/.prettierrc.json index 580d103..e7407ec 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,7 +1,7 @@ { "printWidth": 150, "semi": true, - "singleQuote": true, + "singleQuote": false, "tabWidth": 2, "trailingComma": "es5", "useTabs": true diff --git a/src/components/Button/Button.scss b/src/components/Button/Button.scss index e096d48..041b783 100644 --- a/src/components/Button/Button.scss +++ b/src/components/Button/Button.scss @@ -1,10 +1,13 @@ .btn { cursor: pointer; + display: inline-flex; + column-gap: 7px; + align-items: center; font-family: var(--font-family-base); font-weight: 500; font-size: var(--font-size-md); color: var(--font-color); - padding: 4px 16px; + padding: 5px 16px; border: 1px solid var(--border-color); border-radius: var(--border-radius-md); background: none; @@ -12,4 +15,14 @@ &:hover { border: 1px solid var(--border-color-hover); } + &[disabled] { + opacity: 0.5; + &:hover { + border: 1px solid var(--border-color); + } + } + &[data-loading="true"] { + user-select: none; + cursor: not-allowed; + } } diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 7a06b69..9097459 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,15 +1,20 @@ -import type { ParentComponent, JSX } from 'solid-js'; +import type { ParentComponent, JSX } from "solid-js"; -import { splitProps } from 'solid-js'; -import './Button.scss'; +import { splitProps } from "solid-js"; +import Spinner from "@components/Spinner"; +import "./Button.scss"; -type ButtonProps = JSX.ButtonHTMLAttributes; +type ButtonProps = { + isLoading?: boolean | undefined; + loadingText?: string | undefined; +} & JSX.ButtonHTMLAttributes; const Button: ParentComponent = (props) => { - const [local, attrs] = splitProps(props, ['children']); + const [local, attrs] = splitProps(props, ["children", "isLoading", "loadingText"]); return ( - ); }; diff --git a/src/components/ContactForm/ContactForm.scss b/src/components/ContactForm/ContactForm.scss index 48a4b3b..b1f2c50 100644 --- a/src/components/ContactForm/ContactForm.scss +++ b/src/components/ContactForm/ContactForm.scss @@ -4,19 +4,42 @@ row-gap: 7px; margin-bottom: 3.125rem; - & div:first-of-type { - display: flex; - column-gap: 10px; - } - - & div:last-of-type { + & .form-footer { text-align: center; margin-top: 15px; } + + & .form-result { + font-style: italic; + color: var(--font-color-gray); + } + + & .error-message { + margin: 2px 0 3px 0; + display: block; + color: var(--color-error); + font-size: var(--font-size-sm); + user-select: none; + &::before { + content: "*"; + } + } + + & .flex { + column-gap: 10px; + & div { + width: 100%; + } + } + + & input.error-input, + & textarea.error-input { + border: 1px solid var(--color-error); + } } @media screen and (max-width: 560px) { - .contact-form div:first-of-type { + .contact-form .flex { flex-direction: column; row-gap: 7px; } diff --git a/src/components/ContactForm/ContactForm.tsx b/src/components/ContactForm/ContactForm.tsx index 6949e84..0fb9cca 100644 --- a/src/components/ContactForm/ContactForm.tsx +++ b/src/components/ContactForm/ContactForm.tsx @@ -1,26 +1,115 @@ -import type { Component } from 'solid-js'; +import type { Component, JSX } from "solid-js"; -import Button from '@components/Button'; -import './ContactForm.scss'; +import { createSignal } from "solid-js"; +import { createStore } from "solid-js/store"; +import { useForm } from "@hooks/index"; +import { encode } from "@root/utils/url"; +import Button from "@components/Button"; +import { validateEmail } from "./validations"; +import Submitted from "./Submitted"; +import Error from "./Error"; +import "./ContactForm.scss"; -type InputFieldsType = { - name: string; - email: string; - subject: string; - message: string; -}; +const ErrorMessage: Component<{ message: string }> = (props) => {props.message}; const ContactForm: Component = () => { + const { formSubmit, validate, errors } = useForm({ errorClass: "error-input" }); + const [isSubmitted, setIsSubmitted] = createSignal(false); + const [isLoading, setIsLoading] = createSignal(false); + const [isError, setIsError] = createSignal(false); + const [state, setState] = createStore({ name: "", email: "", subject: "", message: "", "bot-field": "" }); + + const reset = (form: HTMLFormElement) => { + for (const key in state) { + form.elements[key].value = ""; + setState({ [key]: "" }); + } + }; + + const sendMessage = async (form: HTMLFormElement) => { + try { + setIsLoading(true); + fetch("/", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: encode({ + "form-name": "contact", + ...state, + }), + }); + } catch (error) { + setIsError(true); + console.error(error); + } finally { + setIsLoading(false); + setIsSubmitted(true); + reset(form); + } + }; + + const onChange: JSX.EventHandler = (e) => { + const key = e.currentTarget.name; + const value = e.currentTarget.value; + + if (!Object.hasOwn(state, key)) return; + setState({ [key]: value }); + }; + return ( -
-
- - + + - -