mirror of
https://github.com/cupcakearmy/nicco.io.git
synced 2025-12-11 16:35:03 +00:00
move to svelte kit
This commit is contained in:
52
src/lib/api/index.ts
Normal file
52
src/lib/api/index.ts
Normal 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
|
||||
}
|
||||
35
src/lib/components/Icon.svelte
Normal file
35
src/lib/components/Icon.svelte
Normal 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>
|
||||
58
src/lib/components/IconList.svelte
Normal file
58
src/lib/components/IconList.svelte
Normal 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>
|
||||
35
src/lib/components/ImageFrame.svelte
Normal file
35
src/lib/components/ImageFrame.svelte
Normal 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>
|
||||
122
src/lib/components/Nav.svelte
Normal file
122
src/lib/components/Nav.svelte
Normal 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>
|
||||
19
src/lib/components/PageTitle.svelte
Normal file
19
src/lib/components/PageTitle.svelte
Normal 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>
|
||||
31
src/lib/components/PostAttributes.svelte
Normal file
31
src/lib/components/PostAttributes.svelte
Normal 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>
|
||||
53
src/lib/components/PostPreview.svelte
Normal file
53
src/lib/components/PostPreview.svelte
Normal 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>
|
||||
62
src/lib/components/Progress.svelte
Normal file
62
src/lib/components/Progress.svelte
Normal 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>
|
||||
69
src/lib/components/Project.svelte
Normal file
69
src/lib/components/Project.svelte
Normal 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>
|
||||
50
src/lib/components/SearchResult.svelte
Normal file
50
src/lib/components/SearchResult.svelte
Normal 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>
|
||||
24
src/lib/components/SimplePage.svelte
Normal file
24
src/lib/components/SimplePage.svelte
Normal 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>
|
||||
36
src/lib/components/SpacedLetters.svelte
Normal file
36
src/lib/components/SpacedLetters.svelte
Normal 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>
|
||||
128
src/lib/components/WPAdapter.svelte
Normal file
128
src/lib/components/WPAdapter.svelte
Normal 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>
|
||||
65
src/lib/components/Work.svelte
Normal file
65
src/lib/components/Work.svelte
Normal 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>
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user