new portfolio yay!

This commit is contained in:
Tanner Sommers 2024-07-16 19:27:10 -05:00
parent 88bd40c942
commit 786c414eb3
40 changed files with 6496 additions and 430 deletions

3
.eslintrc.json Normal file
View File

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

45
.gitignore vendored
View File

@ -1,21 +1,36 @@
node_modules
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Output
.output
.vercel
/.svelte-kit
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# OS
# misc
.DS_Store
Thumbs.db
*.pem
# Env
.env
.env.*
!.env.example
!.env.test
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

1
.npmrc
View File

@ -1 +0,0 @@
engine-strict=true

View File

@ -1,4 +0,0 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

View File

@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@ -1,38 +1,36 @@
# create-svelte
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).
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Getting Started
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
First, run the development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
## Building
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
To create a production version of your app:
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
```bash
npm run build
```
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
You can preview the production build with `npm run preview`.
## Learn More
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
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.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@ -1,33 +0,0 @@
import js from '@eslint/js';
import ts from 'typescript-eslint';
import svelte from 'eslint-plugin-svelte';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
];

4
next.config.mjs Normal file
View File

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

5574
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,31 @@
{
"name": "portfolio",
"version": "0.0.1",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"model-pipeline:run": "node scripts/model-pipeline.js"
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@react-three/drei": "^9.108.4",
"@react-three/fiber": "^8.16.8",
"@types/three": "^0.166.0",
"next": "14.2.5",
"react": "^18",
"react-dom": "^18",
"three": "^0.166.1",
"three-stdlib": "^2.30.4"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@types/eslint": "^8.56.7",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0-alpha.20",
"vite": "^5.0.3",
"@types/three": "^0.159.0"
},
"type": "module",
"dependencies": {
"three": "^0.159.0",
"@threlte/core": "^7.3.1",
"@threlte/extras": "^8.11.4"
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
}

8
postcss.config.mjs Normal file
View File

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

BIN
public/bootup.mp3 Normal file

Binary file not shown.

BIN
public/enter.mp3 Normal file

Binary file not shown.

BIN
public/key.mp3 Normal file

Binary file not shown.

BIN
public/shrimp_smol.glb Normal file

Binary file not shown.

BIN
public/welcome.mp3 Normal file

Binary file not shown.

View File

@ -1,149 +0,0 @@
import { execSync } from 'node:child_process'
import { readdirSync, copyFileSync, unlinkSync, mkdirSync, existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { exit } from 'node:process'
/**
* This script is used to transform gltf and glb files into Threlte components.
* It uses the `@threlte/gltf` package to do so.
* It works in two steps:
* 1. Transform the gltf/glb files located in the sourceDir directory
* 2. Move the Threlte components to the targetDir directory
*/
const configuration = {
sourceDir: resolve(join('static', 'models')),
targetDir: resolve(join('src', 'lib', 'components', 'models')),
overwrite: false,
root: '/models/',
types: true,
keepnames: false,
meta: false,
shadows: false,
printwidth: 120,
precision: 2,
draco: null,
preload: false,
suspense: false,
isolated: false,
transform: {
enabled: false,
resolution: 1024,
simplify: {
enabled: false,
weld: 0.0001,
ratio: 0.75,
error: 0.001
}
}
}
// if the target directory doesn't exist, create it
mkdirSync(configuration.targetDir, { recursive: true })
// throw error if source directory doesn't exist
if (!existsSync(configuration.sourceDir)) {
throw new Error(`Source directory ${configuration.sourceDir} doesn't exist.`)
}
// read the directory, filter for .glb and .gltf files and files *not* ending
// with -transformed.gltf or -transformed.glb as these should not be transformed
// again.
const gltfFiles = readdirSync(configuration.sourceDir).filter((file) => {
return (
(file.endsWith('.glb') || file.endsWith('.gltf')) &&
!file.endsWith('-transformed.gltf') &&
!file.endsWith('-transformed.glb')
)
})
if (gltfFiles.length === 0) {
console.log('No gltf or glb files found.')
exit()
}
const filteredGltfFiles = gltfFiles.filter((file) => {
if (!configuration.overwrite) {
const componentFilename = file.split('.').slice(0, -1).join('.') + '.svelte'
const componentPath = join(configuration.targetDir, componentFilename)
if (existsSync(componentPath)) {
console.error(`File ${componentPath} already exists, skipping.`)
return false
}
}
return true
})
if (filteredGltfFiles.length === 0) {
console.log('No gltf or glb files to process.')
exit()
}
filteredGltfFiles.forEach((file) => {
// run the gltf transform command on every file
const path = join(configuration.sourceDir, file)
// parse the configuration
const args = []
if (configuration.root) args.push(`--root ${configuration.root}`)
if (configuration.types) args.push('--types')
if (configuration.keepnames) args.push('--keepnames')
if (configuration.meta) args.push('--meta')
if (configuration.shadows) args.push('--shadows')
args.push(`--printwidth ${configuration.printwidth}`)
args.push(`--precision ${configuration.precision}`)
if (configuration.draco) args.push(`--draco ${configuration.draco}`)
if (configuration.preload) args.push('--preload')
if (configuration.suspense) args.push('--suspense')
if (configuration.isolated) args.push('--isolated')
if (configuration.transform.enabled) {
args.push(`--transform`)
args.push(`--resolution ${configuration.transform.resolution}`)
if (configuration.transform.simplify.enabled) {
args.push(`--simplify`)
args.push(`--weld ${configuration.transform.simplify.weld}`)
args.push(`--ratio ${configuration.transform.simplify.ratio}`)
args.push(`--error ${configuration.transform.simplify.error}`)
}
}
const formattedArgs = args.join(' ')
// run the command
const cmd = `npx @threlte/gltf@latest ${path} ${formattedArgs}`
try {
execSync(cmd, {
cwd: configuration.sourceDir
})
} catch (error) {
console.error(`Error transforming model: ${error}`)
}
})
// read dir again, but search for .svelte files only.
const svelteFiles = readdirSync(configuration.sourceDir).filter((file) => file.endsWith('.svelte'))
svelteFiles.forEach((file) => {
// now move every file to /src/components/models
const path = join(configuration.sourceDir, file)
const newPath = join(configuration.targetDir, file)
copyFile: try {
// Sanity check, we checked earlier if the file exists. Still, the CLI takes
// a while, so who knows what happens in the meantime.
if (!configuration.overwrite) {
// check if file already exists
if (existsSync(newPath)) {
console.error(`File ${newPath} already exists, skipping.`)
break copyFile
}
}
copyFileSync(path, newPath)
} catch (error) {
console.error(`Error copying file: ${error}`)
}
// remove the file from /static/models
try {
unlinkSync(path)
} catch (error) {
console.error(`Error removing file: ${error}`)
}
})

13
src/app.d.ts vendored
View File

@ -1,13 +0,0 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

20
src/app/globals.css Normal file
View File

@ -0,0 +1,20 @@
@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 */
}

28
src/app/home/page.tsx Normal file
View File

@ -0,0 +1,28 @@
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>
);
}

55
src/app/layout.tsx Normal file
View File

@ -0,0 +1,55 @@
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>
);
}

157
src/app/page.tsx Normal file
View File

@ -0,0 +1,157 @@
'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;

257
src/app/term/page.tsx Normal file
View File

@ -0,0 +1,257 @@
'use client';
import About from '@/components/About';
import { Contact } from '@/components/Contact';
import Experience from '@/components/Experince';
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(() => {
// Load the key press sound
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);
// Generate suggestion only if 3 or more characters are entered
if (value.trim().length < 3) {
setSuggestion('');
return;
}
const matchedCommand = commands.find((command) =>
command.startsWith(value.trim().toLowerCase()),
);
setSuggestion(matchedCommand ? matchedCommand : '');
};
function processCommand(command: string) {
switch (command.trim().toLowerCase()) {
case 'help':
setOutput((prevOutput) => [
...prevOutput,
'Available commands (click to autocomplete):',
<div key='help'>
<button
key='clear'
onClick={() => {
setInput('help');
inputRef.current?.focus();
}}
className='text-blue-500 hover:underline'
>
Help (this command)
</button>
{' - Display this help message'}
</div>,
<div key='about'>
<button
key='clear'
onClick={() => {
setInput('about');
inputRef.current?.focus();
}}
className='text-blue-500 hover:underline'
>
About
</button>
{' - Learn more about Sticks and his projects'}
</div>,
<div key='projects'>
<button
key='clear'
onClick={() => {
setInput('projects');
inputRef.current?.focus();
}}
className='text-blue-500 hover:underline'
>
Projects
</button>
{' - List of projects created by Sticks'}
</div>,
<div key='experience'>
<button
key='clear'
onClick={() => {
setInput('experience');
inputRef.current?.focus();
}}
className='text-blue-500 hover:underline'
>
Experience
</button>
{" - View Sticks's experience and past positions"}
</div>,
<div key='contact'>
<button
key='clear'
onClick={() => {
setInput('contact');
inputRef.current?.focus();
}}
className='text-blue-500 hover:underline'
>
Contact
</button>
{' - Get in touch with Sticks'}
</div>,
<div key='back'>
<button
key='clear'
onClick={() => {
setInput('back');
inputRef.current?.focus();
}}
className='text-blue-500 hover:underline'
>
Back
</button>
{' - Return to the home page'}
</div>,
<div key='clear'>
<button
key='clear'
onClick={() => {
setInput('clear');
inputRef.current?.focus();
}}
className='text-blue-500 hover:underline'
>
Clear
</button>
{' - Clear the screen'}
</div>,
]);
break;
case 'about':
setOutput((prevOutput) => [
...prevOutput,
<div key='about' className='flex flex-row'>
<About />
</div>,
]);
break;
case 'projects':
setOutput((prevOutput) => [
...prevOutput,
<div key='projects' className='flex flex-row'>
<Projects />
</div>,
]);
break;
case 'experience':
setOutput((prevOutput) => [
...prevOutput,
<div key='experience' className='flex flex-row'>
<Experience />
</div>,
]);
break;
case 'back':
window.location.href = '/home';
break;
case 'contact':
setOutput((prevOutput) => [
...prevOutput,
<div key='contact'>
<Contact />
</div>,
]);
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'>
<span className='text-red-500'>Error:</span> Bad command
or file name: {command}
</div>,
]);
break;
}
}
const handleCommandInput = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
enterAudio.current?.play();
setOutput((prevOutput) => [...prevOutput, `S:\\> ${input}.exe`]);
processCommand(input);
setInput(''); // Clear the input
setSuggestion(''); // Clear the suggestion
} else if (e.key === 'Tab' && suggestion) {
keyAudio.current?.play();
e.preventDefault(); // Prevent the default tab behavior
setInput(suggestion); // Set input to the full suggestion
setSuggestion(''); // Clear the suggestion
} else if (e.key === 'Escape') {
keyAudio.current?.play();
setInput(''); // Clear the input
setSuggestion(''); // Clear the suggestion
} else {
keyAudio.current?.play();
}
};
// Adjustments to the component
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'>
{/* Back Link */}
<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>
);
}

23
src/components/About.tsx Normal file
View File

@ -0,0 +1,23 @@
'use client';
const AboutTxt = `
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- Reading file about_me.inf ----
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- File read successful ----
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Name: SticksDev (Tanner Sommers)
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Age: 21
@@@@@@@@@@@****+++=@@@@@@@@@ Location: United States (UTC-5)
@@@@@@@@@=-@@@@@@@@@@@@@@@@@ Occupation: Software Engineer/Freelancer
@@@@@@@@@.:@@@@@@@@@@@@@@@@@ Skills: JavaScript, TypeScript, React, Svelte, Node.js, Python, C#, Java
@@@@@@@@@@@------@@@@@@@@@@@ Interests: Web Development, Game Development, Cybersecurity
@@@@@@@@@@@@@@@@@--@@@@@@@@@ Hobbies: Coding, Gaming, Chess, Music
@@@@@@@@@==+++***@@@@@@@@@@@ Bio:
@@@@@@@@@%%%%@@%@@@@@@@@@@@@ Hi! I'm Tanner, a software engineer and freelancer from the United States.
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ I love coding, gaming, and learning new things. I'm always looking for new
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ opportunities to grow and expand my skillset. Feel free to reach out to me
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ if you have any questions or just want to chat!
@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ---- End of file ----
`;
export default function About() {
return <pre className='text-white font-mono'>{AboutTxt}</pre>;
}

View File

@ -0,0 +1,32 @@
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

@ -0,0 +1,86 @@
'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

@ -0,0 +1,59 @@
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),
);
const maxLinkLength = Math.max(
...projects.map((project) => project.link.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

@ -0,0 +1,67 @@
'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.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,8 +0,0 @@
<script lang="ts">
import { Canvas } from '@threlte/core'
import Scene from './Scene.svelte'
</script>
<Canvas>
<Scene />
</Canvas>

View File

@ -1,60 +0,0 @@
<script lang="ts">
import { T, useFrame } from '@threlte/core'
let rotation = 0
useFrame(() => {
rotation += 0.001
})
</script>
<T.Group rotation.y={rotation}>
<T.PerspectiveCamera
makeDefault
position={[-10, 10, 10]}
fov={15}
on:create={({ ref }) => {
ref.lookAt(0, 1, 0)
}}
/>
</T.Group>
<!-- Floor -->
<T.Mesh rotation.x={(90 * Math.PI) / 180}>
<T.CircleGeometry args={[3, 16]} />
<T.MeshBasicMaterial
color="#666666"
wireframe
/>
</T.Mesh>
<T.DirectionalLight
intensity={0.8}
position.x={5}
position.y={10}
/>
<T.AmbientLight intensity={0.2} />
<T.Mesh
position.y={1.2}
position.z={-0.75}
>
<T.BoxGeometry />
<T.MeshStandardMaterial color="#0059BA" />
</T.Mesh>
<T.Mesh
position={[1.2, 1.5, 0.75]}
rotation.x={5}
rotation.y={71}
>
<T.TorusKnotGeometry args={[0.5, 0.15, 100, 12, 2, 3]} />
<T.MeshStandardMaterial color="#F85122" />
</T.Mesh>
<T.Mesh
position={[-1.4, 1.5, 0.75]}
rotation={[-5, 128, 10]}
>
<T.IcosahedronGeometry />
<T.MeshStandardMaterial color="#F8EBCE" />
</T.Mesh>

View File

@ -1,5 +0,0 @@
# Threlte Model Pipeline Components
This directory holds automatically generated Threlte components from GLTF models.
Place your models in `static/models` and run `npm run model-pipeline:run` to generate the components.

View File

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@ -1,20 +0,0 @@
<script lang="ts">
import App from '$lib/components/App.svelte'
</script>
<div>
<App />
</div>
<style>
:global(body) {
margin: 0;
}
div {
width: 100vw;
height: 100vh;
background: rgb(13, 19, 32);
background: linear-gradient(180deg, rgba(13, 19, 32, 1) 0%, rgba(8, 12, 21, 1) 100%);
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

View File

@ -1,18 +0,0 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

20
tailwind.config.ts Normal file
View File

@ -0,0 +1,20 @@
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,19 +1,26 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

View File

@ -1,6 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});