├── public ├── CNAME ├── social.jpg ├── favicons │ ├── favicon.ico │ ├── mstile-70x70.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── browserconfig.xml │ ├── manifest.json │ └── safari-pinned-tab.svg └── manifest.json ├── README.md ├── src ├── index.css ├── images │ ├── screensaver.gif │ ├── backgrounds │ │ ├── xp.jpg │ │ ├── logo.jpg │ │ ├── nasa.jpg │ │ ├── classic.jpg │ │ ├── clouds.jpg │ │ └── vaporwave.jpg │ ├── settings.svg │ ├── computer.svg │ ├── email.svg │ ├── paint.svg │ ├── moon.svg │ ├── flag.svg │ ├── resume.svg │ ├── briefcase.svg │ └── bot.svg ├── css │ ├── clock.css.map │ ├── screensaver.css.map │ ├── clock.scss │ ├── clock.css │ ├── settings.css.map │ ├── screensaver.scss │ ├── toolbar.css.map │ ├── screensaver.css │ ├── startbar.css.map │ ├── messenger.css.map │ ├── theme.css.map │ ├── settings.css │ ├── settings.scss │ ├── toolbar.css │ ├── toolbar.scss │ ├── startbar.scss │ ├── startbar.css │ ├── messenger.css │ ├── messenger.scss │ ├── normalize.css │ ├── theme.scss │ └── theme.css ├── index.jsx ├── components │ ├── message.jsx │ ├── shutDown.jsx │ ├── clock.jsx │ ├── toolbar.jsx │ ├── screensaver.jsx │ ├── contact.jsx │ ├── work.jsx │ ├── program.jsx │ ├── startbar.jsx │ ├── settings.jsx │ └── messenger.jsx ├── App.css.map ├── logo.svg ├── App.scss ├── App.css ├── config.js └── App.jsx ├── vite.config.js ├── .gitignore ├── .eslintrc.js ├── package.json └── index.html /public/CNAME: -------------------------------------------------------------------------------- 1 | heathervv.com 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cool 2 | -------------------------------------------------------------------------------- /public/social.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/social.jpg -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/favicons/favicon.ico -------------------------------------------------------------------------------- /src/images/screensaver.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/src/images/screensaver.gif -------------------------------------------------------------------------------- /src/images/backgrounds/xp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/src/images/backgrounds/xp.jpg -------------------------------------------------------------------------------- /public/favicons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/favicons/mstile-70x70.png -------------------------------------------------------------------------------- /src/images/backgrounds/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/src/images/backgrounds/logo.jpg -------------------------------------------------------------------------------- /src/images/backgrounds/nasa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/src/images/backgrounds/nasa.jpg -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/favicons/mstile-144x144.png -------------------------------------------------------------------------------- /public/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /public/favicons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/favicons/mstile-310x150.png -------------------------------------------------------------------------------- /public/favicons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/favicons/mstile-310x310.png -------------------------------------------------------------------------------- /src/images/backgrounds/classic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/src/images/backgrounds/classic.jpg -------------------------------------------------------------------------------- /src/images/backgrounds/clouds.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/src/images/backgrounds/clouds.jpg -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/images/backgrounds/vaporwave.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/src/images/backgrounds/vaporwave.jpg -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/heathervv/portfolio-chatbot/HEAD/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/css/clock.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["clock.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;EACA;EACA;EACA;EACA","file":"clock.css"} -------------------------------------------------------------------------------- /src/css/screensaver.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["screensaver.scss"],"names":[],"mappings":"AACE;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE","file":"screensaver.css"} -------------------------------------------------------------------------------- /src/css/clock.scss: -------------------------------------------------------------------------------- 1 | .clock { 2 | font-size: 18px; 3 | float: right; 4 | padding-top: 3px; 5 | font-weight: bold; 6 | margin-top: -3px; 7 | margin-right: 5px; 8 | padding: 15px 20px 11px; 9 | } 10 | -------------------------------------------------------------------------------- /src/css/clock.css: -------------------------------------------------------------------------------- 1 | .clock { 2 | font-size: 18px; 3 | float: right; 4 | padding-top: 3px; 5 | font-weight: bold; 6 | margin-top: -3px; 7 | margin-right: 5px; 8 | padding: 15px 20px 11px; 9 | } 10 | 11 | /*# sourceMappingURL=clock.css.map */ 12 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react-swc'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | base: '/', 7 | plugins: [react()], 8 | server: { 9 | port: 3000, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './css/normalize.css'; 4 | import './index.css'; 5 | import App from './App'; 6 | 7 | const container = document.getElementById('root'); 8 | const root = createRoot(container); 9 | root.render(); 10 | -------------------------------------------------------------------------------- /public/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/css/settings.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["settings.scss"],"names":[],"mappings":"AAIE;EADF;IAEM;;;AAGJ;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;;AAIJ;EACE;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;;AAKN;EACE","file":"settings.css"} -------------------------------------------------------------------------------- /src/css/screensaver.scss: -------------------------------------------------------------------------------- 1 | .screensaver { 2 | &-visible { 3 | display: block; 4 | position: fixed; 5 | height: 100%; 6 | width: 100%; 7 | top: 0; 8 | left: 0; 9 | z-index: 3; 10 | background-position: bottom center; 11 | background-size: cover; 12 | } 13 | 14 | &-not-visible { 15 | display: none; 16 | } 17 | } -------------------------------------------------------------------------------- /src/css/toolbar.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["toolbar.scss"],"names":[],"mappings":"AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;EACA;;AAGF;EACE;;AAGF;AAAA;EAEE;EACA;;AAIJ;EACE;EACA;EACA;;AAEA;EACE;EACA;;;AAKN;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AAAA;EAEE","file":"toolbar.css"} -------------------------------------------------------------------------------- /src/css/screensaver.css: -------------------------------------------------------------------------------- 1 | .screensaver-visible { 2 | display: block; 3 | position: fixed; 4 | height: 100%; 5 | width: 100%; 6 | top: 0; 7 | left: 0; 8 | z-index: 3; 9 | background-position: bottom center; 10 | background-size: cover; 11 | } 12 | .screensaver-not-visible { 13 | display: none; 14 | } 15 | 16 | /*# sourceMappingURL=screensaver.css.map */ 17 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Portfolio", 3 | "name": "Heather Vandervecht. Web engineer, lover of chatbots.", 4 | "icons": [ 5 | { 6 | "src": "./favicons/favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | # See https://help.github.com/ignore-files/ for more about ignoring files. 4 | 5 | # dependencies 6 | /node_modules 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | .idea/ 22 | .vscode 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /src/images/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/favicons/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Heather", 3 | "icons": [ 4 | { 5 | "src": "/android-chrome-192x192.png", 6 | "sizes": "192x192", 7 | "type": "image/png" 8 | }, 9 | { 10 | "src": "/android-chrome-512x512.png", 11 | "sizes": "512x512", 12 | "type": "image/png" 13 | } 14 | ], 15 | "theme_color": "#ffffff", 16 | "background_color": "#ffffff", 17 | "display": "standalone" 18 | } -------------------------------------------------------------------------------- /src/css/startbar.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["startbar.scss"],"names":[],"mappings":"AAGA;EACE;;AAEA;EAHF;IAII;IACA;IACA;IACA;IACA;IACA;IACA;;EAIE;AAAA;IACE;IACA;IACA;;EAGF;AAAA;AAAA;AAAA;IAEE;IACA;;EAIJ;IACE;;EAGF;IACE;IACA;IACA;IACA;IACA;;EAEA;IACE;IACA;IACA;IACA;IACA;IACA;IACA;;EAGF;AAAA;IAEE;;EAGF;AAAA;IAEE;IACA;IACA;;EAGF;IACE;;EAIJ;IACE;;;AAMF;AAAA;EACE;;;AAKN;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;AAGF;AAAA;EAEE;EACA;EACA;EACA;;AAEA;AAAA;AAAA;AAAA;EAEE;EACA;;AAGF;AAAA;EACE;;AAGF;AAAA;EACE;EACA","file":"startbar.css"} -------------------------------------------------------------------------------- /src/images/computer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/email.svg: -------------------------------------------------------------------------------- 1 | 2 | Back Of Envelope 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/message.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Linkify from 'react-linkify'; 4 | 5 | const Message = ({ type, content, user }) => ( 6 |
7 | {`<${user}>`} 8 | 9 | {content} 10 | 11 |
12 | ); 13 | 14 | Message.defaultProps = { 15 | type: '', 16 | content: '', 17 | user: '', 18 | }; 19 | 20 | Message.propTypes = { 21 | type: PropTypes.string, 22 | content: PropTypes.string, 23 | user: PropTypes.string, 24 | }; 25 | 26 | export default Message; 27 | -------------------------------------------------------------------------------- /src/css/messenger.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["messenger.scss"],"names":[],"mappings":"AAGA;EACE;EACA;EACA;;;AAIE;EADJ;IAEQ;;;;AAIR;EACI;EACA;EACA;EACA;EACA;EACA;;AACA;EAPJ;IAQQ;;;AAEJ;EAVJ;IAWQ;;;AAGJ;EACE;;;AAIN;EACI;EACA;EACA;EACA;;AACA;EACI;EACA;;AAEJ;EACI;;AAEJ;EACI;;AAEJ;EACI;;;AAIR;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACI;EACA;EACA;EACA;EACA;EACA;;AACA;EAPJ;IAQM;;;AAEF;EAVJ;IAWM;;;AAEF;EACI;;;AAIR;EACE;EACA;;AACA;EACE;EACA;EACA;EACA;;AACA;EALF;IAMI;IACA;;;AAGJ;EACE;EACA;EACA;EACA;;AAEA;EACE;;AAGJ;EACE;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;;AAEF;EACE;EACA;EACA;EACA;;AACA;EACE;;AAKF;EACE;EACA;;AAEF;EACE;EACA","file":"messenger.css"} -------------------------------------------------------------------------------- /src/App.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["App.scss"],"names":[],"mappings":"AAGA;AAAA;EAEE;;AACA;EAHF;AAAA;IAII;IACA;IACA;;;;AAIJ;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EACE;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AACA;EAVF;IAWI;IACA;;;AAEF;EACE;EACA;EACA;;AACA;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;;AAEF;EACE;EACA;EACA;;;AAKN;EACE;EACA;EACA;EACA;EACA;;AACA;EANF;IAOI;;;AAEF;EATF;IAUI;IACA;;;AAEF;EACE;;AAEA;EAHF;IAII;;;AAIF;EACE;;AAGJ;EACE;EACA;EACA;EACA;EACA;;AACA;EANF;IAOI;;;AAEF;EATF;IAUI;;;AAEF;EACE;;AACA;EAFF;IAGI;;;AAGJ;EACE;EACA;;AAGJ;EACE;;AAEF;EACE;;;AAIJ;AAAA;EAEE;;;AAIA;EADF;IAEI;;EACA;IACE;;;AAGJ;EACE;EACA;EACA;;;AAIJ;EACE","file":"App.css"} -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:react/recommended', 10 | 'plugin:react-hooks/recommended', 11 | 'airbnb', 12 | ], 13 | parserOptions: { 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | ecmaVersion: 12, 18 | sourceType: 'module', 19 | }, 20 | plugins: ['react', 'react-hooks'], 21 | rules: { 22 | 'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }], 23 | 'react/forbid-prop-types': [1, { 'forbid': [] }], 24 | "react/function-component-definition": [2, { "namedComponents": "arrow-function" }], 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/shutDown.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { apps, icons } from '../config'; 4 | 5 | // Components 6 | import Program from './program'; 7 | 8 | const ShutDown = ({ restart }) => ( 9 | 17 | It is now safe to turn off your computer. 18 |
19 |
20 | 21 |
22 | ); 23 | 24 | ShutDown.propTypes = { 25 | restart: PropTypes.func.isRequired, 26 | }; 27 | 28 | export default ShutDown; 29 | -------------------------------------------------------------------------------- /src/components/clock.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import '../css/clock.css'; 4 | 5 | const Clock = () => { 6 | const [time, setTime] = useState(''); 7 | 8 | const checkTime = (i) => (i < 10 ? `0${i}` : i); 9 | 10 | const startTime = () => { 11 | const today = new Date(); 12 | const hh = today.getHours(); 13 | 14 | let m = today.getMinutes(); 15 | m = checkTime(m); 16 | 17 | let dd = 'AM'; 18 | 19 | let h = hh; 20 | if (h >= 12) { 21 | h = hh - 12; 22 | dd = 'PM'; 23 | } 24 | if (h === 0) { 25 | h = 12; 26 | } 27 | 28 | setTimeout(startTime, 1000); 29 | 30 | setTime(`${h}:${m} ${dd}`); 31 | }; 32 | 33 | useEffect(() => { 34 | startTime(); 35 | }, []); 36 | 37 | return ( 38 |
{time}
39 | ); 40 | }; 41 | 42 | export default Clock; 43 | -------------------------------------------------------------------------------- /src/css/theme.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sourceRoot":"","sources":["theme.scss"],"names":[],"mappings":"AAKA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EAEE;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EAEE;EACA;EACA;EACA;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EARF;IASI;IACA;;;;AAIJ;EACE;EACA;EACA;EACA;EACA;EACA;;AAGE;EADF;IAEI;;;;AAOJ;AAAA;AAAA;EAGE;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;EAEE;;AAIJ;EACE;;AAEA;EAEE;;AAIJ;EACE;;AAEA;EAEE;;AAIJ;EACE;;AAEA;EAEE;;AAIJ;EACE;;AAEA;EAEE;EACA;;AAIJ;EACE;EACA;;AAEA;EACE;EACA;;AAGF;AAAA;EAEE;;AAIJ;EACE;EACA;;AAEA;AAAA;EAEE;;AAEA;AAAA;EACE;EACA;;AAIJ;EACE;EACA;;AAIJ;EACE;EACA;;AAGE;EACE;;AAKN;EACE;EACA;;AAIA;EACE;;AAIA;EACE;EACA;;AAKN;EACE;EACA;;AAGF;AAAA;EAEE;;;AAMF;AAAA;AAAA;EAGE;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;EAEE;;AAIJ;EACE;;AAEA;EAEE;;AAIJ;EACE;;AAEA;EAEE;;AAIJ;EACE;;AAEA;EAEE;;AAIJ;EACE;;AAEA;EAEE;EACA;;AAIJ;EACE;EACA;;AAEA;EACE;EACA;;AAGF;AAAA;EAEE;;AAIJ;EACE;EACA;;AAEA;AAAA;EAEE;;AAEA;AAAA;EACE;EACA;;AAIJ;EACE;EACA;;AAIJ;EACE;EACA;;AAGE;EACE;;AAKN;EACE;EACA;;AAIA;EACE;;AAIA;EACE;EACA;;AAKN;EACE;EACA;;AAGF;AAAA;EAEE;;AAEA;AAAA;EACE;EACA","file":"theme.css"} -------------------------------------------------------------------------------- /src/images/paint.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/css/settings.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 1025px) { 2 | .settings { 3 | width: 600px; 4 | } 5 | } 6 | .settings .sectionTitle { 7 | font-weight: 600; 8 | margin-top: 0; 9 | margin-bottom: 5px; 10 | } 11 | .settings .section:not(:first-child) { 12 | position: relative; 13 | border-top: 1px solid #888; 14 | margin-top: 20px; 15 | } 16 | .settings .section:not(:first-child):before { 17 | content: ""; 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | width: 100%; 22 | height: 1px; 23 | background: rgba(255, 255, 255, 0.8); 24 | } 25 | .settings .section:not(:first-child) .sectionTitle { 26 | margin-top: 15px; 27 | } 28 | .settings .options { 29 | background: #fff; 30 | box-shadow: 3px 3px 0 rgba(255, 255, 255, 0.6), inset 3px 3px rgba(0, 0, 0, 0.5); 31 | padding: 3px 0 3px 3px; 32 | margin-top: 0; 33 | } 34 | .settings .options .option { 35 | display: block; 36 | font-size: 14px; 37 | list-style-type: none; 38 | } 39 | .settings .options .option button { 40 | width: 100%; 41 | text-align: left; 42 | padding: 3px 7px; 43 | } 44 | .settings .options .option.selected button { 45 | background: blue; 46 | color: #fff; 47 | } 48 | .settings .attributions p:last-child { 49 | margin-top: 5px; 50 | } 51 | 52 | /*# sourceMappingURL=settings.css.map */ 53 | -------------------------------------------------------------------------------- /src/components/toolbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import '../css/toolbar.css'; 5 | 6 | const Toolbar = ({ 7 | image, 8 | title, 9 | closeApp, 10 | component, 11 | updateStartbar, 12 | notificationStyle, 13 | }) => ( 14 |
15 |
16 | {image} 17 | {' '} 18 | {title} 19 |
20 | 27 | { 28 | !notificationStyle 29 | && 30 | } 31 |
32 | ); 33 | 34 | Toolbar.defaultProps = { 35 | image: '', 36 | title: '', 37 | closeApp: () => {}, 38 | component: '', 39 | updateStartbar: () => {}, 40 | notificationStyle: false, 41 | }; 42 | 43 | Toolbar.propTypes = { 44 | image: PropTypes.string, 45 | title: PropTypes.string, 46 | closeApp: PropTypes.func, 47 | component: PropTypes.string, 48 | updateStartbar: PropTypes.func, 49 | notificationStyle: PropTypes.bool, 50 | }; 51 | 52 | export default Toolbar; 53 | -------------------------------------------------------------------------------- /src/css/settings.scss: -------------------------------------------------------------------------------- 1 | // $tablet: 750; 2 | $desktop: 1025; 3 | 4 | .settings { 5 | @media (min-width: $desktop + px) { 6 | width: 600px; 7 | } 8 | 9 | .sectionTitle { 10 | font-weight: 600; 11 | margin-top: 0; 12 | margin-bottom: 5px; 13 | } 14 | 15 | .section:not(:first-child) { 16 | position: relative; 17 | border-top: 1px solid #888; 18 | margin-top: 20px; 19 | 20 | &:before { 21 | content: ''; 22 | position: absolute; 23 | top: 0; 24 | left: 0; 25 | width: 100%; 26 | height: 1px; 27 | background: rgba(255, 255, 255, 0.8); 28 | } 29 | 30 | .sectionTitle { 31 | margin-top: 15px; 32 | } 33 | } 34 | 35 | .options { 36 | background: #fff; 37 | box-shadow: 3px 3px 0 rgba(255, 255, 255, 0.6), inset 3px 3px rgba(0, 0, 0, 0.5); 38 | padding: 3px 0 3px 3px; 39 | margin-top: 0; 40 | 41 | .option { 42 | display: block; 43 | font-size: 14px; 44 | list-style-type: none; 45 | 46 | button { 47 | width: 100%; 48 | text-align: left; 49 | padding: 3px 7px; 50 | } 51 | 52 | &.selected button { 53 | background: blue; 54 | color: #fff; 55 | } 56 | } 57 | } 58 | 59 | .attributions p:last-child { 60 | margin-top: 5px; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/images/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portfolio-chatbot", 3 | "version": "2.2.0", 4 | "private": true, 5 | "dependencies": { 6 | "@vitejs/plugin-react-swc": "^3.8.0", 7 | "axios": "^1.7.9", 8 | "npm-run-all": "^4.1.5", 9 | "prop-types": "^15.8.1", 10 | "react": "^18.3.1", 11 | "react-dom": "^18.3.1", 12 | "react-draggable": "^4.4.6", 13 | "react-linkify": "^1.0.0-alpha", 14 | "react-transition-group": "^4.4.5", 15 | "sass": "^1.85.0", 16 | "uuid": "^13.0.0", 17 | "vite": "^6.1.0", 18 | "vite-plugin-svgr": "^4.3.0" 19 | }, 20 | "devDependencies": { 21 | "eslint": "^9.20.1", 22 | "eslint-plugin-import": "^2.31.0", 23 | "eslint-plugin-jsx-a11y": "^6.10.2", 24 | "eslint-plugin-react": "^7.37.4", 25 | "eslint-plugin-react-hooks": "^5.1.0", 26 | "gh-pages": "^6.3.0" 27 | }, 28 | "scripts": { 29 | "build-css": "sass src:src", 30 | "watch-css": "npm run build-css && sass --watch src:src", 31 | "start-js": "vite", 32 | "start": "npm-run-all -p watch-css start-js", 33 | "build-js": "vite build", 34 | "build": "npm-run-all build-css build-js", 35 | "serve": "vite preview", 36 | "predeploy": "npm run build", 37 | "deploy": "gh-pages -d dist" 38 | }, 39 | "homepage": "https://heathervv.github.io", 40 | "browserslist": [ 41 | ">0.2%", 42 | "not dead", 43 | "not ie <= 11", 44 | "not op_mini all" 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /src/components/screensaver.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import graphic from '../images/screensaver.gif'; 3 | import '../css/screensaver.css'; 4 | 5 | const Screensaver = () => { 6 | const [screensaverIsVisible, changeScreensaverVisibility] = useState(false); 7 | 8 | const classNames = [ 9 | 'screensaver', 10 | screensaverIsVisible ? 'screensaver-visible' : 'screensaver-not-visible', 11 | ]; 12 | 13 | const setScreensaver = (title, visibility) => { 14 | document.title = title; 15 | changeScreensaverVisibility(visibility); 16 | }; 17 | 18 | useEffect(() => { 19 | const originalDocTitle = document.title; 20 | let timeout; 21 | 22 | // Set screensaver if user stops moving for X seconds 23 | document.onmousemove = () => { 24 | clearTimeout(timeout); 25 | setScreensaver(originalDocTitle, false); 26 | 27 | timeout = setTimeout(() => { 28 | setScreensaver('Zzz... 🌚', true); 29 | }, 30000); 30 | }; 31 | 32 | // Set screensaver if user changes tab 33 | document.addEventListener('visibilitychange', () => { 34 | if (document.hidden) { 35 | setScreensaver('Zzz... 🌚', true); 36 | } else { 37 | setScreensaver(originalDocTitle, false); 38 | } 39 | }); 40 | }, []); 41 | 42 | return ( 43 |
44 | ); 45 | }; 46 | 47 | export default Screensaver; 48 | -------------------------------------------------------------------------------- /src/css/toolbar.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | padding: 5px 7px 7px; 3 | position: relative; 4 | top: 2px; 5 | left: 3px; 6 | width: calc(100% - 9px); 7 | } 8 | .toolbar:after { 9 | content: ""; 10 | display: block; 11 | clear: both; 12 | } 13 | .toolbar .title { 14 | float: left; 15 | color: #fff; 16 | font-size: 14px; 17 | margin-top: 2px; 18 | min-width: 160px; 19 | } 20 | .toolbar .title img { 21 | max-height: 20px; 22 | max-width: 25px; 23 | width: auto; 24 | margin-right: 2px; 25 | } 26 | .toolbar .title span { 27 | padding-top: 4px; 28 | } 29 | .toolbar .title img, 30 | .toolbar .title span { 31 | display: inline-block; 32 | vertical-align: middle; 33 | } 34 | .toolbar button { 35 | font-weight: 400; 36 | float: right; 37 | margin-left: 5px; 38 | } 39 | .toolbar button.disabled { 40 | pointer-events: none; 41 | cursor: auto; 42 | } 43 | 44 | .messenger .toolbar { 45 | background: linear-gradient(to right, #0000F7 0%, #00FEFE 100%); 46 | } 47 | 48 | .contact .toolbar { 49 | background: linear-gradient(to right, #DE241E 0%, #FFF200 100%); 50 | } 51 | 52 | .work .toolbar { 53 | background: linear-gradient(to right, #2536F8 0%, #DF3285 100%); 54 | } 55 | 56 | .paint .toolbar { 57 | background: linear-gradient(to right, #34ce18 0%, #f9f227 100%); 58 | } 59 | 60 | .shutdown .toolbar, 61 | .settings .toolbar { 62 | background: linear-gradient(to right, rgb(0, 0, 127.5) 0%, blue 100%); 63 | } 64 | 65 | /*# sourceMappingURL=toolbar.css.map */ 66 | -------------------------------------------------------------------------------- /src/images/flag.svg: -------------------------------------------------------------------------------- 1 | 2 | rainbow flag 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/css/toolbar.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | .toolbar { 4 | padding: 5px 7px 7px; 5 | position: relative; 6 | top: 2px; 7 | left: 3px; 8 | width: calc(100% - 9px); 9 | 10 | &:after { 11 | content: ''; 12 | display: block; 13 | clear: both; 14 | } 15 | 16 | .title { 17 | float: left; 18 | color: #fff; 19 | font-size: 14px; 20 | margin-top: 2px; 21 | min-width: 160px; 22 | 23 | img { 24 | max-height: 20px; 25 | max-width: 25px; 26 | width: auto; 27 | margin-right: 2px; 28 | } 29 | 30 | span { 31 | padding-top: 4px; 32 | } 33 | 34 | img, 35 | span { 36 | display: inline-block; 37 | vertical-align: middle; 38 | } 39 | } 40 | 41 | button { 42 | font-weight: 400; 43 | float: right; 44 | margin-left: 5px; 45 | 46 | &.disabled { 47 | pointer-events: none; 48 | cursor: auto; 49 | } 50 | } 51 | } 52 | 53 | .messenger .toolbar { 54 | background: linear-gradient(to right, #0000F7 0%, #00FEFE 100%); 55 | } 56 | 57 | .contact .toolbar { 58 | background: linear-gradient(to right, #DE241E 0%, #FFF200 100%); 59 | } 60 | 61 | .work .toolbar { 62 | background: linear-gradient(to right, #2536F8 0%, #DF3285 100%); 63 | } 64 | 65 | .paint .toolbar { 66 | background: linear-gradient(to right, #34ce18 0%, #f9f227 100%); 67 | } 68 | 69 | .shutdown .toolbar, 70 | .settings .toolbar { 71 | background: linear-gradient(to right, color.adjust(blue, $lightness: -25%) 0%, blue 100%); 72 | } -------------------------------------------------------------------------------- /src/images/resume.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/images/briefcase.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/contact.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { apps, icons, contact } from '../config'; 4 | 5 | // Components 6 | import Program from './program'; 7 | 8 | const Contact = ({ 9 | updateActiveApp, 10 | closeApp, 11 | updateStartbar, 12 | openApps, 13 | minimizedApps, 14 | currentlyActiveApp, 15 | previouslyActiveApp, 16 | }) => ( 17 | 29 | {contact.content} 30 |
31 |
32 | (Try writing here~) 33 |
34 |
35 | ========================================== 36 |
37 |
38 | {contact.emailLink.replace('mailto:', '')} 39 | {' '} 40 |
41 | LinkedIn 42 | {' '} 43 |
44 | GitHub 45 |
46 | ); 47 | 48 | Contact.defaultProps = { 49 | updateActiveApp: () => { }, 50 | closeApp: () => { }, 51 | updateStartbar: () => { }, 52 | openApps: [], 53 | minimizedApps: [], 54 | currentlyActiveApp: '', 55 | previouslyActiveApp: '', 56 | }; 57 | 58 | Contact.propTypes = { 59 | updateActiveApp: PropTypes.func, 60 | closeApp: PropTypes.func, 61 | updateStartbar: PropTypes.func, 62 | openApps: PropTypes.array, 63 | minimizedApps: PropTypes.array, 64 | currentlyActiveApp: PropTypes.string, 65 | previouslyActiveApp: PropTypes.string, 66 | }; 67 | 68 | export default Contact; 69 | -------------------------------------------------------------------------------- /src/components/work.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Linkify from 'react-linkify'; 4 | import { apps, icons, work } from '../config'; 5 | 6 | // Components 7 | import Program from './program'; 8 | 9 | const componentDecorator = (href, text, key) => ( 10 | 11 | {text} 12 | 13 | ); 14 | 15 | const Work = ({ 16 | updateActiveApp, 17 | updateStartbar, 18 | closeApp, 19 | openApps, 20 | minimizedApps, 21 | currentlyActiveApp, 22 | previouslyActiveApp, 23 | }) => ( 24 | 36 | 37 | { 38 | work.map((item) => ( 39 |
40 |

{item.url ? {item.title} : item.title}

41 |

{item.copy}

42 |
43 | )) 44 | } 45 |
46 |
47 | ); 48 | 49 | Work.defaultProps = { 50 | updateActiveApp: () => {}, 51 | updateStartbar: () => {}, 52 | closeApp: () => {}, 53 | openApps: [], 54 | minimizedApps: [], 55 | currentlyActiveApp: '', 56 | previouslyActiveApp: '', 57 | }; 58 | 59 | Work.propTypes = { 60 | updateActiveApp: PropTypes.func, 61 | updateStartbar: PropTypes.func, 62 | closeApp: PropTypes.func, 63 | openApps: PropTypes.array, 64 | minimizedApps: PropTypes.array, 65 | currentlyActiveApp: PropTypes.string, 66 | previouslyActiveApp: PropTypes.string, 67 | }; 68 | 69 | export default Work; 70 | -------------------------------------------------------------------------------- /src/images/bot.svg: -------------------------------------------------------------------------------- 1 | 2 | robot 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/css/startbar.scss: -------------------------------------------------------------------------------- 1 | $tablet: 750; 2 | $desktop: 1025; 3 | 4 | .startbar { 5 | display: none; 6 | 7 | @media (min-width: $tablet + px) { 8 | display: flex; 9 | position: fixed; 10 | left: 0; 11 | bottom: 0; 12 | width: 100%; 13 | padding: 8px 0 5px 8px; 14 | z-index: 3; 15 | 16 | a, 17 | button { 18 | img { 19 | max-height: 20px; 20 | max-width: 25px; 21 | width: auto; 22 | } 23 | 24 | img, 25 | span { 26 | display: inline-block; 27 | vertical-align: middle; 28 | } 29 | } 30 | 31 | .start { 32 | float: left; 33 | } 34 | 35 | .programs { 36 | display: flex; 37 | flex: 1; 38 | margin-left: 10px; 39 | padding-left: 10px; 40 | position: relative; 41 | 42 | &:before { 43 | position: absolute; 44 | top: 1px; 45 | left: 0; 46 | display: block; 47 | content: ''; 48 | height: 100%; 49 | width: 1px; 50 | } 51 | 52 | a:not(.active), 53 | button:not(.active) { 54 | font-weight: 400; 55 | } 56 | 57 | a, 58 | button { 59 | margin-right: 12px; 60 | min-width: 105px; 61 | text-align: center; 62 | } 63 | 64 | .startbar-settings { 65 | width: 128px; 66 | } 67 | } 68 | 69 | .closed-program { 70 | display: none; 71 | } 72 | } 73 | 74 | a, 75 | button { 76 | img { 77 | margin-right: 5px; 78 | } 79 | } 80 | } 81 | 82 | .start-cupboard { 83 | position: fixed; 84 | left: 11px; 85 | bottom: 48px; 86 | width: 275px; 87 | padding: 0 3px 3px 0; 88 | display: none; 89 | z-index: 3; 90 | 91 | &.visible { 92 | display: block; 93 | } 94 | 95 | a, 96 | button { 97 | display: block; 98 | width: 100%; 99 | padding: 9px 7px 15px 10px; 100 | text-align: left; 101 | 102 | img, 103 | span { 104 | display: inline-block; 105 | vertical-align: middle; 106 | } 107 | 108 | span { 109 | padding-top: 9px; 110 | } 111 | 112 | img { 113 | max-width: 30px; 114 | margin-right: 5px; 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | Heather Vandervecht 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 |
49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/css/startbar.css: -------------------------------------------------------------------------------- 1 | .startbar { 2 | display: none; 3 | } 4 | @media (min-width: 750px) { 5 | .startbar { 6 | display: flex; 7 | position: fixed; 8 | left: 0; 9 | bottom: 0; 10 | width: 100%; 11 | padding: 8px 0 5px 8px; 12 | z-index: 3; 13 | } 14 | .startbar a img, 15 | .startbar button img { 16 | max-height: 20px; 17 | max-width: 25px; 18 | width: auto; 19 | } 20 | .startbar a img, 21 | .startbar a span, 22 | .startbar button img, 23 | .startbar button span { 24 | display: inline-block; 25 | vertical-align: middle; 26 | } 27 | .startbar .start { 28 | float: left; 29 | } 30 | .startbar .programs { 31 | display: flex; 32 | flex: 1; 33 | margin-left: 10px; 34 | padding-left: 10px; 35 | position: relative; 36 | } 37 | .startbar .programs:before { 38 | position: absolute; 39 | top: 1px; 40 | left: 0; 41 | display: block; 42 | content: ""; 43 | height: 100%; 44 | width: 1px; 45 | } 46 | .startbar .programs a:not(.active), 47 | .startbar .programs button:not(.active) { 48 | font-weight: 400; 49 | } 50 | .startbar .programs a, 51 | .startbar .programs button { 52 | margin-right: 12px; 53 | min-width: 105px; 54 | text-align: center; 55 | } 56 | .startbar .programs .startbar-settings { 57 | width: 128px; 58 | } 59 | .startbar .closed-program { 60 | display: none; 61 | } 62 | } 63 | .startbar a img, 64 | .startbar button img { 65 | margin-right: 5px; 66 | } 67 | 68 | .start-cupboard { 69 | position: fixed; 70 | left: 11px; 71 | bottom: 48px; 72 | width: 275px; 73 | padding: 0 3px 3px 0; 74 | display: none; 75 | z-index: 3; 76 | } 77 | .start-cupboard.visible { 78 | display: block; 79 | } 80 | .start-cupboard a, 81 | .start-cupboard button { 82 | display: block; 83 | width: 100%; 84 | padding: 9px 7px 15px 10px; 85 | text-align: left; 86 | } 87 | .start-cupboard a img, 88 | .start-cupboard a span, 89 | .start-cupboard button img, 90 | .start-cupboard button span { 91 | display: inline-block; 92 | vertical-align: middle; 93 | } 94 | .start-cupboard a span, 95 | .start-cupboard button span { 96 | padding-top: 9px; 97 | } 98 | .start-cupboard a img, 99 | .start-cupboard button img { 100 | max-width: 30px; 101 | margin-right: 5px; 102 | } 103 | 104 | /*# sourceMappingURL=startbar.css.map */ 105 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/program.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 2 | /* Event handler added to div for delight, not actual functionality */ 3 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 4 | import React from 'react'; 5 | import PropTypes from 'prop-types'; 6 | 7 | // Components 8 | import Draggable from 'react-draggable'; 9 | import Toolbar from './toolbar'; 10 | 11 | const Program = ({ 12 | notificationStyle, 13 | programName, 14 | openApps, 15 | currentlyActiveApp, 16 | previouslyActiveApp, 17 | minimizedApps, 18 | updateActiveApp, 19 | updateStartbar, 20 | programIcon, 21 | programRights, 22 | closeApp, 23 | contentEditable, 24 | systemStyle, 25 | children, 26 | }) => ( 27 | 34 |
updateActiveApp(null, programName.toLowerCase())} 38 | > 39 | 47 |
52 | {children} 53 |
54 |
55 |
56 | ); 57 | 58 | Program.propTypes = { 59 | programName: PropTypes.string.isRequired, 60 | programIcon: PropTypes.string.isRequired, 61 | children: PropTypes.node.isRequired, 62 | programRights: PropTypes.string, 63 | contentEditable: PropTypes.bool, 64 | updateActiveApp: PropTypes.func, 65 | closeApp: PropTypes.func, 66 | updateStartbar: PropTypes.func, 67 | openApps: PropTypes.array, 68 | minimizedApps: PropTypes.array, 69 | currentlyActiveApp: PropTypes.string, 70 | previouslyActiveApp: PropTypes.string, 71 | notificationStyle: PropTypes.bool, 72 | systemStyle: PropTypes.bool, 73 | }; 74 | 75 | Program.defaultProps = { 76 | programRights: null, 77 | contentEditable: false, 78 | updateActiveApp: () => { }, 79 | closeApp: () => { }, 80 | updateStartbar: () => { }, 81 | openApps: [], 82 | minimizedApps: [], 83 | currentlyActiveApp: null, 84 | previouslyActiveApp: null, 85 | notificationStyle: false, 86 | systemStyle: false, 87 | }; 88 | 89 | export default Program; 90 | -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | $tablet: 750; 2 | $desktop: 1025; 3 | 4 | html, 5 | body { 6 | font-family: 'Anonymous Pro', monospace; 7 | @media (min-width: $desktop + px) { 8 | width: 100%; 9 | height: 100%; 10 | overflow: hidden; 11 | } 12 | } 13 | 14 | html { 15 | background: #000; 16 | } 17 | 18 | .shutDownPage { 19 | position: fixed; 20 | top: 0; 21 | left: 0; 22 | width: 100%; 23 | height: 100%; 24 | background: #000; 25 | opacity: 0; 26 | transition: opacity .2s ease-in-out; 27 | pointer-events: none; 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | &.visible { 32 | opacity: 1; 33 | pointer-events: auto; 34 | z-index: 5; 35 | } 36 | } 37 | 38 | .desktop { 39 | position: fixed; 40 | top: 0; 41 | left: 0; 42 | right: 0; 43 | bottom: 0; 44 | overflow: hidden; 45 | background-repeat: no-repeat; 46 | background-position: center; 47 | background-size: cover; 48 | @media (min-width: $desktop + px) { 49 | position: static; 50 | height: 100vh; 51 | } 52 | .icons { 53 | position: absolute; 54 | top: 38px; 55 | left: 45px; 56 | a, 57 | button { 58 | text-decoration: none; 59 | text-align: center; 60 | display: block; 61 | margin-bottom: 50px; 62 | color: #fff; 63 | text-shadow: 1px 2px 0 rgba(#000, .5); 64 | } 65 | img { 66 | width: 60px; 67 | display: block; 68 | margin-bottom: 15px; 69 | } 70 | } 71 | } 72 | 73 | .program { 74 | position: absolute; 75 | width: 100%; 76 | cursor: default; 77 | padding-bottom: 9px; 78 | display: inline-block; 79 | @media (max-width: $desktop - 1 + px) { 80 | transform: none !important; 81 | } 82 | @media (min-width: $desktop + px) { 83 | max-width: 100%; 84 | width: auto; 85 | } 86 | &:not(.notification) { 87 | height: 100%; 88 | 89 | @media (min-width: $desktop + px) { 90 | height: auto; 91 | } 92 | } 93 | &:not(.system) { 94 | .content { 95 | background: #fff; 96 | } 97 | } 98 | .content { 99 | margin-top: 6px; 100 | width: calc(100% - 13px); 101 | padding: 10px; 102 | margin-left: 3px; 103 | height: calc(100% - 44px); 104 | @media (min-width: $tablet + px) { 105 | height: calc(100% - 96px); 106 | } 107 | @media (min-width: $desktop + px) { 108 | height: auto; 109 | } 110 | &[contentEditable=true] { 111 | overflow: scroll; 112 | @media (min-width: $desktop + px) { 113 | max-height: 250px; 114 | } 115 | } 116 | h3 { 117 | margin-bottom: 10px; 118 | line-height: 1.2; 119 | } 120 | } 121 | &.previous-active { 122 | z-index: 1; 123 | } 124 | &.active { 125 | z-index: 2; 126 | } 127 | } 128 | 129 | [data-view="closed"], 130 | [data-view="minimized"] { 131 | display: none; 132 | } 133 | 134 | .txt-file { 135 | @media (min-width: $desktop + px) { 136 | width: 400px; 137 | .content { 138 | max-height: 450px; 139 | } 140 | } 141 | .content { 142 | overflow: scroll; 143 | -webkit-overflow-scrolling: touch; 144 | line-height: 1.3; 145 | } 146 | } 147 | 148 | .work.program .content div:not(:last-child) { 149 | margin-bottom: 30px; 150 | } 151 | -------------------------------------------------------------------------------- /src/css/messenger.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 0; 3 | height: 0; 4 | background: transparent; 5 | } 6 | 7 | @media (min-width: 1025px) { 8 | .messenger { 9 | width: 700px; 10 | } 11 | } 12 | 13 | .messages { 14 | padding-top: 15px !important; 15 | padding-bottom: 25px !important; 16 | width: 100%; 17 | overflow: scroll; 18 | -webkit-overflow-scrolling: touch; 19 | height: calc(100% - 98px) !important; 20 | } 21 | @media (min-width: 750px) { 22 | .messages { 23 | height: calc(100% - 150px) !important; 24 | } 25 | } 26 | @media (min-width: 1025px) { 27 | .messages { 28 | height: 350px !important; 29 | } 30 | } 31 | .messages > div { 32 | padding-bottom: 10px; 33 | } 34 | 35 | .message { 36 | font-size: 16px; 37 | margin-bottom: 10px; 38 | padding: 0 5px; 39 | line-height: 1.3; 40 | } 41 | .message .username { 42 | display: inline-block; 43 | padding-right: 5px; 44 | } 45 | .message .Linkify { 46 | display: inline; 47 | } 48 | .message.received .username { 49 | color: #FF0000; 50 | } 51 | .message.sent .username { 52 | color: #0000FF; 53 | } 54 | 55 | .message-enter { 56 | opacity: 0.01; 57 | transform: translateY(10px); 58 | } 59 | 60 | .message-enter-active { 61 | opacity: 1; 62 | transform: translateY(0); 63 | transition: opacity 0.1s ease-in, transform 0.1s ease-in; 64 | } 65 | 66 | .message-exit { 67 | opacity: 1; 68 | transform: translateY(0); 69 | } 70 | 71 | .message-exit-active { 72 | opacity: 0.01; 73 | transform: translateY(10px); 74 | transition: opacity 0.1s ease-in, transform 0.1s ease-in; 75 | } 76 | 77 | .activeTyping { 78 | position: absolute; 79 | left: 20px; 80 | bottom: 70px; 81 | font-size: 12px; 82 | color: #FF0000; 83 | display: none; 84 | } 85 | @media (min-width: 750px) { 86 | .activeTyping { 87 | bottom: 117px; 88 | } 89 | } 90 | @media (min-width: 1025px) { 91 | .activeTyping { 92 | bottom: 70px; 93 | } 94 | } 95 | .activeTyping.visible { 96 | display: block; 97 | } 98 | 99 | .userInput { 100 | position: relative; 101 | width: calc(100% - 13px); 102 | } 103 | .userInput input { 104 | width: 100%; 105 | padding: 8px; 106 | font-size: 16px; 107 | border: none; 108 | } 109 | @media (min-width: 1025px) { 110 | .userInput input { 111 | padding: 9px 12px 8px; 112 | font-size: 14px; 113 | } 114 | } 115 | .userInput .buttonWrapper { 116 | background: #fff; 117 | max-width: 100%; 118 | overflow: scroll; 119 | -webkit-overflow-scrolling: touch; 120 | } 121 | .userInput .buttonWrapper > div { 122 | white-space: nowrap; 123 | } 124 | .userInput button { 125 | margin: 5px; 126 | cursor: pointer; 127 | } 128 | .userInput .field { 129 | width: calc(100% - 122px); 130 | margin-left: 3px; 131 | margin-top: 10px; 132 | margin-bottom: 2px; 133 | padding: 3px 0 0 3px; 134 | } 135 | .userInput .option-toggle { 136 | position: absolute !important; 137 | right: 0; 138 | top: 2px; 139 | width: 100px; 140 | } 141 | .userInput .option-toggle:active { 142 | top: 4px; 143 | } 144 | .userInput.hidden .field * { 145 | opacity: 0; 146 | pointer-events: none; 147 | } 148 | .userInput.hidden .option-toggle { 149 | pointer-events: none; 150 | cursor: auto; 151 | } 152 | 153 | /*# sourceMappingURL=messenger.css.map */ 154 | -------------------------------------------------------------------------------- /src/css/messenger.scss: -------------------------------------------------------------------------------- 1 | $tablet: 750; 2 | $desktop: 1025; 3 | 4 | ::-webkit-scrollbar { 5 | width: 0; 6 | height: 0; 7 | background: transparent; 8 | } 9 | 10 | .messenger { 11 | @media (min-width: $desktop + px) { 12 | width: 700px; 13 | } 14 | } 15 | 16 | .messages { 17 | padding-top: 15px !important; 18 | padding-bottom: 25px !important; 19 | width: 100%; 20 | overflow: scroll; 21 | -webkit-overflow-scrolling: touch; 22 | height: calc(100% - 98px) !important; 23 | @media (min-width: $tablet + px) { 24 | height: calc(100% - 150px) !important; 25 | } 26 | @media (min-width: $desktop + px) { 27 | height: 350px !important; 28 | } 29 | 30 | & > div { 31 | padding-bottom: 10px; 32 | } 33 | } 34 | 35 | .message { 36 | font-size: 16px; 37 | margin-bottom: 10px; 38 | padding: 0 5px; 39 | line-height: 1.3; 40 | .username { 41 | display: inline-block; 42 | padding-right: 5px; 43 | } 44 | .Linkify { 45 | display: inline; 46 | } 47 | &.received .username { 48 | color: #FF0000; 49 | } 50 | &.sent .username { 51 | color: #0000FF; 52 | } 53 | } 54 | 55 | .message-enter { 56 | opacity: 0.01; 57 | transform: translateY(10px); 58 | } 59 | 60 | .message-enter-active { 61 | opacity: 1; 62 | transform: translateY(0); 63 | transition: opacity .1s ease-in, transform .1s ease-in; 64 | } 65 | 66 | .message-exit { 67 | opacity: 1; 68 | transform: translateY(0); 69 | } 70 | 71 | .message-exit-active { 72 | opacity: 0.01; 73 | transform: translateY(10px); 74 | transition: opacity .1s ease-in, transform .1s ease-in; 75 | } 76 | 77 | .activeTyping { 78 | position: absolute; 79 | left: 20px; 80 | bottom: 70px; 81 | font-size: 12px; 82 | color: #FF0000; 83 | display: none; 84 | @media (min-width: $tablet + px) { 85 | bottom: 117px; 86 | } 87 | @media (min-width: $desktop + px) { 88 | bottom: 70px; 89 | } 90 | &.visible { 91 | display: block; 92 | } 93 | } 94 | 95 | .userInput { 96 | position: relative; 97 | width: calc(100% - 13px); 98 | input { 99 | width: 100%; 100 | padding: 8px; 101 | font-size: 16px; 102 | border: none; 103 | @media (min-width: $desktop + px) { 104 | padding: 9px 12px 8px; 105 | font-size: 14px; 106 | } 107 | } 108 | .buttonWrapper { 109 | background: #fff; 110 | max-width: 100%; 111 | overflow: scroll; 112 | -webkit-overflow-scrolling: touch; 113 | 114 | > div { 115 | white-space:nowrap; 116 | } 117 | } 118 | button { 119 | margin: 5px; 120 | cursor: pointer; 121 | } 122 | 123 | .field { 124 | width: calc(100% - 122px); 125 | margin-left: 3px; 126 | margin-top: 10px; 127 | margin-bottom: 2px; 128 | padding: 3px 0 0 3px; 129 | } 130 | .option-toggle { 131 | position: absolute !important; 132 | right: 0; 133 | top: 2px; 134 | width: 100px; 135 | &:active { 136 | top: 4px; 137 | } 138 | } 139 | 140 | &.hidden { 141 | .field * { 142 | opacity: 0; 143 | pointer-events: none; 144 | } 145 | .option-toggle { 146 | pointer-events: none; 147 | cursor: auto; 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font-family: "Anonymous Pro", monospace; 4 | } 5 | @media (min-width: 1025px) { 6 | html, 7 | body { 8 | width: 100%; 9 | height: 100%; 10 | overflow: hidden; 11 | } 12 | } 13 | 14 | html { 15 | background: #000; 16 | } 17 | 18 | .shutDownPage { 19 | position: fixed; 20 | top: 0; 21 | left: 0; 22 | width: 100%; 23 | height: 100%; 24 | background: #000; 25 | opacity: 0; 26 | transition: opacity 0.2s ease-in-out; 27 | pointer-events: none; 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | } 32 | .shutDownPage.visible { 33 | opacity: 1; 34 | pointer-events: auto; 35 | z-index: 5; 36 | } 37 | 38 | .desktop { 39 | position: fixed; 40 | top: 0; 41 | left: 0; 42 | right: 0; 43 | bottom: 0; 44 | overflow: hidden; 45 | background-repeat: no-repeat; 46 | background-position: center; 47 | background-size: cover; 48 | } 49 | @media (min-width: 1025px) { 50 | .desktop { 51 | position: static; 52 | height: 100vh; 53 | } 54 | } 55 | .desktop .icons { 56 | position: absolute; 57 | top: 38px; 58 | left: 45px; 59 | } 60 | .desktop .icons a, 61 | .desktop .icons button { 62 | text-decoration: none; 63 | text-align: center; 64 | display: block; 65 | margin-bottom: 50px; 66 | color: #fff; 67 | text-shadow: 1px 2px 0 rgba(0, 0, 0, 0.5); 68 | } 69 | .desktop .icons img { 70 | width: 60px; 71 | display: block; 72 | margin-bottom: 15px; 73 | } 74 | 75 | .program { 76 | position: absolute; 77 | width: 100%; 78 | cursor: default; 79 | padding-bottom: 9px; 80 | display: inline-block; 81 | } 82 | @media (max-width: 1024px) { 83 | .program { 84 | transform: none !important; 85 | } 86 | } 87 | @media (min-width: 1025px) { 88 | .program { 89 | max-width: 100%; 90 | width: auto; 91 | } 92 | } 93 | .program:not(.notification) { 94 | height: 100%; 95 | } 96 | @media (min-width: 1025px) { 97 | .program:not(.notification) { 98 | height: auto; 99 | } 100 | } 101 | .program:not(.system) .content { 102 | background: #fff; 103 | } 104 | .program .content { 105 | margin-top: 6px; 106 | width: calc(100% - 13px); 107 | padding: 10px; 108 | margin-left: 3px; 109 | height: calc(100% - 44px); 110 | } 111 | @media (min-width: 750px) { 112 | .program .content { 113 | height: calc(100% - 96px); 114 | } 115 | } 116 | @media (min-width: 1025px) { 117 | .program .content { 118 | height: auto; 119 | } 120 | } 121 | .program .content[contentEditable=true] { 122 | overflow: scroll; 123 | } 124 | @media (min-width: 1025px) { 125 | .program .content[contentEditable=true] { 126 | max-height: 250px; 127 | } 128 | } 129 | .program .content h3 { 130 | margin-bottom: 10px; 131 | line-height: 1.2; 132 | } 133 | .program.previous-active { 134 | z-index: 1; 135 | } 136 | .program.active { 137 | z-index: 2; 138 | } 139 | 140 | [data-view=closed], 141 | [data-view=minimized] { 142 | display: none; 143 | } 144 | 145 | @media (min-width: 1025px) { 146 | .txt-file { 147 | width: 400px; 148 | } 149 | .txt-file .content { 150 | max-height: 450px; 151 | } 152 | } 153 | .txt-file .content { 154 | overflow: scroll; 155 | -webkit-overflow-scrolling: touch; 156 | line-height: 1.3; 157 | } 158 | 159 | .work.program .content div:not(:last-child) { 160 | margin-bottom: 30px; 161 | } 162 | 163 | /*# sourceMappingURL=App.css.map */ 164 | -------------------------------------------------------------------------------- /src/components/startbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { apps, icons } from '../config'; 4 | 5 | // Components 6 | import Clock from './clock'; 7 | 8 | import '../css/startbar.css'; 9 | 10 | // Assets 11 | import flag from '../images/flag.svg'; 12 | import computer from '../images/computer.svg'; 13 | import settings from '../images/settings.svg'; 14 | 15 | const StartBar = ({ 16 | openApps, 17 | shutDown, 18 | start, 19 | openStart, 20 | minimizedApps, 21 | updateStartbar, 22 | currentlyActiveApp, 23 | openSettings, 24 | }) => { 25 | const nextStatus = openStart ? 'close' : 'open'; 26 | 27 | return ( 28 | <> 29 |
30 | 38 |
39 | { 40 | Object.keys(apps).map((app) => { 41 | const className = apps[app].toLowerCase(); 42 | 43 | if (app === 'shutdowncomputer') return null; 44 | 45 | return ( 46 | 64 | ); 65 | }) 66 | } 67 |
68 | 69 |
70 | { 71 | openStart 72 | && ( 73 |
74 | 82 | 90 |
91 | ) 92 | } 93 | 94 | ); 95 | }; 96 | 97 | StartBar.defaultProps = { 98 | shutDown: () => { }, 99 | start: () => { }, 100 | updateStartbar: () => { }, 101 | openApps: [], 102 | openStart: false, 103 | minimizedApps: [], 104 | currentlyActiveApp: '', 105 | openSettings: () => { }, 106 | }; 107 | 108 | StartBar.propTypes = { 109 | openApps: PropTypes.array, 110 | shutDown: PropTypes.func, 111 | start: PropTypes.func, 112 | openStart: PropTypes.bool, 113 | minimizedApps: PropTypes.array, 114 | updateStartbar: PropTypes.func, 115 | currentlyActiveApp: PropTypes.string, 116 | openSettings: PropTypes.func, 117 | }; 118 | 119 | export default StartBar; 120 | -------------------------------------------------------------------------------- /src/components/settings.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { apps, icons, systemSettings } from '../config'; 4 | 5 | // Components 6 | import Program from './program'; 7 | 8 | import '../css/settings.css'; 9 | 10 | const Settings = ({ 11 | updateActiveApp, 12 | updateStartbar, 13 | closeApp, 14 | openApps, 15 | minimizedApps, 16 | currentlyActiveApp, 17 | previouslyActiveApp, 18 | activeSystemSettings, 19 | changeSystemSettings, 20 | }) => ( 21 | 33 |
34 |

Background

35 |
    36 | { 37 | systemSettings.background.map((background) => { 38 | let className = 'option'; 39 | 40 | if (background.name === activeSystemSettings.background.name) { 41 | className += ' selected'; 42 | } 43 | 44 | return ( 45 |
  • 49 | 50 |
  • 51 | ); 52 | }) 53 | } 54 |
55 |
56 | 57 |
58 |

Appearance

59 |
    60 | { 61 | systemSettings.theme.map((theme) => { 62 | let className = 'option'; 63 | 64 | if (theme === activeSystemSettings.theme) { 65 | className += ' selected'; 66 | } 67 | 68 | return ( 69 |
  • 73 | 74 |
  • 75 | ); 76 | }) 77 | } 78 |
79 |
80 | 81 |
82 |

Attributions

83 |

84 | Emoji artwork is provided by 85 | {' '} 86 | Emojitwo 87 | , originally released as 88 | {' '} 89 | Emojione 2.2 90 | {' '} 91 | by 92 | {' '} 93 | Ranks.com 94 | {' '} 95 | with contributions from the Emojitwo community and is licensed under 96 | {' '} 97 | CC-BY 4.0. 98 |

99 |
100 |
101 | ); 102 | 103 | Settings.defaultProps = { 104 | updateActiveApp: () => {}, 105 | updateStartbar: () => {}, 106 | closeApp: () => {}, 107 | openApps: [], 108 | minimizedApps: [], 109 | currentlyActiveApp: '', 110 | previouslyActiveApp: '', 111 | activeSystemSettings: {}, 112 | changeSystemSettings: () => {}, 113 | }; 114 | 115 | Settings.propTypes = { 116 | updateActiveApp: PropTypes.func, 117 | updateStartbar: PropTypes.func, 118 | closeApp: PropTypes.func, 119 | openApps: PropTypes.array, 120 | minimizedApps: PropTypes.array, 121 | currentlyActiveApp: PropTypes.string, 122 | previouslyActiveApp: PropTypes.string, 123 | activeSystemSettings: PropTypes.object, 124 | changeSystemSettings: PropTypes.func, 125 | }; 126 | 127 | export default Settings; 128 | -------------------------------------------------------------------------------- /public/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 25 | 52 | 55 | 58 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import bot from './images/bot.svg'; 2 | import email from './images/email.svg'; 3 | import briefcase from './images/briefcase.svg'; 4 | import settings from './images/settings.svg'; 5 | import moon from './images/moon.svg'; 6 | import backgroundClassic from './images/backgrounds/classic.jpg'; 7 | import backgroundXp from './images/backgrounds/xp.jpg'; 8 | import backgroundNasa from './images/backgrounds/nasa.jpg'; 9 | import backgroundClouds from './images/backgrounds/clouds.jpg'; 10 | import backgroundLogo from './images/backgrounds/logo.jpg'; 11 | import backgroundVaporwave from './images/backgrounds/vaporwave.jpg'; 12 | 13 | export const apps = { 14 | messenger: 'Chat', 15 | work: 'Work', 16 | contact: 'Contact', 17 | shutdown: 'Shutdown', 18 | settings: 'Settings', 19 | }; 20 | 21 | export const icons = { 22 | chat: { 23 | url: bot, 24 | alt: 'Icon of bot', 25 | }, 26 | work: { 27 | url: briefcase, 28 | alt: 'Icon of briefcase', 29 | }, 30 | contact: { 31 | url: email, 32 | alt: 'Icon of email', 33 | }, 34 | shutdown: { 35 | url: moon, 36 | alt: 'Icon of moon', 37 | }, 38 | settings: { 39 | url: settings, 40 | alt: 'Icon of settings', 41 | }, 42 | }; 43 | 44 | export const resumeLink = 'https://standardresume.co/heathervandervecht'; 45 | 46 | export const contact = { 47 | content: "Let's chat! It's a pretty safe bet that you're awesome, and I'm always looking to meet awesome people. I'd love to grab a coffee, or even a beer if that's what you're into - just shoot me a message!", 48 | emailLink: 'mailto:heathervandervecht@gmail.com', 49 | linkedin: 'https://linkedin.com/in/heathervandervecht', 50 | github: 'http://github.com/heathervv', 51 | }; 52 | 53 | export const work = [ 54 | { 55 | title: 'Mentoring', 56 | url: '//www.get-merit.com/p/heather-vandervecht', 57 | copy: 'Realizing that I\'ve been working in tech for over a decade, I wanted to help the next generation of developers and creative thinkers, so I started offering free mentorship sessions. If you\'re interested, feel free to drop some time in my calendar.', 58 | }, 59 | { 60 | title: 'Dialogue', 61 | url: '//dialogue.co/en/', 62 | copy: 'Currently at Dialogue, I\'m the tech lead and staff engineer for a team under the client stream. Like most product companies, what I\'m working on can change quickly, so if you\'re curious to hear more about what I do here, shoot me an email!', 63 | }, 64 | { 65 | title: 'Opencare: Patient Growth', 66 | url: '//opencare.com', 67 | copy: 'I spent most of my time with Opencare on the Patient Growth team as a tech lead. The mission for this product line was to connect patients to top-tier dentists in their neighbourhood based on their wants/needs. Operating in both Canada and the United States, we built and maintained a marketing site (opencare.com), a patient app, and a practice app. There was *a lot* we managed to build, but I\'m personally most proud of the completely rebuilt patient funnel (search.opencare.com/recommendation) which I built entirely in React to replace the original in AngularJS, and mentoring the team through improving QA processes, app accessibility, and feature scoping.', 68 | }, 69 | { 70 | title: 'Opencare: Revenue Cycle Management', 71 | copy: 'This was a product line to help practices manage patient insurance before and after their appointments. This was done through an internal product that enabled the operations team to work through cases. When I joined this product line, the relationship between product and operations was strained - the product team was operating under a "ship fast and often" ethos, which, while great, often disrupted the operations team as they had tight timelines and had a low risk threshold for bugs and mid-day releases. So I focused on improving this communication-breakdown by refreshing the bug process and introducing functional reviews with key stakeholders before releases.', 72 | }, 73 | { 74 | title: 'Telus Digital Life', 75 | url: '//telus.com/mobility/accessories', 76 | copy: 'Nascent worked closely with TELUS to innovate the accessory space, and as a part of Nascent I co-lead the team that worked on this project. The main goal we worked towards with TELUS.com/accessories was how to bring the incredible in-store experience they\'d already developed onto the website. It also presented some fantastic technical opportunities with features like real time inventory and a mobility-wide cart.', 77 | }, 78 | ]; 79 | 80 | export const initialResponse = "Hi there, I'm HeatherBot - the digital version of Heather! Thanks for stopping by for a chat. You can ask me anything using the \"Free type\" button below, but for now I've gone ahead and given you some quick select options to help get you started. Go ahead and ask me something!"; 81 | 82 | export const changeInputResponse = { 83 | free: 'Feel free to ask me whatever you want. 🚀', 84 | options: 'A little guidance never hurt anybody. 🔮', 85 | }; 86 | 87 | // TODO: update URLs to be correct 88 | const systemSettingsBackground = [ 89 | { 90 | name: 'Classic', 91 | url: backgroundClassic, 92 | }, 93 | { 94 | name: 'XP', 95 | url: backgroundXp, 96 | }, 97 | { 98 | name: 'Nasa', 99 | url: backgroundNasa, 100 | }, 101 | { 102 | name: 'Clouds', 103 | url: backgroundClouds, 104 | }, 105 | { 106 | name: 'Logo', 107 | url: backgroundLogo, 108 | }, 109 | { 110 | name: 'Vaporwave', 111 | url: backgroundVaporwave, 112 | }, 113 | ]; 114 | 115 | const systemSettingsTheme = ['Light', 'Dark']; 116 | 117 | export const systemSettings = { 118 | background: systemSettingsBackground, 119 | theme: systemSettingsTheme, 120 | }; 121 | 122 | export const API = 'https://portfolio-chatbot-server.vercel.app/api/message'; 123 | -------------------------------------------------------------------------------- /src/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | /* Document 3 | ========================================================================== */ 4 | *, 5 | *:before, 6 | *:after { 7 | -webkit-box-sizing: border-box; 8 | -moz-box-sizing: border-box; 9 | box-sizing: border-box; 10 | } 11 | /** 12 | * 1. Correct the line height in all browsers. 13 | * 2. Prevent adjustments of font size after orientation changes in iOS. 14 | */ 15 | html { 16 | line-height: 1.15; 17 | /* 1 */ 18 | -webkit-text-size-adjust: 100%; 19 | /* 2 */ } 20 | 21 | /* Sections 22 | ========================================================================== */ 23 | /** 24 | * Remove the margin in all browsers. 25 | */ 26 | body { 27 | margin: 0; } 28 | 29 | /** 30 | * Correct the font size and margin on `h1` elements within `section` and 31 | * `article` contexts in Chrome, Firefox, and Safari. 32 | */ 33 | h1 { 34 | font-size: 2em; 35 | margin: 0.67em 0; } 36 | 37 | /* Grouping content 38 | ========================================================================== */ 39 | /** 40 | * 1. Add the correct box sizing in Firefox. 41 | * 2. Show the overflow in Edge and IE. 42 | */ 43 | hr { 44 | box-sizing: content-box; 45 | /* 1 */ 46 | height: 0; 47 | /* 1 */ 48 | overflow: visible; 49 | /* 2 */ } 50 | 51 | /** 52 | * 1. Correct the inheritance and scaling of font size in all browsers. 53 | * 2. Correct the odd `em` font sizing in all browsers. 54 | */ 55 | pre { 56 | font-family: monospace, monospace; 57 | /* 1 */ 58 | font-size: 1em; 59 | /* 2 */ } 60 | 61 | /* Text-level semantics 62 | ========================================================================== */ 63 | /** 64 | * Remove the gray background on active links in IE 10. 65 | */ 66 | a { 67 | background-color: transparent; } 68 | 69 | /** 70 | * 1. Remove the bottom border in Chrome 57- 71 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 72 | */ 73 | abbr[title] { 74 | border-bottom: none; 75 | /* 1 */ 76 | text-decoration: underline; 77 | /* 2 */ 78 | text-decoration: underline dotted; 79 | /* 2 */ } 80 | 81 | /** 82 | * Add the correct font weight in Chrome, Edge, and Safari. 83 | */ 84 | b, 85 | strong { 86 | font-weight: bolder; } 87 | 88 | /** 89 | * 1. Correct the inheritance and scaling of font size in all browsers. 90 | * 2. Correct the odd `em` font sizing in all browsers. 91 | */ 92 | code, 93 | kbd, 94 | samp { 95 | font-family: monospace, monospace; 96 | /* 1 */ 97 | font-size: 1em; 98 | /* 2 */ } 99 | 100 | /** 101 | * Add the correct font size in all browsers. 102 | */ 103 | small { 104 | font-size: 80%; } 105 | 106 | /** 107 | * Prevent `sub` and `sup` elements from affecting the line height in 108 | * all browsers. 109 | */ 110 | sub, 111 | sup { 112 | font-size: 75%; 113 | line-height: 0; 114 | position: relative; 115 | vertical-align: baseline; } 116 | 117 | sub { 118 | bottom: -0.25em; } 119 | 120 | sup { 121 | top: -0.5em; } 122 | 123 | /* Embedded content 124 | ========================================================================== */ 125 | /** 126 | * Remove the border on images inside links in IE 10. 127 | */ 128 | img { 129 | border-style: none; } 130 | 131 | /* Forms 132 | ========================================================================== */ 133 | /** 134 | * 1. Change the font styles in all browsers. 135 | * 2. Remove the margin in Firefox and Safari. 136 | */ 137 | button, 138 | input, 139 | optgroup, 140 | select, 141 | textarea { 142 | font-family: inherit; 143 | /* 1 */ 144 | font-size: 100%; 145 | /* 1 */ 146 | line-height: 1.15; 147 | /* 1 */ 148 | margin: 0; 149 | /* 2 */ } 150 | 151 | /** 152 | * Show the overflow in IE. 153 | * 1. Show the overflow in Edge. 154 | */ 155 | button, 156 | input { 157 | /* 1 */ 158 | overflow: visible; } 159 | 160 | /** 161 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 162 | * 1. Remove the inheritance of text transform in Firefox. 163 | */ 164 | button, 165 | select { 166 | /* 1 */ 167 | text-transform: none; } 168 | 169 | /** 170 | * Correct the inability to style clickable types in iOS and Safari. 171 | */ 172 | button, 173 | [type="button"], 174 | [type="reset"], 175 | [type="submit"] { 176 | -webkit-appearance: none; 177 | background: none; 178 | border: none; 179 | text-decoration: none; 180 | padding: 0; } 181 | 182 | button { 183 | cursor: pointer; 184 | } 185 | 186 | /** 187 | * Remove the inner border and padding in Firefox. 188 | */ 189 | button::-moz-focus-inner, 190 | [type="button"]::-moz-focus-inner, 191 | [type="reset"]::-moz-focus-inner, 192 | [type="submit"]::-moz-focus-inner { 193 | border-style: none; 194 | padding: 0; } 195 | 196 | /** 197 | * Restore the focus styles unset by the previous rule. 198 | */ 199 | button:-moz-focusring, 200 | [type="button"]:-moz-focusring, 201 | [type="reset"]:-moz-focusring, 202 | [type="submit"]:-moz-focusring { 203 | outline: 1px dotted ButtonText; } 204 | 205 | /** 206 | * Correct the padding in Firefox. 207 | */ 208 | fieldset { 209 | padding: 0.35em 0.75em 0.625em; } 210 | 211 | /** 212 | * 1. Correct the text wrapping in Edge and IE. 213 | * 2. Correct the color inheritance from `fieldset` elements in IE. 214 | * 3. Remove the padding so developers are not caught out when they zero out 215 | * `fieldset` elements in all browsers. 216 | */ 217 | legend { 218 | box-sizing: border-box; 219 | /* 1 */ 220 | color: inherit; 221 | /* 2 */ 222 | display: table; 223 | /* 1 */ 224 | max-width: 100%; 225 | /* 1 */ 226 | padding: 0; 227 | /* 3 */ 228 | white-space: normal; 229 | /* 1 */ } 230 | 231 | /** 232 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 233 | */ 234 | progress { 235 | vertical-align: baseline; } 236 | 237 | /** 238 | * Remove the default vertical scrollbar in IE 10+. 239 | */ 240 | textarea { 241 | overflow: auto; } 242 | 243 | /** 244 | * 1. Add the correct box sizing in IE 10. 245 | * 2. Remove the padding in IE 10. 246 | */ 247 | [type="checkbox"], 248 | [type="radio"] { 249 | box-sizing: border-box; 250 | /* 1 */ 251 | padding: 0; 252 | /* 2 */ } 253 | 254 | /** 255 | * Correct the cursor style of increment and decrement buttons in Chrome. 256 | */ 257 | [type="number"]::-webkit-inner-spin-button, 258 | [type="number"]::-webkit-outer-spin-button { 259 | height: auto; } 260 | 261 | /** 262 | * 1. Correct the odd appearance in Chrome and Safari. 263 | * 2. Correct the outline style in Safari. 264 | */ 265 | [type="search"] { 266 | -webkit-appearance: textfield; 267 | /* 1 */ 268 | outline-offset: -2px; 269 | /* 2 */ } 270 | 271 | /** 272 | * Remove the inner padding in Chrome and Safari on macOS. 273 | */ 274 | [type="search"]::-webkit-search-decoration { 275 | -webkit-appearance: none; } 276 | 277 | /** 278 | * 1. Correct the inability to style clickable types in iOS and Safari. 279 | * 2. Change font properties to `inherit` in Safari. 280 | */ 281 | ::-webkit-file-upload-button { 282 | -webkit-appearance: button; 283 | /* 1 */ 284 | font: inherit; 285 | /* 2 */ } 286 | 287 | /* Interactive 288 | ========================================================================== */ 289 | /* 290 | * Add the correct display in Edge, IE 10+, and Firefox. 291 | */ 292 | details { 293 | display: block; } 294 | 295 | /* 296 | * Add the correct display in all browsers. 297 | */ 298 | summary { 299 | display: list-item; } 300 | 301 | /* Misc 302 | ========================================================================== */ 303 | /** 304 | * Add the correct display in IE 10+. 305 | */ 306 | template { 307 | display: none; } 308 | 309 | /** 310 | * Add the correct display in IE 10. 311 | */ 312 | [hidden] { 313 | display: none; } 314 | -------------------------------------------------------------------------------- /src/css/theme.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | $tablet: 750; 4 | $desktop: 1025; 5 | 6 | .button { 7 | font-family: 'Anonymous Pro', monospace; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | font-size: 16px; 12 | font-weight: bold; 13 | padding: 10px 15px 10px 13px; 14 | vertical-align: middle; 15 | -webkit-appearance: initial; 16 | border: none; 17 | 18 | &:active, 19 | &.active { 20 | padding-top: 12px; 21 | padding-bottom: 8px; 22 | } 23 | } 24 | 25 | .button-medium { 26 | font-family: 'Anonymous Pro', monospace; 27 | display: inline-block; 28 | font-size: 14px; 29 | font-weight: bold; 30 | padding: 7px 13px 6px; 31 | line-height: 1; 32 | 33 | &:active, 34 | &.active { 35 | position: relative; 36 | top: 2px; 37 | padding-top: 7px; 38 | padding-bottom: 5px; 39 | } 40 | } 41 | 42 | .button-small { 43 | display: inline-block; 44 | font-weight: bold; 45 | font-size: 13px; 46 | font-family: sans-serif; 47 | padding: 5px 8px; 48 | text-align: center; 49 | 50 | @media (min-width: $desktop + px) { 51 | min-width: 25px; 52 | min-height: 25px; 53 | } 54 | } 55 | 56 | .button-inset { 57 | display: inline-block; 58 | font-weight: bold; 59 | font-size: 13px; 60 | font-family: sans-serif; 61 | padding: 5px 8px; 62 | text-align: center; 63 | 64 | &.minimize { 65 | @media (max-width: $desktop - 1 + px) { 66 | padding: 8px 10px 5px; 67 | } 68 | } 69 | } 70 | 71 | .theme-light { 72 | 73 | .button .button-inset, 74 | .button-small, 75 | .button-medium { 76 | color: #000; 77 | background: #bdbdbd; 78 | 79 | &:active, 80 | &.active { 81 | background: #e6e6e6; 82 | } 83 | } 84 | 85 | .button { 86 | box-shadow: inset 3px 3px 0 rgba(#fff, .8), inset -3px -3px 0 rgba(#000, .25), 2px 2px 0 #000; 87 | 88 | &:active, 89 | &.active { 90 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #fff, inset -2px -2px 0 #ddd, inset 4px 4px 0 #888; 91 | } 92 | } 93 | 94 | .button-medium { 95 | box-shadow: 2px 2px #000, inset -2px -2px rgba(#000, .25), inset 2px 2px rgba(#fff, .8); 96 | 97 | &:active, 98 | &.active { 99 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #fff, inset -2px -2px 0 #ddd, inset 4px 4px 0 #888; 100 | } 101 | } 102 | 103 | .button-small { 104 | box-shadow: inset -2px -2px 0 rgba(#000, .25), inset 2px 2px 0 rgba(#fff, .8); 105 | 106 | &:active, 107 | &.active { 108 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #ddd, inset -2px -2px 0 #ddd; 109 | } 110 | } 111 | 112 | .button-inset { 113 | box-shadow: 2px 2px 0 rgba(255, 255, 255, 0.6), inset 2px 2px rgba(0, 0, 0, 0.5); 114 | 115 | &:active, 116 | &.active { 117 | background: #e6e6e6; 118 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #ddd, inset -2px -2px 0 #ddd; 119 | } 120 | } 121 | 122 | .startbar { 123 | background: #bdbdbd; 124 | box-shadow: inset 0 4px 0 rgba(#fff, .8); 125 | 126 | .programs:before { 127 | border-left: 2px solid rgba(#fff, .8); 128 | border-right: 2px solid rgba(#000, .25); 129 | } 130 | 131 | a, 132 | button { 133 | color: #000; 134 | } 135 | } 136 | 137 | .start-cupboard { 138 | background: #bdbdbd; 139 | box-shadow: -3px -3px 0 #f2f2f2, inset -3px -3px 0 rgba(#000, .25); 140 | 141 | a, 142 | button { 143 | color: #000; 144 | 145 | &:hover { 146 | background: blue; 147 | color: #fff; 148 | } 149 | } 150 | 151 | .shutdown { 152 | border-top: 1px solid #888; 153 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); 154 | } 155 | } 156 | 157 | .program { 158 | background: #bdbdbd; 159 | box-shadow: -3px -3px 0 #f2f2f2, inset -3px -3px 0 rgba(#000, .25); 160 | 161 | &:not(.system) { 162 | .content { 163 | box-shadow: 3px 3px 0 rgba(#fff, .6), inset 3px 3px rgba(#000, .5); 164 | } 165 | } 166 | } 167 | 168 | .toolbar button.disabled { 169 | color: color.adjust(#BDBDBD, $lightness: -15%); 170 | text-shadow: 1px 1px #fff; 171 | } 172 | 173 | .userInput { 174 | .field { 175 | box-shadow: 3px 3px 0 rgba(#fff, .6), inset 3px 3px rgba(#000, .50); 176 | } 177 | 178 | &.hidden { 179 | .option-toggle { 180 | color: color.adjust(#BDBDBD, $lightness: -15%); 181 | text-shadow: 1px 1px #fff; 182 | } 183 | } 184 | } 185 | 186 | .clock { 187 | color: #000; 188 | box-shadow: inset 3px 3px 0 rgba(#000, .25), 2px 2px 0 rgba(#fff, .8); 189 | } 190 | 191 | .settings, 192 | .notification { 193 | color: #000; 194 | } 195 | } 196 | 197 | .theme-dark { 198 | 199 | .button .button-inset, 200 | .button-small, 201 | .button-medium { 202 | color: #fff; 203 | background: #333; 204 | 205 | &:active, 206 | &.active { 207 | background: #000; 208 | } 209 | } 210 | 211 | .button { 212 | box-shadow: inset 3px 3px 0 rgba(#666, .8), inset -3px -3px 0 rgba(#000, .5), 2px 2px 0 #000; 213 | 214 | &:active, 215 | &.active { 216 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #555, inset -2px -2px 0 #777, inset 4px 4px 0 #474747; 217 | } 218 | } 219 | 220 | .button-medium { 221 | box-shadow: 2px 2px #000, inset -2px -2px rgba(#000, .5), inset 2px 2px rgba(#666, .8); 222 | 223 | &:active, 224 | &.active { 225 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #555, inset -2px -2px 0 #777, inset 4px 4px 0 #474747; 226 | } 227 | } 228 | 229 | .button-small { 230 | box-shadow: inset -2px -2px 0 rgba(#000, .5), inset 2px 2px 0 rgba(#666, .8); 231 | 232 | &:active, 233 | &.active { 234 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #555, inset -2px -2px 0 #666; 235 | } 236 | } 237 | 238 | .button-inset { 239 | box-shadow: 2px 2px 0 rgba(#666, 0.6), inset 2px 2px rgba(#000, 0.75); 240 | 241 | &:active, 242 | &.active { 243 | background: #e6e6e6; 244 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #666, inset -2px -2px 0 #666; 245 | } 246 | } 247 | 248 | .startbar { 249 | background: #333; 250 | box-shadow: inset 0 4px 0 #666; 251 | 252 | .programs:before { 253 | border-left: 2px solid rgba(#666, .8); 254 | border-right: 2px solid rgba(#000, .5); 255 | } 256 | 257 | a, 258 | button { 259 | color: #fff; 260 | } 261 | } 262 | 263 | .start-cupboard { 264 | background: #333; 265 | box-shadow: -3px -3px 0 #666, inset -3px -3px 0 rgba(#000, .5); 266 | 267 | a, 268 | button { 269 | color: #fff; 270 | 271 | &:hover { 272 | background: blue; 273 | color: #fff; 274 | } 275 | } 276 | 277 | .shutdown { 278 | border-top: 1px solid #000; 279 | box-shadow: inset 0 1px 0 rgba(#666, 0.8); 280 | } 281 | } 282 | 283 | .program { 284 | background: #333; 285 | box-shadow: -3px -3px 0 #666, inset -3px -3px 0 rgba(#000, .5); 286 | 287 | &:not(.system) { 288 | .content { 289 | box-shadow: 3px 3px 0 rgba(#000, .6), inset 3px 3px rgba(#666, .5); 290 | } 291 | } 292 | } 293 | 294 | .toolbar button.disabled { 295 | color: color.adjust(#333, $lightness: -10%); 296 | text-shadow: 1px 1px #666; 297 | } 298 | 299 | .userInput { 300 | .field { 301 | box-shadow: 3px 3px 0 rgba(#666, .6), inset 3px 3px rgba(#000, .75); 302 | } 303 | 304 | &.hidden { 305 | .option-toggle { 306 | color: color.adjust(#333, $lightness: -10%); 307 | text-shadow: 1px 1px #ccc; 308 | } 309 | } 310 | } 311 | 312 | .clock { 313 | color: #fff; 314 | box-shadow: inset 3px 3px 0 rgba(#000, .5), 2px 2px 0 rgba(#666, .8); 315 | } 316 | 317 | .settings, 318 | .notification { 319 | color: #fff; 320 | 321 | a { 322 | background: #bdbdbd; 323 | padding: 2px; 324 | } 325 | } 326 | } -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import './App.css'; 3 | 4 | import { 5 | apps, icons, resumeLink, systemSettings as systemConfig, 6 | } from './config'; 7 | import StartBar from './components/startbar'; 8 | import Messenger from './components/messenger'; 9 | import Work from './components/work'; 10 | import Contact from './components/contact'; 11 | import Settings from './components/settings'; 12 | import ShutDown from './components/shutDown'; 13 | import Screensaver from './components/screensaver'; 14 | 15 | import resume from './images/resume.svg'; 16 | 17 | import './css/theme.css'; 18 | 19 | const programComponents = { 20 | chat: Messenger, 21 | work: Work, 22 | contact: Contact, 23 | settings: Settings, 24 | }; 25 | 26 | const App = () => { 27 | const loadSystemBackground = () => { 28 | const existingBackground = localStorage.getItem('background'); 29 | 30 | if (existingBackground) { 31 | return systemConfig.background.find((background) => background.name === existingBackground); 32 | } 33 | 34 | localStorage.setItem('background', systemConfig.background[2].name); 35 | return systemConfig.background[2]; 36 | }; 37 | 38 | const loadSystemTheme = () => { 39 | const existingTheme = localStorage.getItem('theme'); 40 | 41 | if (existingTheme) { 42 | return systemConfig.theme.find((theme) => theme === existingTheme); 43 | } 44 | 45 | localStorage.setItem('theme', systemConfig.theme[0]); 46 | return systemConfig.theme[0]; 47 | }; 48 | 49 | const openInNewTab = (elem) => { 50 | const win = window.open(elem, '_blank'); 51 | 52 | if (win) win.focus(); 53 | }; 54 | 55 | const linkClickListener = (e) => { 56 | const event = window.e || e; 57 | 58 | if (event.target.tagName === 'A') { 59 | openInNewTab(event.target.href); 60 | } 61 | }; 62 | 63 | const [shutDown, setShutDown] = useState(false); 64 | const [openApps, setOpenApps] = useState([apps.messenger.toLowerCase()]); 65 | const [minimizedApps, setMinimizedApps] = useState([]); 66 | const [openStart, setOpenStart] = useState(false); 67 | const [currentlyActiveApp, setCurrentlyActiveApp] = useState(apps.messenger.toLowerCase()); 68 | const [previouslyActiveApp, setPreviouslyActiveApp] = useState(''); 69 | const [systemSettings, setSystemSettings] = useState({ 70 | background: loadSystemBackground(), 71 | theme: loadSystemTheme(), 72 | }); 73 | 74 | useEffect(() => { 75 | if (document.addEventListener) { 76 | document.addEventListener('click', linkClickListener, false); 77 | } else { 78 | document.attachEvent('onclick', linkClickListener); 79 | } 80 | }, []); 81 | 82 | const start = (status) => { 83 | setOpenStart(status !== 'close'); 84 | }; 85 | 86 | const updateActiveApp = (e, component) => { 87 | if (e) e.preventDefault(); 88 | 89 | if (component === currentlyActiveApp) return; 90 | 91 | setPreviouslyActiveApp(currentlyActiveApp); 92 | setCurrentlyActiveApp(component); 93 | }; 94 | 95 | const openApp = (e, component) => { 96 | if (e) e.preventDefault(); 97 | 98 | const updatedMinimizedApps = [...minimizedApps]; 99 | if (updatedMinimizedApps.indexOf(component) > -1) { 100 | for (let i = updatedMinimizedApps.length - 1; i >= 0; i -= 1) { 101 | if (updatedMinimizedApps[i] === component) { 102 | updatedMinimizedApps.splice(i, 1); 103 | } 104 | } 105 | } 106 | 107 | setOpenApps([...openApps, component]); 108 | setMinimizedApps(updatedMinimizedApps); 109 | 110 | updateActiveApp(null, component); 111 | start('close'); 112 | }; 113 | 114 | const closeApp = (e, component) => { 115 | if (e) e.preventDefault(); 116 | 117 | setOpenApps(openApps.filter((a) => a !== component)); 118 | }; 119 | 120 | const updateStartbar = (component, minimizeWindow) => { 121 | const updatedMinimizedApps = [...minimizedApps]; 122 | if (minimizeWindow) { 123 | // if we manually ask to minimize 124 | updatedMinimizedApps.push(component); 125 | } else if (updatedMinimizedApps.indexOf(component) > -1) { 126 | // if app is currently minimized and needs to be brought back 127 | const index = updatedMinimizedApps.indexOf(component); 128 | updatedMinimizedApps.splice(index, 1); 129 | 130 | updateActiveApp(null, component); 131 | } else { 132 | // Otherwise, let's just set to currently active app 133 | updateActiveApp(null, component); 134 | } 135 | 136 | setMinimizedApps(updatedMinimizedApps); 137 | start('close'); 138 | }; 139 | 140 | const triggerShutdown = (e, restart = false) => { 141 | if (e) e.preventDefault(); 142 | 143 | setShutDown(!restart); 144 | setOpenStart(false); 145 | setOpenApps([]); 146 | setMinimizedApps([]); 147 | setCurrentlyActiveApp(''); 148 | setPreviouslyActiveApp(''); 149 | }; 150 | 151 | const changeSystemSettings = (background = null, theme = null) => { 152 | if (background) localStorage.setItem('background', background.name); 153 | 154 | if (theme) localStorage.setItem('theme', theme); 155 | 156 | setSystemSettings({ 157 | background: background || systemSettings.background, 158 | theme: theme || systemSettings.theme, 159 | }); 160 | }; 161 | 162 | return ( 163 |
164 |
165 | 173 | 181 | 186 | 187 | Icon of resume 188 | {' '} 189 | Resume 190 | 191 |
192 | 193 | { 194 | Object.keys(programComponents).map((program) => { 195 | if ( 196 | openApps.indexOf(program) === -1 197 | && minimizedApps.indexOf(program) === -1 198 | ) return null; 199 | 200 | const ProgramBlock = programComponents[program]; 201 | 202 | return ( 203 | 215 | ); 216 | }) 217 | } 218 | 219 | openApp(e, apps.settings.toLowerCase())} 229 | /> 230 | 231 |
232 | triggerShutdown(null, true)} /> 233 |
234 | 235 | 236 |
237 | ); 238 | }; 239 | 240 | export default App; 241 | -------------------------------------------------------------------------------- /src/css/theme.css: -------------------------------------------------------------------------------- 1 | .button { 2 | font-family: "Anonymous Pro", monospace; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | font-size: 16px; 7 | font-weight: bold; 8 | padding: 10px 15px 10px 13px; 9 | vertical-align: middle; 10 | -webkit-appearance: initial; 11 | border: none; 12 | } 13 | .button:active, .button.active { 14 | padding-top: 12px; 15 | padding-bottom: 8px; 16 | } 17 | 18 | .button-medium { 19 | font-family: "Anonymous Pro", monospace; 20 | display: inline-block; 21 | font-size: 14px; 22 | font-weight: bold; 23 | padding: 7px 13px 6px; 24 | line-height: 1; 25 | } 26 | .button-medium:active, .button-medium.active { 27 | position: relative; 28 | top: 2px; 29 | padding-top: 7px; 30 | padding-bottom: 5px; 31 | } 32 | 33 | .button-small { 34 | display: inline-block; 35 | font-weight: bold; 36 | font-size: 13px; 37 | font-family: sans-serif; 38 | padding: 5px 8px; 39 | text-align: center; 40 | } 41 | @media (min-width: 1025px) { 42 | .button-small { 43 | min-width: 25px; 44 | min-height: 25px; 45 | } 46 | } 47 | 48 | .button-inset { 49 | display: inline-block; 50 | font-weight: bold; 51 | font-size: 13px; 52 | font-family: sans-serif; 53 | padding: 5px 8px; 54 | text-align: center; 55 | } 56 | @media (max-width: 1024px) { 57 | .button-inset.minimize { 58 | padding: 8px 10px 5px; 59 | } 60 | } 61 | 62 | .theme-light .button .button-inset, 63 | .theme-light .button-small, 64 | .theme-light .button-medium { 65 | color: #000; 66 | background: #bdbdbd; 67 | } 68 | .theme-light .button .button-inset:active, .theme-light .button .button-inset.active, 69 | .theme-light .button-small:active, 70 | .theme-light .button-small.active, 71 | .theme-light .button-medium:active, 72 | .theme-light .button-medium.active { 73 | background: #e6e6e6; 74 | } 75 | .theme-light .button { 76 | box-shadow: inset 3px 3px 0 rgba(255, 255, 255, 0.8), inset -3px -3px 0 rgba(0, 0, 0, 0.25), 2px 2px 0 #000; 77 | } 78 | .theme-light .button:active, .theme-light .button.active { 79 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #fff, inset -2px -2px 0 #ddd, inset 4px 4px 0 #888; 80 | } 81 | .theme-light .button-medium { 82 | box-shadow: 2px 2px #000, inset -2px -2px rgba(0, 0, 0, 0.25), inset 2px 2px rgba(255, 255, 255, 0.8); 83 | } 84 | .theme-light .button-medium:active, .theme-light .button-medium.active { 85 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #fff, inset -2px -2px 0 #ddd, inset 4px 4px 0 #888; 86 | } 87 | .theme-light .button-small { 88 | box-shadow: inset -2px -2px 0 rgba(0, 0, 0, 0.25), inset 2px 2px 0 rgba(255, 255, 255, 0.8); 89 | } 90 | .theme-light .button-small:active, .theme-light .button-small.active { 91 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #ddd, inset -2px -2px 0 #ddd; 92 | } 93 | .theme-light .button-inset { 94 | box-shadow: 2px 2px 0 rgba(255, 255, 255, 0.6), inset 2px 2px rgba(0, 0, 0, 0.5); 95 | } 96 | .theme-light .button-inset:active, .theme-light .button-inset.active { 97 | background: #e6e6e6; 98 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #ddd, inset -2px -2px 0 #ddd; 99 | } 100 | .theme-light .startbar { 101 | background: #bdbdbd; 102 | box-shadow: inset 0 4px 0 rgba(255, 255, 255, 0.8); 103 | } 104 | .theme-light .startbar .programs:before { 105 | border-left: 2px solid rgba(255, 255, 255, 0.8); 106 | border-right: 2px solid rgba(0, 0, 0, 0.25); 107 | } 108 | .theme-light .startbar a, 109 | .theme-light .startbar button { 110 | color: #000; 111 | } 112 | .theme-light .start-cupboard { 113 | background: #bdbdbd; 114 | box-shadow: -3px -3px 0 #f2f2f2, inset -3px -3px 0 rgba(0, 0, 0, 0.25); 115 | } 116 | .theme-light .start-cupboard a, 117 | .theme-light .start-cupboard button { 118 | color: #000; 119 | } 120 | .theme-light .start-cupboard a:hover, 121 | .theme-light .start-cupboard button:hover { 122 | background: blue; 123 | color: #fff; 124 | } 125 | .theme-light .start-cupboard .shutdown { 126 | border-top: 1px solid #888; 127 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8); 128 | } 129 | .theme-light .program { 130 | background: #bdbdbd; 131 | box-shadow: -3px -3px 0 #f2f2f2, inset -3px -3px 0 rgba(0, 0, 0, 0.25); 132 | } 133 | .theme-light .program:not(.system) .content { 134 | box-shadow: 3px 3px 0 rgba(255, 255, 255, 0.6), inset 3px 3px rgba(0, 0, 0, 0.5); 135 | } 136 | .theme-light .toolbar button.disabled { 137 | color: rgb(150.75, 150.75, 150.75); 138 | text-shadow: 1px 1px #fff; 139 | } 140 | .theme-light .userInput .field { 141 | box-shadow: 3px 3px 0 rgba(255, 255, 255, 0.6), inset 3px 3px rgba(0, 0, 0, 0.5); 142 | } 143 | .theme-light .userInput.hidden .option-toggle { 144 | color: rgb(150.75, 150.75, 150.75); 145 | text-shadow: 1px 1px #fff; 146 | } 147 | .theme-light .clock { 148 | color: #000; 149 | box-shadow: inset 3px 3px 0 rgba(0, 0, 0, 0.25), 2px 2px 0 rgba(255, 255, 255, 0.8); 150 | } 151 | .theme-light .settings, 152 | .theme-light .notification { 153 | color: #000; 154 | } 155 | 156 | .theme-dark .button .button-inset, 157 | .theme-dark .button-small, 158 | .theme-dark .button-medium { 159 | color: #fff; 160 | background: #333; 161 | } 162 | .theme-dark .button .button-inset:active, .theme-dark .button .button-inset.active, 163 | .theme-dark .button-small:active, 164 | .theme-dark .button-small.active, 165 | .theme-dark .button-medium:active, 166 | .theme-dark .button-medium.active { 167 | background: #000; 168 | } 169 | .theme-dark .button { 170 | box-shadow: inset 3px 3px 0 rgba(102, 102, 102, 0.8), inset -3px -3px 0 rgba(0, 0, 0, 0.5), 2px 2px 0 #000; 171 | } 172 | .theme-dark .button:active, .theme-dark .button.active { 173 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #555, inset -2px -2px 0 #777, inset 4px 4px 0 #474747; 174 | } 175 | .theme-dark .button-medium { 176 | box-shadow: 2px 2px #000, inset -2px -2px rgba(0, 0, 0, 0.5), inset 2px 2px rgba(102, 102, 102, 0.8); 177 | } 178 | .theme-dark .button-medium:active, .theme-dark .button-medium.active { 179 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #555, inset -2px -2px 0 #777, inset 4px 4px 0 #474747; 180 | } 181 | .theme-dark .button-small { 182 | box-shadow: inset -2px -2px 0 rgba(0, 0, 0, 0.5), inset 2px 2px 0 rgba(102, 102, 102, 0.8); 183 | } 184 | .theme-dark .button-small:active, .theme-dark .button-small.active { 185 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #555, inset -2px -2px 0 #666; 186 | } 187 | .theme-dark .button-inset { 188 | box-shadow: 2px 2px 0 rgba(102, 102, 102, 0.6), inset 2px 2px rgba(0, 0, 0, 0.75); 189 | } 190 | .theme-dark .button-inset:active, .theme-dark .button-inset.active { 191 | background: #e6e6e6; 192 | box-shadow: inset 2px 2px 0 #000, 1px 1px 0 #666, inset -2px -2px 0 #666; 193 | } 194 | .theme-dark .startbar { 195 | background: #333; 196 | box-shadow: inset 0 4px 0 #666; 197 | } 198 | .theme-dark .startbar .programs:before { 199 | border-left: 2px solid rgba(102, 102, 102, 0.8); 200 | border-right: 2px solid rgba(0, 0, 0, 0.5); 201 | } 202 | .theme-dark .startbar a, 203 | .theme-dark .startbar button { 204 | color: #fff; 205 | } 206 | .theme-dark .start-cupboard { 207 | background: #333; 208 | box-shadow: -3px -3px 0 #666, inset -3px -3px 0 rgba(0, 0, 0, 0.5); 209 | } 210 | .theme-dark .start-cupboard a, 211 | .theme-dark .start-cupboard button { 212 | color: #fff; 213 | } 214 | .theme-dark .start-cupboard a:hover, 215 | .theme-dark .start-cupboard button:hover { 216 | background: blue; 217 | color: #fff; 218 | } 219 | .theme-dark .start-cupboard .shutdown { 220 | border-top: 1px solid #000; 221 | box-shadow: inset 0 1px 0 rgba(102, 102, 102, 0.8); 222 | } 223 | .theme-dark .program { 224 | background: #333; 225 | box-shadow: -3px -3px 0 #666, inset -3px -3px 0 rgba(0, 0, 0, 0.5); 226 | } 227 | .theme-dark .program:not(.system) .content { 228 | box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.6), inset 3px 3px rgba(102, 102, 102, 0.5); 229 | } 230 | .theme-dark .toolbar button.disabled { 231 | color: rgb(25.5, 25.5, 25.5); 232 | text-shadow: 1px 1px #666; 233 | } 234 | .theme-dark .userInput .field { 235 | box-shadow: 3px 3px 0 rgba(102, 102, 102, 0.6), inset 3px 3px rgba(0, 0, 0, 0.75); 236 | } 237 | .theme-dark .userInput.hidden .option-toggle { 238 | color: rgb(25.5, 25.5, 25.5); 239 | text-shadow: 1px 1px #ccc; 240 | } 241 | .theme-dark .clock { 242 | color: #fff; 243 | box-shadow: inset 3px 3px 0 rgba(0, 0, 0, 0.5), 2px 2px 0 rgba(102, 102, 102, 0.8); 244 | } 245 | .theme-dark .settings, 246 | .theme-dark .notification { 247 | color: #fff; 248 | } 249 | .theme-dark .settings a, 250 | .theme-dark .notification a { 251 | background: #bdbdbd; 252 | padding: 2px; 253 | } 254 | 255 | /*# sourceMappingURL=theme.css.map */ 256 | -------------------------------------------------------------------------------- /src/components/messenger.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 2 | /* eslint-disable jsx-a11y/no-autofocus */ 3 | /* Event handler added to div for delight, not actual functionality */ 4 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 5 | import React, { useState, useEffect, useRef } from 'react'; 6 | import PropTypes from 'prop-types'; 7 | import Axios from 'axios'; 8 | import { TransitionGroup, CSSTransition } from 'react-transition-group'; 9 | import Draggable from 'react-draggable'; 10 | import { v4 as uuidv4 } from 'uuid'; 11 | import { 12 | apps, 13 | icons, 14 | initialResponse, 15 | changeInputResponse, 16 | API, 17 | } from '../config'; 18 | 19 | // Components 20 | import Toolbar from './toolbar'; 21 | import Message from './message'; 22 | 23 | import '../css/messenger.css'; 24 | 25 | const username = `Anon${Math.floor(Math.random() * (9999 - 1000) + 1000)}`; 26 | 27 | const Messenger = ({ 28 | updateActiveApp, 29 | closeApp, 30 | updateStartbar, 31 | openApps, 32 | minimizedApps, 33 | currentlyActiveApp, 34 | previouslyActiveApp, 35 | }) => { 36 | const botName = 'HeatherBot'; 37 | const messenger = apps.messenger.toLowerCase(); 38 | const dataView = (openApps.indexOf(messenger) === -1 || minimizedApps.indexOf(messenger) !== -1) ? 'closed' : ''; 39 | 40 | const messages = useRef(); 41 | 42 | const [chatHistory, setChatHistory] = useState([]); 43 | const [isTyping, setIsTyping] = useState(true); 44 | const [inputValue, setInputValue] = useState(''); 45 | const [curatedOptions, setCuratedOptions] = useState({ 46 | visible: false, 47 | links: [ 48 | 'Who are you?', 49 | 'Can I see your work?', 50 | 'What do you like to code in?', 51 | 'I\'d like to get in touch with you', 52 | ], 53 | }); 54 | 55 | // This is the final frontier. All message-based functions end with a call to this one 56 | // It updates the local array with whatever argument was passed to it 57 | const updateHistory = (message, id, user, bot = false) => { 58 | setChatHistory((prevChatHistory) => ([...prevChatHistory, { user, id, message, bot }])); 59 | }; 60 | 61 | useEffect(() => { 62 | // First message doesn't come from bot so we can introduce the app to the user 63 | setTimeout(() => { 64 | updateHistory(initialResponse, uuidv4(), botName, true); 65 | setIsTyping(false); 66 | setCuratedOptions({ 67 | ...curatedOptions, 68 | visible: true, 69 | }); 70 | }, 2000); 71 | }, []); 72 | 73 | useEffect(() => { 74 | // Always keep messenger window scrolled to last message 75 | if (openApps.indexOf(apps.messenger.toLowerCase()) !== -1) { 76 | messages.current.scrollTop = messages.current.scrollHeight; 77 | } 78 | }, [chatHistory, openApps]); 79 | 80 | // This method listens to any successful response from the bot 81 | const handleResponse = (response) => { 82 | const chatbotMessages = response.data.response; 83 | let delay = 1000; 84 | 85 | for (let i = 0; i < chatbotMessages.length; i += 1) { 86 | delay += i > 0 ? Math.floor(Math.random() * 2000) + 1000 : 0; 87 | 88 | setTimeout(() => { 89 | updateHistory(chatbotMessages[i], uuidv4(), botName, true); 90 | 91 | // If we're on the last response, trigger next step 92 | if (i === chatbotMessages.length - 1) { 93 | setIsTyping(false); 94 | 95 | // If user has curated options turned on, check for any new ones from the bot 96 | if (curatedOptions.visible && response.data.followUp) { 97 | setCuratedOptions({ 98 | visible: true, 99 | links: response.data.followUp, 100 | }); 101 | } 102 | } 103 | }, delay); 104 | } 105 | }; 106 | 107 | // This method listens to any failed response from the bot 108 | const handleError = () => { 109 | updateHistory('Sorry, you\'ve found a flaw in my code. I\'ll take this opportunity to grow!', uuidv4(), botName, true); 110 | setIsTyping(false); 111 | }; 112 | 113 | const sendMessage = (event, directValue = null) => { 114 | if ((event && (event.keyCode === 13 || event.which === 13)) || directValue) { 115 | const message = directValue || inputValue; 116 | 117 | // Pass user message into state 118 | updateHistory(message, uuidv4(), username); 119 | 120 | // Send user message to analytics 121 | window.dataLayer.push({ event: 'dialogflow', message }); 122 | 123 | // Send off to bot 124 | setIsTyping(true); 125 | setInputValue(''); 126 | 127 | Axios.post(API, { message }) 128 | .then(handleResponse) 129 | .catch(handleError); 130 | } 131 | }; 132 | 133 | // Toggle for user to use preselected messages or type their own to the bot 134 | const changeInput = (option) => { 135 | updateHistory(changeInputResponse[option], uuidv4(), botName, true); 136 | 137 | setCuratedOptions({ 138 | ...curatedOptions, 139 | visible: !curatedOptions.visible, 140 | }); 141 | }; 142 | 143 | return ( 144 | 149 |
updateActiveApp(e, messenger)} 157 | data-view={dataView} 158 | > 159 | 166 | 167 |
168 | 169 | { 170 | chatHistory.map((item) => ( 171 | 172 | 178 | 179 | )) 180 | } 181 | 182 |
183 | 184 | Heather is typing... 185 | 186 |
187 |
188 | { 189 | curatedOptions.visible ? ( 190 |
191 |
192 | { 193 | curatedOptions.links.map((link) => ( 194 | 202 | )) 203 | } 204 |
205 |
206 | ) : ( 207 | setInputValue(e.target.value)} 213 | onKeyPress={sendMessage} 214 | /> 215 | ) 216 | } 217 |
218 | 231 |
232 |
233 |
234 | ); 235 | }; 236 | 237 | Messenger.defaultProps = { 238 | updateActiveApp: () => { }, 239 | closeApp: () => { }, 240 | updateStartbar: () => { }, 241 | openApps: [], 242 | minimizedApps: [], 243 | currentlyActiveApp: '', 244 | previouslyActiveApp: '', 245 | }; 246 | 247 | Messenger.propTypes = { 248 | updateActiveApp: PropTypes.func, 249 | closeApp: PropTypes.func, 250 | updateStartbar: PropTypes.func, 251 | openApps: PropTypes.array, 252 | minimizedApps: PropTypes.array, 253 | currentlyActiveApp: PropTypes.string, 254 | previouslyActiveApp: PropTypes.string, 255 | }; 256 | 257 | export default Messenger; 258 | --------------------------------------------------------------------------------