diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index aefe1b6..de5bfbe 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -12,5 +12,6 @@ "О сайте": "About us", "Короткий язык": "En", "increment": "increment", - "decrement": "decrement" + "decrement": "decrement", + "login": "Login" } diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index e8902a2..ffa389a 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -12,5 +12,6 @@ "О сайте": "О сайте", "Короткий язык": "Ru", "increment": "Увеличить", - "decrement": "Уменьшить" + "decrement": "Уменьшить", + "login": "Войти" } diff --git a/src/app/providers/StoreProvider/config/StateSchema.ts b/src/app/providers/StoreProvider/config/StateSchema.ts index 1ebb375..94597e8 100644 --- a/src/app/providers/StoreProvider/config/StateSchema.ts +++ b/src/app/providers/StoreProvider/config/StateSchema.ts @@ -1,6 +1,8 @@ import { CounterSchema } from 'entities/Counter'; +import { UserSchema } from 'entities/User'; export interface StateSchema { - counter: CounterSchema + counter: CounterSchema; + user: UserSchema; } diff --git a/src/app/providers/StoreProvider/config/store.ts b/src/app/providers/StoreProvider/config/store.ts index f988ddf..e390f2f 100644 --- a/src/app/providers/StoreProvider/config/store.ts +++ b/src/app/providers/StoreProvider/config/store.ts @@ -1,12 +1,16 @@ -import { configureStore } from '@reduxjs/toolkit'; +import { configureStore, ReducersMapObject } from '@reduxjs/toolkit'; import { counterReducer } from 'entities/Counter'; +import { userReducer } from 'entities/User'; import { StateSchema } from './StateSchema'; export function createReduxStore(initialState?:StateSchema) { + const rootReducers: ReducersMapObject = { + counter: counterReducer, + user: userReducer, + }; + return configureStore({ - reducer: { - counter: counterReducer, - }, + reducer: rootReducers, devTools: __IS_DEV__, preloadedState: initialState, }); diff --git a/src/entities/User/index.ts b/src/entities/User/index.ts new file mode 100644 index 0000000..a831050 --- /dev/null +++ b/src/entities/User/index.ts @@ -0,0 +1,3 @@ +export { userReducer, userActions } from './model/slice/userSlice'; + +export { UserSchema, User } from './model/types/user'; diff --git a/src/entities/User/model/slice/userSlice.ts b/src/entities/User/model/slice/userSlice.ts new file mode 100644 index 0000000..3217abb --- /dev/null +++ b/src/entities/User/model/slice/userSlice.ts @@ -0,0 +1,16 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { UserSchema } from '../types/user'; + +const initialState: UserSchema = { +}; + +export const userSlice = createSlice({ + name: 'user', + initialState, + reducers: { + + }, +}); + +export const { actions: userActions } = userSlice; +export const { reducer: userReducer } = userSlice; diff --git a/src/entities/User/model/types/user.ts b/src/entities/User/model/types/user.ts new file mode 100644 index 0000000..867e22a --- /dev/null +++ b/src/entities/User/model/types/user.ts @@ -0,0 +1,8 @@ +export interface User { + id: string; + username: string; +} + +export interface UserSchema { + authData?: User; +} diff --git a/src/features/AuthByUsername/index.ts b/src/features/AuthByUsername/index.ts new file mode 100644 index 0000000..7f7bd4b --- /dev/null +++ b/src/features/AuthByUsername/index.ts @@ -0,0 +1 @@ +export { LoginModal } from './ui/LoginModal/LoginModal'; diff --git a/src/features/AuthByUsername/ui/LoginForm/LoginForm.module.scss b/src/features/AuthByUsername/ui/LoginForm/LoginForm.module.scss new file mode 100644 index 0000000..b656b2e --- /dev/null +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.module.scss @@ -0,0 +1,14 @@ +.LoginForm { + width: 400px; + display: flex; + flex-direction: column; +} + +.input { + margin-top: 10px; +} + +.loginBtn { + margin-top: 15px; + margin-left: auto; +} diff --git a/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx b/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx new file mode 100644 index 0000000..dd6a517 --- /dev/null +++ b/src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx @@ -0,0 +1,30 @@ +import { classNames } from 'shared/lib/classNames/classNames'; +import { useTranslation } from 'react-i18next'; +import { Button, ButtonTheme } from 'shared/ui/Button/Button'; +import { Input } from 'shared/ui/Input/Input'; +import cls from './LoginForm.module.scss'; + +interface LoginFormProps { + className?: string; +} + +export const LoginForm = ({ className }: LoginFormProps) => { + const { t } = useTranslation(); + return ( +
+ + + + +
+ ); +}; diff --git a/src/features/AuthByUsername/ui/LoginModal/LoginModal.module.scss b/src/features/AuthByUsername/ui/LoginModal/LoginModal.module.scss new file mode 100644 index 0000000..b4273ab --- /dev/null +++ b/src/features/AuthByUsername/ui/LoginModal/LoginModal.module.scss @@ -0,0 +1,2 @@ +.LoginModal { +} diff --git a/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx b/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx new file mode 100644 index 0000000..0c0d739 --- /dev/null +++ b/src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx @@ -0,0 +1,16 @@ +import { classNames } from 'shared/lib/classNames/classNames'; +import { Modal } from 'shared/ui/Modal/Modal'; +import { LoginForm } from 'features/AuthByUsername/ui/LoginForm/LoginForm'; +import cls from './LoginModal.module.scss'; + +interface LoginModalProps { + className?: string; + isOpen: boolean; + onClose: () => void; +} + +export const LoginModal = ({ className, isOpen, onClose }: LoginModalProps) => ( + + + +); diff --git a/src/shared/ui/Input/Input.module.scss b/src/shared/ui/Input/Input.module.scss new file mode 100644 index 0000000..df17ee0 --- /dev/null +++ b/src/shared/ui/Input/Input.module.scss @@ -0,0 +1,49 @@ +.InputWrapper { + display: flex; +} + +.placeholder { + margin-right: 5px; +} + +.input { + background: transparent; + border: none; + outline: none; + width: 100%; + color: transparent; + text-shadow: 0 0 0 var(--primary-color); + + &:focus { + outline: none; + } +} + +.caretWrapper { + flex-grow: 1; + position: relative; +} + +.caret { + height: 3px; + width: 9px; + background: var(--primary-color); + bottom: 0; + left: 0; + position: absolute; + animation: blink 0.7s forwards infinite; +} + +@keyframes blink { + 0% { + opacity: 0; + } + + 50% { + opacity: 0.01; + } + + 100% { + opacity: 1; + } +} diff --git a/src/shared/ui/Input/Input.tsx b/src/shared/ui/Input/Input.tsx new file mode 100644 index 0000000..010da18 --- /dev/null +++ b/src/shared/ui/Input/Input.tsx @@ -0,0 +1,92 @@ +import { classNames } from 'shared/lib/classNames/classNames'; +import { + ChangeEvent, + InputHTMLAttributes, + memo, + useEffect, + useRef, + useState, +} from 'react'; +import cls from './Input.module.scss'; + +type HTMLInputProps = Omit< + InputHTMLAttributes, + 'value' | 'onChange' +>; + +interface InputProps extends HTMLInputProps { + className?: string; + value?: string; + onChange?: (value: string) => void; + autofocus?: boolean; + +} + +export const Input = memo((props: InputProps) => { + const { + className, + value, + onChange, + type = 'text', + placeholder, + autofocus, + + ...otherProps + } = props; + const ref = useRef(null); + const [isFocused, setIsFocused] = useState(false); + const [caretPosition, setCaretPosition] = useState(0); + + const onChangeHandler = (e: ChangeEvent) => { + onChange?.(e.target.value); + }; + + const onBlur = () => { + setIsFocused(false); + }; + + const onFocus = () => { + setIsFocused(true); + }; + + const onSelect = (e: any) => { + setCaretPosition(e?.target?.selectionStart || 0); + }; + + useEffect(() => { + if (autofocus) { + setIsFocused(true); + ref.current?.focus(); + } + }, [autofocus]); + + return ( +
+ {placeholder && ( +
+ {`${placeholder}>`} +
+ )} + +
+ + {isFocused && ( + + )} +
+
+ ); +}); diff --git a/src/shared/ui/Modal/Modal.tsx b/src/shared/ui/Modal/Modal.tsx index b47fd45..96635da 100644 --- a/src/shared/ui/Modal/Modal.tsx +++ b/src/shared/ui/Modal/Modal.tsx @@ -14,6 +14,7 @@ interface ModalProps { children?: ReactNode; isOpen?: boolean; onClose?: () => void; + lazy?:boolean; } const ANIMATION_DELAY = 300; @@ -24,11 +25,19 @@ export const Modal = (props: ModalProps) => { children, isOpen, onClose, + lazy, } = props; const [isClosing, setIsClosing] = useState(false); + const [isMounted, setIsMounted] = useState(false); const timerRef = useRef>(); + useEffect(() => { + if (isOpen) { + setIsMounted(true); + } + }, [isOpen]); + const closeHandler = useCallback(() => { if (onClose) { setIsClosing(true); @@ -65,6 +74,10 @@ export const Modal = (props: ModalProps) => { e.stopPropagation(); }; + if (lazy && !isMounted) { + return null; + } + return (
diff --git a/src/widgets/Navbar/ui/Navbar.tsx b/src/widgets/Navbar/ui/Navbar.tsx index c9b35d5..d07116e 100644 --- a/src/widgets/Navbar/ui/Navbar.tsx +++ b/src/widgets/Navbar/ui/Navbar.tsx @@ -1,8 +1,8 @@ import { classNames } from 'shared/lib/classNames/classNames'; -import { Modal } from 'shared/ui/Modal/Modal'; import React, { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, ButtonTheme } from 'shared/ui/Button/Button'; +import { LoginModal } from 'features/AuthByUsername'; import cls from './Navbar.module.scss'; interface NavbarProps { @@ -13,8 +13,12 @@ export const Navbar = ({ className }: NavbarProps) => { const { t } = useTranslation(); const [isAuthModal, setIsAuthModal] = useState(false); - const onToggleModal = useCallback(() => { - setIsAuthModal((prev) => !prev); + const onShowModal = useCallback(() => { + setIsAuthModal(true); + }, []); + + const onClose = useCallback(() => { + setIsAuthModal(false); }, []); return ( @@ -22,16 +26,11 @@ export const Navbar = ({ className }: NavbarProps) => { - - Lorem ipsum dolor sit amet, consectetur adipisicing elit. A ab - accusantium aliquid architecto autem consequuntur debitis et facere, - laudantium modi nostrum nulla obcaecati odit quas quod sapiente, - veritatis? Illo, veritatis. - +
); };