pagination

This commit is contained in:
2020-10-16 18:44:01 +00:00
committed by Stepan Usatiuk
parent 3bbb23dceb
commit 5bfd40426a
12 changed files with 172 additions and 82 deletions

View File

@@ -7,6 +7,7 @@
left: 0; left: 0;
right: 0; right: 0;
overflow-x: hidden; overflow-x: hidden;
overflow: hidden;
} }
.loadingWrapper { .loadingWrapper {

View File

@@ -3,15 +3,15 @@
.viewComponent { .viewComponent {
position: absolute; position: absolute;
max-width: 100%; max-width: 100%;
width: 80%; width: 100%;
left: 0; left: 0;
right: 0; right: 0;
top: -$pt-navbar-height;
bottom: 0; bottom: 0;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding-top: 2 * $pt-navbar-height + 20px; padding-top: $pt-navbar-height;
max-height: 100%; max-height: 100%;
height: 100%;
} }
#mainContainer { #mainContainer {

View File

@@ -26,16 +26,33 @@
opacity: 0; opacity: 0;
} }
#overviewContainer {
padding-top: 2rem;
width: 100%;
height: 100%;
overflow: auto;
#overview { #overview {
width: 80%;
height: 100%;
margin-left: auto;
margin-right: auto;
.photosLoader {
display: flex; display: flex;
align-items: center;
justify-content: center;
flex-direction: column; flex-direction: column;
padding-top: 5rem;
padding-bottom: 5rem;
}
#actionbar { #actionbar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
justify-content: center; justify-content: center;
width: 100%;
} }
.list { .list {
@@ -74,6 +91,8 @@
} }
} }
}
.bp3-dark { .bp3-dark {
#overview {} #overview {}
} }

View File

@@ -7,13 +7,14 @@ import { photosLoadStart } from "~redux/photos/actions";
import { IPhotoReqJSON } from "~../../src/entity/Photo"; import { IPhotoReqJSON } from "~../../src/entity/Photo";
import { LoadingStub } from "~LoadingStub"; import { LoadingStub } from "~LoadingStub";
import { PhotoCard } from "./PhotoCard"; import { PhotoCard } from "./PhotoCard";
import { Button, Classes, Overlay } from "@blueprintjs/core"; import { Button, Classes, Overlay, Spinner } from "@blueprintjs/core";
import { UploadButton } from "./UploadButton"; import { UploadButton } from "./UploadButton";
import { Photo } from "./Photo"; import { Photo } from "./Photo";
export interface IOverviewComponentProps { export interface IOverviewComponentProps {
photos: IPhotoReqJSON[] | null; photos: IPhotoReqJSON[];
overviewLoaded: boolean; triedLoading: boolean;
allPhotosLoaded: boolean;
overviewFetching: boolean; overviewFetching: boolean;
overviewFetchingError: string | null; overviewFetchingError: string | null;
overviewFetchingSpinner: boolean; overviewFetchingSpinner: boolean;
@@ -27,21 +28,20 @@ export const OverviewComponent: React.FunctionComponent<IOverviewComponentProps>
const [selectedPhoto, setSelectedPhoto] = React.useState<number>(0); const [selectedPhoto, setSelectedPhoto] = React.useState<number>(0);
const [isOverlayOpened, setOverlayOpen] = React.useState<boolean>(false); 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) => { const onCardClick = (id: number) => {
setSelectedPhoto(id); setSelectedPhoto(id);
setOverlayOpen(true); setOverlayOpen(true);
}; };
const photos = props.photos if (
.sort((a, b) => b.shotAt - a.shotAt) props.photos.length === 0 &&
.map((photo) => ( !props.triedLoading &&
!props.overviewFetching
) {
props.fetchPhotos();
}
const photos = props.photos.map((photo) => (
<PhotoCard <PhotoCard
key={photo.id} key={photo.id}
photo={photo} photo={photo}
@@ -49,6 +49,18 @@ export const OverviewComponent: React.FunctionComponent<IOverviewComponentProps>
/> />
)); ));
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 ( return (
<> <>
<Overlay <Overlay
@@ -65,11 +77,16 @@ export const OverviewComponent: React.FunctionComponent<IOverviewComponentProps>
<Photo id={selectedPhoto} /> <Photo id={selectedPhoto} />
</div> </div>
</Overlay> </Overlay>
<div id="overviewContainer" onScroll={onLoaderScroll}>
<div id="overview"> <div id="overview">
<div id="actionbar"> <div id="actionbar">
<UploadButton /> <UploadButton />
</div> </div>
<div className="list">{photos}</div> <div className="list">{photos}</div>
<div className="photosLoader">
{props.overviewFetching && <Spinner />}
</div>
</div>
</div> </div>
</> </>
); );
@@ -78,10 +95,11 @@ export const OverviewComponent: React.FunctionComponent<IOverviewComponentProps>
function mapStateToProps(state: IAppState) { function mapStateToProps(state: IAppState) {
return { return {
photos: state.photos.photos, photos: state.photos.photos,
overviewLoaded: state.photos.overviewLoaded, allPhotosLoaded: state.photos.allPhotosLoaded,
overviewFetching: state.photos.overviewFetching, overviewFetching: state.photos.overviewFetching,
overviewFetchingError: state.photos.overviewFetchingError, overviewFetchingError: state.photos.overviewFetchingError,
overviewFetchingSpinner: state.photos.overviewFetchingSpinner, overviewFetchingSpinner: state.photos.overviewFetchingSpinner,
triedLoading: state.photos.triedLoading,
}; };
} }

View File

View File

@@ -19,8 +19,15 @@ export function getPhotoThumbPath(photo: IPhotoReqJSON, size: number): string {
}?size=${size.toString()}`; }?size=${size.toString()}`;
} }
export async function fetchPhotosList(): Promise<IPhotosListRespBody> { export async function fetchPhotosList(
return fetchJSONAuth("/photos/list", "GET"); 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> { export async function fetchPhoto(id: number): Promise<IPhotosByIDGetRespBody> {

View File

@@ -9,12 +9,13 @@ export interface IPhotoState {
} }
export interface IPhotosState { export interface IPhotosState {
photos: IPhotoReqJSON[] | null; photos: IPhotoReqJSON[];
photoStates: Record<number, IPhotoState>; photoStates: Record<number, IPhotoState>;
overviewFetching: boolean; overviewFetching: boolean;
overviewLoaded: boolean; allPhotosLoaded: boolean;
triedLoading: boolean;
overviewFetchingError: string | null; overviewFetchingError: string | null;
overviewFetchingSpinner: boolean; overviewFetchingSpinner: boolean;
@@ -27,9 +28,10 @@ export interface IPhotosState {
} }
const defaultPhotosState: IPhotosState = { const defaultPhotosState: IPhotosState = {
photos: null, photos: [],
overviewLoaded: false, allPhotosLoaded: false,
overviewFetching: false, overviewFetching: false,
triedLoading: false,
overviewFetchingError: null, overviewFetchingError: null,
overviewFetchingSpinner: false, overviewFetchingSpinner: false,
@@ -52,21 +54,31 @@ export const photosReducer: Reducer<IPhotosState, PhotoAction> = (
return defaultPhotosState; return defaultPhotosState;
case PhotoTypes.PHOTOS_LOAD_START: case PhotoTypes.PHOTOS_LOAD_START:
return { return {
...defaultPhotosState, ...state,
overviewFetching: true, overviewFetching: true,
triedLoading: true,
overviewFetchingSpinner: false, overviewFetchingSpinner: false,
}; };
case PhotoTypes.PHOTOS_START_FETCHING_SPINNER: case PhotoTypes.PHOTOS_START_FETCHING_SPINNER:
return { ...state, overviewFetchingSpinner: true }; 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 { return {
...defaultPhotosState, ...state,
photos: action.photos, photos: [...oldPhotos, ...action.photos],
overviewLoaded: true, triedLoading: true,
allPhotosLoaded,
overviewFetching: false,
}; };
}
case PhotoTypes.PHOTOS_LOAD_FAIL: case PhotoTypes.PHOTOS_LOAD_FAIL:
return { return {
...defaultPhotosState, ...defaultPhotosState,
triedLoading: true,
overviewFetchingError: action.error, overviewFetchingError: action.error,
}; };

View File

@@ -42,6 +42,7 @@ import {
photoUploadSuccess, photoUploadSuccess,
} from "./actions"; } from "./actions";
import { IPhotosNewRespBody } from "~../../src/routes/photos"; 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 // Thanks, https://dev.to/qortex/compute-md5-checksum-for-a-file-in-typescript-59a4
function computeChecksumMd5(file: File): Promise<string> { function computeChecksumMd5(file: File): Promise<string> {
@@ -108,21 +109,24 @@ function computeSize(f: File) {
}); });
} }
// Shouldn't be used anymore
function* startSpinner() { function* startSpinner() {
yield delay(300); yield delay(300);
yield put(photosStartFetchingSpinner()); yield put(photosStartFetchingSpinner());
} }
function* photosLoad() { function* photosLoad() {
const state = yield select();
try { 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({ const { response, timeout } = yield race({
response: call(fetchPhotosList), response: call(fetchPhotosList, skip, IPhotosListPagination),
timeout: delay(10000), timeout: delay(10000),
}); });
yield cancel(spinner); //yield cancel(spinner);
if (timeout) { if (timeout) {
yield put(photosLoadFail("Timeout")); yield put(photosLoadFail("Timeout"));

View File

@@ -1,7 +1,7 @@
import * as Router from "@koa/router"; import * as Router from "@koa/router";
import { IPhotoReqJSON, Photo } from "~entity/Photo"; import { IPhotoReqJSON, Photo } from "~entity/Photo";
import { User } from "~entity/User"; import { User } from "~entity/User";
import { IAPIResponse } from "~types"; import { IAPIResponse, IPhotosListPagination } from "~types";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import send = require("koa-send"); import send = require("koa-send");
import { getHash, getSize } from "~util"; import { getHash, getSize } from "~util";
@@ -171,7 +171,29 @@ photosRouter.get("/photos/list", async (ctx) => {
const { user } = ctx.state; const { user } = ctx.state;
const photos = await Photo.find({ user }); let { skip, num } = ctx.request.query as {
skip: string | number | undefined;
num: string | number | undefined;
};
if (typeof num === "string") {
num = parseInt(num);
}
if (typeof skip === "string") {
skip = parseInt(skip);
}
if (!num || num > IPhotosListPagination) {
num = IPhotosListPagination;
}
const photos = await Photo.find({
where: { user },
take: num,
skip: skip,
order: { shotAt: "DESC" },
});
const photosList: IPhotoReqJSON[] = await Promise.all( const photosList: IPhotoReqJSON[] = await Promise.all(
photos.map(async (photo) => await photo.toReqJSON()), photos.map(async (photo) => await photo.toReqJSON()),

View File

@@ -571,7 +571,7 @@ describe("photos", function () {
}); });
*/ */
it("should list photos", async function () { it("should list photos, sorted", async function () {
const response = await request(callback) const response = await request(callback)
.get("/photos/list") .get("/photos/list")
.set({ .set({
@@ -582,13 +582,18 @@ describe("photos", function () {
expect(response.body.error).to.be.false; expect(response.body.error).to.be.false;
const photos = response.body.data as IPhotoReqJSON[]; const photos = response.body.data as IPhotoReqJSON[];
const userPhotos = [ const userPhotos = [
await seed.dogPhoto.toReqJSON(), await seed.dogPhoto.toReqJSON(),
await seed.catPhoto.toReqJSON(), await seed.catPhoto.toReqJSON(),
]; ].sort((a, b) => b.shotAt - a.shotAt);
const photoIds = photos.map((p) => p.id);
const userPhotoIds = userPhotos.map((p) => p.id);
expect(photos).to.deep.equal(userPhotos); expect(photos).to.deep.equal(userPhotos);
expect(photoIds).to.have.ordered.members(userPhotoIds);
//TODO: Test pagination
}); });
/* /*

View File

@@ -10,3 +10,5 @@ interface IAPISuccessResponse<T> {
} }
export type IAPIResponse<T> = IAPIErrorResponse<T> | IAPISuccessResponse<T>; export type IAPIResponse<T> = IAPIErrorResponse<T> | IAPISuccessResponse<T>;
export const IPhotosListPagination = 30;