refactor About, Blog, Contact, and Desktop components for improved responsiveness and structure; add BlogArticle component for article display

This commit is contained in:
2026-01-30 21:11:20 -05:00
parent 7f56539831
commit 2dbf53db63
5 changed files with 146 additions and 154 deletions

View File

@@ -5,26 +5,26 @@
<h1 class="text-3xl font-bold mb-6 text-[#000080] border-b-2 border-[#808080] pb-3">About Me</h1>
<!-- Profile Card -->
<div class="bg-white border-2 border-[#808080] p-6 shadow-md mb-6">
<div class="flex items-start gap-6 mb-6">
<div class="profile-avatar w-32 h-32 bg-[#000080] border-2 border-[#808080] flex items-center justify-center">
<User :size="64" color="#ffffff" />
<div class="bg-white border-2 border-[#808080] p-4 md:p-6 shadow-md mb-6">
<div class="flex flex-col sm:flex-row items-start gap-4 md:gap-6 mb-6">
<div class="profile-avatar w-24 h-24 md:w-32 md:h-32 bg-[#000080] border-2 border-[#808080] flex items-center justify-center flex-shrink-0">
<User :size="64" color="#ffffff" class="w-12 h-12 md:w-16 md:h-16" />
</div>
<div class="flex-1">
<h2 class="text-3xl font-bold text-[#000080] mb-2">SticksDev</h2>
<p class="text-xl text-gray-700 mb-3">Tanner Sommers</p>
<div class="space-y-2 text-gray-700">
<div class="flex-1 min-w-0">
<h2 class="text-2xl md:text-3xl font-bold text-[#000080] mb-2">SticksDev</h2>
<p class="text-lg md:text-xl text-gray-700 mb-3">Tanner Sommers</p>
<div class="space-y-2 text-sm md:text-base text-gray-700">
<div class="flex items-center gap-2">
<span class="font-semibold min-w-25">Age:</span>
<span class="font-semibold min-w-20 md:min-w-25">Age:</span>
<span>22</span>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold min-w-25">Location:</span>
<span>United States (UTC-5)</span>
<span class="font-semibold min-w-20 md:min-w-25">Location:</span>
<span class="break-words">United States (UTC-5)</span>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold min-w-25">Occupation:</span>
<span>Software Engineer / Freelancer</span>
<span class="font-semibold min-w-20 md:min-w-25">Occupation:</span>
<span class="break-words">Software Engineer / Freelancer</span>
</div>
</div>
</div>

View File

@@ -65,74 +65,17 @@
<p class="text-yellow-600 text-sm">Check back later for updates!</p>
</div>
<!-- Blog Post Modal -->
<Teleport to="body">
<div v-if="selectedPost" ref="modalRef" class="fixed z-60" :style="modalStyle">
<div
class="bg-[#c0c0c0] border-2 border-white border-r-[#808080] border-b-[#808080] shadow-2xl max-w-4xl w-full max-h-[85vh] flex flex-col"
>
<!-- Modal Title Bar -->
<div
class="bg-linear-to-r from-[#000080] to-[#1084d0] px-2 py-1 flex items-center justify-between cursor-move select-none"
@mousedown="startDrag"
>
<span class="text-white font-bold text-sm">{{ selectedPost.title }}</span>
<button
class="w-5 h-5 bg-[#c0c0c0] border border-white border-b-[#808080] border-r-[#808080] flex items-center justify-center text-xs font-bold hover:bg-[#dfdfdf]"
@click="selectedPost = null"
>
<X :size="16" />
</button>
</div>
<!-- Modal Content -->
<div class="bg-white border-2 border-[#808080] border-t-white border-l-white overflow-auto flex-1">
<div class="max-w-3xl mx-auto px-8 py-8">
<div class="mb-6 pb-6 border-b-2 border-[#808080]">
<h1 class="text-4xl font-bold mb-4 text-[#000080]">{{ selectedPost.title }}</h1>
<div class="flex items-center gap-4 text-sm text-gray-600">
<span class="flex items-center gap-1.5 bg-[#f0f0f0] px-3 py-1.5 border border-[#808080]">
<Calendar :size="16" />
{{ formatDate(selectedPost.date) }}
</span>
<span
v-if="selectedPost.tags && selectedPost.tags.length > 0"
class="flex items-center gap-1.5 bg-[#f0f0f0] px-3 py-1.5 border border-[#808080]"
>
<Tag :size="16" />
{{ selectedPost.tags.join(', ') }}
</span>
</div>
</div>
<!-- key change: a single reusable class -->
<ContentRenderer :value="selectedPost" class="blog-prose">
<template #empty>
<p class="text-gray-500 text-center py-8">No content available.</p>
</template>
</ContentRenderer>
</div>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { X, Calendar, Tag, ArrowRight, FileText, Loader2, AlertCircle, Inbox } from 'lucide-vue-next'
import { Calendar, Tag, ArrowRight, FileText, Loader2, AlertCircle, Inbox } from 'lucide-vue-next'
import type { BlogCollectionItem } from '@nuxt/content'
import { ref, computed, onMounted, onUnmounted } from 'vue'
const selectedPost = ref<BlogCollectionItem | null>(null)
const selectedPostPath = ref<string | null>(null)
const modalRef = ref<HTMLElement | null>(null)
const isDragging = ref(false)
const dragStartX = ref(0)
const dragStartY = ref(0)
const modalX = ref(0)
const modalY = ref(0)
const emit = defineEmits<{
'article-opened': [title: string, content: BlogCollectionItem]
'article-closed': []
}>()
const { data: posts, pending, error } = await useAsyncData('blog-posts', () => {
return queryCollection('blog').all()
@@ -143,70 +86,20 @@ const sortedPosts = computed(() => {
return [...posts.value].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
})
const modalStyle = computed(() => {
if (modalX.value === 0 && modalY.value === 0) {
return {
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
width: '100%',
maxWidth: '56rem',
padding: '1rem',
}
}
return {
left: `${modalX.value}px`,
top: `${modalY.value}px`,
width: '100%',
maxWidth: '56rem',
padding: '1rem',
}
})
const openPost = async (path: string) => {
selectedPostPath.value = path
selectedPost.value = await queryCollection('blog').path(path).first()
modalX.value = 0
modalY.value = 0
}
const post = await queryCollection('blog').path(path).first()
const startDrag = (e: MouseEvent) => {
isDragging.value = true
if (modalX.value === 0 && modalY.value === 0) {
const rect = modalRef.value?.getBoundingClientRect()
if (rect) {
modalX.value = rect.left
modalY.value = rect.top
// Emit to parent to open in separate window
if (post) {
emit('article-opened', post.title, post)
}
}
dragStartX.value = e.clientX - modalX.value
dragStartY.value = e.clientY - modalY.value
}
const onDrag = (e: MouseEvent) => {
if (!isDragging.value) return
modalX.value = e.clientX - dragStartX.value
modalY.value = e.clientY - dragStartY.value
}
const stopDrag = () => {
isDragging.value = false
}
const formatDate = (dateString: string) => {
if (!dateString) return 'Unknown date'
const date = new Date(dateString)
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
}
onMounted(() => {
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
})
onUnmounted(() => {
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
})
</script>
<style scoped>

View File

@@ -0,0 +1,49 @@
<!-- eslint-disable vue/multi-word-component-names -->
<template>
<div class="h-full overflow-auto bg-white">
<div v-if="article" class="max-w-3xl mx-auto px-4 md:px-8 py-6 md:py-8">
<div class="mb-6 pb-6 border-b-2 border-[#808080]">
<h1 class="text-3xl md:text-4xl font-bold mb-4 text-[#000080]">{{ article.title }}</h1>
<div class="flex flex-wrap items-center gap-3 md:gap-4 text-sm text-gray-600">
<span class="flex items-center gap-1.5 bg-[#f0f0f0] px-3 py-1.5 border border-[#808080]">
<Calendar :size="16" />
{{ formatDate(article.date) }}
</span>
<span
v-if="article.tags && article.tags.length > 0"
class="flex items-center gap-1.5 bg-[#f0f0f0] px-3 py-1.5 border border-[#808080]"
>
<Tag :size="16" />
{{ article.tags.join(', ') }}
</span>
</div>
</div>
<ContentRenderer :value="article" class="blog-prose">
<template #empty>
<p class="text-gray-500 text-center py-8">No content available.</p>
</template>
</ContentRenderer>
</div>
</div>
</template>
<script setup lang="ts">
import { Calendar, Tag } from 'lucide-vue-next'
import type { BlogCollectionItem } from '@nuxt/content'
defineProps<{
article: BlogCollectionItem | null
}>()
const formatDate = (dateString: string) => {
if (!dateString) return 'Unknown date'
const date = new Date(dateString)
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
}
</script>
<style scoped>
/* No additional styles needed - inherits from global blog-prose */
</style>

View File

@@ -13,15 +13,15 @@
<!-- Email Card -->
<a
href="mailto:tanner@teamhydra.dev"
class="contact-card bg-white border-2 border-[#808080] p-5 shadow-md flex items-start gap-4 hover:border-[#000080] transition-colors group"
class="contact-card bg-white border-2 border-[#808080] p-4 md:p-5 shadow-md flex items-start gap-3 md:gap-4 hover:border-[#000080] transition-colors group"
>
<div class="contact-icon bg-[#000080] border-2 border-[#808080] w-16 h-16 flex items-center justify-center group-hover:bg-[#1084d0] transition-colors">
<Mail :size="32" color="#ffffff" />
<div class="contact-icon bg-[#000080] border-2 border-[#808080] w-12 h-12 md:w-16 md:h-16 shrink-0 flex items-center justify-center group-hover:bg-[#1084d0] transition-colors">
<Mail :size="24" class="md:w-8 md:h-8" color="#ffffff" />
</div>
<div class="flex-1">
<h3 class="text-xl font-bold text-[#000080] mb-1">Email</h3>
<p class="text-gray-600 text-sm mb-2">Best for professional inquiries and projects</p>
<p class="text-[#000080] font-mono text-lg">tanner@teamhydra.dev</p>
<div class="flex-1 min-w-0">
<h3 class="text-lg md:text-xl font-bold text-[#000080] mb-1">Email</h3>
<p class="text-gray-600 text-xs md:text-sm mb-2">Best for professional inquiries and projects</p>
<p class="text-[#000080] font-mono text-sm md:text-lg break-all">tanner@teamhydra.dev</p>
</div>
</a>
@@ -29,19 +29,19 @@
<a
href="https://discord.gg/zira"
target="_blank"
class="contact-card bg-white border-2 border-[#808080] p-5 shadow-md flex items-start gap-4 hover:border-[#000080] transition-colors group"
class="contact-card bg-white border-2 border-[#808080] p-4 md:p-5 shadow-md flex items-start gap-3 md:gap-4 hover:border-[#000080] transition-colors group"
>
<div class="contact-icon bg-[#5865F2] border-2 border-[#808080] w-16 h-16 flex items-center justify-center group-hover:bg-[#4752C4] transition-colors">
<DiscordIcon :size="32" style="color: white" />
<div class="contact-icon bg-[#5865F2] border-2 border-[#808080] w-12 h-12 md:w-16 md:h-16 shrink-0 flex items-center justify-center group-hover:bg-[#4752C4] transition-colors">
<DiscordIcon :size="24" class="md:w-8 md:h-8" style="color: white" />
</div>
<div class="flex-1">
<h3 class="text-xl font-bold text-[#000080] mb-1">Discord</h3>
<p class="text-gray-600 text-sm mb-2">Join the Discord server for casual conversations</p>
<p class="text-gray-700 mb-1">
<span class="font-semibold">Username:</span> <span class="font-mono">sticksdev</span>
<div class="flex-1 min-w-0">
<h3 class="text-lg md:text-xl font-bold text-[#000080] mb-1">Discord</h3>
<p class="text-gray-600 text-xs md:text-sm mb-2">Join the Discord server for casual conversations</p>
<p class="text-gray-700 text-sm md:text-base mb-1">
<span class="font-semibold">Username:</span> <span class="font-mono break-all">sticksdev</span>
</p>
<p class="text-gray-700">
<span class="font-semibold">Channel:</span> #other-support
<p class="text-gray-700 text-sm md:text-base">
<span class="font-semibold">Channel:</span> <span class="break-all">#other-support</span>
</p>
</div>
</a>
@@ -50,15 +50,15 @@
<a
href="https://github.com/SticksDev"
target="_blank"
class="contact-card bg-white border-2 border-[#808080] p-5 shadow-md flex items-start gap-4 hover:border-[#000080] transition-colors group"
class="contact-card bg-white border-2 border-[#808080] p-4 md:p-5 shadow-md flex items-start gap-3 md:gap-4 hover:border-[#000080] transition-colors group"
>
<div class="contact-icon bg-[#24292e] border-2 border-[#808080] w-16 h-16 flex items-center justify-center group-hover:bg-[#1a1e22] transition-colors text-white">
<GitHubIcon :size="32" style="color: white" />
<div class="contact-icon bg-[#24292e] border-2 border-[#808080] w-12 h-12 md:w-16 md:h-16 shrink-0 flex items-center justify-center group-hover:bg-[#1a1e22] transition-colors text-white">
<GitHubIcon :size="24" class="md:w-8 md:h-8" style="color: white" />
</div>
<div class="flex-1">
<h3 class="text-xl font-bold text-[#000080] mb-1">GitHub</h3>
<p class="text-gray-600 text-sm mb-2">Check out my projects and contributions</p>
<p class="text-[#000080] font-mono">@SticksDev</p>
<div class="flex-1 min-w-0">
<h3 class="text-lg md:text-xl font-bold text-[#000080] mb-1">GitHub</h3>
<p class="text-gray-600 text-xs md:text-sm mb-2">Check out my projects and contributions</p>
<p class="text-[#000080] font-mono text-sm md:text-base break-all">@SticksDev</p>
</div>
</a>
</div>

View File

@@ -172,7 +172,27 @@
@activate="activeWindow = 'blog'"
@minimize="minimizeWindow('blog')"
>
<Blog />
<Blog
@article-opened="handleArticleOpened"
@article-closed="handleArticleClosed"
/>
</Window>
<!-- Article Window -->
<Window
v-if="windows.article"
:title="openArticleTitle || 'Blog Article'"
:initial-x="400"
:initial-y="120"
:width="900"
:height="600"
:is-active="activeWindow === 'article'"
:is-minimized="minimizedWindows.article"
@close="closeWindow('article'); handleArticleClosed()"
@activate="activeWindow = 'article'"
@minimize="minimizeWindow('article')"
>
<BlogArticle :article="openArticleContent" />
</Window>
<!-- Start Menu -->
@@ -303,6 +323,13 @@
:isActive="activeWindow === 'blog' && !minimizedWindows.blog"
@click="toggleMinimize('blog')"
/>
<TaskbarButton
:icon="FileText"
:label="openArticleTitle || 'Article'"
:isOpen="windows.article"
:isActive="activeWindow === 'article' && !minimizedWindows.article"
@click="toggleMinimize('article')"
/>
</div>
<div class="time px-1 md:px-2 border-2 border-[#808080] border-t-white border-l-white text-xs md:text-sm">
@@ -323,7 +350,9 @@ import Projects from '~/components/Projects.vue'
import Experience from '~/components/Experience.vue'
import Contact from '~/components/Contact.vue'
import Blog from '~/components/Blog.vue'
import BlogArticle from '~/components/BlogArticle.vue'
import AboutShrimpOS from '~/components/AboutShrimpOS.vue'
import type { BlogCollectionItem } from '@nuxt/content'
const windows = reactive({
about: false,
@@ -331,6 +360,7 @@ const windows = reactive({
experience: false,
contact: false,
blog: false,
article: false,
})
const minimizedWindows = reactive({
@@ -339,10 +369,13 @@ const minimizedWindows = reactive({
experience: false,
contact: false,
blog: false,
article: false,
})
const activeWindow = ref<string | null>(null)
const selectedIcon = ref<string | null>(null)
const openArticleTitle = ref<string | null>(null)
const openArticleContent = ref<any>(null)
const isLoading = ref(false)
const currentTime = ref('')
const isDesktopLoading = ref(true)
@@ -425,6 +458,23 @@ const toggleMinimize = (windowName: keyof typeof windows) => {
}
}
const handleArticleOpened = (title: string, content: BlogCollectionItem) => {
openArticleTitle.value = title
openArticleContent.value = content
windows.article = true
activeWindow.value = 'article'
}
const handleArticleClosed = () => {
openArticleTitle.value = null
openArticleContent.value = null
windows.article = false
minimizedWindows.article = false
if (activeWindow.value === 'article') {
activeWindow.value = null
}
}
const startShutdown = async () => {
showStartMenu.value = false
isShuttingDown.value = true