diff --git a/src/api/authentication.ts b/src/api/authentication.ts new file mode 100644 index 0000000..4384b05 --- /dev/null +++ b/src/api/authentication.ts @@ -0,0 +1,24 @@ +import axios from '@api/customAxios'; + +interface LoginData { + user_name: string; + password: string; +} + +interface LoginResponse { + code: number; + message: string; +} + +export async function login(data: LoginData): Promise { + return axios.post('user/login', data); +} + +export async function logout(): Promise { + return axios.post('user/logout'); +} + +export const userStats = async () => { + const response = await axios.get('user/stat'); + return response; +}; diff --git a/src/api/customAxios.ts b/src/api/customAxios.ts new file mode 100644 index 0000000..fa8883e --- /dev/null +++ b/src/api/customAxios.ts @@ -0,0 +1,10 @@ +import axios from 'axios'; + +export default axios.create({ + withCredentials: true, + baseURL: process.env.NEXT_PUBLIC_FAIROSHOST, + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, +}); diff --git a/src/components/Buttons/Button/Button.tsx b/src/components/Buttons/Button/Button.tsx new file mode 100644 index 0000000..5c64e19 --- /dev/null +++ b/src/components/Buttons/Button/Button.tsx @@ -0,0 +1,136 @@ +import { FC, ReactNode, ReactChild } from 'react'; + +interface ButtonProps { + type?: 'button' | 'submit'; + variant: + | 'primary' + | 'primary-outlined' + | 'secondary' + | 'tertiary' + | 'tertiary-outlined'; + label?: string; + icon?: ReactNode; + onClick?: any; + className?: string; + padding?: string; + children?: ReactChild | ReactChild[]; + disabled?: boolean; +} + +const Button: FC = ({ + type = 'button', + variant, + label, + icon, + onClick, + className, + padding, + children, + disabled = false, +}) => { + const getVariantStyling = () => { + switch (variant) { + case 'primary': + return ( + 'bg-color-shade-dark-4-day text-main-purple text-base effect-style-small-button-drop-shadow' + + ' ' + + (padding ? '' : 'py-3 px-8') + ); + case 'primary-outlined': + return ( + 'bg-none border border-color-accents-purple-heavy text-color-accents-purple-heavy text-base' + + ' ' + + (padding ? '' : 'py-3 px-8') + ); + case 'secondary': + return ( + 'bg-color-shade-white-night text-color-accents-purple-black text-base' + + ' ' + + (padding ? '' : 'py-3 px-8') + ); + case 'tertiary': + return ( + 'text-color-accents-purple-black text-xs' + + ' ' + + (padding ? '' : 'py-2 px-3') + ); + case 'tertiary-outlined': + return ( + 'bg-none border border-color-accents-purple-heavy text-color-accents-purple-heavy text-xs' + + ' ' + + (padding ? '' : 'py-2 px-3') + ); + } + }; + + const getVariantDisabledStyle = () => { + if (disabled) { + switch (variant) { + case 'primary': + return 'text-color-shade-light-3-night disabled:bg-color-shade-dark-4-day'; + case 'primary-outlined': + return 'text-color-shade-light-3-night disabled:border-color-shade-light-3-night'; + case 'secondary': + return 'bg-none text-color-shade-light-3-night'; + case 'tertiary': + return ''; + case 'tertiary-outlined': + return ''; + } + } else return ''; + }; + + const getVariantSelectedStyle = () => { + if (!disabled) { + switch (variant) { + case 'primary': + return 'focus:shadow-dark-purple focus:bg-color-shade-dark-4 effect-style-small-button-drop-shadow'; + case 'primary-outlined': + return 'focus:shadow-dark-purple focus:bg-color-shade-dark-3-day'; + case 'secondary': + return 'focus:shadow-dark-purple focus:bg-color-shade-white-night'; + case 'tertiary': + return 'focus:text-base'; + case 'tertiary-outlined': + return 'focus:shadow-dark-purple focus:bg-color-shade-dark-3-day'; + } + } else return ''; + }; + + const getVariantHoverStyle = () => { + if (!disabled) { + switch (variant) { + case 'primary': + return 'hover:shadow-soft-purple hover:bg-color-shade-dark-4 effect-style-small-button-drop-shadow'; + case 'primary-outlined': + return 'hover:shadow-soft-purple hover:bg-color-shade-dark-3-day'; + case 'secondary': + return 'hover:shadow-soft-purple hover:bg-color-shade-white-night'; + case 'tertiary': + return 'hover:text-base'; + case 'tertiary-outlined': + return 'hover:shadow-soft-purple hover:bg-color-shade-dark-3-day'; + } + } else return ''; + }; + + return ( + + ); +}; + +export default Button; diff --git a/src/components/Buttons/UserDropdownToggle/UserDropdownToggle.tsx b/src/components/Buttons/UserDropdownToggle/UserDropdownToggle.tsx new file mode 100644 index 0000000..0c10585 --- /dev/null +++ b/src/components/Buttons/UserDropdownToggle/UserDropdownToggle.tsx @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { FC, useContext } from 'react'; + +import UserContext from '@context/UserContext'; +import Blockies from 'react-blockies'; + +interface UserDropdownToggleProps { + onClickHandler: any; +} + +const UserDropdownToggle: FC = ({ + onClickHandler, +}) => { + const { address } = useContext(UserContext); + + return ( + + ); +}; + +export default UserDropdownToggle; diff --git a/src/components/Buttons/index.ts b/src/components/Buttons/index.ts new file mode 100644 index 0000000..6cd3f2d --- /dev/null +++ b/src/components/Buttons/index.ts @@ -0,0 +1,4 @@ +import Button from '@components/Buttons/Button/Button'; +import UserDropdownToggle from '@components/Buttons/UserDropdownToggle/UserDropdownToggle'; + +export { Button, UserDropdownToggle }; diff --git a/src/components/FeedbackMessage/FeedbackMessage.tsx b/src/components/FeedbackMessage/FeedbackMessage.tsx new file mode 100644 index 0000000..c57dd49 --- /dev/null +++ b/src/components/FeedbackMessage/FeedbackMessage.tsx @@ -0,0 +1,22 @@ +import { FC } from 'react'; + +interface FeedbackMessageProps { + type: 'success' | 'error'; + message: string; +} + +const FeedbackMessage: FC = ({ message, type }) => { + return ( +
+ {message} +
+ ); +}; + +export default FeedbackMessage; diff --git a/src/components/Forms/LoginForm/LoginForm.tsx b/src/components/Forms/LoginForm/LoginForm.tsx new file mode 100644 index 0000000..e5bc823 --- /dev/null +++ b/src/components/Forms/LoginForm/LoginForm.tsx @@ -0,0 +1,110 @@ +import { FC, useContext, useState } from 'react'; +import router from 'next/router'; +import { useForm } from 'react-hook-form'; + +import UserContext from '@context/UserContext'; +// import PodContext from '@context/PodContext'; + +import { login, userStats } from '@api/authentication'; + +import { AuthenticationHeader } from '@components/Headers'; +import { AuthenticationInput } from '@components/Inputs'; +import { Button } from '@components/Buttons'; +import FeedbackMessage from '@components/FeedbackMessage/FeedbackMessage'; + +const LoginForm: FC = () => { + const { register, handleSubmit, formState } = useForm(); + const { errors } = formState; + + const { setUser, setPassword, setAddress } = useContext(UserContext); + // const { clearPodContext } = useContext(PodContext); + + const [errorMessage, setErrorMessage] = useState(''); + + const onSubmit = (data: { user_name: string; password: string }) => { + login(data) + .then(() => { + setUser(data.user_name); + setPassword(data.password); + + userStats() + .then((res) => { + setAddress(res.data.reference); + // clearPodContext(); + router.push('/gallery'); + }) + .catch(() => { + setErrorMessage( + 'Login failed. Incorrect user credentials, please try again.' + ); + }); + }) + .catch(() => { + setErrorMessage( + 'Login failed. Incorrect user credentials, please try again.' + ); + }); + }; + + return ( +
+ + +
+
+ +
+ +
+ + + + +
+
+ + + +
+
+ ); +}; + +export default LoginForm; diff --git a/src/components/Forms/index.ts b/src/components/Forms/index.ts new file mode 100644 index 0000000..7b37f1d --- /dev/null +++ b/src/components/Forms/index.ts @@ -0,0 +1,3 @@ +import LoginForm from '@components/Forms/LoginForm/LoginForm'; + +export { LoginForm }; diff --git a/src/components/Headers/AuthenticationHeader/AuthenticationHeader.tsx b/src/components/Headers/AuthenticationHeader/AuthenticationHeader.tsx new file mode 100644 index 0000000..68bf3bc --- /dev/null +++ b/src/components/Headers/AuthenticationHeader/AuthenticationHeader.tsx @@ -0,0 +1,24 @@ +import { FC } from 'react'; + +interface AuthenticationHeaderProps { + title: string; + content: string; +} + +const AuthenticationHeader: FC = ({ + title, + content, +}) => { + return ( +
+

+ {title} +

+

+ {content} +

+
+ ); +}; + +export default AuthenticationHeader; diff --git a/src/components/Headers/index.ts b/src/components/Headers/index.ts new file mode 100644 index 0000000..109e0bc --- /dev/null +++ b/src/components/Headers/index.ts @@ -0,0 +1,3 @@ +import AuthenticationHeader from '@components/Headers/AuthenticationHeader/AuthenticationHeader'; + +export { AuthenticationHeader }; diff --git a/src/components/Inputs/AuthenticationInput/AuthenticationInput.tsx b/src/components/Inputs/AuthenticationInput/AuthenticationInput.tsx new file mode 100644 index 0000000..7987f22 --- /dev/null +++ b/src/components/Inputs/AuthenticationInput/AuthenticationInput.tsx @@ -0,0 +1,57 @@ +import { FC } from 'react'; +import { UseFormRegister, FieldValues, FieldError } from 'react-hook-form'; + +interface AuthenticationInputProps { + type: 'text' | 'number' | 'email' | 'password'; + id: string; + name: string; + label: string; + placeholder?: string; + defaultValue?: string | number; + useFormRegister: UseFormRegister; + validationRules?: any; + error?: FieldError; + errorMessage?: string; +} + +const AuthenticationInput: FC = ({ + type, + id, + name, + label, + placeholder, + defaultValue, + useFormRegister, + validationRules, + error, + errorMessage, +}) => { + return ( +
+ + + + + {error ? ( +
+ {errorMessage} +
+ ) : null} +
+ ); +}; + +export default AuthenticationInput; diff --git a/src/components/Inputs/SearchBar/SearchBar.module.scss b/src/components/Inputs/SearchBar/SearchBar.module.scss new file mode 100644 index 0000000..5a492be --- /dev/null +++ b/src/components/Inputs/SearchBar/SearchBar.module.scss @@ -0,0 +1,18 @@ +.searchBar { + all: unset; + flex-grow: 1; + padding: 0px 10px; + font-size: 12px; + font-weight: 400; + color: #28282b; +} + +.searchBar:-webkit-autofill, +.searchBar:-webkit-autofill:hover, +.searchBar:-webkit-autofill:focus, +.searchBar:-webkit-autofill:active { + box-shadow: none; + -webkit-box-shadow: none; + transition: background-color 5000s ease-in-out 0s; + -webkit-text-fill-color: #28282b !important; +} diff --git a/src/components/Inputs/SearchBar/SearchBar.tsx b/src/components/Inputs/SearchBar/SearchBar.tsx new file mode 100644 index 0000000..eb6fede --- /dev/null +++ b/src/components/Inputs/SearchBar/SearchBar.tsx @@ -0,0 +1,36 @@ +import { FC, useContext } from 'react'; + +import SearchContext from '@context/SearchContext'; + +import SearchIcon from '@media/UI/search.svg'; +import CloseIcon from '@media/UI/close.svg'; + +import classes from './SearchBar.module.scss'; + +interface SearchBarProps {} + +const SearchBar: FC = () => { + const { search, updateSearch } = useContext(SearchContext); + + return ( +
+ + + updateSearch(e.target.value)} + /> + +
updateSearch('')}> + +
+
+ ); +}; + +export default SearchBar; diff --git a/src/components/Inputs/index.ts b/src/components/Inputs/index.ts new file mode 100644 index 0000000..64eb08c --- /dev/null +++ b/src/components/Inputs/index.ts @@ -0,0 +1,4 @@ +import AuthenticationInput from '@components/Inputs/AuthenticationInput/AuthenticationInput'; +import SearchBar from '@components/Inputs/SearchBar/SearchBar'; + +export { AuthenticationInput, SearchBar }; diff --git a/src/components/Layouts/AuthenticationLayout/AuthenticationLayout.tsx b/src/components/Layouts/AuthenticationLayout/AuthenticationLayout.tsx new file mode 100644 index 0000000..741ca58 --- /dev/null +++ b/src/components/Layouts/AuthenticationLayout/AuthenticationLayout.tsx @@ -0,0 +1,23 @@ +import { FC, ReactChild } from 'react'; + +import { MainNavigationBar } from '@components/NavigationBars'; + +interface AuthenticationLayoutProps { + children: ReactChild | ReactChild[]; +} + +const AuthenticationLayout: FC = ({ children }) => { + return ( +
+
+ +
+ +
+ {children} +
+
+ ); +}; + +export default AuthenticationLayout; diff --git a/src/components/Layouts/index.ts b/src/components/Layouts/index.ts new file mode 100644 index 0000000..4f53af7 --- /dev/null +++ b/src/components/Layouts/index.ts @@ -0,0 +1,3 @@ +import AuthenticationLayout from '@components/Layouts/AuthenticationLayout/AuthenticationLayout'; + +export { AuthenticationLayout }; diff --git a/src/components/NavigationBars/MainNavigationBar/MainNavigationBar.tsx b/src/components/NavigationBars/MainNavigationBar/MainNavigationBar.tsx new file mode 100644 index 0000000..f3da6a2 --- /dev/null +++ b/src/components/NavigationBars/MainNavigationBar/MainNavigationBar.tsx @@ -0,0 +1,46 @@ +import { FC, useState } from 'react'; +import Link from 'next/link'; + +import Logo from '@media/branding/logo.svg'; +import { SearchBar } from '@components/Inputs'; +import { UserDropdownToggle } from '@components/Buttons'; +import UserDropdown from './UserDropdown/UserDropdown'; + +interface MainNavigationBarProps { + hideComponents?: boolean; +} + +const MainNavigationBar: FC = ({ hideComponents }) => { + const [showUserDropdown, setShowUserDropdown] = useState(false); + + return ( + + ); +}; + +export default MainNavigationBar; diff --git a/src/components/NavigationBars/MainNavigationBar/UserDropdown/UserDropdown.tsx b/src/components/NavigationBars/MainNavigationBar/UserDropdown/UserDropdown.tsx new file mode 100644 index 0000000..eaae93c --- /dev/null +++ b/src/components/NavigationBars/MainNavigationBar/UserDropdown/UserDropdown.tsx @@ -0,0 +1,53 @@ +import { FC, useContext } from 'react'; +import router from 'next/router'; + +import UserContext from '@context/UserContext'; +import { logout } from '@api/authentication'; + +interface UserDropdownProps { + showDropdown: boolean; + setShowDropdown: (showModal: boolean) => void; +} + +const UserDropdown: FC = ({ + showDropdown, + setShowDropdown, +}) => { + const { user } = useContext(UserContext); + + const disconnect = async () => { + await logout(); + router.push('/'); + }; + + return ( + <> +
setShowDropdown(false)} + > +
+
e.stopPropagation()} + > +
+ {user} +
+ +
+
+ Log out +
+
+
+
+
+ + ); +}; + +export default UserDropdown; diff --git a/src/components/NavigationBars/index.ts b/src/components/NavigationBars/index.ts new file mode 100644 index 0000000..346dc0c --- /dev/null +++ b/src/components/NavigationBars/index.ts @@ -0,0 +1,3 @@ +import MainNavigationBar from '@components/NavigationBars/MainNavigationBar/MainNavigationBar'; + +export { MainNavigationBar }; diff --git a/src/context/SearchContext.tsx b/src/context/SearchContext.tsx new file mode 100644 index 0000000..6d78b35 --- /dev/null +++ b/src/context/SearchContext.tsx @@ -0,0 +1,41 @@ +import { FC, ReactNode, createContext, useState } from 'react'; + +interface SearchContext { + search: string; + updateSearch: (newSearch: string) => void; +} + +interface SearchContextProps { + children: ReactNode; +} + +const searchContextDefaultValues: SearchContext = { + search: '', + // eslint-disable-next-line @typescript-eslint/no-empty-function + updateSearch: () => {}, +}; + +const SearchContext = createContext(searchContextDefaultValues); + +const SearchProvider: FC = ({ children }) => { + const [search, setSearch] = useState(''); + + const updateSearch = (newSearch: string) => { + setSearch(newSearch.trim()); + }; + + return ( + + {children} + + ); +}; + +export default SearchContext; + +export { SearchProvider }; diff --git a/src/context/UserContext.tsx b/src/context/UserContext.tsx new file mode 100644 index 0000000..a9f5bcf --- /dev/null +++ b/src/context/UserContext.tsx @@ -0,0 +1,50 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { FC, ReactNode, createContext, useState } from 'react'; + +interface UserContext { + user: string; + setUser: (user: string) => void; + password: string; + setPassword: (user: string) => void; + address: string; + setAddress: (address: string) => void; +} + +interface UserContextProps { + children: ReactNode; +} +const UserContextDefaultValues: UserContext = { + user: '', + setUser: (user: string) => {}, + password: '', + setPassword: (User: string) => {}, + address: '', + setAddress: (address: string) => {}, +}; + +const UserContext = createContext(UserContextDefaultValues); + +const UserProvider: FC = ({ children }) => { + const [user, setUser] = useState(''); + const [password, setPassword] = useState(''); + const [address, setAddress] = useState(''); + return ( + + {children} + + ); +}; + +export default UserContext; + +export { UserProvider }; diff --git a/src/media/UI/close.svg b/src/media/UI/close.svg new file mode 100644 index 0000000..941f99f --- /dev/null +++ b/src/media/UI/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/media/UI/search.svg b/src/media/UI/search.svg new file mode 100644 index 0000000..e9070f6 --- /dev/null +++ b/src/media/UI/search.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/media/branding/logo.svg b/src/media/branding/logo.svg new file mode 100644 index 0000000..32e3cf5 --- /dev/null +++ b/src/media/branding/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 7051f9d..49bd06f 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,9 +1,18 @@ import { AppProps } from 'next/app'; +import { UserProvider } from '@context/UserContext'; +import { SearchProvider } from '@context/SearchContext'; + import '@styles/globals.scss'; function MyApp({ Component, pageProps }: AppProps) { - return ; + return ( + + + + + + ); } export default MyApp; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 7125fd5..4fb7c84 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,7 +1,14 @@ import type { NextPage } from 'next'; +import { AuthenticationLayout } from '@components/Layouts'; +import { LoginForm } from '@components/Forms'; + const Home: NextPage = () => { - return

Log In Page

; + return ( + + + + ); }; export default Home; diff --git a/src/styles/globals.scss b/src/styles/globals.scss index 1b390d2..fc32f56 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -43,194 +43,18 @@ --color-status-positive-night: #7fc18a; --color-status-components-day: #7b61ff; --color-status-components-night: #7b61ff; + --white: hsla(0, 0%, 100%, 1); + --button-blue: hsla(223, 61%, 93%, 1); + --grey: hsla(224, 23%, 91%, 1); + --main-purple: hsla(230, 31%, 38%, 1); + --alert-red: hsla(347, 100%, 61%, 1); + --alert-green: hsla(119, 82%, 36%, 1); } * { font-family: 'Work Sans'; } -.text-style-96-header-1 { - font-size: 96px; - font-family: 'Work Sans'; - font-weight: bolder; - font-style: normal; - line-height: 104px; - letter-spacing: -0.015em; - text-decoration: none; -} - -.text-style-64-header-2 { - font-size: 64px; - font-family: 'Work Sans'; - font-weight: bolder; - font-style: normal; - line-height: 72px; - text-decoration: none; -} - -.text-style-48-header-3 { - font-size: 48px; - font-family: 'Work Sans'; - font-weight: bolder; - font-style: normal; - line-height: 56px; - text-decoration: none; -} - -.text-style-32-header-4 { - font-size: 32px; - font-family: 'Work Sans'; - font-weight: bolder; - font-style: normal; - line-height: 40px; - text-decoration: none; -} - -.text-style-30-numbers-big { - font-size: 30px; - font-family: 'Space Mono'; - font-weight: normal; - font-style: normal; - line-height: 24px; - text-decoration: none; -} - -.text-style-24-header-5 { - font-size: 24px; - font-family: 'Work Sans'; - font-weight: bolder; - font-style: normal; - line-height: 32px; - text-decoration: none; -} - -.text-style-24-numbers-medium { - font-size: 24px; - font-family: 'Space Mono'; - font-weight: normal; - font-style: normal; - line-height: 24px; - text-decoration: none; -} - -.text-style-20-header-6 { - font-size: 20px; - font-family: 'Work Sans'; - font-weight: bolder; - font-style: normal; - line-height: 32px; - text-decoration: none; -} - -.text-style-18-numbers-small { - font-size: 18px; - font-family: 'Space Mono'; - font-weight: normal; - font-style: normal; - line-height: 24px; - text-decoration: none; -} - -.text-style-16-subtitle { - font-size: 16px; - font-family: 'Work Sans'; - font-weight: bolder; - font-style: normal; - line-height: 24px; - text-decoration: none; -} - -.text-style-16-body-regular { - font-size: 16px; - font-family: 'Work Sans'; - font-weight: normal; - font-style: normal; - line-height: 24px; - text-decoration: none; -} - -.text-style-16-body-medium { - font-size: 16px; - font-family: 'Work Sans'; - font-weight: bold; - font-style: normal; - line-height: 24px; - text-decoration: none; -} - -.text-style-16-body-semibold { - font-size: 16px; - font-family: 'Work Sans'; - font-weight: bolder; - font-style: normal; - line-height: 24px; - text-decoration: none; -} - -.text-style-14-subtitle { - font-size: 14px; - font-family: 'Work Sans'; - font-weight: bolder; - font-style: normal; - line-height: 22px; - text-decoration: none; -} - -.text-style-14-numbers-body { - font-size: 14px; - font-family: 'Space Mono'; - font-weight: normal; - font-style: normal; - line-height: 14px; - text-decoration: none; -} - -.text-style-12-caption-regular { - font-size: 12px; - font-family: 'Work Sans'; - font-weight: normal; - font-style: normal; - line-height: 16px; - text-decoration: none; -} - -.text-style-12-numbers-tiny { - font-size: 12px; - font-family: 'Space Mono'; - font-weight: normal; - font-style: normal; - line-height: 12px; - text-decoration: none; -} - -.text-style-12-caption-medium { - font-size: 12px; - font-family: 'Work Sans'; - font-weight: bold; - font-style: normal; - line-height: 16px; - text-decoration: none; -} - -.text-style-12-caption-semibold { - font-size: 12px; - font-family: 'Work Sans'; - font-weight: bolder; - font-style: normal; - line-height: 16px; - text-decoration: none; -} - -.text-style-10-overline { - font-size: 10px; - font-family: 'Work Sans'; - font-weight: bold; - font-style: normal; - line-height: 10px; - letter-spacing: 0.03em; - text-decoration: none; -} - .effect-style-purple-box-shadow { box-shadow: 0px 20px 40px #473999; } diff --git a/tailwind.config.js b/tailwind.config.js index b7df4dc..ac8471a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -63,6 +63,12 @@ module.exports = { 'color-status-positive-night': '#7fc18a', 'color-status-components-day': '#7b61ff', 'color-status-components-night': '#7b61ff', + white: 'hsla(0, 0%, 100%, 1)', + 'button-blue': 'hsla(223, 61%, 93%, 1)', + grey: 'hsla(224, 23%, 91%, 1)', + 'main-purple': 'hsla(230, 31%, 38%, 1)', + 'alert-red': 'hsla(347, 100%, 61%, 1)', + 'alert-green': 'hsla(119, 82%, 36%, 1)', }, }, variants: {