-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[SOK-22] User profile page #13
Changes from 1 commit
4110c13
1597380
442c996
ab29ccb
82efa62
c08cc7f
901e7f1
888ba27
a4590ad
0533838
5f8153f
44d145a
01a321d
a61b010
f9a4f97
61ab747
0d46090
7b59c39
dff7926
3cb9e99
b39dbb2
ee6ad2b
bd06de6
eef02e0
c3d32aa
7bfdf3b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import React, { useState, useRef, useEffect } from 'react' | ||
import { Image } from '@/components/ui/Image/Image' | ||
import { Button } from '@/components/ui/Button/Button' | ||
|
||
export const AVATAR_SRC = 'https://ya-praktikum.tech/api/v2/resources' | ||
|
||
export const Avatar = (props: { | ||
src: string | ||
containerClassName?: string | ||
imageClassName?: string | ||
onAvatarChange: (file: File) => void | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. В этом компоненте ничего больше нельзя поменять, я думаю проще переименовать onAvatarChange > onChange There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Поправил |
||
}) => { | ||
const { src, containerClassName, imageClassName, onAvatarChange } = props | ||
const [isHovered, setIsHovered] = useState(false) | ||
const [previewUrl, setPreviewUrl] = useState<string | null>(null) | ||
const fileInputRef = useRef<HTMLInputElement>(null) | ||
|
||
useEffect(() => { | ||
return () => { | ||
if (previewUrl) { | ||
URL.revokeObjectURL(previewUrl) | ||
} | ||
} | ||
}, [previewUrl]) | ||
|
||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
const file = event.target.files?.[0] | ||
if (file) { | ||
const fileUrl = URL.createObjectURL(file) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Логику с генерацией url я бы вынес в отдельный хук |
||
setPreviewUrl(fileUrl) | ||
onAvatarChange(file) | ||
} | ||
} | ||
|
||
const handleButtonClick = () => { | ||
fileInputRef.current?.click() | ||
} | ||
|
||
const displaySrc = previewUrl || src | ||
|
||
return ( | ||
<div | ||
className={`${containerClassName}`} | ||
gloginov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
onMouseEnter={() => setIsHovered(true)} | ||
onMouseLeave={() => setIsHovered(false)}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Кажется ты чуть-чуть усложнил)) Как по мне проще сделать так: .avatar-editor { There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Есть такое) |
||
{displaySrc ? ( | ||
<Image src={displaySrc} className={imageClassName} alt="Avatar" /> | ||
) : ( | ||
<div className="avatar-placeholder"> | ||
{/* Здесь можно добавить иконку или текст для placeholder */} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Нужно сразу добавить placeholder, я когда открыл страницу первым делом увидел битое изображение. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Добавил |
||
</div> | ||
)} | ||
{isHovered && ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Вот эту механику лучше вынести в css. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Сделано |
||
<div className="overlay"> | ||
<Button | ||
text="Изменить аватар" | ||
className="change-button" | ||
useFixWidth={false} | ||
onClick={handleButtonClick} | ||
/> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Отсюда лучше убрать компонент кнопки, лучше просто добавить стандартный и стилизовать ее, чем переназначать все стили компонента Button There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Изменил |
||
</div> | ||
)} | ||
<input | ||
ref={fileInputRef} | ||
type="file" | ||
accept="image/*" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Исправил |
||
onChange={handleFileChange} | ||
style={{ display: 'none' }} | ||
/> | ||
</div> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import './Form.scss' | ||
import React from 'react' | ||
|
||
interface FormProps { | ||
className?: string | ||
children?: React.ReactNode | ||
onSubmit?: () => void | ||
} | ||
|
||
export const Form = (props: FormProps) => { | ||
const { onSubmit, className, children } = props | ||
return ( | ||
<form onSubmit={onSubmit} className={`form ${className}`}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Класс form лучше переименовать, во что-то более специфичное, что бы избежать конфликтов. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Сделано |
||
{children} | ||
</form> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
interface ImageProps { | ||
src: string | ||
className?: string | ||
alt?: string | ||
} | ||
|
||
export const Image = (props: ImageProps) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Честно говоря, не понимаю зачем нужен этот компонент, выглядит как будто проще написать img |
||
const { src, className, alt } = props | ||
return <img {...props} /> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
@import '../../../scss/vars'; | ||
|
||
.input-default { | ||
width: 301px; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Эти инпуты будут использоваться на других страницах, ширину лучше сделать 100% или вовсе убрать. Можешь указать ширину для родителя формы и сделать инпут и форму 100% There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Исправил |
||
height: 48px; | ||
border-radius: 12px; | ||
border: 2px solid $c_input-default-border; | ||
text-align: center; | ||
background-color: $c_default-background; | ||
color: $c_font-default; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Забыл указать размер шрифта There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Исправил |
||
&:focus-visible, | ||
&:hover { | ||
outline: none; | ||
border-color: $c_input-default-border-hover; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import './Input.scss' | ||
import React, { ChangeEventHandler, CSSProperties } from 'react' | ||
|
||
interface InputProps { | ||
placeholder?: string | ||
name?: string | ||
className?: string | ||
disabled?: boolean | ||
value?: string | ||
type?: string | ||
onChange?: ChangeEventHandler | undefined | ||
style?: CSSProperties | undefined | ||
} | ||
|
||
export const Input = (props: InputProps) => { | ||
const { value, className, disabled, onChange, style } = props | ||
return ( | ||
<input | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Вот тут наверное стоит добавить обертку, возможно нужно будет добавить иконку или сообщение об ошибке. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Насколько я понял, у тебя в форме есть еще обертка form-group, может в таком случае сделать компонент form-group вместо input. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Добавил |
||
value={value} | ||
className={className} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. По умолчанию добавь класс input-default а если это какой-то специфический элемент, ребята будут передавать свои классы пропсами. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Сделано |
||
disabled={disabled} | ||
onChange={onChange} | ||
style={style} | ||
{...props} | ||
/> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,151 @@ | ||
import './Profile.scss' | ||
import React from 'react' | ||
import { Image } from '@/components/ui/Image/Image' | ||
import ProfileSidebarImg from '@/assets/images/profile-sidebar.png' | ||
import ProfileRightSideImg from '@/assets/images/tank-dead.png' | ||
import { Form } from '@/components/ui/Form/Form' | ||
import { Input } from '@/components/ui/Input/Input' | ||
import { useState } from 'react' | ||
import { Button } from '@/components/ui/Button/Button' | ||
import { useNavigate } from 'react-router-dom' | ||
import { changeUserPassword } from '@/store/reducers/user-reducer' | ||
import { useAppDispatch } from '@/store' | ||
import { Header } from '@/components/common/Header/Header' | ||
import { CustomPageTitle } from '@/components/ui/CustomPageTitle/CustomPageTitle' | ||
|
||
export const ChangePassword = () => { | ||
return <>Смена пароля</> | ||
const dispatch = useAppDispatch() | ||
const [showSaveMessage, setShowSaveMessage] = useState(false) | ||
const [userData, setUserData] = useState({ | ||
old_password: '', | ||
new_password: '', | ||
new_password_confirm: '', | ||
}) | ||
const [error, setError] = useState<string | null>(null) | ||
const navigate = useNavigate() | ||
|
||
const handleInputChange = | ||
(field: string) => (event: React.ChangeEvent<HTMLInputElement>) => { | ||
setUserData(prevData => ({ | ||
...prevData, | ||
[field]: event.target.value, | ||
})) | ||
} | ||
|
||
// TODO: перенести в компонент валдиации (добаваленно сюда времено) | ||
const validatePassword = () => { | ||
if (userData.new_password !== userData.new_password_confirm) { | ||
setError('Пароли не совпадают') | ||
return false | ||
} | ||
return true | ||
} | ||
|
||
const saveData = async ( | ||
event: | ||
| React.FormEvent<HTMLFormElement> | ||
| React.MouseEvent<HTMLButtonElement> | ||
) => { | ||
event.preventDefault() | ||
setError(null) | ||
if (!validatePassword()) { | ||
return | ||
} | ||
|
||
try { | ||
await dispatch( | ||
changeUserPassword({ | ||
oldPassword: userData.old_password, | ||
newPassword: userData.new_password, | ||
}) | ||
) | ||
|
||
setShowSaveMessage(true) | ||
setUserData({ | ||
old_password: '', | ||
new_password: '', | ||
new_password_confirm: '', | ||
}) | ||
|
||
navigate('/profile', { | ||
state: { successMessage: 'Пароль успешно изменен!' }, | ||
}) | ||
} catch (err) { | ||
console.log(err) | ||
setError('Ошибка при смене пароля') | ||
} | ||
} | ||
|
||
return ( | ||
<div className={'profile-page-layout'}> | ||
<Header className={'profile-page-layout__header'} /> | ||
<main className={'profile-page-layout__profile-page'}> | ||
<div className={'profile-page-layout__profile-page__sidebar'}> | ||
<Image | ||
src={ProfileSidebarImg} | ||
className={'profile-page-layout__profile-page__sidebar__image'} | ||
alt={'Profile Sidebar'} | ||
/> | ||
</div> | ||
<div className="profile-page-layout__profile-page__profile-container"> | ||
<CustomPageTitle | ||
className={ | ||
'profile-page-layout__profile-page__profile-container__title' | ||
} | ||
text={'Личный кабинет'} | ||
/> | ||
<Form | ||
className={ | ||
'profile-page-layout__profile-page__profile-container__profile-form__change-password' | ||
}> | ||
<Input | ||
className={'input-default old_password'} | ||
name={'oldPassword'} | ||
type={'password'} | ||
placeholder={'Текущий пароль'} | ||
onChange={handleInputChange('old_password')} | ||
/> | ||
<Input | ||
className={'input-default new_password'} | ||
name={'newPassword'} | ||
type={'password'} | ||
placeholder={'Новый пароль'} | ||
onChange={handleInputChange('new_password')} | ||
/> | ||
<Input | ||
className={'input-default new_password_confirm'} | ||
name={'new_password_confirm'} | ||
type={'password'} | ||
placeholder={'Подтвердите пароль'} | ||
onChange={handleInputChange('new_password_confirm')} | ||
/> | ||
{error && ( | ||
<div | ||
className={ | ||
'profile-page-layout__profile-page__profile-container__error-message' | ||
}> | ||
{error} | ||
</div> | ||
)} | ||
<Button | ||
text={'Сохранить'} | ||
className={'save'} | ||
useFixWidth={true} | ||
onClick={saveData} | ||
/> | ||
</Form> | ||
<Button | ||
text={'Назад'} | ||
className={'link-button back'} | ||
href={'/profile'} | ||
/> | ||
</div> | ||
<Image | ||
src={ProfileRightSideImg} | ||
className={'profile-page-layout__tank-dead'} | ||
alt={'Profile Right Side'} | ||
/> | ||
</main> | ||
</div> | ||
) | ||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Эту константу стоит вынести в env файл, обсуди с Георгием.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Перенёс