First draft of HomePageUpgrade from claude

This commit is contained in:
2026-07-16 16:42:37 -07:00
parent 9dfee98b84
commit 8d9fe8e75d
+599 -12
View File
@@ -1,28 +1,615 @@
<template>
<div>
<div class="home-page">
<v-row>
<v-col
cols="12"
sm="6"
md="4"
lg="2"
md="3"
class="sidebar-col"
>
<AppCard
v-if="isAuthenticated"
:to="{ name: 'DashboardPage' }"
elevation="6"
>
You are signed in. Go to Dashboard
</AppCard>
<aside class="sidebar">
<nav
class="sidebar-nav mb-6 mt-8"
aria-label="Primary"
>
<RouterLink
:to="{ name: 'BlogPage' }"
class="sidebar-nav-link"
>
<span>Blog</span><span class="sidebar-nav-path">~/blog</span>
</RouterLink>
</nav>
</aside>
<v-divider class="my-4" />
<div class="social-links">
<a
href="#"
class="social-link"
title="GitHub"
>
<v-icon>mdi-github</v-icon>
</a>
<a
href="#"
class="social-link"
title="Gitea"
>
<v-icon>mdi-git</v-icon>
</a>
<a
href="#"
class="social-link"
title="LinkedIn"
>
<v-icon>mdi-linkedin</v-icon>
</a>
<a
href="#"
class="social-link"
title="Instagram"
>
<v-icon>mdi-instagram</v-icon>
</a>
<a
href="#"
class="social-link"
title="Gmail"
>
<v-icon>mdi-gmail</v-icon>
</a>
</div>
</v-col>
<v-col
cols="12"
md="6"
>
<main>
<div class="terminal mb-10">
<div class="terminal-bar">
<span class="terminal-dot terminal-dot-red"></span>
<span class="terminal-dot terminal-dot-yellow"></span>
<span class="terminal-dot terminal-dot-green"></span>
<span class="terminal-title">calebburke.dev</span>
</div>
<div class="terminal-prompt">
<span class="term-green">caleb</span>
<span class="term-cyan">@</span><span class="term-green">burke</span>
<span class="term-white"> ~</span>
<span class="term-cyan">$</span>
<span class="terminal-cmd"> cat about.txt</span>
<span class="terminal-out"> Full Stack Developer </span>
<span class="term-green">caleb</span>
<span class="term-cyan">@</span>
<span class="term-green">burke</span>
<span class="term-white"> ~</span>
<span class="term-cyan">$ </span>
<span class="terminal-cmd">{{ typedText }}</span>
<span class="term-cursor">&#9608;</span>
<!-- <span class="term-green">caleb</span><span class="term-cyan">@</span
><span class="term-green">burke</span><span class="term-white">:~$</span>
<span class="terminal-cmd"> ls ~/posts | head -3</span>
<span
v-if="isLoading"
class="terminal-out terminal-out-dim"
>loading&hellip;</span
>
<span
v-else-if="recentBlogPosts.length === 0"
class="terminal-out terminal-out-dim"
>(no posts published yet)</span
>
<span
v-else
class="terminal-out terminal-out-dim"
>{{ recentBlogPosts.map((blogPost) => blogPost.slug).join(" ") }}</span
> -->
</div>
</div>
<section class="posts-section">
<div class="section-head">
<div>
<span class="section-eyebrow">~/posts</span>
<h2 class="text-h6 font-weight-bold">Recent posts</h2>
</div>
<RouterLink
:to="{ name: 'BlogPage' }"
class="section-view-all"
>view all &rarr;</RouterLink
>
</div>
<div v-if="isLoading">
<v-skeleton-loader
v-for="index in 3"
:key="index"
type="list-item-two-line"
class="mb-2"
/>
</div>
<p
v-else-if="recentBlogPosts.length === 0"
class="text-medium-emphasis"
>
Nothing published yet &mdash; check back soon.
</p>
<div
v-else
class="posts"
>
<article
v-for="blogPost in recentBlogPosts"
:key="blogPost.id"
class="post"
>
<div class="post-date">
<span class="post-date-day">{{ formatPostDay(blogPost.publishedAt) }}</span>
{{ formatPostMonth(blogPost.publishedAt) }}
</div>
<div>
<h3 class="text-subtitle-1 font-weight-bold mb-1">{{ blogPost.title }}</h3>
<div
v-if="blogPost.tags.length"
class="post-tags"
>
<span
v-for="tag in blogPost.tags"
:key="tag"
class="tag-chip"
>{{ tag }}</span
>
</div>
</div>
</article>
</div>
</section>
</main>
</v-col>
<v-col
cols="12"
md="3"
class="rail-col"
>
<aside class="rail">
<div
v-if="allTags.length"
class="rail-block"
>
<span class="rail-eyebrow">Tags</span>
<div class="tag-cloud">
<span
v-for="tag in allTags"
:key="tag"
class="tag-chip"
>{{ tag }}</span
>
</div>
</div>
<div class="rail-block">
<div class="now-box">
<div class="now-row">
<span class="now-key">release</span
><span class="now-value">{{ status.releaseTag || "dev" }}</span>
</div>
<div class="now-row">
<span class="now-key">commit</span
><span class="now-value">{{ shortCommitHash }}</span>
</div>
<RouterLink
:to="{ name: 'StatusPage' }"
class="now-link"
>
view status
</RouterLink>
</div>
</div>
<div class="rail-block">
<div class="cta-card">
<p v-if="isAuthenticated">Welcome back &mdash; jump into your dashboard.</p>
<p v-else>Sign in to manage posts and track flashcard progress.</p>
<v-btn
:to="isAuthenticated ? { name: 'DashboardPage' } : { name: 'SignInPage' }"
color="primary"
variant="tonal"
size="small"
>
{{ isAuthenticated ? "Go to dashboard" : "Log in" }}
</v-btn>
</div>
</div>
</aside>
</v-col>
</v-row>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, reactive, ref } from "vue"
import { DateTime } from "luxon"
import { useAuth0 } from "@auth0/auth0-vue"
import AppCard from "@/components/common/AppCard.vue"
import http from "@/api/http-client"
import usePublicBlogPosts from "@/use/use-public-blog-posts"
const { isAuthenticated } = useAuth0()
const { blogPosts, isLoading, fetch: fetchBlogPosts } = usePublicBlogPosts()
const recentBlogPosts = computed(() => blogPosts.value.slice(0, 4))
const allTags = computed(() => [...new Set(blogPosts.value.flatMap((blogPost) => blogPost.tags))])
function formatPostDay(publishedAt: string | null): string {
if (!publishedAt) return ""
return DateTime.fromISO(publishedAt).toLocal().toFormat("dd")
}
function formatPostMonth(publishedAt: string | null): string {
if (!publishedAt) return ""
return DateTime.fromISO(publishedAt).toLocal().toFormat("MMM yyyy")
}
const status = reactive({
releaseTag: "",
gitCommitHash: "",
})
const shortCommitHash = computed(() => status.gitCommitHash.slice(0, 7) || "local")
const TYPED_PHRASES = [
" Hello!",
" sudo rm -rf --no-preserve-root /",
" :(){ :|:& };:",
" Cheers!",
" привет",
" クリア",
" Εβίβα!",
" 干杯",
" Будьмо!",
" Na zdraví!",
" 乾杯!",
]
const TYPE_DELAY_MS = 150
const DELETE_DELAY_MS = 100
const PAUSE_AFTER_TYPED_MS = 1500
const PAUSE_AFTER_DELETED_MS = 5000
const INITIAL_DELAY_MS = 800
const typedText = ref("")
let currentPhrase = ""
let typingTimeoutId: ReturnType<typeof setTimeout>
function pickRandomPhrase() {
return TYPED_PHRASES[Math.floor(Math.random() * TYPED_PHRASES.length)]
}
function typeCharacter(index: number) {
typedText.value = currentPhrase.slice(0, index)
if (index < currentPhrase.length) {
typingTimeoutId = setTimeout(() => typeCharacter(index + 1), TYPE_DELAY_MS)
} else {
typingTimeoutId = setTimeout(() => deleteCharacter(index), PAUSE_AFTER_TYPED_MS)
}
}
function deleteCharacter(index: number) {
typedText.value = currentPhrase.slice(0, index)
if (index > 0) {
typingTimeoutId = setTimeout(() => deleteCharacter(index - 1), DELETE_DELAY_MS)
} else {
typingTimeoutId = setTimeout(() => {
currentPhrase = pickRandomPhrase()
typeCharacter(0)
}, PAUSE_AFTER_DELETED_MS)
}
}
onMounted(async () => {
await fetchBlogPosts()
currentPhrase = pickRandomPhrase()
typingTimeoutId = setTimeout(() => typeCharacter(0), INITIAL_DELAY_MS)
try {
const { data } = await http.get("/_status")
status.releaseTag = data.RELEASE_TAG
status.gitCommitHash = data.GIT_COMMIT_HASH
} catch (error) {
console.error("Failed to fetch status:", error)
}
})
onUnmounted(() => {
clearTimeout(typingTimeoutId)
})
</script>
<style scoped>
.home-page {
max-width: 1300px;
margin: 0 auto;
}
.sidebar,
.rail {
position: sticky;
top: 24px;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 2px;
}
.sidebar-nav-link {
display: flex;
justify-content: space-between;
align-items: center;
padding: 7px 10px;
margin: 0 -10px;
border-radius: 6px;
font-family: ui-monospace, Menlo, Monaco, Consolas, "Ubuntu Mono", monospace;
font-size: 14px;
color: rgb(var(--v-theme-textSecondary));
text-decoration: none;
transition:
background-color 0.15s ease,
color 0.15s ease;
}
.sidebar-nav-link:hover,
.sidebar-nav-link.router-link-active {
background-color: rgb(var(--v-theme-hoverColor));
color: rgb(var(--v-theme-textPrimary));
}
.sidebar-nav-path {
color: rgb(var(--v-theme-textSecondary) / 70%);
}
.social-links {
display: flex;
gap: 16px;
}
.social-link {
color: rgb(var(--v-theme-textSecondary));
transition: color 0.15s ease;
}
.social-link:hover {
color: rgb(var(--v-theme-primary));
}
/* Terminal hero */
.terminal {
background-color: rgb(var(--v-theme-surface));
border: 1px solid rgb(var(--v-theme-borderColor));
border-radius: 8px;
overflow: hidden;
}
.terminal-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background-color: rgb(var(--v-theme-containerBg));
border-bottom: 1px solid rgb(var(--v-theme-borderColor));
}
.terminal-dot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.terminal-dot-red {
background-color: rgb(var(--v-theme-error));
}
.terminal-dot-yellow {
background-color: rgb(var(--v-theme-warning));
}
.terminal-dot-green {
background-color: rgb(var(--v-theme-success));
}
.terminal-title {
margin: 0 auto;
transform: translateX(-16px);
font-family: ui-monospace, Menlo, Monaco, Consolas, "Ubuntu Mono", monospace;
font-size: 12px;
color: rgb(var(--v-theme-textSecondary));
}
.terminal-prompt {
display: block;
padding: 20px 22px 24px;
font-family: ui-monospace, Menlo, Monaco, Consolas, "Ubuntu Mono", monospace;
font-size: 14px;
line-height: 1.9;
}
.terminal-cmd {
color: rgb(var(--v-theme-textPrimary));
}
.terminal-out {
display: block;
margin: 0 0 10px;
color: rgb(var(--v-theme-textSecondary));
}
.terminal-out-dim {
color: rgb(var(--v-theme-textSecondary) / 60%);
}
@keyframes terminal-blink {
50% {
opacity: 0;
}
}
/* Section headers */
.section-head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 16px;
}
.section-eyebrow {
display: block;
margin-bottom: 4px;
font-family: ui-monospace, Menlo, Monaco, Consolas, "Ubuntu Mono", monospace;
font-size: 12px;
color: rgb(var(--v-theme-primary));
}
.section-view-all {
font-family: ui-monospace, Menlo, Monaco, Consolas, "Ubuntu Mono", monospace;
font-size: 12px;
color: rgb(var(--v-theme-textSecondary));
text-decoration: none;
border-bottom: 1px solid rgb(var(--v-theme-borderColor));
}
.section-view-all:hover {
color: rgb(var(--v-theme-primary));
}
/* Posts */
.post {
display: grid;
grid-template-columns: 76px 1fr;
gap: 18px;
padding: 18px 0;
border-bottom: 1px solid rgb(var(--v-theme-borderColor));
}
.post:first-child {
padding-top: 0;
}
.post:last-child {
border-bottom: none;
}
.post-date {
font-family: ui-monospace, Menlo, Monaco, Consolas, "Ubuntu Mono", monospace;
font-size: 12px;
line-height: 1.5;
color: rgb(var(--v-theme-textSecondary) / 70%);
padding-top: 3px;
}
.post-date-day {
display: block;
font-size: 20px;
font-weight: 500;
color: rgb(var(--v-theme-textSecondary));
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag-chip {
font-family: ui-monospace, Menlo, Monaco, Consolas, "Ubuntu Mono", monospace;
font-size: 11px;
padding: 2px 8px;
border-radius: 20px;
background-color: rgb(var(--v-theme-surface));
border: 1px solid rgb(var(--v-theme-borderColor));
color: rgb(var(--v-theme-textSecondary));
}
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
/* Right rail */
.rail {
display: flex;
flex-direction: column;
gap: 28px;
}
.rail-eyebrow {
display: block;
margin-bottom: 12px;
font-family: ui-monospace, Menlo, Monaco, Consolas, "Ubuntu Mono", monospace;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: rgb(var(--v-theme-textSecondary) / 70%);
}
.now-box {
padding: 14px 16px;
background-color: rgb(var(--v-theme-surface));
border: 1px solid rgb(var(--v-theme-borderColor));
border-radius: 6px;
font-family: ui-monospace, Menlo, Monaco, Consolas, "Ubuntu Mono", monospace;
font-size: 12px;
}
.now-row {
display: flex;
justify-content: space-between;
gap: 8px;
line-height: 1.9;
}
.now-key {
color: rgb(var(--v-theme-textSecondary) / 70%);
}
.now-value {
color: rgb(var(--v-theme-textPrimary));
}
.now-link {
display: inline-block;
margin-top: 10px;
color: rgb(var(--v-theme-primary));
text-decoration: none;
}
.cta-card {
padding: 16px;
background-color: rgb(var(--v-theme-lightprimary));
border: 1px solid rgb(var(--v-theme-primary) / 25%);
border-radius: 6px;
}
.cta-card p {
margin: 0 0 12px;
font-size: 13px;
color: rgb(var(--v-theme-textSecondary));
line-height: 1.6;
}
@media (max-width: 960px) {
.sidebar,
.rail {
position: static;
}
}
</style>