diff --git a/app/assets/css/main.css b/app/assets/css/main.css index 21a1afe..25df425 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -370,6 +370,12 @@ code { background: var(--code-bg); padding: 0.12em 0.35em; border-radius: 3px; + display: inline-block; + max-width: 100%; + overflow-wrap: anywhere; + vertical-align: baseline; + box-decoration-break: clone; + -webkit-box-decoration-break: clone; } pre { @@ -383,6 +389,8 @@ pre { } pre code { + display: inline; + max-width: none; padding: 0; color: inherit; background: none; diff --git a/app/components/Carousel.vue b/app/components/Carousel.vue new file mode 100644 index 0000000..87acd77 --- /dev/null +++ b/app/components/Carousel.vue @@ -0,0 +1,79 @@ + + + + + diff --git a/app/components/ProjectList.vue b/app/components/ProjectList.vue new file mode 100644 index 0000000..e1957a4 --- /dev/null +++ b/app/components/ProjectList.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/app/layouts/main.vue b/app/layouts/main.vue index 4ca4ebd..a51593f 100644 --- a/app/layouts/main.vue +++ b/app/layouts/main.vue @@ -1,14 +1,8 @@ diff --git a/app/pages/projects/[...slug].vue b/app/pages/projects/[...slug].vue new file mode 100644 index 0000000..4fb2d8f --- /dev/null +++ b/app/pages/projects/[...slug].vue @@ -0,0 +1,197 @@ + + + diff --git a/app/pages/projects/index.vue b/app/pages/projects/index.vue new file mode 100644 index 0000000..a9837b5 --- /dev/null +++ b/app/pages/projects/index.vue @@ -0,0 +1,121 @@ + + + diff --git a/app/utils/projects.ts b/app/utils/projects.ts new file mode 100644 index 0000000..473c52c --- /dev/null +++ b/app/utils/projects.ts @@ -0,0 +1,20 @@ +export function projectEntryHref(page: { + path: string; + href?: string | null; +}) { + return page.href || `/projects${page.path}`; +} + +export function isExternalHref(href: string) { + return /^https?:\/\//i.test(href); +} + +export function projectOgImage( + src: string | undefined, + siteUrl: string, + fallback: string, +) { + if (!src) return fallback; + if (/^https?:\/\//i.test(src)) return src; + return `${siteUrl}${src.startsWith('/') ? src : `/${src}`}`; +} diff --git a/app/utils/themes.ts b/app/utils/themes.ts index e0dacbc..3f3b65c 100644 --- a/app/utils/themes.ts +++ b/app/utils/themes.ts @@ -1,15 +1,16 @@ export const BG_THEMES = [ { id: 'dusk', label: 'Dusk' }, + { id: 'paper', label: 'Paper' }, { id: 'navy', label: 'Navy' }, { id: 'midnight', label: 'Midnight' }, { id: 'indigo', label: 'Indigo' }, - { id: 'storm', label: 'Storm' }, { id: 'slate', label: 'Slate' }, { id: 'ink', label: 'Ink' }, - { id: 'pine', label: 'Pine' }, - { id: 'wine', label: 'Wine' }, - { id: 'cocoa', label: 'Cocoa' }, - { id: 'paper', label: 'Paper' }, + // Disabled for now, I don't like them as much + // { id: 'storm', label: 'Storm' }, + // { id: 'pine', label: 'Pine' }, + // { id: 'wine', label: 'Wine' }, + // { id: 'cocoa', label: 'Cocoa' }, ] as const; export type BgThemeId = (typeof BG_THEMES)[number]['id']; diff --git a/content.config.ts b/content.config.ts index 35739c4..728f40e 100644 --- a/content.config.ts +++ b/content.config.ts @@ -15,5 +15,19 @@ export default defineContentConfig({ hidden: z.boolean().optional(), }), }), + projects: defineCollection({ + type: 'page', + source: { + include: 'projects/**/*.md', + prefix: '', + }, + schema: z.object({ + title: z.string(), + description: z.string(), + href: z.string().optional(), + hidden: z.boolean().optional(), + image: z.string().optional(), + }), + }), }, }); diff --git a/content/projects/mystwright.md b/content/projects/mystwright.md new file mode 100644 index 0000000..28493e6 --- /dev/null +++ b/content/projects/mystwright.md @@ -0,0 +1,5 @@ +--- +title: Mystwright +description: An AI-driven mystery detective game. Generate, investigate, solve. +href: https://mystwright.com/ +--- diff --git a/content/projects/rift-buddy.md b/content/projects/rift-buddy.md new file mode 100644 index 0000000..7bfbdb6 --- /dev/null +++ b/content/projects/rift-buddy.md @@ -0,0 +1,6 @@ +--- +title: Rift Buddy +description: Save, edit, and hot-swap rune pages alongside League of Legends. +href: https://github.com/jaronpate/rift-buddy +image: /projects/rift-buddy/demo.gif +--- diff --git a/content/projects/unicord.md b/content/projects/unicord.md new file mode 100644 index 0000000..9f8e425 --- /dev/null +++ b/content/projects/unicord.md @@ -0,0 +1,130 @@ +--- +title: Unicord 🦄 +description: A Discord bot SDK that works like a web server. +# href: https://unicord.hat.fish +--- + +I wanted writing a Discord bot to feel like writing a small web app. Register a handler, get a request, send a reply. *Bang!...bot done.* This became particularly interesting once bots could be served over HTTP. + +[Unicord 🦄](https://unicord.hat.fish/) is a TypeScript SDK for that. One client, one command API. A chat command and a slash command can share the same function. + +You dont have to choose between the gateway, webhooks, application commands, or fiddle with file based command registration; *just write the damn code!* + +A whole bot can look like this: + +```typescript +import { Client, type Context } from 'unicord'; + +// Initialize and configure +const client = new Client({ + token: '', + application_id: '', + intents: [Intent.ALL], + prefix: '??' +}); + +// Register arbitrary event handler to know when we are logged in +client.on('READY', async (_, payload) => { + console.log(`Logged in as ${payload.user.username}${payload.user.discriminator?.length > 0 ? `#${payload.user.discriminator}` : ''}`); +}); + +// Register a command +client.chatCommands.register('ping', (context: Context, args) => { + context.reply('Pong!', true); +}); + +// Connect! +client.connect(); +``` + +**Boom** done. We are Pong-ing all the Pings! + +--- + +My favorite part to build and use is the ✨\~Hydrator\~✨. :margin-note[(Sounds fancy right? 💪😎)] When handling some interaction the client assumes you want the least data neccessary. Then say you want to know the guild, for instance, you can request it be "Hydrated". + +```typescript +client.chatCommands.register('guild', async (context: Context, _args: any[]) => { + const hydrate = await context.hydrator([Expectation.Guild]); + const hasGuild = hydrate(context); + + const { message } = context; + + if (hasGuild) { + await context.reply(`You are in ${message.guild.name}`, true); + } else { + await context.reply(`We are not in a server`, true); + } +}); +``` + +> If the requested data has been seen recently it is simply filled in from the local cache, otherwise it is fetched from the API. + +You'll notice that you also recieve a boolean from the hydrator. This boolean indicates to the type system if hydration was sucessful. :margin-note[(a.k.a it returns a type assertion!)] + +::notice +For the uninitiated: this means for if statment that follows inside the true case `message.guild` will be typed as `Guild` and in the false case as `undefined`! + +Huzzah! Type safety! 🍻 +:: + +--- + +But wait there's more!? + +What about user input? Can't trust it? Has to be validated? A tale as old as time...however old time is. + +Fear no more Unicord can also **validate**, **resolve**, and **type** your command arguments! + +Check it out: + +```typescript +// First we define a commmand with the helper +const avatarCommand = createCommandHandler({ + description: 'Get a users avatar', + // In the definition we provide our arguments in order: + args: [ + { + id: 'user', + name: 'user', + // And here we inform the client what type we expect + type: CommandOptionType.User, + description: 'The user to get the avatar for', + required: true + } + ], + execute: async (context: Context, args) => { + // Now inside of our execution function `args` + // will have the type: `{ user: User }` + context.reply(args.user.avatarURL); + } +}); +``` + +And just like that when our function is called the first argument is translated into a Discord `User` object! :margin-note[Nifty!] + +> Many different option types exist. You can even have an Enum. When constraints are known the Unicord client can auto reply to bad command usage with an error message and the valid values allowed! + + + +To finish out the example — you would then register your command handler: + +```typescript +// Either as a plain ol' chat command +client.chatCommands.register('avatar', avatarCommand); +// Or a fancy slash command +client.applicationCommands.register('avatar', ApplicationCommandType.Chat, avatarCommand); +``` + +And use it in Discord like so: + +![Example command used in Discord](/projects/unicord/command-example-discord.png) + +--- + +It's pretty unfinished and out of date so not everything works, but I think this first spike really shows some potential in the idea! And it's fun to write imo. + +Source is on [the hub](https://github.com/jaronpate/unicord) and site is [here](https://unicord.hat.fish). diff --git a/public/favicon-alt.png b/public/favicon-alt.png new file mode 100644 index 0000000..a456424 Binary files /dev/null and b/public/favicon-alt.png differ diff --git a/public/favicon.png b/public/favicon.png index a456424..d03c7dd 100644 Binary files a/public/favicon.png and b/public/favicon.png differ diff --git a/public/projects/rift-buddy/demo.gif b/public/projects/rift-buddy/demo.gif new file mode 100644 index 0000000..1da811b Binary files /dev/null and b/public/projects/rift-buddy/demo.gif differ diff --git a/public/projects/unicord/command-example-discord.png b/public/projects/unicord/command-example-discord.png new file mode 100644 index 0000000..689826c Binary files /dev/null and b/public/projects/unicord/command-example-discord.png differ diff --git a/public/resume/index.html b/public/resume/index.html index 0f4f349..0f5810d 100644 --- a/public/resume/index.html +++ b/public/resume/index.html @@ -350,7 +350,7 @@