Compare commits
26 Commits
cd79edaf89
...
refinement
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f8b4e690e | |||
| c2f3b57837 | |||
| 89a35f286d | |||
| febe971d0c | |||
| 0054d35234 | |||
| 4291c2fd9e | |||
| 4019d4cb10 | |||
| dee891ef68 | |||
| fa64c4a254 | |||
| e8ec353c8f | |||
| d31de41ca3 | |||
| b6aa1b0316 | |||
| 91683d1767 | |||
| 65fbec3e33 | |||
| 127aa9acc7 | |||
| 54831798d0 | |||
| b78fea0762 | |||
| efc68a4486 | |||
| b62bdb906c | |||
| ac5cbcba22 | |||
| c2a8aa39f9 | |||
| 64170db444 | |||
| 563787c8cb | |||
| 877005ed79 | |||
| 9cbb405ba0 | |||
| aaa95be433 |
19
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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"
|
||||||
2
.gitignore
vendored
@@ -24,4 +24,4 @@ yarn-error.log*
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
#public facing Images
|
#public facing Images
|
||||||
/public
|
# /public
|
||||||
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:18-alpine as build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
COPY --from=build /app/build /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
14
nginx.conf
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
||||||
308
package-lock.json
generated
@@ -22,8 +22,12 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.10.0",
|
"react-router-dom": "^7.10.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"react-syntax-highlighter": "^16.1.0",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
@@ -2042,9 +2046,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.28.3",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||||
"integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
|
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -3744,6 +3748,15 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/html-minifier-terser": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
|
"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==",
|
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/q": {
|
||||||
"version": "1.5.8",
|
"version": "1.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.8.tgz",
|
||||||
@@ -3880,6 +3899,16 @@
|
|||||||
"@types/react": "^19.0.0"
|
"@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": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||||
@@ -3952,6 +3981,12 @@
|
|||||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/ws": {
|
||||||
"version": "8.18.1",
|
"version": "8.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
@@ -5604,6 +5639,36 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/check-types": {
|
||||||
"version": "11.2.3",
|
"version": "11.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz",
|
||||||
@@ -5851,6 +5916,16 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/commander": {
|
||||||
"version": "8.3.0",
|
"version": "8.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||||
@@ -6538,6 +6613,19 @@
|
|||||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/dedent": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
|
||||||
@@ -8051,6 +8139,19 @@
|
|||||||
"reusify": "^1.0.4"
|
"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": {
|
"node_modules/faye-websocket": {
|
||||||
"version": "0.11.4",
|
"version": "0.11.4",
|
||||||
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
|
"resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
|
||||||
@@ -8432,6 +8533,14 @@
|
|||||||
"node": ">= 6"
|
"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": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -8933,6 +9042,36 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/he": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
@@ -8942,6 +9081,21 @@
|
|||||||
"he": "bin/he"
|
"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": {
|
"node_modules/hoopy": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
|
||||||
@@ -9385,6 +9539,30 @@
|
|||||||
"node": ">= 10"
|
"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": {
|
"node_modules/is-array-buffer": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
|
"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"
|
"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": {
|
"node_modules/is-docker": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||||
@@ -9617,6 +9805,16 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/is-map": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
|
||||||
@@ -11335,6 +11533,20 @@
|
|||||||
"tslib": "^2.0.3"
|
"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": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -12101,6 +12313,31 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/parse-json": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
"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"
|
"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": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"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==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -14265,6 +14541,22 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/regenerate": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
|
||||||
@@ -15282,6 +15574,16 @@
|
|||||||
"deprecated": "Please use @jridgewell/sourcemap-codec instead",
|
"deprecated": "Please use @jridgewell/sourcemap-codec instead",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/spdy": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-router-dom": "^7.10.0",
|
"react-router-dom": "^7.10.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
|
"react-syntax-highlighter": "^16.1.0",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
@@ -43,5 +44,8 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.2 KiB |
BIN
public/images/about/aurora1.jpg
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
public/images/about/beach1.jpg
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
public/images/about/dapperSasha.jpg
Normal file
|
After Width: | Height: | Size: 394 KiB |
BIN
public/images/about/family1.jpg
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
public/images/about/family2.jpg
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
public/images/about/friend2.jpg
Normal file
|
After Width: | Height: | Size: 432 KiB |
BIN
public/images/about/gamecube.jpg
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
public/images/about/girl1.jpg
Normal file
|
After Width: | Height: | Size: 604 KiB |
BIN
public/images/about/kitty.mp4
Normal file
BIN
public/images/about/melfort1.jpg
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
public/images/about/melfort2.jpg
Normal file
|
After Width: | Height: | Size: 806 KiB |
BIN
public/images/about/melfort3.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/images/about/melfort4.jpg
Normal file
|
After Width: | Height: | Size: 349 KiB |
BIN
public/images/about/melfort5.jpg
Normal file
|
After Width: | Height: | Size: 470 KiB |
BIN
public/images/about/outdoor1.jpg
Normal file
|
After Width: | Height: | Size: 406 KiB |
BIN
public/images/about/outdoor2.jpg
Normal file
|
After Width: | Height: | Size: 427 KiB |
BIN
public/images/about/pcBuild.jpg
Normal file
|
After Width: | Height: | Size: 395 KiB |
BIN
public/images/about/slab.jpg
Normal file
|
After Width: | Height: | Size: 520 KiB |
BIN
public/images/about/snowboard1.mp4
Normal file
BIN
public/images/about/snowboard2.mp4
Normal file
BIN
public/images/about/steak.jpg
Normal file
|
After Width: | Height: | Size: 825 KiB |
BIN
public/images/home/1647091917916.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
public/images/projects/20251111_224823.jpg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
public/images/projects/beszel.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
public/images/projects/digitCode.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
public/images/projects/giteaAction.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
public/images/projects/homelabber.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
public/images/work/allanslandscaping.jpeg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/images/work/cnh.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/images/work/homedepot.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/images/work/nutrien.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
public/images/work/tgPic.jpg
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
@@ -1,22 +1,20 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
<head>
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<meta charset="utf-8" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta
|
<meta name="theme-color" content="#000000" />
|
||||||
name="description"
|
<meta name="description" content="Digital portfolio and resume of Sasha Bayda, a passionate web developer." />
|
||||||
content="Digital portfolio and resume of Sasha Bayda, a passionate web developer."
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
/>
|
<!--
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
-->
|
-->
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<!--
|
<!--
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
@@ -25,12 +23,13 @@
|
|||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<title>Sasha Bayda | Digital Resume</title>
|
<title>Bayda's Digital Resume</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<body>
|
||||||
<div id="root"></div>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<!--
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
This HTML file is a template.
|
This HTML file is a template.
|
||||||
If you open it directly in the browser, you will see an empty page.
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
@@ -40,5 +39,6 @@
|
|||||||
To begin the development, run `npm start` or `yarn start`.
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
-->
|
-->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
||||||
BIN
public/old_favicon.ico
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
603
src/App.css
@@ -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;
|
|
||||||
}
|
|
||||||
10
src/App.tsx
@@ -1,13 +1,15 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||||
import './App.css';
|
import './styles/index.css';
|
||||||
import FloatingHeader from './components/floatingHeader';
|
import FloatingHeader from './components/floatingHeader';
|
||||||
import ParticlesBackground from './components/ParticlesBackground';
|
import ParticlesBackground from './components/ParticlesBackground';
|
||||||
import Home from './pages/Home';
|
import Home from './pages/Home';
|
||||||
import About from './pages/About';
|
import About from './pages/About';
|
||||||
import WorkExperience from './pages/WorkExperience';
|
import WorkExperience from './pages/WorkExperience';
|
||||||
import Projects from './pages/Projects';
|
import Projects from './pages/Projects';
|
||||||
import Contact from './pages/Contact';
|
import Skills from './pages/Skills';
|
||||||
|
import SidequestDetail from './pages/SidequestDetail';
|
||||||
|
|
||||||
|
|
||||||
function ScrollToTop() {
|
function ScrollToTop() {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
@@ -39,7 +41,9 @@ function App() {
|
|||||||
<Route path="/about" element={<About />} />
|
<Route path="/about" element={<About />} />
|
||||||
<Route path="/work-experience" element={<WorkExperience />} />
|
<Route path="/work-experience" element={<WorkExperience />} />
|
||||||
<Route path="/projects" element={<Projects />} />
|
<Route path="/projects" element={<Projects />} />
|
||||||
<Route path="/contact" element={<Contact />} />
|
<Route path="/skills" element={<Skills />} />
|
||||||
|
<Route path="/sidequest/:id" element={<SidequestDetail />} />
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ height: "20px" }}></div>
|
<div style={{ height: "20px" }}></div>
|
||||||
|
|||||||
174
src/components/BentoGrid.tsx
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import '../styles/components/BentoGrid.css';
|
||||||
|
|
||||||
|
interface BentoItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
className?: string; // For sizing: bento-large, bento-wide, bento-tall
|
||||||
|
bgImage: string | string[];
|
||||||
|
videoSrc?: string; // Optional video source
|
||||||
|
}
|
||||||
|
|
||||||
|
const items: BentoItem[] = [
|
||||||
|
{
|
||||||
|
id: 'gaming',
|
||||||
|
title: "Gaming",
|
||||||
|
description: "From Old Classic Fighting Game Tournaments to New PC Gaming Experiences. I enjoy it all",
|
||||||
|
className: "bento-large",
|
||||||
|
bgImage: "/images/about/slab.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'snowboarding',
|
||||||
|
title: "Snowboarding",
|
||||||
|
description: "Here is me teaching my girlfriend😅",
|
||||||
|
className: "bento-tall",
|
||||||
|
bgImage: "https://placehold.co/400x800/a7ffeb/004d40?text=Snowboarding",
|
||||||
|
videoSrc: "/images/about/snowboard2.mp4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cooking',
|
||||||
|
title: "Cooking",
|
||||||
|
description: "Everyone cooks sure, I'll give you that. But I want to master the art of cooking a excellent steak.",
|
||||||
|
className: "",
|
||||||
|
bgImage: "/images/about/steak.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'social',
|
||||||
|
title: "Family & Friends",
|
||||||
|
description: "Good times with my favorite people. Including my Girlfriend, Friends and Family",
|
||||||
|
className: "bento-tall",
|
||||||
|
bgImage: [
|
||||||
|
"/images/about/girl1.jpg",
|
||||||
|
"/images/about/family1.jpg",
|
||||||
|
"/images/about/family2.jpg",
|
||||||
|
"/images/about/friend2.jpg",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'homelab',
|
||||||
|
title: "Home Labbing",
|
||||||
|
description: "I enjoy the challenge of self-hosting my own digital services at home. I also love tinkering with new hardware and software setups.",
|
||||||
|
className: "bento-wide",
|
||||||
|
bgImage: "/images/projects/homelabber.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'controller-modding',
|
||||||
|
title: "Controller Modding",
|
||||||
|
description: "Restoring and modifying retro game controllers. Giving new life to old hardware.",
|
||||||
|
className: "", // Standard 1x1
|
||||||
|
bgImage: "/images/about/gamecube.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'outdoor-exploring',
|
||||||
|
title: "Outdoor Exploring",
|
||||||
|
description: "Hiking mountains, exploring caves, beaches and more.",
|
||||||
|
className: "bento-tall",
|
||||||
|
bgImage: [
|
||||||
|
"/images/about/outdoor1.jpg",
|
||||||
|
"/images/about/outdoor2.jpg",
|
||||||
|
"/images/about/beach1.jpg",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'smart-home',
|
||||||
|
title: "Smart Home",
|
||||||
|
description: "Automating my living space with smart devices and custom integrations.",
|
||||||
|
className: "", // Standard 1x1
|
||||||
|
bgImage: "https://placehold.co/400x400/0288d1/e1f5fe?text=Smart+Home"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pc-building',
|
||||||
|
title: "Computer Building",
|
||||||
|
description: "Designing and assembling custom PC builds for performance and aesthetics.",
|
||||||
|
className: "bento-wide",
|
||||||
|
bgImage: "/images/about/pcBuild.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'small-town',
|
||||||
|
title: "Small Town Origin",
|
||||||
|
description: "I'm from Melfort, Saskatchewan. A small town with a big heart.",
|
||||||
|
className: "", // Standard 1x1
|
||||||
|
bgImage: [
|
||||||
|
"/images/about/melfort1.jpg",
|
||||||
|
"/images/about/melfort2.jpg",
|
||||||
|
"/images/about/melfort3.png",
|
||||||
|
"/images/about/melfort4.jpg",
|
||||||
|
"/images/about/melfort5.jpg",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cats',
|
||||||
|
title: "Cats",
|
||||||
|
description: "I see a cat, I pet a cat. Simple as that.",
|
||||||
|
className: "bento-large",
|
||||||
|
bgImage: "https://placehold.co/800x800/ffccbc/bf360c?text=Cats", // Fallback
|
||||||
|
videoSrc: "/images/about/kitty.mp4"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const BentoCard = ({ item, index, delay }: { item: BentoItem; index: number; delay: number }) => {
|
||||||
|
const images = Array.isArray(item.bgImage) ? item.bgImage : [item.bgImage];
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (images.length <= 1) return;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCurrentIndex((prev) => (prev + 1) % images.length);
|
||||||
|
}, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [images.length]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={`bento-item ${item.className || ''}`}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: delay + index * 0.1 }}
|
||||||
|
>
|
||||||
|
{item.videoSrc ? (
|
||||||
|
<video
|
||||||
|
src={item.videoSrc}
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
className="bento-bg"
|
||||||
|
style={{ objectFit: 'cover', opacity: 0.6 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AnimatePresence mode="popLayout" initial={false}>
|
||||||
|
<motion.img
|
||||||
|
key={currentIndex}
|
||||||
|
src={images[currentIndex]}
|
||||||
|
alt={item.title}
|
||||||
|
className="bento-bg"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 0.6 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.8 }}
|
||||||
|
onError={(e) => {
|
||||||
|
e.currentTarget.src = "https://placehold.co/600x400?text=" + item.title;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AnimatePresence>
|
||||||
|
)}
|
||||||
|
<div className="bento-overlay" />
|
||||||
|
<div className="bento-content">
|
||||||
|
<h3 className="bento-title">{item.title}</h3>
|
||||||
|
<p className="bento-description">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BentoGrid({ delay = 0 }: { delay?: number }) {
|
||||||
|
return (
|
||||||
|
<div className="bento-grid-container">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<BentoCard key={item.id} item={item} index={index} delay={delay} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
src/components/BioSection.tsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
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 ${reversed ? 'reversed' : ''}`.trim()}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/components/CodeBlock.tsx
Normal 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;
|
||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useRef } from 'react';
|
|||||||
|
|
||||||
const ParticlesBackground: React.FC = () => {
|
const ParticlesBackground: React.FC = () => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const prevWidth = useRef(window.innerWidth);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
@@ -35,31 +36,6 @@ const ParticlesBackground: React.FC = () => {
|
|||||||
this.x += this.speedX;
|
this.x += this.speedX;
|
||||||
this.y += this.speedY;
|
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
|
//size
|
||||||
let sizeRng = Math.random();
|
let sizeRng = Math.random();
|
||||||
@@ -94,11 +70,17 @@ const ParticlesBackground: React.FC = () => {
|
|||||||
const init = () => {
|
const init = () => {
|
||||||
if (!canvas) return;
|
if (!canvas) return;
|
||||||
canvas.width = window.innerWidth;
|
canvas.width = window.innerWidth;
|
||||||
|
// Use logical height that works well with the 120vh style
|
||||||
canvas.height = window.innerHeight;
|
canvas.height = window.innerHeight;
|
||||||
|
|
||||||
// Reduce particle count on mobile/portrait screens
|
// Calculate particle count based on screen area (resolution)
|
||||||
const isPortrait = canvas.height > canvas.width;
|
// Formula: sqrt(width * height) / factor
|
||||||
const particleCount = isPortrait ? 90 : 180;
|
// Desktop (1920x1080) -> ~120 particles
|
||||||
|
// Mobile (390x844) -> ~48 particles
|
||||||
|
// Mobile Landscape (844x390) -> ~48 particles (Same as portrait!)
|
||||||
|
const area = canvas.width * canvas.height;
|
||||||
|
// Reduce density further to specificially help the heavy About page
|
||||||
|
const particleCount = Math.floor(Math.sqrt(area) / 12);
|
||||||
|
|
||||||
particles = [];
|
particles = [];
|
||||||
for (let i = 0; i < particleCount; i++) {
|
for (let i = 0; i < particleCount; i++) {
|
||||||
@@ -109,17 +91,7 @@ const ParticlesBackground: React.FC = () => {
|
|||||||
const animate = () => {
|
const animate = () => {
|
||||||
if (!ctx || !canvas) return;
|
if (!ctx || !canvas) return;
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
// Draw a subtle gradient background for the canvas itself if needed, or keep transparent
|
// Gradient is now handled by CSS to avoid repainting every frame
|
||||||
// For now, let's keep it transparent so we can style the container behind it if we want,
|
|
||||||
// OR we can make this the definitive background.
|
|
||||||
// Let's add a deep gradient here to match the user's previous aesthetic but cooler.
|
|
||||||
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
|
|
||||||
gradient.addColorStop(0, '#0f0c29');
|
|
||||||
gradient.addColorStop(0.5, '#302b63');
|
|
||||||
gradient.addColorStop(1, '#24243e');
|
|
||||||
ctx.fillStyle = gradient;
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
||||||
|
|
||||||
|
|
||||||
particles.forEach((particle) => {
|
particles.forEach((particle) => {
|
||||||
particle.update();
|
particle.update();
|
||||||
@@ -138,18 +110,38 @@ const ParticlesBackground: React.FC = () => {
|
|||||||
for (let b = a; b < particles.length; b++) {
|
for (let b = a; b < particles.length; b++) {
|
||||||
const dx = particles[a].x - particles[b].x;
|
const dx = particles[a].x - particles[b].x;
|
||||||
const dy = particles[a].y - particles[b].y;
|
const dy = particles[a].y - particles[b].y;
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
// Optimization 1: Strictly check max possible visible distance
|
||||||
|
// The opacity formula becomes <= 0 when distance > (2/3 * distanceThreshold).
|
||||||
|
// Max distanceThreshold is 300, so max visible distance is 200.
|
||||||
|
if (Math.abs(dx) > 200 || Math.abs(dy) > 200) continue;
|
||||||
|
|
||||||
|
const distSq = dx * dx + dy * dy;
|
||||||
|
|
||||||
const sizeAverage = (particles[a].size + particles[b].size) / 2;
|
const sizeAverage = (particles[a].size + particles[b].size) / 2;
|
||||||
// Scale from 100 to 300 based on size average (2 to 6)
|
// Scale from 100 to 300 based on size average (2 to 6)
|
||||||
const distanceThreshold = 100 + (sizeAverage - 2) * 50;
|
const distanceThreshold = 100 + (sizeAverage - 2) * 50;
|
||||||
|
|
||||||
if (distance < distanceThreshold) {
|
// Optimization 2: Only calculate if we are within the VISIBLE threshold (2/3 of logical threshold)
|
||||||
ctx.strokeStyle = `rgba(255, 255, 255, ${(distanceThreshold * (2 / 3) * .001) - distance / 1000})`;
|
// Opacity = (Threshold * 2/3 * 0.001) - (dist / 1000)
|
||||||
ctx.lineWidth = sizeAverage;
|
// 0 = (2/3 T)/1000 - d/1000 => d = 2/3 T
|
||||||
ctx.beginPath();
|
const distinctThreshold = distanceThreshold * (2 / 3);
|
||||||
ctx.moveTo(particles[a].x, particles[a].y);
|
const distinctThresholdSq = distinctThreshold * distinctThreshold;
|
||||||
ctx.lineTo(particles[b].x, particles[b].y);
|
|
||||||
ctx.stroke();
|
if (distSq < distinctThresholdSq) {
|
||||||
|
const distance = Math.sqrt(distSq);
|
||||||
|
|
||||||
|
// Calculate opacity
|
||||||
|
const opacity = (distanceThreshold * (2 / 3) * .001) - distance / 1000;
|
||||||
|
|
||||||
|
if (opacity > 0) {
|
||||||
|
ctx.strokeStyle = `rgba(255, 255, 255, ${opacity})`;
|
||||||
|
ctx.lineWidth = sizeAverage;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(particles[a].x, particles[a].y);
|
||||||
|
ctx.lineTo(particles[b].x, particles[b].y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -159,6 +151,11 @@ const ParticlesBackground: React.FC = () => {
|
|||||||
animate();
|
animate();
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
|
// Ignore vertical-only resizes (addressing mobile browser bar toggle issue)
|
||||||
|
if (window.innerWidth === prevWidth.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prevWidth.current = window.innerWidth;
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,8 +175,9 @@ const ParticlesBackground: React.FC = () => {
|
|||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '120vh', // Extend well below viewport to cover mobile browser bar retraction
|
||||||
zIndex: -1, // Behind everything
|
zIndex: -1, // Behind everything
|
||||||
|
background: 'linear-gradient(to bottom, #0f0c29 0%, #302b63 50%, #24243e 100%)'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
76
src/components/ProjectCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
90
src/components/RichTextRenderer.tsx
Normal 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;
|
||||||
93
src/components/SkillCard.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
export interface SkillDetail {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillCategory {
|
||||||
|
category: string;
|
||||||
|
description?: 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" }}
|
||||||
|
>
|
||||||
|
{category.description && (
|
||||||
|
<p style={{
|
||||||
|
color: "rgba(255, 255, 255, 0.8)",
|
||||||
|
marginBottom: "20px",
|
||||||
|
fontSize: "1rem",
|
||||||
|
lineHeight: "1.5"
|
||||||
|
}}>
|
||||||
|
{category.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import "../styles/components/cards.css";
|
||||||
|
|
||||||
interface ContentCardProps {
|
interface ContentCardProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|||||||
@@ -1,42 +1,81 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import "../styles/components/header.css";
|
||||||
|
|
||||||
export default function FloatingHeader() {
|
export default function FloatingHeader() {
|
||||||
const location = useLocation();
|
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 (
|
return (
|
||||||
<header className="floating-header">
|
<header className="floating-header">
|
||||||
<nav className="header-nav">
|
<div className="header-content">
|
||||||
<Link
|
<div className="mobile-page-title">
|
||||||
to="/"
|
{currentTitle}
|
||||||
className={`nav-link ${location.pathname === "/" ? "active" : ""}`}
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`mobile-toggle ${isMenuOpen ? "open" : ""}`}
|
||||||
|
onClick={toggleMenu}
|
||||||
|
aria-label="Toggle navigation"
|
||||||
>
|
>
|
||||||
Home
|
<span className="hamburger-line"></span>
|
||||||
</Link>
|
<span className="hamburger-line"></span>
|
||||||
<Link
|
<span className="hamburger-line"></span>
|
||||||
to="/work-experience"
|
</button>
|
||||||
className={`nav-link ${location.pathname === "/work-experience" ? "active" : ""}`}
|
|
||||||
>
|
<nav className={`header-nav ${isMenuOpen ? "is-open" : ""}`}>
|
||||||
Work Experience
|
<Link
|
||||||
</Link>
|
to="/"
|
||||||
<Link
|
className={`nav-link ${location.pathname === "/" ? "active" : ""}`}
|
||||||
to="/about"
|
>
|
||||||
className={`nav-link ${location.pathname === "/about" ? "active" : ""}`}
|
Home
|
||||||
>
|
</Link>
|
||||||
About Me
|
<Link
|
||||||
</Link>
|
to="/skills"
|
||||||
<Link
|
className={`nav-link ${location.pathname === "/skills" ? "active" : ""}`}
|
||||||
to="/projects"
|
>
|
||||||
className={`nav-link ${location.pathname === "/projects" ? "active" : ""}`}
|
Skills
|
||||||
>
|
</Link>
|
||||||
Projects
|
<Link
|
||||||
</Link>
|
to="/work-experience"
|
||||||
<Link
|
className={`nav-link ${location.pathname === "/work-experience" ? "active" : ""}`}
|
||||||
to="/contact"
|
>
|
||||||
className={`nav-link ${location.pathname === "/contact" ? "active" : ""}`}
|
Work Experience
|
||||||
>
|
</Link>
|
||||||
Contact
|
<Link
|
||||||
</Link>
|
to="/projects"
|
||||||
</nav>
|
className={`nav-link ${location.pathname === "/projects" ? "active" : ""}`}
|
||||||
|
>
|
||||||
|
Projects and Sidequests
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/about"
|
||||||
|
className={`nav-link ${location.pathname === "/about" ? "active" : ""}`}
|
||||||
|
>
|
||||||
|
Personal About Me
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import VisitedMap from "../components/VisitedMap";
|
import VisitedMap from "../components/VisitedMap";
|
||||||
|
import BioSection from "../components/BioSection";
|
||||||
|
import "../styles/pages/about.css";
|
||||||
|
import BentoGrid from "../components/BentoGrid";
|
||||||
|
|
||||||
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 ABOUT_TEXT = "Hey, I'm Sasha! You probably know I code, but that's just one part of myself. When I step away from the screen I've got many other things that I'm interested in." + "\n\n" + "Check that out below in the Bento Grid to learn more about me!";
|
||||||
|
|
||||||
const VISITED_CITIES = [
|
const VISITED_CITIES = [
|
||||||
"Melfort",
|
"Melfort",
|
||||||
@@ -31,7 +34,7 @@ export default function About() {
|
|||||||
transition={{ duration: 0.8 }}
|
transition={{ duration: 0.8 }}
|
||||||
>
|
>
|
||||||
<div style={{ width: "100%", maxWidth: "1200px" }}>
|
<div style={{ width: "100%", maxWidth: "1200px" }}>
|
||||||
<motion.h1
|
{/* <motion.h1
|
||||||
className="about-title"
|
className="about-title"
|
||||||
initial={{ y: -20, opacity: 0 }}
|
initial={{ y: -20, opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
@@ -39,43 +42,32 @@ export default function About() {
|
|||||||
style={{ textAlign: "center" }}
|
style={{ textAlign: "center" }}
|
||||||
>
|
>
|
||||||
About Me
|
About Me
|
||||||
</motion.h1>
|
</motion.h1> */}
|
||||||
|
|
||||||
<div className="about-content">
|
<BioSection
|
||||||
<motion.div
|
imageSrc="/images/about/dapperSasha.jpg"
|
||||||
className="about-image-container"
|
imageAlt="profile"
|
||||||
initial={{ x: -30, opacity: 0 }}
|
text={ABOUT_TEXT}
|
||||||
animate={{ x: 0, opacity: 1 }}
|
/>
|
||||||
transition={{ delay: 0.4, duration: 0.6 }}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/dapperSasha.jpg"
|
|
||||||
alt="profile"
|
|
||||||
className="about-image"
|
|
||||||
/>
|
|
||||||
</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: "60px", marginBottom: "40px" }}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.8 }}
|
||||||
|
>
|
||||||
|
<BentoGrid delay={1.0} />
|
||||||
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
style={{ marginTop: "40px", width: "100%" }}
|
style={{ marginTop: "40px", width: "100%" }}
|
||||||
initial={{ y: 20, opacity: 0 }}
|
initial={{ y: 20, opacity: 0 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ y: 0, opacity: 1 }}
|
||||||
transition={{ delay: 0.8, duration: 0.6 }}
|
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" }}>
|
<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>
|
</p>
|
||||||
<VisitedMap places={VISITED_CITIES} />
|
<VisitedMap places={VISITED_CITIES} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -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'd 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,21 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
import TypingText from "../components/animatedTyping";
|
import TypingText from "../components/animatedTyping";
|
||||||
import FullPageImage from "../components/fullPageImage";
|
import { useState } from "react";
|
||||||
import { delay, motion } from "framer-motion";
|
import "../styles/pages/home.css";
|
||||||
|
|
||||||
// Animation and typing timing configuration
|
const welcomeText = `Hello! My name is Sasha Bayda and welcome to my digital resume site!
|
||||||
const ANIMATION_TIMINGS = {
|
|
||||||
// Animation delays (in seconds)
|
|
||||||
elementIn: 2.0, // When elements fade in
|
|
||||||
|
|
||||||
// Typing speeds (in milliseconds per character)
|
Here you will find some of my projects, skills, contact information and any information I couldn't fit into my resume.
|
||||||
welcomeTextSpeed: 45,
|
|
||||||
nameSpeed: 120,
|
|
||||||
|
|
||||||
// Typing delays (in milliseconds) - time before text starts typing
|
Feel free to explore and learn more about me and if something isn't answered, don't hesitate to reach out via the links found below!`;
|
||||||
welcomeTextDelay: 2000,
|
|
||||||
nameDelay: 2000,
|
|
||||||
|
|
||||||
// Test items animation (slides in from bottom)
|
const HOVER_TEXTS: { [key: string]: string } = {
|
||||||
testItemsStartDelay: 3.5, // When to start the first test item animation (in seconds)
|
"Email": "Drop me a line! I'm always open to new opportunities and interesting conversations.",
|
||||||
testItemStaggerDelay: 0.2, // Delay between each test item (in seconds)
|
"Phone": "Prefer a mix of voice? Give me a call or text.",
|
||||||
testItemAnimationDuration: 0.5, // Duration of each item's slide-in animation
|
"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 = [
|
const CONTACT_LINKS = [
|
||||||
{ label: "Email", url: "mailto:sasha.bayda@outlook.com" },
|
{ label: "Email", url: "mailto:sasha.bayda@outlook.com" },
|
||||||
@@ -31,50 +26,61 @@ const CONTACT_LINKS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const [displayedText, setDisplayedText] = useState(welcomeText);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mainContentBlock">
|
<div className="mainContentBlock">
|
||||||
<div style={{ height: "30px" }}></div>
|
<div className="home-container">
|
||||||
<div className="hero-card">
|
<motion.div
|
||||||
<div className="horizontalContentItem" style={{ maxHeight: "100vh", justifyContent: "center" }}>
|
className="hero-profile-container"
|
||||||
<motion.div
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
className="flexContainer"
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
initial={{ opacity: 0 }}
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
transition={{ duration: ANIMATION_TIMINGS.elementIn, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/1647091917916.png"
|
|
||||||
alt="portfolio"
|
|
||||||
className="portrait-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">
|
<img
|
||||||
<TypingText text="Sasha Bayda" msPerChar={ANIMATION_TIMINGS.nameSpeed} delayMs={ANIMATION_TIMINGS.nameDelay} textAlign="center" />
|
src="/images/home/1647091917916.png"
|
||||||
|
alt="Sasha Bayda"
|
||||||
|
className="hero-profile-img"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="hero-content">
|
||||||
|
<div className="hero-name">
|
||||||
|
<TypingText
|
||||||
|
text="Sasha Bayda"
|
||||||
|
msPerChar={120}
|
||||||
|
delayMs={100}
|
||||||
|
textAlign="center"
|
||||||
|
/>
|
||||||
</div>
|
</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) => (
|
{CONTACT_LINKS.map((link, index) => (
|
||||||
<motion.a
|
<motion.a
|
||||||
key={index}
|
key={index}
|
||||||
href={link.url}
|
href={link.url}
|
||||||
className="contact-link"
|
className="hero-social-link"
|
||||||
initial={{ y: 50, opacity: 0 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ y: 0, opacity: 1 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{
|
transition={{
|
||||||
duration: ANIMATION_TIMINGS.testItemAnimationDuration,
|
duration: 0.5,
|
||||||
ease: "easeOut",
|
delay: 2.5 + (index * 0.1),
|
||||||
delay: ANIMATION_TIMINGS.testItemsStartDelay + (index * ANIMATION_TIMINGS.testItemStaggerDelay)
|
ease: "easeOut"
|
||||||
}}
|
}}
|
||||||
>{link.label}</motion.a>
|
onMouseEnter={() => setDisplayedText(HOVER_TEXTS[link.label] || welcomeText)}
|
||||||
|
onMouseLeave={() => setDisplayedText(welcomeText)}
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</motion.a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +1,15 @@
|
|||||||
import { motion, Variants } from "framer-motion";
|
import { motion, Variants } from "framer-motion";
|
||||||
import FullPageImage from "../components/fullPageImage";
|
import FullPageImage from "../components/fullPageImage";
|
||||||
|
import ProjectCard, { Project } from "../components/ProjectCard";
|
||||||
|
import "../styles/pages/projects.css";
|
||||||
|
|
||||||
interface Project {
|
const FEATURED_PROJECTS: 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[] = [
|
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
title: "Digital Resume",
|
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.",
|
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",
|
image: "/images/projects/digitCode.jpg",
|
||||||
links: {
|
links: {
|
||||||
repo: "https://github.com/Bayda77/resume-site",
|
repo: "https://github.com/Bayda77/resume-site",
|
||||||
demo: "https://portfolio.sashabayda.ca"
|
demo: "https://portfolio.sashabayda.ca"
|
||||||
@@ -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: "/images/projects/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: "/images/projects/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 = {
|
const containerVariants: Variants = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
visible: {
|
visible: {
|
||||||
@@ -53,70 +218,57 @@ const cardVariants: Variants = {
|
|||||||
export default function Projects() {
|
export default function Projects() {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "relative", width: "100%", minHeight: "100vh" }}>
|
<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">
|
<div className="projects-container">
|
||||||
<motion.h1
|
{/* Featured Projects Section */}
|
||||||
className="projects-title"
|
|
||||||
initial={{ y: -30, opacity: 0 }}
|
|
||||||
animate={{ y: 0, opacity: 1 }}
|
|
||||||
transition={{ duration: 0.8, ease: "easeOut" }}
|
|
||||||
>
|
|
||||||
Featured Projects
|
|
||||||
</motion.h1>
|
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="projects-grid"
|
|
||||||
variants={containerVariants}
|
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
|
variants={containerVariants}
|
||||||
>
|
>
|
||||||
{PROJECTS.map((project) => (
|
<motion.h1
|
||||||
<motion.div
|
className="projects-title"
|
||||||
key={project.id}
|
initial={{ y: -30, opacity: 0 }}
|
||||||
className="project-card"
|
animate={{ y: 0, opacity: 1 }}
|
||||||
variants={cardVariants}
|
transition={{ duration: 0.8, ease: "easeOut" }}
|
||||||
whileHover={{ y: -10, transition: { duration: 0.2 } }}
|
>
|
||||||
>
|
Featured Projects
|
||||||
<div className="project-image-container">
|
</motion.h1>
|
||||||
<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">
|
<div className="projects-grid">
|
||||||
{project.techStack.map(tech => (
|
{FEATURED_PROJECTS.map((project) => (
|
||||||
<span key={tech} className="tech-chip">{tech}</span>
|
<ProjectCard key={project.id} project={project} variants={cardVariants} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<p className="project-description">
|
|
||||||
{project.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="project-links">
|
{/* Sidequests Section */}
|
||||||
{project.links.demo && (
|
<motion.div
|
||||||
<a href={project.links.demo} className="project-link" target="_blank" rel="noopener noreferrer">
|
initial="hidden"
|
||||||
Live Demo <span>→</span>
|
whileInView="visible"
|
||||||
</a>
|
viewport={{ once: true, amount: 0.1 }}
|
||||||
)}
|
variants={containerVariants}
|
||||||
{project.links.repo && (
|
style={{ marginTop: "80px" }}
|
||||||
<a href={project.links.repo} className="project-link" target="_blank" rel="noopener noreferrer">
|
>
|
||||||
GitHub <span>↗</span>
|
<motion.h2
|
||||||
</a>
|
className="projects-title" // Reusing title style for consistency, maybe smaller?
|
||||||
)}
|
style={{ fontSize: "clamp(24px, 4vw, 40px)", marginBottom: "30px" }}
|
||||||
</div>
|
variants={cardVariants}
|
||||||
</motion.div>
|
>
|
||||||
))}
|
Sidequests
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<div className="projects-grid">
|
||||||
|
{SIDEQUESTS.map((project) => (
|
||||||
|
<ProjectCard key={project.id} project={project} variants={cardVariants} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FullPageImage
|
<FullPageImage
|
||||||
src="/20251111_224823.jpg"
|
src="/images/projects/20251111_224823.jpg"
|
||||||
alt="projects background"
|
alt="projects background"
|
||||||
credit="Sasha Bayda"
|
credit="Sasha Bayda"
|
||||||
isFixed={true}
|
isFixed={true}
|
||||||
|
|||||||
107
src/pages/SidequestDetail.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
261
src/pages/Skills.tsx
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
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",
|
||||||
|
description: "Core programming languages I have utilized across professional roles, academic coursework, and personal projects.",
|
||||||
|
skills: [
|
||||||
|
{ name: "JavaScript (ES6+)", description: "Used for frontend logic in 'Digital Resume', WordPress plugins at 'Allan's Landscaping', and the University of Saskatchewan class 'Full Stack Web Programming'." },
|
||||||
|
{ name: "TypeScript", description: "Primary language for 'Digital Resume' and 'Nutrien' cloud apps" },
|
||||||
|
{ name: "Python", description: "Utilized at 'CNH', 'Nutrien', and for the University of Saskatchewan classes 'Artificial Intelligence' & 'Information Visualization'." },
|
||||||
|
{ name: "Java", description: "Object-oriented language used in University of Saskatchewan classes 'Mobile & Cloud Computing', 'Algorithms & Machines', and 'Graphical User Interfaces'." },
|
||||||
|
{ name: "C", description: "Low-level programming applied in University of Saskatchewan classes 'Operating Systems' and 'Computer Networks' for memory and socket management." },
|
||||||
|
{ name: "C++", description: "Utilized for embedded software engineering at 'CNH' to perform whitebox testing" },
|
||||||
|
{ name: "Lua", description: "Lightweight scripting often used in embedded systems and game dev. My first foray into programming was with Lua" },
|
||||||
|
{ name: "HTML", description: "Structured 'Digital Resume', 'Allan's Landscaping' sites, and assignments for the University of Saskatchewan class 'Full Stack Web Programming'." },
|
||||||
|
{ name: "CSS", description: "Styled 'Digital Resume' with glassmorphism and interfaces for the University of Saskatchewan class 'Graphical User Interfaces' and 'Full Stack Web Programming'." },
|
||||||
|
{ name: "SQL", description: "Managed relational data for self-hosted services and in the University of Saskatchewan class 'Full Stack Web Programming'." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Frameworks/Libraries",
|
||||||
|
description: "Key frameworks and libraries that power my applications, enabling efficient and scalable development.",
|
||||||
|
skills: [
|
||||||
|
{ name: "React", description: "Built UI for 'Digital Resume', 'Nutrien' tools, and projects for the University of Saskatchewan class 'Full Stack Web Programming'." },
|
||||||
|
{ name: "Express.js", description: "Minimalist web framework for Node.js servers. Used for early Server Testing" },
|
||||||
|
{ name: "Django REST Framework", description: "Toolkit for building Web APIs with Python/Django." },
|
||||||
|
{ name: "AWS Lambda", description: "Deployed serverless functions for cloud applications at 'Nutrien'." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Databases",
|
||||||
|
description: "Data storage solutions I have implemented for varied use cases, from relational structure to flexible document stores.",
|
||||||
|
skills: [
|
||||||
|
{ name: "PostgreSQL", description: "Backend database for self-hosted services like Authentik and Immich. Used both professionally and at the University of Saskatchewan for various classes." },
|
||||||
|
{ name: "CouchDB", description: "Seamless multi-master sync capability database (No-SQL). Used for 'full stack web programming' class at the University of Saskatchewan." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "DevOps & Tools",
|
||||||
|
description: "Tools and practices I employ to automate workflows, manage infrastructure, and ensure reliable deployments.",
|
||||||
|
skills: [
|
||||||
|
{ name: "Docker", description: "Containerized the 'Digital Resume', 'Gitea Workers', and 20+ 'self-hosted' services." },
|
||||||
|
{ name: "CircleCI", description: "Managed CI/CD pipelines and deployment workflows at 'Nutrien'." },
|
||||||
|
{ name: "Git (CLI)", description: "Version control for all projects; self-hosted via 'Gitea' in 'self-hosted' services." },
|
||||||
|
{ name: "Postman", description: "API platform for building and testing APIs. Used regularly for testing backends" },
|
||||||
|
{ name: "AWS CDK", description: "Defined cloud infrastructure as code for enterprise apps at 'Nutrien'." },
|
||||||
|
{ name: "Linux CLI", description: "Daily driver for self-hosted services system administration and CI/CD scripting." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "AWS Technologies",
|
||||||
|
description: "Cloud services I extensively utilized during my internship at Nutrien to build serverless and scalable solutions.",
|
||||||
|
skills: [
|
||||||
|
{ name: "S3", description: "Utilized for scalable object storage and artifact management." },
|
||||||
|
{ name: "Cloudfront", description: "Configured content delivery for refined user experience." },
|
||||||
|
{ name: "Route 53", description: "Managed DNS records for cloud-hosted applications." },
|
||||||
|
{ name: "API Gateway", description: "Secured and published APIs for serverless backends." },
|
||||||
|
{ name: "Lambda", description: "Executed serverless logic for event-driven architectures." },
|
||||||
|
{ name: "RDS", description: "Provisioned managed relational databases for cloud applications." },
|
||||||
|
{ name: "VPC", description: "Network isolation and security configuration for cloud resources." },
|
||||||
|
{ name: "ECS", description: "Orchestrated containerized services in the cloud environment." },
|
||||||
|
{ name: "SQS", description: "Implemented message queues to decouple microservices." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Concepts",
|
||||||
|
description: "Fundamental software engineering principles and methodologies that guide my architectural decisions and development process.",
|
||||||
|
skills: [
|
||||||
|
{ name: "RESTful APIs", description: "Designed and consumed APIs for 'Digital Resume', 'Nutrien', and the University of Saskatchewan class 'Mobile & Cloud Computing'." },
|
||||||
|
{ name: "Agile/Scrum", description: "Participated in iterative delivery and sprint planning at 'Nutrien'." },
|
||||||
|
{ name: "MVC Architecture", description: "Applied design patterns in University of Saskatchewan classes 'Mobile & Cloud Computing' and 'Full Stack Web Programming'." },
|
||||||
|
{ name: "Cloud Deployment", description: "Implemented strategies for 'Nutrien', self-hosted services, and the University of Saskatchewan class 'Mobile & Cloud Computing'." },
|
||||||
|
{ name: "Responsive Design", description: "Ensured mobile-compatibility for 'Digital Resume', 'Allan's Landscaping', and the University of Saskatchewan class 'Graphical User Interfaces'." }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
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 >
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
const WORK_EXPERIENCES = [
|
const WORK_EXPERIENCES = [
|
||||||
{
|
{
|
||||||
@@ -10,7 +11,7 @@ const WORK_EXPERIENCES = [
|
|||||||
"Provide excellent customer service by explaining repair processes, timelines, and care instructions.",
|
"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."
|
"Collaborate with team members to coordinate and work efficiently, delivering the best possible service."
|
||||||
],
|
],
|
||||||
image: "towerglass.jpg",
|
image: "/images/work/tgPic.jpg",
|
||||||
highlighted: false
|
highlighted: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -22,7 +23,7 @@ const WORK_EXPERIENCES = [
|
|||||||
"Diagnosing technical issues across multiple devices, including routers, tablets, laptops and desktops on both a hardware and software level.",
|
"Diagnosing technical issues across multiple devices, including routers, tablets, laptops and desktops on both a hardware and software level.",
|
||||||
"Working with all members of the business to address technical support questions for various software and challenges."
|
"Working with all members of the business to address technical support questions for various software and challenges."
|
||||||
],
|
],
|
||||||
image: "allanslandscaping.jpeg",
|
image: "/images/work/allanslandscaping.jpeg",
|
||||||
highlighted: true
|
highlighted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -35,7 +36,7 @@ const WORK_EXPERIENCES = [
|
|||||||
"Wrote custom AWS CDK constructs to streamline cloud deployment and updating of constructs through an internal NPM library.",
|
"Wrote custom AWS CDK constructs to streamline cloud deployment and updating of constructs through an internal NPM library.",
|
||||||
"Helped to centralize and streamline our deployment pipeline across hundreds of different repositories, leading to cleaner PR's, submitted code and making updates across repositories more efficient."
|
"Helped to centralize and streamline our deployment pipeline across hundreds of different repositories, leading to cleaner PR's, submitted code and making updates across repositories more efficient."
|
||||||
],
|
],
|
||||||
image: "nutrien.png",
|
image: "/images/work/nutrien.png",
|
||||||
highlighted: true
|
highlighted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -47,7 +48,7 @@ const WORK_EXPERIENCES = [
|
|||||||
"Leveraged Python and OpenCV to control the simulator, ensuring accurate performance evaluations.",
|
"Leveraged Python and OpenCV to control the simulator, ensuring accurate performance evaluations.",
|
||||||
"Utilized C# to integrate controls into the testing framework, streamlining the automation process."
|
"Utilized C# to integrate controls into the testing framework, streamlining the automation process."
|
||||||
],
|
],
|
||||||
image: "metaImage.png",
|
image: "/images/work/cnh.png",
|
||||||
highlighted: true
|
highlighted: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -59,7 +60,7 @@ const WORK_EXPERIENCES = [
|
|||||||
"Obtained reach truck certification to efficiently handle inventory and assist customers with large orders.",
|
"Obtained reach truck certification to efficiently handle inventory and assist customers with large orders.",
|
||||||
"Maintained a safe and organized work environment in a high-traffic retail setting."
|
"Maintained a safe and organized work environment in a high-traffic retail setting."
|
||||||
],
|
],
|
||||||
image: "homedepot.png",
|
image: "/images/work/homedepot.png",
|
||||||
highlighted: false
|
highlighted: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,7 +71,7 @@ const WORK_EXPERIENCES = [
|
|||||||
"Assisted in high-volume automotive glass installations, developing fundamental skills in tool handling and glass preparation.",
|
"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.",
|
"Performed quality control checks, ensuring rigorous safety standards were met for every client.",
|
||||||
],
|
],
|
||||||
image: "towerglass.jpg",
|
image: "/images/work/tgPic.jpg",
|
||||||
highlighted: false
|
highlighted: false
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -138,48 +139,54 @@ export default function WorkExperience() {
|
|||||||
}}>All Jobs</span>
|
}}>All Jobs</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredExperiences.map((experience, index) => (
|
<AnimatePresence mode="popLayout">
|
||||||
<div key={`${experience.company}-${experience.period}`} style={{
|
{filteredExperiences.map((experience, index) => (
|
||||||
marginBottom: "30px",
|
<motion.div
|
||||||
padding: "20px",
|
layout
|
||||||
backgroundColor: "rgba(152, 116, 116, 0.1)",
|
key={`${experience.company}-${experience.period}`}
|
||||||
borderRadius: "10px",
|
initial={{ opacity: 0, x: -20 }}
|
||||||
border: "1px solid rgba(255, 255, 255, 0.2)",
|
animate={{ opacity: 1, x: 0 }}
|
||||||
display: "flex",
|
exit={{ opacity: 0, x: -20 }}
|
||||||
gap: "20px",
|
transition={{ duration: 0.3 }}
|
||||||
alignItems: "flex-start",
|
style={{
|
||||||
animation: "slideInFromLeft 0.6s ease-out forwards",
|
marginBottom: "30px",
|
||||||
// Reset key when filtering to re-trigger animation cleanly, or just use index if that's preferred strictly for list stability (though re-animating feels nicer for a filter change usually, let's keep it simple with unique key first)
|
padding: "20px",
|
||||||
animationDelay: `${index * 0.2}s`, // Faster stagger for filtered list
|
backgroundColor: "rgba(152, 116, 116, 0.1)",
|
||||||
opacity: 0
|
borderRadius: "10px",
|
||||||
}}>
|
border: "1px solid rgba(255, 255, 255, 0.2)",
|
||||||
{experience.image && (
|
display: "flex",
|
||||||
<img
|
gap: "20px",
|
||||||
src={experience.image}
|
alignItems: "flex-start",
|
||||||
alt={experience.company}
|
}}
|
||||||
style={{
|
>
|
||||||
width: "120px",
|
{experience.image && (
|
||||||
height: "120px",
|
<img
|
||||||
objectFit: "cover",
|
src={experience.image}
|
||||||
borderRadius: "8px",
|
alt={experience.company}
|
||||||
flexShrink: 0
|
style={{
|
||||||
}}
|
width: "120px",
|
||||||
/>
|
height: "120px",
|
||||||
)}
|
objectFit: "cover",
|
||||||
<div style={{ flex: 1 }}>
|
borderRadius: "8px",
|
||||||
<h2 style={{ margin: "0 0 5px 0", fontSize: "clamp(16px, 2vw, 24px)" }}>{experience.company}</h2>
|
flexShrink: 0
|
||||||
<p style={{ margin: "0 0 5px 0", fontSize: "clamp(12px, 1.5vw, 18px)", fontStyle: "italic", opacity: 0.9 }}>{experience.position}</p>
|
}}
|
||||||
<p style={{ margin: "0 0 15px 0", fontSize: "clamp(11px, 1.2vw, 16px)", opacity: 0.8 }}>{experience.period}</p>
|
/>
|
||||||
<ul style={{ margin: 0, paddingLeft: "20px" }}>
|
)}
|
||||||
{experience.description.map((item, idx) => (
|
<div style={{ flex: 1 }}>
|
||||||
<li key={idx} style={{ fontSize: "clamp(10px, 1.2vw, 16px)", marginBottom: "8px", opacity: 0.9 }}>
|
<h2 style={{ margin: "0 0 5px 0", fontSize: "clamp(16px, 2vw, 24px)" }}>{experience.company}</h2>
|
||||||
{item}
|
<p style={{ margin: "0 0 5px 0", fontSize: "clamp(12px, 1.5vw, 18px)", fontStyle: "italic", opacity: 0.9 }}>{experience.position}</p>
|
||||||
</li>
|
<p style={{ margin: "0 0 15px 0", fontSize: "clamp(11px, 1.2vw, 16px)", opacity: 0.8 }}>{experience.period}</p>
|
||||||
))}
|
<ul style={{ margin: 0, paddingLeft: "20px" }}>
|
||||||
</ul>
|
{experience.description.map((item, idx) => (
|
||||||
</div>
|
<li key={idx} style={{ fontSize: "clamp(10px, 1.2vw, 16px)", marginBottom: "8px", opacity: 0.9 }}>
|
||||||
</div>
|
{item}
|
||||||
))}
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
109
src/styles/base.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/styles/components/BentoGrid.css
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
.bento-grid-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
grid-auto-rows: 250px;
|
||||||
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-item {
|
||||||
|
position: relative;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-item:hover {
|
||||||
|
transform: translateY(-5px) scale(1.02);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
||||||
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background Image styling */
|
||||||
|
.bento-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.6s ease;
|
||||||
|
z-index: 1;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-item:hover .bento-bg {
|
||||||
|
transform: scale(1.1);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overlay gradient for readability */
|
||||||
|
.bento-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.4) 50%, rgba(0, 0, 0, 0.1) 100%);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
|
padding: 25px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-description {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specific Item Spans */
|
||||||
|
.bento-large {
|
||||||
|
grid-column: span 2;
|
||||||
|
grid-row: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-wide {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-tall {
|
||||||
|
grid-row: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.bento-grid-container {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-auto-rows: 250px;
|
||||||
|
/* Use auto-rows to handle flexible height */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-large {
|
||||||
|
grid-column: span 2;
|
||||||
|
grid-row: span 1;
|
||||||
|
/* Reset to standard height on tablet if needed, or keep tall */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-tall {
|
||||||
|
grid-row: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.bento-grid-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-item {
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bento-large,
|
||||||
|
.bento-wide,
|
||||||
|
.bento-tall {
|
||||||
|
grid-column: span 1;
|
||||||
|
grid-row: span 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
182
src/styles/components/cards.css
Normal 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);
|
||||||
|
}
|
||||||
48
src/styles/components/codeBlock.css
Normal 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;
|
||||||
|
}
|
||||||
169
src/styles/components/header.css
Normal 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
@@ -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';
|
||||||
106
src/styles/pages/about.css
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
/* 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-content.reversed {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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,
|
||||||
|
.about-content.reversed {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-image {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-text-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
|
||||||
|
.about-content,
|
||||||
|
.about-content.reversed {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-title {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
131
src/styles/pages/home.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/styles/pages/projects.css
Normal 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%;
|
||||||
|
}
|
||||||
131
src/styles/pages/sidequestDetail.css
Normal 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;
|
||||||
|
}
|
||||||