16 Commits

Author SHA1 Message Date
4019d4cb10 remove mention of *arr stack 2026-01-13 00:25:49 -06:00
dee891ef68 added markdown support for the blog style pages 2026-01-13 00:25:07 -06:00
fa64c4a254 updated to talk about gitea actions 2026-01-13 00:16:03 -06:00
e8ec353c8f updated tg image 2026-01-12 23:51:14 -06:00
d31de41ca3 changed the code render for the side quests tab 2026-01-12 23:34:12 -06:00
b6aa1b0316 implemented react syntax highlighter 2026-01-12 22:10:32 -06:00
91683d1767 Added blog style pages for more descriptive sidequests and increased bottom padding to stop page from touching 2026-01-12 20:37:18 -06:00
65fbec3e33 Added expand all toggle 2026-01-12 20:19:25 -06:00
127aa9acc7 updated route pathing, skills.tsx for skill card abstraction and header for mobile 2026-01-12 19:16:48 -06:00
54831798d0 fixed project 2026-01-12 18:17:27 -06:00
b78fea0762 fix particles amount 2026-01-12 18:06:42 -06:00
efc68a4486 Refactor styles and restructure CSS files
- Removed App.css and migrated styles to individual component and page-specific CSS files.
- Created base styles in base.css for layout utilities and common elements.
- Added component-specific styles in cards.css, header.css, and other relevant files.
- Updated imports in App.tsx and other components to reflect new CSS structure.
- Enhanced responsiveness and visual consistency across various components and pages.
2026-01-08 14:49:04 -06:00
b62bdb906c removed the contact page and reworked the home page, along with shuffling the header around 2026-01-08 14:18:58 -06:00
ac5cbcba22 Merge branch 'main' into refinement 2026-01-07 16:43:28 -06:00
c2a8aa39f9 feat: Add Skills page, BioSection, ProjectCard, and new image assets, while refactoring the deployment workflow to build Docker images directly on my remote server. 2026-01-07 16:36:24 -06:00
64170db444 Squashed Deploy Configuration Test Env
All checks were successful
Deploy to Production / deploy (push) Successful in 6s
2026-01-07 16:31:27 -06:00
36 changed files with 2365 additions and 946 deletions

View File

@@ -4,72 +4,16 @@ on:
push:
branches: [ "main" ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
type=sha
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
deploy:
runs-on: ubuntu-latest
needs: build-and-push
steps:
- name: Deploy to Remote Server
- name: Deploy Application
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.REMOTE_HOST }}
username: ${{ secrets.REMOTE_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
passphrase: ${{ secrets.SSH_PASSPHRASE }}
script: |
# Login to registry
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
# Pull new image
docker pull ghcr.io/${{ env.IMAGE_NAME }}:latest
# Stop and remove existing container
docker stop resume-frontend || true
docker rm resume-frontend || true
# Run new container
docker run -d \
--name resume-frontend \
--restart unless-stopped \
-p 80:80 \
ghcr.io/${{ env.IMAGE_NAME }}:latest
# Cleanup unused images
docker image prune -f
powershell -ExecutionPolicy Bypass -Command "Write-Host '=== Starting deployment ==='; if (Test-Path 'C:\projects\digital-resume-FE') { Set-Location 'C:\projects\digital-resume-FE'; git pull origin main } else { New-Item -ItemType Directory -Path 'C:\projects' -Force; Set-Location 'C:\projects'; git clone https://gitea.sashabayda.ca/Bayda77/digital-resume-FE.git }; Write-Host '=== Stopping container ==='; docker stop resume-frontend; docker rm resume-frontend; Write-Host '=== Building image ==='; Set-Location 'C:\projects\digital-resume-FE'; docker build -t resume-frontend:latest .; Write-Host '=== Running container ==='; docker run -d --name resume-frontend --network nginx_web --restart unless-stopped -p 3001:80 resume-frontend:latest; Write-Host '=== Verifying ==='; docker ps -a --filter name=resume-frontend"

308
package-lock.json generated
View File

@@ -22,8 +22,12 @@
"react-dom": "^19.1.1",
"react-router-dom": "^7.10.0",
"react-scripts": "5.0.1",
"react-syntax-highlighter": "^16.1.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@types/react-syntax-highlighter": "^15.5.13"
}
},
"node_modules/@adobe/css-tools": {
@@ -2042,9 +2046,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -3744,6 +3748,15 @@
"@types/node": "*"
}
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"license": "MIT",
"dependencies": {
"@types/unist": "*"
}
},
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@@ -3844,6 +3857,12 @@
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==",
"license": "MIT"
},
"node_modules/@types/prismjs": {
"version": "1.26.5",
"resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz",
"integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==",
"license": "MIT"
},
"node_modules/@types/q": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz",
@@ -3880,6 +3899,16 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-syntax-highlighter": {
"version": "15.5.13",
"resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz",
"integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -3952,6 +3981,12 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
@@ -5604,6 +5639,36 @@
"node": ">=10"
}
},
"node_modules/character-entities": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
"integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-legacy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-reference-invalid": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
"integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/check-types": {
"version": "11.2.3",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz",
@@ -5851,6 +5916,16 @@
"node": ">= 0.8"
}
},
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
@@ -6538,6 +6613,19 @@
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/decode-named-character-reference": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
"integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
"license": "MIT",
"dependencies": {
"character-entities": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
@@ -8051,6 +8139,19 @@
"reusify": "^1.0.4"
}
},
"node_modules/fault": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
"license": "MIT",
"dependencies": {
"format": "^0.2.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/faye-websocket": {
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
@@ -8432,6 +8533,14 @@
"node": ">= 6"
}
},
"node_modules/format": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
"engines": {
"node": ">=0.4.x"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -8933,6 +9042,36 @@
"node": ">= 0.4"
}
},
"node_modules/hast-util-parse-selector": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hastscript": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz",
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-parse-selector": "^4.0.0",
"property-information": "^7.0.0",
"space-separated-tokens": "^2.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -8942,6 +9081,21 @@
"he": "bin/he"
}
},
"node_modules/highlight.js": {
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/highlightjs-vue": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz",
"integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==",
"license": "CC0-1.0"
},
"node_modules/hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -9385,6 +9539,30 @@
"node": ">= 10"
}
},
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
"integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-alphanumerical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
"integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
"license": "MIT",
"dependencies": {
"is-alphabetical": "^2.0.0",
"is-decimal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -9530,6 +9708,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-decimal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
"integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-docker": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
@@ -9617,6 +9805,16 @@
"node": ">=0.10.0"
}
},
"node_modules/is-hexadecimal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
"integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
@@ -11335,6 +11533,20 @@
"tslib": "^2.0.3"
}
},
"node_modules/lowlight": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
"license": "MIT",
"dependencies": {
"fault": "^1.0.0",
"highlight.js": "~10.7.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -12101,6 +12313,31 @@
"node": ">=6"
}
},
"node_modules/parse-entities": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
"integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^2.0.0",
"character-entities-legacy": "^3.0.0",
"character-reference-invalid": "^2.0.0",
"decode-named-character-reference": "^1.0.0",
"is-alphanumerical": "^2.0.0",
"is-decimal": "^2.0.0",
"is-hexadecimal": "^2.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/parse-entities/node_modules/@types/unist": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@@ -13701,6 +13938,15 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/prismjs": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -13746,6 +13992,16 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/property-information": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -14183,6 +14439,26 @@
}
}
},
"node_modules/react-syntax-highlighter": {
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-16.1.0.tgz",
"integrity": "sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"highlight.js": "^10.4.1",
"highlightjs-vue": "^1.0.0",
"lowlight": "^1.17.0",
"prismjs": "^1.30.0",
"refractor": "^5.0.0"
},
"engines": {
"node": ">= 16.20.2"
},
"peerDependencies": {
"react": ">= 0.14.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@@ -14265,6 +14541,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/refractor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/refractor/-/refractor-5.0.0.tgz",
"integrity": "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"@types/prismjs": "^1.0.0",
"hastscript": "^9.0.0",
"parse-entities": "^4.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -15282,6 +15574,16 @@
"deprecated": "Please use @jridgewell/sourcemap-codec instead",
"license": "MIT"
},
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/spdy": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",

View File

@@ -17,6 +17,7 @@
"react-dom": "^19.1.1",
"react-router-dom": "^7.10.0",
"react-scripts": "5.0.1",
"react-syntax-highlighter": "^16.1.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
@@ -43,5 +44,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/react-syntax-highlighter": "^15.5.13"
}
}

BIN
public/beszel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
public/fampic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
public/gangpic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

BIN
public/giteaAction.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
public/homelabber.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
public/janepic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

BIN
public/tgPic.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -1,603 +0,0 @@
.background {
overflow: hidden;
overflow-y: auto;
background: transparent;
margin: 0;
height: 100vh;
width: 100vw;
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: flex-start;
}
.mainContentBlock {
box-sizing: border-box;
height: auto;
min-height: 100vh;
max-height: 200vh;
width: 66vw;
min-width: auto;
max-width: max-content;
}
.horizontalContentItem {
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
width: 100%;
}
.verticalContentItem {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
width: 100%;
}
.flexContainer {
display: flex;
align-items: center;
justify-content: space-evenly;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 3%;
}
p {
margin: 0;
padding: 0;
}
/* Home page specific styles */
.hero-card {
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.portrait-img {
object-fit: cover;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 8px;
width: 100%;
height: 100%;
}
.welcome-text {
font-size: clamp(10px, 1.5vw, 100px);
font-family: roboto, sans-serif;
color: white;
display: flex;
overflow: hidden;
}
.name-text {
font-size: clamp(24px, 8vw, 120px);
font-family: roboto, sans-serif;
color: white;
white-space: nowrap;
}
.skills-list {
height: 5vh;
color: white;
font-family: roboto, sans-serif;
}
.spacer {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 75px;
font-family: roboto, sans-serif;
}
.contact-link {
cursor: pointer;
text-decoration: none;
color: white;
transition: color 0.3s ease;
font-size: clamp(10px, 2vw, 1.2em);
}
.contact-link:hover {
color: #00ff00;
}
.floating-header {
position: fixed;
top: 0;
left: 0;
right: 0;
width: 100%;
z-index: 1000;
padding: 20px 0px;
text-align: center;
background: rgba(52, 87, 245, 0.1);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
margin: 0;
}
.header-nav {
display: flex;
justify-content: center;
gap: clamp(10px, 2vw, 40px);
flex-wrap: nowrap;
padding: 0 20px;
}
.nav-link {
text-decoration: none;
color: rgba(255, 255, 255, 0.8);
font-family: "roboto, sans-serif";
font-size: clamp(10px, 1.5vw, 18px);
font-weight: 500;
transition: all 0.3s ease;
position: relative;
padding-bottom: 5px;
white-space: nowrap;
}
.nav-link:hover {
color: rgba(255, 255, 255, 1);
}
.nav-link::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: rgba(255, 255, 255, 0.8);
transition: width 0.3s ease;
}
.nav-link:hover::after {
width: 100%;
}
.nav-link.active {
color: rgba(255, 255, 255, 1);
}
.nav-link.active::after {
width: 100%;
}
.contentContainer {
font-size: clamp(10px, 1.5vw, 60px);
font-family: roboto, sans-serif;
color: white;
max-width: 800px;
}
.contentCard {
margin-bottom: 40px;
padding: 20px;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-radius: 10px;
color: white;
}
.contentCard:last-child {
margin-bottom: 0;
}
.contentCard h2 {
white-space: normal;
}
.contentCard p {
white-space: normal;
font-size: 18px;
}
@keyframes slideInFromLeft {
from {
opacity: 0;
transform: translateX(-50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* About Page Styles */
.about-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: auto;
/* Changed from 80vh to auto to let flex parent handle it, or reduced */
height: 100%;
padding: 20px;
/* Reduced from 40px */
color: white;
font-family: 'Roboto', sans-serif;
}
.about-title {
font-size: clamp(32px, 5vw, 60px);
margin-top: 0;
/* Clear top margin */
margin-bottom: 20px;
/* Reduced from 40px */
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.about-content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 40px;
/* Reduced from 60px */
max-width: 1200px;
width: 100%;
}
.about-image-container {
flex: 0 0 auto;
}
.about-image {
width: 250px;
height: 250px;
object-fit: cover;
border-radius: 20px;
border: 3px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease;
}
.about-image:hover {
transform: scale(1.02);
}
.about-text-container {
flex: 1;
width: 100%;
max-width: 600px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 30px 30px 30px 30px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.about-text {
font-size: clamp(16px, 1.2vw, 18px);
line-height: 1.8;
color: rgba(255, 255, 255, 0.95);
white-space: pre-line;
margin: 0;
}
/* Mobile portrait mode - width < height */
@media (orientation: portrait) {
.mainContentBlock {
width: 100vw;
max-width: 100vw;
padding-left: 10px;
padding-right: 10px;
}
.about-content {
flex-direction: column;
gap: 30px;
}
.about-image {
width: 180px;
height: 180px;
}
.about-text-container {
width: 100%;
padding: 20px;
}
}
@media (max-width: 768px) {
.about-content {
flex-direction: column;
gap: 30px;
}
.about-title {
margin-bottom: 20px;
}
}
/* Projects Page Styles */
.projects-container {
width: 100%;
max-width: 1200px;
padding: 0px;
}
.projects-title {
font-size: clamp(32px, 6vw, 60px);
font-family: 'Roboto', sans-serif;
color: white;
margin-bottom: 30px;
text-align: center;
font-weight: 700;
text-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 30px;
width: 100%;
}
.project-card {
background: rgba(255, 255, 255, 0.07);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 16px;
padding: 25px;
display: flex;
flex-direction: column;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
/* Bouncy feel */
height: 100%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.project-card:hover {
background: rgba(255, 255, 255, 0.12);
transform: translateY(-8px) scale(1.02);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);
border-color: rgba(255, 255, 255, 0.3);
}
.project-card::before {
/* Shine effect */
content: '';
position: absolute;
top: 0;
left: -100%;
width: 50%;
height: 100%;
background: linear-gradient(to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.1) 50%,
rgba(255, 255, 255, 0) 100%);
transform: skewX(-25deg);
transition: none;
pointer-events: none;
}
.project-card:hover::before {
animation: shine 1.5s ease-in-out;
}
@keyframes shine {
0% {
left: -100%;
}
100% {
left: 200%;
}
}
.project-image-container {
width: 100%;
height: 180px;
border-radius: 10px;
overflow: hidden;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.project-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.project-card:hover .project-image {
transform: scale(1.1);
}
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.project-name {
font-size: 1.5rem;
font-weight: 700;
margin: 0;
color: white;
font-family: 'Roboto', sans-serif;
}
.project-description {
color: rgba(255, 255, 255, 0.85);
font-size: 0.95rem;
line-height: 1.6;
margin-bottom: 20px;
flex-grow: 1;
font-weight: 300;
}
.project-tech-stack {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.tech-chip {
background: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.9);
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.3s ease;
}
.project-card:hover .tech-chip {
background: rgba(255, 255, 255, 0.25);
}
.project-links {
display: flex;
gap: 15px;
margin-top: auto;
}
.project-link {
display: inline-flex;
align-items: center;
gap: 6px;
color: white;
text-decoration: none;
font-size: 0.9rem;
font-weight: 600;
transition: color 0.3s ease;
padding: 8px 16px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.project-link:hover {
color: white;
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
}
/* Contact Page Styles */
.contact-wrapper {
position: relative;
width: 100%;
min-height: 100vh;
display: flex;
justify-content: center;
}
.contact-background-placeholder {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: black;
z-index: -1;
}
.contact-main-block {
min-width: 66vw;
display: flex;
justify-content: center;
align-items: center;
}
.contact-card {
width: 100%;
max-width: 600px;
/* Reduced from 800px to be tighter */
padding: 40px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
background-color: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(5px);
display: flex;
flex-direction: column;
align-items: center;
}
.contact-title {
font-size: clamp(40px, 5vw, 60px);
font-family: roboto, sans-serif;
color: white;
text-align: center;
margin-bottom: 40px;
margin-top: 0;
}
.contact-content {
font-size: 1.1rem;
font-family: roboto, sans-serif;
color: rgba(255, 255, 255, 0.9);
text-align: center;
width: 100%;
}
.contact-intro {
margin-bottom: 40px;
}
.contact-links-container {
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
width: 100%;
}
.contact-link-item {
padding: 20px;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 15px;
width: 100%;
max-width: 500px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
}
.contact-link-text {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
}
.contact-label {
opacity: 0.7;
margin-right: 10px;
}
.contact-value {
color: white;
text-decoration: none;
font-weight: 500;
}

View File

@@ -1,13 +1,15 @@
import React, { useEffect, useRef } from 'react';
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
import './App.css';
import './styles/index.css';
import FloatingHeader from './components/floatingHeader';
import ParticlesBackground from './components/ParticlesBackground';
import Home from './pages/Home';
import About from './pages/About';
import WorkExperience from './pages/WorkExperience';
import Projects from './pages/Projects';
import Contact from './pages/Contact';
import Skills from './pages/Skills';
import SidequestDetail from './pages/SidequestDetail';
function ScrollToTop() {
const { pathname } = useLocation();
@@ -39,7 +41,9 @@ function App() {
<Route path="/about" element={<About />} />
<Route path="/work-experience" element={<WorkExperience />} />
<Route path="/projects" element={<Projects />} />
<Route path="/contact" element={<Contact />} />
<Route path="/skills" element={<Skills />} />
<Route path="/sidequest/:id" element={<SidequestDetail />} />
</Routes>
<div>
<div style={{ height: "20px" }}></div>

View File

@@ -0,0 +1,108 @@
import { motion, AnimatePresence } from "framer-motion";
import { useState, useEffect } from "react";
interface BioSectionProps {
imageSrc: string;
imageAlt: string;
text: string;
reversed?: boolean;
}
export default function BioSection({ imageSrc, imageAlt, text, reversed = false }: BioSectionProps) {
const images = imageSrc.split(',').map(src => src.trim()).filter(src => src.length > 0);
const [currentIndex, setCurrentIndex] = useState(0);
const [prevIndex, setPrevIndex] = useState(0);
const [direction, setDirection] = useState(1); // 1 for right, -1 for left (though we always slide right here)
useEffect(() => {
if (images.length <= 1) return;
const interval = setInterval(() => {
setPrevIndex(currentIndex);
setCurrentIndex((prev) => (prev + 1) % images.length);
}, 6000);
return () => clearInterval(interval);
}, [images.length, currentIndex]);
const handleImageClick = () => {
if (images.length <= 1) return;
setPrevIndex(currentIndex);
setCurrentIndex((prev) => (prev + 1) % images.length);
};
const slideVariants = {
enter: {
x: "-100%",
opacity: 1
},
center: {
x: 0,
opacity: 1
},
exit: {
x: "100%",
opacity: 1
}
};
return (
<div
className="about-content"
style={{
flexDirection: reversed ? "row-reverse" : "row",
}}
>
<motion.div
className="about-image-container"
initial={{ x: reversed ? 30 : -30, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.4, duration: 0.6 }}
onClick={handleImageClick}
style={{
position: "relative",
width: "250px",
height: "250px",
overflow: "hidden",
borderRadius: "20px",
border: "3px solid rgba(255, 255, 255, 0.8)",
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.2)",
cursor: images.length > 1 ? "pointer" : "default",
}}
>
<AnimatePresence initial={false}>
<motion.img
key={currentIndex}
src={images[currentIndex]}
alt={imageAlt}
variants={slideVariants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.8, ease: "easeInOut" }}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
objectFit: "cover",
}}
whileHover={{ scale: 1.05 }}
/>
</AnimatePresence>
</motion.div>
<motion.div
className="about-text-container"
initial={{ x: reversed ? -30 : 30, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.6, duration: 0.6 }}
>
<p className="about-text">
{text}
</p>
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import '../styles/components/codeBlock.css';
interface CodeBlockProps {
language?: string;
code: string;
}
const CodeBlock: React.FC<CodeBlockProps> = ({ language, code }) => (
<div className="code-block-container">
<div className="code-block-header">
<div className="window-controls">
<div className="control-dot red"></div>
<div className="control-dot yellow"></div>
<div className="control-dot green"></div>
</div>
<span className="language-label">
{language || 'text'}
</span>
</div>
<SyntaxHighlighter
language={language || 'text'}
style={vscDarkPlus}
customStyle={{
margin: 0,
borderRadius: '0 0 12px 12px',
fontSize: '14px',
background: 'transparent',
padding: '20px'
}}
>
{code}
</SyntaxHighlighter>
</div>
);
export default CodeBlock;

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useRef } from 'react';
const ParticlesBackground: React.FC = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const prevWidth = useRef(window.innerWidth);
useEffect(() => {
const canvas = canvasRef.current;
@@ -35,31 +36,6 @@ const ParticlesBackground: React.FC = () => {
this.x += this.speedX;
this.y += this.speedY;
// update speedX
let speedXRng = Math.random();
if (speedXRng > 0.75) {
this.speedX += Math.random() * 0.1;
} else if (speedXRng < 0.25) {
this.speedX -= Math.random() * 0.1;
}
if (this.speedX > 1) {
this.speedX = 1;
} else if (this.speedX < -1) {
this.speedX = -1;
}
// update speedY
let speedYRng = Math.random();
if (speedYRng > 0.75) {
this.speedY += Math.random() * 0.1;
} else if (speedYRng < 0.25) {
this.speedY -= Math.random() * 0.1;
}
if (this.speedY > 1) {
this.speedY = 1;
} else if (this.speedY < -1) {
this.speedY = -1;
}
//size
let sizeRng = Math.random();
@@ -94,11 +70,16 @@ const ParticlesBackground: React.FC = () => {
const init = () => {
if (!canvas) return;
canvas.width = window.innerWidth;
// Use logical height that works well with the 120vh style
canvas.height = window.innerHeight;
// Reduce particle count on mobile/portrait screens
const isPortrait = canvas.height > canvas.width;
const particleCount = isPortrait ? 90 : 180;
// Calculate particle count based on screen area (resolution)
// Formula: sqrt(width * height) / factor
// Desktop (1920x1080) -> ~120 particles
// Mobile (390x844) -> ~48 particles
// Mobile Landscape (844x390) -> ~48 particles (Same as portrait!)
const area = canvas.width * canvas.height;
const particleCount = Math.floor(Math.sqrt(area) / 12);
particles = [];
for (let i = 0; i < particleCount; i++) {
@@ -159,6 +140,11 @@ const ParticlesBackground: React.FC = () => {
animate();
const handleResize = () => {
// Ignore vertical-only resizes (addressing mobile browser bar toggle issue)
if (window.innerWidth === prevWidth.current) {
return;
}
prevWidth.current = window.innerWidth;
init();
}
@@ -178,7 +164,7 @@ const ParticlesBackground: React.FC = () => {
top: 0,
left: 0,
width: '100%',
height: '100%',
height: '120vh', // Extend well below viewport to cover mobile browser bar retraction
zIndex: -1, // Behind everything
}}
/>

View File

@@ -0,0 +1,76 @@
import { motion, Variants } from "framer-motion";
import "../styles/components/cards.css";
import { Link } from "react-router-dom";
export type DetailSection =
| { type: 'text'; title?: string; content: string }
| { type: 'code'; title?: string; language?: string; code: string }
| { type: 'image'; src: string; alt?: string; caption?: string; title?: string };
export interface Project {
id: number;
title: string;
description: string;
techStack: string[];
image: string;
links: {
demo?: string;
repo?: string;
};
hasDetails?: boolean;
sections?: DetailSection[];
}
interface ProjectCardProps {
project: Project;
variants: Variants;
}
export default function ProjectCard({ project, variants }: ProjectCardProps) {
return (
<motion.div
className="project-card"
variants={variants}
whileHover={{ y: -10, transition: { duration: 0.2 } }}
>
<div className="project-image-container">
<img
src={project.image}
alt={project.title}
className="project-image"
/>
</div>
<div className="project-header">
<h2 className="project-name">{project.title}</h2>
</div>
<div className="project-tech-stack">
{project.techStack.map(tech => (
<span key={tech} className="tech-chip">{tech}</span>
))}
</div>
<p className="project-description">
{project.description}
</p>
<div className="project-links">
{project.links.demo && (
<a href={project.links.demo} className="project-link" target="_blank" rel="noopener noreferrer">
Live Demo <span></span>
</a>
)}
{project.links.repo && (
<a href={project.links.repo} className="project-link" target="_blank" rel="noopener noreferrer">
GitHub <span></span>
</a>
)}
{project.hasDetails && (
<Link to={`/sidequest/${project.id}`} className="project-link" style={{ background: "rgba(255, 255, 255, 0.15)", border: "1px solid rgba(255, 255, 255, 0.3)" }}>
Read More <span></span>
</Link>
)}
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,90 @@
import React from 'react';
import CodeBlock from './CodeBlock';
interface RichTextRendererProps {
content: string;
}
const parseInline = (text: string) => {
const parts = text.split(/(\*\*.*?\*\*)/g);
return parts.map((part, index) => {
if (part.startsWith('**') && part.endsWith('**')) {
return <strong key={index} style={{ color: 'white' }}>{part.slice(2, -2)}</strong>;
}
return part;
});
};
const parseMarkdown = (text: string) => {
const lines = text.split('\n');
const elements: React.ReactNode[] = [];
let currentListItems: React.ReactNode[] = [];
lines.forEach((line, i) => {
const trimmed = line.trim();
if (trimmed.startsWith('* ') || trimmed.startsWith('- ')) {
const content = trimmed.substring(2);
currentListItems.push(
<li key={`li-${i}`} style={{ marginBottom: '4px' }}>
{parseInline(content)}
</li>
);
} else {
if (currentListItems.length > 0) {
elements.push(
<ul key={`ul-${i}`} style={{ paddingLeft: '20px', marginBottom: '16px', listStyleType: 'disc' }}>
{currentListItems}
</ul>
);
currentListItems = [];
}
if (trimmed.length > 0) {
elements.push(
<p key={`p-${i}`} style={{ marginBottom: '16px' }}>
{parseInline(line)}
</p>
);
}
}
});
if (currentListItems.length > 0) {
elements.push(
<ul key="ul-end" style={{ paddingLeft: '20px', marginBottom: '16px', listStyleType: 'disc' }}>
{currentListItems}
</ul>
);
}
return elements;
};
const RichTextRenderer: React.FC<RichTextRendererProps> = ({ content }) => {
if (!content) return null;
const parts = content.split(/```/);
return (
<div className="rich-text-content">
{parts.map((part, index) => {
if (index % 2 === 1) {
// Code block
const firstLineBreak = part.indexOf('\n');
const language = part.slice(0, firstLineBreak).trim();
const code = part.slice(firstLineBreak + 1).trim();
return <CodeBlock key={index} language={language} code={code} />;
}
// Regular text with markdown parsing
return (
<div key={index}>
{parseMarkdown(part)}
</div>
);
})}
</div>
);
};
export default RichTextRenderer;

View File

@@ -0,0 +1,82 @@
import { motion } from "framer-motion";
export interface SkillDetail {
name: string;
description: string;
}
export interface SkillCategory {
category: string;
skills: SkillDetail[];
}
interface SkillCardProps {
category: SkillCategory;
isExpanded: boolean;
onToggle: () => void;
}
export default function SkillCard({ category, isExpanded, onToggle }: SkillCardProps) {
return (
<motion.div
layout
onClick={onToggle}
style={{
background: isExpanded ? "rgba(255, 255, 255, 0.12)" : "rgba(255, 255, 255, 0.07)",
backdropFilter: "blur(10px)",
border: isExpanded ? "1px solid rgba(255, 255, 255, 0.3)" : "1px solid rgba(255, 255, 255, 0.15)",
borderRadius: "12px",
padding: "24px",
cursor: "pointer",
gridColumn: isExpanded ? "1 / -1" : "auto",
zIndex: isExpanded ? 10 : 1
}}
whileHover={!isExpanded ? { scale: 1.02, backgroundColor: "rgba(255, 255, 255, 0.12)" } : {}}
transition={{ duration: 0.3, type: "spring" }}
>
<motion.h3 layout="position" style={{ color: "white", marginTop: 0, marginBottom: "15px", fontSize: "1.2rem" }}>
{category.category} {isExpanded ? <span style={{ fontSize: "0.8em", opacity: 0.7 }}>(Click to collapse)</span> : null}
</motion.h3>
<motion.div layout="position" style={{ display: "flex", flexWrap: "wrap", gap: "10px", marginBottom: isExpanded ? "20px" : "0" }}>
{category.skills.map((skill) => (
<span
key={skill.name}
style={{
background: "rgba(255, 255, 255, 0.15)",
color: "rgba(255, 255, 255, 0.9)",
padding: "6px 12px",
borderRadius: "20px",
fontSize: "0.85rem",
fontWeight: 500,
border: "1px solid rgba(255, 255, 255, 0.1)"
}}
>
{skill.name}
</span>
))}
</motion.div>
{isExpanded && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.3 }}
style={{ borderTop: "1px solid rgba(255,255,255,0.2)", paddingTop: "20px" }}
>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: "15px" }}>
{category.skills.map((skill) => (
<div key={skill.name} style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
<strong style={{ color: "#fff", fontSize: "1rem" }}>{skill.name}</strong>
<p style={{ margin: 0, color: "rgba(255,255,255,0.7)", fontSize: "0.9rem" }}>
{skill.description}
</p>
</div>
))}
</div>
</motion.div>
)}
</motion.div>
);
}

View File

@@ -1,3 +1,5 @@
import "../styles/components/cards.css";
interface ContentCardProps {
children: React.ReactNode;
className?: string;

View File

@@ -1,42 +1,81 @@
import { useState, useEffect } from "react";
import { Link, useLocation } from "react-router-dom";
import "../styles/components/header.css";
export default function FloatingHeader() {
const location = useLocation();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
};
// Close menu when route changes
useEffect(() => {
setIsMenuOpen(false);
}, [location]);
const ROUTE_TITLES: { [key: string]: string } = {
"/": "Sasha Bayda",
"/skills": "Skills",
"/work-experience": "Work Experience",
"/projects": "Projects",
"/about": "About Me"
};
const currentTitle = ROUTE_TITLES[location.pathname] || "Sasha Bayda";
return (
<header className="floating-header">
<nav className="header-nav">
<div className="header-content">
<div className="mobile-page-title">
{currentTitle}
</div>
<button
className={`mobile-toggle ${isMenuOpen ? "open" : ""}`}
onClick={toggleMenu}
aria-label="Toggle navigation"
>
<span className="hamburger-line"></span>
<span className="hamburger-line"></span>
<span className="hamburger-line"></span>
</button>
<nav className={`header-nav ${isMenuOpen ? "is-open" : ""}`}>
<Link
to="/"
className={`nav-link ${location.pathname === "/" ? "active" : ""}`}
>
Home
</Link>
<Link
to="/skills"
className={`nav-link ${location.pathname === "/skills" ? "active" : ""}`}
>
Skills
</Link>
<Link
to="/work-experience"
className={`nav-link ${location.pathname === "/work-experience" ? "active" : ""}`}
>
Work Experience
</Link>
<Link
to="/about"
className={`nav-link ${location.pathname === "/about" ? "active" : ""}`}
>
About Me
</Link>
<Link
to="/projects"
className={`nav-link ${location.pathname === "/projects" ? "active" : ""}`}
>
Projects
Projects and Sidequests
</Link>
<Link
to="/contact"
className={`nav-link ${location.pathname === "/contact" ? "active" : ""}`}
to="/about"
className={`nav-link ${location.pathname === "/about" ? "active" : ""}`}
>
Contact
Personal About Me
</Link>
</nav>
</div>
</header>
);
}

View File

@@ -1,8 +1,15 @@
import { motion } from "framer-motion";
import VisitedMap from "../components/VisitedMap";
import BioSection from "../components/BioSection";
import "../styles/pages/about.css";
const ABOUT_TEXT = "Hi! I'm Sasha Bayda, a passionate developer focused on creating beautiful and functional web experiences. With a background in computer science and a keen eye for design, I strive to bridge the gap between technology and user-centric solutions. When I'm not coding, you can find me exploring the outdoors, experimenting with new recipes, or indulging in photography. Feel free to explore my projects and get in touch if you'd like to collaborate or learn more about my work!" + "\n\n" + "Thank you for visiting my digital resume site. I look forward to connecting with you!";
const JANE_TEXT = "Outside of work, I enjoy spending time with my girlfriend, family and friends. Doing things such as cooking, watching shows or movies, gaming, snowboarding, or just relaxing with a glass of something warm.";
const HOMELAB_TEXT = "I love hosting my own applications and learning new technologies. Right now I host my own gitea server to host my own git repositories and host my own 'runners', immich to host my own photo gallery, and a beszel to monitor all of my applications with a dashboards, graphs and webhook alerts."
const VISITED_CITIES = [
"Melfort",
"Star City",
@@ -41,31 +48,12 @@ export default function About() {
About Me
</motion.h1>
<div className="about-content">
<motion.div
className="about-image-container"
initial={{ x: -30, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.4, duration: 0.6 }}
>
<img
src="/dapperSasha.jpg"
alt="profile"
className="about-image"
<BioSection
imageSrc="/dapperSasha.jpg"
imageAlt="profile"
text={ABOUT_TEXT}
/>
</motion.div>
<motion.div
className="about-text-container"
initial={{ x: 30, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.6, duration: 0.6 }}
>
<p className="about-text">
{ABOUT_TEXT}
</p>
</motion.div>
</div>
<motion.div
style={{ marginTop: "40px", width: "100%" }}
@@ -73,12 +61,28 @@ export default function About() {
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.8, duration: 0.6 }}
>
<h2 style={{ textAlign: "center", fontSize: "1.5rem", marginBottom: "10px", color: "rgba(255,255,255,0.9)" }}>My Hot Chocolate Journey</h2>
<h2 style={{ textAlign: "center", fontSize: "1.5rem", marginBottom: "10px", color: "rgba(255,255,255,0.9)" }}>Places I've Visited</h2>
<p style={{ textAlign: "center", marginBottom: "20px", color: "rgba(255,255,255,0.7)", fontSize: "0.9rem" }}>
Places across the world where I've enjoyed a hot chocolate
Some of the More Interesting Places I've visited, I hope to one day fill this map with more cool spots!
</p>
<VisitedMap places={VISITED_CITIES} />
</motion.div>
<div style={{ marginTop: "40px" }}>
<BioSection
imageSrc="/janepic.jpg, fampic.jpg, gangpic.jpg"
imageAlt="profile"
text={JANE_TEXT}
reversed={true}
/>
</div>
<div style={{ marginTop: "40px" }}>
<BioSection
imageSrc="/beszel.png"
imageAlt="profile"
text={HOMELAB_TEXT}
reversed={false}
/>
</div>
</div>
</motion.div>
</div>

View File

@@ -1,75 +0,0 @@
import { motion } from "framer-motion";
export default function Contact() {
return (
<div className="contact-wrapper">
<div className="mainContentBlock contact-main-block">
<motion.div
className="contact-card"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8 }}
>
<motion.h1
className="contact-title"
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.6 }}
>
Contact Me
</motion.h1>
<motion.div
className="contact-content"
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.4, duration: 0.6 }}
>
<div className="contact-intro">
<p>
I would love to hear from you! Feel free to reach out through any of these channels:
</p>
</div>
<div className="contact-links-container">
<motion.div
className="contact-link-item"
whileHover={{ scale: 1.02, backgroundColor: "rgba(255, 255, 255, 0.15)" }}
>
<p className="contact-link-text">
<span className="contact-label">Email:</span>
<a href="mailto:sasha.bayda@outlook.com" className="contact-value">
sasha.bayda@outlook.com
</a>
</p>
</motion.div>
<motion.div
className="contact-link-item"
whileHover={{ scale: 1.02, backgroundColor: "rgba(255, 255, 255, 0.15)" }}
>
<p className="contact-link-text">
<span className="contact-label">Phone:</span>
<span className="contact-value">(306) 921-7145</span>
</p>
</motion.div>
<motion.div
className="contact-link-item"
whileHover={{ scale: 1.02, backgroundColor: "rgba(255, 255, 255, 0.15)" }}
>
<p className="contact-link-text">
<span className="contact-label">LinkedIn:</span>
<a href="https://www.linkedin.com/in/sasha-bayda/" target="_blank" rel="noopener noreferrer" className="contact-value">
linkedin.com/in/sasha-bayda
</a>
</p>
</motion.div>
</div>
</motion.div>
</motion.div>
</div>
</div>
);
}

View File

@@ -1,26 +1,21 @@
import { motion } from "framer-motion";
import TypingText from "../components/animatedTyping";
import FullPageImage from "../components/fullPageImage";
import { delay, motion } from "framer-motion";
import { useState } from "react";
import "../styles/pages/home.css";
// Animation and typing timing configuration
const ANIMATION_TIMINGS = {
// Animation delays (in seconds)
elementIn: 2.0, // When elements fade in
const welcomeText = `Hello! My name is Sasha Bayda and welcome to my digital resume site!
// Typing speeds (in milliseconds per character)
welcomeTextSpeed: 45,
nameSpeed: 120,
Here you will find some of my projects, skills, contact information and any information I couldn't fit into my resume.
// Typing delays (in milliseconds) - time before text starts typing
welcomeTextDelay: 2000,
nameDelay: 2000,
Feel free to explore and learn more about me and if something isn't answered, don't hesitate to reach out via the contact page!`;
// Test items animation (slides in from bottom)
testItemsStartDelay: 3.5, // When to start the first test item animation (in seconds)
testItemStaggerDelay: 0.2, // Delay between each test item (in seconds)
testItemAnimationDuration: 0.5, // Duration of each item's slide-in animation
const HOVER_TEXTS: { [key: string]: string } = {
"Email": "Drop me a line! I'm always open to new opportunities and interesting conversations.",
"Phone": "Prefer a mix of voice? Give me a call or text.",
"LinkedIn": "Let's connect professionally. View my full experience and network.",
"GitHub": "Dive into my code. See what I'm building and how I solve problems.",
"Portfolio": "See more of my work in action on my portfolio site."
};
const welcomeText = "Hello! My name is Sasha Bayda and welcome to my digital resume site! Here you will find some of my projects, skills, contact information and any information I couldn't fit into my resume. Feel free to explore and learn more about me and if something isn't answered, don't hesitate to reach out via the contact page!";
const CONTACT_LINKS = [
{ label: "Email", url: "mailto:sasha.bayda@outlook.com" },
@@ -31,50 +26,61 @@ const CONTACT_LINKS = [
];
export default function Home() {
const [displayedText, setDisplayedText] = useState(welcomeText);
return (
<div className="mainContentBlock">
<div style={{ height: "30px" }}></div>
<div className="hero-card">
<div className="horizontalContentItem" style={{ maxHeight: "100vh", justifyContent: "center" }}>
<div className="home-container">
<motion.div
className="flexContainer"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: ANIMATION_TIMINGS.elementIn, ease: "easeOut" }}
className="hero-profile-container"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, ease: "easeOut" }}
>
<img
src="/1647091917916.png"
alt="portfolio"
className="portrait-img"
alt="Sasha Bayda"
className="hero-profile-img"
/>
</motion.div>
<div
className="welcome-text flexContainer"
>
<TypingText text={welcomeText} msPerChar={ANIMATION_TIMINGS.welcomeTextSpeed} delayMs={ANIMATION_TIMINGS.welcomeTextDelay} />
</div>
</div>
<div
className="verticalContentItem"
>
<div className="name-text flexContainer">
<TypingText text="Sasha Bayda" msPerChar={ANIMATION_TIMINGS.nameSpeed} delayMs={ANIMATION_TIMINGS.nameDelay} textAlign="center" />
<div className="hero-content">
<div className="hero-name">
<TypingText
text="Sasha Bayda"
msPerChar={120}
delayMs={100}
textAlign="center"
/>
</div>
<div className="horizontalContentItem skills-list">
<motion.p
className="hero-bio"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1, delay: 2.0 }}
>
{displayedText}
</motion.p>
<div className="hero-socials">
{CONTACT_LINKS.map((link, index) => (
<motion.a
key={index}
href={link.url}
className="contact-link"
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="hero-social-link"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: ANIMATION_TIMINGS.testItemAnimationDuration,
ease: "easeOut",
delay: ANIMATION_TIMINGS.testItemsStartDelay + (index * ANIMATION_TIMINGS.testItemStaggerDelay)
duration: 0.5,
delay: 2.5 + (index * 0.1),
ease: "easeOut"
}}
>{link.label}</motion.a>
onMouseEnter={() => setDisplayedText(HOVER_TEXTS[link.label] || welcomeText)}
onMouseLeave={() => setDisplayedText(welcomeText)}
>
{link.label}
</motion.a>
))}
</div>
</div>

View File

@@ -1,24 +1,14 @@
import { motion, Variants } from "framer-motion";
import FullPageImage from "../components/fullPageImage";
import ProjectCard, { Project } from "../components/ProjectCard";
import "../styles/pages/projects.css";
interface Project {
id: number;
title: string;
description: string;
techStack: string[];
image: string; // Ensure these images exist in public/ or use placeholders
links: {
demo?: string;
repo?: string;
};
}
const PROJECTS: Project[] = [
const FEATURED_PROJECTS: Project[] = [
{
id: 1,
title: "Digital Resume",
description: "A fully responsive, glassmorphic portfolio site built to showcase my skills and experience. Features animated page transitions, typing effects, and a dynamic map component.",
techStack: ["React", "TypeScript", "Framer Motion", "Vite"],
techStack: ["React", "TypeScript", "Framer Motion", "@react-google-maps/api", "nginx", "docker"],
image: "/digitCode.jpg",
links: {
repo: "https://github.com/Bayda77/resume-site",
@@ -27,6 +17,181 @@ const PROJECTS: Project[] = [
},
];
export const SIDEQUESTS: Project[] = [
{
id: 101,
title: "Working with Gitea Workers",
description: "Implementing a CI/CD pipeline using Gitea Actions to automate deployment to a Windows production server.",
techStack: ["Gitea Actions", "Docker", "PowerShell", "YAML"],
image: "/giteaAction.png",
links: {},
hasDetails: true,
sections: [
{
type: 'text',
title: 'Overview',
content: "Automating the deployment process using Gitea Actions. This workflow listens for pushes to the main branch, connects to a remote Windows server via SSH, updates the code, rebuilds the Docker container, and restarts the service."
},
{
type: 'code',
language: 'yaml',
title: 'Gitea Runner Config',
code: `# Gitea Actions Runner
gitea-runner:
container_name: gitea-runner
image: gitea/act_runner:latest
restart: unless-stopped
networks:
- web
environment:
- GITEA_INSTANCE_URL=http://gitea:3000
- GITEA_RUNNER_REGISTRATION_TOKEN=\${GITEA_RUNNER_TOKEN}
- GITEA_RUNNER_NAME=docker-runner
# Using catthehacker/ubuntu:act-latest which includes Docker CLI for container operations
- GITEA_RUNNER_LABELS=ubuntu-latest:docker://catthehacker/ubuntu:act-latest,ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04,ubuntu-20.04:docker://catthehacker/ubuntu:act-20.04
- CONFIG_FILE=/config.yaml
volumes:
- ./gitea/runner:/data
- ./gitea/runner/config.yaml:/config.yaml:ro
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- gitea`
},
{
type: 'code',
language: 'yaml',
title: 'Deploy Workflow',
code: `name: Deploy to Production
on:
push:
branches: [ "main" ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy Application
uses: appleboy/ssh-action@v1.0.3
with:
host: \${{ secrets.REMOTE_HOST }}
username: \${{ secrets.REMOTE_USER }}
key: \${{ secrets.SSH_PRIVATE_KEY }}
passphrase: \${{ secrets.SSH_PASSPHRASE }}
script: |
powershell -ExecutionPolicy Bypass -Command "Write-Host '=== Starting deployment ==='; if (Test-Path 'C:\\projects\\digital-resume-FE') { Set-Location 'C:\\projects\\digital-resume-FE'; git pull origin main } else { New-Item -ItemType Directory -Path 'C:\\projects' -Force; Set-Location 'C:\\projects'; git clone https://gitea.sashabayda.ca/Bayda77/digital-resume-FE.git }; Write-Host '=== Stopping container ==='; docker stop resume-frontend; docker rm resume-frontend; Write-Host '=== Building image ==='; Set-Location 'C:\\projects\\digital-resume-FE'; docker build -t resume-frontend:latest .; Write-Host '=== Running container ==='; docker run -d --name resume-frontend --network nginx_web --restart unless-stopped -p 3001:80 resume-frontend:latest; Write-Host '=== Verifying ==='; docker ps -a --filter name=resume-frontend"`
},
{
type: 'text',
title: 'Reasoning',
content: "I wanted to automate the deployment process using Gitea Actions. Gitea is a self-hosted Git server and I wanted to take advantage of its built-in CI/CD pipeline to automate the deployment process. With it being self hosted this ment that I needed to self host my own runners. Which I used as a learning opportunity to learn more about deployment containers"
},
{
type: 'text',
title: 'Challenges',
content: "Some of the main challenges was ensuring security to the host system and ensuring the deployment process was reliable and consistent. This includes stuff such as not exposing port 22 to the internet and keeping 'secrets' secure"
},
{
type: 'code',
language: 'powershell',
title: 'Deployment Script',
code: `powershell -ExecutionPolicy Bypass -Command "Write-Host '=== Starting deployment ==='; if (Test-Path 'C:\\projects\\digital-resume-FE') { Set-Location 'C:\\projects\\digital-resume-FE'; git pull origin main } else { New-Item -ItemType Directory -Path 'C:\\projects' -Force; Set-Location 'C:\\projects'; git clone https://gitea.sashabayda.ca/Bayda77/digital-resume-FE.git }; Write-Host '=== Stopping container ==='; docker stop resume-frontend; docker rm resume-frontend; Write-Host '=== Building image ==='; Set-Location 'C:\\projects\\digital-resume-FE'; docker build -t resume-frontend:latest .; Write-Host '=== Running container ==='; docker run -d --name resume-frontend --network nginx_web --restart unless-stopped -p 3001:80 resume-frontend:latest; Write-Host '=== Verifying ==='; docker ps -a --filter name=resume-frontend"`
},
{
type: 'text',
title: 'Deployment Script Explanation',
content: "The deployment script is a PowerShell script that is used to deploy the digital resume frontend to a Windows server. It first checks if the project directory exists, if it does it pulls the latest changes from the repository, if it doesn't it clones the repository. Then it stops the container, removes it, builds the image, and runs the container. Finally it verifies that the container is running and reports to gitea if it was successfull"
},
]
},
{
id: 102,
title: "Home Lab Architecture",
description: "A comprehensive self-hosted homelab running 20+ services. Orchestrated with Docker Compose for media automation, personal cloud storage, and secure remote access.",
techStack: ["Docker", "Nginx", "Tailscale", "Authentik", "PostgreSQL"],
image: "/homelabber.png", // Placeholder
links: {},
hasDetails: true,
sections: [
{
type: 'text',
title: 'Overview',
content: "My homelab acts as my personal cloud, giving me control over my data and providing a playground for learning system administration. It runs on a dedicated machine using Docker Compose to manage a suite of interconnected microservices. Access is secured via Tailscale VPN and Authentik SSO."
},
{
type: 'text',
title: 'Media & Automation',
content: "The core of the lab is the media stack. **Jellyfin** serves as the streaming frontend. Behind the scenes, automation tools handle content acquisition and organization. **Jellyseerr** provides a clean interface for requesting new additions to the library."
},
{
type: 'text',
title: 'Personal Cloud & Productivity',
content: "I've replaced several commercial services with self-hosted alternatives:\n\n* **Immich**: A powerful photo and video backup solution, replacing Google Photos.\n* **Gitea**: My self-hosted Git server for code management (including CI/CD runners).\n* **Mealie**: A recipe manager to organize and plan meals."
},
{
type: 'text',
title: 'Infrastructure & Security',
content: "Reliability and security are key. **Nginx** handles reverse proxying with **Certbot** managing SSL certificates. **Fail2ban** prevents intrusion attempts. **Authentik** provides a unified identity layer. **Portainer** offers a GUI for Docker management, and **Tailscale** creates a secure mesh network for remote access without exposing ports."
},
{
type: 'text',
title: 'Monitoring',
content: "**Homepage** serves as the main dashboard. **Beszel** and **Uptime Kuma** monitor system resources and service availability, while **Speedtest Tracker** keeps a historic log of network performance."
},
{
type: 'code',
language: 'yaml',
title: 'Docker Compose Configuration',
code: `services:
nginx:
image: nginx:stable
restart: unless-stopped
ports: ["80:80", "443:443"]
# ... configurations
immich-server:
image: ghcr.io/immich-app/immich-server:release
environment:
- DB_HOSTNAME=immich-postgres
- REDIS_HOSTNAME=immich-redis
depends_on: [immich-redis, immich-postgres]
gitea:
image: gitea/gitea:latest
ports: ["3000:3000", "2222:22"]
environment:
- GITEA__actions__ENABLED=true
jellyfin:
image: jellyfin/jellyfin:latest
ports: ["8096:8096"]
volumes:
- ./media:/media
# Monitoring
beszel:
image: henrygd/beszel:latest
ports: ["8090:8090"]
uptime-kuma:
image: louislam/uptime-kuma:latest
ports: ["3002:3001"]
# Security & Networking
tailscale:
image: tailscale/tailscale:latest
network_mode: host
authentik-server:
image: ghcr.io/goauthentik/server:latest
ports: ["9001:9000", "9444:9443"]
# ... full configuration includes 20+ services`
}
]
}
];
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
@@ -53,8 +218,14 @@ const cardVariants: Variants = {
export default function Projects() {
return (
<div style={{ position: "relative", width: "100%", minHeight: "100vh" }}>
<div className="mainContentBlock" style={{ width: "100%", minWidth: "100vw", maxWidth: "100vw", paddingTop: "20px", paddingBottom: "10px", boxSizing: "border-box", display: "flex", justifyContent: "center", position: "relative", zIndex: 1 }}>
<div className="mainContentBlock" style={{ width: "100%", minWidth: "100vw", maxWidth: "100vw", paddingTop: "20px", paddingBottom: "100px", boxSizing: "border-box", display: "flex", justifyContent: "center", position: "relative", zIndex: 1 }}>
<div className="projects-container">
{/* Featured Projects Section */}
<motion.div
initial="hidden"
animate="visible"
variants={containerVariants}
>
<motion.h1
className="projects-title"
initial={{ y: -30, opacity: 0 }}
@@ -64,54 +235,35 @@ export default function Projects() {
Featured Projects
</motion.h1>
<motion.div
className="projects-grid"
variants={containerVariants}
initial="hidden"
animate="visible"
>
{PROJECTS.map((project) => (
<motion.div
key={project.id}
className="project-card"
variants={cardVariants}
whileHover={{ y: -10, transition: { duration: 0.2 } }}
>
<div className="project-image-container">
<img
src={project.image}
alt={project.title}
className="project-image"
/>
</div>
<div className="project-header">
<h2 className="project-name">{project.title}</h2>
</div>
<div className="project-tech-stack">
{project.techStack.map(tech => (
<span key={tech} className="tech-chip">{tech}</span>
<div className="projects-grid">
{FEATURED_PROJECTS.map((project) => (
<ProjectCard key={project.id} project={project} variants={cardVariants} />
))}
</div>
<p className="project-description">
{project.description}
</p>
<div className="project-links">
{project.links.demo && (
<a href={project.links.demo} className="project-link" target="_blank" rel="noopener noreferrer">
Live Demo <span></span>
</a>
)}
{project.links.repo && (
<a href={project.links.repo} className="project-link" target="_blank" rel="noopener noreferrer">
GitHub <span></span>
</a>
)}
</div>
</motion.div>
{/* Sidequests Section */}
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
variants={containerVariants}
style={{ marginTop: "80px" }}
>
<motion.h2
className="projects-title" // Reusing title style for consistency, maybe smaller?
style={{ fontSize: "clamp(24px, 4vw, 40px)", marginBottom: "30px" }}
variants={cardVariants}
>
Sidequests
</motion.h2>
<div className="projects-grid">
{SIDEQUESTS.map((project) => (
<ProjectCard key={project.id} project={project} variants={cardVariants} />
))}
</div>
</motion.div>
</div>
</div>

View File

@@ -0,0 +1,107 @@
import { useEffect } from "react";
import { useParams, Link } from "react-router-dom";
import { motion } from "framer-motion";
import { SIDEQUESTS } from "./Projects";
import CodeBlock from "../components/CodeBlock";
import RichTextRenderer from "../components/RichTextRenderer";
import "../styles/pages/sidequestDetail.css";
export default function SidequestDetail() {
const { id } = useParams<{ id: string }>();
const project = SIDEQUESTS.find(p => p.id === Number(id));
useEffect(() => {
window.scrollTo(0, 0);
}, []);
if (!project) {
return (
<div className="sidequest-error-container">
<h2>Sidequest not found</h2>
<Link to="/projects" className="sidequest-back-link-error">Back to Projects</Link>
</div>
);
}
return (
<div className="mainContentBlock sidequest-container">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="sidequest-content-wrapper"
>
<Link to="/projects" className="sidequest-back-link">
Back to Projects
</Link>
<div className="sidequest-header">
<h1 className="sidequest-title">{project.title}</h1>
<div className="sidequest-tech-stack">
{project.techStack.map(tech => (
<span key={tech} className="sidequest-tech-tag">
{tech}
</span>
))}
</div>
</div>
<div className="sidequest-hero-image-container">
<img
src={project.image}
alt={project.title}
className="sidequest-hero-image"
/>
</div>
<div className="sidequest-details-container">
{/* Dynamic Sections Rendering */}
{project.sections?.map((section, index) => {
switch (section.type) {
case 'text':
return (
<section key={index} className="sidequest-section">
{section.title && <h2 className="sidequest-section-title">{section.title}</h2>}
<div className="sidequest-section-content">
<RichTextRenderer content={section.content} />
</div>
</section>
);
case 'code':
return (
<section key={index} className="sidequest-section">
{section.title && <h2 className="sidequest-section-title">{section.title}</h2>}
<CodeBlock language={section.language} code={section.code} />
</section>
);
case 'image':
return (
<section key={index} className="sidequest-section">
{section.title && <h2 className="sidequest-section-title">{section.title}</h2>}
<img src={section.src} alt={section.alt || ''} className="sidequest-section-image" />
{section.caption && <p className="sidequest-section-caption">{section.caption}</p>}
</section>
);
default:
return null;
}
})}
<div className="sidequest-footer-links">
{project.links.demo && (
<a href={project.links.demo} target="_blank" rel="noopener noreferrer" className="sidequest-footer-link">
Live Demo
</a>
)}
{project.links.repo && (
<a href={project.links.repo} target="_blank" rel="noopener noreferrer" className="sidequest-footer-link">
View Code
</a>
)}
</div>
</div>
<div className="sidequest-footer-spacer"></div>
</motion.div>
</div>
);
}

255
src/pages/Skills.tsx Normal file
View File

@@ -0,0 +1,255 @@
import { motion, Variants, AnimatePresence } from "framer-motion";
import { useState } from "react";
import SkillCard, { SkillCategory } from "../components/SkillCard";
interface ProfessionalSkill {
skill: string;
description: string;
}
const TECHNICAL_SKILLS: SkillCategory[] = [
{
category: "Languages",
skills: [
{ name: "JavaScript (ES6+)", description: "Core language for web interactivity, closures, and async programming." },
{ name: "TypeScript", description: "Strict syntactical superset of JavaScript adding static typing." },
{ name: "Python", description: "Versatile language used for backend scripting and data processing." },
{ name: "Java", description: "Object-oriented language for robust enterprise backends." },
{ name: "C", description: "Low-level system programming and memory management." },
{ name: "C++", description: "High-performance application development with OOP features." },
{ name: "Lua", description: "Lightweight scripting often used in embedded systems and game dev." },
{ name: "HTML", description: "Standard markup language for document structure." },
{ name: "CSS", description: "Style sheet language for presentation and layout." },
{ name: "SQL", description: "Standard language for relational database management." }
]
},
{
category: "Frameworks/Libraries",
skills: [
{ name: "React", description: "Library for building component-based user interfaces." },
{ name: "Express.js", description: "Minimalist web framework for Node.js servers." },
{ name: "Django REST Framework", description: "Toolkit for building Web APIs with Python/Django." },
{ name: "AWS Lambda", description: "Serverless compute service for running code without provisioning." }
]
},
{
category: "Databases",
skills: [
{ name: "PostgreSQL", description: "Advanced open-source relational database (SQL)." },
{ name: "CouchDB", description: "Seamless multi-master sync capability database (No-SQL)." }
]
},
{
category: "DevOps & Tools",
skills: [
{ name: "Docker", description: "Platform for developing, shipping, and running applications in containers." },
{ name: "CircleCI", description: "Continuous integration and delivery platform." },
{ name: "Git (CLI)", description: "Distributed version control system." },
{ name: "Postman", description: "API platform for building and using APIs." },
{ name: "AWS CDK", description: "Software development framework for defining cloud infrastructure in code." },
{ name: "Linux CLI", description: "Command line interface for system interaction and scripting." }
]
},
{
category: "AWS Technologies",
skills: [
{ name: "S3", description: "Scalable object storage service." },
{ name: "Cloudfront", description: "Content Delivery Network (CDN) service." },
{ name: "Route 53", description: "Scalable Domain Name System (DNS) web service." },
{ name: "API Gateway", description: "Service to create, publish, maintain, monitor, and secure APIs." },
{ name: "Lambda", description: "Serverless compute service." },
{ name: "RDS", description: "Managed relational database service." },
{ name: "VPC", description: "Logically isolated section of the AWS Cloud." },
{ name: "ECS", description: "Fully managed container orchestration service." },
{ name: "SQS", description: "Message queuing service for decoupling microservices." }
]
},
{
category: "Concepts",
skills: [
{ name: "RESTful APIs", description: "Architectural style for network-based software." },
{ name: "Agile/Scrum", description: "Iterative approach to project software delivery." },
{ name: "MVC Architecture", description: "Design pattern separating Model, View, and Controller." },
{ name: "Cloud Deployment", description: "Process of deploying applications to cloud infrastructure." },
{ name: "Responsive Design", description: "Design approach for rendering well on variety of devices." }
]
}
];
const PROFESSIONAL_SKILLS: ProfessionalSkill[] = [
{
skill: "Customer Service & Sales",
description: "Clear communication and coordination with customers and clients"
},
{
skill: "Technical Support",
description: "Diagnosing and resolving hardware/software issues across all major devices."
},
{
skill: "Web Development",
description: "WordPress plugin development, front-end design, and site optimization."
},
{
skill: "Scripting & Programming",
description: "JavaScript, Python, HTML/CSS, Git, and NPM package management."
},
{
skill: "Collaboration & Problem-Solving",
description: "Working effectively with diverse teams to meet deadlines and ensure quality results."
},
{
skill: "Adaptability",
description: "Quickly learning new tools, technologies, and processes in dynamic work environments."
}
];
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2
}
}
};
const itemVariants: Variants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { type: "spring", stiffness: 100 }
}
};
export default function Skills() {
const [expandedCategories, setExpandedCategories] = useState<string[]>([]);
const toggleCategory = (category: string) => {
setExpandedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
);
};
const allExpanded = expandedCategories.length === TECHNICAL_SKILLS.length;
const toggleAll = () => {
if (allExpanded) {
setExpandedCategories([]);
} else {
setExpandedCategories(TECHNICAL_SKILLS.map(c => c.category));
}
};
return (
<div className="mainContentBlock" style={{ minWidth: "66vw", display: "flex", justifyContent: "center" }}>
<div style={{ height: "30px" }}></div>
<motion.div
className="skills-container"
variants={containerVariants}
initial="hidden"
animate="visible"
style={{ width: "100%", maxWidth: "1200px", padding: "0 40px", boxSizing: "border-box" }}
>
<motion.div variants={itemVariants} style={{ marginBottom: "50px" }}>
<div style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
borderBottom: "1px solid rgba(255,255,255,0.2)",
marginBottom: "25px",
paddingBottom: "10px",
marginTop: "15px" // Restore approximate default h2 top margin
}}>
<h2 style={{ color: "white", margin: 0 }}>
Technical Skills
</h2>
<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
<span style={{ color: "rgba(255,255,255,0.8)", fontSize: "0.9rem" }}>Expand All</span>
<div
onClick={toggleAll}
style={{
width: "40px",
height: "22px",
background: allExpanded ? "rgba(255, 255, 255, 0.3)" : "rgba(255, 255, 255, 0.1)",
borderRadius: "11px",
display: "flex",
alignItems: "center",
padding: "2px",
cursor: "pointer",
boxSizing: "border-box",
border: "1px solid rgba(255,255,255,0.2)",
transition: "background 0.3s"
}}
>
<motion.div
animate={{
x: allExpanded ? 18 : 0,
backgroundColor: allExpanded ? "#ffffff" : "rgba(255,255,255,0.5)"
}}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
style={{
width: "16px",
height: "16px",
borderRadius: "50%",
backgroundColor: "white",
boxShadow: "0 1px 3px rgba(0,0,0,0.2)"
}}
/>
</div>
</div>
</div>
<div className="tech-skills-grid" style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(320px, 1fr))",
gap: "30px",
position: "relative" // For layout animations
}}>
<AnimatePresence>
{TECHNICAL_SKILLS.map((category) => (
<SkillCard
key={category.category}
category={category}
isExpanded={expandedCategories.includes(category.category)}
onToggle={() => toggleCategory(category.category)}
/>
))}
</AnimatePresence>
</div>
</motion.div>
<motion.div variants={itemVariants} style={{ marginBottom: "50px" }}>
<h2 style={{ color: "white", borderBottom: "1px solid rgba(255,255,255,0.2)", paddingBottom: "10px", marginBottom: "25px" }}>
Professional Skills
</h2>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(320px, 1fr))", gap: "25px" }}>
{PROFESSIONAL_SKILLS.map((skill) => (
<motion.div
key={skill.skill}
style={{
background: "rgba(255, 255, 255, 0.05)",
backdropFilter: "blur(5px)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "12px",
padding: "20px",
display: "flex",
flexDirection: "column",
gap: "8px"
}}
whileHover={{ x: 5, backgroundColor: "rgba(255, 255, 255, 0.08)" }}
>
<h3 style={{ color: "#fff", margin: 0, fontSize: "1.1rem" }}>{skill.skill}</h3>
<p style={{ color: "rgba(255, 255, 255, 0.7)", margin: 0, fontSize: "0.95rem", lineHeight: "1.5" }}>
{skill.description}
</p>
</motion.div>
))}
</div>
</motion.div>
</motion.div >
</div >
);
}

View File

@@ -10,7 +10,7 @@ const WORK_EXPERIENCES = [
"Provide excellent customer service by explaining repair processes, timelines, and care instructions.",
"Collaborate with team members to coordinate and work efficiently, delivering the best possible service."
],
image: "towerglass.jpg",
image: "tgPic.jpg",
highlighted: false
},
{
@@ -70,7 +70,7 @@ const WORK_EXPERIENCES = [
"Assisted in high-volume automotive glass installations, developing fundamental skills in tool handling and glass preparation.",
"Performed quality control checks, ensuring rigorous safety standards were met for every client.",
],
image: "towerglass.jpg",
image: "tgPic.jpg",
highlighted: false
},
];

109
src/styles/base.css Normal file
View File

@@ -0,0 +1,109 @@
/* Base styles - Layout utilities and common elements */
.background {
overflow: hidden;
overflow-y: auto;
background: transparent;
margin: 0;
height: 100vh;
width: 100vw;
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: flex-start;
}
.mainContentBlock {
box-sizing: border-box;
height: auto;
min-height: 100vh;
max-height: 200vh;
width: 66vw;
min-width: auto;
max-width: max-content;
}
.horizontalContentItem {
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
width: 100%;
}
.verticalContentItem {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
width: 100%;
}
.flexContainer {
display: flex;
align-items: center;
justify-content: space-evenly;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 3%;
}
p {
margin: 0;
padding: 0;
}
.contentContainer {
font-size: clamp(10px, 1.5vw, 60px);
font-family: roboto, sans-serif;
color: white;
max-width: 800px;
}
.spacer {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 75px;
font-family: roboto, sans-serif;
}
.contact-link {
cursor: pointer;
text-decoration: none;
color: white;
transition: color 0.3s ease;
font-size: clamp(10px, 2vw, 1.2em);
}
.contact-link:hover {
color: #00ff00;
}
/* Animations */
@keyframes slideInFromLeft {
from {
opacity: 0;
transform: translateX(-50px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Mobile portrait mode - width < height */
@media (orientation: portrait) {
.mainContentBlock {
width: 100vw;
max-width: 100vw;
padding-left: 10px;
padding-right: 10px;
}
}

View File

@@ -0,0 +1,182 @@
/* Content Card and Project Card Component Styles */
.contentCard {
margin-bottom: 40px;
padding: 20px;
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border-radius: 10px;
color: white;
}
.contentCard:last-child {
margin-bottom: 0;
}
.contentCard h2 {
white-space: normal;
}
.contentCard p {
white-space: normal;
font-size: 18px;
}
/* Hero Card (Legacy) */
.hero-card {
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: space-between;
}
/* Project Card Styles */
.project-card {
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 24px;
padding: 25px;
display: flex;
flex-direction: column;
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
height: 100%;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
}
.project-card:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
border-color: rgba(255, 255, 255, 0.4);
}
.project-card::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 50%;
height: 100%;
background: linear-gradient(to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.1) 50%,
rgba(255, 255, 255, 0) 100%);
transform: skewX(-25deg);
transition: none;
pointer-events: none;
}
.project-card:hover::before {
animation: shine 1.5s ease-in-out;
}
@keyframes shine {
0% {
left: -100%;
}
100% {
left: 200%;
}
}
.project-image-container {
width: 100%;
height: 180px;
border-radius: 10px;
overflow: hidden;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.project-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s ease;
}
.project-card:hover .project-image {
transform: scale(1.1);
}
.project-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.project-name {
font-size: 1.5rem;
font-weight: 700;
margin: 0;
color: white;
font-family: 'Roboto', sans-serif;
}
.project-description {
color: rgba(255, 255, 255, 0.85);
font-size: 0.95rem;
line-height: 1.6;
margin-bottom: 20px;
flex-grow: 1;
font-weight: 300;
}
.project-tech-stack {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 20px;
}
.tech-chip {
background: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.9);
padding: 6px 12px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.3s ease;
}
.project-card:hover .tech-chip {
background: rgba(255, 255, 255, 0.25);
}
.project-links {
display: flex;
gap: 15px;
margin-top: auto;
}
.project-link {
display: inline-flex;
align-items: center;
gap: 6px;
color: white;
text-decoration: none;
font-size: 0.9rem;
font-weight: 600;
transition: color 0.3s ease;
padding: 8px 16px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.project-link:hover {
color: white;
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
}

View File

@@ -0,0 +1,48 @@
.code-block-container {
margin: 30px 0;
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
background: #1e1e1e;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.code-block-header {
display: flex;
align-items: center;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.window-controls {
display: flex;
gap: 8px;
margin-right: auto;
}
.control-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.control-dot.red {
background: #ff5f56;
}
.control-dot.yellow {
background: #ffbd2e;
}
.control-dot.green {
background: #27c93f;
}
.language-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
font-family: monospace;
text-transform: uppercase;
margin-top: 2px;
}

View File

@@ -0,0 +1,169 @@
/* Floating Header Component Styles */
.floating-header {
position: fixed;
top: 0;
left: 0;
right: 0;
width: 100%;
z-index: 1000;
padding: 20px 0px;
text-align: center;
background: rgba(52, 87, 245, 0.1);
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
margin: 0;
box-sizing: border-box;
}
.header-nav {
display: flex;
justify-content: center;
gap: clamp(8px, 2vw, 32px);
flex-wrap: nowrap;
padding: 0 20px;
}
.nav-link {
text-decoration: none;
color: rgba(255, 255, 255, 0.8);
font-family: "roboto, sans-serif";
font-size: clamp(14px, 2vw, 20px);
font-weight: 500;
transition: all 0.3s ease;
position: relative;
padding-bottom: 5px;
white-space: nowrap;
}
.nav-link:hover {
color: rgba(255, 255, 255, 1);
}
.nav-link::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 0;
height: 2px;
background: rgba(255, 255, 255, 0.8);
transition: width 0.3s ease;
}
.nav-link:hover::after {
width: 100%;
}
.nav-link.active {
color: rgba(255, 255, 255, 1);
}
.nav-link.active::after {
width: 100%;
}
/* Mobile Navigation Styles */
.header-content {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
.mobile-page-title {
display: none;
/* Hidden on desktop */
}
.mobile-toggle {
display: none;
flex-direction: column;
justify-content: space-between;
width: 30px;
height: 21px;
background: transparent;
border: none;
cursor: pointer;
z-index: 1001;
padding: 0;
}
.hamburger-line {
width: 100%;
height: 3px;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 3px;
transition: all 0.3s ease;
}
@media (max-width: 1024px) {
.floating-header {
padding: 15px 20px;
text-align: left;
}
.mobile-page-title {
display: block;
color: white;
font-size: 1.2rem;
font-weight: 700;
letter-spacing: 0.5px;
z-index: 1001;
/* Ensure visible above nav overlay if needed, though nav usually covers content */
position: relative;
}
.header-content {
justify-content: space-between;
/* Push title left, hamburger right */
}
.mobile-toggle {
display: flex;
}
/* Animate Hamburger to X */
.mobile-toggle.open .hamburger-line:nth-child(1) {
transform: translateY(9px) rotate(45deg);
}
.mobile-toggle.open .hamburger-line:nth-child(2) {
opacity: 0;
}
.mobile-toggle.open .hamburger-line:nth-child(3) {
transform: translateY(-9px) rotate(-45deg);
}
.header-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100vh;
background: rgba(10, 10, 30, 0.95);
backdrop-filter: blur(15px);
flex-direction: column;
justify-content: center;
align-items: center;
gap: 30px;
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
z-index: 999;
/* Below toggle button */
padding: 0;
}
.header-nav.is-open {
transform: translateX(0);
}
.nav-link {
font-size: 24px;
padding: 10px 0;
}
}

4
src/styles/index.css Normal file
View File

@@ -0,0 +1,4 @@
/* Main CSS Entry Point - Shared/Base styles only */
/* Component and page-specific styles are imported directly in their respective files */
@import './base.css';

View File

@@ -0,0 +1,98 @@
/* About Page Styles */
.about-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: auto;
height: 100%;
padding: 20px;
color: white;
font-family: 'Roboto', sans-serif;
}
.about-title {
font-size: clamp(32px, 5vw, 60px);
margin-top: 0;
margin-bottom: 20px;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.about-content {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 40px;
max-width: 1200px;
width: 100%;
}
.about-image-container {
flex: 0 0 auto;
}
.about-image {
width: 250px;
height: 250px;
object-fit: cover;
border-radius: 20px;
border: 3px solid rgba(255, 255, 255, 0.8);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease;
}
.about-image:hover {
transform: scale(1.02);
}
.about-text-container {
flex: 1;
width: 100%;
max-width: 600px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 30px 30px 30px 30px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.about-text {
font-size: clamp(16px, 1.2vw, 18px);
line-height: 1.8;
color: rgba(255, 255, 255, 0.95);
white-space: pre-line;
margin: 0;
}
/* Mobile/Responsive */
@media (orientation: portrait) {
.about-content {
flex-direction: column;
gap: 30px;
}
.about-image {
width: 180px;
height: 180px;
}
.about-text-container {
width: 100%;
padding: 20px;
}
}
@media (max-width: 768px) {
.about-content {
flex-direction: column;
gap: 30px;
}
.about-title {
margin-bottom: 20px;
}
}

131
src/styles/pages/home.css Normal file
View File

@@ -0,0 +1,131 @@
/* Home Page Styles */
.home-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 80vh;
text-align: center;
gap: 30px;
padding: 20px;
}
.hero-profile-container {
padding: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 50%;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.hero-profile-img {
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 50%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
display: block;
}
.hero-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
color: white;
max-width: 700px;
}
.hero-name {
font-size: clamp(40px, 6vw, 80px);
font-family: 'Roboto', sans-serif;
font-weight: 700;
color: white;
margin: 0;
letter-spacing: -0.5px;
text-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.hero-bio {
font-size: clamp(16px, 1.5vw, 20px);
font-weight: 300;
line-height: 1.6;
color: rgba(255, 255, 255, 0.9);
max-width: 600px;
min-height: 160px;
}
.hero-socials {
display: flex;
gap: 20px;
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
}
.hero-social-link {
text-decoration: none;
color: rgba(255, 255, 255, 0.85);
font-size: 16px;
font-weight: 500;
padding: 10px 24px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50px;
background: rgba(255, 255, 255, 0.05);
transition: all 0.3s ease;
backdrop-filter: blur(5px);
}
.hero-social-link:hover {
background: rgba(255, 255, 255, 0.2);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: rgba(255, 255, 255, 0.5);
}
/* Legacy Home styles */
.portrait-img {
object-fit: cover;
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 8px;
width: 100%;
height: 100%;
}
.welcome-text {
font-size: clamp(10px, 1.5vw, 100px);
font-family: roboto, sans-serif;
color: white;
display: flex;
overflow: hidden;
}
.name-text {
font-size: clamp(24px, 8vw, 120px);
font-family: roboto, sans-serif;
color: white;
white-space: nowrap;
}
.skills-list {
height: 5vh;
color: white;
font-family: roboto, sans-serif;
}
/* Mobile/Responsive */
@media (orientation: portrait) {
.home-container {
padding-top: 50px;
}
}
@media (max-width: 768px) {
.home-container {
min-height: 70vh;
gap: 20px;
}
}

View File

@@ -0,0 +1,24 @@
/* Projects Page Styles */
.projects-container {
width: 100%;
max-width: 1200px;
padding: 0px;
}
.projects-title {
font-size: clamp(32px, 6vw, 60px);
font-family: 'Roboto', sans-serif;
color: white;
margin-bottom: 30px;
text-align: center;
font-weight: 700;
text-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
}
.projects-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 30px;
width: 100%;
}

View File

@@ -0,0 +1,131 @@
.sidequest-error-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: white;
}
.sidequest-back-link-error {
color: rgba(255, 255, 255, 0.7);
margin-top: 20px;
}
.sidequest-container {
min-width: 66vw;
display: flex;
justify-content: center;
padding-top: 40px;
}
.sidequest-content-wrapper {
width: 100%;
max-width: 800px;
padding: 0 20px;
}
.sidequest-back-link {
display: inline-flex;
align-items: center;
color: rgba(255, 255, 255, 0.6);
text-decoration: none;
margin-bottom: 30px;
transition: color 0.2s ease;
}
.sidequest-back-link:hover {
color: white;
}
.sidequest-header {
margin-bottom: 40px;
}
.sidequest-title {
font-size: 2.5rem;
margin-bottom: 10px;
color: white;
}
.sidequest-tech-stack {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}
.sidequest-tech-tag {
background: rgba(255, 255, 255, 0.1);
padding: 4px 12px;
border-radius: 15px;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.9);
}
.sidequest-hero-image-container {
width: 100%;
height: 400px;
border-radius: 16px;
overflow: hidden;
margin-bottom: 40px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.sidequest-hero-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.sidequest-details-container {
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 40px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.sidequest-section {
margin-bottom: 30px;
}
.sidequest-section-title {
color: white;
font-size: 1.5rem;
margin-bottom: 15px;
}
.sidequest-section-content {
color: rgba(255, 255, 255, 0.8);
line-height: 1.7;
}
.sidequest-section-image {
width: 100%;
border-radius: 12px;
}
.sidequest-section-caption {
color: rgba(255, 255, 255, 0.5);
font-size: 0.9rem;
margin-top: 10px;
text-align: center;
}
.sidequest-footer-links {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
gap: 20px;
}
.sidequest-footer-link {
color: white;
text-decoration: underline;
}
.sidequest-footer-spacer {
height: 100px;
}