Skip to content

Commit

Permalink
Merge pull request #10 from ktvtk/module7-task2
Browse files Browse the repository at this point in the history
  • Loading branch information
keksobot authored Nov 23, 2024
2 parents 28e0fa5 + cf94866 commit e3a50ed
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 30 deletions.
16 changes: 9 additions & 7 deletions src/components/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {MainScreen} from '../../pages/main-screen/main-screen.tsx';
import {LoginScreen} from '../../pages/login-screen/login-screen.tsx';
import {OfferScreen} from '../../pages/offer-screen/offer-screen.tsx';
import {NotFoundScreen} from '../../pages/not-found-screen/not-found-screen.tsx';
import {AppRoute, AuthorizationStatus} from '../../const.ts';
import {PrivateRoute} from '../private-route/private-route.tsx';
import {AppRoute} from '../../const.ts';
import {PrivateRouteAuthorized, PrivateRouteUnauthorized} from '../private-route/private-route.tsx';
import {FavoriteScreen} from '../../pages/favorites-screen/favorite-screen.tsx';
import {HelmetProvider} from 'react-helmet-async';

Expand All @@ -20,7 +20,11 @@ export function App(): React.JSX.Element {
/>
<Route
path={AppRoute.Login}
element={<LoginScreen />}
element={
<PrivateRouteUnauthorized>
<LoginScreen />
</PrivateRouteUnauthorized>
}
/>
<Route
path={AppRoute.Offer}
Expand All @@ -29,11 +33,9 @@ export function App(): React.JSX.Element {
<Route
path={AppRoute.Favorites}
element={
<PrivateRoute
authorizationStatus={AuthorizationStatus.Auth}
>
<PrivateRouteAuthorized>
<FavoriteScreen />
</PrivateRoute>
</PrivateRouteAuthorized>
}
/>
<Route
Expand Down
20 changes: 15 additions & 5 deletions src/components/private-route/private-route.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import {JSX} from 'react';
import {Navigate} from 'react-router-dom';
import {AppRoute, AuthorizationStatus} from '../../const';
import {useAppSelector} from '../../hooks';

type PrivateRouteProps = {
authorizationStatus: AuthorizationStatus;
children: JSX.Element;
}

export function PrivateRoute(props: PrivateRouteProps): JSX.Element {
const {authorizationStatus, children} = props;

export function PrivateRouteAuthorized(props: PrivateRouteProps): JSX.Element {
const {children} = props;
const isAuthorized = useAppSelector((state) => state.authorizationStatus) === AuthorizationStatus.Authorized;
return (
authorizationStatus === AuthorizationStatus.Auth
isAuthorized

Check failure on line 14 in src/components/private-route/private-route.tsx

View workflow job for this annotation

GitHub Actions / Check

Expected indentation of 4 spaces but found 6
? children
: <Navigate to={AppRoute.Login} />
);
}

export function PrivateRouteUnauthorized(props: PrivateRouteProps): JSX.Element {
const {children} = props;
const isAuthorized = useAppSelector((state) => state.authorizationStatus) === AuthorizationStatus.Unauthorized;
return (
isAuthorized
? children
: <Navigate to={AppRoute.Main} />
);
}
6 changes: 3 additions & 3 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ export enum AppRoute {
}

export enum AuthorizationStatus {
Auth = 'AUTH',
NoAuth = 'NO_AUTH',
Unknown = 'UNKNOWN',
Authorized ,
Unauthorized ,
Unknown
}

export enum PlaceType {
Expand Down
3 changes: 2 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import ReactDOM from 'react-dom/client';
import { App } from './components/app/app.tsx';
import {Provider} from 'react-redux';
import {store} from './store';
import {fetchOffers} from './store/api-actions.ts';
import {checkAuthorization, fetchOffers} from './store/api-actions.ts';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);

store.dispatch(fetchOffers());
store.dispatch(checkAuthorization());

root.render(
<React.StrictMode>
Expand Down
50 changes: 46 additions & 4 deletions src/pages/login-screen/login-screen.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import {JSX} from 'react';
import {FormEvent, JSX, useState} from 'react';
import {Helmet} from 'react-helmet-async';
import {Link} from 'react-router-dom';
import {AppRoute} from '../../const.ts';
import {LoginInfo} from '../../types/user.ts';
import {store} from '../../store';
import {login} from '../../store/api-actions.ts';

export function LoginScreen() : JSX.Element {
const [loginInfo, setLoginInfo] = useState<LoginInfo>({
email: '',
password: ''
});

const submitHandle = (evt: FormEvent) => {
evt.preventDefault();
store.dispatch(login(loginInfo));
console.log(loginInfo)

Check failure on line 18 in src/pages/login-screen/login-screen.tsx

View workflow job for this annotation

GitHub Actions / Check

Unexpected console statement

Check failure on line 18 in src/pages/login-screen/login-screen.tsx

View workflow job for this annotation

GitHub Actions / Check

Missing semicolon
}

Check failure on line 19 in src/pages/login-screen/login-screen.tsx

View workflow job for this annotation

GitHub Actions / Check

Missing semicolon

return (
<div className="page page--gray page--login">
<Helmet>
Expand All @@ -28,13 +42,41 @@ export function LoginScreen() : JSX.Element {
<form className="login__form form" action="#" method="post">
<div className="login__input-wrapper form__input-wrapper">
<label className="visually-hidden">E-mail</label>
<input className="login__input form__input" type="email" name="email" placeholder="Email" required/>
<input
className="login__input form__input"
type="email"
name="email"
placeholder="Email"
onChange={(event) =>
setLoginInfo({
...loginInfo,
email: event.target.value
})
}

Check failure on line 55 in src/pages/login-screen/login-screen.tsx

View workflow job for this annotation

GitHub Actions / Check

Unexpected newline before '}'
required/>

Check failure on line 56 in src/pages/login-screen/login-screen.tsx

View workflow job for this annotation

GitHub Actions / Check

The closing bracket must be aligned with the opening tag (expected column 17 on the next line)
</div>
<div className="login__input-wrapper form__input-wrapper">
<label className="visually-hidden">Password</label>
<input className="login__input form__input" type="password" name="password" placeholder="Password" required/>
<input
className="login__input form__input"
type="password"
name="password"
placeholder="Password"
onChange={(event) =>
setLoginInfo({
...loginInfo,
password: event.target.value
})
}

Check failure on line 70 in src/pages/login-screen/login-screen.tsx

View workflow job for this annotation

GitHub Actions / Check

Unexpected newline before '}'
required/>

Check failure on line 71 in src/pages/login-screen/login-screen.tsx

View workflow job for this annotation

GitHub Actions / Check

The closing bracket must be aligned with the opening tag (expected column 17 on the next line)
</div>
<button className="login__submit form__submit button" type="submit">Sign in</button>
<button
className="login__submit form__submit button"
type="submit"
onClick={submitHandle}
>
Sign in
</button>
</form>
</section>
<section className="locations locations--login locations--current">
Expand Down
13 changes: 8 additions & 5 deletions src/pages/offer-screen/offer-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export function OfferScreen() : JSX.Element {
3,
);
const reviews = useAppSelector((state) => state.reviews);
const favoriteCount = useAppSelector((state) => state.offers).filter((commonOffer) => commonOffer.isFavorite).length;

if (offer === null){
return (<Loading />);
}
Expand All @@ -48,12 +50,13 @@ export function OfferScreen() : JSX.Element {
<nav className="header__nav">
<ul className="header__nav-list">
<li className="header__nav-item user">
<a className="header__nav-link header__nav-link--profile" href="#">
<div className="header__avatar-wrapper user__avatar-wrapper">
</div>
<div className="header__avatar-wrapper user__avatar-wrapper"></div>
<Link to={AppRoute.Main}>
<span className="header__user-name user__name">[email protected]</span>
<span className="header__favorite-count">3</span>
</a>
</Link>
<Link to={AppRoute.Favorites}>
<span className="header__favorite-count">{favoriteCount}</span>
</Link>
</li>
<li className="header__nav-item">
<a className="header__nav-link" href="#">
Expand Down
22 changes: 21 additions & 1 deletion src/servises/api.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import axios, {AxiosInstance, InternalAxiosRequestConfig} from 'axios';
import axios, {AxiosError, AxiosInstance, InternalAxiosRequestConfig} from 'axios';
import {getToken} from './token.ts';
import {store} from '../store';
import {setAuthorizationStatus} from '../store/action.ts';
import {AuthorizationStatus} from '../const.ts';

const baseURL = 'https://14.design.htmlacademy.pro/six-cities';
const requestTimeout = 5000;

type ErrorMessageType = {
errorType: string;
message: string;
};

export const createAPI = () : AxiosInstance => {
const api = axios.create({
baseURL: baseURL,
Expand All @@ -22,6 +30,18 @@ export const createAPI = () : AxiosInstance => {
}
);

api.interceptors.response.use(
(response) => response,
(error: AxiosError<ErrorMessageType>) => {
if (error.response && error.response.status === 401) {
store.dispatch(
setAuthorizationStatus(AuthorizationStatus.Unauthorized),
);
}
throw error;
},
);

return api;
};

3 changes: 3 additions & 0 deletions src/store/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {City} from '../types/city.ts';
import {Offer} from '../types/offer.ts';
import {DetailOffer} from '../types/detail-offer.ts';
import {Review} from '../types/review.ts';
import {AuthorizationStatus} from '../const.ts';

export const changeActiveCity = createAction<City>('offers/changeActiveCity');

Expand All @@ -13,3 +14,5 @@ export const setDetailOffer = createAction<DetailOffer | null>('offers/setDetail
export const setNearOffers = createAction<Offer[]>('offers/setNearOffers');

export const setReviews = createAction<Review[]>('offers/setReviews');

export const setAuthorizationStatus = createAction<AuthorizationStatus>('auth/setAuthorizationStatus');
44 changes: 42 additions & 2 deletions src/store/api-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {createAsyncThunk} from '@reduxjs/toolkit';
import {AppDispatch, State} from '../types/state.ts';
import {AxiosInstance} from 'axios';
import {Offer} from '../types/offer.ts';
import {apiRoute} from '../const.ts';
import {setOffers, setDetailOffer, setNearOffers, setReviews} from './action.ts';
import {apiRoute, AuthorizationStatus} from '../const.ts';
import {setOffers, setDetailOffer, setNearOffers, setReviews, setAuthorizationStatus} from './action.ts';
import {DetailOffer} from '../types/detail-offer.ts';
import {Review} from '../types/review.ts';
import {saveToken} from '../servises/token.ts';
import {AuthInfo, LoginInfo} from '../types/user.ts';

export const fetchOffers = createAsyncThunk<void, undefined, {
dispatch: AppDispatch;
Expand Down Expand Up @@ -54,3 +56,41 @@ export const fetchReviews = createAsyncThunk<void, Offer['id'], {
dispatch(setReviews(data));
}
);

export const login = createAsyncThunk<void, LoginInfo,
{
dispatch: AppDispatch;
state: State;
extra: AxiosInstance;
}
>('auth/login', async (loginInfo, { dispatch, extra: api }) => {
const response = await api.post<AuthInfo>(apiRoute.login, loginInfo);
if (response.status === 200 || response.status === 201) {
dispatch(setAuthorizationStatus(AuthorizationStatus.Authorized));
saveToken(response.data.token);
} else {
throw response;
}
});

export const checkAuthorization = createAsyncThunk<void, undefined,
{
dispatch: AppDispatch;
state: State;
extra: AxiosInstance;
}
>('auth/checkAuthorization', async (_arg, { dispatch, extra: api }) => {
await api.get(apiRoute.login);
dispatch(setAuthorizationStatus(AuthorizationStatus.Unauthorized));
});

export const logut = createAsyncThunk<void, undefined,
{
dispatch: AppDispatch;
state: State;
extra: AxiosInstance;
}
>('auth/logout', async (_arg, { dispatch, extra: api }) => {
await api.delete(apiRoute.logout);
dispatch(setAuthorizationStatus(AuthorizationStatus.Unauthorized));
});
16 changes: 14 additions & 2 deletions src/store/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import {Paris} from '../const.ts';
import {AuthorizationStatus, Paris} from '../const.ts';
import {createReducer} from '@reduxjs/toolkit';
import {changeActiveCity, setDetailOffer, setNearOffers, setOffers, setReviews} from './action.ts';
import {
changeActiveCity,
setAuthorizationStatus,
setDetailOffer,
setNearOffers,
setOffers,
setReviews
} from './action.ts';
import {City} from '../types/city.ts';
import {Offer} from '../types/offer.ts';
import {DetailOffer} from '../types/detail-offer.ts';
Expand All @@ -12,6 +19,7 @@ type InitialState = {
detailOffer: DetailOffer | null;
nearOffers: Offer[];
reviews: Review[];
authorizationStatus: AuthorizationStatus;
}

const initialState : InitialState = {
Expand All @@ -20,6 +28,7 @@ const initialState : InitialState = {
detailOffer: null,
nearOffers: [],
reviews: [],
authorizationStatus: AuthorizationStatus.Unknown,
};

export const reducer = createReducer(initialState, (builder) => {
Expand All @@ -38,5 +47,8 @@ export const reducer = createReducer(initialState, (builder) => {
})
.addCase(setReviews, (state, action) => {
state.reviews = action.payload;
})
.addCase(setAuthorizationStatus, (state, action) => {
state.authorizationStatus = action.payload;
});
});
13 changes: 13 additions & 0 deletions src/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,16 @@ export type User = {
avatarUrl: string;
isPro: boolean;
};

export type LoginInfo = {
email: string;
password: string;
};

export type AuthInfo = {
name: string;
avatarUrl: string;
isPro: boolean;
email: string;
token: string;
};

0 comments on commit e3a50ed

Please sign in to comment.