feat(Components): Add pagination component
This component accept a Page<T> type from [astro](https://docs.astro.build/en/guides/routing/#complete-api-reference)
This commit is contained in:
parent
642e15656e
commit
944b553e73
9 changed files with 258 additions and 83 deletions
|
|
@ -8,4 +8,7 @@ export default defineConfig({
|
||||||
prefetch: true,
|
prefetch: true,
|
||||||
integrations: [react(), astroI18next()],
|
integrations: [react(), astroI18next()],
|
||||||
experimental: {},
|
experimental: {},
|
||||||
|
redirects: {
|
||||||
|
'/projects': '/projects/1',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ $border-radius: 0.5rem;
|
||||||
--prj-primary: #{getColor('teal')};
|
--prj-primary: #{getColor('teal')};
|
||||||
--prj-primary-text: #{getColor('base')};
|
--prj-primary-text: #{getColor('base')};
|
||||||
|
|
||||||
|
--prj-secondary: #{getColor('mauve')};
|
||||||
|
--prj-secondary-text: #{getColor('base')};
|
||||||
|
|
||||||
--prj-danger: #{getColor('red')};
|
--prj-danger: #{getColor('red')};
|
||||||
--prj-danger-text: #{getColor('base')};
|
--prj-danger-text: #{getColor('base')};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ a {
|
||||||
transition: text-shadow 0.2s;
|
transition: text-shadow 0.2s;
|
||||||
--anim-shadow-color: var(--prj-accent-bg);
|
--anim-shadow-color: var(--prj-accent-bg);
|
||||||
|
|
||||||
&:hover {
|
&:not(.clean):hover {
|
||||||
text-shadow: 1px 1px 8px var(--anim-shadow-color);
|
text-shadow: 1px 1px 8px var(--anim-shadow-color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,36 @@
|
||||||
---
|
---
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
href?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { className = '' } = Astro.props;
|
const { className = '', href } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<button class:list={className}>
|
{
|
||||||
<slot />
|
href !== undefined ? (
|
||||||
</button>
|
<a href={href} class:list={['clean', 'btn', className]}>
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<button class:list={className}>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
button {
|
button,
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: var(--prj-spacing-1) var(--prj-spacing-3);
|
padding: var(--prj-spacing-1) var(--prj-spacing-3);
|
||||||
background-color: var(--prj-accent-bg);
|
background-color: var(--prj-accent-bg);
|
||||||
color: var(--prj-accent-text);
|
color: var(--prj-accent-text);
|
||||||
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border-color: var(--prj-accent-bg);
|
border: 1px solid var(--prj-accent-bg);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,25 +52,25 @@ const links = [
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const normilizeUrl = (url: string): string => {
|
|
||||||
let newUrl = `${url}`;
|
|
||||||
|
|
||||||
if (newUrl.endsWith('/')) {
|
|
||||||
newUrl = newUrl.slice(0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setActiveLink = () => {
|
const setActiveLink = () => {
|
||||||
const links =
|
const links =
|
||||||
document.querySelectorAll<HTMLAnchorElement>(`#main-navbar a`);
|
document.querySelectorAll<HTMLAnchorElement>(`#main-navbar a`);
|
||||||
|
|
||||||
links.forEach((link) =>
|
links.forEach((link) => {
|
||||||
normilizeUrl(link.pathname) === normilizeUrl(location.pathname)
|
if (link.pathname === '/' && location.pathname === '/') {
|
||||||
|
link.classList.add('active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (link.pathname === '/' && location.pathname !== '/') {
|
||||||
|
link.classList.remove('active');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
location.pathname.startsWith(link.pathname)
|
||||||
? link.classList.add('active')
|
? link.classList.add('active')
|
||||||
: link.classList.remove('active'),
|
: link.classList.remove('active');
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
// Add active class to the current link
|
// Add active class to the current link
|
||||||
document.addEventListener('astro:page-load', setActiveLink, { once: true });
|
document.addEventListener('astro:page-load', setActiveLink, { once: true });
|
||||||
|
|
|
||||||
133
src/components/Pagination.astro
Normal file
133
src/components/Pagination.astro
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
---
|
||||||
|
import { type Page } from 'astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
page: Page;
|
||||||
|
paginationOffset?: number;
|
||||||
|
urlPattern: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { page, urlPattern, paginationOffset = 3 } = Astro.props;
|
||||||
|
|
||||||
|
const pages = [];
|
||||||
|
|
||||||
|
const lowerEnd = Math.max(page.currentPage - paginationOffset, 1);
|
||||||
|
const highEnd = Math.min(page.currentPage + paginationOffset, page.lastPage);
|
||||||
|
|
||||||
|
const generateUrl = (index: number) => {
|
||||||
|
return urlPattern.replace('{}', index.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let index = lowerEnd; index <= highEnd; index++) {
|
||||||
|
pages.push({
|
||||||
|
index,
|
||||||
|
url: generateUrl(index),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<nav role="navigation" aria-label="Pagination" class="w-100 my-4">
|
||||||
|
<ul class="list-unstyle hstack justify-content-center">
|
||||||
|
{
|
||||||
|
page.url.prev !== undefined && (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={page.url.prev}
|
||||||
|
class="prev-page"
|
||||||
|
aria-label="Go to previous page"
|
||||||
|
>
|
||||||
|
« Prev
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}{
|
||||||
|
lowerEnd !== 1 && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={generateUrl(1)}
|
||||||
|
class="prev-page"
|
||||||
|
aria-label={`Go to page 1`}
|
||||||
|
>
|
||||||
|
1
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span class="start-ellipsis">…</span>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
pages.map((item) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class:list={[{ current: item.index === page.currentPage }]}
|
||||||
|
href={item.url}
|
||||||
|
aria-label={`Go to page ${item.index}`}
|
||||||
|
>
|
||||||
|
{item.index}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
highEnd !== page.lastPage && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<span class="start-ellipsis">…</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={generateUrl(page.lastPage)}
|
||||||
|
class="next-page"
|
||||||
|
aria-label={`Go to page ${page.lastPage}`}
|
||||||
|
>
|
||||||
|
{page.lastPage}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
page.url.next !== undefined && (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={page.url.next}
|
||||||
|
class="next-page"
|
||||||
|
aria-label="Go to next page"
|
||||||
|
>
|
||||||
|
Next »
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
li {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
border: 1px solid var(--prj-link-text);
|
||||||
|
padding: var(--prj-spacing-1) var(--prj-spacing-2);
|
||||||
|
border-radius: var(--prj-border-radius);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background-color 400ms, color 400ms;
|
||||||
|
|
||||||
|
&.current {
|
||||||
|
background-color: var(--prj-secondary);
|
||||||
|
border: 1px solid var(--prj-secondary);
|
||||||
|
color: var(--prj-secondary-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--prj-link-text);
|
||||||
|
border: 1px solid var(--prj-link-text);
|
||||||
|
color: var(--prj-accent-text);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
---
|
---
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from 'astro:content';
|
||||||
import { t, changeLanguage } from "i18next";
|
import { t, changeLanguage } from 'i18next';
|
||||||
import Layout from "../layouts/Layout.astro";
|
import Layout from '../layouts/Layout.astro';
|
||||||
import Card from "../components/Card.astro";
|
import Card from '../components/Card.astro';
|
||||||
import Button from "../components/Button/Button.astro";
|
import Button from '../components/Button/Button.astro';
|
||||||
import { Image } from "astro:assets";
|
import { Image } from 'astro:assets';
|
||||||
import portrait from "../assets/images/portrait.jpg";
|
import portrait from '../assets/images/portrait.jpg';
|
||||||
|
|
||||||
changeLanguage("en");
|
changeLanguage('en');
|
||||||
|
|
||||||
const blog = await getCollection("blog", ({ data }) => import.meta.env.PROD ? data.draft !== true : true);
|
const blog = await getCollection('blog', ({ data }) =>
|
||||||
|
import.meta.env.PROD ? data.draft !== true : true,
|
||||||
|
);
|
||||||
// TODO: show the pinned ones, not the recents
|
// TODO: show the pinned ones, not the recents
|
||||||
const portafolio = await getCollection("portafolio", ({ data }) => import.meta.env.PROD ? data.draft !== true : true);
|
const portafolio = await getCollection('portafolio', ({ data }) =>
|
||||||
|
import.meta.env.PROD ? data.draft !== true : true,
|
||||||
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="aleidk">
|
<Layout title="aleidk">
|
||||||
|
|
@ -49,8 +53,8 @@ const portafolio = await getCollection("portafolio", ({ data }) => import.meta.e
|
||||||
/>
|
/>
|
||||||
<h3 class="fs-4 text-center my-1">Project N°1</h3>
|
<h3 class="fs-4 text-center my-1">Project N°1</h3>
|
||||||
<p class="text-justify">
|
<p class="text-justify">
|
||||||
cillum sint consectetur cupidatat.
|
cillum sint consectetur cupidatat. Lorem ipsum dolor sit amet, qui
|
||||||
Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint
|
minim labore adipisicing minim sint
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
|
|
@ -94,7 +98,7 @@ const portafolio = await getCollection("portafolio", ({ data }) => import.meta.e
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 text-center">
|
<div class="mt-4 text-center">
|
||||||
<Button className="px-4 py-2 fs-5 ">View Work</Button>
|
<Button className="px-4 py-2 fs-5" href="/projects">View Work</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
|
||||||
69
src/pages/projects/[page].astro
Normal file
69
src/pages/projects/[page].astro
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
---
|
||||||
|
import type { InferGetStaticPropsType, GetStaticPaths } from 'astro';
|
||||||
|
import { changeLanguage } from 'i18next';
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import Layout from '@layouts/Layout.astro';
|
||||||
|
import Card from '@components/Card.astro';
|
||||||
|
import Pagination from '@components/Pagination.astro';
|
||||||
|
|
||||||
|
changeLanguage('en');
|
||||||
|
|
||||||
|
export const getStaticPaths = (async ({ paginate }) => {
|
||||||
|
const rawEntries = await getCollection('portafolio', ({ data }) => {
|
||||||
|
return import.meta.env.PROD ? data.draft !== true : true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const entries = rawEntries.map((item, idx) => ({
|
||||||
|
...item.data,
|
||||||
|
id: idx + 1,
|
||||||
|
slug: item.slug,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return paginate(entries, { pageSize: 6 });
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||||
|
|
||||||
|
const { page } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title="List of blog entries">
|
||||||
|
<h1 class="text-center">Projects</h1>
|
||||||
|
|
||||||
|
<section class="clean">
|
||||||
|
<div class="grid grid-cols-1 grid-lg-cols-3 gap-4">
|
||||||
|
{
|
||||||
|
page.data.map((item) => (
|
||||||
|
<div>
|
||||||
|
<Card className="anim-hover-zoom">
|
||||||
|
<a class="clean" href={`/projects/${item.slug}`}>
|
||||||
|
<img
|
||||||
|
src="https://placehold.co/600x400"
|
||||||
|
alt="project img"
|
||||||
|
class="border-radius respect-width"
|
||||||
|
slot="img-header"
|
||||||
|
/>
|
||||||
|
<h3 class="fs-4 text-center my-1">Project N°1</h3>
|
||||||
|
<p class="text-justify">
|
||||||
|
cillum sint consectetur cupidatat. Lorem ipsum dolor sit amet,
|
||||||
|
qui minim labore adipisicing minim sint
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="text-end">
|
||||||
|
<a href={`/projects/${item.slug}`}>See more...</a>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<Pagination page={page} urlPattern="/projects/{}" />
|
||||||
|
</section>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
a.clean:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
---
|
|
||||||
import { changeLanguage } from 'i18next';
|
|
||||||
import { getCollection } from 'astro:content';
|
|
||||||
import Layout from '@layouts/Layout.astro';
|
|
||||||
import Card from '@components/Card.astro';
|
|
||||||
|
|
||||||
changeLanguage('en');
|
|
||||||
|
|
||||||
const rawEntries = await getCollection('portafolio', ({ data }) => {
|
|
||||||
return import.meta.env.PROD ? data.draft !== true : true;
|
|
||||||
});
|
|
||||||
const entries = rawEntries.map((item, idx) => ({
|
|
||||||
...item.data,
|
|
||||||
id: idx + 1,
|
|
||||||
slug: item.slug,
|
|
||||||
}));
|
|
||||||
|
|
||||||
console.log(entries.at(0));
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title="List of blog entries">
|
|
||||||
<h1 class="text-center">Projects</h1>
|
|
||||||
|
|
||||||
<section class="clean grid grid-cols-1 grid-lg-cols-3">
|
|
||||||
{
|
|
||||||
entries.map((item) => (
|
|
||||||
<div>
|
|
||||||
<Card className="anim-hover-zoom">
|
|
||||||
<img
|
|
||||||
src="https://placehold.co/600x400"
|
|
||||||
alt="project img"
|
|
||||||
class="border-radius respect-width"
|
|
||||||
slot="img-header"
|
|
||||||
/>
|
|
||||||
<h3 class="fs-4 text-center my-1">Project N°1</h3>
|
|
||||||
<p class="text-justify">
|
|
||||||
cillum sint consectetur cupidatat. Lorem ipsum dolor sit amet, qui
|
|
||||||
minim labore adipisicing minim sint
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="text-end">
|
|
||||||
<a href={`/projects/${item.slug}`}>See more...</a>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
</Layout>
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue