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;
right: 0;
overflow-x: hidden;
overflow: hidden;
}
.loadingWrapper {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,
};
}

View File

View 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> {

View File

@@ -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,
};

View File

@@ -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"));

View File

@@ -1,7 +1,7 @@
import * as Router from "@koa/router";
import { IPhotoReqJSON, Photo } from "~entity/Photo";
import { User } from "~entity/User";
import { IAPIResponse } from "~types";
import { IAPIResponse, IPhotosListPagination } from "~types";
import * as fs from "fs/promises";
import send = require("koa-send");
import { getHash, getSize } from "~util";
@@ -171,7 +171,29 @@ photosRouter.get("/photos/list", async (ctx) => {
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(
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)
.get("/photos/list")
.set({
@@ -582,13 +582,18 @@ describe("photos", function () {
expect(response.body.error).to.be.false;
const photos = response.body.data as IPhotoReqJSON[];
const userPhotos = [
await seed.dogPhoto.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(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 const IPhotosListPagination = 30;