mirror of
https://github.com/usatiuk/photos.git
synced 2025-10-28 15:27:49 +01:00
simple photo viewer
This commit is contained in:
@@ -23,6 +23,7 @@ import { Overview } from "~Photos/Overview";
|
||||
import { toggleDarkMode } from "~redux/localSettings/actions";
|
||||
import { IAppState } from "~redux/reducers";
|
||||
import { logoutUser } from "~redux/user/actions";
|
||||
import { Photo } from "~Photos/Photo";
|
||||
|
||||
export interface IHomeProps extends RouteComponentProps {
|
||||
user: IUserJSON | null;
|
||||
@@ -100,6 +101,10 @@ export class HomeComponent extends React.PureComponent<IHomeProps> {
|
||||
path="/account"
|
||||
component={Account}
|
||||
/>
|
||||
<Route
|
||||
path="/photos/:id"
|
||||
component={Photo}
|
||||
/>
|
||||
<Route path="/" component={Overview} />
|
||||
</Switch>
|
||||
</animated.div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "./Photos.scss";
|
||||
import "./Overview.scss";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
@@ -12,9 +12,10 @@ import { UploadButton } from "./UploadButton";
|
||||
|
||||
export interface IOverviewComponentProps {
|
||||
photos: IPhotoReqJSON[] | null;
|
||||
fetching: boolean;
|
||||
fetchingError: string | null;
|
||||
fetchingSpinner: boolean;
|
||||
overviewLoaded: boolean;
|
||||
overviewFetching: boolean;
|
||||
overviewFetchingError: string | null;
|
||||
overviewFetchingSpinner: boolean;
|
||||
|
||||
fetchPhotos: () => void;
|
||||
}
|
||||
@@ -22,11 +23,11 @@ export interface IOverviewComponentProps {
|
||||
export const OverviewComponent: React.FunctionComponent<IOverviewComponentProps> = (
|
||||
props,
|
||||
) => {
|
||||
if (!props.photos && !props.fetching) {
|
||||
if (!props.overviewLoaded && !props.overviewFetching) {
|
||||
props.fetchPhotos();
|
||||
}
|
||||
if (!props.photos) {
|
||||
return <LoadingStub spinner={props.fetchingSpinner} />;
|
||||
return <LoadingStub spinner={props.overviewFetchingSpinner} />;
|
||||
}
|
||||
|
||||
const photos = props.photos
|
||||
@@ -46,9 +47,10 @@ export const OverviewComponent: React.FunctionComponent<IOverviewComponentProps>
|
||||
function mapStateToProps(state: IAppState) {
|
||||
return {
|
||||
photos: state.photos.photos,
|
||||
fetching: state.photos.fetching,
|
||||
fetchingError: state.photos.fetchingError,
|
||||
fetchingSpinner: state.photos.fetchingSpinner,
|
||||
overviewLoaded: state.photos.overviewLoaded,
|
||||
overviewFetching: state.photos.overviewFetching,
|
||||
overviewFetchingError: state.photos.overviewFetchingError,
|
||||
overviewFetchingSpinner: state.photos.overviewFetchingSpinner,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
17
frontend/src/Photos/Photo.scss
Normal file
17
frontend/src/Photos/Photo.scss
Normal file
@@ -0,0 +1,17 @@
|
||||
@import "~@blueprintjs/core/lib/scss/variables";
|
||||
|
||||
#photoView {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
#photo {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-dark {
|
||||
#photoView {}
|
||||
}
|
||||
79
frontend/src/Photos/Photo.tsx
Normal file
79
frontend/src/Photos/Photo.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import "./Photo.scss";
|
||||
import * as React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { RouteComponentProps, withRouter } from "react-router";
|
||||
import { Dispatch } from "redux";
|
||||
import { IPhotoReqJSON } from "~../../src/entity/Photo";
|
||||
import { LoadingStub } from "~LoadingStub";
|
||||
import { getPhotoImgPath } from "~redux/api/photos";
|
||||
import { photoLoadStart } from "~redux/photos/actions";
|
||||
import { IPhotoState } from "~redux/photos/reducer";
|
||||
import { IAppState } from "~redux/reducers";
|
||||
|
||||
export interface IPhotoComponentProps extends RouteComponentProps {
|
||||
photo: IPhotoReqJSON | undefined;
|
||||
photoState: IPhotoState | undefined;
|
||||
|
||||
fetchPhoto: (id: number) => void;
|
||||
}
|
||||
|
||||
function getId(props: RouteComponentProps) {
|
||||
return parseInt((props.match?.params as { id: string }).id);
|
||||
}
|
||||
|
||||
export const PhotoComponent: React.FunctionComponent<IPhotoComponentProps> = (
|
||||
props,
|
||||
) => {
|
||||
const id = getId(props);
|
||||
|
||||
if (!props.photo && !props.photoState?.fetching) {
|
||||
console.log(props);
|
||||
props.fetchPhoto(id);
|
||||
}
|
||||
if (!props.photo) {
|
||||
return <LoadingStub spinner={false} />;
|
||||
}
|
||||
|
||||
const fileExists = props.photo.uploaded;
|
||||
|
||||
return (
|
||||
<>
|
||||
{fileExists ? (
|
||||
<div id="photoView">
|
||||
<img
|
||||
id="photo"
|
||||
loading="lazy"
|
||||
src={getPhotoImgPath(props.photo)}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>Photo not uploaded yet</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function mapStateToProps(state: IAppState, props: RouteComponentProps) {
|
||||
const id = getId(props);
|
||||
let photo = undefined;
|
||||
let photoState = undefined;
|
||||
|
||||
if (state.photos.photos) {
|
||||
photo = state.photos.photos.find((p) => p.id === id);
|
||||
}
|
||||
if (state.photos.photoStates[id]) {
|
||||
photoState = state.photos.photoStates[id];
|
||||
}
|
||||
return {
|
||||
photo,
|
||||
photoState,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch) {
|
||||
return { fetchPhoto: (id: number) => dispatch(photoLoadStart(id)) };
|
||||
}
|
||||
|
||||
export const Photo = withRouter(
|
||||
connect(mapStateToProps, mapDispatchToProps)(PhotoComponent),
|
||||
);
|
||||
@@ -13,8 +13,9 @@ import { Dispatch } from "redux";
|
||||
import { photoDeleteCancel, photoDeleteStart } from "~redux/photos/actions";
|
||||
import { connect } from "react-redux";
|
||||
import { LoadingStub } from "~LoadingStub";
|
||||
import { RouteComponentProps, withRouter } from "react-router";
|
||||
|
||||
export interface IPhotoCardComponentProps {
|
||||
export interface IPhotoCardComponentProps extends RouteComponentProps {
|
||||
photo: IPhotoReqJSON;
|
||||
|
||||
deletePhoto: (photo: IPhotoReqJSON) => void;
|
||||
@@ -47,11 +48,9 @@ export class PhotoCardComponent extends React.PureComponent<
|
||||
<Card
|
||||
className="photoCard"
|
||||
interactive={true}
|
||||
/*
|
||||
onClick={() =>
|
||||
this.props.history.push(`/docs/${this.props.doc.id}`)
|
||||
this.props.history.push(`/photos/${this.props.photo.id}`)
|
||||
}
|
||||
*/
|
||||
>
|
||||
{fileExists ? (
|
||||
<img
|
||||
@@ -91,4 +90,6 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
||||
};
|
||||
}
|
||||
|
||||
export const PhotoCard = connect(null, mapDispatchToProps)(PhotoCardComponent);
|
||||
export const PhotoCard = withRouter(
|
||||
connect(null, mapDispatchToProps)(PhotoCardComponent),
|
||||
);
|
||||
|
||||
@@ -11,9 +11,9 @@ const fetchPhotosFn = jest.fn();
|
||||
|
||||
const overviewComponentDefaultProps: IOverviewComponentProps = {
|
||||
photos: null,
|
||||
fetching: false,
|
||||
fetchingError: null,
|
||||
fetchingSpinner: false,
|
||||
overviewFetching: false,
|
||||
overviewFetchingError: null,
|
||||
overviewFetchingSpinner: false,
|
||||
|
||||
fetchPhotos: fetchPhotosFn,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IPhotoReqJSON } from "~../../src/entity/Photo";
|
||||
import {
|
||||
IPhotosByIDDeleteRespBody,
|
||||
IPhotosByIDGetRespBody,
|
||||
IPhotosListRespBody,
|
||||
IPhotosNewRespBody,
|
||||
IPhotosUploadRespBody,
|
||||
@@ -22,6 +23,10 @@ export async function fetchPhotosList(): Promise<IPhotosListRespBody> {
|
||||
return fetchJSONAuth("/photos/list", "GET");
|
||||
}
|
||||
|
||||
export async function fetchPhoto(id: number): Promise<IPhotosByIDGetRespBody> {
|
||||
return fetchJSONAuth(`/photos/byID/${id}`, "GET");
|
||||
}
|
||||
|
||||
export async function createPhoto(
|
||||
hash: string,
|
||||
size: string,
|
||||
|
||||
@@ -10,6 +10,9 @@ export enum PhotoTypes {
|
||||
PHOTOS_LOAD_START = "PHOTOS_LOAD",
|
||||
PHOTOS_LOAD_SUCCESS = "PHOTOS_LOAD_SUCCESS",
|
||||
PHOTOS_LOAD_FAIL = "PHOTOS_LOAD_FAIL",
|
||||
PHOTO_LOAD_START = "PHOTO_LOAD",
|
||||
PHOTO_LOAD_SUCCESS = "PHOTO_LOAD_SUCCESS",
|
||||
PHOTO_LOAD_FAIL = "PHOTO_LOAD_FAIL",
|
||||
PHOTOS_UPLOAD_START = "PHOTOS_UPLOAD",
|
||||
PHOTO_CREATE_SUCCESS = "PHOTO_CREATE_SUCCESS",
|
||||
PHOTO_CREATE_FAIL = "PHOTO_CREATE_FAIL",
|
||||
@@ -36,6 +39,22 @@ export interface IPhotosLoadFailAction extends Action {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface IPhotoLoadStartAction extends Action {
|
||||
type: PhotoTypes.PHOTO_LOAD_START;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface IPhotoLoadSuccessAction extends Action {
|
||||
type: PhotoTypes.PHOTO_LOAD_SUCCESS;
|
||||
photo: IPhotoReqJSON;
|
||||
}
|
||||
|
||||
export interface IPhotoLoadFailAction extends Action {
|
||||
type: PhotoTypes.PHOTO_LOAD_FAIL;
|
||||
id: number;
|
||||
error: string;
|
||||
}
|
||||
|
||||
export interface IPhotosUploadStartAction extends Action {
|
||||
type: PhotoTypes.PHOTOS_UPLOAD_START;
|
||||
files: FileList;
|
||||
@@ -92,6 +111,10 @@ export function photosLoadStart(): IPhotosLoadStartAction {
|
||||
return { type: PhotoTypes.PHOTOS_LOAD_START };
|
||||
}
|
||||
|
||||
export function photoLoadStart(id: number): IPhotoLoadStartAction {
|
||||
return { type: PhotoTypes.PHOTO_LOAD_START, id };
|
||||
}
|
||||
|
||||
export function photosUploadStart(files: FileList): IPhotosUploadStartAction {
|
||||
return { type: PhotoTypes.PHOTOS_UPLOAD_START, files };
|
||||
}
|
||||
@@ -143,6 +166,16 @@ export function photosLoadFail(error: string): IPhotosLoadFailAction {
|
||||
return { type: PhotoTypes.PHOTOS_LOAD_FAIL, error };
|
||||
}
|
||||
|
||||
export function photoLoadSuccess(
|
||||
photo: IPhotoReqJSON,
|
||||
): IPhotoLoadSuccessAction {
|
||||
return { type: PhotoTypes.PHOTO_LOAD_SUCCESS, photo };
|
||||
}
|
||||
|
||||
export function photoLoadFail(id:number,error: string): IPhotoLoadFailAction {
|
||||
return { type: PhotoTypes.PHOTO_LOAD_FAIL,id, error };
|
||||
}
|
||||
|
||||
export function photoDeleteStart(
|
||||
photo: IPhotoReqJSON,
|
||||
): IPhotoDeleteStartAction {
|
||||
@@ -183,4 +216,7 @@ export type PhotoAction =
|
||||
| IPhotoDeleteFailAction
|
||||
| IPhotoDeleteStartAction
|
||||
| IPhotoDeleteSuccessAction
|
||||
| IPhotoDeleteCancelAction;
|
||||
| IPhotoDeleteCancelAction
|
||||
| IPhotoLoadFailAction
|
||||
| IPhotoLoadStartAction
|
||||
| IPhotoLoadSuccessAction;
|
||||
|
||||
@@ -3,20 +3,32 @@ import { IPhotoReqJSON } from "~../../src/entity/Photo";
|
||||
import { UserAction, UserTypes } from "~redux/user/actions";
|
||||
import { PhotoAction, PhotoTypes } from "./actions";
|
||||
|
||||
export interface IPhotosState {
|
||||
photos: IPhotoReqJSON[] | null;
|
||||
export interface IPhotoState {
|
||||
fetching: boolean;
|
||||
fetchingError: string | null;
|
||||
fetchingSpinner: boolean;
|
||||
}
|
||||
|
||||
export interface IPhotosState {
|
||||
photos: IPhotoReqJSON[] | null;
|
||||
|
||||
photoStates: Record<number, IPhotoState>;
|
||||
|
||||
overviewFetching: boolean;
|
||||
overviewLoaded: boolean;
|
||||
overviewFetchingError: string | null;
|
||||
overviewFetchingSpinner: boolean;
|
||||
|
||||
deleteCache: Record<number, IPhotoReqJSON>;
|
||||
}
|
||||
|
||||
const defaultPhotosState: IPhotosState = {
|
||||
photos: null,
|
||||
fetching: false,
|
||||
fetchingError: null,
|
||||
fetchingSpinner: false,
|
||||
overviewLoaded: false,
|
||||
overviewFetching: false,
|
||||
overviewFetchingError: null,
|
||||
overviewFetchingSpinner: false,
|
||||
|
||||
photoStates: {},
|
||||
|
||||
deleteCache: {},
|
||||
};
|
||||
@@ -31,15 +43,67 @@ export const photosReducer: Reducer<IPhotosState, PhotoAction> = (
|
||||
case PhotoTypes.PHOTOS_LOAD_START:
|
||||
return {
|
||||
...defaultPhotosState,
|
||||
fetching: true,
|
||||
fetchingSpinner: false,
|
||||
overviewFetching: true,
|
||||
overviewFetchingSpinner: false,
|
||||
};
|
||||
case PhotoTypes.PHOTOS_START_FETCHING_SPINNER:
|
||||
return { ...state, fetchingSpinner: true };
|
||||
return { ...state, overviewFetchingSpinner: true };
|
||||
case PhotoTypes.PHOTOS_LOAD_SUCCESS:
|
||||
return { ...defaultPhotosState, photos: action.photos };
|
||||
return {
|
||||
...defaultPhotosState,
|
||||
photos: action.photos,
|
||||
overviewLoaded: true,
|
||||
};
|
||||
case PhotoTypes.PHOTOS_LOAD_FAIL:
|
||||
return { ...defaultPhotosState, fetchingError: action.error };
|
||||
return {
|
||||
...defaultPhotosState,
|
||||
overviewFetchingError: action.error,
|
||||
};
|
||||
|
||||
case PhotoTypes.PHOTO_LOAD_START: {
|
||||
const { photoStates } = state;
|
||||
photoStates[action.id] = {
|
||||
fetching: true,
|
||||
fetchingError: null,
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
photoStates,
|
||||
};
|
||||
}
|
||||
case PhotoTypes.PHOTO_LOAD_SUCCESS: {
|
||||
const { photoStates } = state;
|
||||
photoStates[action.photo.id] = {
|
||||
fetching: false,
|
||||
fetchingError: null,
|
||||
};
|
||||
if (state.photos) {
|
||||
const photos = state.photos;
|
||||
const photosNoDup = photos.filter(
|
||||
(p) => p.id !== action.photo.id,
|
||||
);
|
||||
const updPhotos = [action.photo, ...photosNoDup];
|
||||
return { ...state, photos: updPhotos, photoStates };
|
||||
} else {
|
||||
const photos = [action.photo];
|
||||
return {
|
||||
...state,
|
||||
photos,
|
||||
photoStates,
|
||||
};
|
||||
}
|
||||
}
|
||||
case PhotoTypes.PHOTO_LOAD_FAIL: {
|
||||
const { photoStates } = state;
|
||||
photoStates[action.id] = {
|
||||
fetching: false,
|
||||
fetchingError: action.error,
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
photoStates,
|
||||
};
|
||||
}
|
||||
case PhotoTypes.PHOTO_CREATE_SUCCESS:
|
||||
if (state.photos) {
|
||||
const photos = state.photos;
|
||||
|
||||
@@ -14,16 +14,20 @@ import * as SparkMD5 from "spark-md5";
|
||||
import {
|
||||
createPhoto,
|
||||
deletePhoto,
|
||||
fetchPhoto,
|
||||
fetchPhotosList,
|
||||
uploadPhoto,
|
||||
} from "~redux/api/photos";
|
||||
import {
|
||||
IPhotoDeleteStartAction,
|
||||
IPhotoLoadStartAction,
|
||||
IPhotosUploadStartAction,
|
||||
photoCreateFail,
|
||||
photoCreateSuccess,
|
||||
photoDeleteFail,
|
||||
photoDeleteSuccess,
|
||||
photoLoadFail,
|
||||
photoLoadSuccess,
|
||||
photosLoadFail,
|
||||
photosLoadSuccess,
|
||||
photosStartFetchingSpinner,
|
||||
@@ -130,6 +134,32 @@ function* photosLoad() {
|
||||
}
|
||||
}
|
||||
|
||||
function* photoLoad(action: IPhotoLoadStartAction) {
|
||||
try {
|
||||
//const spinner = yield fork(startSpinner);
|
||||
|
||||
const { response, timeout } = yield race({
|
||||
response: call(fetchPhoto, action.id),
|
||||
timeout: delay(10000),
|
||||
});
|
||||
|
||||
//yield cancel(spinner);
|
||||
|
||||
if (timeout) {
|
||||
yield put(photoLoadFail(action.id, "Timeout"));
|
||||
return;
|
||||
}
|
||||
if (response.data) {
|
||||
const photo = response.data;
|
||||
yield put(photoLoadSuccess(photo));
|
||||
} else {
|
||||
yield put(photoLoadFail(action.id, response.error));
|
||||
}
|
||||
} catch (e) {
|
||||
yield put(photoLoadFail(action.id, "Internal error"));
|
||||
}
|
||||
}
|
||||
|
||||
function* photoUpload(f: File) {
|
||||
try {
|
||||
const hash = yield call(computeChecksumMd5, f);
|
||||
@@ -218,6 +248,7 @@ export function* photosSaga() {
|
||||
yield all([
|
||||
takeLatest(PhotoTypes.PHOTOS_LOAD_START, photosLoad),
|
||||
takeLatest(PhotoTypes.PHOTOS_UPLOAD_START, photosUpload),
|
||||
takeLatest(PhotoTypes.PHOTO_LOAD_START, photoLoad),
|
||||
takeEvery(PhotoTypes.PHOTO_DELETE_START, photoDelete),
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user