a badly looking image cards thing

This commit is contained in:
2020-10-14 20:50:30 +03:00
committed by Stepan Usatiuk
parent 07ccc56636
commit c20642dba4
17 changed files with 364 additions and 65 deletions

View File

@@ -1,10 +1,12 @@
import { Spinner } from "@blueprintjs/core";
import * as React from "react";
export function LoadingStub() {
return (
<div className="loadingWrapper">
<Spinner />
</div>
);
export interface ILoadingStubProps {
spinner?: boolean;
}
export const LoadingStub: React.FunctionComponent<ILoadingStubProps> = (
props,
) => {
return <div className="loadingWrapper">{props.spinner && <Spinner />}</div>;
};

View File

@@ -3,24 +3,53 @@ import * as React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { IAppState } from "~redux/reducers";
import { photosLoadStart } from "~redux/photos/actions";
import { IPhotoReqJSON } from "~../../src/entity/Photo";
import { LoadingStub } from "~LoadingStub";
import { PhotoCard } from "./PhotoCard";
export interface IOverviewComponentProps {
photos: IPhotoReqJSON[] | null;
fetching: boolean;
spinner: boolean;
fetchingError: string | null;
fetchingSpinner: boolean;
fetchPhotos: () => void;
}
export function OverviewComponent() {
return <div id="overview">Overview!</div>;
}
export const OverviewComponent: React.FunctionComponent<IOverviewComponentProps> = (
props,
) => {
if (!props.photos && !props.fetching) {
props.fetchPhotos();
}
if (!props.photos) {
return <LoadingStub spinner={props.fetchingSpinner} />;
}
const photos = props.photos.map((photo) => (
<PhotoCard key={photo.id} photo={photo} />
));
return (
<div id="overview">
{" "}
<div className="list">{photos}</div>
</div>
);
};
function mapStateToProps(state: IAppState) {
return {};
return {
photos: state.photos.photos,
fetching: state.photos.fetching,
fetchingError: state.photos.fetchingError,
fetchingSpinner: state.photos.fetchingSpinner,
};
}
function mapDispatchToProps(dispatch: Dispatch) {
return {};
return { fetchPhotos: () => dispatch(photosLoadStart()) };
}
export const Overview = connect(

View File

@@ -0,0 +1,16 @@
import { Card } from "@blueprintjs/core";
import * as React from "react";
import { IPhotoReqJSON } from "~../../src/entity/Photo";
import { getPhotoImgPath } from "~redux/api/photos";
export interface IPhotoCardProps {
photo: IPhotoReqJSON;
}
export const PhotoCard: React.FunctionComponent<IPhotoCardProps> = (props) => {
return (
<Card className="photoCard">
<img src={getPhotoImgPath(props.photo)}></img>
</Card>
);
};

View File

@@ -3,9 +3,35 @@
#overview {
display: flex;
flex-direction: column;
.list {
display: flex;
flex-shrink: 0;
flex-grow: 0;
flex-wrap: wrap;
// 400px is the minimal width for 2 cards to fit
@media (max-width: 400px) {
justify-content: center;
}
.photoCard {
transition: 0.3s;
user-select: none;
height: 15rem;
width: 10.5rem;
margin: 1rem;
padding: 0rem;
overflow: hidden;
img {
height: auto;
width: 100%;
}
}
}
}
.bp3-dark {
#overview {
}
}
#overview {}
}

View File

@@ -1,15 +1,26 @@
import * as React from "react";
import { shallow } from "enzyme";
import { OverviewComponent } from "../Overview";
import { IOverviewComponentProps, OverviewComponent } from "../Overview";
afterEach(() => {
jest.restoreAllMocks();
});
const overviewComponentDefaultProps: IOverviewComponentProps = {
photos: null,
fetching: false,
fetchingError: null,
fetchingSpinner: false,
fetchPhotos: jest.fn(),
};
describe("<Overview />", () => {
it("should not crash", () => {
const wrapper = shallow(<OverviewComponent />);
const wrapper = shallow(
<OverviewComponent {...overviewComponentDefaultProps} />,
);
expect(wrapper.contains("Overview!")).toBeTruthy();
});
});

View File

@@ -1,25 +1,27 @@
import { IUserAuthJSON } from "~../../src/entity/User";
import { IAPIResponse } from "~../../src/types";
import {
IUserLoginRespBody,
IUserSignupRespBody,
} from "~../../src/routes/users";
import { fetchJSON } from "../utils";
export async function login(
username: string,
password: string,
): Promise<IAPIResponse<IUserAuthJSON>> {
): Promise<IUserLoginRespBody> {
return (fetchJSON("/users/login", "POST", {
username,
password,
}) as unknown) as Promise<IAPIResponse<IUserAuthJSON>>;
}) as unknown) as Promise<IUserLoginRespBody>;
}
export async function signup(
username: string,
password: string,
email: string,
): Promise<IAPIResponse<IUserAuthJSON>> {
): Promise<IUserSignupRespBody> {
return (fetchJSON("/users/signup", "POST", {
username,
password,
email,
}) as unknown) as Promise<IAPIResponse<IUserAuthJSON>>;
}) as unknown) as Promise<IUserSignupRespBody>;
}

View File

@@ -0,0 +1,14 @@
import { IPhotoReqJSON } from "~../../src/entity/Photo";
import { IPhotosListRespBody } from "~../../src/routes/photos";
import { apiRoot } from "~env";
import { fetchJSONAuth } from "./utils";
export function getPhotoImgPath(photo: IPhotoReqJSON): string {
return `${apiRoot}/photos/showByID/${photo.id}/${photo.accessToken}`;
}
export async function fetchPhotosList(): Promise<IPhotosListRespBody> {
return (fetchJSONAuth("/photos/list", "GET") as unknown) as Promise<
IPhotosListRespBody
>;
}

View File

@@ -1,15 +1,16 @@
import { fetchJSON, fetchJSONAuth } from "../utils";
import { IAPIResponse } from "~/../../src/types";
import { IUserAuthJSON, IUserJSON } from "~../../src/entity/User";
import { fetchJSONAuth } from "../utils";
import { IUserEditRespBody, IUserGetRespBody } from "~../../src/routes/users";
export async function fetchUser(): Promise<IAPIResponse<IUserJSON>> {
export async function fetchUser(): Promise<IUserGetRespBody> {
return (fetchJSONAuth("/users/user", "GET") as unknown) as Promise<
IAPIResponse<IUserAuthJSON>
IUserGetRespBody
>;
}
export async function changeUserPassword(newPassword: string) {
export async function changeUserPassword(
newPassword: string,
): Promise<IUserEditRespBody> {
return (fetchJSONAuth("/users/edit", "POST", {
password: newPassword,
}) as unknown) as Promise<IAPIResponse<IUserAuthJSON>>;
}) as unknown) as Promise<IUserEditRespBody>;
}

View File

@@ -0,0 +1,51 @@
import { Action } from "redux";
import { IPhotoReqJSON, Photo } from "~../../src/entity/Photo";
export enum PhotoTypes {
PHOTOS_LOAD_START = "PHOTOS_LOAD",
PHOTOS_LOAD_SUCCESS = "PHOTOS_LOAD_SUCCESS",
PHOTOS_LOAD_FAIL = "PHOTOS_LOAD_FAIL",
PHOTOS_START_FETCHING_SPINNER = "PHOTOS_START_FETCHING_SPINNER",
}
export interface IPhotosLoadStartAction extends Action {
type: PhotoTypes.PHOTOS_LOAD_START;
}
export interface IPhotosLoadSuccessAction extends Action {
type: PhotoTypes.PHOTOS_LOAD_SUCCESS;
photos: IPhotoReqJSON[];
}
export interface IPhotosLoadFailAction extends Action {
type: PhotoTypes.PHOTOS_LOAD_FAIL;
error: string;
}
export interface IPhotosStartFetchingSpinner extends Action {
type: PhotoTypes.PHOTOS_START_FETCHING_SPINNER;
}
export function photosLoadStart(): IPhotosLoadStartAction {
return { type: PhotoTypes.PHOTOS_LOAD_START };
}
export function photosLoadSuccess(
photos: IPhotoReqJSON[],
): IPhotosLoadSuccessAction {
return { type: PhotoTypes.PHOTOS_LOAD_SUCCESS, photos };
}
export function photosLoadFail(error: string): IPhotosLoadFailAction {
return { type: PhotoTypes.PHOTOS_LOAD_FAIL, error };
}
export function photosStartFetchingSpinner(): IPhotosStartFetchingSpinner {
return { type: PhotoTypes.PHOTOS_START_FETCHING_SPINNER };
}
export type PhotoAction =
| IPhotosLoadStartAction
| IPhotosLoadFailAction
| IPhotosLoadSuccessAction
| IPhotosStartFetchingSpinner;

View File

@@ -0,0 +1,40 @@
import { Reducer } from "redux";
import { IPhotoReqJSON } from "~../../src/entity/Photo";
import { PhotoAction, PhotoTypes } from "./actions";
export interface IPhotosState {
photos: IPhotoReqJSON[] | null;
fetching: boolean;
fetchingError: string | null;
fetchingSpinner: boolean;
}
const defaultPhotosState: IPhotosState = {
photos: null,
fetching: false,
fetchingError: null,
fetchingSpinner: false,
};
export const photosReducer: Reducer<IPhotosState, PhotoAction> = (
state: IPhotosState = defaultPhotosState,
action: PhotoAction,
) => {
switch (action.type) {
case PhotoTypes.PHOTOS_LOAD_START:
return {
...defaultPhotosState,
fetching: true,
fetchingSpinner: false,
};
case PhotoTypes.PHOTOS_START_FETCHING_SPINNER:
return { ...state, fetchingSpinner: true };
case PhotoTypes.PHOTOS_LOAD_SUCCESS:
return { ...defaultPhotosState, photos: action.photos };
case PhotoTypes.PHOTOS_LOAD_FAIL:
return { ...defaultPhotosState, fetchingError: action.error };
default:
return state;
}
return state;
};

View File

@@ -0,0 +1,52 @@
import {
all,
call,
cancel,
delay,
fork,
put,
race,
takeLatest,
} from "redux-saga/effects";
import { fetchPhotosList } from "~redux/api/photos";
import {
photosLoadFail,
photosLoadSuccess,
photosStartFetchingSpinner,
PhotoTypes,
} from "./actions";
function* startSpinner() {
yield delay(300);
yield put(photosStartFetchingSpinner());
}
function* photosLoad() {
try {
const spinner = yield fork(startSpinner);
const { response, timeout } = yield race({
response: call(fetchPhotosList),
timeout: delay(10000),
});
yield cancel(spinner);
if (timeout) {
yield put(photosLoadFail("Timeout"));
return;
}
if (response.data) {
const photos = response.data;
yield put(photosLoadSuccess(photos));
} else {
yield put(photosLoadFail(response.error));
}
} catch (e) {
yield put(photosLoadFail("Internal error"));
}
}
export function* photosSaga() {
yield all([takeLatest(PhotoTypes.PHOTOS_LOAD_START, photosLoad)]);
}

View File

@@ -7,12 +7,14 @@ import {
ILocalSettingsState,
localSettingsReducer,
} from "./localSettings/reducer";
import { IPhotosState, photosReducer } from "./photos/reducer";
import { IUserState, userReducer } from "./user/reducer";
export interface IAppState {
auth: IAuthState & PersistPartial;
user: IUserState;
localSettings: ILocalSettingsState & PersistPartial;
photos: IPhotosState;
}
const authPersistConfig = {
@@ -33,4 +35,5 @@ export const rootReducer = combineReducers({
localSettingsPersistConfig,
localSettingsReducer as any,
),
photos: photosReducer,
});

View File

@@ -6,6 +6,7 @@ import { rootReducer } from "~redux/reducers";
import { setToken } from "./api/utils";
import { authSaga } from "./auth/sagas";
import { photosSaga } from "./photos/sagas";
import { getUser } from "./user/actions";
import { userSaga } from "./user/sagas";
@@ -26,3 +27,4 @@ export const persistor = persistStore(store, null, () => {
sagaMiddleware.run(authSaga);
sagaMiddleware.run(userSaga);
sagaMiddleware.run(photosSaga);