remove text based componets, add blog, minor theming changes
This commit is contained in:
196
app/assets/css/blog-prose.css
Normal file
196
app/assets/css/blog-prose.css
Normal 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;
|
||||
}
|
||||
@@ -2,124 +2,148 @@
|
||||
|
||||
/* Fallout Terminal Theme */
|
||||
@layer base {
|
||||
:root {
|
||||
--terminal-green: #00ff41;
|
||||
--terminal-green-dim: #00cc33;
|
||||
--terminal-green-dark: #008822;
|
||||
--terminal-bg: #0a0e0a;
|
||||
--terminal-error: #ff0000;
|
||||
}
|
||||
:root {
|
||||
--terminal-green: #00ff41;
|
||||
--terminal-green-dim: #00cc33;
|
||||
--terminal-green-dark: #008822;
|
||||
--terminal-bg: #0a0e0a;
|
||||
--terminal-error: #ff0000;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
body {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
/* Windows content styling */
|
||||
.window-content {
|
||||
color: #000000 !important;
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
/* Windows content styling */
|
||||
.window-content {
|
||||
color: #000000 !important;
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
.window-content *,
|
||||
.window-content p,
|
||||
.window-content span,
|
||||
.window-content div,
|
||||
.window-content pre {
|
||||
color: #000000 !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
.window-content h1,
|
||||
.window-content h2,
|
||||
.window-content p,
|
||||
.window-content span,
|
||||
.window-content div,
|
||||
.window-content pre {
|
||||
color: #000000 !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
.window-content pre {
|
||||
background: #f0f0f0;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.window-content pre {
|
||||
background: #f0f0f0;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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; }
|
||||
}
|
||||
@keyframes flicker {
|
||||
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); }
|
||||
}
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 50% { opacity: 1; }
|
||||
50.1%, 100% { opacity: 0; }
|
||||
}
|
||||
@keyframes blink {
|
||||
0%,
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
50.1%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-flicker {
|
||||
animation: flicker 4s infinite;
|
||||
}
|
||||
.animate-flicker {
|
||||
animation: flicker 4s infinite;
|
||||
}
|
||||
|
||||
.animate-glow {
|
||||
animation: glow 2s ease-in-out infinite;
|
||||
}
|
||||
.animate-glow {
|
||||
animation: glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
.animate-blink {
|
||||
animation: blink 1s step-end infinite;
|
||||
}
|
||||
|
||||
.text-terminal {
|
||||
color: var(--terminal-green);
|
||||
text-shadow: 0 0 5px rgba(0, 255, 65, 0.5);
|
||||
}
|
||||
.text-terminal {
|
||||
color: var(--terminal-green);
|
||||
text-shadow: 0 0 5px rgba(0, 255, 65, 0.5);
|
||||
}
|
||||
|
||||
.text-terminal-dim {
|
||||
color: var(--terminal-green-dim);
|
||||
text-shadow: 0 0 5px rgba(0, 255, 65, 0.5);
|
||||
}
|
||||
.text-terminal-dim {
|
||||
color: var(--terminal-green-dim);
|
||||
text-shadow: 0 0 5px rgba(0, 255, 65, 0.5);
|
||||
}
|
||||
|
||||
.text-terminal-error {
|
||||
color: var(--terminal-error);
|
||||
text-shadow: 0 0 5px rgba(255, 0, 0, 0.5);
|
||||
}
|
||||
.text-terminal-error {
|
||||
color: var(--terminal-error);
|
||||
text-shadow: 0 0 5px rgba(255, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.bg-terminal {
|
||||
background-color: var(--terminal-bg);
|
||||
}
|
||||
.bg-terminal {
|
||||
background-color: var(--terminal-bg);
|
||||
}
|
||||
|
||||
.crt-scanline::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
transparent 50%,
|
||||
rgba(0, 255, 65, 0.05) 50%
|
||||
);
|
||||
background-size: 100% 4px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
.crt-scanline::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
transparent 50%,
|
||||
rgba(0, 255, 65, 0.05) 50%
|
||||
);
|
||||
background-size: 100% 4px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.crt-vignette::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
transparent 0%,
|
||||
rgba(0, 0, 0, 0.3) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 999;
|
||||
}
|
||||
.crt-vignette::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(
|
||||
ellipse at center,
|
||||
transparent 0%,
|
||||
rgba(0, 0, 0, 0.3) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
148
app/components/AboutShrimpOS.vue
Normal file
148
app/components/AboutShrimpOS.vue
Normal 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
220
app/components/Blog.vue
Normal 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>
|
||||
@@ -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:
|
||||
</p>
|
||||
<a
|
||||
href="mailto:tanner@teamhydra.dev"
|
||||
class="text-terminal-dim hover:text-terminal hover:underline transition-colors"
|
||||
>
|
||||
tanner@teamhydra.dev
|
||||
</a>
|
||||
<br />
|
||||
<span class="text-terminal-dim">-- or --</span>
|
||||
<br />
|
||||
<a
|
||||
href="https://discord.gg/zira"
|
||||
target="_blank"
|
||||
class="text-terminal-dim hover:text-terminal hover:underline transition-colors"
|
||||
>
|
||||
Via Discord
|
||||
</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 :)
|
||||
<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="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-[#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>
|
||||
|
||||
<!-- Discord Card -->
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
@@ -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
|
||||
}>()
|
||||
|
||||
@@ -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>
|
||||
<div>
|
||||
<pre class="whitespace-pre-wrap text-terminal">{{ experience[index] }}</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">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>
|
||||
</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>
|
||||
|
||||
@@ -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"> | </span>
|
||||
<p class="text-terminal">{{ project.description }}</p>
|
||||
<span class="text-terminal">|</span>
|
||||
<a
|
||||
:href="project.link"
|
||||
class="text-terminal-dim hover:text-terminal transition-colors ml-1"
|
||||
target="_blank"
|
||||
<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"
|
||||
>
|
||||
{{ project.link }}
|
||||
<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"
|
||||
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"
|
||||
>
|
||||
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>
|
||||
|
||||
72
app/components/TaskbarButton.vue
Normal file
72
app/components/TaskbarButton.vue
Normal 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>
|
||||
@@ -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<{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user