remove old svelte web source
This commit is contained in:
		| @@ -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
									
									
								
							
							
						
						
									
										41
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,26 +1,25 @@ | ||||
| .DS_Store | ||||
| /node_modules/ | ||||
| /src/node_modules/@sapper/ | ||||
| yarn-error.log | ||||
| /cypress/screenshots/ | ||||
| /__sapper__/ | ||||
|  | ||||
| /.svelte-kit | ||||
| /.svelte/ | ||||
| /build/ | ||||
| /functions/ | ||||
| /static/build/ | ||||
|  | ||||
| #amplify | ||||
| amplify/\#current-cloud-backend | ||||
| amplify/.config/local-* | ||||
| amplify/backend/amplify-meta.json | ||||
| amplify/backend/awscloudformation | ||||
| # `dist` folder with the export of SSG | ||||
| dist/ | ||||
| node_modules/ | ||||
| aws-exports.js | ||||
| awsconfiguration.json | ||||
|  | ||||
| /static/**/optimized/ | ||||
| # 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/ | ||||
|   | ||||
							
								
								
									
										0
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										0
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
								
								
									
										18
									
								
								appveyor.yml
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								appveyor.yml
									
									
									
									
									
								
							| @@ -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 | ||||
							
								
								
									
										20
									
								
								axum_server/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								axum_server/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -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/ | ||||
| @@ -1,4 +0,0 @@ | ||||
| { | ||||
| 	"baseUrl": "http://localhost:3000", | ||||
| 	"video": false | ||||
| } | ||||
							
								
								
									
										7319
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7319
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										56
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								package.json
									
									
									
									
									
								
							| @@ -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" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										31
									
								
								src/app.html
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								src/app.html
									
									
									
									
									
								
							| @@ -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
									
									
								
							
							
						
						
									
										3
									
								
								src/global.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +0,0 @@ | ||||
| /// <reference types="@sveltejs/kit" /> | ||||
| /// <reference types="svelte" /> | ||||
| /// <reference types="vite/client" /> | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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-')) | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| declare global { | ||||
|   interface Window { | ||||
|     onMountScripts?: Array<() => void> | ||||
|   } | ||||
| } | ||||
| export function runOnMountScripts() { | ||||
|   window.onMountScripts?.forEach((fn) => { | ||||
|     fn() | ||||
|   }) | ||||
| } | ||||
|  | ||||
| @@ -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, | ||||
| }) | ||||
| @@ -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> | ||||
| @@ -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', | ||||
| }) | ||||
| @@ -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} | ||||
| @@ -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> | ||||
| @@ -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> | ||||
| @@ -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', | ||||
| }) | ||||
| @@ -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> | ||||
| @@ -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'], | ||||
| }) | ||||
| @@ -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> | ||||
| @@ -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', | ||||
| }) | ||||
| @@ -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)} | ||||
|         ><</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> | ||||
| @@ -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]) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
| @@ -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}` | ||||
| } | ||||
| @@ -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(',') | ||||
| } | ||||
| @@ -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]), | ||||
|   }) | ||||
| } | ||||
| @@ -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>` | ||||
|   }, | ||||
| } | ||||
| @@ -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, | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
| @@ -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, | ||||
|   } | ||||
| } | ||||
| @@ -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) | ||||
|     }) | ||||
|   }) | ||||
| }) | ||||
| @@ -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, | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -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, | ||||
| }) | ||||
| @@ -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, | ||||
| }) | ||||
| @@ -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) | ||||
|   )}`, | ||||
| }) | ||||
| @@ -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) | ||||
| @@ -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', | ||||
|   }, | ||||
| }) | ||||
| @@ -1,4 +0,0 @@ | ||||
| /** @type {import('@sveltejs/kit').ParamMatcher} */ | ||||
| export function match(param: string) { | ||||
|   return !['tags', 'page'].some((keyword) => param.startsWith(keyword)) | ||||
| } | ||||
| @@ -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> | ||||
| @@ -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 | ||||
| @@ -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 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> | ||||
| @@ -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 | ||||
| @@ -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" /> | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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" /> | ||||
| @@ -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" /> | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| @@ -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" /> | ||||
| @@ -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 | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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 | ||||
| @@ -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', | ||||
|   } | ||||
| ) | ||||
| @@ -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', | ||||
|   }, | ||||
| }) | ||||
| @@ -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 | ||||
| @@ -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> | ||||
| @@ -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--> | ||||
| @@ -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', | ||||
| }) | ||||
| @@ -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> | ||||
| @@ -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 | ||||
| @@ -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
		Reference in New Issue
	
	Block a user