What a refactor of articles
This commit is contained in:
169
src/lib/components/Footer.css.ts
Normal file
169
src/lib/components/Footer.css.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css'
|
||||
import { radialGradient, rgba, transparentize } from 'polished'
|
||||
import { sprinkles } from '$lib/styles/sprinkles.css'
|
||||
import {
|
||||
breakpoints,
|
||||
colors,
|
||||
mediaAt,
|
||||
menuBackground,
|
||||
transparent,
|
||||
vars,
|
||||
} from '$lib/styles/vars.css'
|
||||
|
||||
export const siteFooterClass = style([
|
||||
sprinkles({
|
||||
fontSize: { mobile: 'base', desktop: 'sm' },
|
||||
paddingX: '2x',
|
||||
paddingTop: '1x',
|
||||
color: 'menuLink',
|
||||
}),
|
||||
|
||||
radialGradient({
|
||||
colorStops: [
|
||||
`${menuBackground} 56%`,
|
||||
`${transparentize(0.4, menuBackground)} 100%`,
|
||||
],
|
||||
extent: '160% 100% at 100% 100%',
|
||||
fallback: transparent,
|
||||
}),
|
||||
{
|
||||
'@media': {
|
||||
[mediaAt(breakpoints.m)]: radialGradient({
|
||||
colorStops: [
|
||||
`${menuBackground} 48%`,
|
||||
`${transparentize(1, menuBackground)} 100%`,
|
||||
],
|
||||
extent: '140% 100% at 100% 100%',
|
||||
fallback: transparent,
|
||||
}),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const headerClass = sprinkles({
|
||||
fontWeight: 'bold',
|
||||
fontSize: 'base',
|
||||
color: 'menuLink',
|
||||
margin: 'none',
|
||||
lineHeight: '3x',
|
||||
marginBottom: '1x',
|
||||
})
|
||||
|
||||
export const sectionListsClass = style([
|
||||
sprinkles({
|
||||
display: 'grid',
|
||||
justifyItems: { mobile: 'center', desktop: 'start' },
|
||||
textAlign: { mobile: 'center', desktop: 'start' },
|
||||
maxWidth: 'max',
|
||||
columnGap: '3x',
|
||||
margin: 'auto',
|
||||
}),
|
||||
{
|
||||
'@media': {
|
||||
[mediaAt(breakpoints.l)]: {
|
||||
gridTemplateColumns: 'auto auto auto',
|
||||
},
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
export const sectionListSectionClass = sprinkles({
|
||||
marginY: '3x',
|
||||
})
|
||||
|
||||
export const listUlClass = sprinkles({
|
||||
listStyle: 'none',
|
||||
padding: 'none',
|
||||
margin: 'none',
|
||||
})
|
||||
|
||||
export const listLiClass = sprinkles({
|
||||
marginLeft: '1x',
|
||||
})
|
||||
|
||||
export const nestedListLiClass = style([
|
||||
listLiClass,
|
||||
sprinkles({
|
||||
fontSize: 'sm',
|
||||
}),
|
||||
])
|
||||
|
||||
export const socialLinkLabelClass = sprinkles({
|
||||
paddingX: '1x',
|
||||
})
|
||||
|
||||
export const svgClass = style({
|
||||
fill: vars.color.menuLink,
|
||||
height: '1em',
|
||||
width: '1em',
|
||||
})
|
||||
|
||||
export const strokeSvgClass = style([
|
||||
svgClass,
|
||||
{
|
||||
stroke: vars.color.menuLink,
|
||||
strokeWidth: '2px',
|
||||
},
|
||||
])
|
||||
|
||||
export const socialLinkClass = sprinkles({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: {
|
||||
mobile: 'center',
|
||||
desktop: 'start',
|
||||
},
|
||||
})
|
||||
|
||||
export const bottomLineClass = sprinkles({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginX: 'auto',
|
||||
paddingBottom: '1x',
|
||||
marginTop: '2x',
|
||||
maxWidth: 'max',
|
||||
})
|
||||
|
||||
export const dateClass = sprinkles({
|
||||
fontSize: 'xs',
|
||||
whiteSpace: 'nowrap',
|
||||
})
|
||||
|
||||
export const boldClass = sprinkles({
|
||||
fontWeight: 'bold',
|
||||
})
|
||||
|
||||
export const hrClass = style([
|
||||
sprinkles({
|
||||
marginY: '2x',
|
||||
marginX: '1x',
|
||||
}),
|
||||
{
|
||||
color: rgba(colors.midnightBlue, 0.14),
|
||||
borderWidth: '1px 0 0',
|
||||
},
|
||||
])
|
||||
|
||||
export const licenceText = sprinkles({
|
||||
textAlign: 'center',
|
||||
width: 'parent',
|
||||
fontSize: 'xs',
|
||||
})
|
||||
|
||||
export const latestPostsClass = style({})
|
||||
|
||||
globalStyle(`${siteFooterClass} a`, {
|
||||
color: vars.color.menuLink,
|
||||
})
|
||||
|
||||
globalStyle(`${headerClass} a:link, ${headerClass} a:visited`, {
|
||||
color: vars.color.menuLink,
|
||||
})
|
||||
|
||||
globalStyle(`${siteFooterClass} a:hover`, {
|
||||
color: vars.color.menuLinkHover,
|
||||
})
|
||||
|
||||
globalStyle(`${latestPostsClass} li a:visited:not(:hover)`, {
|
||||
color: vars.color.linkVisited,
|
||||
})
|
199
src/lib/components/Footer.svelte
Normal file
199
src/lib/components/Footer.svelte
Normal file
@ -0,0 +1,199 @@
|
||||
<script lang="ts">
|
||||
import { format } from 'date-fns'
|
||||
import type { ArticleContent } from '$lib/content/articleContentListing'
|
||||
import SvgIcon from './SvgIcon.svelte'
|
||||
import {
|
||||
boldClass,
|
||||
bottomLineClass,
|
||||
dateClass,
|
||||
headerClass,
|
||||
hrClass,
|
||||
latestPostsClass,
|
||||
listLiClass,
|
||||
listUlClass,
|
||||
nestedListLiClass,
|
||||
sectionListsClass,
|
||||
sectionListSectionClass,
|
||||
siteFooterClass,
|
||||
socialLinkClass,
|
||||
socialLinkLabelClass,
|
||||
strokeSvgClass,
|
||||
svgClass,
|
||||
licenceText,
|
||||
} from './Footer.css'
|
||||
|
||||
export let latestPosts: ArticleContent[]
|
||||
</script>
|
||||
|
||||
<footer class="site-footer navigation-theme {siteFooterClass}">
|
||||
<div class="lists {sectionListsClass}">
|
||||
<section class="site-map {sectionListSectionClass}">
|
||||
<ul class={listUlClass}>
|
||||
<li class={listLiClass}>
|
||||
<a href="/">Introduction</a>
|
||||
</li>
|
||||
<li class={listLiClass}>
|
||||
<a href="/portfolio">Portfolio</a>
|
||||
<ul class={listUlClass}>
|
||||
<li class={nestedListLiClass}>
|
||||
<a href="/portfolio#personal-information">About</a>
|
||||
</li>
|
||||
<li class={nestedListLiClass}>
|
||||
<a href="/portfolio#skills">Skills</a>
|
||||
</li>
|
||||
<li class={nestedListLiClass}>
|
||||
<a href="/portfolio#work-history">Work History</a>
|
||||
</li>
|
||||
<li class={nestedListLiClass}>
|
||||
<a href="/portfolio#projects">Projects</a>
|
||||
</li>
|
||||
<li class={nestedListLiClass}>
|
||||
<a href="/portfolio#education">Education</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="latest-posts {sectionListSectionClass} {latestPostsClass}">
|
||||
<h3 class={headerClass}>
|
||||
<a href="/blog">Latest posts</a>
|
||||
</h3>
|
||||
<ul class={listUlClass}>
|
||||
{#each latestPosts as post}
|
||||
<li class={listLiClass}>
|
||||
<a rel="prefetch" href="/blog/{post.slug}">
|
||||
<span>{post.title}</span>
|
||||
<time class="date {dateClass}" datetime={post.date}>
|
||||
- {format(new Date(post.date), 'do MMM, yyyy')}
|
||||
</time>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<hr class={hrClass} />
|
||||
<section class="subscribe {boldClass}">
|
||||
<a href="/feed.xml" rel="external" title="RSS feed" class="rss">
|
||||
Subscribe
|
||||
<SvgIcon name="rss" className={svgClass} />
|
||||
</a>
|
||||
<a
|
||||
href="/feed.json"
|
||||
rel="external"
|
||||
title="JSON feed"
|
||||
class="json-feed"
|
||||
aria-label="Subscribe with JSON feed"
|
||||
>
|
||||
<SvgIcon name="json-feed" className={svgClass} />
|
||||
</a>
|
||||
</section>
|
||||
</section>
|
||||
<section class="socials {sectionListSectionClass}">
|
||||
<h3 class={headerClass}>Contact</h3>
|
||||
<ul class="social-links {listUlClass}">
|
||||
<li class="email {listLiClass}">
|
||||
<a
|
||||
class={socialLinkClass}
|
||||
href="mailto: michalvankosk@gmail.com"
|
||||
title="E-mail address"
|
||||
>
|
||||
<SvgIcon name="mail" className={svgClass} />
|
||||
<span class={socialLinkLabelClass}>michalvankosk@gmail.com</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="twitter {listLiClass}">
|
||||
<a
|
||||
class={socialLinkClass}
|
||||
href="https://twitter.com/michalvankodev"
|
||||
title="Twitter profile"
|
||||
>
|
||||
<SvgIcon name="twitter" className={strokeSvgClass} />
|
||||
<span class={socialLinkLabelClass}>Twitter</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="github {listLiClass}">
|
||||
<a
|
||||
class={socialLinkClass}
|
||||
href="https://github.com/michalvankodev"
|
||||
title="Github profile"
|
||||
>
|
||||
<SvgIcon name="github" className={strokeSvgClass} />
|
||||
<span class={socialLinkLabelClass}>Github</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="linkedin {listLiClass}">
|
||||
<a
|
||||
class={socialLinkClass}
|
||||
href="https://www.linkedin.com/in/michal-vanko-dev/"
|
||||
title="LinkedIn profile"
|
||||
>
|
||||
<SvgIcon name="linkedin" className={svgClass} />
|
||||
<span class={socialLinkLabelClass}>LinkedIn</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="twitch {listLiClass}">
|
||||
<a
|
||||
class={socialLinkClass}
|
||||
href="https://twitch.tv/michalvankodev"
|
||||
title="Twitch profile"
|
||||
>
|
||||
<SvgIcon name="twitch" className={svgClass} />
|
||||
<span class={socialLinkLabelClass}>Twitch</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="instagram {listLiClass}">
|
||||
<a
|
||||
class={socialLinkClass}
|
||||
href="https://www.instagram.com/michalvankodev/"
|
||||
title="Instagram profile"
|
||||
>
|
||||
<SvgIcon name="instagram" className={svgClass} />
|
||||
<span class={socialLinkLabelClass}>Instagram</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
<footer class={bottomLineClass}>
|
||||
<p
|
||||
class={licenceText}
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dct="http://purl.org/dc/terms/"
|
||||
>
|
||||
<a
|
||||
property="dct:title"
|
||||
rel="cc:attributionURL"
|
||||
href="https://michalvanko.dev/">michalvanko.dev</a
|
||||
>
|
||||
by
|
||||
<a
|
||||
rel="cc:attributionURL dct:creator"
|
||||
property="cc:attributionName"
|
||||
href="https://michalvanko.dev/">Michal Vanko</a
|
||||
>
|
||||
is licensed under
|
||||
<a
|
||||
href="http://creativecommons.org/licenses/by-nc-nd/4.0/?ref=chooser-v1"
|
||||
target="_blank"
|
||||
rel="license noopener noreferrer"
|
||||
style="display:inline-block;"
|
||||
>CC BY-NC-ND 4.0<img
|
||||
style="height:22px!important;margin-left:3px;vertical-align:text-bottom;"
|
||||
src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1"
|
||||
alt="cc"
|
||||
/><img
|
||||
style="height:22px!important;margin-left:3px;vertical-align:text-bottom;"
|
||||
src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1"
|
||||
alt="by"
|
||||
/><img
|
||||
style="height:22px!important;margin-left:3px;vertical-align:text-bottom;"
|
||||
src="https://mirrors.creativecommons.org/presskit/icons/nc.svg?ref=chooser-v1"
|
||||
alt="nc"
|
||||
/><img
|
||||
style="height:22px!important;margin-left:3px;vertical-align:text-bottom;"
|
||||
src="https://mirrors.creativecommons.org/presskit/icons/nd.svg?ref=chooser-v1"
|
||||
alt="nd"
|
||||
/></a
|
||||
>
|
||||
</p>
|
||||
</footer>
|
||||
</footer>
|
84
src/lib/components/Nav.css.ts
Normal file
84
src/lib/components/Nav.css.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { globalStyle, style } from '@vanilla-extract/css'
|
||||
import { radialGradient, transparentize } from 'polished'
|
||||
import { menuBackground, transparent, vars } from '$lib/styles/vars.css'
|
||||
import { sprinkles } from '$lib/styles/sprinkles.css'
|
||||
|
||||
export const navigationClass = style([
|
||||
sprinkles({
|
||||
paddingTop: '1x',
|
||||
paddingBottom: '2x',
|
||||
paddingX: '1x',
|
||||
color: 'menu',
|
||||
textShadow: 'menuLinkShadow',
|
||||
}),
|
||||
radialGradient({
|
||||
colorStops: [
|
||||
`${menuBackground} 0%`,
|
||||
`${transparentize(1, menuBackground)} 100%`,
|
||||
],
|
||||
extent: '120% 100% at 0% 0%',
|
||||
fallback: transparent,
|
||||
}),
|
||||
])
|
||||
|
||||
export const navigationContentClass = sprinkles({
|
||||
display: 'flex',
|
||||
maxWidth: 'max',
|
||||
marginY: 'none',
|
||||
marginX: 'auto',
|
||||
})
|
||||
|
||||
export const navigationLinksClass = sprinkles({
|
||||
listStyle: 'none',
|
||||
margin: 'none',
|
||||
padding: 'none',
|
||||
display: 'flex',
|
||||
flex: '1',
|
||||
})
|
||||
|
||||
export const logoSectionClass = sprinkles({
|
||||
lineHeight: 'none',
|
||||
})
|
||||
|
||||
export const logoLinkClass = sprinkles({
|
||||
padding: 'none',
|
||||
display: 'block',
|
||||
})
|
||||
|
||||
globalStyle(`${navigationClass} a:not(${logoLinkClass})`, {
|
||||
color: vars.color.menuLink,
|
||||
padding: vars.space['1x'],
|
||||
})
|
||||
|
||||
globalStyle(`${navigationClass} a:hover`, {
|
||||
color: vars.color.menuLinkHover,
|
||||
})
|
||||
|
||||
export const logoImgClass = style({
|
||||
height: vars.space['3x'],
|
||||
})
|
||||
|
||||
export const selectedClass = sprinkles({
|
||||
textShadow: 'menuActiveLinkShadow',
|
||||
})
|
||||
|
||||
export const portfolioPageNavigation = style({
|
||||
position: 'sticky',
|
||||
top: '0px',
|
||||
zIndex: 1,
|
||||
width: '100%',
|
||||
fontSize: vars.fontSize.sm,
|
||||
padding: vars.space['1x'],
|
||||
background: vars.color.background,
|
||||
boxShadow: `0px 0.5em 0.5em ${vars.color.background}`,
|
||||
})
|
||||
|
||||
export const portfolioPageNavigationLinksClass = sprinkles({
|
||||
maxWidth: 'l',
|
||||
marginX: 'auto',
|
||||
marginY: 'none',
|
||||
})
|
||||
|
||||
export const portfolioPageNavigationLinkClass = sprinkles({
|
||||
padding: '1x',
|
||||
})
|
94
src/lib/components/Nav.svelte
Normal file
94
src/lib/components/Nav.svelte
Normal file
@ -0,0 +1,94 @@
|
||||
<script>
|
||||
import classNames from 'classnames'
|
||||
import {
|
||||
logoImgClass,
|
||||
logoLinkClass,
|
||||
logoSectionClass,
|
||||
navigationClass,
|
||||
navigationContentClass,
|
||||
navigationLinksClass,
|
||||
portfolioPageNavigation,
|
||||
portfolioPageNavigationLinkClass,
|
||||
portfolioPageNavigationLinksClass,
|
||||
selectedClass,
|
||||
} from './Nav.css'
|
||||
import { page } from '$app/stores'
|
||||
|
||||
$: segment = $page.url.pathname
|
||||
|
||||
let links = [
|
||||
{
|
||||
label: 'Introduction',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
label: 'Blog',
|
||||
url: '/blog',
|
||||
},
|
||||
{
|
||||
label: 'Broadcasts',
|
||||
url: '/broadcasts',
|
||||
},
|
||||
// {
|
||||
// label: "Dev's Cookery",
|
||||
// url: '/cookery',
|
||||
// },
|
||||
{
|
||||
label: 'Portfolio',
|
||||
url: '/portfolio',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<nav class={navigationClass}>
|
||||
<section class={navigationContentClass}>
|
||||
<ul class={navigationLinksClass}>
|
||||
{#each links as link}
|
||||
<li>
|
||||
<a
|
||||
rel="prefetch"
|
||||
class={classNames({ [selectedClass]: segment === link.url })}
|
||||
href={link.url}
|
||||
>
|
||||
{link.label}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<aside class="logo-section {logoSectionClass}">
|
||||
<a class="logo {logoLinkClass}" href=".">
|
||||
<img
|
||||
class={logoImgClass}
|
||||
src="/m-logo.svg"
|
||||
alt="m logo"
|
||||
width="44px"
|
||||
height="44px"
|
||||
/>
|
||||
</a>
|
||||
</aside>
|
||||
</section>
|
||||
</nav>
|
||||
|
||||
{#if segment === '/portfolio'}
|
||||
<section class="page-navigation {portfolioPageNavigation}">
|
||||
<div class={portfolioPageNavigationLinksClass}>
|
||||
<a
|
||||
class={portfolioPageNavigationLinkClass}
|
||||
href="/portfolio#personal-information">About</a
|
||||
>
|
||||
<a class={portfolioPageNavigationLinkClass} href="/portfolio#skills"
|
||||
>Skills</a
|
||||
>
|
||||
<a class={portfolioPageNavigationLinkClass} href="/portfolio#work-history"
|
||||
>Work History</a
|
||||
>
|
||||
<a class={portfolioPageNavigationLinkClass} href="/portfolio#projects"
|
||||
>Projects</a
|
||||
>
|
||||
<a class={portfolioPageNavigationLinkClass} href="/portfolio#education"
|
||||
>Education</a
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
9
src/lib/components/SvgIcon.svelte
Normal file
9
src/lib/components/SvgIcon.svelte
Normal file
@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import svgSprite from '../../svg/build/icons-sprite.svg'
|
||||
export let className: string
|
||||
export let name: string
|
||||
</script>
|
||||
|
||||
<svg aria-hidden="true" class={className}>
|
||||
<use xlink:href={`${svgSprite}#${name}`} />
|
||||
</svg>
|
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
interface ArticleDetails {
|
||||
title: string
|
||||
slug: string
|
||||
preview: string
|
||||
}
|
||||
export let segment: string
|
||||
export let article: ArticleDetails
|
||||
</script>
|
||||
|
||||
<article>
|
||||
<header>
|
||||
<h2>
|
||||
<a rel="prefetch" href={`/${segment}/${article.slug}`}>{article.title}</a>
|
||||
</h2>
|
||||
</header>
|
||||
{@html article.preview}
|
||||
</article>
|
@ -0,0 +1,30 @@
|
||||
import { sprinkles } from '$lib/styles/sprinkles.css'
|
||||
|
||||
export const tagsListClass = sprinkles({
|
||||
listStyle: 'none',
|
||||
margin: 'none',
|
||||
padding: 'none',
|
||||
display: 'inline',
|
||||
})
|
||||
|
||||
export const tagsListLiClass = sprinkles({
|
||||
display: 'inline',
|
||||
fontStyle: 'italic',
|
||||
})
|
||||
|
||||
export const publishedClass = sprinkles({
|
||||
whiteSpace: 'nowrap',
|
||||
fontStyle: 'italic',
|
||||
})
|
||||
|
||||
export const publishedLabelClass = sprinkles({
|
||||
color: 'tintedText',
|
||||
})
|
||||
|
||||
export const footerClass = sprinkles({
|
||||
display: 'flex',
|
||||
fontSize: 'sm',
|
||||
justifyContent: 'space-between',
|
||||
paddingTop: '1x',
|
||||
marginTop: '2x',
|
||||
})
|
@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { horizontalBorderTopClass } from '$lib/styles/scoops.css'
|
||||
|
||||
import { format } from 'date-fns'
|
||||
import type { ArticleContent } from '$lib/content/articleContentListing'
|
||||
import {
|
||||
footerClass,
|
||||
publishedClass,
|
||||
publishedLabelClass,
|
||||
tagsListClass,
|
||||
tagsListLiClass,
|
||||
} from './ArticlePreviewFooter.css'
|
||||
|
||||
export let segment: string
|
||||
export let article: ArticleContent
|
||||
</script>
|
||||
|
||||
<footer class="{footerClass} {horizontalBorderTopClass}">
|
||||
<div class="article-tags">
|
||||
{#if article.tags.length > 0}
|
||||
<span class="lighten">Tags:</span>
|
||||
<ul class={tagsListClass}>
|
||||
{#each article.tags as tag}
|
||||
<li class={tagsListLiClass}>
|
||||
<a href="/{segment}/tags/{tag}">{tag}</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="created-at">
|
||||
<span class={publishedLabelClass}>Published on</span>
|
||||
<time datetime={article.date} class={publishedClass}>
|
||||
{format(new Date(article.date), "do MMMM',' y")}
|
||||
</time>
|
||||
</div>
|
||||
</footer>
|
@ -0,0 +1,20 @@
|
||||
import { globalStyle } from '@vanilla-extract/css'
|
||||
import { vars } from '$lib/styles/vars.css'
|
||||
import { sprinkles } from '$lib/styles/sprinkles.css'
|
||||
|
||||
export const postListClass = sprinkles({
|
||||
padding: 'none',
|
||||
lineHeight: '3x',
|
||||
listStyle: 'none',
|
||||
})
|
||||
|
||||
export const seeAllClass = sprinkles({
|
||||
textAlign: 'end',
|
||||
width: 'parent',
|
||||
maxWidth: 'max',
|
||||
margin: 'auto',
|
||||
})
|
||||
|
||||
globalStyle(`${postListClass} > li:not(:last-child)`, {
|
||||
marginBottom: vars.space['4x'],
|
||||
})
|
@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import ArticleFooter from '$lib/components/articles/ArticlePreviewFooter/ArticlePreviewFooter.svelte'
|
||||
import Paginator from '$lib/components/paginator/Paginator.svelte'
|
||||
import { postListClass } from './ArticlePreviewList.css'
|
||||
import ArticlePreviewCard from '$lib/components/articles/ArticlePreviewCard/ArticlePreviewCard.svelte'
|
||||
import type { PaginationResult } from '$lib/pagination/pagination'
|
||||
import type { ArticleContent } from '$lib/content/articleContentListing'
|
||||
|
||||
export let page: number
|
||||
export let pageSize: number
|
||||
export let filters: Record<string, string>
|
||||
export let posts: PaginationResult<ArticleContent>
|
||||
export let segment: string
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<Paginator
|
||||
{segment}
|
||||
{page}
|
||||
{pageSize}
|
||||
{filters}
|
||||
totalCount={posts.totalCount}
|
||||
/>
|
||||
</header>
|
||||
<ul class="post-list {postListClass}">
|
||||
{#each posts.items as article (article.slug)}
|
||||
<li>
|
||||
<ArticlePreviewCard {article} {segment} />
|
||||
<ArticleFooter {article} {segment} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<footer>
|
||||
<Paginator
|
||||
{segment}
|
||||
{page}
|
||||
{pageSize}
|
||||
{filters}
|
||||
totalCount={posts.totalCount}
|
||||
/>
|
||||
</footer>
|
21
src/lib/components/paginator/Paginator.css.ts
Normal file
21
src/lib/components/paginator/Paginator.css.ts
Normal file
@ -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',
|
||||
})
|
50
src/lib/components/paginator/Paginator.svelte
Normal file
50
src/lib/components/paginator/Paginator.svelte
Normal file
@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
activePage,
|
||||
listClass,
|
||||
listItemClass,
|
||||
pageLinkClass,
|
||||
} from './Paginator.css'
|
||||
|
||||
import { getPaginatorPages, createHref } from './paginatorUtils'
|
||||
|
||||
export const Divider = 'divider'
|
||||
|
||||
export let segment: string
|
||||
export let page: number
|
||||
export let pageSize: number
|
||||
export let totalCount: number
|
||||
export let filters: Record<string, string>
|
||||
|
||||
$: paginatorPages = getPaginatorPages({ page, pageSize, totalCount })
|
||||
</script>
|
||||
|
||||
<ul class={listClass}>
|
||||
{#if page !== 1}
|
||||
<li class="{listItemClass} ">
|
||||
<a class={pageLinkClass} href={createHref(segment, filters, page - 1)}
|
||||
><</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
{#each paginatorPages as pageNumber}
|
||||
{#if pageNumber === Divider}
|
||||
<li class={listItemClass}>...</li>
|
||||
{:else if page === pageNumber}
|
||||
<li class="{listItemClass} {activePage}">{pageNumber}</li>
|
||||
{:else}
|
||||
<li class="{listItemClass} ">
|
||||
<a class={pageLinkClass} href={createHref(segment, filters, pageNumber)}
|
||||
>{pageNumber}</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if page !== paginatorPages.length}
|
||||
<li class="{listItemClass} ">
|
||||
<a class={pageLinkClass} href={createHref(segment, filters, page + 1)}
|
||||
>></a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
52
src/lib/components/paginator/paginatorUtils.test.ts
Normal file
52
src/lib/components/paginator/paginatorUtils.test.ts
Normal file
@ -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])
|
||||
})
|
||||
})
|
||||
})
|
49
src/lib/components/paginator/paginatorUtils.ts
Normal file
49
src/lib/components/paginator/paginatorUtils.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { toParams } from '$lib/pagination/dropTakeParams'
|
||||
import { last, range } from 'ramda'
|
||||
|
||||
export const Divider = 'divider'
|
||||
|
||||
export function getPaginatorPages({
|
||||
page,
|
||||
pageSize,
|
||||
totalCount,
|
||||
}: {
|
||||
page: number
|
||||
pageSize: number
|
||||
totalCount: number
|
||||
}): (number | typeof Divider)[] {
|
||||
const maxLinksLength = 7
|
||||
const linksAroundActive = 2
|
||||
const totalPages = Math.ceil(totalCount / pageSize)
|
||||
const daco = range(1, totalPages + 1).reduce<(number | typeof Divider)[]>((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<string, string>,
|
||||
pageNumber: number
|
||||
) {
|
||||
const filtersPath = toParams(filters)
|
||||
return `/${href}/${filtersPath ? filtersPath + '/' : ''}page/${pageNumber}`
|
||||
}
|
69
src/lib/content/articleContentListing.ts
Normal file
69
src/lib/content/articleContentListing.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { readdir, readFile } from 'fs'
|
||||
import { promisify } from 'util'
|
||||
import { basename } from 'path'
|
||||
import { pipe, prop, sortBy, reverse, filter } from 'ramda'
|
||||
import fm from 'front-matter'
|
||||
import marked from 'marked'
|
||||
import {
|
||||
filterAndCount,
|
||||
type PaginationQuery,
|
||||
} from '$lib/pagination/pagination'
|
||||
|
||||
const { NODE_ENV } = process.env
|
||||
|
||||
export interface ArticleAttributes {
|
||||
layout: string
|
||||
title: string
|
||||
published: boolean
|
||||
date: string
|
||||
thumbnail: string
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface ArticleContent extends ArticleAttributes {
|
||||
preview: string
|
||||
slug: string
|
||||
published: boolean
|
||||
}
|
||||
|
||||
export async function getBlogListing(paginationQuery: PaginationQuery) {
|
||||
const files = await promisify(readdir)(`_posts/blog/`, 'utf-8')
|
||||
const filteredFiles = filterDevelopmentFiles(files)
|
||||
|
||||
const contents = await Promise.all(
|
||||
filteredFiles.map(async (file) => {
|
||||
const fileContent = await promisify(readFile)(
|
||||
`_posts/blog/${file}`,
|
||||
'utf-8'
|
||||
)
|
||||
const parsedAttributes = fm<ArticleAttributes>(fileContent)
|
||||
|
||||
const lineOfTextRegExp = /^(?:\w|\[).+/gm
|
||||
const lines = parsedAttributes.body
|
||||
.match(lineOfTextRegExp)
|
||||
.slice(0, 2)
|
||||
.join('\n')
|
||||
|
||||
const preview = marked(lines)
|
||||
return {
|
||||
...parsedAttributes.attributes,
|
||||
preview,
|
||||
slug: basename(file, '.md'),
|
||||
}
|
||||
})
|
||||
)
|
||||
const filteredContents = pipe(
|
||||
sortBy<ArticleContent>(prop('date')),
|
||||
(items) => reverse(items),
|
||||
filter<(typeof contents)[0]>((article) => article.published),
|
||||
filterAndCount(paginationQuery)
|
||||
)(contents)
|
||||
|
||||
return filteredContents
|
||||
}
|
||||
|
||||
function filterDevelopmentFiles(files: string[]) {
|
||||
return NODE_ENV !== 'production'
|
||||
? files
|
||||
: files.filter((file) => !file.startsWith('dev-'))
|
||||
}
|
@ -17,7 +17,8 @@ export function dropAndTake<Item>({ offset = 0, limit = Infinity }) {
|
||||
) => Item[]
|
||||
}
|
||||
|
||||
export function filterByPropContains<Item>(filters: Record<string, string>) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function filterByPropContains<Item extends Record<string, any>>(filters: Record<string, string>) {
|
||||
return function (items: Item[]) {
|
||||
return items.filter((item) => {
|
||||
return Object.entries(filters).every(([fieldName, value]) =>
|
||||
@ -27,7 +28,8 @@ export function filterByPropContains<Item>(filters: Record<string, string>) {
|
||||
}
|
||||
}
|
||||
|
||||
export function filterAndCount<Item>({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function filterAndCount<Item extends Record<string, any>>({
|
||||
filters,
|
||||
...dropTakeParams
|
||||
}: PaginationQuery) {
|
||||
|
Reference in New Issue
Block a user