diff --git a/VocaDbWeb/Scripts/Bootstrap/PageItem.tsx b/VocaDbWeb/Scripts/Bootstrap/PageItem.tsx
index fc64f93246..6455a4f0d3 100644
--- a/VocaDbWeb/Scripts/Bootstrap/PageItem.tsx
+++ b/VocaDbWeb/Scripts/Bootstrap/PageItem.tsx
@@ -33,7 +33,7 @@ const PageItem: BsPrefixRefForwardingComponent<
}: PageItemProps,
ref,
) => {
- const Component = active || disabled ? 'span' : SafeAnchor;
+ const Component = SafeAnchor;
return (
{
+ return (
+
+
+
+ );
+ },
+);
+
+export default EntryCountBox;
diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/EntryCount.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/EntryCount.tsx
new file mode 100644
index 0000000000..bd4be7ad8b
--- /dev/null
+++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/EntryCount.tsx
@@ -0,0 +1,43 @@
+import Dropdown from '@Bootstrap/Dropdown';
+import SafeAnchor from '@Bootstrap/SafeAnchor';
+import ServerSidePagingStore from '@Stores/ServerSidePagingStore';
+import { observer } from 'mobx-react-lite';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+interface EntryCountProps {
+ pagingStore: ServerSidePagingStore;
+ selections?: number[];
+}
+
+const EntryCount = observer(
+ ({
+ pagingStore,
+ selections = [10, 20, 40, 100],
+ }: EntryCountProps): React.ReactElement => {
+ const { t } = useTranslation(['ViewRes.Search']);
+
+ return (
+
+
+ {t('ViewRes.Search:Index.ShowingItemsOf', {
+ 0: pagingStore.pageSize,
+ 1: pagingStore.totalItems,
+ })}
+
+
+ {selections.map((selection) => (
+ pagingStore.setPageSize(selection)}
+ key={selection}
+ >
+ {t('ViewRes.Search:Index.ItemsPerPage', { 0: selection })}
+
+ ))}
+
+
+ );
+ },
+);
+
+export default EntryCount;
diff --git a/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/ServerSidePaging.tsx b/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/ServerSidePaging.tsx
new file mode 100644
index 0000000000..3e57972e55
--- /dev/null
+++ b/VocaDbWeb/Scripts/Components/Shared/Partials/Knockout/ServerSidePaging.tsx
@@ -0,0 +1,61 @@
+import Pagination from '@Bootstrap/Pagination';
+import ServerSidePagingStore from '@Stores/ServerSidePagingStore';
+import { observer } from 'mobx-react-lite';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+interface ServerSidePagingProps {
+ pagingStore: ServerSidePagingStore;
+}
+
+const ServerSidePaging = observer(
+ ({ pagingStore }: ServerSidePagingProps): React.ReactElement => {
+ const { t } = useTranslation(['VocaDb.Web.Resources.Other']);
+
+ return (
+
+
+ «« {t('VocaDb.Web.Resources.Other:PagedList.First')}
+
+
+ « {t('VocaDb.Web.Resources.Other:PagedList.Previous')}
+
+
+ {pagingStore.showMoreBegin && }
+
+ {pagingStore.pages.map((page) => (
+ pagingStore.setPage(page)}
+ key={page}
+ >
+ {page}
+
+ ))}
+
+ {pagingStore.showMoreEnd && }
+
+
+ {t('VocaDb.Web.Resources.Other:PagedList.Next')} »
+
+
+ {t('VocaDb.Web.Resources.Other:PagedList.Last')} »»
+
+
+ );
+ },
+);
+
+export default ServerSidePaging;
diff --git a/VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts b/VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts
new file mode 100644
index 0000000000..7b78a52680
--- /dev/null
+++ b/VocaDbWeb/Scripts/Stores/ServerSidePagingStore.ts
@@ -0,0 +1,87 @@
+import PagingProperties from '@DataContracts/PagingPropertiesContract';
+import _ from 'lodash';
+import { action, computed, makeObservable, observable } from 'mobx';
+
+export default class ServerSidePagingStore {
+ @observable public page = 1;
+ @action public setPage = (value: number): void => {
+ this.page = value;
+ };
+
+ @observable public totalItems = 0;
+ @action public setTotalItems = (value: number): void => {
+ this.totalItems = value;
+ };
+
+ @observable public pageSize = 10;
+ @action public setPageSize = (value: number): void => {
+ this.pageSize = value;
+ };
+
+ public constructor(pageSize: number = 10) {
+ makeObservable(this);
+
+ this.pageSize = pageSize;
+ }
+
+ @computed public get firstItem(): number {
+ return (this.page - 1) * this.pageSize;
+ }
+
+ @computed public get totalPages(): number {
+ return Math.ceil(this.totalItems / this.pageSize);
+ }
+
+ @computed public get hasMultiplePages(): boolean {
+ return this.totalPages > 1;
+ }
+
+ @computed public get isFirstPage(): boolean {
+ return this.page <= 1;
+ }
+
+ @computed public get isLastPage(): boolean {
+ return this.page >= this.totalPages;
+ }
+
+ @computed public get pages(): number[] {
+ const start = Math.max(this.page - 4, 1);
+ const end = Math.min(this.page + 4, this.totalPages);
+
+ return _.range(start, end + 1);
+ }
+
+ @computed public get showMoreBegin(): boolean {
+ return this.page > 5;
+ }
+
+ @computed public get showMoreEnd(): boolean {
+ return this.page < this.totalPages - 4;
+ }
+
+ public getPagingProperties = (
+ clearResults: boolean = false,
+ ): PagingProperties => {
+ return {
+ start: this.firstItem,
+ maxEntries: this.pageSize,
+ getTotalCount: clearResults || this.totalItems === 0,
+ };
+ };
+
+ @action public goToFirstPage = (): void => {
+ this.page = 1;
+ };
+
+ @action public goToLastPage = (): void => {
+ this.page = this.totalPages;
+ };
+
+ @action public nextPage = (): void => {
+ if (!this.isLastPage) this.page = this.page + 1;
+ };
+
+ @action public previousPage = (): void => {
+ if (!this.isFirstPage) this.page = this.page - 1;
+ };
+}
diff --git a/VocaDbWeb/package-lock.json b/VocaDbWeb/package-lock.json
index 9caf957941..012be95985 100644
--- a/VocaDbWeb/package-lock.json
+++ b/VocaDbWeb/package-lock.json
@@ -19,6 +19,8 @@
"knockout-punches": "^0.5.1",
"lodash": "^4.17.19",
"marked": "^0.3.18",
+ "mobx": "^6.3.2",
+ "mobx-react-lite": "^3.2.0",
"moment": "^2.17.0",
"qtip2": "^3.0.3",
"react": "^17.0.2",
@@ -13392,6 +13394,36 @@
"mkdirp": "bin/cmd.js"
}
},
+ "node_modules/mobx": {
+ "version": "6.3.2",
+ "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.3.2.tgz",
+ "integrity": "sha512-xGPM9dIE1qkK9Nrhevp0gzpsmELKU4MFUJRORW/jqxVFIHHWIoQrjDjL8vkwoJYY3C2CeVJqgvl38hgKTalTWg==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mobx"
+ }
+ },
+ "node_modules/mobx-react-lite": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz",
+ "integrity": "sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mobx"
+ },
+ "peerDependencies": {
+ "mobx": "^6.1.0",
+ "react": "^16.8.0 || ^17"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
"node_modules/moment": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.17.0.tgz",
@@ -31244,6 +31276,17 @@
"minimist": "^1.2.5"
}
},
+ "mobx": {
+ "version": "6.3.2",
+ "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.3.2.tgz",
+ "integrity": "sha512-xGPM9dIE1qkK9Nrhevp0gzpsmELKU4MFUJRORW/jqxVFIHHWIoQrjDjL8vkwoJYY3C2CeVJqgvl38hgKTalTWg=="
+ },
+ "mobx-react-lite": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz",
+ "integrity": "sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g==",
+ "requires": {}
+ },
"moment": {
"version": "2.17.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.17.0.tgz",
diff --git a/VocaDbWeb/package.json b/VocaDbWeb/package.json
index 10b025140b..fcf4a65397 100644
--- a/VocaDbWeb/package.json
+++ b/VocaDbWeb/package.json
@@ -64,6 +64,8 @@
"knockout-punches": "^0.5.1",
"lodash": "^4.17.19",
"marked": "^0.3.18",
+ "mobx": "^6.3.2",
+ "mobx-react-lite": "^3.2.0",
"moment": "^2.17.0",
"qtip2": "^3.0.3",
"react": "^17.0.2",
diff --git a/VocaDbWeb/tsconfig.json b/VocaDbWeb/tsconfig.json
index 9248270f41..c9b6efffac 100644
--- a/VocaDbWeb/tsconfig.json
+++ b/VocaDbWeb/tsconfig.json
@@ -20,9 +20,12 @@
"@Models/*": ["Scripts/Models/*"],
"@Repositories/*": ["Scripts/Repositories/*"],
"@Shared/*": ["Scripts/Shared/*"],
+ "@Stores/*": ["Scripts/Stores/*"],
"@ViewModels/*": ["Scripts/ViewModels/*"]
},
- "resolveJsonModule": true
+ "resolveJsonModule": true,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true
},
"include": ["Scripts/**/*"]
}
diff --git a/VocaDbWeb/webpack.mix.js b/VocaDbWeb/webpack.mix.js
index 27f80a6c28..bf3a038deb 100644
--- a/VocaDbWeb/webpack.mix.js
+++ b/VocaDbWeb/webpack.mix.js
@@ -34,6 +34,7 @@ mix
'@Models': path.join(__dirname, 'Scripts/Models'),
'@Repositories': path.join(__dirname, 'Scripts/Repositories'),
'@Shared': path.join(__dirname, 'Scripts/Shared'),
+ '@Stores': path.join(__dirname, 'Scripts/Stores'),
'@ViewModels': path.join(__dirname, 'Scripts/ViewModels'),
})
.eslint({