Commit Major Implementation now that gitea is setup
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -21,3 +21,7 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
.env
|
||||||
|
|
||||||
|
#public facing Images
|
||||||
|
/public
|
||||||
206
package-lock.json
generated
206
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "digital-resume-frontend",
|
"name": "digital-resume-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-google-maps/api": "^2.20.8",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.8.0",
|
"@testing-library/jest-dom": "^6.8.0",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
@@ -16,8 +17,10 @@
|
|||||||
"@types/node": "^16.18.126",
|
"@types/node": "^16.18.126",
|
||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
|
"framer-motion": "^12.23.12",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-router-dom": "^7.10.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
@@ -2461,6 +2464,22 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"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": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"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": {
|
"node_modules/@rollup/plugin-babel": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
|
||||||
@@ -3447,15 +3496,6 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/@testing-library/jest-dom": {
|
||||||
"version": "6.8.0",
|
"version": "6.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.8.0.tgz",
|
||||||
@@ -3689,6 +3729,12 @@
|
|||||||
"@types/send": "*"
|
"@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": {
|
"node_modules/@types/graceful-fs": {
|
||||||
"version": "4.1.9",
|
"version": "4.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||||
@@ -4624,12 +4670,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/aria-query": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"dependencies": {
|
||||||
"node": ">= 0.4"
|
"dequal": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/array-buffer-byte-length": {
|
"node_modules/array-buffer-byte-length": {
|
||||||
@@ -7458,6 +7504,15 @@
|
|||||||
"eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
|
"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": {
|
"node_modules/eslint-plugin-react": {
|
||||||
"version": "7.37.5",
|
"version": "7.37.5",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz",
|
"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"
|
"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": {
|
"node_modules/fresh": {
|
||||||
"version": "0.5.2",
|
"version": "0.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
@@ -9285,6 +9367,15 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
|
||||||
@@ -11045,6 +11136,12 @@
|
|||||||
"node": ">=4.0"
|
"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": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
@@ -11499,6 +11596,21 @@
|
|||||||
"mkdirp": "bin/cmd.js"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -13947,6 +14059,57 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-scripts": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
||||||
@@ -14853,6 +15016,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"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"
|
"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": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-google-maps/api": "^2.20.8",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.8.0",
|
"@testing-library/jest-dom": "^6.8.0",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
@@ -11,8 +12,10 @@
|
|||||||
"@types/node": "^16.18.126",
|
"@types/node": "^16.18.126",
|
||||||
"@types/react": "^19.1.12",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.9",
|
"@types/react-dom": "^19.1.9",
|
||||||
|
"framer-motion": "^12.23.12",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
|
"react-router-dom": "^7.10.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
|
|||||||
@@ -3,11 +3,12 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<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="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
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" />
|
<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.
|
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>React App</title>
|
<title>Sasha Bayda | Digital Resume</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "Sasha Bayda",
|
||||||
"name": "Create React App Sample",
|
"name": "Sasha Bayda Digital Resume",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
@@ -22,4 +22,4 @@
|
|||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
"background_color": "#ffffff"
|
"background_color": "#ffffff"
|
||||||
}
|
}
|
||||||
603
src/App.css
603
src/App.css
@@ -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;
|
||||||
|
}
|
||||||
54
src/App.tsx
54
src/App.tsx
@@ -1,16 +1,52 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import logo from './logo.svg';
|
import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom';
|
||||||
import './App.css';
|
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() {
|
function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<BrowserRouter>
|
||||||
<header>
|
<ScrollToTop />
|
||||||
<p>
|
<ParticlesBackground />
|
||||||
Hello World!
|
<div className='background'>
|
||||||
</p>
|
<div className='verticalContentItem'>
|
||||||
</header>
|
<FloatingHeader></FloatingHeader>
|
||||||
</div>
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
188
src/components/ParticlesBackground.tsx
Normal file
188
src/components/ParticlesBackground.tsx
Normal 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;
|
||||||
187
src/components/VisitedMap.tsx
Normal file
187
src/components/VisitedMap.tsx
Normal 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);
|
||||||
86
src/components/animatedTyping.tsx
Normal file
86
src/components/animatedTyping.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/components/contentCard.tsx
Normal file
13
src/components/contentCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/components/floatingHeader.tsx
Normal file
43
src/components/floatingHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
23
src/components/fullPageImage.tsx
Normal file
23
src/components/fullPageImage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
body {
|
body {
|
||||||
overflow: hidden; /* Stop rubber-band on window for macos users */
|
overflow: hidden;
|
||||||
background: linear-gradient(135deg, #3457f5eb, #8f50cf);
|
overscroll-behavior: none;
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
86
src/pages/About.tsx
Normal file
86
src/pages/About.tsx
Normal 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
75
src/pages/Contact.tsx
Normal 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
84
src/pages/Home.tsx
Normal 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
126
src/pages/Projects.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
187
src/pages/WorkExperience.tsx
Normal file
187
src/pages/WorkExperience.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user