mirror of
https://github.com/usatiuk/photos.git
synced 2025-10-28 15:27:49 +01:00
a badly looking image cards thing
This commit is contained in:
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
16
frontend/src/Photos/PhotoCard.tsx
Normal file
16
frontend/src/Photos/PhotoCard.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
14
frontend/src/redux/api/photos.ts
Normal file
14
frontend/src/redux/api/photos.ts
Normal 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
|
||||
>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
51
frontend/src/redux/photos/actions.ts
Normal file
51
frontend/src/redux/photos/actions.ts
Normal 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;
|
||||
40
frontend/src/redux/photos/reducer.ts
Normal file
40
frontend/src/redux/photos/reducer.ts
Normal 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;
|
||||
};
|
||||
52
frontend/src/redux/photos/sagas.ts
Normal file
52
frontend/src/redux/photos/sagas.ts
Normal 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)]);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user