remove text based componets, add blog, minor theming changes

This commit is contained in:
2026-01-30 17:58:52 -05:00
parent 24b8a6c4ec
commit d5d0fb904a
18 changed files with 1510 additions and 322 deletions

View File

@@ -0,0 +1,196 @@
/* Nuxt Content Blog Prose Styling
This file provides styles for blog content rendered via Nuxt Content.
It includes general typography styles as well as specific styles
for Shiki code blocks.
*/
.blog-prose {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.8;
color: #1a1a1a;
}
/* Headings */
.blog-prose :is(h1, h2, h3, h4) {
color: #000080;
font-weight: 700;
}
.blog-prose h1 {
font-size: 2.25rem;
margin: 2rem 0 1rem;
padding-bottom: 0.5rem;
border-bottom: 3px solid #000080;
}
.blog-prose h2 {
font-size: 1.875rem;
margin: 2rem 0 0.875rem;
padding-bottom: 0.375rem;
border-bottom: 2px solid #808080;
}
.blog-prose h3 {
font-size: 1.5rem;
margin: 1.75rem 0 0.75rem;
}
.blog-prose h4 {
font-size: 1.25rem;
margin: 1.5rem 0 0.5rem;
}
/* Paragraphs + lists */
.blog-prose p {
margin: 0 0 1.25rem;
font-size: 1.0625rem;
}
.blog-prose :is(ul, ol) {
margin: 0 0 1.25rem 1.5rem;
}
.blog-prose li {
margin-bottom: 0.5rem;
line-height: 1.75;
}
/* Links */
.blog-prose a {
color: #0000ff;
text-decoration: underline;
}
.blog-prose a:hover {
color: #0066ff;
background: #f0f0ff;
}
.blog-prose a:visited {
color: #800080;
}
/* Strong / emphasis */
.blog-prose strong {
font-weight: 700;
color: #000;
}
.blog-prose em {
font-style: italic;
}
/* Horizontal rule */
.blog-prose hr {
border: none;
border-top: 2px solid #808080;
margin: 2rem 0;
}
/* Images */
.blog-prose img {
border: 2px solid #808080;
border-radius: 4px;
margin: 1.5rem 0;
max-width: 100%;
height: auto;
box-shadow: 3px 3px 6px rgba(0, 0, 0, 0.2);
}
/* Blockquote */
.blog-prose blockquote {
border-left: 4px solid #000080;
background: #f0f0f0;
padding: 1rem 1.25rem;
margin: 1.5rem 0;
font-style: italic;
border-radius: 0 4px 4px 0;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
}
.blog-prose blockquote p {
margin-bottom: 0.5rem;
}
.blog-prose blockquote p:last-child {
margin-bottom: 0;
}
/* Tables */
.blog-prose table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
border: 2px solid #808080;
}
.blog-prose th {
background: #000080;
color: #fff;
font-weight: 700;
padding: 0.75rem;
text-align: left;
border: 1px solid #808080;
}
.blog-prose td {
padding: 0.75rem;
border: 1px solid #808080;
}
.blog-prose tr:nth-child(even) {
background: #f5f5f5;
}
/* inline code: code that is NOT within a pre tag */
.blog-prose :not(pre) > code {
background: #f5f5f5;
border: 1px solid #d0d0d0;
padding: 0.2em 0.4em;
border-radius: 3px;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
font-size: 0.9em;
color: #d63384;
}
/* =========================================================
Shiki code blocks
========================================================= */
.blog-prose pre.shiki,
.blog-prose pre.shiki * {
background-image: none !important;
filter: none !important;
text-shadow: none !important;
}
/* The Shiki container */
.blog-prose pre.shiki {
margin: 1.5rem 0;
padding: 1.25rem 1.5rem;
border-radius: 10px;
overflow-x: auto;
border: 1px solid rgba(0, 0, 0, 0.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
'Liberation Mono', 'Courier New', monospace;
font-size: 0.95rem;
line-height: 1.6;
}
/* Shiki usually includes a <code> inside the <pre> */
.blog-prose pre.shiki code {
background: transparent !important;
border: 0 !important;
padding: 0 !important;
}
.blog-prose pre.shiki .line {
display: block;
min-height: 1.4em;
}

View File

@@ -22,7 +22,8 @@
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.window-content *,
.window-content h1,
.window-content h2,
.window-content p,
.window-content span,
.window-content div,
@@ -42,22 +43,45 @@
/* CRT Screen Effects */
@layer utilities {
@keyframes flicker {
0% { opacity: 1; }
2% { opacity: 0.98; }
4% { opacity: 1; }
8% { opacity: 0.97; }
10% { opacity: 1; }
100% { opacity: 1; }
0% {
opacity: 1;
}
2% {
opacity: 0.98;
}
4% {
opacity: 1;
}
8% {
opacity: 0.97;
}
10% {
opacity: 1;
}
100% {
opacity: 1;
}
}
@keyframes glow {
0%, 100% { text-shadow: 0 0 10px rgba(0, 255, 65, 0.5); }
50% { text-shadow: 0 0 15px rgba(0, 255, 65, 0.7); }
0%,
100% {
text-shadow: 0 0 10px rgba(0, 255, 65, 0.5);
}
50% {
text-shadow: 0 0 15px rgba(0, 255, 65, 0.7);
}
}
@keyframes blink {
0%, 50% { opacity: 1; }
50.1%, 100% { opacity: 0; }
0%,
50% {
opacity: 1;
}
50.1%,
100% {
opacity: 0;
}
}
.animate-flicker {

View File

@@ -1,23 +1,126 @@
<!-- eslint-disable vue/multi-word-component-names -->
<template>
<pre class="text-terminal font-mono whitespace-pre-wrap">{{ aboutTxt }}</pre>
<div class="h-full overflow-auto p-6">
<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>
<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 items-center gap-2">
<span class="font-semibold 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>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold min-w-25">Occupation:</span>
<span>Software Engineer / Freelancer</span>
</div>
</div>
</div>
</div>
<div class="border-t-2 border-[#808080] pt-4">
<p class="text-gray-700 leading-relaxed">
Hi! I'm Tanner, a software engineer and freelancer from the US. I love coding, gaming, and learning new things.
I'm always looking for opportunities to grow and expand my skillset. Feel free to reach out to me if you have any
questions or just want to chat!
</p>
</div>
</div>
<!-- Skills Section -->
<div class="bg-white border-2 border-[#808080] p-6 shadow-md mb-6">
<h3 class="text-xl font-bold mb-4 flex items-center gap-2">
<Code :size="20" />
Technical Skills
</h3>
<div class="flex flex-wrap gap-2">
<span
v-for="skill in skills"
:key="skill"
class="skill-badge"
>
{{ skill }}
</span>
</div>
</div>
<!-- Interests & Hobbies -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-white border-2 border-[#808080] p-5 shadow-md">
<h3 class="text-lg font-bold mb-3 flex items-center gap-2">
<Target :size="18" />
Interests
</h3>
<ul class="space-y-2">
<li
v-for="interest in interests"
:key="interest"
class="flex items-center gap-2 text-gray-700"
>
<ChevronRight :size="14" class="text-[#000080]" />
{{ interest }}
</li>
</ul>
</div>
<div class="bg-white border-2 border-[#808080] p-5 shadow-md">
<h3 class="text-lg font-bold mb-3 flex items-center gap-2">
<Gamepad2 :size="18" />
Hobbies
</h3>
<ul class="space-y-2">
<li
v-for="hobby in hobbies"
:key="hobby"
class="flex items-center gap-2 text-gray-700"
>
<ChevronRight :size="14" class="text-[#000080]" />
{{ hobby }}
</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
const aboutTxt = `
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- Reading file about_me.inf ----
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- File read successful ----
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Name: SticksDev (Tanner Sommers)
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Age: 21
@@@@@@@@@@@****+++=@@@@@@@@@ Location: United States (UTC-5)
@@@@@@@@@=-@@@@@@@@@@@@@@@@@ Occupation: Software Engineer/Freelancer
@@@@@@@@@.:@@@@@@@@@@@@@@@@@ Skills: JavaScript, TypeScript, React, Svelte, Node.js, Python, C#, Java
@@@@@@@@@@@------@@@@@@@@@@@ Interests: Web Development, Game Development, Cybersecurity
@@@@@@@@@@@@@@@@@--@@@@@@@@@ Hobbies: Coding, Gaming, Chess, Music
@@@@@@@@@==+++***@@@@@@@@@@@ Bio:
@@@@@@@@@%%%%@@%@@@@@@@@@@@@ Hi! I'm Tanner, a software engineer and freelancer from the United States.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ I love coding, gaming, and learning new things. I'm always looking for new
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ opportunities to grow and expand my skillset. Feel free to reach out to me
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ if you have any questions or just want to chat!
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- End of file ----
`
import { User, Code, Target, Gamepad2, ChevronRight } from 'lucide-vue-next'
const skills = ['JavaScript', 'TypeScript', 'Vue.js', 'React', 'Node.js', 'Python', 'C#', 'Java']
const interests = ['Web Development', 'Game Development', 'Cybersecurity']
const hobbies = ['Coding', 'Gaming', 'Chess', 'Music']
</script>
<style scoped>
.profile-avatar {
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.1);
}
.skill-badge {
color: #ffffff;
padding: 0.375rem 0.75rem;
border: 2px solid #808080;
font-size: 0.875rem;
font-weight: 600;
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.2);
transition: transform 0.2s, box-shadow 0.2s, background-color 0.2s;
}
.skill-badge:hover {
background-color: #1084d0;
transform: translateY(-1px);
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2);
}
</style>

View File

@@ -0,0 +1,148 @@
<template>
<Teleport to="body">
<div
v-if="isOpen"
id="aboutDiag"
class="fixed inset-0 flex items-center justify-center z-50"
@click.self="close"
>
<div
class="bg-[#c0c0c0] border-2 border-white border-r-[#808080] border-b-[#808080] shadow-lg"
style="width: 400px"
@mousedown="startDrag"
>
<!-- Title Bar -->
<div
ref="titleBar"
class="bg-linear-to-r from-[#0054E3] to-[#3C8CF7] px-2 py-1 flex justify-between items-center cursor-move"
>
<div class="flex items-center gap-1 text-white font-bold text-sm">
<span>About ShrimpOS</span>
</div>
<button
class="bg-[#c0c0c0] border-2 border-white border-r-[#808080] border-b-[#808080] w-6 h-6 flex items-center justify-center text-xs font-bold hover:bg-[#dfdfdf]"
@click="close"
>
<X :size="60" />
</button>
</div>
<!-- Content -->
<div class="p-6">
<div class="flex items-start gap-4 mb-4">
<div class="text-6xl">🦐</div>
<div class="flex-1">
<h2 class="text-2xl font-bold mb-2">ShrimpOS</h2>
<p class="text-sm mb-1">Version 1.0</p>
<p class="text-xs text-gray-600 mb-4">© 2026 ShrimpOS. All rights reserved.</p>
</div>
</div>
<div class="border-t-2 border-[#808080] border-b-2 pt-3 pb-3 mb-4">
<p class="text-sm mb-3">
<strong>ShrimpOS is activated.</strong>
</p>
<p class="text-xs text-gray-700 leading-relaxed">
If you encounter any issues or have questions, please contact us
at 1-800-SHRIMP-OS or visit our website at
<a href="https://teamhydra.dev" class="text-blue-600 underline" target="_blank">www.shrimpos.com/support</a>.
</p>
</div>
<div class="mb-4">
<p class="text-xs font-bold mb-2">ShrimpOS is proudly powered by:</p>
<div class="space-y-1 text-xs">
<div class="flex items-center gap-2">
<span class="text-[#42b883]"></span>
<span>Vue.js - Progressive JavaScript Framework</span>
</div>
<div class="flex items-center gap-2">
<span class="text-[#00DC82]"></span>
<span>Nuxt 3 - The Intuitive Vue Framework</span>
</div>
<div class="flex items-center gap-2">
<span class="text-[#F38020]"></span>
<span>Cloudflare Pages - Fast & Secure Hosting</span>
</div>
<div class="flex items-center gap-2">
<span class="text-[#F6821F]"></span>
<span>Cloudflare D1 - Serverless SQL Database</span>
</div>
</div>
</div>
<div class="flex justify-end">
<button
class="bg-[#c0c0c0] border-2 border-white border-r-[#808080] border-b-[#808080] px-6 py-1 text-sm hover:bg-[#dfdfdf] active:border-[#808080] active:border-r-white active:border-b-white"
@click="close"
>
OK
</button>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { X } from 'lucide-vue-next'
import { ref } from 'vue'
defineProps<{
isOpen: boolean
}>()
defineOptions({
name: 'AboutShrimpOS'
})
const emit = defineEmits<{
close: []
}>()
const titleBar = ref<HTMLElement | null>(null)
const isDragging = ref(false)
const dragOffset = ref({ x: 0, y: 0 })
const close = () => {
emit('close')
}
const startDrag = (e: MouseEvent) => {
// Only start dragging if clicking on the title bar, not the close button
if ((e.target as HTMLElement).closest('.bg-gradient-to-r') && !(e.target as HTMLElement).closest('button')) {
isDragging.value = true
const dialog = (e.currentTarget as HTMLElement)
const rect = dialog.getBoundingClientRect()
dragOffset.value = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
}
document.addEventListener('mousemove', drag)
document.addEventListener('mouseup', stopDrag)
}
}
const drag = (e: MouseEvent) => {
if (!isDragging.value) return
const dialog = document.getElementById('aboutDiag')?.firstElementChild as HTMLElement
if (dialog) {
dialog.style.position = 'fixed'
dialog.style.left = `${e.clientX - dragOffset.value.x}px`
dialog.style.top = `${e.clientY - dragOffset.value.y}px`
}
}
const stopDrag = () => {
isDragging.value = false
document.removeEventListener('mousemove', drag)
document.removeEventListener('mouseup', stopDrag)
}
</script>
<style scoped>
.cursor-move {
cursor: move;
}
</style>

220
app/components/Blog.vue Normal file
View File

@@ -0,0 +1,220 @@
<!-- eslint-disable vue/multi-word-component-names -->
<template>
<div class="blog-container h-full overflow-auto">
<!-- Blog Header -->
<div class="mb-6 pb-4 border-b-2 border-[#808080]">
<h1 class="text-2xl font-bold mb-2 flex items-center gap-2">
<FileText :size="24" />
Blog Posts
</h1>
<p class="text-gray-600">The inner machinations of my mind are an enigma.</p>
</div>
<!-- Blog Posts List -->
<div v-if="pending" class="flex items-center justify-center py-12">
<div class="text-center">
<div class="flex justify-center mb-2">
<Loader2 :size="40" class="animate-spin text-gray-600" />
</div>
<p class="text-gray-600">Loading posts...</p>
</div>
</div>
<div v-else-if="error" class="bg-red-100 border-2 border-red-400 p-4">
<p class="text-red-700 font-bold flex items-center gap-2">
<AlertCircle :size="20" />
Error loading blog posts
</p>
<p class="text-red-600 text-sm mt-1">{{ error.message }}</p>
</div>
<div v-else-if="sortedPosts && sortedPosts.length > 0" class="space-y-4">
<article
v-for="post in sortedPosts"
:key="post.path"
class="blog-post bg-white border-2 border-[#808080] p-4 hover:border-[#000080] cursor-pointer transition-colors"
@click="openPost(post.path)"
>
<h2 class="text-xl font-bold text-[#000080] mb-2">{{ post.title }}</h2>
<div class="text-sm text-gray-500 mb-3 flex items-center gap-3">
<span class="flex items-center gap-1">
<Calendar :size="14" />
{{ formatDate(post.date) }}
</span>
<span v-if="post.tags && post.tags.length > 0" class="flex items-center gap-1">
<Tag :size="14" />
{{ post.tags.join(', ') }}
</span>
</div>
<p class="text-gray-700 mb-3">{{ post.description }}</p>
<button
class="px-3 py-1 bg-[#c0c0c0] border-2 border-white border-r-[#808080] border-b-[#808080] text-sm font-bold hover:bg-[#dfdfdf] flex items-center gap-1"
>
Read More
<ArrowRight :size="14" />
</button>
</article>
</div>
<div v-else class="bg-yellow-50 border-2 border-yellow-400 p-6 text-center">
<p class="text-yellow-700 text-lg font-bold mb-2 flex items-center justify-center gap-2">
<Inbox :size="24" />
No blog posts yet
</p>
<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 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 { data: posts, pending, error } = await useAsyncData('blog-posts', () => {
return queryCollection('blog').all()
})
const sortedPosts = computed(() => {
if (!posts.value) return []
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 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
}
}
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>
.blog-post {
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.2);
}
.blog-post:active {
transform: translateY(1px);
box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2);
}
</style>

View File

@@ -1,34 +1,95 @@
<!-- eslint-disable vue/multi-word-component-names -->
<template>
<div class="font-mono">
<h1 class="text-2xl font-bold mb-2">Contact</h1>
<p class="text-terminal-dim mb-2">
You can reach me at the following email address:
<div class="h-full overflow-auto p-6">
<h1 class="text-3xl font-bold mb-6 text-[#000080] border-b-2 border-[#808080] pb-3">Contact Me</h1>
<p class="text-gray-700 mb-6 leading-relaxed">
I'd love to hear from you! Whether you have a question, want to collaborate, or just want to say hi,
feel free to reach out through any of the following channels.
</p>
<!-- Contact Cards -->
<div class="space-y-4">
<!-- Email Card -->
<a
href="mailto:tanner@teamhydra.dev"
class="text-terminal-dim hover:text-terminal hover:underline transition-colors"
class="contact-card bg-white border-2 border-[#808080] p-5 shadow-md flex items-start gap-4 hover:border-[#000080] transition-colors group"
>
tanner@teamhydra.dev
<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>
<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>
</a>
<br />
<span class="text-terminal-dim">-- or --</span>
<br />
<!-- Discord Card -->
<a
href="https://discord.gg/zira"
target="_blank"
class="text-terminal-dim hover:text-terminal hover:underline transition-colors"
class="contact-card bg-white border-2 border-[#808080] p-5 shadow-md flex items-start gap-4 hover:border-[#000080] transition-colors group"
>
Via Discord
<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>
<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>
</p>
<p class="text-gray-700">
<span class="font-semibold">Channel:</span> #other-support
</p>
</div>
</a>
<p class="text-terminal-dim mt-2">
Please use the #other-support channel to get in touch with me. My
username is sticksdev.
</p>
<p class="text-terminal-dim mt-2">
I look forward to hearing from you soon :)
<!-- GitHub Card -->
<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"
>
<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>
<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>
</a>
</div>
<!-- Response Time Note -->
<div class="mt-6 bg-[#ffffcc] border-2 border-[#808080] p-4">
<p class="text-sm text-gray-700">
<Lightbulb :size="16" class="inline mr-1 text-yellow-600" />
<span class="font-bold">Note:</span> I typically respond within 24-48 hours. Looking forward to connecting with you!
</p>
</div>
</div>
</template>
<script setup lang="ts">
import { Mail, Lightbulb } from 'lucide-vue-next'
import { GitHubIcon, DiscordIcon } from 'vue3-simple-icons'
</script>
<style scoped>
.contact-card {
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.contact-card:hover {
transform: translateY(-2px);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.15);
}
.contact-icon {
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.2);
}
</style>

View File

@@ -5,16 +5,22 @@
@click="$emit('click')"
@dblclick="$emit('dblclick')"
>
<div class="icon-image w-12 h-12 mb-1 flex items-center justify-center text-4xl">
{{ icon }}
<div class="icon-image w-12 h-12 mb-1 flex items-center justify-center">
<component
:is="icon"
:size="48"
color="white"
/>
</div>
<span class="text-white text-xs text-center drop-shadow-lg">{{ label }}</span>
</div>
</template>
<script setup lang="ts">
import type { Component } from 'vue'
defineProps<{
icon: string
icon: Component
label: string
isSelected?: boolean
}>()

View File

@@ -1,84 +1,116 @@
<template>
<div class="font-mono">
<h1 class="text-2xl font-bold mb-2">Experience</h1>
<p class="text-terminal-dim mb-4">
Use the buttons below to navigate through my experience.
</p>
<div class="flex justify-between mb-4">
<button
@click="handlePrev"
:disabled="index === 0"
class="text-terminal-dim hover:text-terminal transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
Previous
</button>
<button
@click="handleNext"
:disabled="index === experience.length - 1"
class="text-terminal-dim hover:text-terminal transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
>
Next
</button>
<div class="h-full overflow-auto p-6">
<h1 class="text-3xl font-bold mb-6 text-[#000080] border-b-2 border-[#808080] pb-3">Work Experience</h1>
<div class="space-y-6">
<!-- Level.io -->
<div class="experience-card bg-white border-2 border-[#808080] p-5 shadow-md">
<div class="flex items-start gap-4 mb-4">
<div class="logo-container bg-white border border-gray-300 p-3 rounded flex items-center justify-center" style="min-width: 100px; height: 80px;">
<img
src="https://cdn.prod.website-files.com/65707faecd4cd453c0bb80ad/657087ef22ae05f257ad0b7b_Logo%20(1).svg"
alt="Level.io"
class="max-w-full max-h-full object-contain"
/>
</div>
<div class="flex-1">
<h2 class="text-2xl font-bold text-[#000080] mb-1">Support Specialist</h2>
<h3 class="text-lg font-semibold text-gray-700 mb-2">Level.io</h3>
<p class="text-sm text-gray-600">October 2024 - Present</p>
</div>
</div>
<div class="ml-0 md:ml-28">
<p class="text-gray-700 leading-relaxed">
Currently working as a Support Specialist at Level.io, helping customers solve technical issues and improve their experience with the platform.
Providing technical support, troubleshooting, and working closely with the engineering team to resolve complex problems.
</p>
</div>
</div>
<!-- Gordon Food Services -->
<div class="experience-card bg-white border-2 border-[#808080] p-5 shadow-md">
<div class="flex items-start gap-4 mb-4">
<div class="logo-container bg-white border border-gray-300 p-3 rounded flex items-center justify-center" style="min-width: 100px; height: 80px;">
<img
src="https://gfs.com/wp-content/uploads/2022/07/logo_gfs.png"
alt="Gordon Food Services"
class="max-w-full max-h-full object-contain"
/>
</div>
<div class="flex-1">
<h2 class="text-2xl font-bold text-[#000080] mb-1">Network Engineer / IT Specialist</h2>
<h3 class="text-lg font-semibold text-gray-700 mb-2">Gordon Food Services</h3>
<p class="text-sm text-gray-600">June 2022 - June 2024</p>
</div>
</div>
<div class="ml-0 md:ml-28">
<p class="text-gray-700 leading-relaxed mb-3">
Worked with Gordon Food Services to help maintain their network infrastructure and provide IT support to employees.
Responsible for troubleshooting network issues, setting up new network equipment, and providing support with IT issues.
</p>
<ul class="list-disc list-inside text-gray-700 space-y-1 text-sm">
<li>Maintained and troubleshot network infrastructure</li>
<li>Set up and configured network equipment</li>
<li>Provided technical support to employees</li>
<li>Managed IT helpdesk operations</li>
</ul>
</div>
</div>
<!-- Team Hydra -->
<div class="experience-card bg-white border-2 border-[#808080] p-5 shadow-md">
<div class="flex items-start gap-4 mb-4">
<div class="logo-container bg-white border border-gray-300 p-3 rounded flex items-center justify-center" style="min-width: 100px; height: 80px;">
<img
src="https://hep.gg/hydralogo"
alt="Team Hydra"
class="max-w-full max-h-full object-contain"
/>
</div>
<div class="flex-1">
<h2 class="text-2xl font-bold text-[#000080] mb-1">Software Developer</h2>
<h3 class="text-lg font-semibold text-gray-700 mb-2">Team Hydra</h3>
<p class="text-sm text-gray-600">September 2020 - Present</p>
</div>
</div>
<div class="ml-0 md:ml-28">
<p class="text-gray-700 leading-relaxed mb-3">
Working with Team Hydra on a variety of projects, including developing web applications, mobile apps, Discord bots, and APIs.
Great team environment with continuous learning opportunities.
</p>
<ul class="list-disc list-inside text-gray-700 space-y-1 text-sm">
<li>Developed web applications and APIs</li>
<li>Built Discord bots and integrations</li>
<li>Created mobile applications</li>
<li>Collaborated on various client projects</li>
</ul>
</div>
</div>
<div>
<pre class="whitespace-pre-wrap text-terminal">{{ experience[index] }}</pre>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const experience = [
`
@@@@@@@@@@
@@@@@*:@@@@@
@#*@+**-@:@@
@#@@@ @@@**@**@:*@@ @@@*@
@@+@@@:@@@@@*%*@**@*.%@@@@@:@@@+@@
@@@@@@@@@@@@@@*@*:#*@@@@@***:@#@@@**:*@*@@@@@@@@@@@%@@ Company Name: Team Hydra
@@@%%@@***********@@********@@**+*******+@@**@@@ Position: Software Developer
@@@#%@********+*@********@*************@@@ Start Date: 2020-09-01
@@@@%********.*@*%**@*@+.*********@@@@ End: Present
@@@@#**@*%**+@@*@@@@+@@+****@**+@@@@ About:
@@@@**@@*****@@*@@=@@*****@@%*@@@@ I've worked with team hydra on a variety of projects,
@ @@+*@@@*****@#@@*****@@@#*@@ @ including developing web applications, mobile apps, discord bots,
@@@ @@%%@@@@@***@@***@@@@@%@@@ @@@ and APIs. It's a great team to work with, and I've learned a lot.
@@@%@@@@*****+**@@@@%@@@ I highly recommend them to anyone looking for software development
@@@%%%***#@**@****%%%@@@ services and a career in software development.
@@@@@@@@@@@@@@@@@@@@@@@@@@
@@ @@
`,
`
*###########*
################*
######## *####
###### *
*####*
##### #########
##### ######### ordon food services
#####* ######### Position: Network Engineer/IT Specialist
###### *#### Start Date: 2022-06-01
#######* =###### End: 2024-06-01
################# About: I worked with Gordon food services to help maintain their network infrastructure
############* and provide IT support to their employees. I was responsible for troubleshooting network
####### issues, setting up new network equipment, and providing support to employees with IT issues.
#### I was laid off, but I enjoyed my time there and learned a lot about network engineering and
* IT support.
`,
]
const index = ref(0)
const handleNext = () => {
if (index.value < experience.length - 1) {
index.value++
}
}
const handlePrev = () => {
if (index.value > 0) {
index.value--
}
}
// No script needed for static content
</script>
<style scoped>
.experience-card {
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.experience-card:hover {
transform: translateY(-2px);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.15);
}
.logo-container img {
filter: grayscale(0%);
transition: filter 0.2s;
}
.logo-container:hover img {
filter: grayscale(0%) brightness(1.1);
}
</style>

View File

@@ -1,57 +1,128 @@
<template>
<div class="font-mono">
<h1 class="text-terminal text-md mb-4">
--- Reading database projects.db ---<br>
Found {{ projects.length }} projects in database. Displaying all projects:
</h1>
<div v-for="(project, index) in preparedProjects" :key="index" class="flex flex-row flex-wrap mb-1">
<p class="text-terminal">{{ project.title }}</p>
<span class="text-terminal">&nbsp;|&nbsp;</span>
<p class="text-terminal">{{ project.description }}</p>
<span class="text-terminal">|</span>
<div class="h-full overflow-auto p-6">
<h1 class="text-3xl font-bold mb-6 text-black border-b-2 border-[#808080] pb-3">My Projects</h1>
<p class="text-gray-700 mb-6 leading-relaxed">
Here are some of my featured projects. Check them out on GitHub!
</p>
<!-- Projects Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div
v-for="(project, index) in projects"
:key="index"
class="project-card bg-white border-2 border-[#808080] p-5 shadow-md hover:border-[#000080] transition-all group"
>
<div class="flex items-start justify-between mb-3">
<div class="project-icon">
<component
:is="project.icon"
:size="32"
:class="['transition-colors', 'group-hover:text-[#1084d0]']"
/>
</div>
<a
:href="project.link"
class="text-terminal-dim hover:text-terminal transition-colors ml-1"
target="_blank"
class="px-3 py-1 bg-[#c0c0c0] border-2 border-white border-r-[#808080] border-b-[#808080] text-xs font-bold hover:bg-[#dfdfdf] flex items-center gap-1"
>
{{ project.link }}
View on GitHub
</a>
</div>
<h3 class="text-xl font-bold text-black mb-2">{{ project.title }}</h3>
<p class="text-gray-700 text-sm mb-3 leading-relaxed">{{ project.description }}</p>
<div class="flex flex-wrap gap-2">
<span
v-for="(tech, techIndex) in project.technologies"
:key="techIndex"
class="tech-badge text-xs px-2 py-1 bg-[#f0f0f0] border border-[#808080] text-gray-700"
>
{{ tech }}
</span>
</div>
</div>
</div>
<!-- More Projects CTA -->
<div class="mt-6 bg-white border-2 border-[#808080] p-5 shadow-md text-center">
<p class="text-gray-700 mb-3">
Want to see more? Check out my GitHub profile for all my projects and contributions!
</p>
<a
href="https://github.com/SticksDev"
target="_blank"
class="inline-block px-6 py-2 bg-[#24292e] text-white border-2 border-[#808080] font-bold hover:bg-[#1a1e22] transition-colors"
>
Visit GitHub Profile
</a>
</div>
<h1 class="text-terminal text-md mt-4">
--- End of database ---
</h1>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { Component } from 'vue'
import { Printer, Globe, Sparkles } from 'lucide-vue-next'
const projects = [
type Project = {
title: string
description: string
link: string
icon: Component
technologies: string[]
}
const projects: Project[] = [
{
title: 'BambuConnect',
description: 'A simple 3rd party client for managing your 3D printer.',
description:
'A third-party client for managing your Bambu Lab 3D printer with an intuitive interface and real-time monitoring capabilities.',
link: 'https://github.com/SticksDev/BambuConnect',
icon: Printer,
technologies: ['TypeScript', 'Electron', '3D Printing'],
},
{
title: 'VRDCN_NetworkTest',
description: 'A simple network test for a VR Streaming service. Written in Go.',
description:
'A network testing tool designed for VR streaming services, built with Go for high performance and reliability.',
link: 'https://github.com/SticksDev/VRCDN_NetworkTest',
icon: Globe,
technologies: ['Go', 'Networking', 'VR'],
},
{
title: 'Runic Spells',
description: 'A simple spell system for Minecraft using Java and PaperMC APIs.',
description:
'A comprehensive spell system for Minecraft servers using Java and PaperMC APIs, featuring custom magical abilities.',
link: 'https://github.com/SticksDev/runic_spells',
icon: Sparkles,
technologies: ['Java', 'Minecraft', 'PaperMC'],
},
]
const preparedProjects = computed(() => {
const maxTitleLength = Math.max(...projects.map((project) => project.title.length))
const maxDescriptionLength = Math.max(...projects.map((project) => project.description.length))
return projects.map((project) => ({
...project,
title: project.title.padEnd(maxTitleLength, ' '),
description: project.description.padEnd(maxDescriptionLength, ' '),
}))
})
</script>
<style scoped>
.project-card {
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.project-card:hover {
transform: translateY(-2px);
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.15);
}
.project-icon {
transition: transform 0.2s, color 0.2s;
}
.project-card:hover .project-icon {
transform: scale(1.1);
}
.tech-badge {
font-family: 'Courier New', monospace;
font-weight: 600;
}
</style>

View File

@@ -0,0 +1,72 @@
<template>
<Transition name="taskbar-fade">
<button
v-if="isOpen"
class="taskbar-item px-3 py-1 border-2 text-sm"
:class="[
isActive
? 'bg-[#000080] text-white border-[#808080] border-r-white border-b-white active-button'
: 'bg-[#c0c0c0] border-white border-r-[#808080] border-b-[#808080]'
]"
@click="$emit('click')"
>
<div class="flex items-center gap-1 icon-wrapper">
<component
:is="icon"
:size="16"
/>
<span>{{ label }}</span>
</div>
</button>
</Transition>
</template>
<script setup lang="ts">
import type { Component } from 'vue'
defineProps<{
icon: Component
label: string
isOpen: boolean
isActive: boolean
}>()
defineEmits<{
click: []
}>()
</script>
<style scoped>
.taskbar-item {
transition: background-color 0.15s ease, color 0.15s ease;
}
/* Default inactive hover */
.taskbar-item:not(.active-button):hover {
background: #000080;
color: #fff;
}
/* Disable hover effects entirely on active */
.taskbar-item.active-button:hover {
background: #000080; /* same as active */
color: white;
cursor: default;
}
.taskbar-fade-enter-active,
.taskbar-fade-leave-active {
transition: all 0.3s ease;
}
.taskbar-fade-enter-from {
opacity: 0;
transform: translateY(10px);
}
.taskbar-fade-leave-to {
opacity: 0;
transform: translateY(10px);
}
</style>

View File

@@ -1,3 +1,4 @@
<!-- eslint-disable vue/multi-word-component-names -->
<template>
<div
ref="windowRef"
@@ -17,9 +18,7 @@
class="close-btn 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="$emit('close')"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-10">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
<X :size="60" />
</button>
</div>
@@ -31,6 +30,7 @@
</template>
<script setup lang="ts">
import { X } from 'lucide-vue-next';
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps<{

View File

@@ -41,7 +41,7 @@
<Transition name="icon-fade">
<DesktopIcon
v-if="visibleIcons >= 1"
icon="👤"
:icon="User"
label="About Me"
@dblclick="openWindow('about')"
/>
@@ -49,7 +49,7 @@
<Transition name="icon-fade">
<DesktopIcon
v-if="visibleIcons >= 2"
icon="💼"
:icon="Briefcase"
label="Projects"
@dblclick="openWindow('projects')"
/>
@@ -57,7 +57,7 @@
<Transition name="icon-fade">
<DesktopIcon
v-if="visibleIcons >= 3"
icon="🎓"
:icon="GraduationCap"
label="Experience"
@dblclick="openWindow('experience')"
/>
@@ -65,11 +65,19 @@
<Transition name="icon-fade">
<DesktopIcon
v-if="visibleIcons >= 4"
icon="📧"
:icon="Mail"
label="Contact"
@dblclick="openWindow('contact')"
/>
</Transition>
<Transition name="icon-fade">
<DesktopIcon
v-if="visibleIcons >= 5"
:icon="FileText"
label="Blog"
@dblclick="openWindow('blog')"
/>
</Transition>
</div>
<!-- Windows -->
@@ -129,6 +137,20 @@
<Contact />
</Window>
<Window
v-if="windows.blog"
title="Blog - ShrimpOS"
:initial-x="350"
:initial-y="100"
:width="1000"
:height="650"
:is-active="activeWindow === 'blog'"
@close="closeWindow('blog')"
@activate="activeWindow = 'blog'"
>
<Blog />
</Window>
<!-- Start Menu -->
<div
v-if="showStartMenu"
@@ -146,48 +168,65 @@
@click="openWindow('about'); showStartMenu = false"
class="menu-item w-full text-left px-4 py-2 hover:bg-[#000080] hover:text-white flex items-center gap-3 border-b border-gray-400"
>
<span class="text-lg">👤</span>
<User/>
<span>About Me</span>
</button>
<button
@click="openWindow('projects'); showStartMenu = false"
class="menu-item w-full text-left px-4 py-2 hover:bg-[#000080] hover:text-white flex items-center gap-3 border-b border-gray-400"
>
<span class="text-lg">💼</span>
<Briefcase/>
<span>Projects</span>
</button>
<button
@click="openWindow('experience'); showStartMenu = false"
class="menu-item w-full text-left px-4 py-2 hover:bg-[#000080] hover:text-white flex items-center gap-3 border-b border-gray-400"
>
<span class="text-lg">🎓</span>
<GraduationCap/>
<span>Experience</span>
</button>
<button
@click="openWindow('contact'); showStartMenu = false"
class="menu-item w-full text-left px-4 py-2 hover:bg-[#000080] hover:text-white flex items-center gap-3 border-b border-gray-400"
>
<span class="text-lg">📧</span>
<Mail/>
<span>Contact</span>
</button>
<button
@click="openWindow('blog'); showStartMenu = false"
class="menu-item w-full text-left px-4 py-2 hover:bg-[#000080] hover:text-white flex items-center gap-3 border-b border-gray-400"
>
<FileText/>
<span>Blog</span>
</button>
<button
@click="showAbout = true; showStartMenu = false"
class="menu-item w-full text-left px-4 py-2 hover:bg-[#000080] hover:text-white flex items-center gap-3 border-b border-gray-400"
>
<Info/>
<span>About ShrimpOS...</span>
</button>
<button
@click="startShutdown"
class="menu-item w-full text-left px-4 py-2 hover:bg-[#000080] hover:text-white flex items-center gap-3"
>
<span class="text-lg">🔌</span>
<Power/>
<span>Shut Down...</span>
</button>
</div>
</div>
<!-- About ShrimpOS Dialog -->
<AboutShrimpOS :is-open="showAbout" @close="showAbout = false" />
<!-- Shutdown Screen -->
<div
v-if="isShuttingDown"
class="absolute inset-0 bg-black flex flex-col items-center justify-center z-[100]"
>
<div class="text-center">
<h2 class="text-4xl font-bold mb-8" style="color: #ff8c00;">{{ shutdownMessage }}</h2>
<div v-if="shutdownStage === 'shutting-down'" class="w-64 h-2 bg-gray-800 overflow-hidden mx-auto">
<div class="text-center flex flex-col items-center">
<h2 class="text-4xl font-bold mb-8 text-white">{{ shutdownMessage }}</h2>
<div v-if="shutdownStage === 'shutting-down'" class="w-64 h-2 bg-gray-800 overflow-hidden">
<div class="loading-bar h-full bg-gradient-to-r from-blue-500 via-blue-400 to-blue-500"></div>
</div>
</div>
@@ -205,38 +244,41 @@
</button>
<div class="flex-1 flex gap-1 ml-2">
<button
v-if="windows.about"
class="taskbar-item px-3 py-1 bg-[#c0c0c0] border-2 text-sm"
:class="activeWindow === 'about' ? 'border-[#808080] border-r-white border-b-white' : 'border-white border-r-[#808080] border-b-[#808080]'"
<TaskbarButton
:icon="User"
label="About Me"
:isOpen="windows.about"
:isActive="activeWindow === 'about'"
@click="activeWindow = 'about'"
>
👤 About Me
</button>
<button
v-if="windows.projects"
class="taskbar-item px-3 py-1 bg-[#c0c0c0] border-2 text-sm"
:class="activeWindow === 'projects' ? 'border-[#808080] border-r-white border-b-white' : 'border-white border-r-[#808080] border-b-[#808080]'"
/>
<TaskbarButton
:icon="Briefcase"
label="Projects"
:isOpen="windows.projects"
:isActive="activeWindow === 'projects'"
@click="activeWindow = 'projects'"
>
💼 Projects
</button>
<button
v-if="windows.experience"
class="taskbar-item px-3 py-1 bg-[#c0c0c0] border-2 text-sm"
:class="activeWindow === 'experience' ? 'border-[#808080] border-r-white border-b-white' : 'border-white border-r-[#808080] border-b-[#808080]'"
/>
<TaskbarButton
:icon="GraduationCap"
label="Experience"
:isOpen="windows.experience"
:isActive="activeWindow === 'experience'"
@click="activeWindow = 'experience'"
>
🎓 Experience
</button>
<button
v-if="windows.contact"
class="taskbar-item px-3 py-1 bg-[#c0c0c0] border-2 text-sm"
:class="activeWindow === 'contact' ? 'border-[#808080] border-r-white border-b-white' : 'border-white border-r-[#808080] border-b-[#808080]'"
/>
<TaskbarButton
:icon="Mail"
label="Contact"
:isOpen="windows.contact"
:isActive="activeWindow === 'contact'"
@click="activeWindow = 'contact'"
>
📧 Contact
</button>
/>
<TaskbarButton
:icon="FileText"
label="Blog"
:isOpen="windows.blog"
:isActive="activeWindow === 'blog'"
@click="activeWindow = 'blog'"
/>
</div>
<div class="time px-2 border-2 border-[#808080] border-t-white border-l-white text-sm">
@@ -247,6 +289,7 @@
</template>
<script setup lang="ts">
import { User, Briefcase, GraduationCap, Mail, FileText, Power, Info } from 'lucide-vue-next';
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import Window from '~/components/Window.vue'
import DesktopIcon from '~/components/DesktopIcon.vue'
@@ -255,12 +298,15 @@ import About from '~/components/About.vue'
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 AboutShrimpOS from '~/components/AboutShrimpOS.vue'
const windows = reactive({
about: false,
projects: false,
experience: false,
contact: false,
blog: false,
})
const activeWindow = ref<string | null>(null)
@@ -271,6 +317,7 @@ const loadingProgress = ref(0)
const loadingMessage = ref('Loading Desktop...')
const visibleIcons = ref(0)
const showStartMenu = ref(false)
const showAbout = ref(false)
const isShuttingDown = ref(false)
const shutdownStage = ref<'shutting-down' | 'safe-to-turn-off'>('shutting-down')
const shutdownMessage = ref('Shutting down...')
@@ -303,7 +350,7 @@ const startShutdown = async () => {
await new Promise(resolve => setTimeout(resolve, 1500))
// Hide icons one by one (in reverse)
for (let i = 4; i >= 1; i--) {
for (let i = 5; i >= 1; i--) {
visibleIcons.value = i - 1
await new Promise(resolve => setTimeout(resolve, 200))
}
@@ -360,6 +407,7 @@ const startDesktopLoading = async () => {
loadingMessage.value = 'Preparing workspace...'
await new Promise(resolve => setTimeout(resolve, 400))
visibleIcons.value = 5
loadingProgress.value = 100
await new Promise(resolve => setTimeout(resolve, 300))

View File

@@ -1,7 +1,10 @@
<template>
<div class="min-h-screen min-w-screen bg-black flex items-center justify-center overflow-hidden">
<!-- BIOS Screen -->
<div v-if="bootStage === 'bios'" class="w-full h-screen bg-black text-left p-8 font-mono text-sm text-gray-300">
<div v-if="bootStage === 'bios'" class="w-full h-screen bg-black text-left p-8 font-mono text-sm text-gray-300 relative">
<div class="absolute top-2 right-2 text-xs text-gray-500 animate-pulse">
Press ESC to skip
</div>
<div class="mb-4">
<p class="text-white font-bold">ShrimpBIOS v2.0</p>
<p class="text-gray-500">Copyright (C) {{ new Date().getFullYear() }}, SticksDev Inc.</p>
@@ -12,12 +15,15 @@
</div>
<div v-if="biosMessages.length >= 7" class="mt-8">
<p class="animate-pulse">Press any key to boot from HDD...</p>
<p class="animate-pulse">Press any key to boot from HDD or ESC to skip...</p>
</div>
</div>
<!-- Windows XP Style Boot Screen -->
<div v-else-if="bootStage === 'loading'" class="w-full h-screen flex flex-col items-center justify-center relative">
<div class="absolute top-4 right-4 text-xs text-gray-500 animate-pulse">
Press ESC to skip
</div>
<!-- Logo -->
<div class="mb-16 text-center">
<h1 class="text-6xl font-bold text-white mb-2" style="font-family: 'Trebuchet MS', sans-serif; letter-spacing: -2px;">
@@ -55,9 +61,7 @@
<!-- Power icon -->
<div class="relative z-10">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-10 text-white">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.636 5.636a9 9 0 1 0 12.728 0M12 3v9" />
</svg>
<Power color="white" :size="45" />
</div>
<!-- Shine effect -->
@@ -70,6 +74,7 @@
</template>
<script setup lang="ts">
import { Power } from 'lucide-vue-next';
import { ref, onMounted } from 'vue'
const bootStage = ref<'off' | 'bios' | 'loading'>('off')
@@ -103,13 +108,14 @@ const startBootSequence = async () => {
bootAudio!.play().catch(() => {
// Audio autoplay blocked, continue without sound
console.warn('Boot audio playback was blocked, continuing without sound.')
resolve()
})
// Fallback if audio doesn't end (increased to 15 seconds)
// Play 5 seconds of audio then resolve
setTimeout(() => {
resolve()
}, 15000)
}, 4000)
})
}
@@ -132,7 +138,19 @@ const startBootSequence = async () => {
await new Promise(resolve => setTimeout(resolve, 3000))
}
// Navigate to desktop
// Navigate to desktop and fade out audio
if (bootAudio) {
const fadeOutInterval = setInterval(() => {
if (bootAudio!.volume > 0.05) {
bootAudio!.volume -= 0.05
} else {
bootAudio!.volume = 0
bootAudio!.pause()
clearInterval(fadeOutInterval)
}
}, 200)
}
navigateTo('/desktop')
}
@@ -142,12 +160,23 @@ onMounted(() => {
bootAudio = new Audio('/bootup.mp3')
}
// Listen for keypress during BIOS screen
const handleKeyPress = () => {
if (bootStage.value === 'bios' && biosMessages.value.length >= 7) {
bootStage.value = 'loading'
// Listen for keypress during boot screens
const handleKeyPress = (e: KeyboardEvent) => {
// ESC key to skip to desktop
if (e.key === 'Escape' && (bootStage.value === 'bios' || bootStage.value === 'loading')) {
// Stop any playing audio
if (bootAudio) {
bootAudio.pause()
bootAudio.currentTime = 0
}
// Go directly to desktop
navigateTo('/desktop')
document.removeEventListener('keydown', handleKeyPress)
}
// Any key during BIOS to continue to loading
else if (bootStage.value === 'bios' && biosMessages.value.length >= 7) {
bootStage.value = 'loading'
}
}
document.addEventListener('keydown', handleKeyPress)
})

16
content.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import { defineContentConfig, defineCollection, z } from '@nuxt/content'
export default defineContentConfig({
collections: {
blog: defineCollection({
type: 'page',
source: 'blog/**/*.md',
schema: z.object({
title: z.string(),
description: z.string(),
date: z.string(),
tags: z.array(z.string()).optional(),
})
})
}
})

136
content/blog/hello-world.md Normal file
View File

@@ -0,0 +1,136 @@
---
title: Hello, World!
description: Is this thing on?
date: 2025-01-30
tags: ['nuxt', 'content', 'blog']
---
Well Hello, World! Welcome to my new portfolio site built with Nuxt 3 and Nuxt Content. This is my first blog post, and I'm excited to share my thoughts and experiences with you.
I plan to write a variety of posts here, ranging from technical tutorials to personal reflections. Stay tuned for more content!
I want to quickly get into how I made this site.
## Building with Nuxt 3 and Nuxt Content
This all started with me wanting to add some new stuff to my portfolio site. I had previously built it with React and NextJS, but I was very unhappy with JSX for writing content, as well as development speed. A friend of mine had suggested Vue to me a while back, and I had dabbled with it a bit, so I decided to give Vue a try.
While going through the Vue docs and tutorials, I came across Nuxt 3, which is a framework built on top of Vue that provides a lot of great features out of the box, including server-side rendering, static site generation, and more. I was sold on the idea of using Nuxt 3 for my portfolio site.
After learning vue and the basics of Nuxt 3, I wanted to add a blog to my site. I found Nuxt Content, which is a module for Nuxt that allows you to write content in Markdown and other formats and easily integrate it into your Nuxt app. It was exactly what I was looking for.
### Nuxt Content
Nuxt Content made it incredibly easy to add a blog to my site. I could write my posts in Markdown, and Nuxt Content would handle the rest. It provided a simple API for querying and displaying my content, as well as features like syntax highlighting and image optimization.
Here's a code example of how I used Nuxt Content to fetch and display my blog posts:
```vue
<template>
<div>
<h1>Blog Posts</h1>
<ul>
<li v-for="post in posts" :key="post.slug">
<NuxtLink :to="`/blog/${post.slug}`">{{ post.title }}</NuxtLink>
</li>
</ul>
</div>
</template>
<script setup>
const { data: posts } = await useAsyncData('blog-posts', () => {
return await queryCollection('blog').all()
});
</script>
```
That's it! With just a few lines of code, I was able to fetch and display my blog posts on my site.
## The Window System
One of the unique features of my portfolio site is the window system. I wanted to create a desktop-like experience for users, where they could open and close different windows to view my projects and blog posts. In classic windows 95 style!
Currently, windows can be opened and closed, and moved around. Hopefully in the future I can add resizing and minimizing as well.
Here's a code snippet of how I implemented the window system using Vue's component system:
```vue
<template>
<div
v-if="isOpen"
class="window"
:style="{
left: `${x}px`,
top: `${y}px`,
zIndex: isActive ? 100 : 50,
}"
@mousedown="activate"
>
<!-- Window Title Bar -->
<div class="title-bar" @mousedown="startDrag">
<span>{{ title }}</span>
<button @click="close">×</button>
</div>
<!-- Window Content -->
<div class="content">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const props = defineProps<{
title: string;
initialX: number;
initialY: number;
isActive: boolean;
}>();
const emit = defineEmits<{
close: [];
activate: [];
}>();
const x = ref(props.initialX);
const y = ref(props.initialY);
const isDragging = ref(false);
const startDrag = (e: MouseEvent) => {
isDragging.value = true;
const startX = e.clientX - x.value;
const startY = e.clientY - y.value;
const onMove = (e: MouseEvent) => {
if (isDragging.value) {
x.value = e.clientX - startX;
y.value = e.clientY - startY;
}
};
const onUp = () => {
isDragging.value = false;
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
};
const close = () => emit('close');
const activate = () => emit('activate');
</script>
```
Really simple boilerplate for draggable windows! Each window component manages its own position and dragging state. The parent component can handle opening, closing, and activating windows.
## Conclusion
Overall, I'm really happy with how my portfolio site turned out using Nuxt 3 and Nuxt Content. The development experience was smooth and enjoyable, and I was able to add a blog and a unique window system with ease. I'm excited to continue adding more content and features to my site in the future!
Until next time!
-Tanner (or sticks)

View File

@@ -14,7 +14,11 @@ export default defineNuxtConfig({
'@tresjs/nuxt',
],
css: ['./app/assets/css/main.css'],
nitro: {
preset: 'cloudflare_pages',
},
css: ['./app/assets/css/main.css', './app/assets/css/blog-prose.css'],
vite: {
plugins: [tailwindcss()],
},
@@ -38,7 +42,7 @@ export default defineNuxtConfig({
},
{
property: 'og:image',
content: 'https://img.sticks.ovh/stickspfpnew.png',
content: 'https://img.sticks.ovh/sticksnewpfp',
},
{ property: 'twitter:card', content: 'summary_large_image' },
{ property: 'twitter:url', content: 'https://sticksdev.tech' },
@@ -49,14 +53,14 @@ export default defineNuxtConfig({
},
{
property: 'twitter:image',
content: 'https://sticksdev.tech/images/meta-tags.png',
content: 'https://img.sticks.ovh/sticksnewpfp',
},
],
link: [
{
rel: 'icon',
type: 'image/png',
href: 'https://img.sticks.ovh/stickspfpnew.png',
href: 'https://img.sticks.ovh/sticksnewpfp',
},
],
},

22
package-lock.json generated
View File

@@ -20,11 +20,13 @@
"@vueuse/core": "^14.1.0",
"better-sqlite3": "^12.6.2",
"eslint": "^9.39.2",
"lucide-vue-next": "^0.563.0",
"nuxt": "^4.3.0",
"tailwindcss": "^4.1.18",
"three": "^0.182.0",
"vue": "^3.5.27",
"vue-router": "^4.6.4"
"vue-router": "^4.6.4",
"vue3-simple-icons": "^15.6.0"
}
},
"node_modules/@alloc/quick-lru": {
@@ -13949,6 +13951,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-vue-next": {
"version": "0.563.0",
"resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.563.0.tgz",
"integrity": "sha512-zsE/lCKtmaa7bGfhSpN84br1K9YoQ5pCN+2oKWjQQG3Lo6ufUUKBuHSjNFI6RvUevxaajNXb8XwFUKeTXG3sIA==",
"license": "ISC",
"peerDependencies": {
"vue": ">=3.0.1"
}
},
"node_modules/magic-regexp": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/magic-regexp/-/magic-regexp-0.10.0.tgz",
@@ -20389,6 +20400,15 @@
"vue": "^3.5.0"
}
},
"node_modules/vue3-simple-icons": {
"version": "15.6.0",
"resolved": "https://registry.npmjs.org/vue3-simple-icons/-/vue3-simple-icons-15.6.0.tgz",
"integrity": "sha512-e7FkRt5yY6wDmeBzOL7PQw/n5PIiYh5Xbk9f0aAVJpl1E1ddMRLLRjiD9tG8P2UTtCc0Yh48XN3tn0TdZAuL0w==",
"license": "MIT",
"dependencies": {
"vue": "^3"
}
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",

View File

@@ -23,10 +23,12 @@
"@vueuse/core": "^14.1.0",
"better-sqlite3": "^12.6.2",
"eslint": "^9.39.2",
"lucide-vue-next": "^0.563.0",
"nuxt": "^4.3.0",
"tailwindcss": "^4.1.18",
"three": "^0.182.0",
"vue": "^3.5.27",
"vue-router": "^4.6.4"
"vue-router": "^4.6.4",
"vue3-simple-icons": "^15.6.0"
}
}