diff --git a/frontend/src/components/pagination.njk b/frontend/src/components/pagination.njk
new file mode 100644
index 0000000..d8d1995
--- /dev/null
+++ b/frontend/src/components/pagination.njk
@@ -0,0 +1,40 @@
+
+
+
+{% includeOnce 'components/button.njk' %}
diff --git a/frontend/src/components/pagination.ts b/frontend/src/components/pagination.ts
new file mode 100644
index 0000000..85aa9fa
--- /dev/null
+++ b/frontend/src/components/pagination.ts
@@ -0,0 +1,83 @@
+import {TesoftComponent} from "../scripts/main.ts";
+import {TesoftButton} from "./button.ts";
+
+export class TesoftPagination extends TesoftComponent {
+ static observedAttributes = ["cur-page", "last-page"];
+
+ private readonly curPageDiv: HTMLDivElement;
+ private readonly prevButton: TesoftButton;
+ private readonly nextButton: TesoftButton;
+
+ constructor() {
+ super();
+
+ const template = document.getElementById("tesoft-pagination-template") as HTMLTemplateElement;
+ const templateContent = template.content;
+
+ const shadowRoot = this.attachShadow({mode: "open"});
+ shadowRoot.appendChild(templateContent.cloneNode(true));
+
+ this.curPageDiv = shadowRoot.querySelector(".cur-page")!;
+ this.prevButton = shadowRoot.querySelector(".prev")!;
+ this.nextButton = shadowRoot.querySelector(".next")!;
+ }
+
+ // noinspection JSUnusedGlobalSymbols
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
+ if (name === "cur-page") {
+ this.update(this.parsePage(newValue), this.parsePage(this.getAttribute("last-page")));
+ } else if (name === "last-page") {
+ this.update(this.parsePage(this.getAttribute("cur-page")), this.parsePage(newValue));
+ }
+ }
+
+ set curPage(value: number) {
+ this.setAttribute("cur-page", value.toString());
+ }
+
+ set lastPage(value: number) {
+ this.setAttribute("last-page", value.toString());
+ }
+
+ private update(curPage: number, lastPage: number) {
+ this.curPageDiv.innerText = curPage.toString();
+
+ if (curPage <= 1) {
+ this.prevButton.setAttribute("disabled", "");
+ } else {
+ this.prevButton.removeAttribute("disabled");
+ }
+ this.prevButton.onclick = () => {
+ this.dispatchEvent(new CustomEvent("update-page", {
+ bubbles: true,
+ cancelable: true,
+ composed: true,
+ detail: {
+ page: curPage - 1,
+ },
+ }));
+ };
+
+ if (curPage >= lastPage) {
+ this.nextButton.setAttribute("disabled", "");
+ } else {
+ this.nextButton.removeAttribute("disabled");
+ }
+ this.nextButton.onclick = () => {
+ this.dispatchEvent(new CustomEvent("updatePage", {
+ bubbles: true,
+ cancelable: true,
+ composed: true,
+ detail: {
+ page: curPage + 1,
+ },
+ }));
+ };
+ }
+
+ private parsePage(str: string | null): number {
+ return str ? parseInt(str) : 1;
+ }
+}
+
+customElements.define("tesoft-pagination", TesoftPagination);