Skip to content
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

Merged
merged 26 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4110c13
[SOK-16]
gloginov Sep 27, 2024
1597380
[SOK-16]
gloginov Sep 27, 2024
442c996
[SOK-16]
gloginov Sep 27, 2024
ab29ccb
[SOK-16]
gloginov Sep 27, 2024
82efa62
Верстка главной страницы сайта
Timur233 Sep 26, 2024
c08cc7f
Error Page. Обернул станицу в layout
Timur233 Sep 27, 2024
901e7f1
Правки
Timur233 Sep 27, 2024
888ba27
- Добавил компонент заголовка PageTitle, заголовок
Timur233 Sep 27, 2024
a4590ad
Pre-commit settings
Timur233 Sep 26, 2024
0533838
Pre-commit settings
Timur233 Sep 26, 2024
5f8153f
Отключил eslint в некоторых фалах
Timur233 Sep 26, 2024
44d145a
format
Timur233 Sep 27, 2024
01a321d
Добавил поддержку jsx
Timur233 Sep 27, 2024
a61b010
added: profile page, change password page, from component, input comp…
VladToby Sep 30, 2024
f9a4f97
added: profile page, change password page, from component, input comp…
VladToby Sep 30, 2024
61ab747
Merge branch 'develop' of https://github.com/Timur233/falcon-tanks in…
Timur233 Sep 30, 2024
0d46090
fix profile page layout, fix styles
VladToby Oct 1, 2024
7b59c39
fix avatar component, fix profile page layout, fix default form class…
VladToby Oct 1, 2024
dff7926
Fix input, fix change password page, fix page & styles
VladToby Oct 2, 2024
3cb9e99
Fix profile page background images
VladToby Oct 2, 2024
b39dbb2
Merge with develop
VladToby Oct 2, 2024
ee6ad2b
Merge with develop
VladToby Oct 2, 2024
bd06de6
Fix form erros, rework user reducer, fix avatar src
VladToby Oct 2, 2024
eef02e0
Input max width
VladToby Oct 3, 2024
c3d32aa
Added overlay colors to vars
VladToby Oct 3, 2024
7bfdf3b
delete comments
VladToby Oct 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions packages/client/src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
// import { useEffect } from 'react'

import '@/app/App.scss'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import RootLayout from '@/layouts/root-layout'
import PrivateLayout from '@/layouts/private-layout'
import AuthLayout from '@/layouts/auth-layout'
import PublicLayout from '@/layouts/public-layout'
import RootLayout from '@/layouts/RootLayout/RootLayout'
import PrivateLayout from '@/layouts/PrivateLayout/PrivateLayout'
import AuthLayout from '@/layouts/AuthLayout/AuthLayout'
import PublicLayout from '@/layouts/PublicLayout/PublicLayout'
import { Main } from '@/pages/Main/Main'
import { SignIn } from '@/pages/SignIn/SignIn'
import { SignUp } from '@/pages/SignUp/SignUp'
Expand All @@ -15,7 +14,6 @@ import { Error } from '@/pages/Error/Error'
import { Thread } from '@/pages/Thread/Thread'
import { Leaderboard } from '@/pages/Leaderboard/Leaderboard'
import { Profile } from '@/pages/Profile/Profile'
import { ProfileEdit } from '@/pages/Profile/Edit'
import { ChangePassword } from '@/pages/Profile/ChangePassword'

const routerConfig = createBrowserRouter([
Expand Down Expand Up @@ -55,10 +53,6 @@ const routerConfig = createBrowserRouter([
path: '/profile',
element: <Profile />,
},
{
path: '/profile/edit',
element: <ProfileEdit />,
},
{
path: '/profile/change-password',
element: <ChangePassword />,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/client/src/assets/images/tank-dead.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 72 additions & 0 deletions packages/client/src/components/ui/Avatar/Avatar.tsx
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'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Эту константу стоит вынести в env файл, обсуди с Георгием.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Перенёс


export const Avatar = (props: {
src: string
containerClassName?: string
imageClassName?: string
onAvatarChange: (file: File) => void
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

В этом компоненте ничего больше нельзя поменять, я думаю проще переименовать onAvatarChange > onChange

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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)

Choose a reason for hiding this comment

The 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)}>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Кажется ты чуть-чуть усложнил)) Как по мне проще сделать так:

.avatar-editor {
&::hover {
.avatar-editor__overlay {
display: block;
}
}
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 */}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Нужно сразу добавить placeholder, я когда открыл страницу первым делом увидел битое изображение.
Возьми из интернета любой placeholder для аватарки.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Добавил

</div>
)}
{isHovered && (
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вот эту механику лучше вынести в css.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сделано

<div className="overlay">
<Button
text="Изменить аватар"
className="change-button"
useFixWidth={false}
onClick={handleButtonClick}
/>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Отсюда лучше убрать компонент кнопки, лучше просто добавить стандартный и стилизовать ее, чем переназначать все стили компонента Button

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Изменил

</div>
)}
<input
ref={fileInputRef}
type="file"
accept="image/*"
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

АПИ не поддерживает SVG, нужно указать вручную доступные форматы
image

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Исправил

onChange={handleFileChange}
style={{ display: 'none' }}
/>
</div>
)
}
20 changes: 20 additions & 0 deletions packages/client/src/components/ui/Button/Button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,23 @@
text-shadow: $main-text-shadow;
}
}

.link-button {
background: none;
border: none;
font-size: $nav_link-font-size;
font-weight: $nav_link-font-weight;
letter-spacing: 1px;

text-shadow: $main-text-shadow;
text-transform: uppercase;
text-decoration: none;

color: $text-color;
transition: color $animation-base-speed;

&:hover,
&_active {
color: $primary-color;
}
}
10 changes: 8 additions & 2 deletions packages/client/src/components/ui/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Link } from 'react-router-dom'
import './Button.scss'
import React from 'react'

export const Button = (props: {
text: string
className?: string | undefined
useFixWidth?: boolean | undefined
href?: string | undefined
/* eslint-disable @typescript-eslint/no-explicit-any */
onClick?: (() => Promise<void>) | (() => any) | undefined
onClick?:
| ((
event:
| React.MouseEvent<HTMLButtonElement>
| React.FormEvent<HTMLFormElement>
) => void)
| (() => Promise<void>)
}) => {
const { text, className, useFixWidth = false, href = '/', onClick } = props
return (
Expand Down
Empty file.
17 changes: 17 additions & 0 deletions packages/client/src/components/ui/Form/Form.tsx
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}`}>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Класс form лучше переименовать, во что-то более специфичное, что бы избежать конфликтов.
Можно что-то в духе form-fields

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сделано

{children}
</form>
)
}
10 changes: 10 additions & 0 deletions packages/client/src/components/ui/Image/Image.tsx
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) => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Честно говоря, не понимаю зачем нужен этот компонент, выглядит как будто проще написать img

const { src, className, alt } = props
return <img {...props} />
}
17 changes: 17 additions & 0 deletions packages/client/src/components/ui/Input/Input.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@import '../../../scss/vars';

.input-default {
width: 301px;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Эти инпуты будут использоваться на других страницах, ширину лучше сделать 100% или вовсе убрать. Можешь указать ширину для родителя формы и сделать инпут и форму 100%

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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;

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Забыл указать размер шрифта

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Исправил

&:focus-visible,
&:hover {
outline: none;
border-color: $c_input-default-border-hover;
}
}
27 changes: 27 additions & 0 deletions packages/client/src/components/ui/Input/Input.tsx
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вот тут наверное стоит добавить обертку, возможно нужно будет добавить иконку или сообщение об ошибке.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Насколько я понял, у тебя в форме есть еще обертка form-group, может в таком случае сделать компонент form-group вместо input.
У него будет больше функциональности которую в последствии можно будет переиспользовать.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Добавил

value={value}
className={className}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

По умолчанию добавь класс input-default а если это какой-то специфический элемент, ребята будут передавать свои классы пропсами.
Например:
По умолчанию "input-default", а для своей формы могу сделать так "input-default my-form__input"

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Сделано

disabled={disabled}
onChange={onChange}
style={style}
{...props}
/>
)
}
150 changes: 149 additions & 1 deletion packages/client/src/pages/Profile/ChangePassword.tsx
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>
)
}
3 changes: 0 additions & 3 deletions packages/client/src/pages/Profile/Edit.tsx

This file was deleted.

Loading