Skip to content

Commit

Permalink
feat: user reducer / input component
Browse files Browse the repository at this point in the history
  • Loading branch information
TomatoVan committed Nov 9, 2023
1 parent 24347a2 commit 61025fb
Show file tree
Hide file tree
Showing 16 changed files with 268 additions and 17 deletions.
3 changes: 2 additions & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
"О сайте": "About us",
"Короткий язык": "En",
"increment": "increment",
"decrement": "decrement"
"decrement": "decrement",
"login": "Login"
}
3 changes: 2 additions & 1 deletion public/locales/ru/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
"О сайте": "О сайте",
"Короткий язык": "Ru",
"increment": "Увеличить",
"decrement": "Уменьшить"
"decrement": "Уменьшить",
"login": "Войти"
}
4 changes: 3 additions & 1 deletion src/app/providers/StoreProvider/config/StateSchema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { CounterSchema } from 'entities/Counter';
import { UserSchema } from 'entities/User';

export interface StateSchema {
counter: CounterSchema
counter: CounterSchema;
user: UserSchema;

}
12 changes: 8 additions & 4 deletions src/app/providers/StoreProvider/config/store.ts
Original file line number Diff line number Diff line change
@@ -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<StateSchema> = {
counter: counterReducer,
user: userReducer,
};

return configureStore<StateSchema>({
reducer: {
counter: counterReducer,
},
reducer: rootReducers,
devTools: __IS_DEV__,
preloadedState: initialState,
});
Expand Down
3 changes: 3 additions & 0 deletions src/entities/User/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { userReducer, userActions } from './model/slice/userSlice';

export { UserSchema, User } from './model/types/user';
16 changes: 16 additions & 0 deletions src/entities/User/model/slice/userSlice.ts
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 8 additions & 0 deletions src/entities/User/model/types/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface User {
id: string;
username: string;
}

export interface UserSchema {
authData?: User;
}
1 change: 1 addition & 0 deletions src/features/AuthByUsername/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { LoginModal } from './ui/LoginModal/LoginModal';
14 changes: 14 additions & 0 deletions src/features/AuthByUsername/ui/LoginForm/LoginForm.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.LoginForm {
width: 400px;
display: flex;
flex-direction: column;
}

.input {
margin-top: 10px;
}

.loginBtn {
margin-top: 15px;
margin-left: auto;
}
30 changes: 30 additions & 0 deletions src/features/AuthByUsername/ui/LoginForm/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={classNames(cls.LoginForm, {}, [className])}>
<Input
autofocus
type="text"
className={cls.input}
placeholder={t('Введите username')}
/>
<Input
type="text"
className={cls.input}
placeholder={t('Введите пароль')}
/>

<Button theme={ButtonTheme.OUTLINE} className={cls.loginBtn}>{t('login')}</Button>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.LoginModal {
}
16 changes: 16 additions & 0 deletions src/features/AuthByUsername/ui/LoginModal/LoginModal.tsx
Original file line number Diff line number Diff line change
@@ -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) => (
<Modal lazy onClose={onClose} isOpen={isOpen} className={classNames(cls.LoginModal, {}, [className])}>
<LoginForm />
</Modal>
);
49 changes: 49 additions & 0 deletions src/shared/ui/Input/Input.module.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
92 changes: 92 additions & 0 deletions src/shared/ui/Input/Input.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>,
'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<HTMLInputElement>(null);
const [isFocused, setIsFocused] = useState(false);
const [caretPosition, setCaretPosition] = useState(0);

const onChangeHandler = (e: ChangeEvent<HTMLInputElement>) => {
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 (
<div className={classNames(cls.InputWrapper, {}, [className])}>
{placeholder && (
<div className={cls.placeholder}>
{`${placeholder}>`}
</div>
)}

<div className={cls.caretWrapper}>
<input
ref={ref}
type={type}
value={value}
onChange={onChangeHandler}
className={cls.input}
onBlur={onBlur}
onSelect={onSelect}
onFocus={onFocus}
{...otherProps}
/>
{isFocused && (
<span
className={cls.caret}
style={{ left: `${caretPosition * 9}px` }}
/>
)}
</div>
</div>
);
});
13 changes: 13 additions & 0 deletions src/shared/ui/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface ModalProps {
children?: ReactNode;
isOpen?: boolean;
onClose?: () => void;
lazy?:boolean;
}

const ANIMATION_DELAY = 300;
Expand All @@ -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<ReturnType<typeof setTimeout>>();

useEffect(() => {
if (isOpen) {
setIsMounted(true);
}
}, [isOpen]);

const closeHandler = useCallback(() => {
if (onClose) {
setIsClosing(true);
Expand Down Expand Up @@ -65,6 +74,10 @@ export const Modal = (props: ModalProps) => {
e.stopPropagation();
};

if (lazy && !isMounted) {
return null;
}

return (
<Portal>
<div className={classNames(cls.Modal, mods, [className])}>
Expand Down
19 changes: 9 additions & 10 deletions src/widgets/Navbar/ui/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -13,25 +13,24 @@ 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 (
<div className={classNames(cls.Navbar, {}, [className])}>
<Button
theme={ButtonTheme.CLEAR_INVERTED}
className={cls.links}
onClick={onToggleModal}
onClick={onShowModal}
>
{t('Войти')}
</Button>
<Modal isOpen={isAuthModal} onClose={onToggleModal}>
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.
</Modal>
<LoginModal isOpen={isAuthModal} onClose={onClose} />
</div>
);
};

0 comments on commit 61025fb

Please sign in to comment.