refactor About, Blog, Contact, and Desktop components for improved responsiveness and structure; add BlogArticle component for article display
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,54 +86,13 @@ 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) => {
|
||||
@@ -198,15 +100,6 @@ const formatDate = (dateString: string) => {
|
||||
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>
|
||||
|
||||
49
app/components/BlogArticle.vue
Normal file
49
app/components/BlogArticle.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user