Добавил модалку
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;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
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 styles from '@/components/home/ExcursionCard.module.scss'
|
||||||
import Button from '@/components/ui/Button/Button';
|
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 {
|
interface ExcursionCardProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -14,6 +16,7 @@ interface ExcursionCardProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ExcursionCard({ title, description, imageUrl, city, minPeople, maxPeople, cost, ref }: 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}>
|
return <div className={styles.card} ref={ref}>
|
||||||
|
|
||||||
<div className={styles.cardImageWrapper}><img src={imageUrl} alt={title} className={styles.cardImage} /></div>
|
<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.cost}>стоимость: {cost}</span>
|
||||||
<span className={styles.description}>{description}</span>
|
<span className={styles.description}>{description}</span>
|
||||||
<span className={styles.people}>Человек: от {minPeople} до {maxPeople}</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 >
|
</div >
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ interface IFiltersProps {
|
||||||
export default function Filters({ onChangeFilter }: IFiltersProps) {
|
export default function Filters({ onChangeFilter }: IFiltersProps) {
|
||||||
const [filter, setFilter] = useState<Partial<IExcursionsFilter>>({})
|
const [filter, setFilter] = useState<Partial<IExcursionsFilter>>({})
|
||||||
|
|
||||||
const handleRangeChange = (range: { min: number | ''; max: number | '' }) => {
|
const handleRangeChange = (range: { min: number | 0; max: number | 0 }) => {
|
||||||
console.log('Выбранный диапазон:', range);
|
setFilter({ ...filter, minCost: +range.min, maxCost: +range.max })
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className={styles.filtersContainer}
|
return <div className={styles.filtersContainer}
|
||||||
|
|
@ -26,7 +26,7 @@ export default function Filters({ onChangeFilter }: IFiltersProps) {
|
||||||
<div className={styles.filters}>
|
<div className={styles.filters}>
|
||||||
<Input placeholder={'Москва'} label={"Город"} value={filter.city} onChange={(e) => setFilter({ ...filter, city: e.target.value })} />
|
<Input placeholder={'Москва'} label={"Город"} value={filter.city} onChange={(e) => setFilter({ ...filter, city: e.target.value })} />
|
||||||
<NumberRangeInput label='Цена' minLimit={0} maxLimit={10000} onChange={handleRangeChange} />
|
<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>
|
<Button onClick={() => onChangeFilter(filter)}>Отфильтровать</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import styles from '@/components/home/Listing.module.scss';
|
import styles from '@/components/home/Listing.module.scss';
|
||||||
import ExcursionCard from '@/components/home/ExcursionCard'
|
import ExcursionCard from '@/components/home/ExcursionCard'
|
||||||
import type { IExcursionCard } from '@/types';
|
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 type { IExcursionsFilter } from '@/types';
|
||||||
import ApiService from '@/services/apiService';
|
import ApiService from '@/services/apiService';
|
||||||
import Loader from '@/components/ui/Loader/Loader';
|
import Loader from '@/components/ui/Loader/Loader';
|
||||||
|
|
@ -13,26 +13,19 @@ interface IListingProps {
|
||||||
isPriceSortAsc?: boolean;
|
isPriceSortAsc?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Listing = (props: IListingProps) => {
|
const Listing = forwardRef((props: IListingProps, ref) => {
|
||||||
const offset = useRef(0)
|
const offset = useRef(0)
|
||||||
const cardRefs = useRef([])
|
const lastCardRef = useRef(null)
|
||||||
const lastCardIndex = useRef(null)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [excursions, setExcursions] = useState<IExcursionCard[]>([])
|
const [excursions, setExcursions] = useState<IExcursionCard[]>([])
|
||||||
|
|
||||||
const setCardRef = useCallback((el: HTMLDivElement, index: number) => {
|
|
||||||
cardRefs.current[index] = el;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const apiService = new ApiService()
|
const apiService = new ApiService()
|
||||||
|
|
||||||
|
const fetchExcursions = async () => {
|
||||||
const fetchExcursions = async (newOffset: number, filter?: Partial<IExcursionsFilter>) => {
|
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
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])
|
setExcursions((prev) => [...prev, ...newExcursions])
|
||||||
offset.current += LIMIT
|
offset.current = offset.current + LIMIT
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
@ -40,34 +33,30 @@ const Listing = (props: IListingProps) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
offset.current = 0
|
offset.current = 0
|
||||||
lastCardIndex.current = null
|
lastCardRef.current = null
|
||||||
setExcursions([])
|
setExcursions([])
|
||||||
fetchExcursions(offset.current)
|
fetchExcursions()
|
||||||
}, [props.filter, props.isPriceSortAsc])
|
}, [props.filter, props.isPriceSortAsc])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!excursions.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting && !isLoading) {
|
if (entry.isIntersecting && !isLoading) {
|
||||||
fetchExcursions(offset.current, props.filter)
|
fetchExcursions()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, { threshold: .8 })
|
}, { threshold: .9 })
|
||||||
|
|
||||||
if (lastCardIndex.current !== null) {
|
if (!excursions.length) {
|
||||||
observer.unobserve(cardRefs.current[lastCardIndex.current]);
|
return
|
||||||
}
|
|
||||||
if (cardRefs.current.length) {
|
|
||||||
lastCardIndex.current = cardRefs.current.length - 1;
|
|
||||||
const lastEl = cardRefs.current[lastCardIndex.current];
|
|
||||||
if (lastEl instanceof Element) {
|
|
||||||
observer.observe(lastEl);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastCardRef.current !== null) {
|
||||||
|
observer.unobserve(lastCardRef.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observer.observe(lastCardRef.current);
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [excursions])
|
}, [excursions])
|
||||||
|
|
||||||
|
|
@ -81,7 +70,7 @@ const Listing = (props: IListingProps) => {
|
||||||
<ExcursionCard
|
<ExcursionCard
|
||||||
{...excursion}
|
{...excursion}
|
||||||
key={index}
|
key={index}
|
||||||
ref={(el) => setCardRef(el, index)}
|
ref={(el) => { lastCardRef.current = index === excursions.length - 1 ? el : null }}
|
||||||
minPeople={excursion.minCountPeople}
|
minPeople={excursion.minCountPeople}
|
||||||
maxPeople={excursion.maxCountPeople}
|
maxPeople={excursion.maxCountPeople}
|
||||||
/>
|
/>
|
||||||
|
|
@ -95,7 +84,7 @@ const Listing = (props: IListingProps) => {
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
|
|
||||||
export default Listing;
|
export default Listing;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,10 @@
|
||||||
background: white;
|
background: white;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-width: 500px;
|
max-width: 60%;
|
||||||
width: 100%;
|
height: 100%;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modalHeader {
|
.modalHeader {
|
||||||
|
|
@ -35,6 +36,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.modalContent {
|
.modalContent {
|
||||||
|
height: 100%;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ export default function Home() {
|
||||||
const [filter, setFilter] = useState({})
|
const [filter, setFilter] = useState({})
|
||||||
const [isPriceSortOrderAsc, setIsPriceSortOrderAsc] = useState<boolean>(true)
|
const [isPriceSortOrderAsc, setIsPriceSortOrderAsc] = useState<boolean>(true)
|
||||||
function changeFiltersHandle(filter: Partial<IExcursionsFilter>) {
|
function changeFiltersHandle(filter: Partial<IExcursionsFilter>) {
|
||||||
setFilter(filter)
|
setFilter({ ...filter }) // Использую деструктуризацию для вызова перерисовки при нажатии на энтер
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className={styles.home}>
|
return <div className={styles.home}>
|
||||||
|
|
@ -21,7 +21,7 @@ export default function Home() {
|
||||||
<div className={styles.sort}>
|
<div className={styles.sort}>
|
||||||
Сортировка:
|
Сортировка:
|
||||||
<Button
|
<Button
|
||||||
onClick={() => (setIsPriceSortOrderAsc((old) => !old))}
|
onClick={() => (setIsPriceSortOrderAsc((i) => !i))}
|
||||||
iconAfter={
|
iconAfter={
|
||||||
<div className={styles.sortIcon}
|
<div className={styles.sortIcon}
|
||||||
style={!isPriceSortOrderAsc ? {
|
style={!isPriceSortOrderAsc ? {
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,24 @@ interface IGetExcursionsRequest {
|
||||||
|
|
||||||
export default class ApiService {
|
export default class ApiService {
|
||||||
getExcursions({ limit, offset, filter, isPriceSortAsc }: IGetExcursionsRequest): Promise<IExcursionCard[]> {
|
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) {
|
||||||
if (filter.city) {
|
if (filter.city) {
|
||||||
result = result.filter((card) => card.city.includes(filter.city))
|
result = result.filter((card) => card.city.includes(filter.city))
|
||||||
}
|
}
|
||||||
if (filter.minCost) {
|
if (filter.minCost) {
|
||||||
result = result.filter((card) => card.cost >= +filter.minCost)
|
result = result.filter((card) => card.cost >= filter.minCost)
|
||||||
}
|
}
|
||||||
if (filter.maxCost) {
|
if (filter.maxCost) {
|
||||||
result = result.filter((card) => card.cost <= +filter.maxCost)
|
result = result.filter((card) => card.cost <= filter.maxCost)
|
||||||
}
|
}
|
||||||
if (filter.countPeople) {
|
if (filter.countPeople) {
|
||||||
result = result.filter((card) => (
|
result = result.filter((card) => (
|
||||||
|
|
@ -31,6 +36,7 @@ export default class ApiService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
result = result.slice(offset, offset + limit)
|
||||||
return new Promise((res, reg) => {
|
return new Promise((res, reg) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
res(result)
|
res(result)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue