Добавил модалку
This commit is contained in:
parent
4ecf83690e
commit
c48658b560
7 changed files with 67 additions and 45 deletions
|
|
@ -32,3 +32,10 @@
|
|||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modalCardAttributes {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import styles from '@/components/home/ExcursionCard.module.scss'
|
||||
import Button from '@/components/ui/Button/Button';
|
||||
import type { Ref } from 'react'
|
||||
import { useState, type Ref } from 'react'
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Modal } from '../ui/Modal/Modal';
|
||||
|
||||
interface ExcursionCardProps {
|
||||
title: string;
|
||||
|
|
@ -14,6 +16,7 @@ interface ExcursionCardProps {
|
|||
}
|
||||
|
||||
export default function ExcursionCard({ title, description, imageUrl, city, minPeople, maxPeople, cost, ref }: ExcursionCardProps) {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false)
|
||||
return <div className={styles.card} ref={ref}>
|
||||
|
||||
<div className={styles.cardImageWrapper}><img src={imageUrl} alt={title} className={styles.cardImage} /></div>
|
||||
|
|
@ -22,6 +25,22 @@ export default function ExcursionCard({ title, description, imageUrl, city, minP
|
|||
<span className={styles.cost}>стоимость: {cost}</span>
|
||||
<span className={styles.description}>{description}</span>
|
||||
<span className={styles.people}>Человек: от {minPeople} до {maxPeople}</span>
|
||||
<Button onClick={() => console.log(`Booking ${title}`)} >Подробнее</Button>
|
||||
<Button onClick={() => setIsModalVisible((v) => !v)} >Подробнее</Button>
|
||||
{
|
||||
createPortal(
|
||||
<Modal isOpen={isModalVisible}
|
||||
onClose={() => setIsModalVisible(false)}
|
||||
>
|
||||
<div className={styles.cardImageWrapper}><img src={imageUrl} alt={title} className={styles.cardImage} /></div>
|
||||
<h3 className={styles.title}>{title}</h3>
|
||||
<div className={styles.modalCardAttributes}>
|
||||
<span className={styles.city}>город: {city}</span>
|
||||
<span className={styles.cost}>стоимость: {cost}</span>
|
||||
<span className={styles.people}>Человек: от {minPeople} до {maxPeople}</span>
|
||||
</div>
|
||||
<div className={styles.modalCardDescription}>{description}</div>
|
||||
</Modal>, document.body)
|
||||
}
|
||||
|
||||
</div >
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ interface IFiltersProps {
|
|||
export default function Filters({ onChangeFilter }: IFiltersProps) {
|
||||
const [filter, setFilter] = useState<Partial<IExcursionsFilter>>({})
|
||||
|
||||
const handleRangeChange = (range: { min: number | ''; max: number | '' }) => {
|
||||
console.log('Выбранный диапазон:', range);
|
||||
const handleRangeChange = (range: { min: number | 0; max: number | 0 }) => {
|
||||
setFilter({ ...filter, minCost: +range.min, maxCost: +range.max })
|
||||
};
|
||||
|
||||
return <div className={styles.filtersContainer}
|
||||
|
|
@ -26,7 +26,7 @@ export default function Filters({ onChangeFilter }: IFiltersProps) {
|
|||
<div className={styles.filters}>
|
||||
<Input placeholder={'Москва'} label={"Город"} value={filter.city} onChange={(e) => setFilter({ ...filter, city: e.target.value })} />
|
||||
<NumberRangeInput label='Цена' minLimit={0} maxLimit={10000} onChange={handleRangeChange} />
|
||||
<Input mask={/^\d{0,2}$/} label={'Кол-во человек'} value={`${filter.countPeople}`} onChange={(e) => +e.target.value} placeholder='10' />
|
||||
<Input mask={/^\d{0,2}$/} label={'Кол-во человек'} value={`${filter.countPeople}`} onChange={(e) => setFilter({ ...filter, countPeople: +e.target.value })} placeholder='10' />
|
||||
<Button onClick={() => onChangeFilter(filter)}>Отфильтровать</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import styles from '@/components/home/Listing.module.scss';
|
||||
import ExcursionCard from '@/components/home/ExcursionCard'
|
||||
import type { IExcursionCard } from '@/types';
|
||||
import { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { useEffect, useRef, useState, useCallback, type ReactNode, forwardRef, useImperativeHandle } from 'react';
|
||||
import type { IExcursionsFilter } from '@/types';
|
||||
import ApiService from '@/services/apiService';
|
||||
import Loader from '@/components/ui/Loader/Loader';
|
||||
|
|
@ -13,26 +13,19 @@ interface IListingProps {
|
|||
isPriceSortAsc?: boolean;
|
||||
}
|
||||
|
||||
const Listing = (props: IListingProps) => {
|
||||
const Listing = forwardRef((props: IListingProps, ref) => {
|
||||
const offset = useRef(0)
|
||||
const cardRefs = useRef([])
|
||||
const lastCardIndex = useRef(null)
|
||||
const lastCardRef = useRef(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [excursions, setExcursions] = useState<IExcursionCard[]>([])
|
||||
|
||||
const setCardRef = useCallback((el: HTMLDivElement, index: number) => {
|
||||
cardRefs.current[index] = el;
|
||||
}, []);
|
||||
|
||||
const apiService = new ApiService()
|
||||
|
||||
|
||||
const fetchExcursions = async (newOffset: number, filter?: Partial<IExcursionsFilter>) => {
|
||||
const fetchExcursions = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const newExcursions = await (apiService.getExcursions({ limit: LIMIT, offset: newOffset, filter, isPriceSortAsc: props.isPriceSortAsc }))
|
||||
const newExcursions = await (apiService.getExcursions({ limit: LIMIT, offset: offset.current, filter: props.filter, isPriceSortAsc: props.isPriceSortAsc }))
|
||||
setExcursions((prev) => [...prev, ...newExcursions])
|
||||
offset.current += LIMIT
|
||||
offset.current = offset.current + LIMIT
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
|
@ -40,34 +33,30 @@ const Listing = (props: IListingProps) => {
|
|||
|
||||
useEffect(() => {
|
||||
offset.current = 0
|
||||
lastCardIndex.current = null
|
||||
lastCardRef.current = null
|
||||
setExcursions([])
|
||||
fetchExcursions(offset.current)
|
||||
fetchExcursions()
|
||||
}, [props.filter, props.isPriceSortAsc])
|
||||
|
||||
useEffect(() => {
|
||||
if (!excursions.length) {
|
||||
return
|
||||
}
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting && !isLoading) {
|
||||
fetchExcursions(offset.current, props.filter)
|
||||
fetchExcursions()
|
||||
}
|
||||
})
|
||||
}, { threshold: .8 })
|
||||
}, { threshold: .9 })
|
||||
|
||||
if (lastCardIndex.current !== null) {
|
||||
observer.unobserve(cardRefs.current[lastCardIndex.current]);
|
||||
}
|
||||
if (cardRefs.current.length) {
|
||||
lastCardIndex.current = cardRefs.current.length - 1;
|
||||
const lastEl = cardRefs.current[lastCardIndex.current];
|
||||
if (lastEl instanceof Element) {
|
||||
observer.observe(lastEl);
|
||||
if (!excursions.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (lastCardRef.current !== null) {
|
||||
observer.unobserve(lastCardRef.current)
|
||||
}
|
||||
|
||||
observer.observe(lastCardRef.current);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [excursions])
|
||||
|
||||
|
|
@ -81,7 +70,7 @@ const Listing = (props: IListingProps) => {
|
|||
<ExcursionCard
|
||||
{...excursion}
|
||||
key={index}
|
||||
ref={(el) => setCardRef(el, index)}
|
||||
ref={(el) => { lastCardRef.current = index === excursions.length - 1 ? el : null }}
|
||||
minPeople={excursion.minCountPeople}
|
||||
maxPeople={excursion.maxCountPeople}
|
||||
/>
|
||||
|
|
@ -95,7 +84,7 @@ const Listing = (props: IListingProps) => {
|
|||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
||||
export default Listing;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@
|
|||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
max-width: 60%;
|
||||
height: 100%;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
|
|
@ -35,6 +36,6 @@
|
|||
}
|
||||
|
||||
.modalContent {
|
||||
height: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export default function Home() {
|
|||
const [filter, setFilter] = useState({})
|
||||
const [isPriceSortOrderAsc, setIsPriceSortOrderAsc] = useState<boolean>(true)
|
||||
function changeFiltersHandle(filter: Partial<IExcursionsFilter>) {
|
||||
setFilter(filter)
|
||||
setFilter({ ...filter }) // Использую деструктуризацию для вызова перерисовки при нажатии на энтер
|
||||
}
|
||||
|
||||
return <div className={styles.home}>
|
||||
|
|
@ -21,7 +21,7 @@ export default function Home() {
|
|||
<div className={styles.sort}>
|
||||
Сортировка:
|
||||
<Button
|
||||
onClick={() => (setIsPriceSortOrderAsc((old) => !old))}
|
||||
onClick={() => (setIsPriceSortOrderAsc((i) => !i))}
|
||||
iconAfter={
|
||||
<div className={styles.sortIcon}
|
||||
style={!isPriceSortOrderAsc ? {
|
||||
|
|
|
|||
|
|
@ -10,19 +10,24 @@ interface IGetExcursionsRequest {
|
|||
|
||||
export default class ApiService {
|
||||
getExcursions({ limit, offset, filter, isPriceSortAsc }: IGetExcursionsRequest): Promise<IExcursionCard[]> {
|
||||
const excursionsSorted = excursions.sort((a, b) => isPriceSortAsc ? a.cost - b.cost : b.cost - a.cost)
|
||||
let result = JSON.parse(JSON.stringify(excursions))
|
||||
|
||||
let result = excursions.slice(offset, offset + limit)
|
||||
|
||||
if (isPriceSortAsc) {
|
||||
result = result.sort((a, b) => a.cost - b.cost)
|
||||
} else {
|
||||
result = result.sort((a, b) => b.cost - a.cost)
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
if (filter.city) {
|
||||
result = result.filter((card) => card.city.includes(filter.city))
|
||||
}
|
||||
if (filter.minCost) {
|
||||
result = result.filter((card) => card.cost >= +filter.minCost)
|
||||
result = result.filter((card) => card.cost >= filter.minCost)
|
||||
}
|
||||
if (filter.maxCost) {
|
||||
result = result.filter((card) => card.cost <= +filter.maxCost)
|
||||
result = result.filter((card) => card.cost <= filter.maxCost)
|
||||
}
|
||||
if (filter.countPeople) {
|
||||
result = result.filter((card) => (
|
||||
|
|
@ -31,6 +36,7 @@ export default class ApiService {
|
|||
)
|
||||
}
|
||||
}
|
||||
result = result.slice(offset, offset + limit)
|
||||
return new Promise((res, reg) => {
|
||||
setTimeout(() => {
|
||||
res(result)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue