Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
# ---- Build stage ---------------------------------------------------------
# Pin bun to the version that wrote bun.lockb so the frozen install matches.
FROM oven/bun:1.4.2-slim AS build
# Nuxt Content's build uses node:sqlite, which Bun doesn't implement. With node
# on PATH, `bun run build` runs the nuxt CLI (a node shebang) under Node 22,
# matching the runtime image.
COPY --from=node:22-slim /usr/local/bin/node /usr/local/bin/node
WORKDIR /app

# Install dependencies first (cached layer). --ignore-scripts skips the
Expand Down
24 changes: 23 additions & 1 deletion app/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/* `font-body` utility -> PP Neue Machina Plain (used on window/dialog titles). */
--font-body: "PP Neue Machina Plain", ui-sans-serif, system-ui, sans-serif;

/* /games cards: a neutral face for stats, plus per-game display faces. */
/* Game cards: a neutral face for stats, plus per-game display faces. */
--font-stat: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-wow: "Cinzel", ui-serif, Georgia, serif;
--font-overwatch: "Teko", ui-sans-serif, system-ui, sans-serif;
Expand All @@ -27,3 +27,25 @@
--color-green-900: #0a5331;
--color-green-950: #052e16;
}

/* Proximity border glow: useProximityGlow sets --glow-x/--glow-y to the
cursor's position relative to each card, so the ring lights up on whichever
edges are near the cursor. The mask keeps the gradient to a thin border. */
.glow-card {
position: relative;
}

.glow-card::before {
content: '';
position: absolute;
inset: 0;
z-index: 10;
padding: 1.5px;
border-radius: inherit;
pointer-events: none;
background: radial-gradient(320px circle at var(--glow-x, -9999px) var(--glow-y, -9999px),
var(--ui-primary), transparent 60%);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
6 changes: 3 additions & 3 deletions app/components/Discord/Card.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const { timestamp } = useTimestamp()
const user = computed(() => lanyard.value?.discord_user)

// Banner, bio, pronouns and connections come from the profile proxy (Lanyard omits them).
const banner = computed(() => bannerUrl(user.value?.id ?? profile.value?.user?.id, profile.value?.user?.banner, 600))
const banner = computed(() => bannerUrl(user.value?.id ?? profile.value?.user?.id, profile.value?.user?.banner, 1024))
const bannerColor = computed(() => profile.value?.user?.banner_color ?? '#5865f2')
const bio = computed(() => parseDiscordText(profile.value?.user_profile?.bio ?? profile.value?.user?.bio))
const pronouns = computed(() => profile.value?.user_profile?.pronouns)
Expand Down Expand Up @@ -71,8 +71,8 @@ function hideOnError(e: Event) {
<template>
<div class="w-full text-slate-200">
<!-- Banner -->
<div class="relative h-24 w-full overflow-hidden" :style="{ backgroundColor: bannerColor }">
<NuxtImg v-if="banner" :src="banner" alt="banner" width="600" height="240"
<div class="relative aspect-[3/1] max-h-40 w-full overflow-hidden" :style="{ backgroundColor: bannerColor }">
<NuxtImg v-if="banner" :src="banner" alt="banner" width="1024" height="410"
class="h-full w-full object-cover" loading="lazy" draggable="false" @error="hideOnError" />
<div v-else class="absolute inset-0 opacity-40 [background:radial-gradient(circle_at_20%_120%,#ffffff,transparent_55%)]" />
</div>
Expand Down
20 changes: 9 additions & 11 deletions app/components/Games/Banner.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
<script setup lang="ts">
// Shared wide card shell for /games, laid out like the Battle.net launcher:
// identity plus stats in a column on the left with "View More" under them,
// and the game logo floating on the art at the right.
defineProps<{ background?: string; accent?: string }>()
// Shared wide game card: a UiPanel (so it matches the other front page cards)
// whose body is laid out like the Battle.net launcher, with identity plus
// stats on the left, "View More" under them, and the game logo on the art at
// the right.
defineProps<{ title: string; icon: string; background?: string }>()
const open = ref(false)
</script>

<template>
<article
class="relative overflow-hidden rounded-lg border border-gray-900 bg-[#0e0c12] font-stat transition-[border-color,box-shadow] duration-300 hover:border-primary-800/50 hover:shadow-lg hover:shadow-primary-950/40">
<UiPanel :title="title" :icon="icon" :padded="false" class="font-stat">
<!-- Art is scoped to this section so opening the drawer below doesn't
rescale or re-crop it. -->
<div class="relative">
<div class="relative flex flex-1 flex-col">
<img v-if="background" :src="background" alt="" loading="lazy"
class="absolute inset-0 h-full w-full object-cover object-[center_25%] opacity-60" />
<div class="absolute inset-0 bg-gradient-to-r from-black/90 via-black/20 to-black/70" />

<div
class="relative flex flex-col gap-5 p-5 md:min-h-52 md:flex-row-reverse md:items-center md:justify-between">
class="relative flex flex-1 flex-col gap-5 p-5 md:min-h-52 md:flex-row-reverse md:items-center md:justify-between">
<div class="flex shrink-0 justify-center drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)] md:w-36 md:justify-end md:self-start">
<slot name="logo" />
</div>
Expand Down Expand Up @@ -46,7 +46,5 @@ const open = ref(false)
</div>
</template>
</UCollapsible>

<div v-if="accent" class="absolute inset-y-0 left-0 w-1" :style="{ backgroundColor: accent }" />
</article>
</UiPanel>
</template>
18 changes: 4 additions & 14 deletions app/pages/games.vue → app/components/Games/List.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@ import type { OverwatchPlayer, WowCharacter } from '~/types/games'

const API = 'https://api.ok8.sh'

useHead({ title: 'Games — Jordan Jones' })
defineOgImage('Default', {
title: 'Games',
description: 'Characters and careers from the games I play.',
})

// Client-only (here and via <ClientOnly> below) so the prerendered page
// doesn't bake in stats from build time.
const { data: wow, pending: wowPending } = useFetch<{ characters: WowCharacter[] }>(`${API}/wow/characters`, { server: false, lazy: true })
Expand All @@ -19,12 +13,7 @@ const empty = computed(() => !pending.value && !wow.value?.characters.length &&
</script>

<template>
<section class="container mx-auto flex w-full max-w-6xl flex-col gap-4 px-2 py-6 md:px-6">
<header class="mb-2">
<h1 class="text-3xl font-bold text-white">Games</h1>
<p class="text-sm text-neutral-400">What I've been playing, pulled live from Blizzard.</p>
</header>

<div class="grid gap-4 2xl:grid-cols-2">
<ClientOnly>
<template v-if="pending">
<USkeleton v-for="i in 2" :key="i" class="h-52 rounded-lg bg-white/5" />
Expand All @@ -34,11 +23,12 @@ const empty = computed(() => !pending.value && !wow.value?.characters.length &&
<GamesOverwatchCard v-for="p in ow?.players" :key="p.battletag" :player="p" />

<UEmpty v-if="empty" icon="i-ph-game-controller" title="Nothing to show right now"
description="The game APIs didn't return anything. Check back in a bit." variant="naked" />
description="The game APIs didn't return anything. Check back in a bit." variant="naked"
class="col-span-full" />

<template #fallback>
<USkeleton v-for="i in 2" :key="i" class="h-52 rounded-lg bg-white/5" />
</template>
</ClientOnly>
</section>
</div>
</template>
2 changes: 1 addition & 1 deletion app/components/Games/OverwatchCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const owValue = 'font-overwatch text-3xl font-semibold italic leading-none'
</script>

<template>
<GamesBanner :background="p.namecard" accent="#f99e1a">
<GamesBanner title="Overwatch" icon="i-ph-crosshair" :background="p.namecard">
<template #logo>
<img src="https://blz-contentstack-images.akamaized.net/v3/assets/blt2477dcaf4ebd440c/blt12c582d9d58631b9/69d573c3c714e07b0dc39b07/overwatch_logo.png"
alt="Overwatch" class="w-40 md:w-36" />
Expand Down
4 changes: 2 additions & 2 deletions app/components/Games/WowCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ const classBackground = computed(() =>
</script>

<template>
<GamesBanner :background="c.media.main ?? classBackground"
:accent="classColor">
<GamesBanner :title="c.flavor === 'retail' ? 'World of Warcraft' : 'World of Warcraft Classic'"
icon="i-ph-sword" :background="c.media.main ?? classBackground">
<template #logo>
<div v-if="c.flavor === 'retail'" role="img" aria-label="World of Warcraft: Midnight"
class="h-20 w-26 bg-no-repeat md:h-28 md:w-36"
Expand Down
19 changes: 0 additions & 19 deletions app/components/Layout/Dock.vue

This file was deleted.

18 changes: 1 addition & 17 deletions app/components/Layout/Menu.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
const colorMode = useColorMode()
const { registry, open, close } = useWindows()
const { openContact } = useContact()
const { reduced, toggle: toggleMotion } = useMotionPreference()

Expand All @@ -16,17 +15,6 @@ const linksItems = computed(() => [
[{ label: 'Email me', icon: 'i-ph-envelope-simple', onSelect: () => openContact() }],
])

const windowItems = computed(() => {
const windows = Object.values(registry.value)
if (!windows.length) return [[{ label: 'No windows yet', icon: 'i-ph-app-window', disabled: true }]]
return [windows.map(w => ({
label: w.title,
icon: w.icon || 'i-ph-app-window',
trailingIcon: w.closed ? undefined : 'i-ph-check',
onSelect: (e: Event) => { e.preventDefault(); w.closed ? open(w.id) : close(w.id) },
}))]
})

const themes = [
{ label: 'Dark', icon: 'i-ph-moon', value: 'dark' },
{ label: 'Light', icon: 'i-ph-sun', value: 'light' },
Expand All @@ -53,16 +41,12 @@ const triggerClass = 'text-muted hover:text-highlighted data-[state=open]:text-h

<template>
<nav class="hidden items-center sm:flex">
<UButton to="/games" label="Games" color="neutral" variant="ghost" size="xs" :class="triggerClass" />
<UButton to="/blog" label="Blog" color="neutral" variant="ghost" size="xs" :class="triggerClass" />

<UDropdownMenu :items="linksItems" :content="{ align: 'start' }" :ui="{ content: 'w-48' }">
<UButton label="Links" color="neutral" variant="ghost" size="xs" :class="triggerClass" />
</UDropdownMenu>

<UDropdownMenu :items="windowItems" :content="{ align: 'start' }" :ui="{ content: 'w-52' }">
<UButton label="Windows" color="neutral" variant="ghost" size="xs" :class="triggerClass" />
</UDropdownMenu>

<UDropdownMenu :items="settingsItems" :content="{ align: 'start' }" :ui="{ content: 'w-44' }">
<UButton label="Settings" color="neutral" variant="ghost" size="xs" :class="triggerClass" />
</UDropdownMenu>
Expand Down
18 changes: 0 additions & 18 deletions app/components/Layout/ResetLayout.vue

This file was deleted.

35 changes: 35 additions & 0 deletions app/components/Layout/SectionNav.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script setup lang="ts">
const { reduced } = useMotionPreference()
const active = ref<string>(homeSections[0].id)

function go(id: string) {
document.getElementById(id)?.scrollIntoView({ behavior: reduced.value ? 'auto' : 'smooth', block: 'start' })
}

let observer: IntersectionObserver | undefined

onMounted(() => {
// The page scrolls inside the layout's frame, not the window. The bottom
// margin makes a section "active" once it reaches the top 40% of it.
observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) active.value = entry.target.id
}
}, { root: document.getElementById('content-frame'), rootMargin: '0px 0px -60% 0px' })

for (const { id } of homeSections) {
const el = document.getElementById(id)
if (el) observer.observe(el)
}
})

onUnmounted(() => observer?.disconnect())
</script>

<template>
<nav class="flex items-center gap-1" aria-label="Page sections">
<UButton v-for="section in homeSections" :key="section.id" :label="section.label" :icon="section.icon"
size="xs" color="primary" :variant="active === section.id ? 'soft' : 'ghost'"
:aria-current="active === section.id ? 'true' : undefined" @click="go(section.id)" />
</nav>
</template>
4 changes: 2 additions & 2 deletions app/components/Status/Windows.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defineProps<{
<div class="flex w-full max-w-3xl flex-col gap-4 lg:flex-row lg:items-start">
<!-- Status code "image" -->
<div class="w-full lg:w-2/5">
<UiPanel :title="String(code)" icon="i-ph-warning-octagon" :dock="false" :padded="false">
<UiPanel :title="String(code)" icon="i-ph-warning-octagon" :padded="false">
<div
class="relative flex aspect-square w-full items-center justify-center overflow-hidden bg-gradient-to-br from-primary-900/30 via-black to-black">
<div
Expand All @@ -26,7 +26,7 @@ defineProps<{

<!-- Related content -->
<div class="w-full lg:flex-1">
<UiPanel :title="title" icon="i-ph-info" :dock="false">
<UiPanel :title="title" icon="i-ph-info">
<div class="flex h-full flex-col gap-4 p-1">
<p class="text-slate-300">{{ description }}</p>
<slot name="extra" />
Expand Down
Loading
Loading