├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html ├── jo.svg ├── manifest.json ├── robots.txt ├── sitemap.xml └── social-image.png ├── src ├── app │ ├── App.js │ └── HelmetMeta.js ├── components │ ├── background │ │ ├── DisplacementSphere.css │ │ ├── DisplacementSphere.js │ │ ├── sphereFragShader.js │ │ └── sphereVertShader.js │ ├── content │ │ ├── Content.js │ │ ├── SocialIcons.js │ │ ├── SponsorButton.js │ │ ├── TextDecrypt.js │ │ └── Today.js │ ├── footer │ │ └── FooterText.js │ ├── logo │ │ ├── Logo.js │ │ └── LogoLink.js │ ├── speedDial │ │ └── SpeedDial.js │ └── theme │ │ ├── ThemeProvider.js │ │ ├── ThemeToggle.js │ │ └── Themes.js ├── hooks │ ├── useInViewport.js │ └── usePrefersReducedMotion.js ├── index.css ├── index.js ├── pages │ ├── Home.js │ ├── PageNotFound.js │ └── Resume.js ├── settings │ ├── resume.json │ └── settings.json └── utils │ ├── getName.js │ ├── logCredits.js │ ├── style.js │ ├── three.js │ └── transition.js └── vercel.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: JoHoop 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env 26 | 27 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 JoHoop 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Personal Website React 2 | 3 | [![Screenshot](/public/social-image.png?raw=true)](https://jolienhoop.com) 4 | 5 | [![Website jolienhoop.com](https://img.shields.io/website-up-down-green-red/http/shields.io.svg)](https://jolienhoop.com) 6 | [![GitHub license](https://img.shields.io/github/license/Naereen/StrapDown.js.svg)](https://github.com/JoHoop/personal-website-react/blob/master/LICENSE) 7 | [![GitHub contributors](https://img.shields.io/github/contributors/JoHoop/personal-website-react.svg)](https://github.com/JoHoop/personal-website-react/graphs/contributors/) 8 | [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/JoHoop/personal-website-react/graphs/commit-activity) 9 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com) 10 | 11 | A clean, responsive, single-page webapp template for developers. View demo at [jolienhoop.com](https://jolienhoop.com) 12 | 13 | - built using [React](https://reactjs.org/) 14 | - bootstrapped with [Create React App](https://github.com/facebook/create-react-app) 15 | - styled with [Material-UI](https://material-ui.com/) 16 | - deployed with [Vercel](https://vercel.com) and hosted at [jolienhoop.com](https://jolienhoop.com) 17 | 18 | Kudos to [Cody Bennett](https://github.com/CodyJasonBennett), [José Coelho](https://github.com/jcoelho93) and [Brittany Chiang](https://github.com/bchiang7) for the inspiration. 19 | 20 | ## Features 21 | 22 | - All of the personal information is populated from the resume.json file following the [JSON Resume](https://jsonresume.org/) standard, a community driven open source initiative to create a JSON based standard for resumes. Discover the official schema [here](https://jsonresume.org/schema/). 23 | - The toggle/switch for the dark mode syncs its state to the local storage. 24 | 25 | #### Coming soon 26 | 27 | - Two beautiful resume page templates generated based on the data in the resume.json file -- a modern approach of the traditional printed CV. 28 | - Rich Google search results using structured data with [json-ld.org/](https://json-ld.org/). 29 | 30 | ## Customization 31 | 32 | Feel free to fork this project and customize it with your own information and style. 33 | 34 | Refer to the [Material UI docs](https://material-ui.com/customization/theming/) for guidance on how to quickly customize the themes, components and colors to suit your tastes. 35 | 36 | If you improve the app in any way a pull request would be very much appreciated ✌️ 37 | 38 | ## Available Scripts 39 | 40 | In the project directory, you can run: 41 | 42 | ### `npm install` 43 | 44 | to install the dependencies. 45 | 46 | ### `npm start` 47 | 48 | to run the app in the development mode at [http://localhost:3000](http://localhost:3000)
49 | 50 | The page will reload if you make edits.
51 | You will also see any lint errors in the console. 52 | 53 | ### `npm run build` 54 | 55 | Builds the app for production to the `build` folder.
56 | It correctly bundles React in production mode and optimizes the build for the best performance. 57 | 58 | The build is minified and the filenames include the hashes.
59 | Your app is ready to be deployed! 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "personal-website-react", 3 | "version": "0.2.0", 4 | "homepage": "https://jolienhoop.com", 5 | "description": "This is a clean customizable portfolio template for developers.", 6 | "repository": "https://github.com/JoHoop/personal-website-react", 7 | "author": "Jo Lienhoop ", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@material-ui/core": "^4.11.0", 11 | "@material-ui/icons": "^4.9.1", 12 | "@material-ui/lab": "^4.0.0-alpha.56", 13 | "@testing-library/jest-dom": "^4.2.4", 14 | "@testing-library/react": "^9.3.2", 15 | "@testing-library/user-event": "^7.1.2", 16 | "classnames": "^2.2.6", 17 | "ios-inner-height": "^1.1.1", 18 | "popmotion": "^8.7.3", 19 | "react": "^16.13.1", 20 | "react-dom": "^16.13.1", 21 | "react-helmet": "^6.1.0", 22 | "react-router-dom": "^5.2.0", 23 | "react-scripts": "^5.0.1", 24 | "react-snapshot": "^1.3.0", 25 | "react-transition-group": "^4.4.1", 26 | "react-typical": "^0.1.3", 27 | "three": "^0.125.0", 28 | "use-dencrypt-effect": "1.0.1" 29 | }, 30 | "scripts": { 31 | "start": "react-scripts start", 32 | "build": "react-scripts build && react-snapshot", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject" 35 | }, 36 | "reactSnapshot": { 37 | "exclude": [ 38 | "/resume" 39 | ], 40 | "snapshotDelay": 1000 41 | }, 42 | "eslintConfig": { 43 | "extends": "react-app" 44 | }, 45 | "browserslist": { 46 | "production": [ 47 | ">0.2%", 48 | "not dead", 49 | "not op_mini all" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoHoop/personal-website-react/afdb269aa2121d39d477d58ec0704b4ec0ff13b4/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoHoop/personal-website-react/afdb269aa2121d39d477d58ec0704b4ec0ff13b4/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoHoop/personal-website-react/afdb269aa2121d39d477d58ec0704b4ec0ff13b4/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoHoop/personal-website-react/afdb269aa2121d39d477d58ec0704b4ec0ff13b4/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoHoop/personal-website-react/afdb269aa2121d39d477d58ec0704b4ec0ff13b4/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoHoop/personal-website-react/afdb269aa2121d39d477d58ec0704b4ec0ff13b4/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 14 | 20 | 26 | 27 | 28 | 29 | Jo Lienhoop | Bremen, Germany 30 | 40 | 41 | 45 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 61 | 65 | 69 | 73 | 77 | 78 | 79 | 80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /public/jo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Jo Lienhoop | Bremen, Germany", 3 | "short_name": "jolienhoop.com", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#00bfbf", 19 | "background_color": "#171c28" 20 | } 21 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | Sitemap: https://jolienhoop.com/sitemap.xml 2 | User-agent: * 3 | Disallow: /test/ 4 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://jolienhoop.com 5 | 2021-03-01 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/social-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoHoop/personal-website-react/afdb269aa2121d39d477d58ec0704b4ec0ff13b4/public/social-image.png -------------------------------------------------------------------------------- /src/app/App.js: -------------------------------------------------------------------------------- 1 | import React, { lazy } from "react"; 2 | 3 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 4 | import { HelmetMeta } from "./HelmetMeta"; 5 | import { ThemeProvider } from "../components/theme/ThemeProvider"; 6 | import { CssBaseline } from "@material-ui/core"; 7 | import { logCredits } from "../utils/logCredits"; 8 | 9 | import { Home } from "../pages/Home"; 10 | 11 | const Resume = lazy(() => import("../pages/Resume")); 12 | const PageNotFound = lazy(() => import("../pages/PageNotFound")); 13 | 14 | export const App = () => { 15 | logCredits(); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/app/HelmetMeta.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Helmet from "react-helmet"; 3 | import Resume from "../settings/resume.json"; 4 | import Settings from "../settings/settings.json"; 5 | 6 | export const HelmetMeta = () => { 7 | return ( 8 | 9 | 10 | {Resume.basics.name} | {Resume.basics.location.city}, {Resume.basics.location.country} 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/background/DisplacementSphere.css: -------------------------------------------------------------------------------- 1 | .displacement-sphere { 2 | position: fixed; 3 | width: 100vw; 4 | top: 0; 5 | right: 0; 6 | bottom: 0; 7 | left: 0; 8 | opacity: 0; 9 | z-index: -1; 10 | transition-property: opacity; 11 | transition-duration: 3s; 12 | transition-timing-function: "cubic-bezier(0.4, 0.0, 0.2, 1)"; 13 | } 14 | 15 | .displacement-sphere--entering, 16 | .displacement-sphere--entered { 17 | opacity: 1; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/background/DisplacementSphere.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useContext } from "react"; 2 | import classNames from "classnames"; 3 | import { 4 | Vector2, 5 | sRGBEncoding, 6 | WebGLRenderer, 7 | PerspectiveCamera, 8 | Scene, 9 | DirectionalLight, 10 | AmbientLight, 11 | UniformsUtils, 12 | UniformsLib, 13 | MeshPhongMaterial, 14 | SphereBufferGeometry, 15 | Mesh, 16 | } from "three"; 17 | import { spring, value } from "popmotion"; 18 | import innerHeight from "ios-inner-height"; 19 | import vertShader from "./sphereVertShader"; 20 | import fragShader from "./sphereFragShader"; 21 | import { Transition } from "react-transition-group"; 22 | import { usePrefersReducedMotion } from "../../hooks/usePrefersReducedMotion"; 23 | import { useInViewport } from "../../hooks/useInViewport"; 24 | import { reflow } from "../../utils/transition"; 25 | import { media, rgbToThreeColor } from "../../utils/style"; 26 | import { cleanScene, removeLights, cleanRenderer } from "../../utils/three"; 27 | import "./DisplacementSphere.css"; 28 | import { ThemeContext } from "../theme/ThemeProvider"; 29 | 30 | const DisplacementSphere = (props) => { 31 | const { theme } = useContext(ThemeContext); 32 | const rgbBackground = theme === "light" ? "250 250 250" : "17 17 17"; 33 | const width = useRef(window.innerWidth); 34 | const height = useRef(window.innerHeight); 35 | const start = useRef(Date.now()); 36 | const canvasRef = useRef(); 37 | const mouse = useRef(); 38 | const renderer = useRef(); 39 | const camera = useRef(); 40 | const scene = useRef(); 41 | const lights = useRef(); 42 | const uniforms = useRef(); 43 | const material = useRef(); 44 | const geometry = useRef(); 45 | const sphere = useRef(); 46 | const tweenRef = useRef(); 47 | const sphereSpring = useRef(); 48 | const prefersReducedMotion = Boolean(usePrefersReducedMotion() && false); //disabled until switching themes fixed 49 | const isInViewport = useInViewport(canvasRef); 50 | 51 | useEffect(() => { 52 | mouse.current = new Vector2(0.8, 0.5); 53 | renderer.current = new WebGLRenderer({ 54 | canvas: canvasRef.current, 55 | powerPreference: "high-performance", 56 | }); 57 | renderer.current.setSize(width.current, height.current); 58 | renderer.current.setPixelRatio(1); 59 | renderer.current.outputEncoding = sRGBEncoding; 60 | 61 | camera.current = new PerspectiveCamera( 62 | 55, 63 | width.current / height.current, 64 | 0.1, 65 | 200 66 | ); 67 | camera.current.position.z = 52; 68 | 69 | scene.current = new Scene(); 70 | 71 | material.current = new MeshPhongMaterial(); 72 | material.current.onBeforeCompile = (shader) => { 73 | uniforms.current = UniformsUtils.merge([ 74 | UniformsLib["ambient"], 75 | UniformsLib["lights"], 76 | shader.uniforms, 77 | { time: { type: "f", value: 0 } }, 78 | ]); 79 | 80 | shader.uniforms = uniforms.current; 81 | shader.vertexShader = vertShader; 82 | shader.fragmentShader = fragShader; 83 | shader.lights = true; 84 | }; 85 | 86 | geometry.current = new SphereBufferGeometry(32, 128, 128); 87 | 88 | sphere.current = new Mesh(geometry.current, material.current); 89 | sphere.current.position.z = 0; 90 | sphere.current.modifier = Math.random(); 91 | scene.current.add(sphere.current); 92 | 93 | return () => { 94 | cleanScene(scene.current); 95 | cleanRenderer(renderer.current); 96 | }; 97 | }, []); 98 | 99 | useEffect(() => { 100 | const dirLight = new DirectionalLight( 101 | rgbToThreeColor("250 250 250"), 102 | 0.6 103 | ); 104 | const ambientLight = new AmbientLight( 105 | rgbToThreeColor("250 250 250"), 106 | theme === "light" ? 0.8 : 0.1 107 | ); 108 | 109 | dirLight.position.z = 200; 110 | dirLight.position.x = 100; 111 | dirLight.position.y = 100; 112 | 113 | lights.current = [dirLight, ambientLight]; 114 | scene.current.background = rgbToThreeColor(rgbBackground); 115 | lights.current.forEach((light) => scene.current.add(light)); 116 | 117 | return () => { 118 | removeLights(lights.current); 119 | }; 120 | }, [rgbBackground, theme]); 121 | 122 | useEffect(() => { 123 | const handleResize = () => { 124 | const canvasHeight = innerHeight(); 125 | const windowWidth = window.innerWidth; 126 | const fullHeight = canvasHeight + canvasHeight * 0.3; 127 | canvasRef.current.style.height = fullHeight; 128 | renderer.current.setSize(windowWidth, fullHeight); 129 | camera.current.aspect = windowWidth / fullHeight; 130 | camera.current.updateProjectionMatrix(); 131 | 132 | // Render a single frame on resize when not animating 133 | if (prefersReducedMotion) { 134 | renderer.current.render(scene.current, camera.current); 135 | } 136 | 137 | if (windowWidth <= media.mobile) { 138 | sphere.current.position.x = 14; 139 | sphere.current.position.y = 10; 140 | } else if (windowWidth <= media.tablet) { 141 | sphere.current.position.x = 18; 142 | sphere.current.position.y = 14; 143 | } else { 144 | sphere.current.position.x = 22; 145 | sphere.current.position.y = 16; 146 | } 147 | }; 148 | 149 | window.addEventListener("resize", handleResize); 150 | handleResize(); 151 | 152 | return () => { 153 | window.removeEventListener("resize", handleResize); 154 | }; 155 | }, [prefersReducedMotion]); 156 | 157 | useEffect(() => { 158 | const onMouseMove = (event) => { 159 | const { rotation } = sphere.current; 160 | 161 | const position = { 162 | x: event.clientX / window.innerWidth, 163 | y: event.clientY / window.innerHeight, 164 | }; 165 | 166 | if (!sphereSpring.current) { 167 | sphereSpring.current = value(rotation.toArray(), (values) => 168 | rotation.set( 169 | values[0], 170 | values[1], 171 | sphere.current.rotation.z 172 | ) 173 | ); 174 | } 175 | 176 | tweenRef.current = spring({ 177 | from: sphereSpring.current.get(), 178 | to: [position.y / 2, position.x / 2], 179 | stiffness: 30, 180 | damping: 20, 181 | velocity: sphereSpring.current.getVelocity(), 182 | mass: 2, 183 | restSpeed: 0.0001, 184 | }).start(sphereSpring.current); 185 | }; 186 | 187 | if (!prefersReducedMotion && isInViewport) { 188 | window.addEventListener("mousemove", onMouseMove); 189 | } 190 | 191 | return () => { 192 | window.removeEventListener("mousemove", onMouseMove); 193 | 194 | if (tweenRef.current) { 195 | tweenRef.current.stop(); 196 | } 197 | }; 198 | }, [isInViewport, prefersReducedMotion]); 199 | 200 | useEffect(() => { 201 | let animation; 202 | 203 | const animate = () => { 204 | animation = requestAnimationFrame(animate); 205 | 206 | if (uniforms.current !== undefined) { 207 | uniforms.current.time.value = 208 | 0.00005 * (Date.now() - start.current); 209 | } 210 | 211 | sphere.current.rotation.z += 0.001; 212 | renderer.current.render(scene.current, camera.current); 213 | }; 214 | 215 | if (!prefersReducedMotion && isInViewport) { 216 | animate(); 217 | } else { 218 | renderer.current.render(scene.current, camera.current); 219 | } 220 | 221 | return () => { 222 | cancelAnimationFrame(animation); 223 | }; 224 | }, [isInViewport, prefersReducedMotion]); 225 | 226 | return ( 227 | 228 | {(status) => ( 229 | 238 | )} 239 | 240 | ); 241 | }; 242 | 243 | export default DisplacementSphere; 244 | -------------------------------------------------------------------------------- /src/components/background/sphereFragShader.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | #define PHONG 3 | 4 | uniform vec3 diffuse; 5 | uniform vec3 emissive; 6 | uniform vec3 specular; 7 | uniform float shininess; 8 | uniform float opacity; 9 | uniform float time; 10 | varying vec2 vUv; 11 | varying vec3 newPosition; 12 | varying float noise; 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | void main() { 39 | #include 40 | 41 | vec3 color = vec3(vUv * (0.2 - 2.0 * noise), 1.0); 42 | vec3 finalColors = vec3(color.b * 1.5, color.r, color.r); 43 | vec4 diffuseColor = vec4(cos(finalColors * noise * 3.0), 1.0); 44 | ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0)); 45 | vec3 totalEmissiveRadiance = emissive; 46 | 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | 62 | vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; 63 | 64 | #include 65 | #include 66 | #include 67 | #include 68 | #include 69 | 70 | gl_FragColor = vec4(outgoingLight, diffuseColor.a); 71 | } 72 | `; 73 | -------------------------------------------------------------------------------- /src/components/background/sphereVertShader.js: -------------------------------------------------------------------------------- 1 | // 2 | // GLSL textureless classic 3D noise "cnoise", 3 | // with an RSL-style periodic variant "pnoise". 4 | // Author: Stefan Gustavson (stefan.gustavson@liu.se) 5 | // Version: 2011-10-11 6 | // 7 | // Many thanks to Ian McEwan of Ashima Arts for the 8 | // ideas for permutation and gradient selection. 9 | // 10 | // Copyright (c) 2011 Stefan Gustavson. All rights reserved. 11 | // Distributed under the MIT license. See LICENSE file. 12 | // https://github.com/ashima/webgl-noise 13 | // 14 | 15 | export default ` 16 | vec3 mod289(vec3 x) 17 | { 18 | return x - floor(x * (1.0 / 289.0)) * 289.0; 19 | } 20 | 21 | vec4 mod289(vec4 x) 22 | { 23 | return x - floor(x * (1.0 / 289.0)) * 289.0; 24 | } 25 | 26 | vec4 permute(vec4 x) 27 | { 28 | return mod289(((x*34.0)+1.0)*x); 29 | } 30 | 31 | vec4 taylorInvSqrt(vec4 r) 32 | { 33 | return 1.79284291400159 - 0.85373472095314 * r; 34 | } 35 | 36 | vec3 fade(vec3 t) { 37 | return t*t*t*(t*(t*6.0-15.0)+10.0); 38 | } 39 | 40 | // Classic Perlin noise 41 | float cnoise(vec3 P) 42 | { 43 | vec3 Pi0 = floor(P); // Integer part for indexing 44 | vec3 Pi1 = Pi0 + vec3(1.0); // Integer part + 1 45 | Pi0 = mod289(Pi0); 46 | Pi1 = mod289(Pi1); 47 | vec3 Pf0 = fract(P); // Fractional part for interpolation 48 | vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 49 | vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); 50 | vec4 iy = vec4(Pi0.yy, Pi1.yy); 51 | vec4 iz0 = Pi0.zzzz; 52 | vec4 iz1 = Pi1.zzzz; 53 | 54 | vec4 ixy = permute(permute(ix) + iy); 55 | vec4 ixy0 = permute(ixy + iz0); 56 | vec4 ixy1 = permute(ixy + iz1); 57 | 58 | vec4 gx0 = ixy0 * (1.0 / 7.0); 59 | vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; 60 | gx0 = fract(gx0); 61 | vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); 62 | vec4 sz0 = step(gz0, vec4(0.0)); 63 | gx0 -= sz0 * (step(0.0, gx0) - 0.5); 64 | gy0 -= sz0 * (step(0.0, gy0) - 0.5); 65 | 66 | vec4 gx1 = ixy1 * (1.0 / 7.0); 67 | vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; 68 | gx1 = fract(gx1); 69 | vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); 70 | vec4 sz1 = step(gz1, vec4(0.0)); 71 | gx1 -= sz1 * (step(0.0, gx1) - 0.5); 72 | gy1 -= sz1 * (step(0.0, gy1) - 0.5); 73 | 74 | vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); 75 | vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); 76 | vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); 77 | vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); 78 | vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); 79 | vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); 80 | vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); 81 | vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); 82 | 83 | vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); 84 | g000 *= norm0.x; 85 | g010 *= norm0.y; 86 | g100 *= norm0.z; 87 | g110 *= norm0.w; 88 | vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); 89 | g001 *= norm1.x; 90 | g011 *= norm1.y; 91 | g101 *= norm1.z; 92 | g111 *= norm1.w; 93 | 94 | float n000 = dot(g000, Pf0); 95 | float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); 96 | float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); 97 | float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); 98 | float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); 99 | float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); 100 | float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); 101 | float n111 = dot(g111, Pf1); 102 | 103 | vec3 fade_xyz = fade(Pf0); 104 | vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); 105 | vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); 106 | float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); 107 | return 2.2 * n_xyz; 108 | } 109 | 110 | // Classic Perlin noise, periodic variant 111 | float pnoise(vec3 P, vec3 rep) 112 | { 113 | vec3 Pi0 = mod(floor(P), rep); // Integer part, modulo period 114 | vec3 Pi1 = mod(Pi0 + vec3(1.0), rep); // Integer part + 1, mod period 115 | Pi0 = mod289(Pi0); 116 | Pi1 = mod289(Pi1); 117 | vec3 Pf0 = fract(P); // Fractional part for interpolation 118 | vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 119 | vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); 120 | vec4 iy = vec4(Pi0.yy, Pi1.yy); 121 | vec4 iz0 = Pi0.zzzz; 122 | vec4 iz1 = Pi1.zzzz; 123 | 124 | vec4 ixy = permute(permute(ix) + iy); 125 | vec4 ixy0 = permute(ixy + iz0); 126 | vec4 ixy1 = permute(ixy + iz1); 127 | 128 | vec4 gx0 = ixy0 * (1.0 / 7.0); 129 | vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; 130 | gx0 = fract(gx0); 131 | vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); 132 | vec4 sz0 = step(gz0, vec4(0.0)); 133 | gx0 -= sz0 * (step(0.0, gx0) - 0.5); 134 | gy0 -= sz0 * (step(0.0, gy0) - 0.5); 135 | 136 | vec4 gx1 = ixy1 * (1.0 / 7.0); 137 | vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; 138 | gx1 = fract(gx1); 139 | vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); 140 | vec4 sz1 = step(gz1, vec4(0.0)); 141 | gx1 -= sz1 * (step(0.0, gx1) - 0.5); 142 | gy1 -= sz1 * (step(0.0, gy1) - 0.5); 143 | 144 | vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); 145 | vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); 146 | vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); 147 | vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); 148 | vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); 149 | vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); 150 | vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); 151 | vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); 152 | 153 | vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); 154 | g000 *= norm0.x; 155 | g010 *= norm0.y; 156 | g100 *= norm0.z; 157 | g110 *= norm0.w; 158 | vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); 159 | g001 *= norm1.x; 160 | g011 *= norm1.y; 161 | g101 *= norm1.z; 162 | g111 *= norm1.w; 163 | 164 | float n000 = dot(g000, Pf0); 165 | float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); 166 | float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); 167 | float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); 168 | float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); 169 | float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); 170 | float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); 171 | float n111 = dot(g111, Pf1); 172 | 173 | vec3 fade_xyz = fade(Pf0); 174 | vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); 175 | vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); 176 | float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); 177 | return 2.2 * n_xyz; 178 | } 179 | 180 | float turbulence(vec3 p) { 181 | float w = 100.0; 182 | float t = -.5; 183 | for (float f = 1.0 ; f <= 10.0 ; f++) { 184 | float power = pow(2.0, f); 185 | t += abs(pnoise(vec3(power * p), vec3(10.0, 10.0, 10.0)) / power); 186 | } 187 | return t; 188 | } 189 | 190 | // START 191 | uniform float time; 192 | varying vec2 vUv; 193 | varying vec3 vNormal; 194 | varying float noise; 195 | 196 | varying vec3 vViewPosition; 197 | 198 | void main() { 199 | #include 200 | #include 201 | #include 202 | #include 203 | #ifndef FLAT_SHADED // Normal computed with derivatives when FLAT_SHADED 204 | vNormal = normalize(transformedNormal); 205 | #endif 206 | 207 | vViewPosition = - mvPosition.xyz; 208 | 209 | vUv = uv; 210 | 211 | noise = turbulence(0.01 * position + normal + time * 0.8); 212 | vec3 displacement = vec3((position.x) * noise, position.y * noise, position.z * noise); 213 | gl_Position = projectionMatrix * modelViewMatrix * vec4((position + normal) + displacement, 1.0); 214 | } 215 | `; 216 | -------------------------------------------------------------------------------- /src/components/content/Content.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Typography, Container } from "@material-ui/core"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import { TextDecrypt } from "./TextDecrypt"; 5 | import Resume from "../../settings/resume.json"; 6 | import { FirstName } from "../../utils/getName"; 7 | 8 | const useStyles = makeStyles((theme) => ({ 9 | main: { 10 | marginTop: "auto", 11 | marginBottom: "auto", 12 | "@media (max-width: 768px)": { 13 | marginLeft: theme.spacing(4), 14 | }, 15 | }, 16 | })); 17 | 18 | export const Content = () => { 19 | const classes = useStyles(); 20 | 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/content/SocialIcons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, Tooltip, IconButton, Zoom } from '@material-ui/core'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Resume from '../../settings/resume.json'; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | socialIcons: { 8 | position: 'absolute', 9 | top: theme.spacing(6), 10 | right: theme.spacing(6), 11 | }, 12 | iconButton: { 13 | height: '2.5rem', 14 | width: '2.5rem', 15 | display: 'block', 16 | marginBottom: theme.spacing(2), 17 | }, 18 | icon: { 19 | fontSize: '1.25rem', 20 | }, 21 | })); 22 | 23 | export const SocialIcons = () => { 24 | const classes = useStyles(); 25 | 26 | const socialItems = Resume.basics.profiles.map((socialItem) => ( 27 | 35 | 40 | 45 | 46 | 47 | 48 | 49 | )); 50 | 51 | return
{socialItems}
; 52 | }; 53 | -------------------------------------------------------------------------------- /src/components/content/SponsorButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | 4 | const useStyles = makeStyles((theme) => ({ 5 | svgHover: { 6 | fill: theme.palette.secondary.main, 7 | '&:hover': { 8 | transform: 'scale(1.1)', 9 | }, 10 | '&:focus': { 11 | transform: 'scale(1.1)', 12 | }, 13 | transition: 'transform 0.15s cubic-bezier(0.2, 0, 0.13, 2)', 14 | transform: 'scale(1)', 15 | overflow: 'visible !important', 16 | }, 17 | })); 18 | 19 | export const HeartIcon = () => { 20 | const classes = useStyles(); 21 | 22 | return ( 23 | 30 | 34 | 35 | ); 36 | }; 37 | 38 | export const HeartIconFilled = () => { 39 | const classes = useStyles(); 40 | 41 | return ( 42 | 49 | 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/components/content/TextDecrypt.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDencrypt } from "use-dencrypt-effect"; 3 | 4 | const decryptOptions = { 5 | chars: [ 6 | "-", 7 | ".", 8 | "/", 9 | "*", 10 | "!", 11 | "?", 12 | "#", 13 | "%", 14 | "&", 15 | "@", 16 | "$", 17 | "€", 18 | "(", 19 | ")", 20 | "[", 21 | "]", 22 | "{", 23 | "}", 24 | "<", 25 | ">", 26 | "~", 27 | "0", 28 | "1", 29 | "2", 30 | "3", 31 | "4", 32 | "5", 33 | "6", 34 | "7", 35 | "8", 36 | "9", 37 | "a", 38 | "b", 39 | "c", 40 | "d", 41 | "e", 42 | "f", 43 | "g", 44 | "h", 45 | "i", 46 | "j", 47 | "k", 48 | "l", 49 | "m", 50 | "n", 51 | "o", 52 | "p", 53 | "q", 54 | "r", 55 | "s", 56 | "t", 57 | "u", 58 | "v", 59 | "w", 60 | "x", 61 | "y", 62 | "z", 63 | ], 64 | interval: 50, 65 | }; 66 | 67 | export const TextDecrypt = (props) => { 68 | const { result, dencrypt } = useDencrypt(decryptOptions); 69 | 70 | useEffect(() => { 71 | const updateText = () => { 72 | dencrypt(props.text || ""); 73 | }; 74 | 75 | const action = setTimeout(updateText, 0); 76 | 77 | return () => clearTimeout(action); 78 | }, [dencrypt, props.text]); 79 | 80 | return ( 81 |

82 | {result} 83 | {" "} 84 |

85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /src/components/content/Today.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Typography } from "@material-ui/core"; 3 | 4 | export const Today = () => { 5 | var date = new Date(); 6 | var hour = date.getHours(); 7 | var time = `${ 8 | (hour < 4 && "night") || 9 | (hour < 12 && "morning") || 10 | (hour < 18 && "afternoon") || 11 | (hour < 22 && "evening") || 12 | "night" 13 | }`; 14 | var days = [ 15 | "weekend", 16 | "Monday", 17 | "Tuesday", 18 | "Wednesday", 19 | "Thursday", 20 | "Friday", 21 | "weekend", 22 | ]; 23 | var day = days[date.getDay()]; 24 | 25 | return ( 26 | 27 | Have a good {day === "weekend" ? day : time}. 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/components/footer/FooterText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import { Typography, Link } from '@material-ui/core'; 4 | import { TextDecrypt } from '../content/TextDecrypt'; 5 | import { HeartIcon } from '../content/SponsorButton'; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | footerText: { 9 | position: 'absolute', 10 | bottom: theme.spacing(6), 11 | left: theme.spacing(6), 12 | '&:hover': { 13 | color: theme.palette.primary.main, 14 | }, 15 | transition: 'all 0.5s ease', 16 | display: 'flex', 17 | alignItems: 'center', 18 | flexWrap: 'wrap', 19 | }, 20 | })); 21 | 22 | export const FooterText = () => { 23 | const classes = useStyles(); 24 | 25 | return ( 26 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/logo/Logo.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | 4 | const useStyles = makeStyles((theme) => ({ 5 | svgHover: { 6 | fill: theme.palette.foreground.default, 7 | "&:hover": { 8 | fill: theme.palette.primary.main, 9 | }, 10 | transition: "all 0.5s ease", 11 | }, 12 | })); 13 | 14 | export const Logo = () => { 15 | const classes = useStyles(); 16 | 17 | return ( 18 | 23 | 27 | 31 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/components/logo/LogoLink.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link, Tooltip, Zoom } from "@material-ui/core"; 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import Resume from "../../settings/resume.json"; 5 | import { Logo } from "./Logo"; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | svg: { 9 | width: "40px", 10 | height: "40px", 11 | position: "absolute", 12 | top: theme.spacing(6), 13 | left: theme.spacing(6), 14 | boxShadow: 15 | "0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12)", 16 | borderRadius: "50%", 17 | }, 18 | })); 19 | 20 | export const LogoLink = () => { 21 | const classes = useStyles(); 22 | 23 | return ( 24 | 29 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/speedDial/SpeedDial.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import { SpeedDial, SpeedDialIcon, SpeedDialAction } from "@material-ui/lab"; 4 | import Resume from "../../settings/resume.json"; 5 | 6 | const useStyles = makeStyles((theme) => ({ 7 | speedDial: { 8 | position: "absolute", 9 | top: theme.spacing(6), 10 | right: theme.spacing(6), 11 | }, 12 | iconColor: { 13 | color: theme.palette.foreground.default, 14 | }, 15 | })); 16 | 17 | export const SpeedDials = () => { 18 | const classes = useStyles(); 19 | 20 | const [open, setOpen] = React.useState(false); 21 | 22 | const handleClose = () => { 23 | setOpen(false); 24 | }; 25 | 26 | const handleOpen = () => { 27 | setOpen(true); 28 | }; 29 | 30 | const actionIcons = Resume.basics.profiles.map((action) => ( 31 | } 34 | tooltipTitle={action.network} 35 | onClick={handleClose} 36 | href={action.url} 37 | target="_blank" 38 | rel="noopener noreferrer" 39 | underline="none" 40 | color="inherit" 41 | /> 42 | )); 43 | 44 | return ( 45 | <> 46 | 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/theme/ThemeProvider.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, createContext } from "react"; 2 | import { LightTheme, DarkTheme } from "./Themes"; 3 | import { MuiThemeProvider } from "@material-ui/core/styles"; 4 | 5 | export const ThemeContext = createContext(); 6 | 7 | export const ThemeProvider = ({ children }) => { 8 | const getInitialMode = () => { 9 | if (typeof localStorage === "undefined") return true; 10 | const isReturningUser = "dark" in localStorage; 11 | const savedMode = JSON.parse(localStorage.getItem("dark")); 12 | const userPrefersDark = getPrefColorScheme(); 13 | if (isReturningUser) { 14 | return savedMode; 15 | } 16 | return !!userPrefersDark; 17 | }; 18 | 19 | const getPrefColorScheme = () => { 20 | if (!window.matchMedia) return; 21 | 22 | return window.matchMedia("(prefers-color-scheme: dark)").matches; 23 | }; 24 | 25 | const [theme, setTheme] = useState(getInitialMode() ? "dark" : "light"); 26 | 27 | const toggleTheme = () => { 28 | if (theme === "light") { 29 | setTheme("dark"); 30 | } else { 31 | setTheme("light"); 32 | } 33 | }; 34 | 35 | useEffect(() => { 36 | typeof localStorage !== "undefined" && 37 | localStorage.setItem("dark", JSON.stringify(theme === "dark")); 38 | }, [theme]); 39 | 40 | return ( 41 | 47 | 50 | {children} 51 | 52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/theme/ThemeToggle.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from "react"; 2 | import { ThemeContext } from "./ThemeProvider"; 3 | import { Tooltip, IconButton, Zoom } from "@material-ui/core"; 4 | import { makeStyles } from "@material-ui/core/styles"; 5 | import { Brightness4, Brightness7 } from "@material-ui/icons"; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | iconButton: { 9 | position: "absolute", 10 | bottom: theme.spacing(6), 11 | right: theme.spacing(6), 12 | height: "2.5rem", 13 | width: "2.5rem", 14 | }, 15 | icon: { 16 | fontSize: "1.25rem", 17 | }, 18 | })); 19 | 20 | export const ThemeToggle = () => { 21 | const { theme, toggleTheme } = useContext(ThemeContext); 22 | const classes = useStyles(); 23 | 24 | return ( 25 | 30 | 36 | {theme === "light" ? ( 37 | 38 | ) : ( 39 | 40 | )} 41 | 42 | 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/theme/Themes.js: -------------------------------------------------------------------------------- 1 | import { createMuiTheme, responsiveFontSizes } from '@material-ui/core'; 2 | import Settings from '../../settings/settings.json'; 3 | 4 | export const primary = `${Settings.colors.primary}`; 5 | export const secondary = `${Settings.colors.secondary}`; 6 | export const black = `${Settings.colors.black}`; 7 | export const white = `${Settings.colors.white}`; 8 | 9 | export const LightTheme = responsiveFontSizes( 10 | createMuiTheme({ 11 | palette: { 12 | type: 'light', 13 | primary: { 14 | main: primary, 15 | }, 16 | secondary: { 17 | main: secondary, 18 | }, 19 | background: { 20 | default: white, 21 | }, 22 | foreground: { 23 | default: black, 24 | }, 25 | }, 26 | typography: { 27 | fontSize: 16, 28 | htmlFontSize: 16, 29 | h2: { 30 | fontWeight: 500, 31 | }, 32 | h5: { 33 | fontWeight: 500, 34 | fontFamily: 'Roboto Mono, monospace', 35 | }, 36 | body1: { 37 | fontWeight: 500, 38 | fontFamily: 'Roboto Mono, monospace', 39 | }, 40 | }, 41 | overrides: { 42 | MuiCssBaseline: { 43 | '@global': { 44 | body: { 45 | color: black, 46 | backgroundColor: white, 47 | }, 48 | }, 49 | }, 50 | MuiIconButton: { 51 | root: { 52 | boxShadow: 53 | '0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12)', 54 | '&:hover': { 55 | backgroundColor: primary, 56 | }, 57 | transition: 'all 0.5s ease', 58 | }, 59 | }, 60 | MuiFab: { 61 | root: { 62 | width: '2.5rem', 63 | height: '2.5rem', 64 | fontSize: '1.25rem', 65 | }, 66 | primary: { 67 | color: black, 68 | backgroundColor: 'transparent', 69 | '&:hover': { 70 | color: black, 71 | backgroundColor: primary, 72 | }, 73 | transition: 'all 0.5s ease !important', 74 | }, 75 | }, 76 | MuiSpeedDialAction: { 77 | fab: { 78 | color: white, 79 | backgroundColor: 'transparent', 80 | '&:hover': { 81 | color: white, 82 | backgroundColor: primary, 83 | }, 84 | transition: 'all 0.5s ease', 85 | margin: '0px', 86 | marginBottom: '16px', 87 | }, 88 | }, 89 | MuiTooltip: { 90 | tooltip: { 91 | fontFamily: 'Roboto Mono, monospace', 92 | backgroundColor: primary, 93 | color: black, 94 | fontSize: 11, 95 | }, 96 | }, 97 | }, 98 | }) 99 | ); 100 | 101 | export const DarkTheme = responsiveFontSizes( 102 | createMuiTheme({ 103 | palette: { 104 | type: 'dark', 105 | primary: { 106 | main: primary, 107 | }, 108 | secondary: { 109 | main: secondary, 110 | }, 111 | background: { 112 | default: black, 113 | }, 114 | foreground: { 115 | default: white, 116 | }, 117 | }, 118 | typography: { 119 | fontSize: 16, 120 | htmlFontSize: 16, 121 | h2: { 122 | fontWeight: 500, 123 | }, 124 | h5: { 125 | fontWeight: 500, 126 | fontFamily: 'Roboto Mono, monospace', 127 | }, 128 | body1: { 129 | fontWeight: 500, 130 | fontFamily: 'Roboto Mono, monospace', 131 | }, 132 | }, 133 | overrides: { 134 | MuiCssBaseline: { 135 | '@global': { 136 | body: { 137 | color: white, 138 | backgroundColor: black, 139 | }, 140 | }, 141 | }, 142 | MuiIconButton: { 143 | root: { 144 | boxShadow: 145 | '0px 3px 5px -1px rgba(0,0,0,0.2),0px 6px 10px 0px rgba(0,0,0,0.14),0px 1px 18px 0px rgba(0,0,0,0.12)', 146 | '&:hover': { 147 | backgroundColor: primary, 148 | }, 149 | transition: 'all 0.5s ease', 150 | }, 151 | }, 152 | MuiFab: { 153 | root: { 154 | width: '2.5rem', 155 | height: '2.5rem', 156 | fontSize: '1.25rem', 157 | }, 158 | primary: { 159 | color: white, 160 | backgroundColor: 'transparent', 161 | '&:hover': { 162 | color: white, 163 | backgroundColor: primary, 164 | }, 165 | transition: 'all 0.5s ease !important', 166 | }, 167 | }, 168 | MuiSpeedDialAction: { 169 | fab: { 170 | color: white, 171 | backgroundColor: 'transparent', 172 | '&:hover': { 173 | color: white, 174 | backgroundColor: primary, 175 | }, 176 | transition: 'all 0.5s ease', 177 | margin: '0px', 178 | marginBottom: '16px', 179 | }, 180 | }, 181 | MuiTooltip: { 182 | tooltip: { 183 | fontFamily: 'Roboto Mono, monospace', 184 | backgroundColor: primary, 185 | color: white, 186 | fontSize: 11, 187 | }, 188 | }, 189 | }, 190 | }) 191 | ); 192 | -------------------------------------------------------------------------------- /src/hooks/useInViewport.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useInViewport = ( 4 | elementRef, 5 | unobserveOnIntersect, 6 | options = {} 7 | ) => { 8 | const [intersect, setIntersect] = useState(false); 9 | const [isUnobserved, setIsUnobserved] = useState(false); 10 | 11 | useEffect(() => { 12 | if (!elementRef?.current) return; 13 | 14 | const observer = new IntersectionObserver(([entry]) => { 15 | const { isIntersecting, target } = entry; 16 | 17 | setIntersect(isIntersecting); 18 | 19 | if (isIntersecting && unobserveOnIntersect) { 20 | observer.unobserve(target); 21 | setIsUnobserved(true); 22 | } 23 | }, options); 24 | 25 | if (!isUnobserved) { 26 | observer.observe(elementRef.current); 27 | } 28 | 29 | return () => observer.disconnect(); 30 | }, [elementRef, unobserveOnIntersect, options, isUnobserved]); 31 | 32 | return intersect; 33 | }; 34 | -------------------------------------------------------------------------------- /src/hooks/usePrefersReducedMotion.js: -------------------------------------------------------------------------------- 1 | export const usePrefersReducedMotion = () => { 2 | if (!window.matchMedia) return false; 3 | 4 | return window.matchMedia("(prefers-reduced-motion: reduce)").matches; 5 | }; 6 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | ::-moz-selection { 2 | background: #00bfbf; 3 | color: #fafafa; 4 | text-shadow: none; 5 | } 6 | ::selection { 7 | background: #00bfbf; 8 | color: #fafafa; 9 | text-shadow: none; 10 | } 11 | ::-webkit-scrollbar { 12 | width: 0px; 13 | background: transparent; 14 | } 15 | html { 16 | overflow: scroll; 17 | overflow-x: hidden; 18 | font-size: 16px; 19 | } 20 | body { 21 | transition: all 0.5s ease; 22 | } 23 | p { 24 | margin-block-start: 0.5em; 25 | margin-block-end: 0.5em; 26 | } 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render } from "react-snapshot"; 3 | import { App } from "./app/App"; 4 | import "./index.css"; 5 | 6 | render(, document.getElementById("root")); 7 | -------------------------------------------------------------------------------- /src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LogoLink } from '../components/logo/LogoLink'; 3 | import { Content } from '../components/content/Content'; 4 | import { Hidden } from '@material-ui/core'; 5 | import { makeStyles } from '@material-ui/core/styles'; 6 | import DisplacementSphere from '../components/background/DisplacementSphere'; 7 | import { ThemeToggle } from '../components/theme/ThemeToggle'; 8 | import { FooterText } from '../components/footer/FooterText'; 9 | import { SocialIcons } from '../components/content/SocialIcons'; 10 | import { SpeedDials } from '../components/speedDial/SpeedDial'; 11 | 12 | const useStyles = makeStyles(() => ({ 13 | root: { 14 | display: 'flex', 15 | flexDirection: 'column', 16 | minHeight: '100vh', 17 | }, 18 | })); 19 | 20 | export const Home = () => { 21 | const classes = useStyles(); 22 | 23 | return ( 24 | <> 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /src/pages/PageNotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const PageNotFound = () => { 4 | return
Page not found...
; 5 | }; 6 | -------------------------------------------------------------------------------- /src/pages/Resume.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Resume = () => { 4 | return
Resume...
; 5 | }; 6 | -------------------------------------------------------------------------------- /src/settings/resume.json: -------------------------------------------------------------------------------- 1 | { 2 | "basics": { 3 | "name": "Jo Lienhoop", 4 | "label": "Computer Science student", 5 | "email": "hey@jolienhoop.com", 6 | "phone": "(XXX) XXX-XXXX", 7 | "picture": "jl.png", 8 | "x_title": "Hey, I'm", 9 | "job": "CS student", 10 | "description": "Hey, I'm Jo Lienhoop, a Computer Science student based in Bremen, Germany. I love all things technical. Talk soon!", 11 | "summary": "based in Bremen, Germany.", 12 | "keywords": "personal,website,portfolio,template,kit,jo,lienhoop,jolienhoop,computer,science,bremen,germany,react,github,linkedin,google,gitlab,telegram,material,design,ui,webapp", 13 | "url": "https://jolienhoop.com", 14 | "location": { 15 | "address": "XX XXXXXstreet", 16 | "postalCode": "XXXXX", 17 | "city": "Bremen", 18 | "country": "Germany", 19 | "countryCode": "DEU", 20 | "region": "Bremen" 21 | }, 22 | "profiles": [ 23 | { 24 | "network": "Google", 25 | "username": "hey@jolienhoop.com", 26 | "url": "mailto:hey@jolienhoop.com", 27 | "x_icon": "fab fa-google" 28 | }, 29 | { 30 | "network": "LinkedIn", 31 | "username": "jolienhoop", 32 | "url": "https://www.linkedin.com/in/jolienhoop/", 33 | "x_icon": "fab fa-linkedin-in" 34 | }, 35 | { 36 | "network": "GitHub", 37 | "username": "johoop", 38 | "url": "https://github.com/JoHoop", 39 | "x_icon": "fab fa-github" 40 | }, 41 | { 42 | "network": "GitLab", 43 | "username": "joli", 44 | "url": "https://gitlab.informatik.uni-bremen.de/joli", 45 | "x_icon": "fab fa-gitlab" 46 | } 47 | ] 48 | }, 49 | "work": [ 50 | { 51 | "company": "Company", 52 | "position": "President", 53 | "website": "http://company.com", 54 | "startDate": "2013-01-01", 55 | "endDate": "2014-01-01", 56 | "summary": "Description...", 57 | "highlights": ["Started the company"] 58 | } 59 | ], 60 | "volunteer": [ 61 | { 62 | "organization": "Organization", 63 | "position": "Volunteer", 64 | "website": "http://organization.com/", 65 | "startDate": "2012-01-01", 66 | "endDate": "2013-01-01", 67 | "summary": "Description...", 68 | "highlights": ["Awarded 'Volunteer of the Month'"] 69 | } 70 | ], 71 | "education": [ 72 | { 73 | "institution": "University", 74 | "area": "Software Development", 75 | "studyType": "Bachelor", 76 | "startDate": "2011-01-01", 77 | "endDate": "2013-01-01", 78 | "gpa": "4.0", 79 | "courses": ["DB1101 - Basic SQL"] 80 | } 81 | ], 82 | "awards": [ 83 | { 84 | "title": "Award", 85 | "date": "2014-11-01", 86 | "awarder": "Company", 87 | "summary": "There is no spoon." 88 | } 89 | ], 90 | "publications": [ 91 | { 92 | "name": "Publication", 93 | "publisher": "Company", 94 | "releaseDate": "2014-10-01", 95 | "website": "http://publication.com", 96 | "summary": "Description..." 97 | } 98 | ], 99 | "skills": [ 100 | { 101 | "name": "Web Development", 102 | "level": "Master", 103 | "keywords": ["HTML", "CSS", "Javascript"] 104 | } 105 | ], 106 | "languages": [ 107 | { 108 | "language": "German", 109 | "fluency": "Native speaker" 110 | }, 111 | { 112 | "language": "English", 113 | "fluency": "Fluent speaker" 114 | }, 115 | { 116 | "language": "French", 117 | "fluency": "Good knowledge" 118 | } 119 | ], 120 | "interests": [ 121 | { 122 | "name": "Wildlife", 123 | "keywords": ["Ferrets", "Unicorns"] 124 | } 125 | ], 126 | "references": [ 127 | { 128 | "name": "Jane Doe", 129 | "reference": "Reference..." 130 | } 131 | ] 132 | } 133 | -------------------------------------------------------------------------------- /src/settings/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": { 3 | "primary": "#00bfbf", 4 | "secondary": "#db61a2", 5 | "black": "#111111", 6 | "white": "#fafafa" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/getName.js: -------------------------------------------------------------------------------- 1 | import Resume from "../settings/resume.json"; 2 | 3 | const names = Resume.basics.name.split(" "); 4 | 5 | export const FirstName = names[0]; 6 | 7 | export const LastName = names[names.length - 1]; 8 | 9 | export const Initials = FirstName.charAt(0) 10 | .toUpperCase() 11 | .concat(LastName.charAt(0).toUpperCase()); 12 | -------------------------------------------------------------------------------- /src/utils/logCredits.js: -------------------------------------------------------------------------------- 1 | import { primary } from "../components/theme/Themes"; 2 | 3 | export const logCredits = () => { 4 | const pieceEmoji = String.fromCodePoint(0x270c); 5 | 6 | const logStyle = [ 7 | `color: ${primary}`, 8 | "font-size: 3em", 9 | "font-weight: 300", 10 | "padding: 100px 0px 100px 0px", 11 | ].join(";"); 12 | 13 | return console.log( 14 | `%c © ${new Date().getFullYear()} github.com/johoop ${pieceEmoji}`, 15 | logStyle 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/style.js: -------------------------------------------------------------------------------- 1 | import { Color } from 'three/src/math/Color'; 2 | 3 | /** 4 | * Media query breakpoints 5 | */ 6 | export const media = { 7 | desktop: 1600, 8 | laptop: 1280, 9 | tablet: 1024, 10 | mobile: 696, 11 | mobileS: 320, 12 | }; 13 | 14 | /** 15 | * Convert a px string to a number 16 | */ 17 | export const pxToNum = px => Number(px.replace('px', '')); 18 | 19 | /** 20 | * Convert a number to a px string 21 | */ 22 | export const numToPx = num => `${num}px`; 23 | 24 | /** 25 | * Convert pixel values to rem for a11y 26 | */ 27 | export const pxToRem = px => `${px / 16}rem`; 28 | 29 | /** 30 | * Convert ms token values to a raw numbers for ReactTransitionGroup 31 | * Transition delay props 32 | */ 33 | export const msToNum = msString => Number(msString.replace('ms', '')); 34 | 35 | /** 36 | * Convert a number to an ms string 37 | */ 38 | export const numToMs = num => `${num}ms`; 39 | 40 | /** 41 | * Convert an rgb theme property (e.g. rgbBlack: '0 0 0') 42 | * to a ThreeJS Color class 43 | */ 44 | export const rgbToThreeColor = rgb => 45 | new Color(...rgb.split(' ').map(value => Number(value) / 255)); 46 | -------------------------------------------------------------------------------- /src/utils/three.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clean up a scene's materials and geometry 3 | */ 4 | export const cleanScene = scene => { 5 | scene.traverse(object => { 6 | if (!object.isMesh) return; 7 | 8 | object.geometry.dispose(); 9 | 10 | if (object.material.isMaterial) { 11 | cleanMaterial(object.material); 12 | } else { 13 | for (const material of object.material) { 14 | cleanMaterial(material); 15 | } 16 | } 17 | }); 18 | 19 | scene.dispose(); 20 | }; 21 | 22 | /** 23 | * Clean up and dispose of a material 24 | */ 25 | export const cleanMaterial = material => { 26 | material.dispose(); 27 | 28 | for (const key of Object.keys(material)) { 29 | const value = material[key]; 30 | if (value && typeof value === 'object' && 'minFilter' in value) { 31 | value.dispose(); 32 | } 33 | } 34 | }; 35 | 36 | /** 37 | * Clean up and dispose of a renderer 38 | */ 39 | export const cleanRenderer = renderer => { 40 | renderer.dispose(); 41 | renderer.forceContextLoss(); 42 | renderer = null; 43 | }; 44 | 45 | /** 46 | * Clean up lights by removing them from their parent 47 | */ 48 | export const removeLights = lights => { 49 | for (const light of lights) { 50 | light.parent.remove(light); 51 | } 52 | }; 53 | 54 | /** 55 | * A reasonable default pixel ratio 56 | */ 57 | export const renderPixelRatio = 2; 58 | -------------------------------------------------------------------------------- /src/utils/transition.js: -------------------------------------------------------------------------------- 1 | const visibleStatus = ['entering', 'entered']; 2 | 3 | /** 4 | * Is the given TransitionStatus visible? 5 | */ 6 | export const isVisible = status => visibleStatus.includes(status); 7 | 8 | /** 9 | * Is the given TransitionStatus hidden? 10 | */ 11 | export const isHidden = status => !visibleStatus.includes(status); 12 | 13 | /** 14 | * Forces a reflow to trigger transitions on enter 15 | */ 16 | export const reflow = node => node && node.offsetHeight; 17 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | --------------------------------------------------------------------------------