mirror of
https://github.com/usatiuk/photos.git
synced 2025-10-28 15:27:49 +01:00
pagination
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loadingWrapper {
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
.viewComponent {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
width: 80%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -$pt-navbar-height;
|
||||
bottom: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-top: 2 * $pt-navbar-height + 20px;
|
||||
padding-top: $pt-navbar-height;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#mainContainer {
|
||||
|
||||
@@ -26,52 +26,71 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#overviewContainer {
|
||||
padding-top: 2rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
#overview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
#overview {
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
#actionbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.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) {
|
||||
.photosLoader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding-top: 5rem;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
.photoCard {
|
||||
#actionbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: 0.3s;
|
||||
user-select: none;
|
||||
height: 15rem;
|
||||
width: 20rem;
|
||||
margin: 1rem;
|
||||
padding: 0rem;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
.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 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transition: 0.3s;
|
||||
user-select: none;
|
||||
height: 15rem;
|
||||
width: 20rem;
|
||||
margin: 1rem;
|
||||
padding: 0rem;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
min-height: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.bp3-dark {
|
||||
|
||||
@@ -7,13 +7,14 @@ import { photosLoadStart } from "~redux/photos/actions";
|
||||
import { IPhotoReqJSON } from "~../../src/entity/Photo";
|
||||
import { LoadingStub } from "~LoadingStub";
|
||||
import { PhotoCard } from "./PhotoCard";
|
||||
import { Button, Classes, Overlay } from "@blueprintjs/core";
|
||||
import { Button, Classes, Overlay, Spinner } from "@blueprintjs/core";
|
||||
import { UploadButton } from "./UploadButton";
|
||||
import { Photo } from "./Photo";
|
||||
|
||||
export interface IOverviewComponentProps {
|
||||
photos: IPhotoReqJSON[] | null;
|
||||
overviewLoaded: boolean;
|
||||
photos: IPhotoReqJSON[];
|
||||
triedLoading: boolean;
|
||||
allPhotosLoaded: boolean;
|
||||
overviewFetching: boolean;
|
||||
overviewFetchingError: string | null;
|
||||
overviewFetchingSpinner: boolean;
|
||||
@@ -27,27 +28,38 @@ export const OverviewComponent: React.FunctionComponent<IOverviewComponentProps>
|
||||
const [selectedPhoto, setSelectedPhoto] = React.useState<number>(0);
|
||||
const [isOverlayOpened, setOverlayOpen] = React.useState<boolean>(false);
|
||||
|
||||
if (!props.overviewLoaded && !props.overviewFetching) {
|
||||
props.fetchPhotos();
|
||||
}
|
||||
if (!props.photos) {
|
||||
return <LoadingStub spinner={props.overviewFetchingSpinner} />;
|
||||
}
|
||||
|
||||
const onCardClick = (id: number) => {
|
||||
setSelectedPhoto(id);
|
||||
setOverlayOpen(true);
|
||||
};
|
||||
|
||||
const photos = props.photos
|
||||
.sort((a, b) => b.shotAt - a.shotAt)
|
||||
.map((photo) => (
|
||||
<PhotoCard
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
onClick={() => onCardClick(photo.id)}
|
||||
/>
|
||||
));
|
||||
if (
|
||||
props.photos.length === 0 &&
|
||||
!props.triedLoading &&
|
||||
!props.overviewFetching
|
||||
) {
|
||||
props.fetchPhotos();
|
||||
}
|
||||
|
||||
const photos = props.photos.map((photo) => (
|
||||
<PhotoCard
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
onClick={() => onCardClick(photo.id)}
|
||||
/>
|
||||
));
|
||||
|
||||
function onLoaderScroll(e: React.UIEvent<HTMLElement>) {
|
||||
if (
|
||||
e.currentTarget.scrollTop + e.currentTarget.clientHeight >=
|
||||
e.currentTarget.scrollHeight
|
||||
) {
|
||||
console.log(props.allPhotosLoaded, props.overviewFetching);
|
||||
if (!props.allPhotosLoaded && !props.overviewFetching) {
|
||||
props.fetchPhotos();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -65,11 +77,16 @@ export const OverviewComponent: React.FunctionComponent<IOverviewComponentProps>
|
||||
<Photo id={selectedPhoto} />
|
||||
</div>
|
||||
</Overlay>
|
||||
<div id="overview">
|
||||
<div id="actionbar">
|
||||
<UploadButton />
|
||||
<div id="overviewContainer" onScroll={onLoaderScroll}>
|
||||
<div id="overview">
|
||||
<div id="actionbar">
|
||||
<UploadButton />
|
||||
</div>
|
||||
<div className="list">{photos}</div>
|
||||
<div className="photosLoader">
|
||||
{props.overviewFetching && <Spinner />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="list">{photos}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -78,10 +95,11 @@ export const OverviewComponent: React.FunctionComponent<IOverviewComponentProps>
|
||||
function mapStateToProps(state: IAppState) {
|
||||
return {
|
||||
photos: state.photos.photos,
|
||||
overviewLoaded: state.photos.overviewLoaded,
|
||||
allPhotosLoaded: state.photos.allPhotosLoaded,
|
||||
overviewFetching: state.photos.overviewFetching,
|
||||
overviewFetchingError: state.photos.overviewFetchingError,
|
||||
overviewFetchingSpinner: state.photos.overviewFetchingSpinner,
|
||||
triedLoading: state.photos.triedLoading,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
0
frontend/src/Photos/PhotoLoader.tsx
Normal file
0
frontend/src/Photos/PhotoLoader.tsx
Normal file
@@ -19,8 +19,15 @@ export function getPhotoThumbPath(photo: IPhotoReqJSON, size: number): string {
|
||||
}?size=${size.toString()}`;
|
||||
}
|
||||
|
||||
export async function fetchPhotosList(): Promise<IPhotosListRespBody> {
|
||||
return fetchJSONAuth("/photos/list", "GET");
|
||||
export async function fetchPhotosList(
|
||||
skip: number,
|
||||
num: number,
|
||||
): Promise<IPhotosListRespBody> {
|
||||
const params = new URLSearchParams({
|
||||
skip: skip.toString(),
|
||||
num: num.toString(),
|
||||
});
|
||||
return fetchJSONAuth(`/photos/list?${params.toString()}`, "GET");
|
||||
}
|
||||
|
||||
export async function fetchPhoto(id: number): Promise<IPhotosByIDGetRespBody> {
|
||||
|
||||
@@ -9,12 +9,13 @@ export interface IPhotoState {
|
||||
}
|
||||
|
||||
export interface IPhotosState {
|
||||
photos: IPhotoReqJSON[] | null;
|
||||
photos: IPhotoReqJSON[];
|
||||
|
||||
photoStates: Record<number, IPhotoState>;
|
||||
|
||||
overviewFetching: boolean;
|
||||
overviewLoaded: boolean;
|
||||
allPhotosLoaded: boolean;
|
||||
triedLoading: boolean;
|
||||
overviewFetchingError: string | null;
|
||||
overviewFetchingSpinner: boolean;
|
||||
|
||||
@@ -27,9 +28,10 @@ export interface IPhotosState {
|
||||
}
|
||||
|
||||
const defaultPhotosState: IPhotosState = {
|
||||
photos: null,
|
||||
overviewLoaded: false,
|
||||
photos: [],
|
||||
allPhotosLoaded: false,
|
||||
overviewFetching: false,
|
||||
triedLoading: false,
|
||||
overviewFetchingError: null,
|
||||
overviewFetchingSpinner: false,
|
||||
|
||||
@@ -52,21 +54,31 @@ export const photosReducer: Reducer<IPhotosState, PhotoAction> = (
|
||||
return defaultPhotosState;
|
||||
case PhotoTypes.PHOTOS_LOAD_START:
|
||||
return {
|
||||
...defaultPhotosState,
|
||||
...state,
|
||||
overviewFetching: true,
|
||||
triedLoading: true,
|
||||
overviewFetchingSpinner: false,
|
||||
};
|
||||
case PhotoTypes.PHOTOS_START_FETCHING_SPINNER:
|
||||
return { ...state, overviewFetchingSpinner: true };
|
||||
case PhotoTypes.PHOTOS_LOAD_SUCCESS:
|
||||
case PhotoTypes.PHOTOS_LOAD_SUCCESS: {
|
||||
let { allPhotosLoaded } = state;
|
||||
if (action.photos.length === 0) {
|
||||
allPhotosLoaded = true;
|
||||
}
|
||||
const oldPhotos = state.photos ? state.photos : [];
|
||||
return {
|
||||
...defaultPhotosState,
|
||||
photos: action.photos,
|
||||
overviewLoaded: true,
|
||||
...state,
|
||||
photos: [...oldPhotos, ...action.photos],
|
||||
triedLoading: true,
|
||||
allPhotosLoaded,
|
||||
overviewFetching: false,
|
||||
};
|
||||
}
|
||||
case PhotoTypes.PHOTOS_LOAD_FAIL:
|
||||
return {
|
||||
...defaultPhotosState,
|
||||
triedLoading: true,
|
||||
overviewFetchingError: action.error,
|
||||
};
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
photoUploadSuccess,
|
||||
} from "./actions";
|
||||
import { IPhotosNewRespBody } from "~../../src/routes/photos";
|
||||
import { IPhotosListPagination } from "~../../src/types";
|
||||
|
||||
// Thanks, https://dev.to/qortex/compute-md5-checksum-for-a-file-in-typescript-59a4
|
||||
function computeChecksumMd5(file: File): Promise<string> {
|
||||
@@ -108,21 +109,24 @@ function computeSize(f: File) {
|
||||
});
|
||||
}
|
||||
|
||||
// Shouldn't be used anymore
|
||||
function* startSpinner() {
|
||||
yield delay(300);
|
||||
yield put(photosStartFetchingSpinner());
|
||||
}
|
||||
|
||||
function* photosLoad() {
|
||||
const state = yield select();
|
||||
try {
|
||||
const spinner = yield fork(startSpinner);
|
||||
//const spinner = yield fork(startSpinner);
|
||||
|
||||
const skip = state.photos.photos ? state.photos.photos.length : 0;
|
||||
const { response, timeout } = yield race({
|
||||
response: call(fetchPhotosList),
|
||||
response: call(fetchPhotosList, skip, IPhotosListPagination),
|
||||
timeout: delay(10000),
|
||||
});
|
||||
|
||||
yield cancel(spinner);
|
||||
//yield cancel(spinner);
|
||||
|
||||
if (timeout) {
|
||||
yield put(photosLoadFail("Timeout"));
|
||||
|
||||
Reference in New Issue
Block a user