From 27d17874f46c0a956f8dcd4705f438ed51149561 Mon Sep 17 00:00:00 2001 From: Michal Vanko Date: Sun, 3 Apr 2022 20:14:08 +0200 Subject: [PATCH] Style paginator component --- _pages/portfolio.md | 10 ++-- package-lock.json | 43 ++++++++------- package.json | 4 +- src/components/blog/ArticleFooter.svelte | 2 +- src/components/paginator/Paginator.css.ts | 21 ++++++++ src/components/paginator/Paginator.svelte | 52 ++++++++++++++++++ .../paginator/paginatorUtils.test.ts | 52 ++++++++++++++++++ src/components/paginator/paginatorUtils.ts | 50 +++++++++++++++++ src/lib/pagination/pagination.ts | 8 +-- src/lib/pagination/searchParams.test.ts | 27 +++++++--- src/lib/pagination/searchParams.ts | 20 +++++-- src/routes/blog/[...params].svelte | 54 ++++++++++++++++--- src/routes/feed/_feed.ts | 4 +- svelte.config.js | 1 + vitest.config.js | 3 ++ 15 files changed, 299 insertions(+), 52 deletions(-) create mode 100644 src/components/paginator/Paginator.css.ts create mode 100644 src/components/paginator/Paginator.svelte create mode 100644 src/components/paginator/paginatorUtils.test.ts create mode 100644 src/components/paginator/paginatorUtils.ts create mode 100644 vitest.config.js diff --git a/_pages/portfolio.md b/_pages/portfolio.md index e6d2d44..b633151 100644 --- a/_pages/portfolio.md +++ b/_pages/portfolio.md @@ -90,7 +90,7 @@ work_history: displayed: true address: name: eSOLUTIONS s.r.o. - location: Hroncová 2 + location: Gorkého 8 zipcode: 040 01 city: Košice country: Slovakia @@ -151,7 +151,7 @@ projects: This project is built with modern web technologies including: **CycleJS**, **Reactive Streams**, **D3**, **Jest**, **Webpack**. displayed: true image: - image_description: " responzIO main controller" + image_description: ' responzIO main controller' source: /images/uploads/responzio.png name: responzIO - description: >- @@ -300,14 +300,16 @@ presentations: affect the future of the world. - displayed: true name: Spreading the web - description: A presentation about the rising number of use cases for utilizing + description: + A presentation about the rising number of use cases for utilizing web technologies outside of the web platform such as native mobile applications and robotics. link: https://michalvankodev.github.io/spreading-the-web/#/ - displayed: true name: Docker link: http://michalvankodev.github.io/dockerpresentation/#/ - description: An introduction to Docker containerization technology and how it + description: + An introduction to Docker containerization technology and how it differs from virtualization. education: - description: |- diff --git a/package-lock.json b/package-lock.json index fdf0596..217c488 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "name": "michalvankodev", "version": "0.0.1", "dependencies": { - "@mobily/ts-belt": "^3.10.0", "@vanilla-extract/css": "^1.6.8", "@vanilla-extract/sprinkles": "^1.4.0", "@vanilla-extract/vite-plugin": "^3.1.4", @@ -42,7 +41,8 @@ "tslib": "^2.3.1", "typescript": "^4.6.2", "vite": "^2.8.6", - "vitest": "^0.7.10" + "vitest": "^0.7.13", + "vitest-svelte-kit": "^0.0.6" } }, "node_modules/@babel/code-frame": { @@ -447,14 +447,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "node_modules/@mobily/ts-belt": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@mobily/ts-belt/-/ts-belt-3.10.0.tgz", - "integrity": "sha512-F3XLU3zMDzJOf9KlKgnNOz5rdAtMG/UBxEDU4UNA4ewKFRd5DsbIIJmeAifLudNwcXmoIgtZ39KwVjPaL/CjgA==", - "engines": { - "node": ">= 10.*" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4025,9 +4017,9 @@ } }, "node_modules/vitest": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.7.10.tgz", - "integrity": "sha512-We5a7cnY2aUpX4tAO+w2KRhJiJ4FznfWjYKkqWoAqs4x4pKgyRsMJNZ7OSY/lFHOoRz3yv0mgwfVlZiRc0/mmA==", + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.7.13.tgz", + "integrity": "sha512-UCHeJEOK+qCBa/e4UtkCfv0wIZ125T4Nf2R0J/46v/Wnv6bt9zGfAyKAI6siYFhvLvg20MgDIreROtVgedHFWw==", "dev": true, "dependencies": { "@types/chai": "^4.3.0", @@ -4042,7 +4034,7 @@ "vitest": "vitest.mjs" }, "engines": { - "node": ">=v14.16.0" + "node": ">=v14.19.1" }, "funding": { "url": "https://github.com/sponsors/antfu" @@ -4068,6 +4060,12 @@ } } }, + "node_modules/vitest-svelte-kit": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/vitest-svelte-kit/-/vitest-svelte-kit-0.0.6.tgz", + "integrity": "sha512-bQ1GcCAk600YV1xOiJBhltGE/HO/j6FozNY2BFq2GP1mHh3pj0KrGZlyx0kVlXx+BSKDXQHuYZtwlHwNlvv0fQ==", + "dev": true + }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -4498,11 +4496,6 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, - "@mobily/ts-belt": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@mobily/ts-belt/-/ts-belt-3.10.0.tgz", - "integrity": "sha512-F3XLU3zMDzJOf9KlKgnNOz5rdAtMG/UBxEDU4UNA4ewKFRd5DsbIIJmeAifLudNwcXmoIgtZ39KwVjPaL/CjgA==" - }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6943,9 +6936,9 @@ } }, "vitest": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.7.10.tgz", - "integrity": "sha512-We5a7cnY2aUpX4tAO+w2KRhJiJ4FznfWjYKkqWoAqs4x4pKgyRsMJNZ7OSY/lFHOoRz3yv0mgwfVlZiRc0/mmA==", + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.7.13.tgz", + "integrity": "sha512-UCHeJEOK+qCBa/e4UtkCfv0wIZ125T4Nf2R0J/46v/Wnv6bt9zGfAyKAI6siYFhvLvg20MgDIreROtVgedHFWw==", "dev": true, "requires": { "@types/chai": "^4.3.0", @@ -6957,6 +6950,12 @@ "vite": "^2.8.6" } }, + "vitest-svelte-kit": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/vitest-svelte-kit/-/vitest-svelte-kit-0.0.6.tgz", + "integrity": "sha512-bQ1GcCAk600YV1xOiJBhltGE/HO/j6FozNY2BFq2GP1mHh3pj0KrGZlyx0kVlXx+BSKDXQHuYZtwlHwNlvv0fQ==", + "dev": true + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", diff --git a/package.json b/package.json index a858cf3..f145de6 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "svgstore": "svgstore -o static/build/icons-sprite.svg src/svg/**.svg" }, "dependencies": { - "@mobily/ts-belt": "^3.10.0", "@vanilla-extract/css": "^1.6.8", "@vanilla-extract/sprinkles": "^1.4.0", "@vanilla-extract/vite-plugin": "^3.1.4", @@ -50,6 +49,7 @@ "tslib": "^2.3.1", "typescript": "^4.6.2", "vite": "^2.8.6", - "vitest": "^0.7.10" + "vitest": "^0.7.13", + "vitest-svelte-kit": "^0.0.6" } } diff --git a/src/components/blog/ArticleFooter.svelte b/src/components/blog/ArticleFooter.svelte index d4d4950..bf21c1e 100644 --- a/src/components/blog/ArticleFooter.svelte +++ b/src/components/blog/ArticleFooter.svelte @@ -21,7 +21,7 @@ diff --git a/src/components/paginator/Paginator.css.ts b/src/components/paginator/Paginator.css.ts new file mode 100644 index 0000000..398b93f --- /dev/null +++ b/src/components/paginator/Paginator.css.ts @@ -0,0 +1,21 @@ +import { sprinkles } from '$lib/styles/sprinkles.css' + +export const listClass = sprinkles({ + listStyle: 'none', + display: 'flex', + justifyContent: 'center', +}) + +export const listItemClass = sprinkles({ + paddingX: '1x', +}) + +export const activePage = sprinkles({ + //fontStyle: 'italic', + fontWeight: 'bold', + paddingX: '2x', +}) + +export const pageLinkClass = sprinkles({ + paddingX: '1x', +}) diff --git a/src/components/paginator/Paginator.svelte b/src/components/paginator/Paginator.svelte new file mode 100644 index 0000000..485130c --- /dev/null +++ b/src/components/paginator/Paginator.svelte @@ -0,0 +1,52 @@ + + + diff --git a/src/components/paginator/paginatorUtils.test.ts b/src/components/paginator/paginatorUtils.test.ts new file mode 100644 index 0000000..0a38cd0 --- /dev/null +++ b/src/components/paginator/paginatorUtils.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from 'vitest' +import { Divider, getPaginatorPages } from './paginatorUtils' + +describe('Paginator component', () => { + describe('Paginator generates feasable pages to display', () => { + test('Page: 1/5', () => { + expect( + getPaginatorPages({ page: 1, totalCount: 5, pageSize: 1 }) + ).toEqual([1, 2, 3, 4, 5]) + }) + test('Page: 4/7', () => { + expect( + getPaginatorPages({ page: 4, totalCount: 7, pageSize: 1 }) + ).toEqual([1, 2, 3, 4, 5, 6, 7]) + }) + test('Page: 4/8', () => { + expect( + getPaginatorPages({ page: 4, totalCount: 8, pageSize: 1 }) + ).toEqual([1, 2, 3, 4, 5, 6, Divider, 8]) + }) + test('Page: 1/10', () => { + expect( + getPaginatorPages({ page: 1, totalCount: 10, pageSize: 1 }) + ).toEqual([1, 2, 3, 4, 5, 6, Divider, 10]) + }) + test('Page: 2/10', () => { + expect( + getPaginatorPages({ page: 2, totalCount: 10, pageSize: 1 }) + ).toEqual([1, 2, 3, 4, 5, 6, Divider, 10]) + }) + test('Page: 5/10', () => { + expect( + getPaginatorPages({ page: 5, totalCount: 10, pageSize: 1 }) + ).toEqual([1, Divider, 3, 4, 5, 6, 7, Divider, 10]) + }) + test('Page: 7/10', () => { + expect( + getPaginatorPages({ page: 7, totalCount: 10, pageSize: 1 }) + ).toEqual([1, Divider, 5, 6, 7, 8, 9, 10]) + }) + test('Page: 8/10', () => { + expect( + getPaginatorPages({ page: 8, totalCount: 10, pageSize: 1 }) + ).toEqual([1, Divider, 5, 6, 7, 8, 9, 10]) + }) + test('Page: 10/10', () => { + expect( + getPaginatorPages({ page: 10, totalCount: 10, pageSize: 1 }) + ).toEqual([1, Divider, 5, 6, 7, 8, 9, 10]) + }) + }) +}) diff --git a/src/components/paginator/paginatorUtils.ts b/src/components/paginator/paginatorUtils.ts new file mode 100644 index 0000000..c35b5f5 --- /dev/null +++ b/src/components/paginator/paginatorUtils.ts @@ -0,0 +1,50 @@ +import { toParams } from '$lib/pagination/searchParams' +import { last, range } from 'ramda' + +export const Divider = 'divider' + +export function getPaginatorPages({ + page, + pageSize, + totalCount, +}: { + page: number + pageSize: number + totalCount: number +}) { + const maxLinksLength = 7 + const linksAroundActive = 2 + const totalPages = Math.ceil(totalCount / pageSize) + const daco = range(1, totalPages + 1).reduce((acc, link) => { + const isFirst = link === 1 + const isLast = link === totalPages + const isPageOnStart = page <= 3 && link < maxLinksLength + const isPageOnEnd = + page >= totalPages - 3 && link > totalPages - maxLinksLength + 1 + + if ([isFirst, isLast, isPageOnStart, isPageOnEnd].some((value) => value)) { + return [...acc, link] + } + + if (link < page - linksAroundActive || link > page + linksAroundActive) { + if (last(acc) === Divider) { + return acc + } + return [...acc, Divider] + } + + return [...acc, link] + }, []) + + return daco +} + +export function createHref( + href: string, + filters: Record, + pageNumber: number +) { + const filtersPath = toParams(filters) + console.log(filtersPath, filters) + return `/${href}/${filtersPath ? filtersPath + '/' : ''}page/${pageNumber}` +} diff --git a/src/lib/pagination/pagination.ts b/src/lib/pagination/pagination.ts index 3dabaf0..64e2a72 100644 --- a/src/lib/pagination/pagination.ts +++ b/src/lib/pagination/pagination.ts @@ -1,6 +1,4 @@ -import { identity } from 'ramda' -import { flow, A } from '@mobily/ts-belt' -const { drop, take } = A +import { identity, drop, take, pipe } from 'ramda' export interface PaginationQuery { offset?: number @@ -14,7 +12,9 @@ export interface PaginationResult { } export function dropAndTake({ offset = 0, limit = Infinity }) { - return flow(drop(offset), take(limit)) + return pipe(drop(offset), take(limit)) as ( + items: Item[] + ) => Item[] } export function filterByPropContains(filters: Record) { diff --git a/src/lib/pagination/searchParams.test.ts b/src/lib/pagination/searchParams.test.ts index 8928369..721ff1c 100644 --- a/src/lib/pagination/searchParams.test.ts +++ b/src/lib/pagination/searchParams.test.ts @@ -42,22 +42,37 @@ describe('get search params', () => { }) test('should parse values into searchParams for first page', () => { - const params = 'tags/News/page/1' - expect(getPaginationSearchParams(7, params).toString()).toEqual( + const params = { + pageSize: 7, + page: 1, + filters: { + tags: 'News', + }, + } + expect(getPaginationSearchParams(params).toString()).toEqual( 'limit=7&offset=0&tags=News' ) }) test('should parse values into searchParams for third page', () => { - const params = 'tags/News/page/3' - expect(getPaginationSearchParams(7, params).toString()).toEqual( + const params = { + pageSize: 7, + page: 3, + filters: { + tags: 'News', + }, + } + expect(getPaginationSearchParams(params).toString()).toEqual( 'limit=7&offset=14&tags=News' ) }) test('should return first page without any params specified', () => { - const params = '' - expect(getPaginationSearchParams(7, params).toString()).toEqual( + const params = { + pageSize: 7, + page: 1, + } + expect(getPaginationSearchParams(params).toString()).toEqual( 'limit=7&offset=0' ) }) diff --git a/src/lib/pagination/searchParams.ts b/src/lib/pagination/searchParams.ts index 1cc4a51..a8e89fa 100644 --- a/src/lib/pagination/searchParams.ts +++ b/src/lib/pagination/searchParams.ts @@ -34,12 +34,26 @@ export function parseParams(params: string) { return Object.fromEntries(splits) } +export function toParams(records: Record) { + return Object.entries(records) + .map(([key, value]) => `${key}/${value}`) + .join('/') +} + +export interface PaginationSearchParams { + pageSize: number + page: number + filters?: Record +} + /** * Convert svelte `load` params into a `URLSearchParams` so they can be used to fetch endpoints with pagination queries */ -export function getPaginationSearchParams(pageSize: number, params: string) { - const { page = 1, ...filters } = parseParams(params) - +export function getPaginationSearchParams({ + pageSize, + page, + filters, +}: PaginationSearchParams) { const offset = pageSize * (page - 1) const limit = pageSize return new URLSearchParams({ limit, offset, ...filters }) diff --git a/src/routes/blog/[...params].svelte b/src/routes/blog/[...params].svelte index b089d14..7fd6928 100644 --- a/src/routes/blog/[...params].svelte +++ b/src/routes/blog/[...params].svelte @@ -1,28 +1,47 @@ @@ -33,18 +52,28 @@

You've found void in the space.

{:else}

- Recent - {#if tagQuery} - {tagQuery} + {#if filters.tags} + {filters.tags} + {:else} + Blog {/if} posts

- {#if tagQuery} + {#if filters.tags} {/if} {/if} +
+ +
    {#each posts.items as post}
  • @@ -60,3 +89,12 @@
  • {/each}
+ diff --git a/src/routes/feed/_feed.ts b/src/routes/feed/_feed.ts index 737f045..37dc423 100644 --- a/src/routes/feed/_feed.ts +++ b/src/routes/feed/_feed.ts @@ -24,8 +24,8 @@ export async function getFeed() { }, }) - const blogListing = await getBlogListing() - blogListing.forEach((post) => { + const blogListing = await getBlogListing({}) + blogListing.items.forEach((post) => { feed.addItem({ title: post.title, id: `https://michalvanko.dev/blog/${post.slug}`, diff --git a/svelte.config.js b/svelte.config.js index 9bd3f3c..5546daf 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -10,6 +10,7 @@ const config = { kit: { adapter: adapterStatic(), vite: { plugins: [vanillaExtractPlugin()] }, + prerender: { default: true } }, preprocess: preprocess({ sourceMap: dev, diff --git a/vitest.config.js b/vitest.config.js new file mode 100644 index 0000000..a24f56c --- /dev/null +++ b/vitest.config.js @@ -0,0 +1,3 @@ +import { extractFromSvelteConfig } from "vitest-svelte-kit" + +export default extractFromSvelteConfig()