move to svelte kit

This commit is contained in:
2021-08-02 09:53:08 +02:00
parent d8e58997e4
commit b1540a7600
64 changed files with 1485 additions and 2895 deletions

52
src/lib/api/index.ts Normal file
View File

@@ -0,0 +1,52 @@
import axios from 'axios'
export const API = axios.create({
baseURL: import.meta.env.VITE_API_URL as string,
})
export function gql(s: TemplateStringsArray) {
return s.join('')
}
export async function Call<T>(query: string, variables: Record<string, any> = {}): Promise<T> {
const { data } = await API({
url: '/graphql',
method: 'post',
data: {
query,
variables,
},
})
return data.data as T
}
export type Page = {
title: string
content: string | null
slug: string
id: string
status: string
}
export interface Work extends Page {
work: {
date: string
image: MediaItem
link: string
role: string
}
}
export interface Project extends Page {
project: {
date: string
link: string
description: string
}
}
export type MediaItem = {
srcSet: string
altText: string
sourceUrl: string
}

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { onMount } from 'svelte'
export let icon: string
$: src = `/icons/${icon}.svg`
let html: string | null = null
onMount(async () => {
html = await fetch(src).then((res) => res.text())
})
</script>
{#if html === null}
<img {...$$restProps} {src} alt={icon} />
{:else}
<span {...$$restProps}>
{@html html}
</span>
{/if}
<style>
span,
img {
display: inline-block;
width: 1em;
height: 1em;
contain: strict;
box-sizing: content-box;
transform: translateY(0.2em);
}
span > :global(svg) {
display: block;
}
</style>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import Icon from './Icon.svelte'
type Link = {
href: string
name: string
icon: string
}
export let links: Link[] = []
function isExternal(link: string) {
return /^https?\:\/\//.test(link)
}
$: list = links.map((link) => ({
...link,
external: isExternal(link.href),
}))
</script>
<ul>
{#each list as { href, name, icon, external }}
<a rel={external ? 'noopener noreferrer' : ''} {href} target={external ? '_blank' : ''}>
<li>
<Icon class="icon" {icon} />
{name}
</li>
</a>
{/each}
</ul>
<style>
ul {
list-style: none;
margin: 0;
padding: 0;
}
a {
transition: transform 200ms ease;
padding: 0.75em 0.5em;
cursor: pointer;
border-radius: 0.5em;
display: block;
}
a:hover {
box-shadow: 0px 6px 6px -3px #00000012;
transform: translateY(0.25em) translateX(0.15em) scale(1.05);
}
a :global(.icon) {
transform: translateY(0.3em);
font-size: 2em;
margin-right: 0.5rem;
}
</style>

View File

@@ -0,0 +1,35 @@
<script lang="ts" context="module">
import { initialize } from 'svelte-cloudinary'
initialize({ cloud_name: 'cupcakearmy' })
</script>
<script lang="ts">
import { image } from 'svelte-cloudinary'
export let src: string
// export let srcset: string
export let alt: string
$: cleaned = src.replace('https://api.nicco.io', '/nicco')
</script>
<img use:image={{ src: cleaned, bind: { width: true }, lazy: true }} {alt} />
<!-- <img {srcset} {alt} /> -->
<style>
img {
width: calc(100% - 0.25em);
object-fit: cover;
object-position: center;
border: 0.125em solid var(--clr-primary);
transition: var(--animation);
transform: scale(1);
margin: 0;
}
img {
transform: scale(1.1);
margin: 1em 0;
}
</style>

View File

@@ -0,0 +1,122 @@
<script lang="ts">
import { page } from '$app/stores'
import Icon from './Icon.svelte'
const routes = [
{ name: 'About', href: '/about' },
{ name: 'Works', href: '/works' },
{ name: 'Projects', href: '/projects' },
{ name: 'Blog', href: '/blog' },
{ name: 'Contact', href: '/contact' },
]
let nav: HTMLDivElement
</script>
<nav bind:this={nav}>
<a href="/">
<h1 class:active={$page.path === '/'}>NB</h1>
</a>
<ul>
<li>
<a href="/search">
<Icon icon="search-outline" />
</a>
</li>
{#each routes as { name, href }}
<li>
<a {href}>
<span>{name}</span>
<div class:active={$page.path.startsWith(href)} />
</a>
</li>
{/each}
</ul>
</nav>
<style>
nav :global(*) {
box-sizing: initial;
}
nav {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
width: 3em;
height: 100%;
background-color: var(--clr-primary);
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
border-right: 0.1em solid var(--clr-secondary);
}
ul {
list-style: none;
margin: 0;
padding: 0;
max-height: 100%;
overflow: auto;
}
a {
writing-mode: vertical-rl;
padding: 1em;
text-decoration: none;
}
li a {
line-height: 1em;
width: 1em;
position: relative;
}
li a span {
z-index: 5;
position: relative;
}
li a div {
z-index: 4;
width: 0.125em;
height: 100%;
top: 0;
left: 1.12em;
position: absolute;
transition: all 500ms ease;
}
li a div.active {
background-color: var(--clr-secondary);
}
li:hover a div:not(.active) {
background-color: var(--clr-light);
}
h1 {
margin: 0;
writing-mode: horizontal-tb;
letter-spacing: -0.15em;
width: 1.15em;
font-size: 1.5em;
}
h1.active {
box-shadow: 0 0.1em var(--clr-secondary);
}
@media (max-width: 30em) {
nav {
width: 2.5em;
}
a {
padding: 0.5em;
}
li a div {
transform: translateX(-0.5em);
}
}
</style>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import SpacedLetters from './SpacedLetters.svelte'
export let title = ''
export let readable = false
</script>
<div>
<h1>
<SpacedLetters letters={title} {readable} />
</h1>
</div>
<style>
div {
margin-top: calc(28vh - 3em);
margin-bottom: 3em;
}
</style>

View File

@@ -0,0 +1,31 @@
<script>
import dj from 'dayjs'
import { readingTimeInMinutes } from '../lib/readingTime'
export let post
export let full = false
function format(date) {
return dj(date).format('MMM D, YYYY')
}
$: created = format(post.date)
$: modified = format(post.modified)
</script>
<style>
.attributes {
display: flex;
justify-content: space-between;
font-weight: 400;
margin-top: -0.125em;
}
</style>
<div class="attributes">
<div>
{created}
{#if full && created !== modified}<br /> <small>Last update: {modified}</small>{/if}
</div>
<div>~ {readingTimeInMinutes(post.content)} min</div>
</div>

View File

@@ -0,0 +1,53 @@
<script>
import ImageFrame from '../components/ImageFrame.svelte'
import PostAttributes from '../components/PostAttributes.svelte'
export let post
</script>
<style>
a {
display: block;
margin-bottom: 5em;
}
a > :global(img) {
height: 12em;
}
h2 {
margin-top: 0.25em;
position: relative;
top: 0;
transition: var(--animation);
background-color: var(--clr-light);
}
a:hover h2 {
top: -1em;
transform: scale(0.95);
}
a > :global(div) {
opacity: 1;
transition: var(--animation);
}
a:hover > :global(div) {
opacity: 0;
}
a.without {
border: 2px solid var(--clr-primary);
padding: 5%;
width: calc(100% + 10%);
transform: translateX(-5%);
}
</style>
<a href={`blog/${post.slug}`} class:without={!post.featured}>
{#if post.featured}
<ImageFrame src={post.featured.url} alt={post.featured.description} />
{/if}
<PostAttributes {post} />
<h2>
{@html post.title}
</h2>
</a>

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import { spring } from 'svelte/motion'
import { scroll } from '$lib/stores'
let el
const springed = spring(
{ scroll: 0 },
{
stiffness: 0.05,
damping: 0.7,
}
)
function updateState(value) {
const max = 359.99999
const R = 50
let alpha = (360 / 1) * value
alpha = Math.min(alpha, max)
const a = ((90 - alpha) * Math.PI) / 180
const x = R + R * Math.cos(a) * 2
const y = R - R * Math.sin(a) * 2
const center = alpha > 180 ? 1 : 0
const path = `M${R},${-50} A${R * 2},${R * 2},0,${center},1,${x},${y} L${R},${R} L${R},${-R}`
if (el) el.setAttribute('d', path)
}
$: springed.set({ scroll: $scroll })
$: updateState($springed.scroll)
</script>
<div>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" version="1.1">
<path bind:this={el} fill="var(--clr-secondary)" d="" />
</svg>
<span>{$scroll.toFixed(2)}</span>
</div>
<style>
div {
position: absolute;
bottom: 1em;
right: 1em;
pointer-events: none;
text-align: center;
}
span {
display: block;
font-size: 0.5em;
background-color: var(--clr-primary);
height: 1.5em;
}
svg {
border: 0.125em solid var(--clr-primary);
width: 2em;
height: 2em;
position: relative;
top: 0.45em;
}
</style>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import dayjs from 'dayjs'
import Icon from './Icon.svelte'
export let project: import('$lib/api').Project
</script>
<section>
<a href={project.project.link} target="_blank" rel="noopener">
<h2>{project.title}</h2>
</a>
<div class="subtitle">
<b>{project.project.description}</b>
<b class="date">{dayjs(project.project.date, 'X').format('MMM YY')}</b>
</div>
<p>
{@html project.content}
</p>
<div class="link">
<Icon icon="link-outline" />
<a rel="noopener noreferrer" target="_blank" href={project.project.link}
>{project.project.link.replace(/https?:\/\//, '')}</a
>
</div>
</section>
<style>
h2 {
font-size: 2em;
margin-bottom: 0.25em;
}
div.subtitle {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.date {
align-self: flex-end;
}
.link {
display: flex;
overflow: auto;
}
.link a {
margin-left: 0.5rem;
display: block;
}
section {
margin-bottom: 6em;
}
a {
font-family: monospace;
}
@media (max-width: 30em) {
div.subtitle {
flex-direction: column;
}
}
</style>

View File

@@ -0,0 +1,50 @@
<script>
export let result
const [type, slug] = result.ref.split('/')
let href = '/'
$: {
switch (type) {
case 'works':
case 'projects':
href = `${type}`
break
case 'post':
href = `/blog/${slug}`
break
case 'page':
href = `/${slug}`
break
}
}
</script>
<li>
<a {href}>
<h3>{slug.replace(/-/g, ' ')}</h3>
<span>{type}</span>
<code>Score: {result.score.toFixed(1)}</code>
</a>
</li>
<style>
h3 {
margin: 0;
margin-top: 2.5em;
margin-bottom: 0.2em;
text-transform: capitalize;
}
span {
display: inline-block;
padding: 0.1em 0.15em;
background-color: var(--clr-primary);
margin: 0;
line-height: 90%;
height: 1.25em;
}
code {
margin-left: 1em;
}
</style>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import PageTitle from './PageTitle.svelte'
export let title = ''
export let expanded = true
export let readable = false
</script>
<PageTitle {title} {readable} />
<section class:expanded>
<slot />
</section>
<style>
section {
max-width: 30em;
margin-bottom: 4em;
}
section.expanded {
margin-top: 5em;
}
</style>

View File

@@ -0,0 +1,36 @@
<script lang="ts">
export let letters: string = ''
export let even = false
export let readable = false
</script>
<div class:even class:readable>
{#if even}
{#each letters as letter}<span>{letter}</span>{/each}
{:else}{letters}{/if}
</div>
<style>
span {
width: 1em;
text-align: center;
display: inline-block;
}
div {
font-size: min(8vw, 2.5em);
text-transform: uppercase;
user-select: none;
letter-spacing: 0.35em;
}
div.even {
font-size: 8vw;
}
div.readable {
letter-spacing: initial;
text-transform: initial;
font-size: 2.25rem;
}
</style>

View File

@@ -0,0 +1,128 @@
<script lang="ts">
import 'highlight.js/styles/github.css'
import hljs from 'highlight.js/lib/core'
import javascript from 'highlight.js/lib/languages/javascript'
import python from 'highlight.js/lib/languages/python'
import yaml from 'highlight.js/lib/languages/yaml'
import json from 'highlight.js/lib/languages/json'
import bash from 'highlight.js/lib/languages/bash'
import docker from 'highlight.js/lib/languages/dockerfile'
import rust from 'highlight.js/lib/languages/rust'
import css from 'highlight.js/lib/languages/css'
import typescript from 'highlight.js/lib/languages/typescript'
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('python', python)
hljs.registerLanguage('yaml', yaml)
hljs.registerLanguage('json', json)
hljs.registerLanguage('bash', bash)
hljs.registerLanguage('docker', docker)
hljs.registerLanguage('rust', rust)
hljs.registerLanguage('css', css)
hljs.registerLanguage('typescript', typescript)
import { onMount } from 'svelte'
export let content: string
function encodeTextToUrl(text: string): string {
return text
.replace(/[^A-Za-z ]/, '')
.replace('/ +/', ' ')
.replace(' ', '-')
}
onMount(() => {
hljs.highlightAll()
const selector = [1, 2, 3, 4, 5, 6].map((i) => `div.adapter h${i}`).join(', ')
const elements = window.document.querySelectorAll(selector)
elements.forEach((el) => {
if (el.textContent) {
const hash = encodeTextToUrl(el.textContent)
el.innerHTML = `<a class="target-link" name="${hash}" href="${window.location.pathname}#${hash}">${el.innerHTML}</a>`
}
})
})
</script>
<div class="adapter">
{@html content}
</div>
<style>
div :global(.alignfull) {
width: calc(100vw - 6em);
margin-left: -2em;
}
div :global(.alignwide) {
width: calc(100% + 4em);
margin-left: -2em;
}
@media (max-width: 30em) {
div :global(.alignfull) {
width: calc(100vw - 4em);
margin-left: -1em;
}
div :global(.alignwide) {
width: calc(100% + 2em);
margin-left: -1em;
}
}
div :global(figure img) {
width: 100%;
height: 100%;
}
div :global(figure) {
margin: 2em 0;
}
div :global(figure figcaption) {
opacity: 0.75;
font-style: italic;
}
div :global(a) {
border-bottom: 0.125em solid var(--clr-primary);
}
div :global(pre) {
padding: 1em;
background: #0000000d;
overflow: auto;
}
div :global(code) {
background: #00000012;
padding: 0.25em;
font-size: 0.8em;
}
div :global(pre code) {
background: initial;
padding: initial;
-moz-tab-size: 2;
tab-size: 2;
}
div :global(h1),
div :global(h2),
div :global(h3),
div :global(h4),
div :global(h5),
div :global(h6) {
margin: 0;
margin-top: 3em;
border-left: 0.2rem solid var(--clr-primary);
padding-left: 0.5rem;
margin-left: -0.7rem;
}
div :global(.target-link) {
border-bottom: none;
}
div :global(p.has-background) {
padding: 0.5em;
}
</style>

View File

@@ -0,0 +1,65 @@
<script lang="ts">
import type { Work } from '$lib/api'
import dayjs from 'dayjs'
import ImageFrame from '$lib/components/ImageFrame.svelte'
import Icon from '$lib/components/Icon.svelte'
export let work: Work
</script>
<section>
<a href={work.work.link} target="_blank" rel="noopener">
<div class="horizontal">
<div class="title regular">{work.title}</div>
<div>
<Icon icon="link-outline" />
<span>{work.work.link.replace(/https?:\/\//, '')}</span>
</div>
</div>
<!-- <ImageFrame src={work.work.image.sizes.medium_large} alt={work.image.description} /> -->
<!-- <ImageFrame srcset={work.work.image.srcSet} alt={work.work.image.altText} /> -->
<ImageFrame src={work.work.image.sourceUrl} alt={work.work.image.altText} />
</a>
<div class="horizontal regular">
<div>{work.work.role}</div>
<div>{dayjs(work.work.date, 'X').format('MMM YY')}</div>
</div>
{#if work.content}
<p>
{@html work.content}
</p>
{/if}
</section>
<style>
.title {
font-size: 2em;
line-height: 1;
}
.horizontal {
display: flex;
justify-content: space-between;
align-items: end;
}
.regular {
font-weight: 400;
}
section {
margin-bottom: 6em;
}
a {
font-family: monospace;
}
@media (max-width: 30em) {
.horizontal {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View File

@@ -1,6 +0,0 @@
export function readingTimeInMinutes(text, options = {}) {
options = Object.assign({ wpm: 200 }, options)
const cleaned = text.replace(/(<.*?>)|(\\n)|(&#\d*?;)/g, '')
const words = cleaned.split(' ').length
return Math.round(words / options.wpm)
}

View File

@@ -1,50 +0,0 @@
import axios from 'axios'
const isDev = process.env.NODE_ENV !== 'production' && false
axios.defaults.baseURL = `${isDev ? 'http://localhost' : 'https://api.nicco.io'}/wp-json/wp/v2`
function normalize(post) {
return {
...post,
...post.acf,
id: post.id,
title: post.title.rendered,
content: post.content.rendered,
}
}
function combineUrlAndParams(url, params) {
const p = new URLSearchParams({
per_page: 100,
...params,
}).toString()
return `${url}?${p}`
}
export async function getOne(url, params = {}) {
const { data } = await axios(combineUrlAndParams(url, params))
if (!data.length) return null
else return normalize(data[0])
}
export async function getAll(url, params = {}) {
const { data, headers } = await axios(combineUrlAndParams(url, params))
const totalPages = parseInt(headers['x-wp-totalpages'])
const results = [...data]
if (totalPages > 1) {
for (let page = 2; page <= totalPages; page++) {
const { data } = await axios(combineUrlAndParams(url, { ...params, page }))
results.push(...data)
}
}
return results.map(normalize)
}
export function sortByDate(data) {
return data.sort((a, b) => parseInt(b.date) - parseInt(a.date))
}
export function respond(res, body) {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify(body))
}