addded change profile
This commit is contained in:
42
package-lock.json
generated
42
package-lock.json
generated
@@ -3182,9 +3182,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3202,9 +3199,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3222,9 +3216,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3242,9 +3233,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3262,9 +3250,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3282,9 +3267,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3869,9 +3851,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3888,9 +3867,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3907,9 +3883,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -3926,9 +3899,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -11962,9 +11932,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -11985,9 +11952,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12008,9 +11972,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -12031,9 +11992,6 @@
|
|||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import LogoIcon from "./icons/LogoIcon";
|
import LogoIcon from "./icons/LogoIcon";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import {NAV_BOTTOM, NAV_ITEMS} from "../config/feedConstants";
|
import {NAV_BOTTOM, NAV_ITEMS} from "../config/feedConstants";
|
||||||
import {NavIcons} from "./icons/NavIcons";
|
import {NavIcons} from "./icons/NavIcons";
|
||||||
|
import Profile from "./profile/Profile";
|
||||||
|
|
||||||
export default function Sidebar({ open, activeNav, onClose, onNavClick }) {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`f-overlay${open ? " show" : ""}`} onClick={onClose} />
|
<div className={`f-overlay${open ? " show" : ""}`} onClick={onClose} />
|
||||||
@@ -39,6 +47,8 @@ export default function Sidebar({ open, activeNav, onClose, onNavClick }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<div className="f-sb-divider" />
|
||||||
|
<Profile data={profileData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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" />
|
<rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
alert: () => (
|
box: () => (
|
||||||
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round">
|
<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>
|
</svg>
|
||||||
),
|
),
|
||||||
search: () => (
|
paw: () => (
|
||||||
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round">
|
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="round" strokeLinejoin="round">
|
||||||
<circle cx="11" cy="11" r="7" /><line x1="16.5" y1="16.5" x2="21" y2="21" />
|
<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>
|
</svg>
|
||||||
),
|
),
|
||||||
chat: () => (
|
chat: () => (
|
||||||
|
|||||||
19
src/components/profile/Profile.jsx
Normal file
19
src/components/profile/Profile.jsx
Normal 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} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
src/components/profile/ProfileAvatar.jsx
Normal file
19
src/components/profile/ProfileAvatar.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
137
src/components/profile/ProfileFormFields.jsx
Normal file
137
src/components/profile/ProfileFormFields.jsx
Normal 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 };
|
||||||
11
src/components/profile/ProfileInfo.jsx
Normal file
11
src/components/profile/ProfileInfo.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
92
src/components/profile/ProfileModal.jsx
Normal file
92
src/components/profile/ProfileModal.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/components/profile/index.js
Normal file
5
src/components/profile/index.js
Normal 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";
|
||||||
32
src/components/profile/profileValidation.js
Normal file
32
src/components/profile/profileValidation.js
Normal 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 || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// ─── CONSTANTS ────────────────────────────────────────────────────────────────
|
// ─── CONSTANTS ────────────────────────────────────────────────────────────────
|
||||||
export const NAV_ITEMS = [
|
export const NAV_ITEMS = [
|
||||||
{ icon: "grid", label: "Главная" },
|
{ icon: "grid", label: "Главная" },
|
||||||
{ icon: "alert", label: "Потери" },
|
{ icon: "box", label: "Вещи" },
|
||||||
{ icon: "search", label: "Находки" },
|
{ icon: "paw", label: "Питомцы" },
|
||||||
{ icon: "chat", label: "Чат" },
|
{ icon: "chat", label: "Чат" },
|
||||||
{ icon: "file", label: "Мои объявления" },
|
{ icon: "file", label: "Мои объявления" },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -93,6 +93,47 @@
|
|||||||
box-shadow: 0 0 12px rgba(45,224,200,.3); /* мягкое свечение */
|
box-shadow: 0 0 12px rgba(45,224,200,.3); /* мягкое свечение */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Profile Component ── */
|
||||||
|
.f-profile {
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
padding: 10px 14px; border-radius: 12px;
|
||||||
|
cursor: pointer; transition: background .15s;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
.f-profile:hover {
|
||||||
|
background: rgba(45, 224, 200, .08);
|
||||||
|
border-color: rgba(45, 224, 200, .25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.f-profile-avatar {
|
||||||
|
width: 40px; height: 40px;
|
||||||
|
border-radius: 50%; object-fit: cover;
|
||||||
|
flex-shrink: 0; border: 2px solid var(--teal);
|
||||||
|
}
|
||||||
|
.f-profile-avatar-placeholder {
|
||||||
|
width: 40px; height: 40px;
|
||||||
|
border-radius: 50%; flex-shrink: 0;
|
||||||
|
background: rgba(45, 224, 200, .15);
|
||||||
|
border: 2px solid rgba(45, 224, 200, .3);
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f-profile-info { flex: 1; min-width: 0; }
|
||||||
|
.f-profile-name {
|
||||||
|
color: #fff; font-weight: 700; font-size: .85rem;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.f-profile-email {
|
||||||
|
color: var(--muted); font-size: .7rem;
|
||||||
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f-sb-divider {
|
||||||
|
height: 1px; background: rgba(255, 255, 255, .08);
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.f-right {
|
.f-right {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user