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