goodbye react, hello vue
This commit is contained in:
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "next/core-web-vitals"
|
|
||||||
}
|
|
||||||
50
.gitignore
vendored
50
.gitignore
vendored
@@ -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
|
|
||||||
|
|||||||
81
README.md
81
README.md
@@ -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
7
app/app.vue
Normal 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
125
app/assets/css/main.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
34
app/components/Contact.vue
Normal file
34
app/components/Contact.vue
Normal 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>
|
||||||
32
app/components/DesktopIcon.vue
Normal file
32
app/components/DesktopIcon.vue
Normal 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>
|
||||||
84
app/components/Experience.vue
Normal file
84
app/components/Experience.vue
Normal 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>
|
||||||
57
app/components/Projects.vue
Normal file
57
app/components/Projects.vue
Normal 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"> | </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>
|
||||||
116
app/components/ShrimpRender.vue
Normal file
116
app/components/ShrimpRender.vue
Normal 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
112
app/components/Window.vue
Normal 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
449
app/pages/desktop.vue
Normal 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
182
app/pages/index.vue
Normal 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
6
eslint.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// @ts-check
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
|
export default withNuxt(
|
||||||
|
// Your custom configs here
|
||||||
|
)
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {};
|
|
||||||
|
|
||||||
export default nextConfig;
|
|
||||||
64
nuxt.config.ts
Normal file
64
nuxt.config.ts
Normal 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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
22538
package-lock.json
generated
22538
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
|
||||||
const config = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
dh=cb120397c5630a298ec69774c3cd793fd024a2b0
|
|
||||||
BIN
public/enter.mp3
BIN
public/enter.mp3
Binary file not shown.
BIN
public/key.mp3
BIN
public/key.mp3
Binary file not shown.
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
@@ -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 */
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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'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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
157
src/app/page.tsx
157
src/app/page.tsx
@@ -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;
|
|
||||||
@@ -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'>
|
|
||||||
< 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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> |
|
|
||||||
<p> {project.description}</p>|
|
|
||||||
<a
|
|
||||||
href={project.link}
|
|
||||||
className='hover:text-blue-500 duration-200'
|
|
||||||
target='blank'
|
|
||||||
>
|
|
||||||
{project.link}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<h1 className='text-white font-mono text-md mt-4'>
|
|
||||||
--- End of database ---
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user