diff --git a/package-lock.json b/package-lock.json index b3999eb..43ff23e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3182,9 +3182,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3202,9 +3199,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3222,9 +3216,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3242,9 +3233,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3262,9 +3250,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3282,9 +3267,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3869,9 +3851,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3888,9 +3867,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3907,9 +3883,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3926,9 +3899,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -11962,9 +11932,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11985,9 +11952,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12008,9 +11972,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -12031,9 +11992,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index ac1dda8..9c00ded 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -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 ( <>
@@ -39,6 +47,8 @@ export default function Sidebar({ open, activeNav, onClose, onNavClick }) {
); })} +
+
diff --git a/src/components/icons/NavIcons.jsx b/src/components/icons/NavIcons.jsx index 304f75d..e16b378 100644 --- a/src/components/icons/NavIcons.jsx +++ b/src/components/icons/NavIcons.jsx @@ -5,14 +5,14 @@ export const NavIcons = { ), - alert: () => ( + box: () => ( - + ), - search: () => ( - - + paw: () => ( + + ), chat: () => ( diff --git a/src/components/profile/Profile.jsx b/src/components/profile/Profile.jsx new file mode 100644 index 0000000..cc43ab1 --- /dev/null +++ b/src/components/profile/Profile.jsx @@ -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 ( + <> +
setShowModal(true)}> + + +
+ + {showModal && setShowModal(false)} initialData={data} />} + + ); +} diff --git a/src/components/profile/ProfileAvatar.jsx b/src/components/profile/ProfileAvatar.jsx new file mode 100644 index 0000000..07dd5bf --- /dev/null +++ b/src/components/profile/ProfileAvatar.jsx @@ -0,0 +1,19 @@ +// ─── Profile avatar component ─────────────────────────────────────────────── +export default function ProfileAvatar({ avatar, name }) { + if (avatar) { + return ( + {name} + ); + } + + return ( +
+ 👤 +
+ ); +} diff --git a/src/components/profile/ProfileFormFields.jsx b/src/components/profile/ProfileFormFields.jsx new file mode 100644 index 0000000..52ab918 --- /dev/null +++ b/src/components/profile/ProfileFormFields.jsx @@ -0,0 +1,137 @@ +import React from "react"; +import CloseIcon from "../icons/CloseIcon"; + +// ─── Input field component ─────────────────────────────────────────────────── +function InputField({ label, error, ...props }) { + return ( +
+ + + {error &&

{error}

} +
+ ); +} + +// ─── Image upload component ────────────────────────────────────────────────── +function ImageUpload({ + displayImage, + imgError, + compressing, + fileInputRef, + onFileChange, + onRemove, +}) { + const MAX_SIZE_MB = 5; + + return ( +
+ + {displayImage ? ( +
+ profile + +
+ ) : ( + + )} + {imgError &&

{imgError}

} +

JPEG / PNG / WebP · до {MAX_SIZE_MB} МБ

+
+ ); +} + +// ─── Name fields row ───────────────────────────────────────────────────────── +function NameFieldsRow({ form, errors, onChange }) { + return ( +
+ onChange("firstName", e.target.value)} + /> + onChange("lastName", e.target.value)} + /> +
+ ); +} + +// ─── Contact fields ───────────────────────────────────────────────────────── +function ContactFields({ form, errors, onChange, onPhoneChange }) { + return ( + <> + onPhoneChange(e.target.value)} + /> + onChange("email", e.target.value)} + /> + + ); +} + +// ─── Submit button ────────────────────────────────────────────────────────── +function SubmitButton({ loading, onSubmit }) { + return ( + + ); +} + +// ─── Modal header ────────────────────────────────────────────────────────── +function ModalHeader({ onClose }) { + return ( +
+ Редактирование профиля + +
+ ); +} + +export { InputField, ImageUpload, NameFieldsRow, ContactFields, SubmitButton, ModalHeader }; diff --git a/src/components/profile/ProfileInfo.jsx b/src/components/profile/ProfileInfo.jsx new file mode 100644 index 0000000..2f3a087 --- /dev/null +++ b/src/components/profile/ProfileInfo.jsx @@ -0,0 +1,11 @@ +// ─── Profile info component ───────────────────────────────────────────────── +export default function ProfileInfo({ firstName, email }) { + const displayName = firstName || "Профиль"; + + return ( +
+
{displayName}
+ {email &&
{email}
} +
+ ); +} diff --git a/src/components/profile/ProfileModal.jsx b/src/components/profile/ProfileModal.jsx new file mode 100644 index 0000000..e382810 --- /dev/null +++ b/src/components/profile/ProfileModal.jsx @@ -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 ( +
e.target === e.currentTarget && onClose()}> +
+ + + + + + + + + +
+
+ ); +} diff --git a/src/components/profile/index.js b/src/components/profile/index.js new file mode 100644 index 0000000..d798750 --- /dev/null +++ b/src/components/profile/index.js @@ -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"; diff --git a/src/components/profile/profileValidation.js b/src/components/profile/profileValidation.js new file mode 100644 index 0000000..d86ce44 --- /dev/null +++ b/src/components/profile/profileValidation.js @@ -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 || "", + }; +} diff --git a/src/config/feedConstants.js b/src/config/feedConstants.js index 9acf3dc..773c5c5 100644 --- a/src/config/feedConstants.js +++ b/src/config/feedConstants.js @@ -1,8 +1,8 @@ // ─── CONSTANTS ──────────────────────────────────────────────────────────────── export const NAV_ITEMS = [ { icon: "grid", label: "Главная" }, - { icon: "alert", label: "Потери" }, - { icon: "search", label: "Находки" }, + { icon: "box", label: "Вещи" }, + { icon: "paw", label: "Питомцы" }, { icon: "chat", label: "Чат" }, { icon: "file", label: "Мои объявления" }, ]; diff --git a/src/styles/sidebar.css b/src/styles/sidebar.css index 1cadf87..7fec599 100644 --- a/src/styles/sidebar.css +++ b/src/styles/sidebar.css @@ -93,6 +93,47 @@ 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) { .f-right { display: none;