remove old svelte web source

This commit is contained in:
Michal Vanko 2024-09-30 21:13:20 +02:00
parent f42ebc7044
commit 1ce8ccfdd5
149 changed files with 20 additions and 10198 deletions

View File

@ -1,27 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': require('typescript'),
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2019,
},
env: {
browser: true,
es2017: true,
node: true,
},
rules: {
'@typescript-eslint/explicit-module-boundary-types': 0,
},
}

41
.gitignore vendored
View File

@ -1,26 +1,25 @@
.DS_Store .DS_Store
/node_modules/
/src/node_modules/@sapper/
yarn-error.log
/cypress/screenshots/
/__sapper__/
/.svelte-kit # `dist` folder with the export of SSG
/.svelte/
/build/
/functions/
/static/build/
#amplify
amplify/\#current-cloud-backend
amplify/.config/local-*
amplify/backend/amplify-meta.json
amplify/backend/awscloudformation
dist/ dist/
node_modules/
aws-exports.js
awsconfiguration.json
/static/**/optimized/
# Local Netlify folder # Local Netlify folder
.netlify .netlify
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Image generator
generated_images/

View File

View File

@ -1,18 +0,0 @@
version: "{build}"
shallow_clone: true
init:
- git config --global core.autocrlf false
build: off
environment:
matrix:
# node.js
- nodejs_version: stable
install:
- ps: Install-Product node $env:nodejs_version
- npm install cypress
- npm install

View File

@ -1,20 +0,0 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# `dist` folder with the export of SSG
dist/
# Image generator
generated_images/

View File

@ -1,4 +0,0 @@
{
"baseUrl": "http://localhost:3000",
"video": false
}

7319
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,56 +0,0 @@
{
"name": "michalvankodev",
"description": "My personal website with blog",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"prebuild": "npm run svgstore",
"build": "vite build",
"preview": "vite preview",
"start": "svelte-kit start",
"test": "vitest",
"svgstore": "svgstore -o src/svg/build/icons-sprite.svg src/svg/**.svg",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"dependencies": {
"@vanilla-extract/css": "^1.9.2",
"@vanilla-extract/sprinkles": "^1.5.1",
"@vanilla-extract/vite-plugin": "^3.7.0",
"classnames": "^2.3.2",
"date-fns": "^2.29.3",
"feed": "^4.2.2",
"front-matter": "^4.0.2",
"marked": "^3.0.8",
"modern-normalize": "^1.1.0",
"polished": "^4.2.2",
"prismjs": "^1.29.0",
"ramda": "^0.28.0"
},
"devDependencies": {
"@sveltejs/adapter-static": "^1.0.4",
"@sveltejs/kit": "^1.1.1",
"@tsconfig/svelte": "^3.0.0",
"@types/classnames": "^2.3.1",
"@types/node": "^18.11.18",
"@types/ramda": "^0.28.21",
"@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2",
"eslint": "^8.32.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "~2.8.3",
"prettier-plugin-svelte": "^2.9.0",
"svelte": "^3.55.1",
"svelte-preprocess": "^5.0.0",
"svgstore-cli": "^2.0.1",
"tslib": "^2.4.1",
"typescript": "^4.9.4",
"vite": "^4.0.4",
"vitest": "^0.27.2",
"vitest-svelte-kit": "^0.0.6"
}
}

View File

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="theme-color" content="#333333" />
<meta name="description" content="Personal website of @michalvankodev" />
<meta name="keywords" content="personal, blog, webdev, tech, programming" />
<meta name="robots" content="index, follow" />
<link rel="alternate" type="application/rss+xml" title="RSS feed for latest posts" href="https://michalvanko.dev/feed.xml" />
<link rel="alternate" title="JSON feed for latest posts" type="application/json" href="https://michalvanko.dev/feed.json" />
<link rel="stylesheet" href="/print.css" media="print" />
<link rel="stylesheet" href="/fonts.css" />
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="/prism.css" />
<link rel="icon" type="image/svg+xml" href="/m-logo.svg" />
<link rel="icon" type="image/png" href="/m-logo-192.png" />
<!-- This contains the contents of the <svelte:head> component, if
the current page has one -->
%sveltekit.head%
</head>
<body>
<div style="display: contents">
%sveltekit.body%
</div>
</body>
</html>

3
src/global.d.ts vendored
View File

@ -1,3 +0,0 @@
/// <reference types="@sveltejs/kit" />
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@ -1,38 +0,0 @@
import { error } from '@sveltejs/kit'
import fm from 'front-matter'
import { readFile } from 'fs'
import { parseField } from '$lib/markdown/parse-markdown'
import { promisify } from 'util'
export interface ArticleAttributes {
slug: string
layout: string
segments: string[]
title: string
published: boolean
date: string
thumbnail: string
tags: string[]
body: string
}
export async function getArticleContent(slug: string) {
let postSource: string
try {
postSource = await promisify(readFile)(`_posts/blog/${slug}.md`, 'utf-8')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
if (e.code === 'ENOENT') {
throw error(404, 'Post not found \n' + e.toString())
}
throw e
}
const parsedPost = fm<ArticleAttributes>(postSource)
const post = parseField<ArticleAttributes>('body')({
...parsedPost.attributes,
body: parsedPost.body,
})
return post
}

View File

@ -1,58 +0,0 @@
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'
import type { ArticleAttributes } from './articleContent'
export interface ArticlePreviewAttributes extends ArticleAttributes {
preview: string
}
const { NODE_ENV } = process.env
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<ArticlePreviewAttributes>(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-'))
}

View File

@ -1,11 +0,0 @@
declare global {
interface Window {
onMountScripts?: Array<() => void>
}
}
export function runOnMountScripts() {
window.onMountScripts?.forEach((fn) => {
fn()
})
}

View File

@ -1,169 +0,0 @@
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,
})

View File

@ -1,199 +0,0 @@
<script lang="ts">
import { format } from 'date-fns'
import type { ArticlePreviewAttributes } from '$lib/articleContent/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: ArticlePreviewAttributes[]
</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="/{post.segments[0]}/{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>

View File

@ -1,85 +0,0 @@
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',
flexWrap: 'wrap',
})
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',
})

View File

@ -1,94 +0,0 @@
<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}

View File

@ -1,9 +0,0 @@
<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>

View File

@ -1,18 +0,0 @@
<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>

View File

@ -1,30 +0,0 @@
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',
})

View File

@ -1,37 +0,0 @@
<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>

View File

@ -1,20 +0,0 @@
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'],
})

View File

@ -1,41 +0,0 @@
<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>

View File

@ -1,21 +0,0 @@
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',
})

View File

@ -1,50 +0,0 @@
<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)}
>&lt;</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)}
>&gt;</a
>
</li>
{/if}
</ul>

View File

@ -1,52 +0,0 @@
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])
})
})
})

View File

@ -1,51 +0,0 @@
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 shownPages = 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 shownPages
}
export function createHref(
href: string,
filters: Record<string, string>,
pageNumber: number
) {
const filtersPath = toParams(filters)
return `/${href}/${filtersPath ? filtersPath + '/' : ''}page/${pageNumber}`
}

View File

@ -1,42 +0,0 @@
import { map, multiply } from 'ramda'
export interface ImageOptions {
width?: number
height?: number
}
/**
* Get the URL for resource with specified parameters for Netlify Large Media trasformation
*
* @see https://docs.netlify.com/large-media/transform-images/
*/
export function getNFResize(href: string, { width, height }: ImageOptions) {
return `${href}?nf_resize=fit${height ? `&h=${height}` : ''}${
width ? `&w=${width}` : ''
}`
}
export const PIXEL_DENSITIES = [1, 1.5, 2, 3, 4]
function multiplyImageOptions(
multiplier,
imageOptions: ImageOptions
): ImageOptions {
return map(multiply(multiplier), imageOptions)
}
/**
* Generate `srcset` attribute for all `PIXEL_DENSITIES` to serve images in appropriate quality
* for each device with specific density
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset
*/
export function generateSrcSet(href: string, imageOptions: ImageOptions) {
return PIXEL_DENSITIES.map(
(density) =>
`${getNFResize(
href,
multiplyImageOptions(density, imageOptions)
)} ${density}x`
).join(',')
}

View File

@ -1,11 +0,0 @@
import marked from 'marked'
import { renderer } from './renderer-extension'
marked.use({ renderer })
export function parseField<T>(field: keyof T) {
return (item: T): T => ({
...item,
[field]: marked(item[field]),
})
}

View File

@ -1,49 +0,0 @@
import { generateSrcSet, getNFResize } from '$lib/large-media'
import Prism from 'prismjs'
import loadLanguages from 'prismjs/components/index.js'
loadLanguages(['bash', 'markdown', 'json', 'yaml', 'typescript'])
export const renderer = {
heading(text: string, level: string) {
const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-')
return `
<h${level}>
<a name="${escapedText}" class="anchor" href="#${escapedText}">
<span class="header-link"></span>
</a>
${text}
</h${level}>
`
},
image(href: string, title: string, text: string) {
const figcaption = title ? `<figcaption>${title}</figcaption>` : ''
const isLocal = !href.startsWith('http')
const src = isLocal ? getNFResize(href, { height: 800, width: 800 }) : href
const srcset = isLocal
? `srcset="${generateSrcSet(href, { width: 800, height: 800 })}"`
: ''
return `
<figure>
<img
alt="${text}"
${srcset}
src="${src}"
/>
${figcaption}
</figure>
`
},
code(source: string, lang?: string) {
// When lang is not specified it is usually an empty string which has to be handled
const usedLang = !lang ? 'shell' : lang
const highlightedSource = Prism.highlight(
source,
Prism.languages[usedLang],
usedLang
)
return `<pre class='language-${usedLang}'><code class='language-${usedLang}'>${highlightedSource}</code></pre>`
},
}

View File

@ -1,11 +0,0 @@
import { describe, test, expect } from 'vitest'
import { getDropTakeFromPageParams } from './dropTakeParams'
describe('convert search params', () => {
test('should convert from page size and page to offset and limit', () => {
expect(getDropTakeFromPageParams(7, 2)).toEqual({
offset: 7,
limit: 7,
})
})
})

View File

@ -1,42 +0,0 @@
import { init, splitEvery } from 'ramda'
export function parseParams(params: string) {
let splittedParams = params.split('/')
if (splittedParams.length % 2 !== 0) {
splittedParams = init(splittedParams)
}
const splits = splitEvery(2, splittedParams)
return Object.fromEntries(splits)
}
export function toParams(records: Record<string, string>) {
return Object.entries(records)
.map(([key, value]) => `${key}/${value}`)
.join('/')
}
export interface PaginationParams {
pageSize: number
page: number
filters?: Record<string, string>
}
export interface DropTakeParams {
offset: number
limit: number
}
/**
* Convert svelte `load` params into a `offset` and `limit` so they can be used to fetch endpoints with pagination queries
*/
export function getDropTakeFromPageParams(
pageSize: number,
page: number
): DropTakeParams {
const offset = pageSize * (page - 1)
const limit = pageSize
return {
offset,
limit,
}
}

View File

@ -1,98 +0,0 @@
import { range } from 'ramda'
import { describe, expect, test } from 'vitest'
import { filterByPropContains, dropAndTake, filterAndCount } from './pagination'
describe('pagination', () => {
test('does not drop any items by default', () => {
const items = range(0, 100)
expect(dropAndTake({})(items)).toHaveLength(100)
})
test('limits out exact number of items', () => {
const items = range(0, 100)
expect(dropAndTake({ limit: 10 })(items)).toHaveLength(10)
expect(dropAndTake({ limit: 10 })(items)[0]).toBe(0)
expect(dropAndTake({ limit: 10 })(items)[9]).toBe(9)
})
test('offset is skipping a number of items from the front', () => {
const items = range(0, 100)
expect(dropAndTake({ offset: 10 })(items)).toHaveLength(90)
expect(dropAndTake({ offset: 10 })(items)[0]).toBe(10)
})
test('is able to combine limit and offset', () => {
const items = range(0, 100)
expect(dropAndTake({ offset: 10, limit: 10 })(items)).toHaveLength(10)
expect(dropAndTake({ offset: 10, limit: 10 })(items)[0]).toBe(10)
expect(dropAndTake({ offset: 10, limit: 10 })(items)[9]).toBe(19)
})
test('is able to filter by a field', () => {
const items = [
{
id: 1,
prop: ['yes'],
},
{
id: 2,
prop: ['yes', 'no'],
},
]
expect(filterByPropContains({ prop: 'no' })(items)).toHaveLength(1)
expect(filterByPropContains({ prop: 'no' })(items)[0].id).toBe(2)
expect(filterByPropContains({ prop: 'yes' })(items)[0].id).toBe(1)
expect(filterByPropContains({ prop: 'yes' })(items)).toHaveLength(2)
})
describe('is able to combine limit and offset while filtering by field', () => {
const items = [
{
id: 1,
prop: ['yes'],
},
{
id: 2,
prop: ['yes', 'no'],
},
{
id: 3,
prop: ['yes', 'no'],
},
]
test('combine all parameters', () => {
const result = filterAndCount({
offset: 1,
limit: 1,
filters: { prop: 'no' },
})(items)
expect(result.totalCount).toBe(2)
expect(result.items[0].id).toBe(3)
})
test('with 0 offset', () => {
const result = filterAndCount({
offset: 0,
limit: 1,
filters: { prop: 'no' },
})(items)
expect(result.totalCount).toBe(2)
expect(result.items[0].id).toBe(2)
})
test('without filter', () => {
const result = filterAndCount({ offset: 1, limit: 1 })(items)
expect(result.totalCount).toBe(3)
expect(result.items[0].id).toBe(2)
})
test('without any params', () => {
const result = filterAndCount({})(items)
expect(result.totalCount).toBe(3)
expect(result.items.length).toEqual(result.totalCount)
})
})
})

View File

@ -1,48 +0,0 @@
import { identity, drop, take, pipe } from 'ramda'
export interface PaginationQuery {
offset?: number
limit?: number
filters?: Record<string, string>
}
export interface PaginationResult<ItemType> {
items: ItemType[]
totalCount: number
}
export function dropAndTake<Item>({ offset = 0, limit = Infinity }) {
return pipe(drop<Item>(offset), take<Item>(limit)) as (
items: Item[]
) => Item[]
}
// 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]) =>
item[fieldName].includes(value)
)
})
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function filterAndCount<Item extends Record<string, any>>({
filters,
...dropTakeParams
}: PaginationQuery) {
return function (items: Item[]) {
const filterFunction = filters
? filterByPropContains<Item>(filters)
: identity
const filteredItems = filterFunction(items)
return {
items: dropAndTake<Item>(dropTakeParams)(filteredItems),
totalCount: filteredItems.length,
}
}
}

View File

@ -1,27 +0,0 @@
import { globalStyle, style } from '@vanilla-extract/css'
import { vars } from '$lib/styles/vars.css'
export const contentClass = style({})
globalStyle(`${contentClass} ul, ${contentClass} ol`, {
lineHeight: vars.lineHeight['2x'],
})
globalStyle(`${contentClass} li`, {
marginBottom: vars.space['2x'],
})
globalStyle(`${contentClass} img`, {
maxHeight: vars.height.image,
})
globalStyle(`${contentClass} img:only-child`, {
display: 'block',
margin: '0 auto',
})
globalStyle(`${contentClass} .video-embed`, {
margin: '0 auto',
maxWidth: vars.width.image,
aspectRatio: vars.aspectRatio.monitor,
})

View File

@ -1,155 +0,0 @@
import { globalStyle } from '@vanilla-extract/css'
import { breakpoints, colors, vars } from './vars.css'
globalStyle('html', {
scrollBehavior: 'smooth',
})
globalStyle('body', {
margin: 0,
fontFamily:
'cantarell, roboto, -apple-system, blinkmacsystemfont, segoe ui, oxygen, ubuntu, fira sans, droid sans, helvetica neue, sans-serif',
fontSize: '16px',
lineHeight: 1.65,
color: vars.color.articleText,
background: vars.color.background,
minHeight: '100vh',
'@media': {
[`screen and (min-width: ${breakpoints.s}px)`]: {
fontSize: '18px',
},
[`screen and (min-width: ${breakpoints.m}px)`]: {
fontSize: '24px',
},
print: {
fontSize: '12px',
}
},
})
globalStyle('h1, h2, h3, h4, h5, h6', {
marginTop: vars.space['2x'],
marginBottom: vars.space['1x'],
marginLeft: vars.space.none,
marginRight: vars.space.none,
lineHeight: vars.lineHeight['1x'],
color: vars.color.header,
fontWeight: 500,
letterSpacing: '-0.01em',
})
globalStyle('h1', {
fontSize: vars.fontSize['5x'],
fontWeight: 800,
})
globalStyle('h2', {
fontSize: vars.fontSize['4x'],
fontWeight: 700,
})
globalStyle('h3', {
fontSize: vars.fontSize['3x'],
})
globalStyle('h4', {
fontSize: vars.space['2x'],
})
globalStyle('a', {
textDecoration: 'none',
transition: 'color 0.2s',
})
globalStyle('a:link', {
color: vars.color.link,
})
globalStyle('a:hover', {
color: vars.color.linkHover,
textDecoration: 'underline',
})
globalStyle('a:visited', {
color: vars.color.linkVisited,
})
globalStyle('a:visited:hover', {
color: vars.color.linkVisitedHover,
})
globalStyle('main pre, main pre[class*="language-"], main :not(pre) > code', {
fontFamily: 'menlo, inconsolata, monospace',
backgroundColor: vars.color.codeBackground,
paddingTop: vars.space['1x'],
paddingBottom: vars.space['1x'],
paddingLeft: vars.space['1x'],
paddingRight: vars.space['1x'],
color: vars.color.code,
lineHeight: vars.lineHeight['0x'],
boxShadow: vars.boxShadow.codeBoxShadow,
borderRadius: 3,
})
globalStyle('main code, main code[class*="language-"]', {
fontSize: vars.fontSize.sm,
'@media': {
[`screen and (min-width: ${breakpoints.m}px)`]: {
fontSize: vars.fontSize.xs,
},
},
})
globalStyle('code', {
whiteSpace: 'pre-line',
})
globalStyle('pre code', {
whiteSpace: 'pre',
})
globalStyle('figure', {
marginTop: vars.space['2x'],
marginBottom: vars.space['2x'],
marginLeft: vars.space['1x'],
marginRight: vars.space['1x'],
textAlign: 'center',
})
globalStyle('figcaption', {
fontSize: vars.fontSize.xs,
fontStyle: 'italic',
})
globalStyle('blockquote', {
lineHeight: vars.lineHeight['2x'],
margin: vars.space['0x'],
paddingLeft: vars.space['2x'],
paddingRight: vars.space['0x'],
paddingTop: vars.space['1x'],
paddingBottom: vars.space['2x'],
background: vars.color.quoteBackground,
borderRadius: 3,
borderLeft: `2px solid ${colors.tearkiss}`,
boxShadow: vars.boxShadow.contentBoxShadow,
fontSize: vars.fontSize.sm,
overflow: 'auto',
})
globalStyle('blockquote p', {
marginTop: vars.space['0x'],
marginBottom: vars.space['0x'],
})
globalStyle('p', {
marginTop: vars.space['1x'],
marginBottom: vars.space['1x'],
})
globalStyle('b, strong', {
fontWeight: 600,
})
globalStyle('::selection', {
background: vars.color.selection,
})

View File

@ -1,10 +0,0 @@
import { style } from '@vanilla-extract/css'
import { desaturate, transparentize } from 'polished'
import { colors } from './vars.css'
export const horizontalBorderTopClass = style({
borderTop: `1px solid ${transparentize(
0.6,
desaturate(0.5, colors.tearkiss)
)}`,
})

View File

@ -1,80 +0,0 @@
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles'
import { breakpoints, vars } from './vars.css'
const responsiveProperties = defineProperties({
conditions: {
mobile: {},
tablet: { '@media': `screen and (min-width: ${breakpoints.m}px)` },
desktop: { '@media': `screen and (min-width: ${breakpoints.l}px)` },
},
defaultCondition: 'mobile',
properties: {
display: ['none', 'flex', 'block', 'inline', 'inline-block', 'grid'],
position: ['relative', 'absolute', 'fixed'],
flexDirection: ['row', 'column'],
flexWrap: ['wrap', 'nowrap'],
flexShrink: [0],
flexGrow: [0, 1],
justifyContent: [
'stretch',
'start',
'center',
'end',
'space-around',
'space-between',
],
justifyItems: [
'stretch',
'start',
'center',
'end',
'space-around',
'space-between',
],
alignItems: ['stretch', 'flex-start', 'center', 'flex-end'],
flex: ['1'],
gap: vars.space,
textAlign: ['center', 'justify', 'start', 'end'],
textShadow: vars.textShadow,
paddingTop: vars.space,
paddingBottom: vars.space,
paddingLeft: vars.space,
paddingRight: vars.space,
marginTop: vars.space,
marginBottom: vars.space,
marginRight: vars.space,
marginLeft: vars.space,
columnGap: vars.space,
fontSize: vars.fontSize,
fontFamily: vars.fontFamily,
fontWeight: vars.fontWeight,
fontStyle: ['italic', 'normal'],
lineHeight: vars.lineHeight,
whiteSpace: ['normal', 'nowrap'],
width: vars.width,
maxWidth: vars.width,
height: vars.height,
listStyle: ['none'],
overflow: ['auto'],
aspectRatio: vars.aspectRatio,
},
shorthands: {
padding: ['paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight'],
paddingX: ['paddingLeft', 'paddingRight'],
paddingY: ['paddingTop', 'paddingBottom'],
placeItems: ['justifyContent', 'alignItems'],
typeSize: ['fontSize', 'lineHeight'],
margin: ['marginTop', 'marginBottom', 'marginLeft', 'marginRight'],
marginX: ['marginLeft', 'marginRight'],
marginY: ['marginTop', 'marginBottom'],
},
})
const colorProperties = defineProperties({
properties: {
color: vars.color,
background: vars.color,
},
})
export const sprinkles = createSprinkles(responsiveProperties, colorProperties)

View File

@ -1,154 +0,0 @@
import { createGlobalTheme } from '@vanilla-extract/css'
import {
darken,
desaturate,
lighten,
mix,
modularScale,
saturate,
tint,
transparentize,
} from 'polished'
export const colors = {
tearkiss: '#42a6f0',
pinky: '#fea6eb',
lightCyan: '#d8f6ff',
midnightBlue: '#171664',
frenchViolet: '#7332c3',
}
export const menuBackground = transparentize(0.6, colors.tearkiss)
export const twitchEmbedBackground = transparentize(0.6, colors.pinky)
export const background = tint(0.7, colors.lightCyan)
export const codeBackground = tint(0.2, background)
export const quoteBackground = darken(0.02, background)
export const transparent = transparentize(1, '#ffffff')
const articleText = desaturate(0.16, colors.midnightBlue)
export enum breakpoints {
s = 400,
m = 700,
image = 800,
l = 1000,
max = 1140,
}
export function mediaAt(breakpoint: breakpoints) {
return `screen and (min-width: ${breakpoint}px)`
}
const createScale =
(base: number, ratio: number, unit = 'em') =>
(steps: number) =>
`${modularScale(steps, base, ratio)}${unit}`
const spaceScale = createScale(0.2, 2)
const fontSizeScale = createScale(1, 1.125)
const lineHeightScale = createScale(1.05, 1.125)
// const borderRadiusScale = createScale(1.5, 4)
export const vars = createGlobalTheme(':root', {
space: {
none: '0',
auto: 'auto',
'0x': spaceScale(0),
'1x': spaceScale(1),
'2x': spaceScale(2),
'3x': spaceScale(3),
'4x': spaceScale(4),
'5x': spaceScale(5),
'6x': spaceScale(6),
'7x': spaceScale(7),
'8x': spaceScale(8),
},
color: {
articleText,
tintedText: tint(0.25, articleText),
selection: tint(0.4, colors.pinky),
link: saturate(0.2, mix(0.66, colors.tearkiss, colors.midnightBlue)),
linkHover: colors.tearkiss,
linkVisited: colors.frenchViolet,
linkVisitedHover: lighten(0.1, colors.frenchViolet),
code: lighten(0.15, articleText),
menu: colors.midnightBlue,
menuLink: colors.midnightBlue,
menuLinkHover: lighten(0.15, colors.midnightBlue),
header: lighten(0.1, colors.midnightBlue),
background,
codeBackground,
quoteBackground,
menuBackground,
},
fontFamily: {
body: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
},
fontSize: {
xs: fontSizeScale(-2),
sm: fontSizeScale(-1),
base: fontSizeScale(0),
xl: fontSizeScale(1),
'2x': fontSizeScale(2),
'3x': fontSizeScale(3),
'4x': fontSizeScale(5),
'5x': fontSizeScale(7),
'6x': fontSizeScale(9),
},
lineHeight: {
none: '0',
'0x': lineHeightScale(0),
'1x': lineHeightScale(1),
'2x': lineHeightScale(2),
'3x': lineHeightScale(3),
'4x': lineHeightScale(4),
'5x': lineHeightScale(5),
},
fontWeight: {
thin: 'thin',
normal: 'normal',
bold: 'bold',
},
textShadow: {
menuLinkShadow: `0.02em 0.02em 0.03em ${transparentize(
0.7,
colors.midnightBlue
)}`,
menuActiveLinkShadow: `0.01em 0.01em 0.05em ${transparentize(
0.1,
colors.midnightBlue
)}`,
},
boxShadow: {
contentBoxShadow: `0px 0px 2px 1px ${transparentize(
0.5,
desaturate(0.5, colors.tearkiss)
)}`,
codeBoxShadow: `inset 0px 0px 2px 1px ${transparentize(
0.8,
desaturate(0.5, colors.tearkiss)
)}`,
},
width: {
auto: 'auto',
s: '400px',
m: '700px',
image: '800px',
l: '1000px',
max: '1140px',
full: '100vw',
parent: '100%',
layoutMax: '42rem',
headerFooterMax: '52rem',
additionalBlockMax: '46rem',
},
height: {
full: '100hw',
parent: '100%',
image: '640px',
},
aspectRatio: {
monitor: '16 / 9',
},
})

View File

@ -1,4 +0,0 @@
/** @type {import('@sveltejs/kit').ParamMatcher} */
export function match(param: string) {
return !['tags', 'page'].some((keyword) => param.startsWith(keyword))
}

View File

@ -1,20 +0,0 @@
<script lang="ts">
import type { LayoutData } from './$types'
import Nav from '$lib/components/Nav.svelte'
import Footer from '$lib/components/Footer.svelte'
import 'modern-normalize/modern-normalize.css'
import '$lib/styles/global.css'
import { mainContentClass } from './layout.css'
export let data: LayoutData
export let latestPosts = data.latestPosts
</script>
<div class="app-content">
<Nav />
<main class={mainContentClass}>
<slot />
</main>
<Footer {latestPosts} />
</div>

View File

@ -1,11 +0,0 @@
import type { LayoutLoad } from './$types'
export const prerender = true
export const load = (async ({ fetch }) => {
const blogPostsResponse = await fetch(`/articles/pageSize/5.json`)
const blogPostsContent = await blogPostsResponse.json()
return {
latestPosts: blogPostsContent.posts.items,
}
}) satisfies LayoutLoad

View File

@ -1,83 +0,0 @@
<script lang="ts">
import { generateSrcSet, getNFResize } from '$lib/large-media'
import {
citeOwnerClass,
mottoClass,
profilePicClass,
profilePicImgClass,
twitchAsideClass,
twitchEmbedClass,
twitchIframeClass,
twitchStreamPromoClass,
welcomeNoteClass,
} from './page.css'
</script>
<svelte:head>
<title>Introduction @michalvankodev</title>
</svelte:head>
<header class="index-header">
<figure class="profile-pic {profilePicClass}">
<picture>
<img
alt="Portrait"
class="{profilePicImgClass}"
srcset={generateSrcSet('/images/profile-portugal-landscape.jpg', {
width: 800,
})}
src={getNFResize('/images/profile-portugal-landscape.jpg', {
width: 800,
})}
/>
</picture>
</figure>
<p class="motto {mottoClass}">
<cite>“Let your ambition carry you.”</cite>
<span class="cite-owner {citeOwnerClass}">- La Flame</span>
</p>
</header>
<p class={welcomeNoteClass}>
Hey, welcome to my personal website. My name is
<strong>Michal&nbsp;Vanko</strong>
and I'm a
<em> <a href="https://en.wikipedia.org/wiki/Programmer">programmer</a> </em>
. I'll try to share some stories and opinions about things that I'm interested
in.
</p>
<section class="twitch-stream-promo {twitchStreamPromoClass}">
<h2>Follow my twitch stream</h2>
<div class="twitch-embed {twitchEmbedClass}">
<div class="twitch-video {twitchIframeClass}">
<iframe
title="My twitch channel"
src="https://player.twitch.tv/?channel=michalvankodev&parent=michalvanko.dev&parent=localhost&autoplay=false"
loading="lazy"
frameborder="0"
scrolling="no"
allowfullscreen
height="100%"
width="100%"
class="embed {twitchIframeClass}"
/>
</div>
<aside class={twitchAsideClass}>
Come hang out and chat with me <strong>every Tuesday and Thursday</strong>
afternoon central Europe time. I stream working on my side-projects and talking
anything about the developer lifestyle.
</aside>
<!-- <div class="twitch-chat">
<iframe
title="Twitch chat"
frameborder="0"
scrolling="no"
src="https://www.twitch.tv/embed/michalvankodev/chat?parent=michalvanko.dev&parent=localhost"
height="100%"
width="100%"
/>
</div> -->
</div>
</section>

View File

@ -1,23 +0,0 @@
import {
getDropTakeFromPageParams,
parseParams,
} from '$lib/pagination/dropTakeParams'
import { json } from '@sveltejs/kit'
import { getBlogListing } from '$lib/articleContent/articleContentListing'
import type { RequestHandler } from './$types'
export const prerender = true
export const GET = (async ({ params }) => {
const handledParams = params.params === 'index' ? '' : params.params
const { page = 1, pageSize = 7, ...filters } = parseParams(handledParams)
const paginationParams = getDropTakeFromPageParams(
Number(pageSize),
Number(page)
)
const paginationQuery = { ...paginationParams, filters }
const filteredContents = await getBlogListing(paginationQuery)
return json({
posts: filteredContents,
})
}) satisfies RequestHandler

View File

@ -1,32 +0,0 @@
<script lang="ts">
import type { PageData } from './$types'
import ArticlePreviewList from '$lib/components/articles/ArticlePreviewList/ArticlePreviewList.svelte'
import { seeAllClass } from '$lib/components/articles/ArticlePreviewList/ArticlePreviewList.css'
export let data: PageData
$: ({ posts, filters } = data)
</script>
<svelte:head>
<title>My blog @michalvankodev</title>
</svelte:head>
{#if posts.items.length === 0}
<p class="no-posts">You've found void in the space.</p>
{:else}
<h1>
{#if filters.tags}
<em>{filters.tags}</em>
{:else}
Blog
{/if}
posts
</h1>
{#if filters.tags}
<div class={seeAllClass}>
<a href="/blog">See all posts</a>
</div>
{/if}
{/if}
<ArticlePreviewList {...data} segment="blog" />

View File

@ -1,17 +0,0 @@
import { parseParams } from '$lib/pagination/dropTakeParams'
import type { PageLoad } from './$types'
import type { ArticlePreviewAttributes } from '$lib/articleContent/articleContentListing'
import type { PaginationResult } from '$lib/pagination/pagination'
export const load = (async ({ fetch, params }) => {
const { page = 1, pageSize = 7, ...filters } = parseParams(params.params)
const articleResponse = await fetch(
`/articles/segments/blog${params.params ? `/${params.params}` : ''}.json`
).then((r) => r.json())
return {
posts: articleResponse.posts as PaginationResult<ArticlePreviewAttributes >,
page: Number(page),
pageSize,
filters,
}
}) satisfies PageLoad

View File

@ -1,9 +0,0 @@
import type { PageServerLoad } from './$types'
import { getArticleContent } from '$lib/articleContent/articleContent'
export const prerender = true
export const load = (async ({ params: { slug } }) => {
const post = await getArticleContent(slug);
return post
}) satisfies PageServerLoad

View File

@ -1,24 +0,0 @@
<script lang="ts">
import ArticleFooter from '$lib/components/articles/ArticlePreviewFooter/ArticlePreviewFooter.svelte'
import type { PageData } from './$types'
import { contentClass } from '$lib/styles/article/article.css'
import { onMount } from 'svelte'
import { runOnMountScripts } from '$lib/articleContent/onMountScripts'
export let data: PageData
onMount(() => {
runOnMountScripts()
})
</script>
<svelte:head>
<title>{data.title}</title>
</svelte:head>
<h1>{data.title}</h1>
<article class="content {contentClass}">
{@html data.body}
</article>
<ArticleFooter article={data} segment="blog" />

View File

@ -1,30 +0,0 @@
<script lang="ts">
import type { PageData } from './$types'
import ArticlePreviewList from '$lib/components/articles/ArticlePreviewList/ArticlePreviewList.svelte'
import { seeAllClass } from '$lib/components/articles/ArticlePreviewList/ArticlePreviewList.css'
export let data: PageData
$: ({ posts, filters } = data)
</script>
<svelte:head>
<title>Broadcasts @michalvankodev</title>
</svelte:head>
{#if posts.items.length === 0}
<p class="no-posts">You've found void in the space.</p>
{:else}
<h1>
{#if filters.tags}
<em>{filters.tags}</em>
{/if}
Broadcasts
</h1>
{#if filters.tags}
<div class={seeAllClass}>
<a href="/broadcasts">See all broadcasts</a>
</div>
{/if}
{/if}
<ArticlePreviewList {...data} segment="broadcasts" />

View File

@ -1,18 +0,0 @@
import { parseParams } from '$lib/pagination/dropTakeParams'
import type { PageLoad } from './$types'
import type { ArticlePreviewAttributes } from '$lib/articleContent/articleContentListing'
import type { PaginationResult } from '$lib/pagination/pagination'
export const load = (async ({ fetch, params }) => {
const { page = 1, pageSize = 7, ...filters } = parseParams(params.params)
const articleResponse = await fetch(
`/articles/segments/broadcasts${params.params ? `/${params.params}` : ''}.json`
).then((r) => r.json())
return {
posts: articleResponse.posts as PaginationResult<ArticlePreviewAttributes>,
page: Number(page),
pageSize,
filters,
}
}) satisfies PageLoad

View File

@ -1,9 +0,0 @@
import type { PageServerLoad } from './$types'
import { getArticleContent } from '$lib/articleContent/articleContent'
export const prerender = true
export const load = (async ({ params: { slug } }) => {
const post = await getArticleContent(slug);
return post
}) satisfies PageServerLoad

View File

@ -1,24 +0,0 @@
<script lang="ts">
import ArticleFooter from '$lib/components/articles/ArticlePreviewFooter/ArticlePreviewFooter.svelte'
import type { PageData } from './$types'
import { contentClass } from '$lib/styles/article/article.css'
import { onMount } from 'svelte'
import { runOnMountScripts } from '$lib/articleContent/onMountScripts'
export let data: PageData
onMount(() => {
runOnMountScripts()
})
</script>
<svelte:head>
<title>{data.title}</title>
</svelte:head>
<h1>{data.title}</h1>
<article class="content {contentClass}">
{@html data.body}
</article>
<ArticleFooter article={data} segment="broadcasts" />

View File

@ -1,13 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit'
import { getFeed } from '../feed'
export const prerender = true
export const GET = (async ({ setHeaders }) => {
const feed = await getFeed()
setHeaders({
'Content-Type': 'application/json',
'Cache-Control': 'max-age=86400',
})
return new Response(feed.json1())
}) satisfies RequestHandler

View File

@ -1,41 +0,0 @@
import { getBlogListing } from '$lib/articleContent/articleContentListing'
import { Feed } from 'feed'
export async function getFeed() {
const feed = new Feed({
title: 'michalvanko.dev latest posts',
id: 'https://michalvanko.dev',
link: 'https://michalvanko.dev',
description: 'Latest posts published on michalvanko.dev',
copyright: 'All rights reserved 2020, Michal Vanko',
generator: 'sapper with Feed for node.js',
updated: new Date(),
image: 'https://michalvanko.dev/eye.png',
favicon: 'https://michalvanko.dev/m-favicon-192x192.png',
language: 'en',
author: {
name: 'Michal Vanko',
email: 'michalvankosk@gmail.com',
link: 'https://michalvanko.dev',
},
feedLinks: {
json: 'https://michalvanko.dev/feed.json',
rss: 'https://michalvanko.dev/feed.xml',
},
})
const blogListing = await getBlogListing({})
blogListing.items.forEach((post) => {
feed.addItem({
title: post.title,
id: `https://michalvanko.dev/blog/${post.slug}`,
link: `https://michalvanko.dev/blog/${post.slug}`,
description: post.preview,
date: new Date(post.date),
image: post.thumbnail
? `https://michalvanko.dev/${post.thumbnail}`
: undefined,
})
})
return feed
}

View File

@ -1,13 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit'
import { getFeed } from '../feed'
export const prerender = true
export const GET = (async ({ setHeaders }) => {
const feed = await getFeed()
setHeaders({
'Content-Type': 'application/xml',
'Cache-Control': 'max-age=86400',
})
return new Response(feed.rss2())
}) satisfies RequestHandler

View File

@ -1,74 +0,0 @@
import { globalStyle, style } from '@vanilla-extract/css'
import { vars } from '$lib/styles/vars.css'
import { sprinkles } from '$lib/styles/sprinkles.css'
export const appContentClass = style([
sprinkles({
display: 'grid',
}),
{
gridTemplateRows: 'auto 1fr auto',
gridTemplateColumns: '100%',
},
])
export const mainContentClass = sprinkles({
position: 'relative',
padding: {
mobile: '3x',
desktop: 'none',
},
})
// Layout global styles
// atomic design needs to get rid off these global selectors LOL
// There should be written markdown renderer for each type of output
// where every component gets the layout atomic class
// TODO Create atomic classes for maxWidhts and use them everywhere in the content
globalStyle(
`${mainContentClass} h1, ${mainContentClass} h2, ${mainContentClass} h3, ${mainContentClass} h4, ${mainContentClass} h5, ${mainContentClass} h6, ${mainContentClass} p, ${mainContentClass} ul, ${mainContentClass} ol, ${mainContentClass} figure, ${mainContentClass} img, ${mainContentClass} blockquote, ${mainContentClass} iframe:not(.embed), ${mainContentClass} footer, ${mainContentClass} table`,
{
maxWidth: vars.width.layoutMax,
marginLeft: 'auto',
marginRight: 'auto',
}
)
globalStyle(`${mainContentClass} h1, ${mainContentClass} footer`, {
maxWidth: vars.width.headerFooterMax,
})
globalStyle(`${mainContentClass} h2`, {
maxWidth: vars.width.additionalBlockMax,
})
globalStyle(`${mainContentClass} iframe:not(.embed)`, {
maxWidth: vars.width.additionalBlockMax,
display: 'block',
})
globalStyle(`${mainContentClass} img`, {
maxWidth: vars.width.parent,
borderRadius: 5,
boxShadow: vars.boxShadow.contentBoxShadow,
})
globalStyle(`${mainContentClass} table`, {
maxWidth: vars.width.image,
fontSize: vars.fontSize.sm,
lineHeight: vars.lineHeight['3x'],
})
globalStyle(`${mainContentClass} figure`, {
maxWidth: vars.width.image,
})
globalStyle(
`${mainContentClass} pre, ${mainContentClass} pre[class*="language-"]`,
{
maxWidth: vars.width.additionalBlockMax,
marginLeft: 'auto',
marginRight: 'auto',
}
)

View File

@ -1,92 +0,0 @@
import { sprinkles } from '$lib/styles/sprinkles.css'
import {
transparent,
twitchEmbedBackground,
mediaAt,
breakpoints,
} from '$lib/styles/vars.css'
import { style } from '@vanilla-extract/css'
import { radialGradient, transparentize } from 'polished'
export const profilePicClass = sprinkles({
textAlign: 'center',
marginX: 'auto',
marginY: 'none',
})
export const profilePicImgClass = style({
aspectRatio: "auto 800 / 709",
maxHeight: '66vh'
})
export const mottoClass = sprinkles({
textAlign: 'center',
marginX: 'auto',
marginY: '2x',
fontSize: '2x',
})
export const welcomeNoteClass = sprinkles({
textAlign: 'center',
marginX: 'auto',
})
export const citeOwnerClass = sprinkles({
whiteSpace: 'nowrap',
})
export const twitchStreamPromoClass = style([
radialGradient({
colorStops: [
`${twitchEmbedBackground} 0%`,
`${transparentize(1, twitchEmbedBackground)} 60%`,
],
extent: '90% 40% at 50% 70%',
fallback: transparent,
}),
{
'@media': {
[mediaAt(breakpoints.l)]: radialGradient({
colorStops: [
`${twitchEmbedBackground} 0%`,
`${transparentize(1, twitchEmbedBackground)} 100%`,
],
extent: '180% 50% at 100% 50%',
fallback: transparent,
}),
},
},
])
export const twitchIframeClass = sprinkles({
flexGrow: 1,
maxWidth: 'image',
aspectRatio: 'monitor',
width: {
mobile: 'parent',
desktop: 'auto',
},
})
export const twitchEmbedClass = sprinkles({
display: 'flex',
padding: '3x',
justifyContent: 'center',
alignItems: 'center',
flexDirection: {
mobile: 'column',
desktop: 'row',
},
gap: '4x',
width: {
mobile: 'parent',
desktop: 'auto',
},
})
export const twitchAsideClass = sprinkles({
width: {
mobile: 'parent',
desktop: 's',
},
})

View File

@ -1,85 +0,0 @@
import { readFile } from 'fs'
import { promisify } from 'util'
import fm from 'front-matter'
// TODO Switch marked for unified
import marked from 'marked'
import { parseField } from '$lib/markdown/parse-markdown'
import type { PageServerLoad } from './$types'
export const prerender = true
export interface RecordAttributes {
name: string
description: string
displayed: boolean
}
export interface ProjectAttributes extends RecordAttributes {
image: {
source: string
image_description: string
}
}
export interface WorkAttributes extends RecordAttributes {
address: {
name: string
location: string
zipcode: string
city: string
country: string
}
}
export interface PresentationAttributes extends RecordAttributes {
link: string
}
export interface PortfolioAttributes {
title: string
work_history: WorkAttributes[]
work_history_prelude: string
projects: ProjectAttributes[]
education: RecordAttributes[]
presentations: PresentationAttributes[]
}
export type PortfolioContent = {
title: string
workHistory: WorkAttributes[]
workHistoryPrelude: string
projects: ProjectAttributes[]
education: RecordAttributes[]
presentations: PresentationAttributes[]
body: string
}
export const load = (async () => {
const pageSource = await promisify(readFile)('_pages/portfolio.md', 'utf-8')
const parsed = fm<PortfolioAttributes>(pageSource)
const workHistory = (parsed.attributes.work_history || [])
.filter((workHistory) => workHistory.displayed)
.map(parseField('description'))
const projects = (parsed.attributes.projects || [])
.filter((project) => project.displayed)
.map(parseField('description'))
const education = (parsed.attributes.education || [])
.filter((education) => education.displayed)
.map(parseField('description'))
const presentations = (parsed.attributes.presentations || []).filter(
(education) => education.displayed
)
const response: PortfolioContent = {
title: parsed.attributes.title,
body: marked(parsed.body),
workHistoryPrelude: marked(parsed.attributes.work_history_prelude),
workHistory,
projects,
education,
presentations,
}
return response
}) satisfies PageServerLoad

View File

@ -1,81 +0,0 @@
<script lang="ts">
import Work from './components/work.svelte'
import Project from './components/project.svelte'
import Presentation from './components/presentation.svelte'
import type { PageData } from './$types'
import { listClass, listItemClass, nameTagClass } from './page.css'
export let data: PageData
</script>
<svelte:head>
<title>{data.title}</title>
</svelte:head>
<h1 class="name-tag {nameTagClass}">Michal Vanko</h1>
<h2 class="name-tag {nameTagClass}">
Software Architect and Engineering Manager
</h2>
<section id="personal-information">
{@html data.body}
</section>
<section id="work-history">
<h2>Work experience</h2>
<section class="work-history-prelude">
{@html data.workHistoryPrelude}
</section>
<ul class={listClass}>
{#each data.workHistory as work}
<li class={listItemClass}>
<Work {work} />
</li>
{/each}
</ul>
</section>
<section id="projects">
<h2>Projects</h2>
<ul class={listClass}>
{#each data.projects as project}
<li class={listItemClass}>
<Project {project} />
</li>
{/each}
</ul>
</section>
<section id="presentations">
<h2>Presentations</h2>
<ul class="">
{#each data.presentations as presentation}
<li class="">
<Presentation {presentation} />
</li>
{/each}
</ul>
</section>
<section id="education">
<h2>Education</h2>
<ul class={listClass}>
{#each data.education as work}
<li class={listItemClass}>
<Work {work} />
</li>
{/each}
</ul>
</section>
<style>
:global([id])::before {
content: '';
display: block;
height: 5em;
margin-top: -5em;
visibility: hidden;
}
</style>

View File

@ -1,39 +0,0 @@
<section id="personal">
<h3>Personal Information</h3>
<p>I was born on 26th of May in Košice, Slovakia and I still live here.</p>
<h4>Hobbies:</h4>
<p>
I enjoy playing basketball with my friends. I also like to play other team
sports like football and hockey. I also play squash and table tennis. Once
I've won a competition in squash at my university. During summer I love
water skiing and swimming in a nearby lake.
<br />
I am very passionate about music. I've also tried some software for composing
music but I am not really hooked into that yet. From time to time I enjoy playing
board games with my friends.
</p>
<h4>Interests:</h4>
<p>
I like to explore new technologies and I'm passionate about <em
>Open Source movement</em
>,
<em>Internet of Things</em> applications and
<em>Linux desktop evolution</em>.
<br />
I am interested in modern software architecture and
<em>reactive programming</em>.
<br />
I've attended various <strong>tech conferences and hackathons</strong>. I
like them for all of the fascinating ideas that might be invented.
<br />
I've given presentations on various topics related to
<em>web development</em>. You can
<a href="#presentations">take a look at some of them here</a>.
<br />
I enjoy <strong>teaching and explaining</strong> how various technologies
and techniques work to my colleagues for their better understanding.
<br />
I take advantage of <strong>test driven development</strong>.
</p>
</section>
<!--/personal-->

View File

@ -1,18 +0,0 @@
import { sprinkles } from '$lib/styles/sprinkles.css'
export const presentationFrameClass = sprinkles({
width: 'image',
height: 'image',
})
export const presentationPreviewLinksClass = sprinkles({
fontSize: 'sm',
})
export const presentationDescriptionClass = sprinkles({
paddingLeft: '1x',
})
export const presentationNameClass = sprinkles({
fontSize: 'base',
})

View File

@ -1,41 +0,0 @@
<script lang="ts">
import type { PresentationAttributes } from 'src/routes/portfolio/index.json'
import {
presentationDescriptionClass,
presentationFrameClass,
presentationNameClass,
presentationPreviewLinksClass,
} from './presentation.css'
export let presentation: PresentationAttributes
export let previewVisible = false
function togglePreviewVisible() {
previewVisible = !previewVisible
}
</script>
<article>
<a href={presentation.link} target="_blank">
<h3 class={presentationNameClass}>{presentation.name}</h3>
</a>
<section class="description {presentationDescriptionClass}">
{@html presentation.description}
</section>
<section class="preview">
<div class={presentationPreviewLinksClass}>
<a href="#presentations" on:click|preventDefault={togglePreviewVisible}
>{previewVisible ? 'Close' : 'Open'} preview</a
>
</div>
{#if previewVisible}
<iframe
class={presentationFrameClass}
src={presentation.link}
title="Presentation of {presentation.name}"
/>
{/if}
</section>
</article>

View File

@ -1,10 +0,0 @@
import { globalStyle, style } from '@vanilla-extract/css'
export const projectScopeClass = style({})
globalStyle(`${projectScopeClass} img`, {
float: 'right',
width: '25%',
})
// We need to get rid off the global selectors LOL

View File

@ -1,21 +0,0 @@
<script lang="ts">
import type { ProjectAttributes } from '../../routes/portfolio/index.json'
import { projectScopeClass } from './project.css'
export let project: ProjectAttributes
</script>
<article class="project {projectScopeClass}">
<h3>{project.name}</h3>
<section class="description">
{#if project.image}
<img
src={project.image.source}
class="project-image"
alt={project.image.image_description}
/>
{/if}
{@html project.description}
</section>
<aside />
</article>

Some files were not shown because too many files have changed in this diff Show More