Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Module5 task1 #6

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"classnames": "2.3.2",
"history": "5.3.0",
"http-status-codes": "2.3.0",
"leaflet": "1.7.1",
"leaflet": "^1.7.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-helmet-async": "1.3.0",
Expand All @@ -30,7 +30,7 @@
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.5.1",
"@types/faker": "5.5.9",
"@types/leaflet": "1.9.3",
"@types/leaflet": "^1.9.3",
"@types/react": "18.2.25",
"@types/react-dom": "18.2.11",
"@types/react-redux": "7.1.27",
Expand Down
35 changes: 19 additions & 16 deletions src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,29 @@ import { PrivateRoute } from '../components/private-route';
import { Page404 } from '../pages/errors';
import { AppRoutes } from '../constants/routes.ts';
import { Offer } from '../types/offer.ts';
import { ErrorBoundary } from '../components/index.ts';

type Props = {
offers: Offer[];
};

export const App = ({ offers }: Props) => (
<BrowserRouter>
<Routes>
<Route path={AppRoutes.Default}>
<Route index element={<MainPage offers={offers} />} />
<Route path={AppRoutes.Login} element={<LoginPage />} />
<Route path={AppRoutes.Favorites} element={(
<PrivateRoute isAuthenticated>
<FavoritesPage offers={offers} />
</PrivateRoute>
)}
/>
<Route path={AppRoutes.OfferForRouter} element={<OfferPage />} />
</Route>
<Route path='*' element={<Page404 />} />
</Routes>
</BrowserRouter>
<ErrorBoundary>
<BrowserRouter>
<Routes>
<Route path={AppRoutes.Default}>
<Route index element={<MainPage offers={offers} />} />
<Route path={AppRoutes.Login} element={<LoginPage />} />
<Route path={AppRoutes.Favorites} element={(
<PrivateRoute isAuthenticated>
<FavoritesPage offers={offers} />
</PrivateRoute>
)}
/>
<Route path={AppRoutes.OfferForRouter} element={<OfferPage />} />
</Route>
<Route path='*' element={<Page404 />} />
</Routes>
</BrowserRouter>
</ErrorBoundary>
);
54 changes: 54 additions & 0 deletions src/components/city-offers/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useMemo, useState } from 'react';
import type { Offer } from '../../types/offer';
import { OffersList } from '../offers-list';
import { Map } from '../map';

type Props = {
offers: Offer[];
};

export const CityOffers = ({ offers }: Props) => {
const [hoveredOfferId, setHoveredOfferId] = useState<Offer['id'] | undefined>(undefined);
const offersAmount = offers.length;
const ACTIVE_CITY = 'Amsterdam';

const handleOfferHover = (id: Offer['id'] | undefined) => {
setHoveredOfferId(id);
};

const hoveredOffer = useMemo(() => {
return offers.find((offer) => offer.id === hoveredOfferId);
}, [hoveredOfferId, offers]);

return (
<div className="cities">
<div className="cities__places-container container">
<section className="cities__places places">
<h2 className="visually-hidden">Places</h2>
<b className="places__found">{offersAmount} places to stay in Amsterdam</b>
<form className="places__sorting" action="#" method="get">
<span className="places__sorting-caption">Sort by</span>
<span className="places__sorting-type" tabIndex={0}>
Popular
<svg className="places__sorting-arrow" width="7" height="4">
<use xlinkHref="#icon-arrow-select"></use>
</svg>
</span>
<ul className="places__options places__options--custom places__options--opened">
<li className="places__option places__option--active" tabIndex={0}>Popular</li>
<li className="places__option" tabIndex={0}>Price: low to high</li>
<li className="places__option" tabIndex={0}>Price: high to low</li>
<li className="places__option" tabIndex={0}>Top rated first</li>
</ul>
</form>
<OffersList offers={offers} onOfferHover={handleOfferHover} />
</section>
<div className="cities__right-section">
<section className="cities__map">
<Map offers={offers} activeCityName={ACTIVE_CITY} selectedOffer={hoveredOffer} />
</section>
</div>
</div>
</div>
);
};
32 changes: 32 additions & 0 deletions src/components/error-boundary/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ErrorInfo, PureComponent, ReactNode } from 'react';

type Props = {
FallbackComponent?: ReactNode;
children: ReactNode;
}

type State = {
hasError: boolean;
}

export class ErrorBoundary extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// eslint-disable-next-line no-console
console.error('ErrorBoundary поймал ошибку: ', error, errorInfo);
this.setState({ hasError: true });
}

render() {
if (this.state.hasError) {
const ErrorComponent = this.props.FallbackComponent ?? <h1>Something went wrong.</h1>;
return ErrorComponent;
}

return this.props.children;
}
}
5 changes: 5 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ export { OfferCard } from './offer-card';
export { OffersList } from './offers-list';
export { FavoriteCard } from './favorite-card';
export { CommentForm } from './comment-form';
export { Map } from './map';
export { ErrorBoundary } from './error-boundary';
export { Tabs } from './tabs';
export { Tab } from './tab';
export { CityOffers } from './city-offers';
70 changes: 70 additions & 0 deletions src/components/map/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useRef, useEffect, useState } from 'react';
import { Marker, layerGroup } from 'leaflet';
import { useMap } from '../../hooks/use-map';
import type { Offer } from '../../types/offer';
import type { MarkerRef } from '../../types/map';
import {
defaultCustomIcon,
currentCustomIcon
} from '../../constants/map';
import 'leaflet/dist/leaflet.css';

type Props = {
width?: string;
height?: string;
offers: Offer[];
selectedOffer?: Offer;
activeCityName: string;
};

export const Map = ({
width = '512px',
height = '100%',
offers,
activeCityName,
selectedOffer
}: Props): JSX.Element => {
const [{ city, points }] = useState(() => {
const filteredOffers = offers.filter((offer) => offer.city.name === activeCityName);
const offerData = filteredOffers.find((offer) => offer.city.name === activeCityName);

return {
city: offerData?.city,
points: filteredOffers.map((offer) => ({ title: offer.title, ...offer.location }))
};
});

const mapRef = useRef(null);
const map = useMap(mapRef, city);
const markerLayerRef = useRef(layerGroup());
const markersRef = useRef<MarkerRef[]>([]);

useEffect(() => {
if (map) {
if (!markersRef.current.length) {
points.forEach(({ latitude, longitude, title }) => {
const marker = new Marker({ lat: latitude, lng: longitude })
.setIcon(
selectedOffer && title === selectedOffer.title
? currentCustomIcon
: defaultCustomIcon
)
.addTo(markerLayerRef.current);
markersRef.current.push({ marker, title });
});

markerLayerRef.current.addTo(map);
} else {
markersRef.current.forEach(({ marker, title }) => {
marker.setIcon(
selectedOffer && title === selectedOffer.title
? currentCustomIcon
: defaultCustomIcon
);
});
}
}
}, [map, points, selectedOffer]);

return <div style={{ width, height }} ref={mapRef}></div>;
};
24 changes: 6 additions & 18 deletions src/components/offers-list/index.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,21 @@
import { useCallback, useState } from 'react';
import { Offer } from '../../types/offer.ts';
import { OfferCard } from '../offer-card';
import { FavoriteCard } from '../favorite-card';

type Props = {
offers: Offer[];
favorites?: boolean;
onOfferHover?: (id?: Offer['id']) => void;
};

export const OffersList = ({ offers, favorites }: Props) => {
const [activeCardId, setActiveCardId] = useState<Offer['id'] | undefined>(undefined);

// eslint-disable-next-line no-console
console.log('activeCardId: ', activeCardId);

const handleCardSetActiveStatus = useCallback((id: Offer['id'] | undefined) => {
setActiveCardId(id);
}, []);

export const OffersList = ({ offers, favorites, onOfferHover }: Props) => {
return (
<div className={favorites ? 'favorites__places' : 'cities__places-list places__list tabs__content'}>
{offers.length ? offers.map((offer) => {
const { id } = offer;

// TODO: Объединить карточки в один компонент
{offers.length && offers.map((offer) => {
return favorites ?
<FavoriteCard key={id} {...offer} /> :
<OfferCard key={id} {...offer} onHover={handleCardSetActiveStatus} />;
}) : null}
<FavoriteCard key={offer.id} {...offer} /> :
<OfferCard key={offer.id} {...offer} onHover={onOfferHover} />;
})}
</div>
);
};
20 changes: 20 additions & 0 deletions src/components/tab/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Link } from 'react-router-dom';

type Props = {
name: string;
isActive?: boolean;
};

// TODO: заменить ссылку
export const Tab = ({ name, isActive }: Props) => {
// TODO: Забирать значение активного таба из стора или из роутера
isActive = name === 'Amsterdam';

return (
<li className="locations__item">
<Link className={`locations__item-link tabs__item ${isActive ? 'tabs__item--active' : ''}`} to='#'>
<span>{name}</span>
</Link>
</li>
);
};
21 changes: 21 additions & 0 deletions src/components/tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Tab } from '../tab';

type Props = {
activeCity?: string;
citiesNames: string[];
};

// eslint-disable-next-line
export const Tabs = ({ citiesNames, activeCity }: Props) => {

Check failure on line 9 in src/components/tabs/index.tsx

View workflow job for this annotation

GitHub Actions / build

'activeCity' is declared but its value is never read.

Check failure on line 9 in src/components/tabs/index.tsx

View workflow job for this annotation

GitHub Actions / build

'activeCity' is declared but its value is never read.
return (
<div className="tabs">
<section className="locations container">
<ul className="locations__list tabs__list">
{citiesNames && citiesNames.map((name) => {
return <Tab key={name} name={name} />;
})}
</ul>
</section>
</div>
);
};
22 changes: 22 additions & 0 deletions src/constants/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Icon } from 'leaflet';

// Зум
export const DEFAULT_MAP_ZOOM = 10;

// Маркеры
const URL_MARKER_DEFAULT =
'https://assets.htmlacademy.ru/content/intensive/javascript-1/demo/interactive-map/pin.svg';
const URL_MARKER_CURRENT =
'https://assets.htmlacademy.ru/content/intensive/javascript-1/demo/interactive-map/main-pin.svg';

export const defaultCustomIcon = new Icon({
iconUrl: URL_MARKER_DEFAULT,
iconSize: [40, 40],
iconAnchor: [20, 40]
});

export const currentCustomIcon = new Icon({
iconUrl: URL_MARKER_CURRENT,
iconSize: [40, 40],
iconAnchor: [20, 40]
});
41 changes: 41 additions & 0 deletions src/hooks/use-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useEffect, useState, MutableRefObject, useRef } from 'react';
import { Map, TileLayer } from 'leaflet';
import { OfferCity } from '../types/offer';
import { DEFAULT_MAP_ZOOM } from '../constants/map';

export const useMap = (
mapRef: MutableRefObject<HTMLElement | null>,
city?: OfferCity
): Map | null => {
const [map, setMap] = useState<Map | null>(null);
const isRenderedRef = useRef<boolean>(false);

useEffect(() => {
if (mapRef.current && !isRenderedRef.current && city) {
const { latitude, longitude } = city.location;

const instance = new Map(mapRef.current, {
center: {
lat: latitude,
lng: longitude
},
zoom: DEFAULT_MAP_ZOOM
});

const layer = new TileLayer(
'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
{
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>'
}
);

instance.addLayer(layer);

setMap(instance);
isRenderedRef.current = true;
}
}, [mapRef, city]);

return map;
};
Loading
Loading