Commit Basics

This commit is contained in:
2025-12-30 16:57:21 -06:00
parent 23589efb3e
commit 207df341f1
19 changed files with 1953 additions and 31 deletions

4
.gitignore vendored
View File

@@ -21,3 +21,7 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.env
#public facing Images
/public

206
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "digital-resume-frontend",
"version": "0.1.0",
"dependencies": {
"@react-google-maps/api": "^2.20.8",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
@@ -16,8 +17,10 @@
"@types/node": "^16.18.126",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"framer-motion": "^12.23.12",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.10.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
@@ -2461,6 +2464,22 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@googlemaps/js-api-loader": {
"version": "1.16.8",
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.8.tgz",
"integrity": "sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==",
"license": "Apache-2.0"
},
"node_modules/@googlemaps/markerclusterer": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz",
"integrity": "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==",
"license": "Apache-2.0",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"supercluster": "^8.0.1"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -3080,6 +3099,36 @@
}
}
},
"node_modules/@react-google-maps/api": {
"version": "2.20.8",
"resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.20.8.tgz",
"integrity": "sha512-wtLYFtCGXK3qbIz1H5to3JxbosPnKsvjDKhqGylXUb859EskhzR7OpuNt0LqdLarXUtZCJTKzPn3BNaekNIahg==",
"license": "MIT",
"dependencies": {
"@googlemaps/js-api-loader": "1.16.8",
"@googlemaps/markerclusterer": "2.5.3",
"@react-google-maps/infobox": "2.20.0",
"@react-google-maps/marker-clusterer": "2.20.0",
"@types/google.maps": "3.58.1",
"invariant": "2.2.4"
},
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19",
"react-dom": "^16.8 || ^17 || ^18 || ^19"
}
},
"node_modules/@react-google-maps/infobox": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-2.20.0.tgz",
"integrity": "sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ==",
"license": "MIT"
},
"node_modules/@react-google-maps/marker-clusterer": {
"version": "2.20.0",
"resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.20.0.tgz",
"integrity": "sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw==",
"license": "MIT"
},
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3447,15 +3496,6 @@
"node": ">=18"
}
},
"node_modules/@testing-library/dom/node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"license": "Apache-2.0",
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz",
@@ -3689,6 +3729,12 @@
"@types/send": "*"
}
},
"node_modules/@types/google.maps": {
"version": "3.58.1",
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz",
"integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==",
"license": "MIT"
},
"node_modules/@types/graceful-fs": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
@@ -4624,12 +4670,12 @@
}
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/array-buffer-byte-length": {
@@ -7458,6 +7504,15 @@
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
}
},
"node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eslint-plugin-react": {
"version": "7.37.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
@@ -8399,6 +8454,33 @@
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/framer-motion": {
"version": "12.23.12",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz",
"integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==",
"license": "MIT",
"dependencies": {
"motion-dom": "^12.23.12",
"motion-utils": "^12.23.6",
"tslib": "^2.4.0"
},
"peerDependencies": {
"@emotion/is-prop-valid": "*",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@emotion/is-prop-valid": {
"optional": true
},
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
}
},
"node_modules/fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -9285,6 +9367,15 @@
"node": ">= 0.4"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
@@ -11045,6 +11136,12 @@
"node": ">=4.0"
}
},
"node_modules/kdbush": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
"license": "ISC"
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -11499,6 +11596,21 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/motion-dom": {
"version": "12.23.12",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz",
"integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==",
"license": "MIT",
"dependencies": {
"motion-utils": "^12.23.6"
}
},
"node_modules/motion-utils": {
"version": "12.23.6",
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -13947,6 +14059,57 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.0.tgz",
"integrity": "sha512-FVyCOH4IZ0eDDRycODfUqoN8ZSR2LbTvtx6RPsBgzvJ8xAXlMZNCrOFpu+jb8QbtZnpAd/cEki2pwE848pNGxw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.0.tgz",
"integrity": "sha512-Q4haR150pN/5N75O30iIsRJcr3ef7p7opFaKpcaREy0GQit6uCRu1NEiIFIwnHJQy0bsziRFBweR/5EkmHgVUQ==",
"license": "MIT",
"dependencies": {
"react-router": "7.10.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-router/node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -14853,6 +15016,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -15678,6 +15847,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/supercluster": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
"license": "ISC",
"dependencies": {
"kdbush": "^4.0.2"
}
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",

View File

@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@react-google-maps/api": "^2.20.8",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
@@ -11,8 +12,10 @@
"@types/node": "^16.18.126",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"framer-motion": "^12.23.12",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.10.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"

View File

@@ -3,11 +3,12 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
content="Digital portfolio and resume of Sasha Bayda, a passionate web developer."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
@@ -24,7 +25,7 @@
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`.
-->
<title>React App</title>
<title>Sasha Bayda | Digital Resume</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "Sasha Bayda",
"name": "Sasha Bayda Digital Resume",
"icons": [
{
"src": "favicon.ico",
@@ -22,4 +22,4 @@
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
}

View File

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

View File

@@ -1,16 +1,52 @@
import React from 'react';
import logo from './logo.svg';
import React, { useEffect, useRef } from 'react';
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
import './App.css';
import FloatingHeader from './components/floatingHeader';
import ParticlesBackground from './components/ParticlesBackground';
import Home from './pages/Home';
import About from './pages/About';
import WorkExperience from './pages/WorkExperience';
import Projects from './pages/Projects';
import Contact from './pages/Contact';
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
const backgroundDiv = document.querySelector('.background');
if (backgroundDiv) {
backgroundDiv.scrollTop = 0;
}
}, [pathname]);
return null;
}
function App() {
return (
<div>
<header>
<p>
Hello World!
</p>
</header>
</div>
<BrowserRouter>
<ScrollToTop />
<ParticlesBackground />
<div className='background'>
<div className='verticalContentItem'>
<FloatingHeader></FloatingHeader>
<div>
<div style={{ height: "65px" }}></div>
</div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/work-experience" element={<WorkExperience />} />
<Route path="/projects" element={<Projects />} />
<Route path="/contact" element={<Contact />} />
</Routes>
<div>
<div style={{ height: "20px" }}></div>
</div>
</div>
</div>
</BrowserRouter>
);
}

View File

@@ -0,0 +1,188 @@
import React, { useEffect, useRef } from 'react';
const ParticlesBackground: React.FC = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
let animationFrameId: number;
let particles: Particle[] = [];
class Particle {
x: number;
y: number;
size: number;
speedX: number;
speedY: number;
color: string;
constructor() {
this.x = Math.random() * (canvas?.width || window.innerWidth);
this.y = Math.random() * (canvas?.height || window.innerHeight);
this.size = Math.random() * 4 + 2;
this.speedX = Math.random() * 1 - 0.5;
this.speedY = Math.random() * 1 - 0.5;
this.color = `rgba(255, 255, 255, ${Math.random() * 0.3 + 0.1})`;
}
update() {
// update position
this.x += this.speedX;
this.y += this.speedY;
// update speedX
let speedXRng = Math.random();
if (speedXRng > 0.75) {
this.speedX += Math.random() * 0.1;
} else if (speedXRng < 0.25) {
this.speedX -= Math.random() * 0.1;
}
if (this.speedX > 1) {
this.speedX = 1;
} else if (this.speedX < -1) {
this.speedX = -1;
}
// update speedY
let speedYRng = Math.random();
if (speedYRng > 0.75) {
this.speedY += Math.random() * 0.1;
} else if (speedYRng < 0.25) {
this.speedY -= Math.random() * 0.1;
}
if (this.speedY > 1) {
this.speedY = 1;
} else if (this.speedY < -1) {
this.speedY = -1;
}
//size
let sizeRng = Math.random();
if (sizeRng > 0.75) {
this.size += Math.random() / 2;
} else if (sizeRng < 0.25) {
this.size -= Math.random() / 2;
}
if (this.size < 2) {
this.size = 2;
} else if (this.size > 6) {
this.size = 6;
}
if (canvas) {
if (this.x > canvas.width) this.x = 0;
if (this.x < 0) this.x = canvas.width;
if (this.y > canvas.height) this.y = 0;
if (this.y < 0) this.y = canvas.height;
}
}
draw() {
if (!ctx) return;
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
const init = () => {
if (!canvas) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Reduce particle count on mobile/portrait screens
const isPortrait = canvas.height > canvas.width;
const particleCount = isPortrait ? 90 : 180;
particles = [];
for (let i = 0; i < particleCount; i++) {
particles.push(new Particle());
}
};
const animate = () => {
if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw a subtle gradient background for the canvas itself if needed, or keep transparent
// 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) => {
particle.update();
particle.draw();
});
// Connect particles with lines if close
connect();
animationFrameId = requestAnimationFrame(animate);
};
const connect = () => {
if (!ctx) return;
for (let a = 0; a < particles.length; a++) {
for (let b = a; b < particles.length; b++) {
const dx = particles[a].x - particles[b].x;
const dy = particles[a].y - particles[b].y;
const distance = Math.sqrt(dx * dx + dy * dy);
const sizeAverage = (particles[a].size + particles[b].size) / 2;
// Scale from 100 to 300 based on size average (2 to 6)
const distanceThreshold = 100 + (sizeAverage - 2) * 50;
if (distance < distanceThreshold) {
ctx.strokeStyle = `rgba(255, 255, 255, ${(distanceThreshold * (2 / 3) * .001) - distance / 1000})`;
ctx.lineWidth = sizeAverage;
ctx.beginPath();
ctx.moveTo(particles[a].x, particles[a].y);
ctx.lineTo(particles[b].x, particles[b].y);
ctx.stroke();
}
}
}
}
init();
animate();
const handleResize = () => {
init();
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
cancelAnimationFrame(animationFrameId);
};
}, []);
return (
<canvas
ref={canvasRef}
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: -1, // Behind everything
}}
/>
);
};
export default ParticlesBackground;

View File

@@ -0,0 +1,187 @@
import React, { useMemo } from 'react';
import { GoogleMap, useJsApiLoader, Marker } from '@react-google-maps/api';
const containerStyle = {
width: '100%',
height: '400px',
borderRadius: '20px',
border: '1px solid rgba(255, 255, 255, 0.2)'
};
const defaultCenter = {
lat: 52.8564,
lng: -104.6100
};
// Coordinate lookup table
const PLACE_COORDINATES: Record<string, { lat: number; lng: number }> = {
"Melfort": { lat: 52.8564, lng: -104.6100 },
"Star City": { lat: 52.9333, lng: -104.3333 },
"Saskatoon": { lat: 52.1332, lng: -106.6700 },
"Regina": { lat: 50.4452, lng: -104.6189 },
"Winnipeg": { lat: 49.8951, lng: -97.1384 },
"Vancouver": { lat: 49.2827, lng: -123.1207 },
"Edmonton": { lat: 53.5461, lng: -113.4938 },
"Calgary": { lat: 51.0447, lng: -114.0719 },
"Kyiv": { lat: 50.4501, lng: 30.5234 },
"Torhovytsy": { lat: 50.5560, lng: 25.3989 },
"Lviv": { lat: 49.8397, lng: 24.0297 },
"Puerto Vallarta": { lat: 20.6534, lng: -105.2253 },
"Havana": { lat: 23.1136, lng: -82.3666 },
"Cancun": { lat: 21.1619, lng: -86.8515 },
"Waikiki Beach": { lat: 21.2769, lng: -157.8274 },
};
interface VisitedMapProps {
places: string[];
}
function VisitedMap({ places }: VisitedMapProps) {
const { isLoaded } = useJsApiLoader({
id: 'google-map-script',
googleMapsApiKey: process.env.REACT_APP_GOOGLE_MAPS_API_KEY || ""
});
const markers = useMemo(() => {
return places
.map(place => ({
name: place,
pos: PLACE_COORDINATES[place]
}))
.filter(item => item.pos !== undefined);
}, [places]);
const mapCenter = useMemo(() => {
if (markers.length > 0) {
return markers[0].pos;
}
return defaultCenter;
}, [markers]);
if (!isLoaded) {
return <div style={{ color: 'white', textAlign: 'center' }}>Loading Map...</div>;
}
if (!process.env.REACT_APP_GOOGLE_MAPS_API_KEY) {
return (
<div style={containerStyle}>
<div style={{ height: "100%", width: "100%", display: "flex", alignItems: "center", justifyContent: "center", background: "rgba(0,0,0,0.5)", color: "white", borderRadius: "20px" }}>
Google Maps API Key Missing
</div>
</div>
)
}
return (
<GoogleMap
mapContainerStyle={containerStyle}
center={mapCenter}
zoom={8}
options={{
disableDefaultUI: false,
zoomControl: true,
streetViewControl: false,
mapTypeControl: false,
styles: [
{
"elementType": "geometry",
"stylers": [{ "color": "#242f3e" }]
},
{
"elementType": "labels.text.stroke",
"stylers": [{ "color": "#242f3e" }]
},
{
"elementType": "labels.text.fill",
"stylers": [{ "color": "#746855" }]
},
{
"featureType": "administrative.locality",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#d59563" }]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#d59563" }]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [{ "color": "#263c3f" }]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#6b9a76" }]
},
{
"featureType": "road",
"elementType": "geometry",
"stylers": [{ "color": "#38414e" }]
},
{
"featureType": "road",
"elementType": "geometry.stroke",
"stylers": [{ "color": "#212a37" }]
},
{
"featureType": "road",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#9ca5b3" }]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [{ "color": "#746855" }]
},
{
"featureType": "road.highway",
"elementType": "geometry.stroke",
"stylers": [{ "color": "#1f2835" }]
},
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#f3d19c" }]
},
{
"featureType": "transit",
"elementType": "geometry",
"stylers": [{ "color": "#2f3948" }]
},
{
"featureType": "transit.station",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#d59563" }]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [{ "color": "#17263c" }]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [{ "color": "#515c6d" }]
},
{
"featureType": "water",
"elementType": "labels.text.stroke",
"stylers": [{ "color": "#17263c" }]
}
]
}}
>
{markers.map((marker, index) => (
<Marker
key={index}
position={marker.pos}
title={marker.name}
/>
))}
</GoogleMap>
);
}
export default React.memo(VisitedMap);

View File

@@ -0,0 +1,86 @@
import { motion } from "framer-motion";
import { useState, useEffect } from "react";
interface TypingTextProps {
text: string;
msPerChar?: number; // milliseconds per character
delayMs?: number; // milliseconds to delay before typing starts
textAlign?: "left" | "center" | "right"; // text alignment
}
export default function TypingText({ text, msPerChar: speed = 100, delayMs = 0, textAlign = "left" }: TypingTextProps) {
const [displayedText, setDisplayedText] = useState<string>("");
const [isComplete, setIsComplete] = useState(false);
useEffect(() => {
setDisplayedText("");
setIsComplete(false);
let index = 0;
// Set up initial delay timeout
const delayTimeout = setTimeout(() => {
const interval = setInterval(() => {
if (index < text.length) {
setDisplayedText(text.substring(0, index + 1));
index++;
if (index === text.length) {
setIsComplete(true);
clearInterval(interval);
}
}
}, speed);
return () => clearInterval(interval);
}, delayMs);
return () => clearTimeout(delayTimeout);
}, [text, speed, delayMs]);
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
style={{
whiteSpace: "pre-wrap",
overflowWrap: "break-word",
wordWrap: "break-word",
width: "100%",
position: "relative",
textAlign: textAlign
}}
>
{/* Transparent placeholder - reserves space without causing layout shift */}
<div style={{ color: "transparent", pointerEvents: "none" }}>
{text}
</div>
{/* Positioned absolutely over placeholder to show revealed text */}
<div style={{
position: "absolute",
top: 0,
left: textAlign === "right" ? "auto" : 0,
right: textAlign === "right" ? 0 : "auto",
width: textAlign === "center" ? "100%" : "auto",
textAlign: textAlign
}}>
{displayedText}
{/* blinking cursor - shows while typing or delaying, hides when complete */}
{!isComplete && (
<motion.span
animate={{ opacity: [0, 1, 0] }}
transition={{ repeat: Infinity, duration: 1 }}
style={{
display: "inline-block",
width: "4px",
height: "1em",
backgroundColor: "currentColor",
marginLeft: "4px",
verticalAlign: "middle"
}}
/>
)}
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,13 @@
interface ContentCardProps {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}
export default function ContentCard({ children, className = "", style }: ContentCardProps) {
return (
<div className={`contentCard ${className}`} style={style}>
{children}
</div>
);
}

View File

@@ -0,0 +1,43 @@
import { Link, useLocation } from "react-router-dom";
export default function FloatingHeader() {
const location = useLocation();
return (
<header className="floating-header">
<nav className="header-nav">
<Link
to="/"
className={`nav-link ${location.pathname === "/" ? "active" : ""}`}
>
Home
</Link>
<Link
to="/work-experience"
className={`nav-link ${location.pathname === "/work-experience" ? "active" : ""}`}
>
Work Experience
</Link>
<Link
to="/about"
className={`nav-link ${location.pathname === "/about" ? "active" : ""}`}
>
About Me
</Link>
<Link
to="/projects"
className={`nav-link ${location.pathname === "/projects" ? "active" : ""}`}
>
Projects
</Link>
<Link
to="/contact"
className={`nav-link ${location.pathname === "/contact" ? "active" : ""}`}
>
Contact
</Link>
</nav>
</header>
);
}

View File

@@ -0,0 +1,23 @@
interface FullPageImageProps {
src: string;
alt: string;
credit?: string;
isFixed?: boolean;
}
export default function FullPageImage({ src, alt, credit, isFixed = false }: FullPageImageProps) {
const containerStyle = isFixed
? {height: "100vh", overflow: "hidden", width: "100vw", position: "fixed" as const, top: 0, left: 0, zIndex: 0}
: {height: "100vh", overflow: "hidden", width: "100vw", position: "relative" as const, left: "50%", right: "50%", marginLeft: "-50vw", marginRight: "-50vw"};
return (
<div style={containerStyle}>
<img src={src} alt={alt} style={{width: "100%", height: "100%", objectFit: "cover"}} />
{credit && (
<div style={{position: "absolute", bottom: "20px", right: "20px", color: "white", fontFamily: "roboto, sans-serif", fontSize: "14px", backgroundColor: "rgba(0, 0, 0, 0.5)", padding: "8px 12px", borderRadius: "4px"}}>
Credit: {credit}
</div>
)}
</div>
);
}

View File

@@ -1,5 +1,4 @@
body {
overflow: hidden; /* Stop rubber-band on window for macos users */
background: linear-gradient(135deg, #3457f5eb, #8f50cf);
margin: 0;
overflow: hidden;
overscroll-behavior: none;
}

86
src/pages/About.tsx Normal file
View File

@@ -0,0 +1,86 @@
import { motion } from "framer-motion";
import VisitedMap from "../components/VisitedMap";
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 VISITED_CITIES = [
"Melfort",
"Star City",
"Saskatoon",
"Regina",
"Winnipeg",
"Vancouver",
"Edmonton",
"Calgary",
"Kyiv",
"Torhovytsy",
"Lviv",
"Puerto Vallarta",
"Havana",
"Cancun",
"Waikiki Beach"
];
export default function About() {
return (
<div className="mainContentBlock" style={{ minWidth: "66vw", display: "flex", justifyContent: "center" }}>
<motion.div
className="about-container"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8 }}
>
<div style={{ width: "100%", maxWidth: "1200px" }}>
<motion.h1
className="about-title"
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.6 }}
style={{ textAlign: "center" }}
>
About Me
</motion.h1>
<div className="about-content">
<motion.div
className="about-image-container"
initial={{ x: -30, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.4, duration: 0.6 }}
>
<img
src="/dapperSasha.jpg"
alt="profile"
className="about-image"
/>
</motion.div>
<motion.div
className="about-text-container"
initial={{ x: 30, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: 0.6, duration: 0.6 }}
>
<p className="about-text">
{ABOUT_TEXT}
</p>
</motion.div>
</div>
<motion.div
style={{ marginTop: "40px", width: "100%" }}
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.8, duration: 0.6 }}
>
<h2 style={{ textAlign: "center", fontSize: "1.5rem", marginBottom: "10px", color: "rgba(255,255,255,0.9)" }}>My Hot Chocolate Journey</h2>
<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
</p>
<VisitedMap places={VISITED_CITIES} />
</motion.div>
</div>
</motion.div>
</div>
);
}

75
src/pages/Contact.tsx Normal file
View File

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

84
src/pages/Home.tsx Normal file
View File

@@ -0,0 +1,84 @@
import TypingText from "../components/animatedTyping";
import FullPageImage from "../components/fullPageImage";
import { delay, motion } from "framer-motion";
// Animation and typing timing configuration
const ANIMATION_TIMINGS = {
// Animation delays (in seconds)
elementIn: 2.0, // When elements fade in
// Typing speeds (in milliseconds per character)
welcomeTextSpeed: 45,
nameSpeed: 120,
// Typing delays (in milliseconds) - time before text starts typing
welcomeTextDelay: 2000,
nameDelay: 2000,
// Test items animation (slides in from bottom)
testItemsStartDelay: 3.5, // When to start the first test item animation (in seconds)
testItemStaggerDelay: 0.2, // Delay between each test item (in seconds)
testItemAnimationDuration: 0.5, // Duration of each item's slide-in animation
};
const welcomeText = "Hello! My name is Sasha Bayda and welcome to my digital resume site! Here you will find some of my projects, skills, contact information and any information I couldn't fit into my resume. Feel free to explore and learn more about me and if something isn't answered, don't hesitate to reach out via the contact page!";
const CONTACT_LINKS = [
{ label: "Email", url: "mailto:sasha.bayda@outlook.com" },
{ label: "Phone", url: "tel:+13069217145" },
{ label: "LinkedIn", url: "https://www.linkedin.com/in/sasha-bayda/" },
{ label: "GitHub", url: "https://github.com/Bayda77" },
{ label: "Portfolio", url: "https://portfolio.sashabayda.ca" },
];
export default function Home() {
return (
<div className="mainContentBlock">
<div style={{ height: "30px" }}></div>
<div className="hero-card">
<div className="horizontalContentItem" style={{ maxHeight: "100vh", justifyContent: "center" }}>
<motion.div
className="flexContainer"
initial={{ opacity: 0 }}
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">
<TypingText text="Sasha Bayda" msPerChar={ANIMATION_TIMINGS.nameSpeed} delayMs={ANIMATION_TIMINGS.nameDelay} textAlign="center" />
</div>
<div className="horizontalContentItem skills-list">
{CONTACT_LINKS.map((link, index) => (
<motion.a
key={index}
href={link.url}
className="contact-link"
initial={{ y: 50, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{
duration: ANIMATION_TIMINGS.testItemAnimationDuration,
ease: "easeOut",
delay: ANIMATION_TIMINGS.testItemsStartDelay + (index * ANIMATION_TIMINGS.testItemStaggerDelay)
}}
>{link.label}</motion.a>
))}
</div>
</div>
</div>
</div>
);
}

126
src/pages/Projects.tsx Normal file
View File

@@ -0,0 +1,126 @@
import { motion, Variants } from "framer-motion";
import FullPageImage from "../components/fullPageImage";
interface Project {
id: number;
title: string;
description: string;
techStack: string[];
image: string; // Ensure these images exist in public/ or use placeholders
links: {
demo?: string;
repo?: string;
};
}
const PROJECTS: Project[] = [
{
id: 1,
title: "Digital Resume",
description: "A fully responsive, glassmorphic portfolio site built to showcase my skills and experience. Features animated page transitions, typing effects, and a dynamic map component.",
techStack: ["React", "TypeScript", "Framer Motion", "Vite"],
image: "/digitCode.jpg",
links: {
repo: "https://github.com/Bayda77/resume-site",
demo: "https://portfolio.sashabayda.ca"
}
},
];
const containerVariants: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15
}
}
};
const cardVariants: Variants = {
hidden: { y: 50, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: {
type: "spring",
stiffness: 100,
damping: 12
}
}
};
export default function Projects() {
return (
<div style={{ position: "relative", width: "100%", minHeight: "100vh" }}>
<div className="mainContentBlock" style={{ width: "100%", minWidth: "100vw", maxWidth: "100vw", paddingTop: "20px", paddingBottom: "10px", boxSizing: "border-box", display: "flex", justifyContent: "center", position: "relative", zIndex: 1 }}>
<div className="projects-container">
<motion.h1
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
className="projects-grid"
variants={containerVariants}
initial="hidden"
animate="visible"
>
{PROJECTS.map((project) => (
<motion.div
key={project.id}
className="project-card"
variants={cardVariants}
whileHover={{ y: -10, transition: { duration: 0.2 } }}
>
<div className="project-image-container">
<img
src={project.image}
alt={project.title}
className="project-image"
/>
</div>
<div className="project-header">
<h2 className="project-name">{project.title}</h2>
</div>
<div className="project-tech-stack">
{project.techStack.map(tech => (
<span key={tech} className="tech-chip">{tech}</span>
))}
</div>
<p className="project-description">
{project.description}
</p>
<div className="project-links">
{project.links.demo && (
<a href={project.links.demo} className="project-link" target="_blank" rel="noopener noreferrer">
Live Demo <span></span>
</a>
)}
{project.links.repo && (
<a href={project.links.repo} className="project-link" target="_blank" rel="noopener noreferrer">
GitHub <span></span>
</a>
)}
</div>
</motion.div>
))}
</motion.div>
</div>
</div>
<FullPageImage
src="/20251111_224823.jpg"
alt="projects background"
credit="Sasha Bayda"
isFixed={true}
/>
</div>
);
}

View File

@@ -0,0 +1,187 @@
import { useState } from 'react';
const WORK_EXPERIENCES = [
{
company: "Tower Glass Ltd.",
position: "Automotive Glass Installer/Salesman (Full-Time)",
period: "July 2025 to Present",
description: [
"Install, repair, and replace windshields, side, and rear automotive glass on a variety of vehicle makes and models.",
"Provide excellent customer service by explaining repair processes, timelines, and care instructions.",
"Collaborate with team members to coordinate and work efficiently, delivering the best possible service."
],
image: "towerglass.jpg",
highlighted: false
},
{
company: "Allan's Landscaping and Disposal Services Ltd.",
position: "Web Developer/Technical Support Specialist (Part-Time)",
period: "Jan. 2024 to July 2025",
description: [
"Implementing and developing WordPress plugins to redesign Allan's Landscaping Website.",
"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."
],
image: "allanslandscaping.jpeg",
highlighted: true
},
{
company: "Nutrien",
position: "Cloud DevOps Intern (Contract)",
period: "Jan. 2023 - Dec. 2023",
description: [
"Worked with AWS CDK to develop fully cloud-based applications using CircleCI deployments and React front ends.",
"Updated CircleCI build scripts to perform different workflows depending on the environment, such as dev and prod separated deployments.",
"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."
],
image: "nutrien.png",
highlighted: true
},
{
company: "CNH",
position: "Embedded Software Engineering Intern (Contract)",
period: "May 2022 - Aug. 2022",
description: [
"Developed an automated regression testing suite for tractor configurations, enhancing testing efficiency.",
"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."
],
image: "metaImage.png",
highlighted: true
},
{
company: "Home Depot",
position: "Sales Associate - Lumber (Contract)",
period: "Summer 2021",
description: [
"Worked as a sales associate in the lumber section, providing direct customer assistance.",
"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."
],
image: "homedepot.png",
highlighted: false
},
{
company: "Tower Glass Ltd.",
position: "Journeyman Glass Apprentice",
period: "Summers From 2016 to 2019",
description: [
"Assisted in high-volume automotive glass installations, developing fundamental skills in tool handling and glass preparation.",
"Performed quality control checks, ensuring rigorous safety standards were met for every client.",
],
image: "towerglass.jpg",
highlighted: false
},
];
export default function WorkExperience() {
const [showAll, setShowAll] = useState(false);
const filteredExperiences = showAll
? WORK_EXPERIENCES
: WORK_EXPERIENCES.filter(exp => exp.highlighted);
return (
<div className="mainContentBlock" style={{ minWidth: "66vw", alignItems: "center" }}>
<div style={{ height: "30px" }}></div>
<div className="verticalContentItem" style={{ justifyContent: "center", gap: "15px" }}>
<div className="contentContainer">
{/* Toggle Switch */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
marginBottom: '20px',
gap: '15px'
}}>
<span style={{
color: !showAll ? 'white' : 'rgba(255,255,255,0.6)',
fontFamily: 'roboto, sans-serif',
fontWeight: !showAll ? 'bold' : 'normal',
transition: 'all 0.3s ease',
fontSize: '16px'
}}>Software Jobs</span>
<div
onClick={() => setShowAll(!showAll)}
style={{
width: '60px',
height: '30px',
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: '15px',
position: 'relative',
cursor: 'pointer',
transition: 'background-color 0.3s ease',
border: '1px solid rgba(255,255,255,0.4)'
}}
>
<div style={{
width: '26px',
height: '26px',
backgroundColor: 'white',
borderRadius: '50%',
position: 'absolute',
top: '1px',
left: showAll ? '31px' : '1px',
transition: 'left 0.3s cubic-bezier(0.4, 0.0, 0.2, 1)',
boxShadow: '0 2px 4px rgba(0,0,0,0.2)'
}} />
</div>
<span style={{
color: showAll ? 'white' : 'rgba(255,255,255,0.6)',
fontFamily: 'roboto, sans-serif',
fontWeight: showAll ? 'bold' : 'normal',
transition: 'all 0.3s ease',
fontSize: '16px'
}}>All Jobs</span>
</div>
{filteredExperiences.map((experience, index) => (
<div key={`${experience.company}-${experience.period}`} style={{
marginBottom: "30px",
padding: "20px",
backgroundColor: "rgba(152, 116, 116, 0.1)",
borderRadius: "10px",
border: "1px solid rgba(255, 255, 255, 0.2)",
display: "flex",
gap: "20px",
alignItems: "flex-start",
animation: "slideInFromLeft 0.6s ease-out forwards",
// 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)
animationDelay: `${index * 0.2}s`, // Faster stagger for filtered list
opacity: 0
}}>
{experience.image && (
<img
src={experience.image}
alt={experience.company}
style={{
width: "120px",
height: "120px",
objectFit: "cover",
borderRadius: "8px",
flexShrink: 0
}}
/>
)}
<div style={{ flex: 1 }}>
<h2 style={{ margin: "0 0 5px 0", fontSize: "clamp(16px, 2vw, 24px)" }}>{experience.company}</h2>
<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) => (
<li key={idx} style={{ fontSize: "clamp(10px, 1.2vw, 16px)", marginBottom: "8px", opacity: 0.9 }}>
{item}
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
</div>
);
}