mirror of
https://github.com/usatiuk/photos.git
synced 2025-10-28 07:27:47 +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,16 +26,33 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#overviewContainer {
|
||||
padding-top: 2rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
#overview {
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
.photosLoader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding-top: 5rem;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
#actionbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list {
|
||||
@@ -74,6 +91,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.bp3-dark {
|
||||
#overview {}
|
||||
}
|
||||
@@ -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,21 +28,20 @@ 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) => (
|
||||
if (
|
||||
props.photos.length === 0 &&
|
||||
!props.triedLoading &&
|
||||
!props.overviewFetching
|
||||
) {
|
||||
props.fetchPhotos();
|
||||
}
|
||||
|
||||
const photos = props.photos.map((photo) => (
|
||||
<PhotoCard
|
||||
key={photo.id}
|
||||
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 (
|
||||
<>
|
||||
<Overlay
|
||||
@@ -65,11 +77,16 @@ export const OverviewComponent: React.FunctionComponent<IOverviewComponentProps>
|
||||
<Photo id={selectedPhoto} />
|
||||
</div>
|
||||
</Overlay>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
@@ -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"));
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
@@ -10,3 +10,5 @@ interface IAPISuccessResponse<T> {
|
||||
}
|
||||
|
||||
export type IAPIResponse<T> = IAPIErrorResponse<T> | IAPISuccessResponse<T>;
|
||||
|
||||
export const IPhotosListPagination = 30;
|
||||
|
||||
Reference in New Issue
Block a user