fixed calendar on post

This commit is contained in:
SlimusMinus
2026-08-12 23:59:16 +03:00
parent ba5ff93dca
commit d1e9c9fc7e
4 changed files with 104 additions and 34 deletions

View File

@@ -1,11 +1,14 @@
import { useState, useEffect, useRef } from "react";
// src/components/DateCalendarPopup.jsx
import { useState, useEffect, useRef, useLayoutEffect } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from 'react-i18next';
export default function DateCalendarPopup({ date, onClose }) {
export default function DateCalendarPopup({ date, onClose, anchorRef }) {
const { t } = useTranslation();
const months = t('calendar.months', { returnObjects: true });
const dayNames = t('calendar.days', { returnObjects: true });
const ref = useRef(null);
const [coords, setCoords] = useState(null);
const selectedDate = new Date(date);
const isValidDate = !Number.isNaN(selectedDate.getTime());
@@ -13,11 +16,32 @@ export default function DateCalendarPopup({ date, onClose }) {
const [year, setYear] = useState(isValidDate ? selectedDate.getFullYear() : new Date().getFullYear());
const [month, setMonth] = useState(isValidDate ? selectedDate.getMonth() : new Date().getMonth());
// Двухпроходное позиционирование:
// 1) сразу после монтирования попап невидим (opacity: 0), но уже в DOM —
// нужно, чтобы измерить его реальные width/height.
// 2) как только размеры известны — пересчитываем координаты так, чтобы
// попап открывался влево-вверх от якоря, и показываем (opacity: 1).
useLayoutEffect(() => {
if (!anchorRef?.current || !ref.current) return;
const anchorRect = anchorRef.current.getBoundingClientRect();
const popupRect = ref.current.getBoundingClientRect();
setCoords({
top: anchorRect.top + window.scrollY - popupRect.height - 4,
left: anchorRect.right + window.scrollX - popupRect.width,
});
}, [anchorRef, month, year]); // пересчитываем и при смене месяца — высота грид может немного плавать
useEffect(() => {
const handler = (e) => { if (ref.current && !ref.current.contains(e.target)) onClose(); };
const handler = (e) => {
const clickedAnchor = anchorRef?.current?.contains(e.target);
const clickedPopup = ref.current?.contains(e.target);
if (!clickedAnchor && !clickedPopup) onClose();
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [onClose]);
}, [onClose, anchorRef]);
const goToPrevMonth = () => {
if (month === 0) {
@@ -51,8 +75,20 @@ export default function DateCalendarPopup({ date, onClose }) {
selectedDate.getMonth() === month &&
selectedDate.getDate() === day;
return (
<div className="f-date-calendar" ref={ref}>
return createPortal(
<div
className="f-date-calendar"
ref={ref}
style={{
position: "absolute",
top: coords?.top ?? 0,
left: coords?.left ?? 0,
zIndex: 1000,
// Пока координаты не вычислены (первый проход) — прячем, чтобы
// не мелькал в неправильном месте до пересчёта.
visibility: coords ? "visible" : "hidden",
}}
>
<div className="f-cal-header">
<button type="button" onClick={goToPrevMonth}>‹</button>
<span>{months[month]} {year}</span>
@@ -73,6 +109,7 @@ export default function DateCalendarPopup({ date, onClose }) {
) : <div key={i} />
)}
</div>
</div>
</div>,
document.body
);
}

View File

@@ -1,11 +1,9 @@
// src/components/PostOnFeed.jsx
import {useRef, useState} from "react";
import PinIcon from "../components/icons/PinIcon";
import CalendarIcon from "../components/icons/CalendarIcon";
import PhoneIcon from "../components/icons/PhoneIcon";
import MapIcon from "../components/icons/MapIcon";
import HeartIcon from "../components/icons/HeartIcon";
import TrashIcon from "../components/icons/TrashIcon";
import { useState } from "react";
import {useContactSeller} from "../hooks/useContactSeller";
import {useAuthContext} from "../context/AuthContext";
import {getImageUrl} from "../config/authConstants";
@@ -16,6 +14,7 @@ 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 dateAnchorRef = useRef(null);
const { isAuthenticated } = useAuthContext();
const { contactSeller, error: contactError, clearError } = useContactSeller(ad);
@@ -72,22 +71,38 @@ export default function PostOnFeed({ ad, liked, onLike, onEdit, onDelete, onMark
<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"}`}>
<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">
{ad.title}
{isClosed && <span className="f-status-badge f-status-closed">{t('postCard.foundStatus')}</span>}
{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>}
{address && <div className="f-meta-pill"><PinIcon/> {address}
</div>}
{createdAt && (
<div className="f-meta-pill f-date-pill" onClick={e => { e.stopPropagation(); setShowCalendar(s => !s); }}>
<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)} />}
{showCalendar && (
<DateCalendarPopup
date={ad.createdAt}
onClose={() => setShowCalendar(false)}
anchorRef={dateAnchorRef}
/>
)}
</div>
)}
</div>
@@ -102,30 +117,44 @@ export default function PostOnFeed({ ad, liked, onLike, onEdit, onDelete, onMark
</div>
<div className="f-card-foot-right">
{!isOwner && !isClosed && (
<button type="button" className="f-card-contact" onClick={handleContact}>
<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); }}>
onClick={e => {
e.stopPropagation();
onEdit(ad);
}}>
✏️
</button>
)}
{onDelete && isOwner && (
<button type="button" className="f-card-delete-btn"
onClick={e => { e.stopPropagation(); onDelete(ad.id); }}>
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); }}>
onClick={e => {
e.stopPropagation();
onMarkFound(ad.id);
}}>
{t('postCard.itemFound')}
</button>
)}
<button type="button" className={`f-like-btn${liked ? " liked" : ""}`}
onClick={e => { e.stopPropagation(); onLike(ad.id); }}>
<button type="button"
className={`f-like-btn${liked ? " liked" : ""}`}
onClick={e => {
e.stopPropagation();
onLike(ad.id);
}}>
<HeartIcon filled={liked}/>
{ad.likeCount || 0}
</button>

View File

@@ -14,6 +14,7 @@
--bg-app: linear-gradient(135deg, #1a2340 0%, #0d1018 100%);
--bg-card: linear-gradient(155deg, #1d2436, #131720);
--bg-card-popup: linear-gradient(155deg, #2b3552, #1e2742);
--bg-surface: rgba(13,16,24,.7);
--bg-surface-solid: #131720;
--bg-filter: rgba(13,16,24,.4);
@@ -35,6 +36,7 @@
--bg-app: linear-gradient(135deg, #f0f2f5 0%, #e2e6ed 100%);
--bg-card: #ffffff;
--bg-card-popup: #ffffff;
--bg-surface: rgba(255,255,255,.8);
--bg-surface-solid: #ffffff;
--bg-filter: rgba(255,255,255,.6);

View File

@@ -245,17 +245,19 @@
/* ── Calendar Popup ── */
.f-date-calendar {
position: absolute;
bottom: calc(100% + 6px);
left: 0;
z-index: 100;
background: var(--bg-card);
background: var(--bg-card-popup);
border: 1px solid var(--border);
border-radius: 12px;
padding: 10px;
width: 220px;
box-shadow: 0 8px 32px rgba(0,0,0,.25);
}
.f-date-calendar-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.25);
z-index: 1000;
}
.f-cal-header {
display: flex;
align-items: center;