change design
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import "../styles/index.css";
|
||||
import { getImageUrl } from "../config/authConstants";
|
||||
import Sidebar from "../components/Sidebar";
|
||||
import BottomNav from "../components/nav/BottomNav";
|
||||
import RightSidebar from "../components/RightSidebar";
|
||||
import CreateUpdatePost from "./CreateUpdatePost";
|
||||
import ImageViewer from "../components/ImageViewer";
|
||||
@@ -45,6 +46,11 @@ export default function MainFeed() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [liked, setLiked] = useState(new Set());
|
||||
|
||||
const feedRef = useRef(null);
|
||||
|
||||
const isSearchView = new URLSearchParams(location.search).get("view") === "search";
|
||||
const hasActiveFilters = searchQuery.trim().length > 0 || selectedCities.length > 0;
|
||||
|
||||
const feed = useFeedPosts();
|
||||
const myPosts = useMyPosts(user?.id);
|
||||
const cityFeed = useCityFeed(selectedCities);
|
||||
@@ -135,6 +141,28 @@ export default function MainFeed() {
|
||||
else modals.open("edit", ad);
|
||||
}, [isAuthenticated, modals]);
|
||||
|
||||
const handleHomeClick = useCallback(() => {
|
||||
setActiveNav("home");
|
||||
if (isSearchView) navigate("/feed");
|
||||
feedRef.current?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}, [isSearchView, navigate]);
|
||||
|
||||
const handleSearchClick = useCallback(() => {
|
||||
setActiveNav("home");
|
||||
setSearchQuery("");
|
||||
setSelectedCities([]);
|
||||
if (!isSearchView) navigate("/feed?view=search");
|
||||
feedRef.current?.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}, [isSearchView, navigate]);
|
||||
|
||||
// Карта с объявлениями — иконка добавлена, функционал ещё не реализован
|
||||
const handleMapClick = useCallback(() => {}, []);
|
||||
|
||||
const handleProfileClick = useCallback(() => {
|
||||
if (!isAuthenticated) modals.open("auth");
|
||||
else modals.open("profile");
|
||||
}, [isAuthenticated, modals]);
|
||||
|
||||
const filteredAds = useMemo(() => {
|
||||
let result;
|
||||
if (activeNav === "myads") {
|
||||
@@ -161,7 +189,7 @@ export default function MainFeed() {
|
||||
const listError = activeNav === "myads" ? myPosts.error : feed.error;
|
||||
|
||||
return (
|
||||
<div className="f-app">
|
||||
<div className={`f-app${isSearchView ? " f-search-view" : ""}`}>
|
||||
<Sidebar
|
||||
open={sidebarOpen}
|
||||
activeNav={activeNav}
|
||||
@@ -171,31 +199,28 @@ export default function MainFeed() {
|
||||
onLoginClick={() => modals.open("auth")}
|
||||
onRulesClick={() => modals.open("rules")}
|
||||
onReviewsClick={() => navigate("/reviews")}
|
||||
onCreatePost={handleCreatePost}
|
||||
onProfileClick={() => modals.open("profile")}
|
||||
profileData={profileData}
|
||||
/>
|
||||
|
||||
<div className="f-main">
|
||||
<FeedTopBar
|
||||
onMenuClick={() => setSidebarOpen(v => !v)}
|
||||
onCreatePost={handleCreatePost}
|
||||
isAuthenticated={isAuthenticated}
|
||||
profileData={profileData}
|
||||
onProfileClick={() => modals.open("profile")}
|
||||
onLoginClick={() => modals.open("auth")}
|
||||
t={t}
|
||||
/>
|
||||
<div className="f-main f-main-feed">
|
||||
<FeedTopBar onMenuClick={() => setSidebarOpen(v => !v)} />
|
||||
|
||||
<FeedSearchBar value={searchQuery} onChange={setSearchQuery} t={t} />
|
||||
<div className="f-search-panel">
|
||||
<FeedSearchBar value={searchQuery} onChange={setSearchQuery} t={t} />
|
||||
|
||||
{activeNav === "home" && (
|
||||
<div className="f-city-filter-wrap">
|
||||
<CityFilter cities={cities} value={selectedCities} onChange={setSelectedCities} />
|
||||
</div>
|
||||
)}
|
||||
{activeNav === "home" && (
|
||||
<div className="f-city-filter-wrap">
|
||||
<CityFilter cities={cities} value={selectedCities} onChange={setSelectedCities} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TypeFilterBar value={typeFilter} onChange={setTypeFilter} t={t} />
|
||||
|
||||
<div className="f-wrap">
|
||||
<div className="f-feed">
|
||||
<div className="f-feed" ref={feedRef}>
|
||||
<FeedContent
|
||||
loading={listLoading}
|
||||
cityLoading={cityFeed.cityLoading}
|
||||
@@ -221,7 +246,14 @@ export default function MainFeed() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<RightSidebar />
|
||||
<RightSidebar
|
||||
ads={filteredAds}
|
||||
profileData={profileData}
|
||||
isAuthenticated={isAuthenticated}
|
||||
onProfileClick={() => modals.open("profile")}
|
||||
onLoginClick={() => modals.open("auth")}
|
||||
onNavClick={setActiveNav}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -253,6 +285,18 @@ export default function MainFeed() {
|
||||
postData={modals.modal.payload.ad}
|
||||
/>
|
||||
)}
|
||||
|
||||
<BottomNav
|
||||
activeTab={isSearchView ? "search" : "home"}
|
||||
hasFilters={hasActiveFilters}
|
||||
profileData={profileData}
|
||||
onHome={handleHomeClick}
|
||||
onMap={handleMapClick}
|
||||
onCreatePost={handleCreatePost}
|
||||
onSearch={handleSearchClick}
|
||||
onProfile={handleProfileClick}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {useRef, useState} from "react";
|
||||
import {useRef, useState, useEffect} from "react";
|
||||
import PinIcon from "../components/icons/PinIcon";
|
||||
import CalendarIcon from "../components/icons/CalendarIcon";
|
||||
import PhoneIcon from "../components/icons/PhoneIcon";
|
||||
@@ -14,10 +14,31 @@ import {useTranslation} from 'react-i18next';
|
||||
export default function PostOnFeed({ ad, liked, onLike, onEdit, onDelete, onMarkFound, isOwner, onImageClick, onAuthRequired }) {
|
||||
const { t } = useTranslation();
|
||||
const [showCalendar, setShowCalendar] = useState(false);
|
||||
const [titleExpanded, setTitleExpanded] = useState(false);
|
||||
const [descExpanded, setDescExpanded] = useState(false);
|
||||
const [titleTruncated, setTitleTruncated] = useState(false);
|
||||
const [descTruncated, setDescTruncated] = useState(false);
|
||||
const dateAnchorRef = useRef(null);
|
||||
const titleRef = useRef(null);
|
||||
const descRef = useRef(null);
|
||||
const { isAuthenticated } = useAuthContext();
|
||||
const { contactSeller, error: contactError, clearError } = useContactSeller(ad);
|
||||
|
||||
// Определяем, обрезан ли title/description (тремя точками), и реагируем на ресайз
|
||||
useEffect(() => {
|
||||
const check = () => {
|
||||
if (!titleExpanded && titleRef.current) {
|
||||
setTitleTruncated(titleRef.current.scrollWidth > titleRef.current.clientWidth + 1);
|
||||
}
|
||||
if (!descExpanded && descRef.current) {
|
||||
setDescTruncated(descRef.current.scrollHeight > descRef.current.clientHeight + 1);
|
||||
}
|
||||
};
|
||||
check();
|
||||
window.addEventListener("resize", check);
|
||||
return () => window.removeEventListener("resize", check);
|
||||
}, [titleExpanded, descExpanded]);
|
||||
|
||||
const handleContact = (e) => {
|
||||
e.stopPropagation();
|
||||
if (!isAuthenticated) {
|
||||
@@ -35,131 +56,137 @@ export default function PostOnFeed({ ad, liked, onLike, onEdit, onDelete, onMark
|
||||
|
||||
const isClosed = ad.status === 'CLOSED';
|
||||
|
||||
const roleClass = ad.userRole === 'LOST_AND_FOUND' ? 'f-card-role-lost-found'
|
||||
: ad.userRole === 'METRO' ? 'f-card-role-metro' : '';
|
||||
|
||||
const roleBadge = ad.userRole === 'LOST_AND_FOUND' ? (
|
||||
<div className="f-role-badge f-role-lost-found" title={t('postCard.lostFoundBureau')}>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M12 6v6l4 2"/>
|
||||
</svg>
|
||||
{t('postCard.lostFoundBureau')}
|
||||
const typeBadge = (
|
||||
<div className={`f-type-badge ${ad.type === "loss" ? "f-type-loss" : "f-type-found"}`}>
|
||||
{ad.type === "loss" ? t('postCard.loss') : t('postCard.found')}
|
||||
</div>
|
||||
) : ad.userRole === 'METRO' ? (
|
||||
<div className="f-role-badge f-role-metro" title={t('postCard.metro')}>
|
||||
<span className="f-role-metro-letter">М</span>
|
||||
{t('postCard.metro')}
|
||||
</div>
|
||||
) : null;
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`f-card${isClosed ? ' f-card-closed' : ''}${roleClass ? ' ' + roleClass : ''}`}>
|
||||
<div className={`f-card${isClosed ? ' f-card-closed' : ''}`}>
|
||||
|
||||
<div className="f-card-inner">
|
||||
<div className="f-card-img" onClick={e => { e.stopPropagation(); if (onImageClick) onImageClick(ad.images, 0, ad); }}>
|
||||
{ad.reward && (
|
||||
<div className="f-card-reward">
|
||||
💰 {t("postCard.reward")}
|
||||
</div>
|
||||
)}
|
||||
{imageUrl
|
||||
? <img src={imageUrl} alt={ad.title} />
|
||||
: <span className="f-card-emoji">{ad.emoji || "📋"}</span>
|
||||
}
|
||||
</div>
|
||||
<div className="f-card-body">
|
||||
<div className="f-card-badges">
|
||||
{ad.category && <div className="f-cat-badge">{ad.category}</div>}
|
||||
<div className={`f-type-badge ${ad.type === "loss" ? "f-type-loss"
|
||||
: "f-type-found"}`}>
|
||||
{ad.type === "loss" ? t('postCard.loss') : t('postCard.found')}
|
||||
</div>
|
||||
{roleBadge}
|
||||
</div>
|
||||
<div className="f-card-title">
|
||||
<div className="f-card-head">
|
||||
<div className="f-card-author">
|
||||
<div
|
||||
ref={titleRef}
|
||||
className={`f-card-author-name${titleTruncated ? " f-truncatable" : ""}${titleExpanded ? " f-expanded" : ""}`}
|
||||
onClick={titleTruncated ? () => setTitleExpanded(v => !v) : undefined}
|
||||
>
|
||||
{ad.title}
|
||||
{isClosed && <span className="f-status-badge f-status-closed">{t(
|
||||
'postCard.foundStatus')}</span>}
|
||||
</div>
|
||||
<div className="f-card-desc">{ad.description}</div>
|
||||
<div className="f-card-meta">
|
||||
{address && <div className="f-meta-pill"><PinIcon/> {address}
|
||||
</div>}
|
||||
{createdAt && (
|
||||
<div
|
||||
className="f-meta-pill f-date-pill"
|
||||
ref={dateAnchorRef}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setShowCalendar(s => !s);
|
||||
}}
|
||||
>
|
||||
<CalendarIcon/> {createdAt}
|
||||
{showCalendar && (
|
||||
<DateCalendarPopup
|
||||
date={ad.createdAt}
|
||||
onClose={() => setShowCalendar(false)}
|
||||
anchorRef={dateAnchorRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="f-card-foot">
|
||||
<div className="f-card-foot-left">
|
||||
{ad.phone && (
|
||||
<a className="f-card-phone" href={`tel:${ad.phone}`}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<PhoneIcon/> {ad.phone}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="f-card-foot-right">
|
||||
{!isOwner && !isClosed && (
|
||||
<button type="button" className="f-card-contact"
|
||||
onClick={handleContact}>
|
||||
{t('postCard.write')}
|
||||
</button>
|
||||
)}
|
||||
{onEdit && isOwner && !isClosed && (
|
||||
<button type="button" className="f-card-edit-btn"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onEdit(ad);
|
||||
}}>
|
||||
✏️
|
||||
</button>
|
||||
)}
|
||||
{onDelete && isOwner && (
|
||||
<button type="button" className="f-card-delete-btn"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDelete(ad.id);
|
||||
}}>
|
||||
<TrashIcon/>
|
||||
</button>
|
||||
)}
|
||||
{onMarkFound && isOwner && !isClosed && (
|
||||
<button type="button" className="f-card-found-btn"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onMarkFound(ad.id);
|
||||
}}>
|
||||
{t('postCard.itemFound')}
|
||||
</button>
|
||||
)}
|
||||
<button type="button"
|
||||
className={`f-like-btn${liked ? " liked" : ""}`}
|
||||
</div>
|
||||
<div className="f-card-head-side">
|
||||
{typeBadge}
|
||||
{isClosed && (
|
||||
<span className="f-status-badge f-status-closed">{t('postCard.foundStatus')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="f-card-img" onClick={e => { e.stopPropagation(); if (onImageClick) onImageClick(ad.images, 0, ad); }}>
|
||||
{imageUrl
|
||||
? <img src={imageUrl} alt={ad.title} />
|
||||
: <span className="f-card-emoji">{ad.emoji || "📋"}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="f-card-actions">
|
||||
<div className="f-card-actions-left">
|
||||
<button type="button"
|
||||
className={`f-card-icon-btn${liked ? " liked" : ""}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onLike(ad.id);
|
||||
}}>
|
||||
<HeartIcon filled={liked}/>
|
||||
</button>
|
||||
<span className="f-card-like-count">{ad.likeCount || 0}</span>
|
||||
</div>
|
||||
<div className="f-card-actions-right">
|
||||
{onEdit && isOwner && !isClosed && (
|
||||
<button type="button" className="f-card-edit-btn"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onLike(ad.id);
|
||||
onEdit(ad);
|
||||
}}>
|
||||
<HeartIcon filled={liked}/>
|
||||
{ad.likeCount || 0}
|
||||
✏️
|
||||
</button>
|
||||
)}
|
||||
{onDelete && isOwner && (
|
||||
<button type="button" className="f-card-delete-btn"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onDelete(ad.id);
|
||||
}}>
|
||||
<TrashIcon/>
|
||||
</button>
|
||||
)}
|
||||
{ad.reward && (
|
||||
<span className="f-card-reward-text">
|
||||
{t("postCard.reward")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="f-card-caption">
|
||||
<span
|
||||
ref={descRef}
|
||||
className={`f-card-caption-text${descTruncated ? " f-truncatable" : ""}${descExpanded ? " f-expanded" : ""}`}
|
||||
onClick={descTruncated ? () => setDescExpanded(v => !v) : undefined}
|
||||
>
|
||||
{ad.description}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="f-card-meta">
|
||||
{address && <div className="f-meta-pill"><PinIcon/> {address}</div>}
|
||||
{createdAt && (
|
||||
<div
|
||||
className="f-meta-pill f-date-pill"
|
||||
ref={dateAnchorRef}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
setShowCalendar(s => !s);
|
||||
}}
|
||||
>
|
||||
<CalendarIcon/> {createdAt}
|
||||
{showCalendar && (
|
||||
<DateCalendarPopup
|
||||
date={ad.createdAt}
|
||||
onClose={() => setShowCalendar(false)}
|
||||
anchorRef={dateAnchorRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="f-card-foot">
|
||||
<div className="f-card-foot-left">
|
||||
{ad.phone && (
|
||||
<a className="f-card-phone" href={`tel:${ad.phone}`}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<PhoneIcon/> {ad.phone}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="f-card-foot-right">
|
||||
{!isOwner && !isClosed && (
|
||||
<button type="button" className="f-card-contact"
|
||||
onClick={handleContact}>
|
||||
{t('postCard.write')}
|
||||
</button>
|
||||
)}
|
||||
{onMarkFound && isOwner && !isClosed && (
|
||||
<button type="button" className="f-card-found-btn"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onMarkFound(ad.id);
|
||||
}}>
|
||||
{t('postCard.itemFound')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -172,4 +199,4 @@ export default function PostOnFeed({ ad, liked, onLike, onEdit, onDelete, onMark
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ export default function ReviewsPage() {
|
||||
isAuthenticated={isAuthenticated}
|
||||
onLoginClick={() => setAuthModalOpen(true)}
|
||||
onRulesClick={() => setRulesModalOpen(true)}
|
||||
onCreatePost={() => navigate("/feed")}
|
||||
onProfileClick={() => setProfileModalOpen(true)}
|
||||
profileData={profileData}
|
||||
/>
|
||||
|
||||
<div className="f-main">
|
||||
|
||||
Reference in New Issue
Block a user