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..562c593 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 {
-
+
-
+
Other places in the neighbourhood
-
+
diff --git a/src/services/api.ts b/src/services/api.ts
new file mode 100644
index 0000000..35f6851
--- /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..db11914
--- /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());
+};
diff --git a/src/services/token.ts b/src/services/token.ts
new file mode 100644
index 0000000..df439d4
--- /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);
+};
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..4cffb9f
--- /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));
+ },
+ );
diff --git a/src/store/index.ts b/src/store/index.ts
index e741183..de49794 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..a532220
--- /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..7c922a1 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;
+ price: number;
+ city: City;
+ location: Point;
isFavorite: boolean;
+ isPremium: boolean;
rating: number;
- reviews: Review[];
- city: City;
- point: Point;
+ 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..4b7cf60 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}%`;