addded change profile

This commit is contained in:
a.krylov
2026-06-09 00:01:58 +03:00
parent 5282c30f10
commit 63ce7765df
12 changed files with 374 additions and 50 deletions

View File

@@ -1,9 +1,17 @@
import LogoIcon from "./icons/LogoIcon";
import React from "react";
import React, { useState } from "react";
import {NAV_BOTTOM, NAV_ITEMS} from "../config/feedConstants";
import {NavIcons} from "./icons/NavIcons";
import Profile from "./profile/Profile";
export default function Sidebar({ open, activeNav, onClose, onNavClick }) {
const [profileData, setProfileData] = useState({
firstName: "Иван",
lastName: "Иванов",
email: "ivan@example.com",
phone: "+7 (999) 123-45-67",
});
return (
<>
<div className={`f-overlay${open ? " show" : ""}`} onClick={onClose} />
@@ -39,6 +47,8 @@ export default function Sidebar({ open, activeNav, onClose, onNavClick }) {
</div>
);
})}
<div className="f-sb-divider" />
<Profile data={profileData} />
</div>
</div>
</>

View File

@@ -5,14 +5,14 @@ export const NavIcons = {
<rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
),
alert: () => (
box: () => (
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round">
<circle cx="12" cy="12" r="9" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /><polyline points="3.27 6.96 12 12.01 20.73 6.96" /><line x1="12" y1="22.08" x2="12" y2="12" />
</svg>
),
search: () => (
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round">
<circle cx="11" cy="11" r="7" /><line x1="16.5" y1="16.5" x2="21" y2="21" />
paw: () => (
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round">
<path d="M6 8c0-1 1-2 2-2s2 1 2 2v1M16 8c0-1 1-2 2-2s2 1 2 2v1M8 10h8M10 13h4M6 12c-1 0-2 1.5-2 3 0 2.5 1.5 4 2.5 4h9c1 0 2.5-1.5 2.5-4 0-1.5-1-3-2-3" />
</svg>
),
chat: () => (

View File

@@ -0,0 +1,19 @@
import React, { useState } from "react";
import ProfileModal from "./ProfileModal";
import ProfileAvatar from "./ProfileAvatar";
import ProfileInfo from "./ProfileInfo";
export default function Profile({ data = {} }) {
const [showModal, setShowModal] = useState(false);
return (
<>
<div className="f-profile" onClick={() => setShowModal(true)}>
<ProfileAvatar avatar={data.avatar} name={`${data.firstName || ""} ${data.lastName || ""}`} />
<ProfileInfo firstName={data.firstName} email={data.email} />
</div>
{showModal && <ProfileModal onClose={() => setShowModal(false)} initialData={data} />}
</>
);
}

View File

@@ -0,0 +1,19 @@
// ─── Profile avatar component ───────────────────────────────────────────────
export default function ProfileAvatar({ avatar, name }) {
if (avatar) {
return (
<img
src={avatar}
alt={name}
className="f-profile-avatar"
title={name}
/>
);
}
return (
<div className="f-profile-avatar-placeholder" title={name}>
👤
</div>
);
}

View File

@@ -0,0 +1,137 @@
import React from "react";
import CloseIcon from "../icons/CloseIcon";
// ─── Input field component ───────────────────────────────────────────────────
function InputField({ label, error, ...props }) {
return (
<div className="f-m-group">
<label className="f-m-label">{label}</label>
<input
className={`f-m-field${error ? " err" : ""}`}
{...props}
/>
{error && <p className="f-err-msg">{error}</p>}
</div>
);
}
// ─── Image upload component ──────────────────────────────────────────────────
function ImageUpload({
displayImage,
imgError,
compressing,
fileInputRef,
onFileChange,
onRemove,
}) {
const MAX_SIZE_MB = 5;
return (
<div className="f-m-group">
<label className="f-m-label">Фото профиля</label>
{displayImage ? (
<div className="f-img-preview-wrap">
<img src={displayImage} alt="profile" className="f-img-preview" />
<button
className="f-img-remove"
onClick={onRemove}
type="button"
aria-label="Удалить фото"
>
<CloseIcon />
</button>
</div>
) : (
<label className="f-img-upload-label" aria-disabled={compressing}>
<input
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/webp"
onChange={onFileChange}
className="f-img-input"
disabled={compressing}
/>
{compressing ? <span>Обработка...</span> : <span>📷 Добавить фото</span>}
</label>
)}
{imgError && <p className="f-err-msg">{imgError}</p>}
<p className="f-hint">JPEG / PNG / WebP · до {MAX_SIZE_MB} МБ</p>
</div>
);
}
// ─── Name fields row ─────────────────────────────────────────────────────────
function NameFieldsRow({ form, errors, onChange }) {
return (
<div className="f-m-row">
<InputField
label="Имя"
placeholder="Иван"
value={form.firstName}
error={errors.firstName}
onChange={(e) => onChange("firstName", e.target.value)}
/>
<InputField
label="Фамилия"
placeholder="Иванов"
value={form.lastName}
error={errors.lastName}
onChange={(e) => onChange("lastName", e.target.value)}
/>
</div>
);
}
// ─── Contact fields ─────────────────────────────────────────────────────────
function ContactFields({ form, errors, onChange, onPhoneChange }) {
return (
<>
<InputField
label="Номер телефона"
type="tel"
placeholder="+7 (___) ___-__-__"
value={form.phone}
error={errors.phone}
onChange={(e) => onPhoneChange(e.target.value)}
/>
<InputField
label="Электронная почта"
type="email"
placeholder="example@email.com"
value={form.email}
error={errors.email}
onChange={(e) => onChange("email", e.target.value)}
/>
</>
);
}
// ─── Submit button ──────────────────────────────────────────────────────────
function SubmitButton({ loading, onSubmit }) {
return (
<button
className="f-m-submit"
onClick={onSubmit}
disabled={loading}
>
<div className="f-m-submit-row">
{loading && <div className="f-m-spinner" />}
Сохранить профиль
</div>
</button>
);
}
// ─── Modal header ──────────────────────────────────────────────────────────
function ModalHeader({ onClose }) {
return (
<div className="f-modal-head">
<span className="f-modal-title">Редактирование профиля</span>
<button className="f-close-btn" onClick={onClose}>
<CloseIcon />
</button>
</div>
);
}
export { InputField, ImageUpload, NameFieldsRow, ContactFields, SubmitButton, ModalHeader };

View File

@@ -0,0 +1,11 @@
// ─── Profile info component ─────────────────────────────────────────────────
export default function ProfileInfo({ firstName, email }) {
const displayName = firstName || "Профиль";
return (
<div className="f-profile-info">
<div className="f-profile-name">{displayName}</div>
{email && <div className="f-profile-email">{email}</div>}
</div>
);
}

View File

@@ -0,0 +1,92 @@
import { useState, useCallback } from "react";
import { formatPhone } from "../../utils/formatPhone";
import { useImageUpload } from "../../hooks/useImageUpload";
import { validateProfileForm, initializeFormState } from "./profileValidation";
import {
ImageUpload,
NameFieldsRow,
ContactFields,
SubmitButton,
ModalHeader,
} from "./ProfileFormFields";
export default function ProfileModal({ onClose, initialData = {} }) {
const [form, setForm] = useState(initializeFormState(initialData));
const [errors, setErrors] = useState({});
const existingImageUrl = initialData.avatar || null;
const {
imageFile,
displayImage,
imgError,
compressing,
fileInputRef,
handleFileChange,
removeImage,
} = useImageUpload(existingImageUrl);
// ─── Field change handler ───────────────────────────────────────────────
const handleFieldChange = useCallback((key, value) => {
setForm(prev => ({ ...prev, [key]: value }));
setErrors(prev => ({ ...prev, [key]: "" }));
}, []);
// ─── Phone field change with formatting ──────────────────────────────────
const handlePhoneChange = useCallback((value) => {
const formatted = formatPhone(value);
handleFieldChange("phone", formatted);
}, [handleFieldChange]);
// ─── Submit handler ─────────────────────────────────────────────────────
const handleSubmit = async () => {
const validationErrors = validateProfileForm(form);
if (Object.keys(validationErrors).length) {
setErrors(validationErrors);
return;
}
const profileData = {
firstName: form.firstName.trim(),
lastName: form.lastName.trim(),
phone: form.phone.trim(),
email: form.email.trim(),
avatar: imageFile || existingImageUrl,
};
console.log("Profile data to save:", profileData);
onClose();
};
return (
<div className="f-modal-bg" onClick={e => e.target === e.currentTarget && onClose()}>
<div className="f-modal">
<ModalHeader onClose={onClose} />
<ImageUpload
displayImage={displayImage}
imgError={imgError}
compressing={compressing}
fileInputRef={fileInputRef}
onFileChange={handleFileChange}
onRemove={removeImage}
/>
<NameFieldsRow
form={form}
errors={errors}
onChange={handleFieldChange}
/>
<ContactFields
form={form}
errors={errors}
onChange={handleFieldChange}
onPhoneChange={handlePhoneChange}
/>
<SubmitButton loading={compressing} onSubmit={handleSubmit} />
</div>
</div>
);
}

View File

@@ -0,0 +1,5 @@
export { default as Profile } from "./Profile";
export { default as ProfileModal } from "./ProfileModal";
export { default as ProfileAvatar } from "./ProfileAvatar";
export { default as ProfileInfo } from "./ProfileInfo";
export { validateProfileForm, initializeFormState } from "./profileValidation";

View File

@@ -0,0 +1,32 @@
// ─── Form validation logic ──────────────────────────────────────────────────
export function validateProfileForm(form) {
const errors = {};
if (!form.firstName.trim()) {
errors.firstName = "Введите имя";
}
if (!form.lastName.trim()) {
errors.lastName = "Введите фамилию";
}
if (form.email && !form.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
errors.email = "Некорректный email";
}
if (form.phone && form.phone.replace(/\D/g, "").length < 11) {
errors.phone = "Некорректный номер телефона";
}
return errors;
}
// ─── Form state initialization ──────────────────────────────────────────────
export function initializeFormState(initialData = {}) {
return {
firstName: initialData.firstName || "",
lastName: initialData.lastName || "",
phone: initialData.phone || "",
email: initialData.email || "",
};
}