From 795703b266f1fdb5852bb58bb24f0b4957a0e2c3 Mon Sep 17 00:00:00 2001 From: mayonnaise <90057279+Mayanzev@users.noreply.github.com> Date: Tue, 30 Apr 2024 01:48:35 +0300 Subject: [PATCH 1/2] module7-task1 --- src/components/app/app.tsx | 19 ++++-- src/components/city-list/city-list.tsx | 16 +++-- .../error-message/error-message.css | 9 +++ .../error-message/error-message.tsx | 13 ++++ src/components/map/map.tsx | 8 +-- src/components/offer-card/offer-card.tsx | 7 ++- src/const.ts | 59 +++++++++++++++++++ src/hooks/use-map.tsx | 6 +- src/index.tsx | 5 ++ src/mocks/cities.ts | 34 ----------- src/mocks/offers.ts | 59 ------------------- src/mocks/points.ts | 20 ------- .../favorites-screen/favorites-screen.tsx | 10 ++-- src/pages/loading-screen/loading-screen.tsx | 7 +++ src/pages/main-screen/main-screen.tsx | 11 ++-- src/pages/offer-screen/offer-screen.tsx | 31 +++++----- src/services/api.ts | 54 +++++++++++++++++ src/services/process-error-handle.ts | 8 +++ src/services/token.ts | 16 +++++ src/store/action.ts | 23 ++++++-- src/store/api-actions.ts | 32 ++++++++++ src/store/index.ts | 13 +++- src/store/reducer.ts | 34 +++++++---- src/types/city.ts | 4 -- src/types/location.ts | 10 ++++ src/types/offer.ts | 17 +++--- src/types/point.ts | 4 -- src/utils.ts | 2 + 28 files changed, 333 insertions(+), 198 deletions(-) create mode 100644 src/components/error-message/error-message.css create mode 100644 src/components/error-message/error-message.tsx delete mode 100644 src/mocks/cities.ts delete mode 100644 src/mocks/offers.ts delete mode 100644 src/mocks/points.ts create mode 100644 src/pages/loading-screen/loading-screen.tsx create mode 100644 src/services/api.ts create mode 100644 src/services/process-error-handle.ts create mode 100644 src/services/token.ts create mode 100644 src/store/api-actions.ts delete mode 100644 src/types/city.ts create mode 100644 src/types/location.ts delete mode 100644 src/types/point.ts diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx index 58c794b..87bd2cd 100644 --- a/src/components/app/app.tsx +++ b/src/components/app/app.tsx @@ -6,11 +6,20 @@ import NotFoundScreen from '../../pages/not-found-screen/not-found-screen.tsx'; import OfferScreen from '../../pages/offer-screen/offer-screen.tsx'; import PrivateRoute from '../private-route/private-route.tsx'; import { AppRoute, AuthorizationStatus } from '../../const.ts'; -import { Offer } from '../../types/offer'; -import { OFFERS } from '../../mocks/offers.ts'; +import { useAppSelector } from '../../hooks/index.ts'; +import LoadingScreen from '../../pages/loading-screen/loading-screen.tsx'; + function App(): JSX.Element { - const favourites: Offer[] = OFFERS.filter((o) => o.isFavorite); + + const isQuestionsDataLoading = useAppSelector((state) => state.isQuestionsDataLoading); + + if (isQuestionsDataLoading) { + return ( + + ); + } + return ( @@ -20,12 +29,12 @@ function App(): JSX.Element { path={AppRoute.Favorites} element={ - + } /> } /> - } /> + } /> ); diff --git a/src/components/city-list/city-list.tsx b/src/components/city-list/city-list.tsx index 5f062c1..5877faf 100644 --- a/src/components/city-list/city-list.tsx +++ b/src/components/city-list/city-list.tsx @@ -1,13 +1,11 @@ -import { City } from '../../types/city'; -import { useAppDispatch } from '../../hooks'; +import { City } from '../../types/location'; +import { useAppDispatch, useAppSelector } from '../../hooks'; import { changeCity } from '../../store/action'; -import { CITIES } from '../../mocks/cities'; +import { CITIES } from '../../const'; -type CityListProps = { - chosenCity: City; -} -function CityList({chosenCity}: CityListProps): JSX.Element { +function CityList(): JSX.Element { + const chosenCity = useAppSelector((state) => state.city); const dispatch = useAppDispatch(); const handleCityChange = (city: City) => { dispatch(changeCity(city)); @@ -15,12 +13,12 @@ function CityList({chosenCity}: CityListProps): JSX.Element { return( diff --git a/src/pages/loading-screen/loading-screen.tsx b/src/pages/loading-screen/loading-screen.tsx new file mode 100644 index 0000000..4d5f781 --- /dev/null +++ b/src/pages/loading-screen/loading-screen.tsx @@ -0,0 +1,7 @@ +function LoadingScreen(): JSX.Element { + return ( +

Loading ...

+ ); + } + + export default LoadingScreen; \ No newline at end of file diff --git a/src/pages/main-screen/main-screen.tsx b/src/pages/main-screen/main-screen.tsx index 96993a8..5261bfe 100644 --- a/src/pages/main-screen/main-screen.tsx +++ b/src/pages/main-screen/main-screen.tsx @@ -7,9 +7,10 @@ import CityList from '../../components/city-list/city-list'; import CardsSortingOptions from '../../components/cards-sorting-options/cards-sorting-options'; function MainScreen(): JSX.Element { - const [city, offers] = useAppSelector((state) => [state.city, state.offers]); - const chosenOffers = offers.filter((offer) => offer.city === city); - const points = chosenOffers.map((offer) => offer.point); + const city = useAppSelector((state) => state.city); + const offers = useAppSelector((state) => state.offers); + const chosenOffers = offers.filter((offer) => offer.city.name === city.name); + const points = chosenOffers.map((offer) => offer.location); const favoriteOffers = offers.filter((offer) => offer.isFavorite); return (
@@ -48,14 +49,14 @@ function MainScreen(): JSX.Element {

Cities

- +

Places

- {chosenOffers.length} places to stay in {city.title} + {chosenOffers.length} places to stay in {city.name}
diff --git a/src/pages/offer-screen/offer-screen.tsx b/src/pages/offer-screen/offer-screen.tsx index 2f8a840..471020c 100644 --- a/src/pages/offer-screen/offer-screen.tsx +++ b/src/pages/offer-screen/offer-screen.tsx @@ -1,17 +1,14 @@ import { Link } from 'react-router-dom'; import ReviewsList from '../../components/reviews-list/reviews-list'; -import { OFFERS } from '../../mocks/offers'; -import { Offer } from '../../types/offer'; -import { POINTS } from '../../mocks/points'; import Map from '../../components/map/map'; import OfferList from '../../components/offer-list/offer-list'; -import { typeOfCardList } from '../../utils'; +import { ratingPercentage, typeOfCardList } from '../../utils'; +import { useAppSelector } from '../../hooks'; +import { REVIEWS } from '../../mocks/reviews'; -type OfferScreenProps = { - offer: Offer; -} -function OfferScreen({offer}: OfferScreenProps): JSX.Element { +function OfferScreen(): JSX.Element { + const [offer, offers] = useAppSelector((state) => [state.chosenOffer, state.offers]); return (
@@ -71,14 +68,14 @@ function OfferScreen({offer}: OfferScreenProps): JSX.Element {
- {offer.isPremium ? ( + {offer?.isPremium ? (
Premium
) : null}

- {offer.title} + {offer?.title}

- + Rating
- {offer.rating} + {offer?.rating}
  • - {offer.type} + {offer?.type}
  • 3 Bedrooms @@ -106,7 +103,7 @@ function OfferScreen({offer}: OfferScreenProps): JSX.Element {
- €{offer.price} + €{offer?.price}  night
@@ -163,17 +160,17 @@ function OfferScreen({offer}: OfferScreenProps): JSX.Element {

- +
- + offer.location)}/>

Other places in the neighbourhood

- +
diff --git a/src/services/api.ts b/src/services/api.ts new file mode 100644 index 0000000..ef5b976 --- /dev/null +++ b/src/services/api.ts @@ -0,0 +1,54 @@ +import axios, { AxiosError, AxiosInstance, AxiosResponse } from "axios"; +import { getToken } from "./token"; +import { StatusCodes } from "http-status-codes"; +import { processErrorHandle } from "./process-error-handle"; + +type DetailMessageType = { + type: string; + message: string; +} + +const StatusCodeMapping: Record = { + [StatusCodes.BAD_REQUEST]: true, + [StatusCodes.UNAUTHORIZED]: true, + [StatusCodes.NOT_FOUND]: true +}; + +const shouldDisplayError = (response: AxiosResponse) => !!StatusCodeMapping[response.status]; + +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, + }); + + api.interceptors.request.use( + (config) => { + const token = getToken(); + + if (token && config.headers) { + config.headers['x-token'] = token; + } + + return config; + }, + ); + + api.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + if (error.response && shouldDisplayError(error.response)) { + const detailMessage = (error.response.data); + + processErrorHandle(detailMessage.message); + } + + throw error; + } + ); + + return api; +}; diff --git a/src/services/process-error-handle.ts b/src/services/process-error-handle.ts new file mode 100644 index 0000000..e0d2ef3 --- /dev/null +++ b/src/services/process-error-handle.ts @@ -0,0 +1,8 @@ +import {store} from '../store'; +import {setError} from '../store/action'; +import {clearErrorAction} from '../store/api-actions'; + +export const processErrorHandle = (message: string): void => { + store.dispatch(setError(message)); + store.dispatch(clearErrorAction()); +}; \ No newline at end of file diff --git a/src/services/token.ts b/src/services/token.ts new file mode 100644 index 0000000..9c7a9e8 --- /dev/null +++ b/src/services/token.ts @@ -0,0 +1,16 @@ +const AUTH_TOKEN_KEY_NAME = 'six-cities-token'; + +export type Token = string; + +export const getToken = (): Token => { + const token = localStorage.getItem(AUTH_TOKEN_KEY_NAME); + return token ?? ''; +}; + +export const saveToken = (token: Token): void => { + localStorage.setItem(AUTH_TOKEN_KEY_NAME, token); +}; + +export const dropToken = (): void => { + localStorage.removeItem(AUTH_TOKEN_KEY_NAME); +}; \ No newline at end of file diff --git a/src/store/action.ts b/src/store/action.ts index 30000ad..366ca6f 100644 --- a/src/store/action.ts +++ b/src/store/action.ts @@ -1,8 +1,7 @@ import { createAction } from '@reduxjs/toolkit'; -import { City } from '../types/city'; -import { Point } from '../types/point'; - -export const getOffers = createAction('OFFERS_GET'); +import { City } from '../types/location'; +import { Point } from '../types/location'; +import { Offer } from '../types/offer'; export const changeCity = createAction('CITY_CHANGE', (value: City) => ({ payload: value @@ -15,3 +14,19 @@ export const changeSortOptions = createAction('CHANGE_SORT_OPTIONS', (value: str export const changeHighlightedMarker = createAction('CHANGE_HIGHLIGHTED_MARKER', (value: Point | undefined) => ({ payload: value })); + +export const loadOffers = createAction('LOAD_OFFERS', (value: Offer[]) => ({ + payload: value +})); + +export const changeChosenOffer = createAction('CHANGE_CHOSEN_OFFER', (value:Offer) => ({ + payload: value +})); + +export const setQuestionsDataLoadingStatus = createAction('SET_QUESTIONS_DATA_LOADING_STATUS', (value: boolean) => ({ + payload: value +})); + +export const setError = createAction('SET_ERROR', (value: string | null) => ({ + payload: value +})); diff --git a/src/store/api-actions.ts b/src/store/api-actions.ts new file mode 100644 index 0000000..44c7ca6 --- /dev/null +++ b/src/store/api-actions.ts @@ -0,0 +1,32 @@ +import { createAsyncThunk } from "@reduxjs/toolkit"; +import { AppDispatch, State } from "../types/state"; +import { AxiosInstance } from "axios"; +import { loadOffers, setError, setQuestionsDataLoadingStatus } from "./action"; +import { Offer } from "../types/offer"; +import { APIRoute, TIMEOUT_SHOW_ERROR } from "../const"; + +import {store} from './'; + +export const clearErrorAction = createAsyncThunk( + 'CLEAR_ERROR_ACTION', + () => { + setTimeout( + () => store.dispatch(setError(null)), + TIMEOUT_SHOW_ERROR, + ); + }, +); + +export const fetchOffersAction = createAsyncThunk( + 'FETCH_OFFERS_ACTION', + async (_arg, {dispatch, extra: api}) => { + dispatch(setQuestionsDataLoadingStatus(true)); + const {data} = await api.get(APIRoute.Offers); + dispatch(setQuestionsDataLoadingStatus(false)); + dispatch(loadOffers(data)); + }, + ); \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts index e741183..44e24d6 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,4 +1,15 @@ import { configureStore } from '@reduxjs/toolkit'; import { reducer } from './reducer'; +import { createAPI } from '../services/api'; -export const store = configureStore({reducer}); +export const api = createAPI(); + +export const store = configureStore({ + reducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + thunk: { + extraArgument: api, + }, + }) +}); diff --git a/src/store/reducer.ts b/src/store/reducer.ts index f131dd1..70300df 100644 --- a/src/store/reducer.ts +++ b/src/store/reducer.ts @@ -1,11 +1,10 @@ import { createReducer } from '@reduxjs/toolkit'; -import { CITIES } from '../mocks/cities'; -import { OFFERS } from '../mocks/offers'; -import { City } from '../types/city'; +import { City } from '../types/location'; import { Offer } from '../types/offer'; -import { changeCity, changeHighlightedMarker, changeSortOptions, getOffers } from './action'; +import { changeChosenOffer, changeCity, changeHighlightedMarker, changeSortOptions, loadOffers, setError, setQuestionsDataLoadingStatus } from './action'; import { filters } from '../utils'; -import { Point } from '../types/point'; +import { Point } from '../types/location'; +import { CITIES } from '../const'; type StateType = { @@ -13,20 +12,23 @@ type StateType = { offers: Offer[]; sortType: string; highlightedMarker?: Point; + chosenOffer: Offer | undefined; + isQuestionsDataLoading: boolean; + error: string | null; } const initialState: StateType = { city: CITIES[0], - offers: OFFERS, + offers: [], sortType: filters.POPULAR, - highlightedMarker: undefined + highlightedMarker: undefined, + chosenOffer: undefined, + isQuestionsDataLoading: false, + error: null }; const reducer = createReducer(initialState, (builder) => { builder - .addCase(getOffers, (state) => { - state.offers = OFFERS; - }) .addCase(changeCity, (state, action) => { state.city = action.payload; }) @@ -35,6 +37,18 @@ const reducer = createReducer(initialState, (builder) => { }) .addCase(changeHighlightedMarker, (state, action) => { state.highlightedMarker = action.payload; + }) + .addCase(loadOffers, (state, action) => { + state.offers = action.payload; + }) + .addCase(changeChosenOffer, (state, action) => { + state.chosenOffer = action.payload; + }) + .addCase(setQuestionsDataLoadingStatus, (state, action) => { + state.isQuestionsDataLoading = action.payload; + }) + .addCase(setError, (state, action) => { + state.error = action.payload; }); }); diff --git a/src/types/city.ts b/src/types/city.ts deleted file mode 100644 index a809991..0000000 --- a/src/types/city.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Point } from '../types/point'; -export type City = { - title: string; -} & Point; diff --git a/src/types/location.ts b/src/types/location.ts new file mode 100644 index 0000000..a60f947 --- /dev/null +++ b/src/types/location.ts @@ -0,0 +1,10 @@ +export type Point = { + latitude: number; + longitude: number; + zoom: number; +} + +export type City = { + name: string; + location: Point +} diff --git a/src/types/offer.ts b/src/types/offer.ts index 9811de6..bc6822d 100644 --- a/src/types/offer.ts +++ b/src/types/offer.ts @@ -1,17 +1,14 @@ -import { Point } from './point'; -import { Review } from '../types/review'; -import { City } from './city'; +import { Point, City } from './location'; export type Offer = { id: string; - image: string[]; - isPremium: boolean; - price: number; title: string; type: string; - isFavorite: boolean; - rating: number; - reviews: Review[]; + price: number; city: City; - point: Point; + location: Point; + isFavorite: boolean + isPremium: boolean + rating: number + previewImage: string }; diff --git a/src/types/point.ts b/src/types/point.ts deleted file mode 100644 index 04a0b11..0000000 --- a/src/types/point.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Point = { - lat: number; - lng: number; - }; diff --git a/src/utils.ts b/src/utils.ts index cd9dea5..10145d3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -37,3 +37,5 @@ export const getSortedOffers = ( return offers; } }; + +export const ratingPercentage = (rating: number) => `${(rating / 5) * 100}%` \ No newline at end of file From 5b88998aa0262137516ddc880dc3c9596a5d663d Mon Sep 17 00:00:00 2001 From: mayonnaise <90057279+Mayanzev@users.noreply.github.com> Date: Tue, 30 Apr 2024 02:03:51 +0300 Subject: [PATCH 2/2] fix --- src/components/error-message/error-message.tsx | 2 +- src/const.ts | 4 ++-- src/pages/loading-screen/loading-screen.tsx | 12 ++++++------ src/pages/offer-screen/offer-screen.tsx | 2 +- src/services/api.ts | 10 +++++----- src/services/process-error-handle.ts | 2 +- src/services/token.ts | 2 +- src/store/api-actions.ts | 14 +++++++------- src/store/index.ts | 14 +++++++------- src/types/location.ts | 2 +- src/types/offer.ts | 8 ++++---- src/utils.ts | 2 +- 12 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/components/error-message/error-message.tsx b/src/components/error-message/error-message.tsx index a1ed6ac..93d568b 100644 --- a/src/components/error-message/error-message.tsx +++ b/src/components/error-message/error-message.tsx @@ -10,4 +10,4 @@ function ErrorMessage(): JSX.Element | null { } -export default ErrorMessage; \ No newline at end of file +export default ErrorMessage; diff --git a/src/const.ts b/src/const.ts index 64bdd7c..361c404 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,4 +1,4 @@ -import { City } from "./types/location"; +import { City } from './types/location'; export enum AppRoute { Main = '/', @@ -74,4 +74,4 @@ export const CITIES: City[] = [ }, ]; -export const TIMEOUT_SHOW_ERROR = 2000; \ No newline at end of file +export const TIMEOUT_SHOW_ERROR = 2000; diff --git a/src/pages/loading-screen/loading-screen.tsx b/src/pages/loading-screen/loading-screen.tsx index 4d5f781..b886023 100644 --- a/src/pages/loading-screen/loading-screen.tsx +++ b/src/pages/loading-screen/loading-screen.tsx @@ -1,7 +1,7 @@ function LoadingScreen(): JSX.Element { - return ( -

Loading ...

- ); - } - - export default LoadingScreen; \ No newline at end of file + return ( +

Loading ...

+ ); +} + +export default LoadingScreen; diff --git a/src/pages/offer-screen/offer-screen.tsx b/src/pages/offer-screen/offer-screen.tsx index 471020c..562c593 100644 --- a/src/pages/offer-screen/offer-screen.tsx +++ b/src/pages/offer-screen/offer-screen.tsx @@ -164,7 +164,7 @@ function OfferScreen(): JSX.Element {
- offer.location)}/> + nearOffer.location)}/>
diff --git a/src/services/api.ts b/src/services/api.ts index ef5b976..35f6851 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,7 +1,7 @@ -import axios, { AxiosError, AxiosInstance, AxiosResponse } from "axios"; -import { getToken } from "./token"; -import { StatusCodes } from "http-status-codes"; -import { processErrorHandle } from "./process-error-handle"; +import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'; +import { getToken } from './token'; +import { StatusCodes } from 'http-status-codes'; +import { processErrorHandle } from './process-error-handle'; type DetailMessageType = { type: string; @@ -26,7 +26,7 @@ export const createAPI = (): AxiosInstance => { }); api.interceptors.request.use( - (config) => { + (config) => { const token = getToken(); if (token && config.headers) { diff --git a/src/services/process-error-handle.ts b/src/services/process-error-handle.ts index e0d2ef3..db11914 100644 --- a/src/services/process-error-handle.ts +++ b/src/services/process-error-handle.ts @@ -5,4 +5,4 @@ import {clearErrorAction} from '../store/api-actions'; export const processErrorHandle = (message: string): void => { store.dispatch(setError(message)); store.dispatch(clearErrorAction()); -}; \ No newline at end of file +}; diff --git a/src/services/token.ts b/src/services/token.ts index 9c7a9e8..df439d4 100644 --- a/src/services/token.ts +++ b/src/services/token.ts @@ -13,4 +13,4 @@ export const saveToken = (token: Token): void => { export const dropToken = (): void => { localStorage.removeItem(AUTH_TOKEN_KEY_NAME); -}; \ No newline at end of file +}; diff --git a/src/store/api-actions.ts b/src/store/api-actions.ts index 44c7ca6..4cffb9f 100644 --- a/src/store/api-actions.ts +++ b/src/store/api-actions.ts @@ -1,9 +1,9 @@ -import { createAsyncThunk } from "@reduxjs/toolkit"; -import { AppDispatch, State } from "../types/state"; -import { AxiosInstance } from "axios"; -import { loadOffers, setError, setQuestionsDataLoadingStatus } from "./action"; -import { Offer } from "../types/offer"; -import { APIRoute, TIMEOUT_SHOW_ERROR } from "../const"; +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { AppDispatch, State } from '../types/state'; +import { AxiosInstance } from 'axios'; +import { loadOffers, setError, setQuestionsDataLoadingStatus } from './action'; +import { Offer } from '../types/offer'; +import { APIRoute, TIMEOUT_SHOW_ERROR } from '../const'; import {store} from './'; @@ -29,4 +29,4 @@ export const fetchOffersAction = createAsyncThunk - getDefaultMiddleware({ - thunk: { - extraArgument: api, - }, - }) + reducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + thunk: { + extraArgument: api, + }, + }) }); diff --git a/src/types/location.ts b/src/types/location.ts index a60f947..a532220 100644 --- a/src/types/location.ts +++ b/src/types/location.ts @@ -6,5 +6,5 @@ export type Point = { export type City = { name: string; - location: Point + location: Point; } diff --git a/src/types/offer.ts b/src/types/offer.ts index bc6822d..7c922a1 100644 --- a/src/types/offer.ts +++ b/src/types/offer.ts @@ -7,8 +7,8 @@ export type Offer = { price: number; city: City; location: Point; - isFavorite: boolean - isPremium: boolean - rating: number - previewImage: string + isFavorite: boolean; + isPremium: boolean; + rating: number; + previewImage: string; }; diff --git a/src/utils.ts b/src/utils.ts index 10145d3..4b7cf60 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -38,4 +38,4 @@ export const getSortedOffers = ( } }; -export const ratingPercentage = (rating: number) => `${(rating / 5) * 100}%` \ No newline at end of file +export const ratingPercentage = (rating: number) => `${(rating / 5) * 100}%`;