generated from alphane/template
BIg changes #2
+599
-12
@@ -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">█</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…</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 →</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 — 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 — 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>
|
||||
|
||||
Reference in New Issue
Block a user