feat: Add project

This commit is contained in:
mattcroat
2023-04-28 15:24:55 +02:00
commit 002f364996
41 changed files with 3489 additions and 0 deletions

160
src/app.css Normal file
View File

@ -0,0 +1,160 @@
@import '@fontsource/manrope';
@import '@fontsource/jetbrains-mono';
html {
/* font */
--font-sans: 'Manrope', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/* dark */
--brand-dark: var(--orange-3);
--text-1-dark: var(--gray-3);
--text-2-dark: var(--gray-5);
--surface-1-dark: var(--gray-12);
--surface-2-dark: var(--gray-11);
--surface-3-dark: var(--gray-10);
--surface-4-dark: var(--gray-9);
--background-dark: var(--gradient-8);
--border-dark: var(--gray-9);
/* light */
--brand-light: var(--orange-10);
--text-1-light: var(--gray-8);
--text-2-light: var(--gray-7);
--surface-1-light: var(--gray-0);
--surface-2-light: var(--gray-1);
--surface-3-light: var(--gray-2);
--surface-4-light: var(--gray-3);
--background-light: none;
--border-light: var(--gray-4);
}
:root {
color-scheme: dark;
--brand: var(--brand-dark);
--text-1: var(--text-1-dark);
--text-2: var(--text-2-dark);
--surface-1: var(--surface-1-dark);
--surface-2: var(--surface-2-dark);
--surface-3: var(--surface-3-dark);
--surface-4: var(--surface-4-dark);
--background: var(--background-dark);
--border: var(--border-dark);
}
@media (prefers-color-scheme: light) {
:root {
color-scheme: light;
--brand: var(--brand-light);
--text-1: var(--text-1-light);
--text-2: var(--text-2-light);
--surface-1: var(--surface-1-light);
--surface-2: var(--surface-2-light);
--surface-3: var(--surface-3-light);
--surface-4: var(--surface-4-light);
--background: var(--background-light);
--border: var(--border-light);
}
}
[color-scheme='dark'] {
color-scheme: dark;
--brand: var(--brand-dark);
--text-1: var(--text-1-dark);
--text-2: var(--text-2-dark);
--surface-1: var(--surface-1-dark);
--surface-2: var(--surface-2-dark);
--surface-3: var(--surface-3-dark);
--surface-4: var(--surface-4-dark);
--background: var(--background-dark);
--border: var(--border-dark);
}
[color-scheme='light'] {
color-scheme: light;
--brand: var(--brand-light);
--text-1: var(--text-1-light);
--text-2: var(--text-2-light);
--surface-1: var(--surface-1-light);
--surface-2: var(--surface-2-light);
--surface-3: var(--surface-3-light);
--surface-4: var(--surface-4-light);
--background: var(--background-light);
--border: var(--border-light);
}
html,
body {
height: 100%;
}
html {
color: var(--text-1);
accent-color: var(--link);
background-image: var(--background);
background-attachment: fixed;
}
img {
border-radius: var(--radius-3);
}
ul,
ol {
list-style: none;
padding: 0;
}
li {
padding-inline-start: 0;
}
.surface-1 {
background-color: var(--surface-1);
color: var(--text-2);
}
.surface-2 {
background-color: var(--surface-2);
color: var(--text-2);
}
.surface-3 {
background-color: var(--surface-3);
color: var(--text-1);
}
.surface-4 {
background-color: var(--surface-4);
color: var(--text-1);
}
.prose :is(h2, h3, h4, h5, h6) {
margin-top: var(--size-8);
margin-bottom: var(--size-3);
}
.prose p:not(:is(h2, h3, h4, h5, h6) + p) {
margin-top: var(--size-7);
}
.prose :is(ul, ol) {
list-style-type: '🔥';
padding-left: var(--size-5);
}
.prose :is(ul, ol) li {
margin-block: var(--size-2);
padding-inline-start: var(--size-2);
}
.prose pre {
max-inline-size: 100%;
padding: var(--size-3);
border-radius: 8px;
tab-size: 2;
}

12
src/app.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {}

21
src/app.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="icon" href="https://fav.farm/🔥" />
<link rel="alternate" type="application/atom+xml" href="/rss.xml" />
<script type="module">
const theme = localStorage.getItem('color-scheme')
theme
? document.documentElement.setAttribute('color-scheme', theme)
: localStorage.setItem('color-scheme', 'dark')
</script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -0,0 +1,6 @@
<script lang="ts">
export let src: string
export let alt: string
</script>
<img {src} {alt} loading="lazy" />

View File

@ -0,0 +1,3 @@
import img from './img.svelte'
export { img }

5
src/lib/config.ts Normal file
View File

@ -0,0 +1,5 @@
import { dev } from '$app/environment'
export const title = 'Shakespeare'
export const description = 'SvelteKit blog for poets'
export const url = dev ? 'http://localhost:5173/' : 'https://shakespeare.pages.dev'

23
src/lib/theme.ts Normal file
View File

@ -0,0 +1,23 @@
import { writable } from 'svelte/store'
import { browser } from '$app/environment'
type Theme = 'light' | 'dark'
const userTheme = browser && localStorage.getItem('color-scheme')
export const theme = writable(userTheme ?? 'dark')
export function toggleTheme() {
theme.update((currentTheme) => {
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'
document.documentElement.setAttribute('color-scheme', newTheme)
localStorage.setItem('color-scheme', newTheme)
return newTheme
})
}
export function setTheme(newTheme: Theme) {
theme.set(newTheme)
}

10
src/lib/types.ts Normal file
View File

@ -0,0 +1,10 @@
export type Categories = 'sveltekit' | 'svelte'
export type Post = {
title: string
slug: string
description: string
date: string
categories: Categories[]
published: boolean
}

6
src/lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
type DateStyle = Intl.DateTimeFormatOptions['dateStyle']
export function formatDate(date: string, dateStyle: DateStyle = 'medium', locales = 'en') {
const formatter = new Intl.DateTimeFormat(locales, { dateStyle })
return formatter.format(new Date(date))
}

6
src/mdsvex.svelte Normal file
View File

@ -0,0 +1,6 @@
<script context="module" lang="ts">
import { img } from '$lib/components/custom'
export { img }
</script>
<slot />

9
src/posts/counter.svelte Normal file
View File

@ -0,0 +1,9 @@
<script lang="ts">
let count = 0
const increment = () => (count += 1)
</script>
<button on:click={increment}>
{count}
</button>

19
src/posts/first-post.md Normal file
View File

@ -0,0 +1,19 @@
---
title: First post
description: First post.
date: '2023-4-14'
categories:
- sveltekit
- svelte
published: true
---
## Markdown
Hey friends! 👋
```ts
function greet(name: string) {
console.log(`Hey ${name}! 👋`)
}
```

23
src/posts/second-post.md Normal file
View File

@ -0,0 +1,23 @@
---
title: Second
description: Second post.
date: '2023-4-16'
categories:
- sveltekit
- svelte
published: true
---
<script>
import Counter from './counter.svelte'
</script>
## Svelte
Media inside the **static** folder is served from `/`.
![Svelte](favicon.png)
## Counter
<Counter />

15
src/routes/+error.svelte Normal file
View File

@ -0,0 +1,15 @@
<script>
import { page } from '$app/stores'
</script>
<div class="error">
<h1>{$page.status}: {$page.error?.message}</h1>
</div>
<style>
.error {
height: 100%;
display: grid;
place-content: center;
}
</style>

45
src/routes/+layout.svelte Normal file
View File

@ -0,0 +1,45 @@
<script lang="ts">
import Footer from './footer.svelte'
import Header from './header.svelte'
import PageTransition from './transition.svelte'
import 'open-props/style'
import 'open-props/normalize'
import 'open-props/buttons'
import '../app.css'
export let data
</script>
<div class="layout">
<Header />
<main>
<PageTransition url={data.url}>
<slot />
</PageTransition>
</main>
<Footer />
</div>
<style>
.layout {
height: 100%;
max-inline-size: 1440px;
display: grid;
grid-template-rows: auto 1fr auto;
margin-inline: auto;
padding-inline: var(--size-7);
}
main {
padding-block: var(--size-9);
}
@media (min-width: 1440px) {
.layout {
padding-inline: 0;
}
}
</style>

7
src/routes/+layout.ts Normal file
View File

@ -0,0 +1,7 @@
export const prerender = true
export async function load({ url }) {
return {
url: url.pathname
}
}

51
src/routes/+page.svelte Normal file
View File

@ -0,0 +1,51 @@
<script lang="ts">
import { formatDate } from '$lib/utils'
import * as config from '$lib/config'
export let data
</script>
<svelte:head>
<title>{config.title}</title>
</svelte:head>
<section>
<ul class="posts">
{#each data.posts as post}
<li class="post">
<a href={post.slug} class="title">{post.title}</a>
<p class="date">{formatDate(post.date)}</p>
<p class="description">{post.description}</p>
</li>
{/each}
</ul>
</section>
<style>
.posts {
display: grid;
gap: var(--size-7);
}
.post {
max-inline-size: var(--size-content-3);
}
.post:not(:last-child) {
border-bottom: 1px solid var(--border);
padding-bottom: var(--size-7);
}
.title {
font-size: var(--font-size-fluid-3);
text-transform: capitalize;
}
.date {
color: var(--text-2);
}
.description {
margin-top: var(--size-3);
}
</style>

7
src/routes/+page.ts Normal file
View File

@ -0,0 +1,7 @@
import type { Post } from '$lib/types'
export async function load({ fetch }) {
const response = await fetch('api/posts')
const posts: Post[] = await response.json()
return { posts }
}

View File

@ -0,0 +1,55 @@
<script lang="ts">
import { formatDate } from '$lib/utils'
export let data
</script>
<svelte:head>
<title>{data.meta.title}</title>
<meta property="og:type" content="article" />
<meta property="og:title" content={data.meta.title} />
</svelte:head>
<article>
<hgroup>
<h1>{data.meta.title}</h1>
<p>Published at {formatDate(data.meta.date)}</p>
</hgroup>
<div class="tags">
{#each data.meta.categories as category}
<span class="surface-4">&num;{category}</span>
{/each}
</div>
<div class="prose">
<svelte:component this={data.content} />
</div>
</article>
<style>
article {
max-inline-size: var(--size-content-3);
margin-inline: auto;
}
h1 {
text-transform: capitalize;
}
h1 + p {
margin-top: var(--size-2);
color: var(--text-2);
}
.tags {
display: flex;
gap: var(--size-3);
margin-top: var(--size-7);
}
.tags > * {
padding: var(--size-2) var(--size-3);
border-radius: var(--radius-round);
}
</style>

View File

@ -0,0 +1,14 @@
import { error } from '@sveltejs/kit'
export async function load({ params }) {
try {
const post = await import(`../../posts/${params.slug}.md`)
return {
content: post.default,
meta: post.metadata
}
} catch (e) {
throw error(404, `Could not find ${params.slug}`)
}
}

View File

@ -0,0 +1,2 @@
<h1>About</h1>
<p>I like long walks on the beach.</p>

View File

@ -0,0 +1,30 @@
import { json } from '@sveltejs/kit'
import type { Post } from '$lib/types'
async function getPosts() {
let posts: Post[] = []
const paths = import.meta.glob('/src/posts/*.md', { eager: true })
for (const path in paths) {
const file = paths[path]
const slug = path.split('/').at(-1)?.replace('.md', '')
if (file && typeof file === 'object' && 'metadata' in file && slug) {
const metadata = file.metadata as Omit<Post, 'slug'>
const post = { ...metadata, slug } satisfies Post
post.published && posts.push(post)
}
}
posts = posts.sort(
(first, second) => new Date(second.date).getTime() - new Date(first.date).getTime()
)
return posts
}
export async function GET() {
const posts = await getPosts()
return json(posts)
}

View File

@ -0,0 +1,2 @@
<h1>Contact</h1>
<p>New phone, who dis?</p>

18
src/routes/footer.svelte Normal file
View File

@ -0,0 +1,18 @@
<script lang="ts">
import * as config from '$lib/config'
</script>
<footer>
<p>{config.title} &copy {new Date().getFullYear()}</p>
</footer>
<style>
footer {
padding-block: var(--size-7);
border-top: 1px solid var(--border);
}
p {
color: var(--text-2);
}
</style>

52
src/routes/header.svelte Normal file
View File

@ -0,0 +1,52 @@
<script lang="ts">
import Toggle from './toggle.svelte'
import * as config from '$lib/config'
</script>
<nav>
<a href="/" class="title">
<b>{config.title}</b>
</a>
<ul class="links">
<li>
<a href="/about">About</a>
</li>
<li>
<a href="/contact">Contact</a>
</li>
<li>
<a href="/rss.xml" target="_blank">RSS</a>
</li>
</ul>
<Toggle />
</nav>
<style>
nav {
padding-block: var(--size-7);
}
.links {
margin-block: var(--size-7);
}
a {
color: inherit;
text-decoration: none;
}
@media (min-width: 768px) {
nav {
display: flex;
justify-content: space-between;
}
.links {
display: flex;
gap: var(--size-7);
margin-block: 0;
}
}
</style>

View File

@ -0,0 +1,37 @@
import * as config from '$lib/config'
import type { Post } from '$lib/types'
export const prerender = true
export async function GET({ fetch }) {
const response = await fetch('api/posts')
const posts: Post[] = await response.json()
const headers = { 'Content-Type': 'application/xml' }
const xml = `
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>${config.title}</title>
<description>${config.description}</description>
<link>${config.url}</link>
<atom:link href="${config.url}/rss.xml" rel="self" type="application/rss+xml"/>
${posts
.map(
(post) => `
<item>
<title>${post.title}</title>
<description>${post.description}</description>
<link>${config.url}/${post.slug}</link>
<guid isPermaLink="true">${config.url}/${post.slug}</guid>
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
</item>
`
)
.join('')}
</channel>
</rss>
`.trim()
return new Response(xml, { headers })
}

35
src/routes/toggle.svelte Normal file
View File

@ -0,0 +1,35 @@
<script lang="ts">
import { fly } from 'svelte/transition'
import { Moon, Sun } from 'lucide-svelte'
import { theme, toggleTheme } from '$lib/theme'
</script>
<button on:click={toggleTheme} aria-label="Toggle theme">
{#if $theme === 'dark'}
<div in:fly={{ y: 10 }}>
<Sun />
<span>Light</span>
</div>
{:else}
<div in:fly={{ y: -10 }}>
<Moon />
<span>Dark</span>
</div>
{/if}
</button>
<style>
button {
padding: 0;
font-weight: inherit;
background: none;
border: none;
box-shadow: none;
overflow: hidden;
}
button > * {
display: flex;
gap: var(--size-2);
}
</style>

View File

@ -0,0 +1,17 @@
<script lang="ts">
import { fade } from 'svelte/transition'
export let url: string
</script>
{#key url}
<div class="transition" in:fade>
<slot />
</div>
{/key}
<style>
.transition {
height: 100%;
}
</style>