diff --git a/ui/package-lock.json b/ui/package-lock.json index 1bcd290..dcdb639 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -14,7 +14,8 @@ "react-dom": "^18.2.0", "react-icons": "^4.7.1", "react-leaflet": "^4.2.0", - "stylelint-config-prettier-scss": "^0.0.1" + "stylelint-config-prettier-scss": "^0.0.1", + "yup": "^1.1.1" }, "devDependencies": { "@babel/core": "^7.21.0", @@ -15953,6 +15954,11 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz", + "integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==" + }, "node_modules/property-information": { "version": "5.6.0", "dev": true, @@ -18890,6 +18896,11 @@ "node": ">=0.6.0" } }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + }, "node_modules/tmpl": { "version": "1.0.5", "dev": true, @@ -18962,6 +18973,11 @@ "node": ">=0.6" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + }, "node_modules/tr46": { "version": "0.0.3", "dev": true, @@ -20540,6 +20556,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yup": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.1.1.tgz", + "integrity": "sha512-KfCGHdAErqFZWA5tZf7upSUnGKuTOnsI3hUsLr7fgVtx+DK04NPV01A68/FslI4t3s/ZWpvXJmgXhd7q6ICnag==", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zwitch": { "version": "1.0.5", "dev": true, diff --git a/ui/package.json b/ui/package.json index c493781..262c23a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -14,13 +14,14 @@ "build-storybook": "build-storybook" }, "dependencies": { - "leaflet": "^1.9.3", "classnames": "^2.3.2", + "leaflet": "^1.9.3", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-leaflet": "^4.2.0", "react-icons": "^4.7.1", - "stylelint-config-prettier-scss": "^0.0.1" + "react-leaflet": "^4.2.0", + "stylelint-config-prettier-scss": "^0.0.1", + "yup": "^1.1.1" }, "devDependencies": { "@babel/core": "^7.21.0", diff --git a/ui/src/assets/images/logo/pngs/logo_accent.png b/ui/src/assets/images/logo/pngs/logo_accent.png new file mode 100644 index 0000000..fc3170e Binary files /dev/null and b/ui/src/assets/images/logo/pngs/logo_accent.png differ diff --git a/ui/src/assets/images/logo/svgs/logo_accent.svg b/ui/src/assets/images/logo/svgs/logo_accent.svg new file mode 100644 index 0000000..78a189f --- /dev/null +++ b/ui/src/assets/images/logo/svgs/logo_accent.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/ui/src/components/Button/Button.jsx b/ui/src/components/Button/Button.jsx index 619cadf..1a82588 100644 --- a/ui/src/components/Button/Button.jsx +++ b/ui/src/components/Button/Button.jsx @@ -43,7 +43,7 @@ Button.propTypes = { /** color variant */ variant: PropTypes.oneOf(buttonVariants), /** event that will be triggered when the button is clicked */ - onClick: PropTypes.func.isRequired, + onClick: PropTypes.func, /** content that will be displayed in the button */ children: PropTypes.string.isRequired, /** additional css styles */ diff --git a/ui/src/components/LoginForm/LoginForm.jsx b/ui/src/components/LoginForm/LoginForm.jsx new file mode 100644 index 0000000..cdc38f1 --- /dev/null +++ b/ui/src/components/LoginForm/LoginForm.jsx @@ -0,0 +1,125 @@ +import styles from './LoginForm.module.scss' +import Button from '../Button/Button' +import { useRef, useState } from 'react' +import { object, string } from 'yup' +import logo from '../../assets/images/logo/svgs/logo_accent.svg' +import classNames from 'classnames' + +export default function LoginForm() { + const emailOrNickInput = useRef(null) + const passwordInput = useRef(null) + + const [errors, setErrors] = useState({}) + + const [loginError, setLoginError] = useState(false) + + const loginForm = useRef(null) + + const schema = object({ + emailOrNick: string().required('Pole "Email/nick" jest wymagane.'), + password: string().required('Pole "Hasło" jest wymagane.') + }) + + const handleChange = (event) => { + const { name } = event.target + if (errors[name]) { + setErrors((prev) => { + const newErrors = { ...prev } + delete newErrors[name] + return newErrors + }) + } + setLoginError(false) + } + + const handleSubmit = (event) => { + event.preventDefault() + schema + .validate( + { + emailOrNick: emailOrNickInput.current.value, + password: passwordInput.current.value + }, + { + abortEarly: false + } + ) + .then(() => { + console.log('ok') + // TODO: send data to server + setLoginError(true) // if failed to login + }) + .catch((error) => { + const newErrors = {} + error.inner.forEach((error) => { + newErrors[error.path] = error.message + }) + setErrors(newErrors) + loginForm.current[Object.keys(newErrors)[0]]?.focus() + }) + } + + return ( +
+
+ logo +
+

Nie masz jeszcze konta?

+ +
+
+
+

Zaloguj się do swojego konta

+
+ + + {errors.emailOrNick && ( + {errors.emailOrNick} + )} +
+ +
+ + + {errors.password && ( + {errors.password} + )} +
+ + {/* TODO: change view to password reset */} + Zapomniałem hasła + + {loginError && ( + Nie udało się zalogować + )} + +
+
+ ) +} diff --git a/ui/src/components/LoginForm/LoginForm.module.scss b/ui/src/components/LoginForm/LoginForm.module.scss new file mode 100644 index 0000000..9034abc --- /dev/null +++ b/ui/src/components/LoginForm/LoginForm.module.scss @@ -0,0 +1,107 @@ +@use '../../assets/styles/abstracts' as *; + +$form-gap: 1.875rem; +$password-reset-offset: -$form-gap; + +.view { + min-height: 100vh; + box-sizing: border-box; + display: grid; + grid-template-rows: auto 1fr; + align-items: center; + background-color: #282828; + color: $base-white; + font-size: calc($font-size-p * 1px); + font-weight: 600; + padding: 1rem 2rem; + gap: 1.875rem; +} + +.viewHeader { + grid-row: 1; + grid-column: 1; + display: flex; + justify-content: space-between; + align-items: center; +} + +.form { + grid-column: 1; + justify-self: center; + min-width: 34rem; + display: grid; + gap: $form-gap; + text-align: center; + background-color: $alternative-a900; + border: 0.375rem solid $primary-p700; + border-radius: 0.625rem; + padding: 2.875rem 5.4375rem; + + @media screen and (height > 720px) { + grid-row: 1 / -1; + } +} + +.header__level3 { + font-weight: 600; + font-size: calc($font-size-h3 * 1px); + .viewHeader & { + display: inline; + } + .form & { + margin-bottom: 1.875rem; + } +} + +button.button { + background-color: $primary-p500; + color: $alternative-a900; + font-weight: 600; + border-radius: $border-radius-md; + padding: 0.5625rem 1.875rem; + + &.registerButton { + font-size: calc($font-size-button-md * 1px); + margin-left: 1.5rem; + } + + &.submitButton { + font-size: calc($font-size-button-lg * 1px); + justify-self: center; + min-width: calc(2 / 3 * 100%); + } +} + +.inputGroup { + display: grid; + + input { + font-size: inherit; + padding: 0.8125rem 1.1875rem; + } +} + +.inputGroup, +.passwordReset { + text-align: left; +} + +.error { + &Span { + color: $communicates-error; + } + + &Input { + outline: solid $communicates-error; + } +} + +.passwordReset { + margin-top: $password-reset-offset; + text-decoration: none; + color: $base-white; +} + +.logo { + max-width: 20rem; +} diff --git a/ui/src/components/LoginForm/LoginForm.stories.jsx b/ui/src/components/LoginForm/LoginForm.stories.jsx new file mode 100644 index 0000000..077c541 --- /dev/null +++ b/ui/src/components/LoginForm/LoginForm.stories.jsx @@ -0,0 +1,15 @@ +import React from 'react' +import LoginForm from './LoginForm' +import '../../assets/styles/index.scss' + +export default { + component: LoginForm, + parameters: { + layout: 'fullscreen' + } +} + +const Template = (args) => + +export const Primary = Template.bind({}) +Primary.storyName = 'LoginForm'