goodbye react, hello vue

This commit is contained in:
2026-01-29 21:23:52 -05:00
parent 8f454e5ce8
commit 24b8a6c4ec
35 changed files with 20258 additions and 4519 deletions

View File

@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

50
.gitignore vendored
View File

@@ -1,36 +1,24 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# dependencies # Node dependencies
/node_modules node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing # Logs
/coverage logs
*.log
# next.js # Misc
/.next/
/out/
# production
/build
# misc
.DS_Store .DS_Store
*.pem .fleet
.idea
# debug # Local env files
npm-debug.log* .env
yarn-debug.log* .env.*
yarn-error.log* !.env.example
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -1,36 +1,75 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). # Nuxt Minimal Starter
## Getting Started Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
First, run the development server: ## Setup
Make sure to install dependencies:
```bash ```bash
npm run dev # npm
# or npm install
yarn dev
# or # pnpm
pnpm dev pnpm install
# or
bun dev # yarn
yarn install
# bun
bun install
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. ## Development Server
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. Start the development server on `http://localhost:3000`:
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ```bash
# npm
npm run dev
## Learn More # pnpm
pnpm dev
To learn more about Next.js, take a look at the following resources: # yarn
yarn dev
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. # bun
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. bun run dev
```
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! ## Production
## Deploy on Vercel Build the application for production:
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. ```bash
# npm
npm run build
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. # pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

7
app/app.vue Normal file
View File

@@ -0,0 +1,7 @@
<template>
<div class="bg-terminal min-h-screen crt-scanline crt-vignette">
<div class="animate-flicker min-h-screen">
<NuxtPage />
</div>
</div>
</template>

125
app/assets/css/main.css Normal file
View File

@@ -0,0 +1,125 @@
@import 'tailwindcss';
/* Fallout Terminal Theme */
@layer base {
: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;
}
/* 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 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 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; }
}
.animate-flicker {
animation: flicker 4s infinite;
}
.animate-glow {
animation: glow 2s ease-in-out 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-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);
}
.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-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;
}
}

View File

@@ -1,6 +1,9 @@
'use client'; <template>
<pre class="text-terminal font-mono whitespace-pre-wrap">{{ aboutTxt }}</pre>
</template>
const AboutTxt = ` <script setup lang="ts">
const aboutTxt = `
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- Reading file about_me.inf ---- @@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- Reading file about_me.inf ----
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- File read successful ---- @@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- File read successful ----
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Name: SticksDev (Tanner Sommers) @@@@@@@@@@@@@@@@@@@@@@@@@@@@ Name: SticksDev (Tanner Sommers)
@@ -16,8 +19,5 @@ const AboutTxt = `
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ opportunities to grow and expand my skillset. Feel free to reach out to me @@@@@@@@@@@@@@@@@@@@@@@@@@@@ opportunities to grow and expand my skillset. Feel free to reach out to me
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ if you have any questions or just want to chat! @@@@@@@@@@@@@@@@@@@@@@@@@@@@ if you have any questions or just want to chat!
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- End of file ---- @@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- End of file ----
`; `
</script>
export default function About() {
return <pre className='text-white font-mono'>{AboutTxt}</pre>;
}

View File

@@ -0,0 +1,34 @@
<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 :)
</p>
</div>
</template>
<script setup lang="ts">
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div
class="desktop-icon flex flex-col items-center cursor-pointer p-2 rounded select-none w-20"
:class="{ 'bg-[#000080] bg-opacity-50': isSelected }"
@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>
<span class="text-white text-xs text-center drop-shadow-lg">{{ label }}</span>
</div>
</template>
<script setup lang="ts">
defineProps<{
icon: string
label: string
isSelected?: boolean
}>()
defineEmits<{
click: []
dblclick: []
}>()
</script>
<style scoped>
.desktop-icon:hover {
background: rgba(0, 0, 128, 0.3);
}
</style>

View File

@@ -0,0 +1,84 @@
<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>
</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--
}
}
</script>

View File

@@ -0,0 +1,57 @@
<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>
<a
:href="project.link"
class="text-terminal-dim hover:text-terminal transition-colors ml-1"
target="_blank"
>
{{ project.link }}
</a>
</div>
<h1 class="text-terminal text-md mt-4">
--- End of database ---
</h1>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
const projects = [
{
title: 'BambuConnect',
description: 'A simple 3rd party client for managing your 3D printer.',
link: 'https://github.com/SticksDev/BambuConnect',
},
{
title: 'VRDCN_NetworkTest',
description: 'A simple network test for a VR Streaming service. Written in Go.',
link: 'https://github.com/SticksDev/VRCDN_NetworkTest',
},
{
title: 'Runic Spells',
description: 'A simple spell system for Minecraft using Java and PaperMC APIs.',
link: 'https://github.com/SticksDev/runic_spells',
},
]
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>

View File

@@ -0,0 +1,116 @@
<template>
<div ref="containerRef" class="shrimp w-full h-full cursor-pointer" @click="toggleRotation"></div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { AsciiEffect } from 'three/examples/jsm/effects/AsciiEffect.js'
const containerRef = ref<HTMLDivElement | null>(null)
const isRotating = ref(false)
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let effect: AsciiEffect
let model: THREE.Group
let animationId: number
const toggleRotation = () => {
isRotating.value = !isRotating.value
}
onMounted(() => {
if (!containerRef.value) return
// Scene setup
scene = new THREE.Scene()
scene.background = null
// Camera
camera = new THREE.PerspectiveCamera(
60,
containerRef.value.clientWidth / containerRef.value.clientHeight,
1,
1000
)
camera.position.set(0, 50, 15)
camera.lookAt(0, 0, 0)
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight)
renderer.setPixelRatio(window.devicePixelRatio)
// ASCII Effect
effect = new AsciiEffect(renderer, ' .:-=+*#%@', { invert: false })
effect.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight)
effect.domElement.style.color = 'rgba(0, 255, 65, 0.7)'
effect.domElement.style.backgroundColor = 'transparent'
containerRef.value.appendChild(effect.domElement)
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, Math.PI / 2)
scene.add(ambientLight)
// Load GLTF model
const loader = new GLTFLoader()
loader.load('/shrimp_smol.glb', (gltf) => {
model = new THREE.Group()
model.position.set(0, -1, 0)
// Find and add the mesh
const mesh = gltf.scene.getObjectByName('Mesh_Mesh_head_geo001_lambert2SG001')
if (mesh) {
mesh.rotation.x = -Math.PI / 2
model.add(mesh)
}
scene.add(model)
})
// Animation loop
const animate = () => {
animationId = requestAnimationFrame(animate)
if (model) {
model.rotation.z += 0.01
}
effect.render(scene, camera)
}
animate()
// Handle window resize
const handleResize = () => {
if (!containerRef.value) return
camera.aspect = containerRef.value.clientWidth / containerRef.value.clientHeight
camera.updateProjectionMatrix()
renderer.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight)
effect.setSize(containerRef.value.clientWidth, containerRef.value.clientHeight)
}
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
if (animationId) {
cancelAnimationFrame(animationId)
}
if (effect && effect.domElement && containerRef.value) {
containerRef.value.removeChild(effect.domElement)
}
window.removeEventListener('resize', () => {})
})
</script>
<style scoped>
.shrimp {
min-height: 400px;
}
</style>

112
app/components/Window.vue Normal file
View File

@@ -0,0 +1,112 @@
<template>
<div
ref="windowRef"
class="window absolute bg-[#c0c0c0] border-2 shadow-lg"
:style="windowStyle"
:class="{ 'z-50': isActive, 'z-10': !isActive }"
@mousedown="bringToFront"
>
<!-- Title Bar -->
<div
class="title-bar flex items-center justify-between px-1 py-1 cursor-move select-none"
:class="isActive ? 'bg-[#000080]' : 'bg-[#808080]'"
@mousedown="startDrag"
>
<span class="text-white font-bold text-sm px-2">{{ title }}</span>
<button
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>
</button>
</div>
<!-- Window Content -->
<div class="window-content bg-white border-2 border-[#808080] border-t-white border-l-white p-6 overflow-auto text-black" :style="contentStyle">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps<{
title: string
initialX?: number
initialY?: number
width?: number
height?: number
isActive?: boolean
}>()
const emit = defineEmits<{
close: []
activate: []
}>()
const windowRef = ref<HTMLElement | null>(null)
const x = ref(props.initialX ?? 100)
const y = ref(props.initialY ?? 100)
const isDragging = ref(false)
const dragStartX = ref(0)
const dragStartY = ref(0)
const windowStyle = computed(() => ({
left: `${x.value}px`,
top: `${y.value}px`,
width: props.width ? `${props.width}px` : '600px',
}))
const contentStyle = computed(() => ({
height: props.height ? `${props.height}px` : '400px',
}))
const startDrag = (e: MouseEvent) => {
isDragging.value = true
dragStartX.value = e.clientX - x.value
dragStartY.value = e.clientY - y.value
emit('activate')
}
const onDrag = (e: MouseEvent) => {
if (!isDragging.value) return
x.value = e.clientX - dragStartX.value
y.value = e.clientY - dragStartY.value
}
const stopDrag = () => {
isDragging.value = false
}
const bringToFront = () => {
emit('activate')
}
onMounted(() => {
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
})
onUnmounted(() => {
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
})
</script>
<style scoped>
.window {
box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.5);
}
.title-bar {
background: linear-gradient(90deg, #000080, #1084d0);
}
.close-btn:active {
border-style: inset;
}
</style>

449
app/pages/desktop.vue Normal file
View File

@@ -0,0 +1,449 @@
<template>
<div class="desktop-container min-h-screen bg-[#008080] relative overflow-hidden" :class="{ 'hourglass-cursor': isLoading || isDesktopLoading }">
<!-- Desktop Background -->
<div class="absolute inset-0 flex flex-col items-center justify-center opacity-50 pointer-events-none">
<div class="w-[600px] h-[600px] pointer-events-auto">
<ClientOnly>
<ShrimpRender />
</ClientOnly>
</div>
<p class="text-white text-3xl font-bold mt-8 drop-shadow-[0_2px_4px_rgba(0,0,0,0.8)]">In shrimp, we trust.</p>
</div>
<!-- Loading Window -->
<div
v-if="isDesktopLoading"
class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-[#c0c0c0] border-2 border-white border-r-[#808080] border-b-[#808080] shadow-lg z-50"
style="width: 400px;"
>
<div class="bg-[#000080] px-2 py-1 flex items-center justify-between">
<span class="text-white font-bold text-sm">Loading ShrimpOS Desktop...</span>
</div>
<div class="bg-white border-2 border-[#808080] border-t-white border-l-white p-6">
<div class="flex items-center gap-4 mb-4">
<div class="text-4xl"></div>
<div>
<p class="text-black font-bold mb-2">Please wait...</p>
<p class="text-black text-sm">{{ loadingMessage }}</p>
</div>
</div>
<div class="w-full bg-[#dfdfdf] border-2 border-[#808080] border-t-white border-l-white h-6">
<div
class="bg-[#000080] h-full transition-all duration-300"
:style="{ width: `${loadingProgress}%` }"
></div>
</div>
</div>
</div>
<!-- Desktop Icons -->
<div class="desktop-icons absolute top-4 left-4 flex flex-col gap-2">
<Transition name="icon-fade">
<DesktopIcon
v-if="visibleIcons >= 1"
icon="👤"
label="About Me"
@dblclick="openWindow('about')"
/>
</Transition>
<Transition name="icon-fade">
<DesktopIcon
v-if="visibleIcons >= 2"
icon="💼"
label="Projects"
@dblclick="openWindow('projects')"
/>
</Transition>
<Transition name="icon-fade">
<DesktopIcon
v-if="visibleIcons >= 3"
icon="🎓"
label="Experience"
@dblclick="openWindow('experience')"
/>
</Transition>
<Transition name="icon-fade">
<DesktopIcon
v-if="visibleIcons >= 4"
icon="📧"
label="Contact"
@dblclick="openWindow('contact')"
/>
</Transition>
</div>
<!-- Windows -->
<Window
v-if="windows.about"
title="About Me - ShrimpOS"
:initial-x="150"
:initial-y="100"
:width="1200"
:height="550"
:is-active="activeWindow === 'about'"
@close="closeWindow('about')"
@activate="activeWindow = 'about'"
>
<About />
</Window>
<Window
v-if="windows.projects"
title="Projects - ShrimpOS"
:initial-x="200"
:initial-y="150"
:width="900"
:height="650"
:is-active="activeWindow === 'projects'"
@close="closeWindow('projects')"
@activate="activeWindow = 'projects'"
>
<Projects />
</Window>
<Window
v-if="windows.experience"
title="Experience - ShrimpOS"
:initial-x="250"
:initial-y="200"
:width="1200"
:height="700"
:is-active="activeWindow === 'experience'"
@close="closeWindow('experience')"
@activate="activeWindow = 'experience'"
>
<Experience />
</Window>
<Window
v-if="windows.contact"
title="Contact - ShrimpOS"
:initial-x="300"
:initial-y="250"
:width="700"
:height="500"
:is-active="activeWindow === 'contact'"
@close="closeWindow('contact')"
@activate="activeWindow = 'contact'"
>
<Contact />
</Window>
<!-- Start Menu -->
<div
v-if="showStartMenu"
class="start-menu absolute bottom-10 left-0 w-64 bg-[#c0c0c0] border-2 border-white border-r-[#808080] border-b-[#808080] shadow-2xl z-50"
>
<!-- Start Menu Header -->
<div class="bg-gradient-to-r from-[#000080] to-[#1084d0] text-white px-3 py-2 font-bold text-sm flex items-center gap-2">
<span class="text-lg">🦐</span>
<span>ShrimpOS</span>
</div>
<!-- Menu Items -->
<div class="bg-[#c0c0c0]">
<button
@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>
<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>
<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>
<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>
<span>Contact</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>
<span>Shut Down...</span>
</button>
</div>
</div>
<!-- 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="loading-bar h-full bg-gradient-to-r from-blue-500 via-blue-400 to-blue-500"></div>
</div>
</div>
</div>
<!-- Taskbar -->
<div class="taskbar absolute bottom-0 left-0 right-0 h-10 bg-[#c0c0c0] border-t-2 border-white flex items-center px-2 shadow-lg z-50">
<button
@click="showStartMenu = !showStartMenu"
class="start-button px-3 py-1 bg-[#c0c0c0] border-2 font-bold flex items-center gap-2 hover:bg-[#dfdfdf]"
:class="showStartMenu ? 'border-[#808080] border-r-white border-b-white' : 'border-white border-r-[#808080] border-b-[#808080]'"
>
<span class="text-lg">🦐</span>
<span>Start</span>
</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]'"
@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]'"
@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]'"
@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]'"
@click="activeWindow = 'contact'"
>
📧 Contact
</button>
</div>
<div class="time px-2 border-2 border-[#808080] border-t-white border-l-white text-sm">
{{ currentTime }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import Window from '~/components/Window.vue'
import DesktopIcon from '~/components/DesktopIcon.vue'
import ShrimpRender from '~/components/ShrimpRender.vue'
import About from '~/components/About.vue'
import Projects from '~/components/Projects.vue'
import Experience from '~/components/Experience.vue'
import Contact from '~/components/Contact.vue'
const windows = reactive({
about: false,
projects: false,
experience: false,
contact: false,
})
const activeWindow = ref<string | null>(null)
const isLoading = ref(false)
const currentTime = ref('')
const isDesktopLoading = ref(true)
const loadingProgress = ref(0)
const loadingMessage = ref('Loading Desktop...')
const visibleIcons = ref(0)
const showStartMenu = ref(false)
const isShuttingDown = ref(false)
const shutdownStage = ref<'shutting-down' | 'safe-to-turn-off'>('shutting-down')
const shutdownMessage = ref('Shutting down...')
const openWindow = (windowName: keyof typeof windows) => {
// Show hourglass cursor
isLoading.value = true
// Simulate loading delay
setTimeout(() => {
windows[windowName] = true
activeWindow.value = windowName
isLoading.value = false
}, 500)
}
const closeWindow = (windowName: keyof typeof windows) => {
windows[windowName] = false
if (activeWindow.value === windowName) {
activeWindow.value = null
}
}
const startShutdown = async () => {
showStartMenu.value = false
isShuttingDown.value = true
shutdownStage.value = 'shutting-down'
shutdownMessage.value = 'ShrimpOS is shutting down...'
await new Promise(resolve => setTimeout(resolve, 1500))
// Hide icons one by one (in reverse)
for (let i = 4; i >= 1; i--) {
visibleIcons.value = i - 1
await new Promise(resolve => setTimeout(resolve, 200))
}
await new Promise(resolve => setTimeout(resolve, 500))
shutdownStage.value = 'safe-to-turn-off'
shutdownMessage.value = "It's now safe to turn off your computer."
setTimeout(() => {
navigateTo('/')
}, 5000)
}
const updateTime = () => {
const now = new Date()
const hours = now.getHours().toString().padStart(2, '0')
const minutes = now.getMinutes().toString().padStart(2, '0')
currentTime.value = `${hours}:${minutes}`
}
let timeInterval: ReturnType<typeof setInterval>
const startDesktopLoading = async () => {
// Play welcome sound
if (typeof window !== 'undefined') {
const welcomeAudio = new Audio('/welcome.mp3')
welcomeAudio.volume = 0.3
welcomeAudio.play().catch((error) => {
console.log('Audio autoplay blocked:', error)
})
}
loadingMessage.value = 'Loading Desktop...'
await new Promise(resolve => setTimeout(resolve, 500))
loadingProgress.value = 25
loadingMessage.value = 'Loading desktop data...'
await new Promise(resolve => setTimeout(resolve, 400))
loadingProgress.value = 50
visibleIcons.value = 1
await new Promise(resolve => setTimeout(resolve, 300))
loadingProgress.value = 60
visibleIcons.value = 2
loadingMessage.value = 'Loading apps...'
await new Promise(resolve => setTimeout(resolve, 300))
loadingProgress.value = 70
visibleIcons.value = 3
await new Promise(resolve => setTimeout(resolve, 300))
loadingProgress.value = 80
visibleIcons.value = 4
loadingMessage.value = 'Preparing workspace...'
await new Promise(resolve => setTimeout(resolve, 400))
loadingProgress.value = 100
await new Promise(resolve => setTimeout(resolve, 300))
isDesktopLoading.value = false
}
onMounted(() => {
updateTime()
timeInterval = setInterval(updateTime, 1000)
startDesktopLoading()
// Close start menu when clicking outside
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement
if (!target.closest('.start-button') && !target.closest('.start-menu')) {
showStartMenu.value = false
}
})
})
onUnmounted(() => {
clearInterval(timeInterval)
})
</script>
<style scoped>
.hourglass-cursor,
.hourglass-cursor * {
cursor: wait !important;
}
.start-button:active {
border-style: inset;
}
.taskbar-item:hover {
background: #dfdfdf;
}
.menu-item {
transition: all 0.1s ease;
}
.icon-fade-enter-active {
animation: iconAppear 0.3s ease-out;
}
.icon-fade-leave-active {
animation: iconDisappear 0.3s ease-out;
}
@keyframes iconAppear {
0% {
opacity: 0;
transform: scale(0.8) translateY(-10px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes iconDisappear {
0% {
opacity: 1;
transform: scale(1) translateY(0);
}
100% {
opacity: 0;
transform: scale(0.8) translateY(-10px);
}
}
@keyframes loading-slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(400%);
}
}
.loading-bar {
width: 25%;
animation: loading-slide 1.2s ease-in-out infinite;
}
</style>

182
app/pages/index.vue Normal file
View File

@@ -0,0 +1,182 @@
<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 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>
</div>
<div class="space-y-1 mb-4">
<p v-for="(message, index) in biosMessages" :key="index">{{ message }}</p>
</div>
<div v-if="biosMessages.length >= 7" class="mt-8">
<p class="animate-pulse">Press any key to boot from HDD...</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">
<!-- 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;">
ShrimpOS
</h1>
<p class="text-white text-sm opacity-80">Professional Edition</p>
</div>
<!-- Loading Bar Container -->
<div class="relative">
<div 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>
<p class="text-white text-xs mt-4 text-center opacity-60">Starting up...</p>
</div>
<!-- Bottom Text -->
<div class="absolute bottom-8 text-center">
<p class="text-white text-xs opacity-40">Copyright © SticksDev {{ new Date().getFullYear() }}</p>
</div>
</div>
<!-- Pre-boot Power Button -->
<div v-else class="flex flex-col items-center justify-center text-center">
<div class="mb-8">
<button
@click="startBootSequence"
class="power-button group relative w-32 h-32 rounded-full flex items-center justify-center cursor-pointer transition-all duration-300 hover:scale-105"
>
<!-- Button outer ring -->
<div class="absolute inset-0 rounded-full bg-gradient-to-b from-gray-800 via-gray-900 to-black border-4 border-gray-700 shadow-2xl group-hover:border-gray-600 transition-all"></div>
<!-- Button inner glow -->
<div class="absolute inset-4 rounded-full bg-gradient-to-b from-gray-900 to-black border border-gray-800"></div>
<!-- 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>
</div>
<!-- Shine effect -->
<div class="absolute inset-0 rounded-full bg-gradient-to-tr from-transparent via-white to-transparent opacity-10"></div>
</button>
</div>
<p class="text-gray-500 text-sm animate-pulse">Press power button to start</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const bootStage = ref<'off' | 'bios' | 'loading'>('off')
const biosMessages = ref<string[]>([])
const biosMessageList: string[] = [
'Detecting RAM.....................64 MB OK',
'Primary Master: 40GB HDD',
'Primary Slave: None',
'Secondary Master: CD-ROM',
'Secondary Slave: None',
'Boot Device: HDD-0',
'Verifying DMI Pool Data.........',
]
let bootAudio: HTMLAudioElement | null = null
const startBootSequence = async () => {
bootStage.value = 'bios'
// Start playing audio immediately when boot starts
let audioPromise: Promise<void> | null = null
if (typeof window !== 'undefined') {
bootAudio = new Audio('/bootup.mp3')
bootAudio.volume = 0.3
audioPromise = new Promise((resolve) => {
bootAudio!.onended = () => {
resolve()
}
bootAudio!.play().catch(() => {
// Audio autoplay blocked, continue without sound
resolve()
})
// Fallback if audio doesn't end (increased to 15 seconds)
setTimeout(() => {
resolve()
}, 15000)
})
}
// Show BIOS messages one by one
for (let i = 0; i < biosMessageList.length; i++) {
biosMessages.value.push(biosMessageList[i]!)
await new Promise(resolve => setTimeout(resolve, 300))
}
// Wait for user to press a key or auto-continue
await new Promise(resolve => setTimeout(resolve, 800))
// Transition to loading screen
bootStage.value = 'loading'
// Wait for audio to finish
if (audioPromise) {
await audioPromise
} else {
await new Promise(resolve => setTimeout(resolve, 3000))
}
// Navigate to desktop
navigateTo('/desktop')
}
onMounted(() => {
// Preload audio
if (typeof window !== 'undefined') {
bootAudio = new Audio('/bootup.mp3')
}
// Listen for keypress during BIOS screen
const handleKeyPress = () => {
if (bootStage.value === 'bios' && biosMessages.value.length >= 7) {
bootStage.value = 'loading'
document.removeEventListener('keydown', handleKeyPress)
}
}
document.addEventListener('keydown', handleKeyPress)
})
</script>
<style scoped>
@keyframes loading-slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(400%);
}
}
.loading-bar {
width: 25%;
animation: loading-slide 1.2s ease-in-out infinite;
}
.power-button {
filter: drop-shadow(0 10px 30px rgba(0, 0, 0, 0.5));
}
.power-button:hover {
filter: drop-shadow(0 15px 40px rgba(0, 0, 0, 0.7));
}
.power-button:active {
transform: scale(0.95);
}
</style>

6
eslint.config.mjs Normal file
View File

@@ -0,0 +1,6 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
// Your custom configs here
)

View File

@@ -1,4 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

64
nuxt.config.ts Normal file
View File

@@ -0,0 +1,64 @@
import tailwindcss from '@tailwindcss/vite';
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
modules: [
'@nuxt/content',
'@nuxt/eslint',
'@nuxt/fonts',
'@nuxt/icon',
'@nuxt/image',
'@tresjs/nuxt',
],
css: ['./app/assets/css/main.css'],
vite: {
plugins: [tailwindcss()],
},
app: {
head: {
title: "Stick's Portfolio",
meta: [
{ name: 'title', content: "Stick's Portfolio" },
{
name: 'description',
content: 'Sometimes I make the computers do the things.',
},
{ name: 'theme-color', content: '#0047AB' },
{ property: 'og:type', content: 'website' },
{ property: 'og:url', content: 'https://sticksdev.tech' },
{ property: 'og:title', content: "Stick's Portfolio" },
{
property: 'og:description',
content: 'Sometimes I make the computers do the things.',
},
{
property: 'og:image',
content: 'https://img.sticks.ovh/stickspfpnew.png',
},
{ property: 'twitter:card', content: 'summary_large_image' },
{ property: 'twitter:url', content: 'https://sticksdev.tech' },
{ property: 'twitter:title', content: "Stick's Portfolio" },
{
property: 'twitter:description',
content: 'Sometimes I make the computers do the things.',
},
{
property: 'twitter:image',
content: 'https://sticksdev.tech/images/meta-tags.png',
},
],
link: [
{
rel: 'icon',
type: 'image/png',
href: 'https://img.sticks.ovh/stickspfpnew.png',
},
],
},
},
});

22588
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,32 @@
{ {
"name": "portfolio", "name": "portfolio",
"version": "0.1.0", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "build": "nuxt build",
"build": "next build", "dev": "nuxt dev",
"start": "next start", "generate": "nuxt generate",
"lint": "next lint" "preview": "nuxt preview",
"postinstall": "nuxt prepare"
}, },
"dependencies": { "dependencies": {
"@react-three/drei": "^9.114.3", "@nuxt/content": "^3.11.0",
"@react-three/fiber": "^8.17.10", "@nuxt/eslint": "^1.13.0",
"@types/three": "^0.169.0", "@nuxt/fonts": "^0.13.0",
"next": "14.2.15", "@nuxt/icon": "^2.2.1",
"react": "^18", "@nuxt/image": "^2.0.0",
"react-dom": "^18", "@tailwindcss/vite": "^4.1.18",
"three": "^0.169.0", "@tresjs/cientos": "^5.2.5",
"three-stdlib": "^2.33.0" "@tresjs/core": "^5.3.3",
}, "@tresjs/nuxt": "^5.1.7",
"devDependencies": { "@tresjs/post-processing": "^3.2.5",
"@types/node": "^22", "@vueuse/core": "^14.1.0",
"@types/react": "^18", "better-sqlite3": "^12.6.2",
"@types/react-dom": "^18", "eslint": "^9.39.2",
"eslint": "^8", "nuxt": "^4.3.0",
"eslint-config-next": "14.2.15", "tailwindcss": "^4.1.18",
"postcss": "^8", "three": "^0.182.0",
"tailwindcss": "^3.4.13", "vue": "^3.5.27",
"typescript": "^5" "vue-router": "^4.6.4"
} }
} }

View File

@@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

View File

@@ -1 +0,0 @@
dh=cb120397c5630a298ec69774c3cd793fd024a2b0

Binary file not shown.

Binary file not shown.

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow:

View File

@@ -1,20 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.shrimp {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
padding: 0% !important;
}
/* Global styles adjustment */
html,
body {
min-height: 100vh;
min-width: 100vw;
background-color: black; /* Ensure the background color is set here for full coverage */
}

View File

@@ -1,28 +0,0 @@
import Shrimp from '@/components/ShrimpRender';
import Image from 'next/image';
export default function Home() {
return (
<div className='min-h-screen min-w-screen'>
<Shrimp />
<div className='absolute z-[1] bottom-0 left-1/2 transform -translate-x-1/2 text-white font-mono pb-10 text-center'>
<p>SticksDev</p>
<p>In shrimp, we trust.</p>
{/* Contact link */}
<div className='text-blue-400 flex flex-row space-x-2'>
<a href='/term' className='hover:text-blue-500 duration-200'>
Open Terminal to Learn More
</a>
<p className='text-white'>or</p>
<a
href='mailto:sticks@teamhydra.dev'
className='hover:text-blue-500 duration-200'
>
Get in touch
</a>
</div>
</div>
</div>
);
}

View File

@@ -1,55 +0,0 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang='en'>
<head>
<title>Stick&apos;s Portfolio</title>
<meta name='title' content="Stick's Portfolio" />
<meta
name='description'
content='Sometimes I make the computers do the things.'
/>
<meta name='theme-color' content='#0047AB'></meta>
<meta property='og:type' content='website' />
<meta property='og:url' content='https://sticksdev.tech' />
<meta property='og:title' content="Stick's Portfolio" />
<meta
property='og:description'
content='Sometimes I make the computers do the things.'
/>
<meta
property='og:image'
content='https://img.sticks.ovh/stickspfpnew.png'
/>
<meta property='twitter:card' content='summary_large_image' />
<meta property='twitter:url' content='https://sticksdev.tech' />
<meta property='twitter:title' content="Stick's Portfolio" />
<meta
property='twitter:description'
content='Sometimes I make the computers do the things.'
/>
<meta
property='twitter:image'
content='https://sticksdev.techimages/meta-tags.png'
/>
</head>
<link
rel='icon'
type='image/png'
href='https://img.sticks.ovh/stickspfpnew.png'
/>
<body className={inter.className}>{children}</body>
</html>
);
}

View File

@@ -1,157 +0,0 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
function Home() {
// eslint-disable-next-line react-hooks/exhaustive-deps
const bootMessages = [
'Initializing ShrimpBIOS v0.1.0... (c) 2024 SticksDev. All rights reserved.',
'Checking Memory...',
'...100KB OK',
'...512KB OK',
'...1024KB OK',
'...2048KB OK',
'...4096KB OK',
'Memory OK',
'Checking CPU...',
'...CPU OK. Found Intel Pentium II 400MHz',
'Running POST Cycle for Integrated Graphics...',
'...Graphics OK. Found Intel i740',
'Running POST Cycle for Audio...',
'...Audio OK. Found SoundBlaster 16',
'Running POST Cycle for Storage...',
'...Storage OK. Found 1x 40GB HDD',
'...Storage OK. Found 1x 1.44MB FDD',
'...Storage OK. Found 1x CD-ROM Drive',
'Preparing to boot from HDD...',
'Booting from HDD...',
'ShrimpOS v1.0.0 (c) 2024 SticksDev. All rights reserved.',
'Welcome to ShrimpOS!',
'In shrimp, we trust.',
'Sending you to the home in 3...',
'2...',
'1...',
'S:\\> home.exe',
'Inlining ShrimpOS Home...',
'Done!',
'Preparing assets...',
'Done!',
'Loading ShrimpOS Home...',
'Done!',
'Welcome to ShrimpOS Home!',
];
const [currentMessage, setCurrentMessage] = useState('');
const [messageIndex, setMessageIndex] = useState(0);
const [hasBootSequenceStarted, setHasBootSequenceStarted] = useState(false);
const [isBootSequenceDone, setIsBootSequenceDone] = useState(false);
const [skippedBootSequence, setSkippedBootSequence] = useState(false);
// Audio refs
const bootAudio = useRef<HTMLAudioElement | null>(null);
const welcomeAudio = useRef<HTMLAudioElement | null>(null);
function startBootSequence() {
const bootAudio = new Audio('/bootup.mp3');
bootAudio.play();
setMessageIndex(1);
setHasBootSequenceStarted(true);
// When bootAudio finsihes, play the welcome audio and goto /home
bootAudio.onended = () => {
const welcomeAudio = new Audio('/welcome.mp3');
// Fade out the main div
const mainDiv = document.getElementById('main');
if (!mainDiv) return;
mainDiv.style.opacity = '0';
welcomeAudio.play();
setIsBootSequenceDone(true);
welcomeAudio.onended = () => {
window.location.href = '/home';
};
};
}
function handleSkipBootSequence() {
setSkippedBootSequence(true);
setMessageIndex(bootMessages.length);
// Kill all audios
bootAudio.current?.pause();
welcomeAudio.current?.pause();
window.location.href = '/home';
}
useEffect(() => {
if (!hasBootSequenceStarted) return;
if (messageIndex < bootMessages.length) {
const timer = setTimeout(() => {
setCurrentMessage(bootMessages[messageIndex]);
setMessageIndex(messageIndex + 1);
}, 588); // Change the delay here to speed up or slow down the sequence
return () => clearTimeout(timer);
}
}, [messageIndex, bootMessages, hasBootSequenceStarted]);
useEffect(() => {
// Load the bootup sound
bootAudio.current = new Audio('/bootup.mp3');
welcomeAudio.current = new Audio('/welcome.mp3');
}, []);
return (
<div className='min-h-screen min-w-screen bg-black flex items-center justify-center'>
{!hasBootSequenceStarted && (
<div
className={`flex flex-col items-center justify-center text-center text-white font-mono ${
hasBootSequenceStarted ? 'hidden' : ''
}`}
>
<p>You notice a mysterious computer in front of you.</p>
<p>It seems to be off.</p>
<button
onClick={() => startBootSequence()}
className='bg-blue-400 hover:bg-blue-500 text-white font-bold py-2 px-4 rounded mt-2'
>
Turn it on
</button>
</div>
)}
<div
className='absolute top-0 left-0 text-white font-mono p-4 text-left transition-opacity duration-300'
id='main'
>
{hasBootSequenceStarted &&
bootMessages
.slice(0, messageIndex)
.map((message, index) => <p key={index}>{message}</p>)}
</div>
{/* Add skip boot sequence button in the right corner */}
<button
onClick={handleSkipBootSequence}
className={`absolute top-0 right-0 text-white font-mono p-4 text-right transition-opacity duration-300 ${
hasBootSequenceStarted ? '' : 'hidden' // Hide the button if the boot sequence hasn't started
} ${
skippedBootSequence ? 'hidden' : '' // Hide the button if the user has already skipped the boot sequence
}`}
>
Skip Boot Sequence
</button>
{/* Center text to fade in once boot seq is done */}
<div
className={`flex flex-col items-center justify-center text-white text-center font-mono p-4 transition-opacity duration-500 ${
isBootSequenceDone ? 'opacity-100' : 'opacity-0 hidden'
}`}
id='center'
>
<h1 className='text-2xl font-bold'>ShrimpOS</h1>
<p className='text-gray-500'>Welcome.</p>
</div>
</div>
);
}
export default Home;

View File

@@ -1,197 +0,0 @@
'use client';
import About from '@/components/About';
import { Contact } from '@/components/Contact';
import Experience from '@/components/Experience';
import Projects from '@/components/Projects';
import React, { useState, useEffect, useRef } from 'react';
export default function Home() {
const [input, setInput] = useState('');
const [output, setOutput] = useState<React.ReactNode[]>([
'ShrimpTerm v1.0.0',
"Type 'help' for a list of commands.",
'Press Tab for autocomplete, Escape to clear the current input.',
]);
const [suggestion, setSuggestion] = useState('');
const inputRef = useRef<HTMLInputElement | null>(null);
const keyAudio = useRef<HTMLAudioElement | null>(null);
const enterAudio = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
keyAudio.current = new Audio('/key.mp3');
keyAudio.current.volume = 0.1;
enterAudio.current = new Audio('/enter.mp3');
enterAudio.current.volume = 0.1;
inputRef.current?.focus();
}, []);
const commands = [
'help',
'about',
'clear',
'projects',
'experience',
'contact',
'back',
];
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
setInput(value);
if (value.trim().length < 3) {
setSuggestion('');
return;
}
const matchedCommand = commands.find((command) =>
command.startsWith(value.trim().toLowerCase()),
);
setSuggestion(matchedCommand || '');
};
const processCommand = (command: string) => {
switch (command.trim().toLowerCase()) {
case 'help':
setOutput((prevOutput) => [
...prevOutput,
'Available commands (click to autocomplete):',
createCommandButton(
'help',
'Help (this command) - List available commands',
),
createCommandButton(
'about',
'About - Learn more about Sticks and his projects',
),
createCommandButton(
'projects',
'Projects - List of projects created by Sticks',
),
createCommandButton(
'experience',
"Experience - View Sticks's experience",
),
createCommandButton(
'contact',
'Contact - Get in touch with Sticks',
),
createCommandButton(
'back',
'Back - Return to the home page',
),
createCommandButton('clear', 'Clear - Clear the screen'),
]);
break;
case 'about':
setOutput((prevOutput) => [
...prevOutput,
<About key='about' />,
]);
break;
case 'projects':
setOutput((prevOutput) => [
...prevOutput,
<Projects key='projects' />,
]);
break;
case 'experience':
setOutput((prevOutput) => [
...prevOutput,
<Experience key='experience' />,
]);
break;
case 'back':
window.location.href = '/home';
break;
case 'contact':
setOutput((prevOutput) => [
...prevOutput,
<Contact key='contact' />,
]);
break;
case 'clear':
setOutput([
'ShrimpTerm v1.0.0',
"Type 'help' for a list of commands.",
'Press Tab for autocomplete, Escape to clear the current input.',
]);
break;
default:
setOutput((prevOutput) => [
...prevOutput,
<div key='error' className='text-red-500'>
Error:{' '}
<span className='text-red-400'>
{command} is not a valid command or batch file.
</span>
</div>,
]);
}
};
const handleCommandInput = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
enterAudio.current?.play();
setOutput((prevOutput) => [...prevOutput, `S:\\> ${input}.exe`]);
processCommand(input);
setInput('');
setSuggestion('');
} else if (e.key === 'Tab' && suggestion) {
e.preventDefault();
keyAudio.current?.play();
setInput(suggestion);
setSuggestion('');
} else if (e.key === 'Escape') {
keyAudio.current?.play();
setInput('');
setSuggestion('');
} else {
keyAudio.current?.play();
}
};
const createCommandButton = (command: string, label: string) => (
<div key={command}>
<button
className='text-white'
onClick={() => {
setInput(command);
inputRef.current?.focus();
}}
>
{label.split(' - ')[0]} -{' '}
<span className='text-white hover:underline hover:text-blue-500 duration-300'>
{label.split(' - ')[1] || (
<span className='text-gray-500'>No description</span>
)}
</span>
</button>
</div>
);
return (
<div className='bg-black'>
<div className='absolute z-[1] top-0 left-0 text-white font-mono p-10 overflow-auto min-h-screen min-w-screen'>
<a href='/home' className='hover:text-blue-500 duration-200'>
&lt; Back
</a>
{output.map((line, index) => (
<p key={index}>{line}</p>
))}
<p className='text-gray-500'>
{input && suggestion ? suggestion : ''}
</p>
<input
ref={inputRef}
className='bg-transparent text-white outline-none'
value={input}
onChange={handleInput}
onKeyDown={handleCommandInput}
placeholder='Type a command...'
/>
</div>
</div>
);
}

View File

@@ -1,32 +0,0 @@
export function Contact() {
return (
<div>
<h1 className='text-2xl font-bold font-mono'>Contact</h1>
<p className='text-gray-500'>
You can reach me at the following email address:
</p>
<a
href='mailto:tanner@teamhydra.dev'
className='text-blue-500 hover:underline'
>
tanner@teamhydra.dev
</a>
<br />
-- or --
<br />
<a
href='https://discord.gg/zira'
className='text-blue-500 hover:underline'
>
Via Discord
</a>
<p className='text-gray-500'>
Please use the #other-support channel to get in touch with me. My
username is sticksdev.
</p>
<p className='text-gray-500'>
I look forward to hearing from you soon :)
</p>
</div>
);
}

View File

@@ -1,86 +0,0 @@
'use strict';
'use client';
import { useState } from 'react';
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.
`,
];
export default function Experience() {
const [index, setIndex] = useState(0);
const handleNext = () => {
if (index < experience.length - 1) {
setIndex(index + 1);
}
};
const handlePrev = () => {
if (index > 0) {
setIndex(index - 1);
}
};
return (
<div>
<h1 className='text-2xl font-bold font-mono'>Experience</h1>
<p className='text-gray-500'>
Use the buttons below to navigate through my experience.
</p>
<div className='flex justify-between'>
<button
onClick={handlePrev}
className='text-blue-500 hover:underline'
disabled={index === 0}
>
Previous
</button>
<button
onClick={handleNext}
className='text-blue-500 hover:underline'
disabled={index === experience.length - 1}
>
Next
</button>
</div>
<div className=''>
<pre className='whitespace-pre-wrap'>{experience[index]}</pre>
</div>
</div>
);
}

View File

@@ -1,63 +0,0 @@
export const projects = [
{
title: 'BambuConnect',
description: 'A simple 3rd party client for managing your 3D printer.',
link: 'https://github.com/SticksDev/BambuConnect',
},
{
title: 'VRDCN_NetworkTest',
description:
'A simple network test for a VR Streaming service. Written in Go.',
link: 'https://github.com/SticksDev/VRCDN_NetworkTest',
},
{
title: 'Runic Spells',
description:
'A simple spell system for Minecraft using Java and PaperMC APIs.',
link: 'https://github.com/SticksDev/runic_spells',
},
];
export default function Projects() {
// Step 1: Calculate maximum lengths
const maxTitleLength = Math.max(
...projects.map((project) => project.title.length),
);
const maxDescriptionLength = Math.max(
...projects.map((project) => project.description.length),
);
// Step 2: Prepare projects with padded strings for rendering
const preparedProjects = projects.map((project) => ({
...project,
title: project.title.padEnd(maxTitleLength, ' '),
description: project.description.padEnd(maxDescriptionLength, ' '),
}));
return (
<div className='bg-black font-mono text-white'>
<h1 className='text-white font-mono text-md mb-4'>
--- Reading database projects.db ---<br></br>
Found {projects.length} projects in database. Displaying all
projects:
</h1>
{preparedProjects.map((project, index) => (
<div key={index} className='flex flex-row'>
<p>{project.title}</p> &nbsp;|
<p>&nbsp;{project.description}</p>|
<a
href={project.link}
className='hover:text-blue-500 duration-200'
target='blank'
>
&nbsp;{project.link}
</a>
</div>
))}
<h1 className='text-white font-mono text-md mt-4'>
--- End of database ---
</h1>
</div>
);
}

View File

@@ -1,67 +0,0 @@
'use client';
import { Canvas, useFrame } from '@react-three/fiber';
import * as THREE from 'three';
import React, { useRef } from 'react';
import { AsciiRenderer, OrbitControls, useGLTF } from '@react-three/drei';
import { GLTF } from 'three-stdlib';
type GLTFResult = GLTF & {
nodes: {
Mesh_Mesh_head_geo001_lambert2SG001: THREE.Mesh;
};
materials: {
['lambert2SG.001']: THREE.MeshStandardMaterial;
};
};
export function ShrimpModel(props: JSX.IntrinsicElements['group']) {
const { nodes, materials } = useGLTF('/shrimp_smol.glb') as GLTFResult;
const groupRef = useRef<THREE.Group>(null!);
useFrame((state, delta) => (groupRef.current.rotation.z += delta * .5))
return (
<group {...props} ref={groupRef} dispose={null}>
<mesh
castShadow
receiveShadow
geometry={nodes.Mesh_Mesh_head_geo001_lambert2SG001.geometry}
material={materials['lambert2SG.001']}
rotation={[-Math.PI / 2, 0, 0]}
/>
</group>
);
}
useGLTF.preload('/shrimp_smol.glb');
function CameraHelper() {
const camera = new THREE.PerspectiveCamera(60, 1.77, 1, 3);
return (
<group position={[0.5, 40, 8.5]} rotation={new THREE.Euler(-1.5, 0, 0)}>
<cameraHelper args={[camera]} />
</group>
);
}
export default function Shrimp() {
const eular = new THREE.Euler(-1.5, 0, 0);
return (
<>
<Canvas
className='shrimp'
camera={{
position: [0.4, 40, 11],
rotation: eular,
fov: 60,
aspect: 1.77,
near: 1,
}}
>
<color attach='background' args={['black']} />
<ambientLight intensity={Math.PI / 2} />
<ShrimpModel position={[0, -1, 0]} rotation={[0, 0, 0]} />
<AsciiRenderer fgColor='red' />
</Canvas>
</>
);
}

View File

@@ -1,20 +0,0 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
},
plugins: [],
};
export default config;

View File

@@ -1,26 +1,18 @@
{ {
"compilerOptions": { // https://nuxt.com/docs/guide/concepts/typescript
"lib": ["dom", "dom.iterable", "esnext"], "files": [],
"allowJs": true, "references": [
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{ {
"name": "next" "path": "./.nuxt/tsconfig.app.json"
}
],
"paths": {
"@/*": ["./src/*"]
}
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], {
"exclude": ["node_modules"] "path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
} }