From 99b916ffb4ca762fbfd67e220b2ece69d17fb850 Mon Sep 17 00:00:00 2001 From: Gurikov Maxim Date: Wed, 20 Nov 2024 21:22:10 +0500 Subject: [PATCH] done task --- index.html | 1 + src/Pages/favorites-page/favorites-page.tsx | 11 +- src/Pages/offer-page/offer-page.tsx | 173 +++++++++++--------- src/api/api.ts | 13 ++ src/components/app.tsx | 9 +- src/components/offer/offer-card.tsx | 10 +- src/components/spinner/Spinner.tsx | 12 ++ src/components/spinner/spinner.css | 14 ++ src/dataTypes/enums/api-routes.ts | 3 + src/index.tsx | 7 +- src/mocks/detailed-offer.ts | 141 ---------------- src/mocks/offers.ts | 120 -------------- src/mocks/reviews.ts | 27 --- src/store/actions.ts | 52 +++++- src/store/store.ts | 48 +++++- 15 files changed, 246 insertions(+), 395 deletions(-) create mode 100644 src/api/api.ts create mode 100644 src/components/spinner/Spinner.tsx create mode 100644 src/components/spinner/spinner.css create mode 100644 src/dataTypes/enums/api-routes.ts delete mode 100644 src/mocks/detailed-offer.ts delete mode 100644 src/mocks/offers.ts delete mode 100644 src/mocks/reviews.ts diff --git a/index.html b/index.html index e2bbeb1..b40cac7 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,7 @@ 6 cities + diff --git a/src/Pages/favorites-page/favorites-page.tsx b/src/Pages/favorites-page/favorites-page.tsx index e605929..63c684a 100644 --- a/src/Pages/favorites-page/favorites-page.tsx +++ b/src/Pages/favorites-page/favorites-page.tsx @@ -1,15 +1,10 @@ import { Helmet } from 'react-helmet-async'; import { Layout } from '../../components/layout.tsx'; import { OfferGroup } from '../../components/offer/offer-group.tsx'; -import { Offer } from '../../dataTypes/offer.ts'; +import {useAppSelector} from '../../store/store.ts'; -interface FavoritesPageProps { - offers: Offer[]; -} - -export function FavoritesPage({ - offers, -}: FavoritesPageProps): React.JSX.Element { +export function FavoritesPage(): React.JSX.Element { + const offers = useAppSelector((state) => state.offers).filter((offer) => offer.isFavorite); return (
diff --git a/src/Pages/offer-page/offer-page.tsx b/src/Pages/offer-page/offer-page.tsx index 2649246..e23f346 100644 --- a/src/Pages/offer-page/offer-page.tsx +++ b/src/Pages/offer-page/offer-page.tsx @@ -1,108 +1,121 @@ import { Helmet } from 'react-helmet-async'; import { Layout } from '../../components/layout.tsx'; import { OffersList } from '../../components/offer/offers-list.tsx'; -import { offerMocks } from '../../mocks/offers.ts'; import { Reviews } from '../../components/reviews/reviews.tsx'; -import { reviewMocks } from '../../mocks/reviews.ts'; import { Map } from '../../components/map/map.tsx'; -import React from 'react'; +import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { OfferInsideItems } from '../../components/offer/offer-inside-items.tsx'; -import { detailedOfferMocks } from '../../mocks/detailed-offer.ts'; import { OfferHost } from '../../components/offer/offer-host.tsx'; import { capitalize, pluralizeAndCombine } from '../../utils/string-utils.ts'; import { Rating } from '../../components/rating.tsx'; import { OfferGallery } from '../../components/offer/offer-gallery.tsx'; import { BookmarkButton } from '../../components/bookmark-button.tsx'; +import { store, useAppSelector } from '../../store/store.ts'; +import { + fetchNearbyOffers, + fetchOffer, + setCurrentOffer, +} from '../../store/actions.ts'; +import { Spinner } from '../../components/spinner/Spinner.tsx'; export function OfferPage(): React.JSX.Element { const offerId = useParams().id; - const offers = offerMocks.filter((offer) => offer.id !== offerId).slice(0, 3); - const currentOffer = detailedOfferMocks.find( - (offer) => offer.id === offerId, - )!; + useEffect(() => { + store.dispatch(setCurrentOffer(null)); + store.dispatch(fetchOffer(offerId!)); + store.dispatch(fetchNearbyOffers(offerId!)); + }, [offerId]); + const nearbyOffers = useAppSelector((state) => state.nearbyOffers).slice( + 0, + 3, + ); + const currentOffer = useAppSelector((state) => state.currentOffer); return (
6 cities - offer -
- -
-
- {currentOffer.isPremium && ( -
- Premium + {!currentOffer ? ( + + ) : ( + <> +
+ +
+
+ {currentOffer.isPremium && ( +
+ Premium +
+ )} +
+

{currentOffer.title}

+ +
+ +
    +
  • + {capitalize(currentOffer.type)} +
  • +
  • + {pluralizeAndCombine('bedroom', currentOffer.bedrooms)} +
  • +
  • + Max{' '} + {pluralizeAndCombine('adult', currentOffer.maxAdults)} +
  • +
+
+ + €{currentOffer.price} + +  night +
+ +
+ +
+

+ {currentOffer.description} +

+
+
+
- )} -
-

{currentOffer.title}

-
- ({ + location: x.location, + id: x.id, + }))} + selectedPoint={{ + location: currentOffer.location, + id: currentOffer.id, + }} /> -
    -
  • - {capitalize(currentOffer.type)} -
  • -
  • - {pluralizeAndCombine('bedroom', currentOffer.bedrooms)} -
  • -
  • - Max {pluralizeAndCombine('adult', currentOffer.maxAdults)} -
  • -
-
- - €{currentOffer.price} - -  night -
- -
- -
-

- A quiet cozy and picturesque that hides behind a a river - by the unique lightness of Amsterdam. The building is - green and from 18th century. -

-

- An independent House, strategically located between - Rembrand Square and National Opera, but where the bustle - of the city comes to rest in this alley flowery and - colorful. -

+
+
+
+

+ Other places in the neighbourhood +

+
+ {nearbyOffers && }
-
- -
-
- ({ - location: x.location, - id: x.id, - }))} - selectedPoint={{ - location: currentOffer.location, - id: currentOffer.id, - }} - /> -
-
-
-

- Other places in the neighbourhood -

-
- +
- -
+ + )}
diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000..50ee062 --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,13 @@ +import axios, {AxiosInstance} from 'axios'; + +const BACKEND_URL = 'https://14.design.htmlacademy.pro/six-cities'; +const REQUEST_TIMEOUT = 5000; + +export const createAPI = (): AxiosInstance => { + const api = axios.create({ + baseURL: BACKEND_URL, + timeout: REQUEST_TIMEOUT, + }); + + return api; +}; diff --git a/src/components/app.tsx b/src/components/app.tsx index dccd602..95ce034 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -7,15 +7,10 @@ import { NotFoundPage } from '../pages/not-found-page/not-found-page.tsx'; import { AuthorizationWrapper } from './authorization-wrapper.tsx'; import { AppRoutes } from '../dataTypes/enums/app-routes.ts'; import { HelmetProvider } from 'react-helmet-async'; -import { Offer } from '../dataTypes/offer.ts'; import { Provider } from 'react-redux'; import { store } from '../store/store.ts'; -interface AppProps { - offers: Offer[]; -} - -export function App({ offers }: AppProps): React.JSX.Element { +export function App(): React.JSX.Element { return ( @@ -27,7 +22,7 @@ export function App({ offers }: AppProps): React.JSX.Element { path={AppRoutes.Favorites} element={ - + } /> diff --git a/src/components/offer/offer-card.tsx b/src/components/offer/offer-card.tsx index af73a91..baed48e 100644 --- a/src/components/offer/offer-card.tsx +++ b/src/components/offer/offer-card.tsx @@ -40,7 +40,7 @@ export function OfferCard({ onMouseLeave={handleMouseLeave} className={cn( 'place-card', - { 'cities__card': isOnMainPage }, + { cities__card: isOnMainPage }, { 'near-places__card': !isOnMainPage }, )} > @@ -59,7 +59,7 @@ export function OfferCard({ Place image€{price} / night - +

diff --git a/src/components/spinner/Spinner.tsx b/src/components/spinner/Spinner.tsx new file mode 100644 index 0000000..acaccd1 --- /dev/null +++ b/src/components/spinner/Spinner.tsx @@ -0,0 +1,12 @@ +interface SpinnerProps { + caption?: string; +} + +export function Spinner({ caption }: SpinnerProps) { + return ( + <> +
+ {caption && {caption}} + + ); +} diff --git a/src/components/spinner/spinner.css b/src/components/spinner/spinner.css new file mode 100644 index 0000000..ba49813 --- /dev/null +++ b/src/components/spinner/spinner.css @@ -0,0 +1,14 @@ +.spinner { + border: 4px solid rgba(0, 0, 0, 0.1); + border-left-color: #09f; + border-radius: 50%; + width: 50px; + height: 50px; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} diff --git a/src/dataTypes/enums/api-routes.ts b/src/dataTypes/enums/api-routes.ts new file mode 100644 index 0000000..e4f8343 --- /dev/null +++ b/src/dataTypes/enums/api-routes.ts @@ -0,0 +1,3 @@ +export enum ApiRoutes { + Offers = '/offers', +} diff --git a/src/index.tsx b/src/index.tsx index e7a9153..456f533 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +1,17 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import { App } from './components/app.tsx'; -import { offerMocks } from './mocks/offers.ts'; +import { store } from './store/store.ts'; +import { fetchOffers } from './store/actions.ts'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement, ); +store.dispatch(fetchOffers()); + root.render( - + , ); diff --git a/src/mocks/detailed-offer.ts b/src/mocks/detailed-offer.ts deleted file mode 100644 index 262f5b7..0000000 --- a/src/mocks/detailed-offer.ts +++ /dev/null @@ -1,141 +0,0 @@ -export const detailedOfferMocks = [ - { - id: '6af6f711-c28d-4121-82cd-e0b462a27f00', - title: 'Beautiful & luxurious studio at great location', - type: 'apartment', - price: 120, - city: { - name: 'Amsterdam', - location: { - latitude: 52.35514938496378, - longitude: 4.673877537499948, - zoom: 8, - }, - }, - location: { - latitude: 52.35514938496378, - longitude: 4.673877537499948, - zoom: 8, - }, - isFavorite: true, - isPremium: false, - rating: 4, - description: - 'A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam.', - bedrooms: 3, - goods: ['Heating'], - host: { - name: 'Oliver Conner', - avatarUrl: 'https://url-to-image/image.png', - isPro: false, - }, - images: ['https://url-to-image/image.png'], - maxAdults: 4, - }, - { - id: '6af6f711-c28d-4121-82cd-e0b462a27f11', - title: 'Beautiful & luxurious studio at great location', - type: 'apartment', - price: 120, - city: { - name: 'Amsterdam', - location: { - latitude: 52.35514938496378, - longitude: 4.673877537499948, - zoom: 8, - }, - }, - location: { - latitude: 52.35514938496378, - longitude: 4.673877537499948, - zoom: 8, - }, - isFavorite: false, - isPremium: false, - rating: 4, - description: - 'A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam.', - bedrooms: 3, - goods: ['Heating'], - host: { - name: 'Oliver Conner', - avatarUrl: 'https://url-to-image/image.png', - isPro: false, - }, - images: ['https://url-to-image/image.png'], - maxAdults: 4, - }, - { - id: '6af6f711-c28d-4121-82cd-e0b462a27f22', - title: 'Beautiful & luxurious studio at great location', - type: 'apartment', - price: 120, - city: { - name: 'Amsterdam', - location: { - latitude: 52.35514938496378, - longitude: 4.673877537499948, - zoom: 8, - }, - }, - location: { - latitude: 52.35514938496378, - longitude: 4.673877537499948, - zoom: 8, - }, - isFavorite: false, - isPremium: false, - rating: 4, - description: - 'A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam.', - bedrooms: 3, - goods: ['Heating'], - host: { - name: 'Oliver Conner', - avatarUrl: 'https://url-to-image/image.png', - isPro: false, - }, - images: ['https://url-to-image/image.png'], - maxAdults: 4, - }, - { - id: '6af6f711-c28d-4121-82cd-e0b462a27f33', - title: 'Beautiful & luxurious studio at great location', - type: 'apartment', - price: 120, - city: { - name: 'Amsterdam', - location: { - latitude: 52.35514938496378, - longitude: 4.673877537499948, - zoom: 8, - }, - }, - location: { - latitude: 52.3609553943508, - longitude: 4.85309666406198, - zoom: 8, - }, - isFavorite: false, - isPremium: false, - rating: 4, - description: - 'A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam.', - bedrooms: 3, - goods: ['Heating', 'plastation 5 pro max', 'Aboba', 'Amogus'], - host: { - name: 'Angelina Noname', - avatarUrl: 'img/avatar-angelina.jpg', - isPro: true, - }, - images: [ - 'img/apartment-01.jpg', - 'img/apartment-02.jpg', - 'img/apartment-03.jpg', - 'img/room.jpg', - 'img/studio-01.jpg', - 'img/apartment-01.jpg', - ], - maxAdults: 4, - }, -]; diff --git a/src/mocks/offers.ts b/src/mocks/offers.ts deleted file mode 100644 index 87a63c7..0000000 --- a/src/mocks/offers.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Offer } from '../dataTypes/offer.ts'; -import { RoomType } from '../dataTypes/enums/room-type.ts'; - -export const offerMocks: Offer[] = [ - { - id: '6af6f711-c28d-4121-82cd-e0b462a27f00', - title: 'Beautiful & luxurious studio at great location', - type: RoomType.Apartment, - price: 120, - city: { - name: 'Amsterdam', - location: { - latitude: 52.35514938496378, - longitude: 4.673877537499948, - zoom: 8, - }, - }, - location: { - latitude: 52.3909553943508, - longitude: 4.85309666406198, - zoom: 8, - }, - isFavorite: false, - isPremium: false, - rating: 4, - previewImage: 'apartment-01.jpg', - }, - { - id: '6af6f711-c28d-4121-82cd-e0b462a27f33', - title: 'aboba', - type: RoomType.Room, - price: 21412, - city: { - name: 'Amsterdam', - location: { - latitude: 52.35514938496378, - longitude: 4.673877537499948, - zoom: 8, - }, - }, - location: { - latitude: 52.3609553943508, - longitude: 4.85309666406198, - zoom: 8, - }, - isFavorite: false, - isPremium: true, - rating: 3, - previewImage: 'apartment-02.jpg', - }, - { - id: '6af6f711-c28d-4121-82cd-e0b462a27f11', - title: 'Beautiful & luxurious studio at great location', - type: RoomType.Room, - price: 14, - city: { - name: 'Paris', - location: { - latitude: 48.864716123123, - longitude: 2.34901412113, - zoom: 8, - }, - }, - location: { - latitude: 48.864716123123, - longitude: 2.34901412113, - zoom: 8, - }, - isFavorite: true, - isPremium: false, - rating: 4, - previewImage: 'apartment-03.jpg', - }, - { - id: '6af6f711-c28d-4121-82cd-e0b462a27f22', - title: 'Beautiful & luxurious studio at great location', - type: RoomType.Apartment, - price: 88, - city: { - name: 'Amsterdam', - location: { - latitude: 52.35514938496378, - longitude: 4.673877537499948, - zoom: 8, - }, - }, - location: { - latitude: 52.3809553943508, - longitude: 4.939309666406198, - zoom: 8, - }, - isFavorite: true, - isPremium: true, - rating: 5, - previewImage: 'apartment-02.jpg', - }, - { - id: '6af6f711-c28d-4121-82cd-e0b462a27f44', - title: 'amogus', - type: RoomType.Apartment, - price: 88, - city: { - name: 'Paris', - location: { - latitude: 52.35514938496378, - longitude: 4.673877537499948, - zoom: 8, - }, - }, - location: { - latitude: 48.9, - longitude: 2.4, - zoom: 8, - }, - isFavorite: true, - isPremium: true, - rating: 5, - previewImage: 'apartment-02.jpg', - }, -]; diff --git a/src/mocks/reviews.ts b/src/mocks/reviews.ts deleted file mode 100644 index 10b85f8..0000000 --- a/src/mocks/reviews.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Review } from '../dataTypes/review.ts'; - -export const reviewMocks: Review[] = [ - { - id: 'b67ddfd5-b953-4a30-8c8d-bd083cd6b62a', - date: '2019-05-08T14:13:56.569Z', - user: { - name: 'Oliver Conner', - avatarUrl: 'img/avatar-max.jpg', - isPro: false, - }, - comment: - 'A quiet cozy and picturesque that hides behind a a river by the unique lightness of Amsterdam.', - rating: 4, - }, - { - id: 'b67ddfd5-b953-4a30-8c8d-bd083cd6b62b', - date: '2019-05-09T14:16:56.569Z', - user: { - name: 'Alice Conner', - avatarUrl: 'img/avatar-angelina.jpg', - isPro: true, - }, - comment: 'aboba amogus', - rating: 5, - }, -]; diff --git a/src/store/actions.ts b/src/store/actions.ts index edd99b1..d2e3b1b 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,10 +1,60 @@ -import { createAction } from '@reduxjs/toolkit'; +import { createAction, createAsyncThunk } from '@reduxjs/toolkit'; import { City } from '../dataTypes/city.ts'; import { Offer } from '../dataTypes/offer.ts'; import { SortOffers } from '../dataTypes/sort-offers.ts'; +import { AxiosInstance } from 'axios'; +import { AppDispatch, State } from '../dataTypes/store-types.ts'; +import { ApiRoutes } from '../dataTypes/enums/api-routes.ts'; +import { DetailedOffer } from '../dataTypes/detailed-offer.ts'; +import { Nullable } from 'vitest'; export const changeCity = createAction('offers/changeCity'); export const setOffers = createAction('offers/setOffers'); +export const setCurrentOffer = createAction>( + 'offers/setCurrentOffer', +); + export const setSorting = createAction('offers/setSorting'); + +export const setNearbyOffers = createAction('offers/setNearbyOffers'); + +export const fetchOffers = createAsyncThunk< + void, + undefined, + { + dispatch: AppDispatch; + state: State; + extra: AxiosInstance; + } +>('data/fetchOffers', async (_arg, { dispatch, extra: api }) => { + const { data } = await api.get(ApiRoutes.Offers); + dispatch(setOffers(data)); +}); + +export const fetchOffer = createAsyncThunk< + void, + Offer['id'], + { + dispatch: AppDispatch; + state: State; + extra: AxiosInstance; + } +>('data/fetchOffer', async (id, { dispatch, extra: api }) => { + const { data } = await api.get(`${ApiRoutes.Offers}/${id}`); + dispatch(setCurrentOffer(data)); +}); + +export const fetchNearbyOffers = createAsyncThunk< + void, + Offer['id'], + { + dispatch: AppDispatch; + state: State; + extra: AxiosInstance; + } +>('data/fetchNearbyOffers', async (id, { dispatch, extra: api }) => { + const { data } = await api.get(`${ApiRoutes.Offers}/${id}/nearby`); + dispatch(setNearbyOffers(data)); +}); diff --git a/src/store/store.ts b/src/store/store.ts index a46b772..1cf2dc5 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,17 +1,39 @@ import { configureStore, createReducer } from '@reduxjs/toolkit'; -import {changeCity, setOffers, setSorting} from './actions.ts'; -import { offerMocks } from '../mocks/offers.ts'; +import { + changeCity, + setCurrentOffer, + setNearbyOffers, + setOffers, + setSorting, +} from './actions.ts'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { AppDispatch, State } from '../dataTypes/store-types.ts'; import { PARIS } from '../consts/cities.ts'; -import {Offer} from '../dataTypes/offer.ts'; +import { Offer } from '../dataTypes/offer.ts'; +import { createAPI } from '../api/api.ts'; +import { City } from '../dataTypes/city.ts'; +import { SortOffers } from '../dataTypes/sort-offers.ts'; +import { Nullable } from 'vitest'; +import { DetailedOffer } from '../dataTypes/detailed-offer.ts'; -const initialState = { +type InitialState = { + city: City; + offers: Offer[]; + sorting: SortOffers; + currentOffer: Nullable; + nearbyOffers: Offer[]; +}; + +const initialState: InitialState = { city: PARIS, - offers: offerMocks, + offers: [], sorting: (offers: Offer[]) => offers, + currentOffer: null, + nearbyOffers: [], }; +export const api = createAPI(); + const reducer = createReducer(initialState, (builder) => { builder .addCase(changeCity, (state, action) => { @@ -22,10 +44,24 @@ const reducer = createReducer(initialState, (builder) => { }) .addCase(setSorting, (state, action) => { state.sorting = action.payload; + }) + .addCase(setCurrentOffer, (state, action) => { + state.currentOffer = action.payload; + }) + .addCase(setNearbyOffers, (state, action) => { + state.nearbyOffers = action.payload; }); }); -export const store = configureStore({ reducer }); +export const store = configureStore({ + reducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + thunk: { + extraArgument: api, + }, + }), +}); export const useAppDispatch = () => useDispatch();