From bfd03cda874bf34e3923a37cb824183a1c45677b Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 26 May 2025 06:44:27 -0700 Subject: [PATCH 01/65] First commit for universe --- CLAUDE.md | 66 +++++++ eslint.config.js | 12 +- package-lock.json | 122 +++++++++++++ package.json | 2 + src/app.d.ts | 2 +- src/assets/icons/labs.svg | 3 + src/assets/icons/universe.svg | 3 + src/assets/icons/work.svg | 3 + src/assets/styles/variables.scss | 15 ++ src/lib/components/Header.svelte | 52 ++++++ src/lib/components/Pill.svelte | 84 +++++++++ src/lib/components/PostContent.svelte | 170 ++++++++++++++++++ src/lib/components/PostItem.svelte | 79 ++++++++ src/lib/components/PostList.svelte | 24 +++ src/lib/components/SegmentedController.svelte | 27 +++ src/lib/posts.ts | 63 +++++++ .../quick-thought-about-design-systems.md | 8 + src/lib/posts/svg-animations-are-fun.md | 8 + .../the-perfect-todo-app-doesnt-exist.md | 48 +++++ src/lib/posts/welcome-to-my-blog.md | 33 ++++ src/routes/+layout.svelte | 3 + src/routes/+page.svelte | 7 +- src/routes/blog/+page.server.ts | 10 ++ src/routes/blog/+page.svelte | 34 ++++ src/routes/blog/[slug]/+page.server.ts | 15 ++ src/routes/blog/[slug]/+page.svelte | 18 ++ 26 files changed, 900 insertions(+), 11 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/assets/icons/labs.svg create mode 100644 src/assets/icons/universe.svg create mode 100644 src/assets/icons/work.svg create mode 100644 src/lib/components/Header.svelte create mode 100644 src/lib/components/Pill.svelte create mode 100644 src/lib/components/PostContent.svelte create mode 100644 src/lib/components/PostItem.svelte create mode 100644 src/lib/components/PostList.svelte create mode 100644 src/lib/components/SegmentedController.svelte create mode 100644 src/lib/posts.ts create mode 100644 src/lib/posts/quick-thought-about-design-systems.md create mode 100644 src/lib/posts/svg-animations-are-fun.md create mode 100644 src/lib/posts/the-perfect-todo-app-doesnt-exist.md create mode 100644 src/lib/posts/welcome-to-my-blog.md create mode 100644 src/routes/blog/+page.server.ts create mode 100644 src/routes/blog/+page.svelte create mode 100644 src/routes/blog/[slug]/+page.server.ts create mode 100644 src/routes/blog/[slug]/+page.svelte diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e3ccc8e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,66 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +**Start development server:** + +```bash +npm run dev +``` + +**Build for production:** + +```bash +npm run build +``` + +**Type checking and linting:** + +```bash +npm run check # Type check with svelte-check +npm run lint # Check formatting and linting +npm run format # Auto-format code with prettier +``` + +**Preview production build:** + +```bash +npm run preview +``` + +## Architecture Overview + +This is a SvelteKit personal portfolio site for @jedmund that integrates with multiple external APIs to display real-time data about music listening habits and gaming activity. + +### Key Architecture Components + +**API Integration Layer** (`src/routes/api/`) + +- **Redis caching**: Shared Redis client (`redis-client.ts`) used across API routes for caching external API responses +- **External APIs**: Last.fm (music), Steam (games), PSN (PlayStation games), Giant Bomb (game metadata) +- **Data enrichment**: Last.fm data is enhanced with iTunes artwork and Giant Bomb metadata + +**Frontend Structure** + +- **Component-based**: Reusable Svelte components in `$lib/components/` +- **Page composition**: Main page (`+page.svelte`) composed of multiple `Page` components with different content sections +- **Data loading**: Server-side data fetching in `+page.ts` with error handling + +**Styling System** + +- **SCSS-based**: Global variables, fonts, themes automatically imported via Vite config +- **Asset management**: SVG icons and illustrations with automatic processing and alias imports + +### Key Aliases (svelte.config.js) + +- `$components` → `src/lib/components` +- `$icons` → `src/assets/icons` +- `$illos` → `src/assets/illos` +- `$styles` → `src/styles` + +### Environment Dependencies + +- Requires `LASTFM_API_KEY` and `REDIS_URL` environment variables +- Uses Node.js adapter for deployment diff --git a/eslint.config.js b/eslint.config.js index a351fa9..5a7eb2c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,8 +1,8 @@ -import js from '@eslint/js'; -import ts from 'typescript-eslint'; -import svelte from 'eslint-plugin-svelte'; -import prettier from 'eslint-config-prettier'; -import globals from 'globals'; +import js from '@eslint/js' +import ts from 'typescript-eslint' +import svelte from 'eslint-plugin-svelte' +import prettier from 'eslint-config-prettier' +import globals from 'globals' /** @type {import('eslint').Linter.FlatConfig[]} */ export default [ @@ -30,4 +30,4 @@ export default [ { ignores: ['build/', '.svelte-kit/', 'dist/'] } -]; +] diff --git a/package-lock.json b/package-lock.json index 59bd17a..0cb1e0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,9 @@ "@types/steamapi": "^2.2.5", "dotenv": "^16.4.5", "giantbombing-api": "^1.0.4", + "gray-matter": "^4.0.3", "ioredis": "^5.4.1", + "marked": "^15.0.12", "node-itunes-search": "^1.2.3", "psn-api": "github:jedmund/psn-api", "redis": "^4.7.0", @@ -2471,6 +2473,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", @@ -2533,6 +2548,18 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -2881,6 +2908,43 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -3069,6 +3133,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3261,6 +3334,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -3348,6 +3430,18 @@ "@jridgewell/sourcemap-codec": "^1.4.15" } }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", @@ -4196,6 +4290,19 @@ "node": ">=14.0.0" } }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", @@ -4288,6 +4395,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -4421,6 +4534,15 @@ "node": ">=8" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", diff --git a/package.json b/package.json index 192950c..1757ed8 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,9 @@ "@types/steamapi": "^2.2.5", "dotenv": "^16.4.5", "giantbombing-api": "^1.0.4", + "gray-matter": "^4.0.3", "ioredis": "^5.4.1", + "marked": "^15.0.12", "node-itunes-search": "^1.2.3", "psn-api": "github:jedmund/psn-api", "redis": "^4.7.0", diff --git a/src/app.d.ts b/src/app.d.ts index 743f07b..f70d0e1 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -10,4 +10,4 @@ declare global { } } -export {}; +export {} diff --git a/src/assets/icons/labs.svg b/src/assets/icons/labs.svg new file mode 100644 index 0000000..83b42fb --- /dev/null +++ b/src/assets/icons/labs.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/universe.svg b/src/assets/icons/universe.svg new file mode 100644 index 0000000..66bff28 --- /dev/null +++ b/src/assets/icons/universe.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/work.svg b/src/assets/icons/work.svg new file mode 100644 index 0000000..ce09cba --- /dev/null +++ b/src/assets/icons/work.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/styles/variables.scss b/src/assets/styles/variables.scss index 5c5f680..1dd4be8 100644 --- a/src/assets/styles/variables.scss +++ b/src/assets/styles/variables.scss @@ -25,6 +25,7 @@ $unit-20x: $unit * 20; /* Page properties * -------------------------------------------------------------------------- */ $page-corner-radius: $unit; +$card-corner-radius: $unit-3x; $page-top-margin: $unit-6x; @@ -86,6 +87,20 @@ $grey-color: #f0f0f0; $image-border-color: rgba(0, 0, 0, 0.03); +/* Shadows + * -------------------------------------------------------------------------- */ +$card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); +$card-shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.12); + +/* Pill colors + * -------------------------------------------------------------------------- */ +$work-bg: #ffcdc5; +$work-color: #d0290d; +$universe-bg: #ffebc5; +$universe-color: #b97d14; +$labs-bg: #c5eaff; +$labs-color: #1482c1; + $facebook-color: #3b5998; $twitter-color: #55acee; $instagram-color: #3f729b; diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte new file mode 100644 index 0000000..290da4b --- /dev/null +++ b/src/lib/components/Header.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/src/lib/components/Pill.svelte b/src/lib/components/Pill.svelte new file mode 100644 index 0000000..48edfde --- /dev/null +++ b/src/lib/components/Pill.svelte @@ -0,0 +1,84 @@ + + + + + {text} + + + diff --git a/src/lib/components/PostContent.svelte b/src/lib/components/PostContent.svelte new file mode 100644 index 0000000..919d66e --- /dev/null +++ b/src/lib/components/PostContent.svelte @@ -0,0 +1,170 @@ + + +
+
+ {#if post.title} +

{post.title}

+ {/if} + +
+ +
+ {@html post.content} +
+ + +
+ + diff --git a/src/lib/components/PostItem.svelte b/src/lib/components/PostItem.svelte new file mode 100644 index 0000000..cfd42aa --- /dev/null +++ b/src/lib/components/PostItem.svelte @@ -0,0 +1,79 @@ + + +
+ + {#if post.title} +

{post.title}

+ {/if} +

{post.excerpt}

+ +
+
+ + diff --git a/src/lib/components/PostList.svelte b/src/lib/components/PostList.svelte new file mode 100644 index 0000000..f4e947e --- /dev/null +++ b/src/lib/components/PostList.svelte @@ -0,0 +1,24 @@ + + +
+ {#if posts && posts.length > 0} + {#each posts as post} + + {/each} + {:else} +

No posts found.

+ {/if} +
+ + diff --git a/src/lib/components/SegmentedController.svelte b/src/lib/components/SegmentedController.svelte new file mode 100644 index 0000000..535e33c --- /dev/null +++ b/src/lib/components/SegmentedController.svelte @@ -0,0 +1,27 @@ + + + + + diff --git a/src/lib/posts.ts b/src/lib/posts.ts new file mode 100644 index 0000000..578eceb --- /dev/null +++ b/src/lib/posts.ts @@ -0,0 +1,63 @@ +import fs from 'fs' +import path from 'path' +import matter from 'gray-matter' +import { marked } from 'marked' + +export interface Post { + title?: string + type: 'note' | 'article' | 'image' + date: string + slug: string + published: boolean + content: string + excerpt?: string + images?: string[] +} + +const postsDirectory = path.join(process.cwd(), 'src/lib/posts') + +export async function getAllPosts(): Promise { + const fileNames = fs.readdirSync(postsDirectory) + + const posts = fileNames + .filter((fileName) => fileName.endsWith('.md')) + .map((fileName) => { + const filePath = path.join(postsDirectory, fileName) + const fileContents = fs.readFileSync(filePath, 'utf8') + const { data, content } = matter(fileContents) + + const slug = data.slug || fileName.replace(/\.md$/, '') + + return { + ...data, + slug, + content, + excerpt: getExcerpt(content, data.type) + } as Post + }) + .filter((post) => post.published) + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + + return posts +} + +export async function getPostBySlug(slug: string): Promise { + const posts = await getAllPosts() + const post = posts.find((p) => p.slug === slug) + + if (!post) return null + + return { + ...post, + content: marked(post.content) as string + } +} + +function getExcerpt(content: string, type: 'note' | 'article'): string { + const plainText = content.replace(/[#*`\[\]]/g, '').trim() + const maxLength = type === 'note' ? 280 : 160 + + if (plainText.length <= maxLength) return plainText + + return plainText.substring(0, maxLength).trim() + '...' +} diff --git a/src/lib/posts/quick-thought-about-design-systems.md b/src/lib/posts/quick-thought-about-design-systems.md new file mode 100644 index 0000000..7a9f474 --- /dev/null +++ b/src/lib/posts/quick-thought-about-design-systems.md @@ -0,0 +1,8 @@ +--- +type: 'note' +date: '2024-01-16T14:20:00Z' +slug: 'quick-thought-about-design-systems' +published: true +--- + +Been thinking about how the best design systems aren't the ones with the most components, but the ones with the clearest principles. You can have a thousand perfectly crafted components, but if your team doesn't understand the _why_ behind them, you've just built a very pretty prison. diff --git a/src/lib/posts/svg-animations-are-fun.md b/src/lib/posts/svg-animations-are-fun.md new file mode 100644 index 0000000..70fd4f5 --- /dev/null +++ b/src/lib/posts/svg-animations-are-fun.md @@ -0,0 +1,8 @@ +--- +type: 'note' +date: '2024-01-20T16:45:00Z' +slug: 'svg-animations-are-fun' +published: true +--- + +Just spent an hour making a squiggly line animation for my site. Could I have shipped three features in that time? Yes. Did the squiggly line spark more joy? Also yes. Not everything needs to be optimized for productivity. diff --git a/src/lib/posts/the-perfect-todo-app-doesnt-exist.md b/src/lib/posts/the-perfect-todo-app-doesnt-exist.md new file mode 100644 index 0000000..7f68bad --- /dev/null +++ b/src/lib/posts/the-perfect-todo-app-doesnt-exist.md @@ -0,0 +1,48 @@ +--- +title: "The Perfect Todo App Doesn't Exist" +type: 'article' +date: '2024-01-18T09:00:00Z' +slug: 'the-perfect-todo-app-doesnt-exist' +published: true +--- + +I've tried them all. Things, Todoist, Notion, Apple Reminders, pen and paper, sticky notes on my monitor. Each promises to be the solution to my productivity woes, and each eventually joins the graveyard of abandoned systems. + +## The Cycle + +It always starts the same way: + +1. Discover new todo app +2. Feel rush of organizational dopamine +3. Migrate all tasks with excessive enthusiasm +4. Use religiously for 2-3 weeks +5. Gradually abandon as real life intrudes +6. Feel guilty about abandoned system +7. Return to step 1 + +## The Problem Isn't the Tool + +After years of this cycle, I've realized something: the perfect todo app doesn't exist because the problem isn't the app. It's the expectation that any tool can magically transform us into productivity machines. + +Real productivity isn't about finding the perfect system. It's about: + +- Saying no to things that don't matter +- Being realistic about what you can accomplish +- Accepting that some days you'll get nothing done +- Understanding that busy isn't the same as productive + +## What Works (For Me) + +These days, I use a simple text file. One for today, one for this week, one for "someday maybe." No tags, no priorities, no due dates unless absolutely necessary. Just a list of things I'd like to do. + +Some get done. Some don't. The world keeps spinning. + +## The Real Secret + +The perfect todo app doesn't exist because perfection doesn't exist. The best system is the one you'll actually use, even if it's just a piece of paper or a note on your phone. + +Stop optimizing your system. Start doing the work. + +--- + +_What's your relationship with todo apps? Have you found something that works, or are you still searching for the perfect system?_ diff --git a/src/lib/posts/welcome-to-my-blog.md b/src/lib/posts/welcome-to-my-blog.md new file mode 100644 index 0000000..234cdd9 --- /dev/null +++ b/src/lib/posts/welcome-to-my-blog.md @@ -0,0 +1,33 @@ +--- +title: 'Welcome to My Blog' +type: 'article' +date: '2024-01-15T10:30:00Z' +slug: 'welcome-to-my-blog' +published: true +--- + +After years of sharing my thoughts across various social platforms, I've decided to bring everything home. This blog will be a space for both quick notes and longer-form thoughts about design, development, and whatever else catches my interest. + +## Why Now? + +The internet feels different these days. Social media platforms come and go, APIs change, and our content gets scattered across dozens of services. I wanted a simple, permanent place for my writing that I control completely. + +## What to Expect + +You'll find a mix of content here: + +- **Notes**: Quick thoughts, observations, and links I find interesting +- **Articles**: Deeper dives into topics I'm passionate about +- **Updates**: What I'm working on and thinking about + +The design is intentionally minimal. No comments, no likes, no algorithms—just words on a page, the way blogs used to be. + +## Technical Details + +This blog is built with SvelteKit and uses markdown files for storage. It's fast, simple, and exactly what I need. No database, no CMS, just files in a folder. + +Feel free to view source if you're curious about the implementation. Everything is open and straightforward. + +--- + +_Thanks for reading. Here's to owning our own words._ diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 64dc3e3..8aa5967 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,4 +1,5 @@ @@ -11,6 +12,8 @@ user-scalable=no" /> +
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 897ddd5..e3e531b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,6 +1,5 @@ + + + Blog - jedmund + + + +
+ +
+ + diff --git a/src/routes/blog/[slug]/+page.server.ts b/src/routes/blog/[slug]/+page.server.ts new file mode 100644 index 0000000..46de106 --- /dev/null +++ b/src/routes/blog/[slug]/+page.server.ts @@ -0,0 +1,15 @@ +import { getPostBySlug } from '$lib/posts' +import { error } from '@sveltejs/kit' +import type { PageServerLoad } from './$types' + +export const load: PageServerLoad = async ({ params }) => { + const post = await getPostBySlug(params.slug) + + if (!post) { + throw error(404, 'Post not found') + } + + return { + post + } +} diff --git a/src/routes/blog/[slug]/+page.svelte b/src/routes/blog/[slug]/+page.svelte new file mode 100644 index 0000000..df38dac --- /dev/null +++ b/src/routes/blog/[slug]/+page.svelte @@ -0,0 +1,18 @@ + + + + {pageTitle} - jedmund + + + + + + From f970913cc844fd6ea0f25352c33322438e1a9c88 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 26 May 2025 06:51:28 -0700 Subject: [PATCH 02/65] Image posts and slideshows --- src/lib/components/ImagePost.svelte | 133 ++++++++++++++++++++++ src/lib/components/PostContent.svelte | 13 +++ src/lib/components/PostItem.svelte | 89 +++++++++++---- src/lib/posts/beautiful-sunset-gallery.md | 15 +++ src/lib/posts/minimalist-workspace.md | 10 ++ 5 files changed, 237 insertions(+), 23 deletions(-) create mode 100644 src/lib/components/ImagePost.svelte create mode 100644 src/lib/posts/beautiful-sunset-gallery.md create mode 100644 src/lib/posts/minimalist-workspace.md diff --git a/src/lib/components/ImagePost.svelte b/src/lib/components/ImagePost.svelte new file mode 100644 index 0000000..d50bedf --- /dev/null +++ b/src/lib/components/ImagePost.svelte @@ -0,0 +1,133 @@ + + +{#if images.length === 1} + +
+ +
+{:else if images.length > 1} + +
+
+ {alt} {selectedIndex + 1} +
+
+ {#each images as image, index} + + {/each} +
+
+{/if} + + \ No newline at end of file diff --git a/src/lib/components/PostContent.svelte b/src/lib/components/PostContent.svelte index 919d66e..678afd8 100644 --- a/src/lib/components/PostContent.svelte +++ b/src/lib/components/PostContent.svelte @@ -1,5 +1,6 @@ diff --git a/src/lib/posts/beautiful-sunset-gallery.md b/src/lib/posts/beautiful-sunset-gallery.md new file mode 100644 index 0000000..c093c1e --- /dev/null +++ b/src/lib/posts/beautiful-sunset-gallery.md @@ -0,0 +1,15 @@ +--- +title: "Beautiful Sunset Gallery" +type: "image" +date: "2024-01-19T18:30:00Z" +slug: "beautiful-sunset-gallery" +published: true +images: + - "https://images.unsplash.com/photo-1495616811223-4d98c6e9c869?w=800&h=600&fit=crop" + - "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=800&h=600&fit=crop" + - "https://images.unsplash.com/photo-1470252649378-9c29740c9fa8?w=800&h=600&fit=crop" + - "https://images.unsplash.com/photo-1475924156734-496f6cac6ec1?w=800&h=600&fit=crop" + - "https://images.unsplash.com/photo-1472214103451-9374bd1c798e?w=800&h=600&fit=crop" +--- + +Caught these stunning sunsets during my recent travels. Each one tells its own story of endings and beginnings. \ No newline at end of file diff --git a/src/lib/posts/minimalist-workspace.md b/src/lib/posts/minimalist-workspace.md new file mode 100644 index 0000000..f1030a7 --- /dev/null +++ b/src/lib/posts/minimalist-workspace.md @@ -0,0 +1,10 @@ +--- +type: "image" +date: "2024-01-17T10:00:00Z" +slug: "minimalist-workspace" +published: true +images: + - "https://images.unsplash.com/photo-1555212697-194d092e3b8f?w=800&h=600&fit=crop" +--- + +My workspace this morning. Sometimes less really is more. \ No newline at end of file From 4a24fbd3b7e34717903be4cd8b7d9f92599ad10b Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 26 May 2025 11:53:36 -0700 Subject: [PATCH 03/65] images and slideshows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lightbox still doesn’t close when clicking background --- src/assets/styles/variables.scss | 1 + src/lib/components/ImagePost.svelte | 123 +++++++++---- src/lib/components/Lightbox.svelte | 266 ++++++++++++++++++++++++++++ 3 files changed, 360 insertions(+), 30 deletions(-) create mode 100644 src/lib/components/Lightbox.svelte diff --git a/src/assets/styles/variables.scss b/src/assets/styles/variables.scss index 1dd4be8..0bddff8 100644 --- a/src/assets/styles/variables.scss +++ b/src/assets/styles/variables.scss @@ -25,6 +25,7 @@ $unit-20x: $unit * 20; /* Page properties * -------------------------------------------------------------------------- */ $page-corner-radius: $unit; +$image-corner-radius: $unit-2x; $card-corner-radius: $unit-3x; $page-top-margin: $unit-6x; diff --git a/src/lib/components/ImagePost.svelte b/src/lib/components/ImagePost.svelte index d50bedf..1c4e1e9 100644 --- a/src/lib/components/ImagePost.svelte +++ b/src/lib/components/ImagePost.svelte @@ -1,4 +1,6 @@ {#if images.length === 1} -
+
+ {:else if images.length > 1}
-
+
+
- {#each images as image, index} - + {#each Array(totalSlots) as _, index} + {#if index < images.length} + + {:else} + + {/if} {/each}
{/if} + + \ No newline at end of file + diff --git a/src/lib/components/Lightbox.svelte b/src/lib/components/Lightbox.svelte new file mode 100644 index 0000000..3159949 --- /dev/null +++ b/src/lib/components/Lightbox.svelte @@ -0,0 +1,266 @@ + + +{#if isOpen} + +{/if} + + \ No newline at end of file From 49bde18f272612187997f2129da7417901e7e472 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 26 May 2025 12:34:59 -0700 Subject: [PATCH 04/65] Add link posts, fix layout --- src/lib/components/Header.svelte | 3 +- src/lib/components/LinkCard.svelte | 248 ++++++++++++++++++ src/lib/components/Page.svelte | 4 + src/lib/components/PostContent.svelte | 16 +- src/lib/components/PostItem.svelte | 53 +++- src/lib/components/SegmentedController.svelte | 2 +- src/lib/posts.ts | 10 +- src/lib/posts/auto-metadata-link.md | 9 + src/lib/posts/interesting-article.md | 14 + src/lib/posts/typographica-announcement.md | 9 + src/routes/+layout.svelte | 5 + src/routes/+page.svelte | 2 +- src/routes/api/og-metadata/+server.ts | 111 ++++++++ src/routes/{blog => universe}/+page.server.ts | 0 src/routes/{blog => universe}/+page.svelte | 2 +- .../{blog => universe}/[slug]/+page.server.ts | 0 .../{blog => universe}/[slug]/+page.svelte | 0 17 files changed, 469 insertions(+), 19 deletions(-) create mode 100644 src/lib/components/LinkCard.svelte create mode 100644 src/lib/posts/auto-metadata-link.md create mode 100644 src/lib/posts/interesting-article.md create mode 100644 src/lib/posts/typographica-announcement.md create mode 100644 src/routes/api/og-metadata/+server.ts rename src/routes/{blog => universe}/+page.server.ts (100%) rename src/routes/{blog => universe}/+page.svelte (96%) rename src/routes/{blog => universe}/[slug]/+page.server.ts (100%) rename src/routes/{blog => universe}/[slug]/+page.svelte (100%) diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 290da4b..e7ff69d 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -16,8 +16,7 @@ .site-header { display: flex; justify-content: center; - padding: $unit-4x 0; - margin-bottom: $unit-2x; + padding: $unit-5x 0; } .header-content { diff --git a/src/lib/components/LinkCard.svelte b/src/lib/components/LinkCard.svelte new file mode 100644 index 0000000..5b7a997 --- /dev/null +++ b/src/lib/components/LinkCard.svelte @@ -0,0 +1,248 @@ + + +{#if loading} + +{:else if error} + +{:else if metadata} + +{/if} + + diff --git a/src/lib/components/Page.svelte b/src/lib/components/Page.svelte index 216cad8..c3390cb 100644 --- a/src/lib/components/Page.svelte +++ b/src/lib/components/Page.svelte @@ -22,6 +22,10 @@ padding: $unit-5x; max-width: 784px; + &:first-child { + margin-top: 0; + } + &.no-horizontal-padding { padding-left: 0; padding-right: 0; diff --git a/src/lib/components/PostContent.svelte b/src/lib/components/PostContent.svelte index 678afd8..c775363 100644 --- a/src/lib/components/PostContent.svelte +++ b/src/lib/components/PostContent.svelte @@ -1,6 +1,7 @@ -
- -

{name}

-

{description}

+
+ +
+

{@html highlightedDescription}

+
diff --git a/src/lib/components/ProjectList.svelte b/src/lib/components/ProjectList.svelte index acfafc3..2a5493d 100644 --- a/src/lib/components/ProjectList.svelte +++ b/src/lib/components/ProjectList.svelte @@ -12,6 +12,7 @@ backgroundColor: string name: string description: string + highlightColor: string } const projects: Project[] = [ @@ -19,58 +20,95 @@ SVGComponent: MaitsuLogo, backgroundColor: '#FFF7EA', name: 'Maitsu', - description: "I'm building a hobby journal that helps people make something new every week." + description: "Maitsu is a hobby journal that helps people make something new every week.", + highlightColor: '#F77754' }, { SVGComponent: SlackLogo, backgroundColor: '#4a154b', name: 'Slack', description: - 'I led design for Workflows and other consumer-facing automation features at Slack.' + 'At Slack, I helped redefine strategy for Workflows and other features in under the automation umbrella.', + highlightColor: '#611F69' }, { SVGComponent: FigmaLogo, backgroundColor: '#2c2c2c', name: 'Figma', - description: 'I helped lead and set the direction for the early prototyping team at Figma.' + description: 'At Figma, I designed features and led R&D and strategy for the nascent prototyping team.', + highlightColor: '#0ACF83' }, { SVGComponent: PinterestLogo, backgroundColor: '#f7f7f7', name: 'Pinterest', description: - 'As the first design hire at Pinterest, I helped define product direction and grow the design team.' + 'At Pinterest, I was the first product design hired, and touched almost every part of the product.', + highlightColor: '#CB1F27' } ]
    - {#each projects as project} +
  • +
    +

    + @jedmund is a software designer and strategist based out of San Francisco. +

    +

    + In his 15 year career, he's focused his design practice on building tools that help people connect with technology—and their own creativity. +

    +
    +
  • + {#each projects as project, index}
  • - +
  • {/each}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 97ae689..e8b87b2 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,152 +1,5 @@ - - -

@jedmund is a software designer

-
- - -
- - - -

A little about me

-
- -
-

- Hello! My name is Justin Edmund. I'm a software designer and developer living in San - Francisco. -

-

- Right now, I'm spending my free time building a hobby journaling app called Maitsu. I've spent time at several companies over the last 11 years, but you might know me from - Pinterest, where I was the first - design hire. -

-

- I was born and raised in New York City and spend a lot of time in Tokyo. I graduated from Carnegie Mellon University in 2011 with a Bachelors of Arts in Communication Design. -

-

- I occasionally write about design and development on my blog. -

-
-
- - -

Notable mentions

-
- - -
- - -

Now playing

-
- - - - -
- -
-

© 2024 Justin Edmund

-
- - + diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte new file mode 100644 index 0000000..b5b1f5b --- /dev/null +++ b/src/routes/about/+page.svelte @@ -0,0 +1,115 @@ + + + + +

A little about me

+
+ +
+

+ Hello! My name is Justin Edmund. I'm a software designer and developer living in San + Francisco. +

+

+ Right now, I'm spending my free time building a hobby journaling app called Maitsu. I've spent time at several companies over the last 11 years, but you might know me from + Pinterest, where I was the first + design hire. +

+

+ I was born and raised in New York City and spend a lot of time in Tokyo. I graduated from Carnegie Mellon University in 2011 with a Bachelors of Arts in Communication Design. +

+

+ I occasionally write about design and development on my blog. +

+
+
+ + +

Notable mentions

+
+ + +
+ + +

Now playing

+
+ + + + +
+ +
+

© 2024 Justin Edmund

+
+ + \ No newline at end of file diff --git a/src/routes/about/+page.ts b/src/routes/about/+page.ts new file mode 100644 index 0000000..feeb82c --- /dev/null +++ b/src/routes/about/+page.ts @@ -0,0 +1,28 @@ +import type { PageLoad } from './$types' +import type { Album } from '$lib/types/lastfm' + +export const load: PageLoad = async ({ fetch }) => { + try { + const [albums] = await Promise.all([ + fetchRecentAlbums(fetch) + ]) + + return { + albums + } + } catch (err) { + console.error('Error fetching data:', err) + return { + albums: [], + games: [], + error: err instanceof Error ? err.message : 'An unknown error occurred' + } + } +} + +async function fetchRecentAlbums(fetch: typeof window.fetch): Promise { + const response = await fetch('/api/lastfm') + if (!response.ok) throw new Error(`Failed to fetch albums: ${response.status}`) + const musicData: { albums: Album[] } = await response.json() + return musicData.albums +} \ No newline at end of file From 6ebe2d59dc25cc0531c42d6856d78571256daa7d Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 26 May 2025 13:30:42 -0700 Subject: [PATCH 06/65] Adds footer --- src/lib/components/Footer.svelte | 74 ++++++++++++++++++++++++++++++++ src/routes/+layout.svelte | 3 ++ src/routes/about/+page.svelte | 10 ----- 3 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 src/lib/components/Footer.svelte diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte new file mode 100644 index 0000000..686b27b --- /dev/null +++ b/src/lib/components/Footer.svelte @@ -0,0 +1,74 @@ + + +
+ +
+ + \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index c644b23..263789c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,5 +1,6 @@ @@ -18,6 +19,8 @@ user-scalable=no" +