├── .firebaserc ├── .gitignore ├── README.md ├── firebase.json ├── package-lock.json ├── package.json ├── public ├── CV.pdf ├── MSc_Thesis.pdf ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.tsx ├── components │ ├── Banner.tsx │ ├── ErrorMessage.tsx │ ├── InputArea.tsx │ ├── Terminal.tsx │ ├── TerminalOutput.tsx │ └── WelcomeMessage.tsx ├── firebase.ts ├── index.css ├── index.tsx ├── react-app-env.d.ts └── serviceWorker.ts └── tsconfig.json /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "personal-website-6a2a9" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .firebase 26 | firebase-debug.log 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Personal Website 2 | 3 | My personal website built using React and TypeScript. 4 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "build", 4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 5 | "rewrites": [ 6 | { 7 | "source": "**", 8 | "destination": "/index.html" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "craigfeldman.com", 3 | "private": true, 4 | "dependencies": { 5 | "@types/node": "^17.0.31", 6 | "@types/react": "^18.0.8", 7 | "@types/react-dom": "^18.0.3", 8 | "firebase": "^9.7.0", 9 | "react": "^18.1.0", 10 | "react-dom": "^18.1.0", 11 | "react-scripts": "5.0.1", 12 | "typescript": "^4.6.4" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "deploy": "npm run build && firebase deploy", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/CV.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craig-feldman/personal-website-react/ea292811e9cb73bbaed53bb5ed04df9a1c3c8cca/public/CV.pdf -------------------------------------------------------------------------------- /public/MSc_Thesis.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craig-feldman/personal-website-react/ea292811e9cb73bbaed53bb5ed04df9a1c3c8cca/public/MSc_Thesis.pdf -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craig-feldman/personal-website-react/ea292811e9cb73bbaed53bb5ed04df9a1c3c8cca/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | craigfeldman.com 20 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craig-feldman/personal-website-react/ea292811e9cb73bbaed53bb5ed04df9a1c3c8cca/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craig-feldman/personal-website-react/ea292811e9cb73bbaed53bb5ed04df9a1c3c8cca/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Craig Feldman", 3 | "name": "Craig Feldman's website", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#171717" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .terminal-container { 2 | height: 100%; 3 | overflow-y: auto; 4 | overflow-x: hidden; 5 | cursor: text; 6 | background-color: #171717; 7 | } 8 | 9 | .terminal-content { 10 | padding: 20px; 11 | font-size: 15px; 12 | line-height: 20px; 13 | white-space: pre-wrap; 14 | color: #aaa; 15 | font-family: monospace; 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | .terminal-banner { 21 | color: teal; 22 | text-shadow: 0 0 5px teal; 23 | line-height: normal; 24 | font-weight: bold; 25 | font-size: calc(1vw + 7px); 26 | margin-bottom: 20px; 27 | } 28 | @media only screen and (max-width: 400px) { 29 | .terminal-banner { 30 | font-size: 8px; 31 | } 32 | } 33 | 34 | @media only screen and (max-width: 300px) { 35 | .terminal-banner { 36 | font-size: 6px; 37 | } 38 | } 39 | 40 | .terminal-welcome-message { 41 | margin-bottom: 20px; 42 | } 43 | 44 | .terminal-command-output { 45 | padding: 10px; 46 | max-width: 800px; 47 | } 48 | 49 | .terminal-command-output dt { 50 | color: #eeeeee; 51 | text-shadow: 0 0 4px #eeeeee; 52 | } 53 | 54 | .terminal-command-output dd { 55 | margin-inline-start: 20px; 56 | } 57 | 58 | .terminal-command-output dd:not(:last-child) { 59 | margin-block-end: 0.3em; 60 | } 61 | 62 | .terminal-command-output dd::before { 63 | content: "- "; 64 | } 65 | 66 | .terminal-command-output ul { 67 | margin-top: 0; 68 | } 69 | 70 | .terminal-command-record { 71 | scroll-margin: 15px; 72 | } 73 | 74 | .terminal-input-area { 75 | display: inline-flex; 76 | width: 100%; 77 | align-items: center; 78 | } 79 | 80 | .terminal-prompt { 81 | margin-right: 5px; 82 | } 83 | 84 | .terminal-heading::before { 85 | margin-right: 5px; 86 | content: '-- ' 87 | } 88 | 89 | .terminal-input { 90 | font-family: inherit; 91 | font-size: inherit; 92 | font-size: inherit; 93 | color: rgb(240, 191, 129); 94 | background: transparent; 95 | border: 0px; 96 | outline: none; /* no highlight on focus */ 97 | width: 100%; 98 | } 99 | 100 | .terminal-error-group { 101 | display: flex; 102 | flex-direction: column; 103 | } 104 | 105 | .terminal-error { 106 | color: red; 107 | } 108 | 109 | .terminal-glow { 110 | color: #eeeeee; 111 | text-shadow: 0 0 4px #eeeeee; 112 | } 113 | 114 | a { 115 | color: #dea5f5; 116 | } 117 | a:hover { 118 | color: black; 119 | background-color: #c4a5f5; 120 | text-decoration: none; 121 | } 122 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./App.css"; 3 | import Terminal from "./components/Terminal"; 4 | 5 | const getYear = () => { 6 | return new Date().getFullYear(); 7 | }; 8 | 9 | const welcomeMessage = `Welcome to my site fellow humans and bots. 10 | 11 | Type 'help' to view a list of available commands. 12 | `; 13 | 14 | const bannerCondensed = 15 | " _ __ _ _ \n" + 16 | " __ _ _ __ _(_)__ _ / _|___| |__| |_ __ __ _ _ _ \n" + 17 | "/ _| '_/ _` | / _` | | _/ -_) / _` | ' \\/ _` | ' \\ \n" + 18 | "\\__|_| \\__,_|_\\__, | |_| \\___|_\\__,_|_|_|_\\__,_|_||_|\n " + 19 | " |___/ \n" + 20 | " \u00A9 " + 21 | getYear(); 22 | 23 | const prompt = ">"; 24 | 25 | function App() { 26 | return ( 27 | 32 | ); 33 | } 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /src/components/Banner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type BannerProps = { 4 | banner: string; 5 | }; 6 | const Banner = (props: BannerProps) => ( 7 |
{props.banner}
8 | ); 9 | 10 | export default Banner; 11 | -------------------------------------------------------------------------------- /src/components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type ErrorMessageProps = { 4 | command: string; 5 | }; 6 | const ErrorMessage = (props: ErrorMessageProps) => { 7 | return ( 8 |
9 | 10 | {`command not found: ${props.command}.`} 11 | 12 | {`Type 'help' to view a list of available commands`} 13 |
14 | ); 15 | }; 16 | 17 | export default ErrorMessage; 18 | -------------------------------------------------------------------------------- /src/components/InputArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | type InputAreaProps = { 4 | terminalPrompt: string; 5 | setOutput: React.Dispatch>; 6 | processCommand: (input: string) => void; 7 | getHistory: (direction: "up" | "down") => string; 8 | getAutocomplete: (input: string) => string; 9 | inputRef: React.RefObject; 10 | }; 11 | const InputArea = (props: InputAreaProps) => { 12 | const [input, setInput] = useState(""); 13 | /** 14 | * Sets the input state to the value of the input. 15 | * 16 | * If the input is a period, we get the autocomplete for the input up to, but excluding the period. 17 | * We handle this case specially to allow autocomplete on mobile because KeyboardEvent.key tends to be 'unidentified' for inputs on mobile keyboards. 18 | */ 19 | const handleInputChange = (event: React.ChangeEvent) => { 20 | const inputValue = event.target.value; 21 | if (inputValue.charAt(inputValue.length - 1) === ".") { 22 | setInput( 23 | props.getAutocomplete(inputValue.substring(0, inputValue.length - 1)) 24 | ); 25 | } else setInput(inputValue); 26 | }; 27 | const handleInputKeyDown = (event: React.KeyboardEvent) => { 28 | switch (event.key) { 29 | case "Enter": 30 | props.processCommand(input); 31 | setInput(""); 32 | break; 33 | case "ArrowUp": 34 | event.preventDefault(); 35 | setInput(props.getHistory("up")); 36 | break; 37 | case "ArrowDown": 38 | event.preventDefault(); 39 | setInput(props.getHistory("down")); 40 | break; 41 | case "Tab": 42 | // Provide autocomplete on tab. For mobile, we have to handle autocomplete in the input's onChange event. 43 | event.preventDefault(); 44 | setInput(props.getAutocomplete(input)); 45 | break; 46 | } 47 | }; 48 | return ( 49 |
50 | {props.terminalPrompt} 51 | 63 |
64 | ); 65 | }; 66 | 67 | export default InputArea; 68 | -------------------------------------------------------------------------------- /src/components/Terminal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import Banner from "./Banner"; 3 | import TerminalOutput from "./TerminalOutput"; 4 | import InputArea from "./InputArea"; 5 | import ErrorMessage from "./ErrorMessage"; 6 | import WelcomeMessage from "./WelcomeMessage"; 7 | import { logEvent } from "firebase/analytics"; 8 | import { analytics } from "../firebase"; 9 | 10 | // Just a little helper function so I don't have to continually update my age 11 | const getAge = (birthDate: Date) => { 12 | var today = new Date(); 13 | var age = today.getFullYear() - birthDate.getFullYear(); 14 | var m = today.getMonth() - birthDate.getMonth(); 15 | if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { 16 | age--; 17 | } 18 | return age; 19 | }; 20 | 21 | const downloadFile = (uri: string, downloadName: string) => { 22 | const link = document.createElement("a"); 23 | link.download = downloadName; 24 | link.href = uri; 25 | link.click(); 26 | link.remove(); 27 | }; 28 | 29 | type TerminalProps = { 30 | terminalPrompt?: string; 31 | banner?: string; 32 | welcomeMessage?: string; 33 | }; 34 | const Terminal = (props: TerminalProps) => { 35 | const { terminalPrompt = ">", banner, welcomeMessage } = props; 36 | const [output, setOutput] = useState<(string | JSX.Element)[]>([]); 37 | const [history, setHistory] = useState([]); 38 | const [historyIndex, setHistoryIndex] = useState(3); 39 | const inputRef = React.useRef(null); 40 | const scrollRef = React.useRef(null); 41 | 42 | const scrollLastCommandTop = () => { 43 | scrollRef.current?.scrollIntoView(); 44 | }; 45 | 46 | useEffect(scrollLastCommandTop, [output]); 47 | 48 | const echoCommands = [ 49 | "help", 50 | "about", 51 | "projects", 52 | "contact", 53 | "awards", 54 | "repo", 55 | "skills", 56 | "website", 57 | ] as const; 58 | type EchoCommand = typeof echoCommands[number]; 59 | const utilityCommands = ["clear", "all", "cv"] as const; 60 | type UtilityCommand = typeof utilityCommands[number]; 61 | const allCommands = [...echoCommands, ...utilityCommands] as const; 62 | type Command = typeof allCommands[number]; 63 | 64 | function isEchoCommand(arg: string): arg is EchoCommand { 65 | return (echoCommands as ReadonlyArray).includes(arg); 66 | } 67 | 68 | function isUtilityCommand(arg: string): arg is UtilityCommand { 69 | return (utilityCommands as ReadonlyArray).includes(arg); 70 | } 71 | 72 | function isValidCommand(arg: string): arg is Command { 73 | return isEchoCommand(arg) || isUtilityCommand(arg); 74 | } 75 | 76 | const glow = (text: string) => { 77 | return {text}; 78 | }; 79 | 80 | const commands: { [key in EchoCommand]: JSX.Element } = { 81 | help: ( 82 |
83 |

84 | Wow, I thought the only people who would visit this site would be bots 85 | and spammers, guess I was wrong. Just type any of the commands below 86 | to get some more info. You can even type a few letters and press [tab] 87 | or '.' to autocomplete. 88 |

89 |
90 |
about
91 |
Stop stalking me
92 |
projects
93 |
Yeah, I've made some cool stuff before
94 |
skills
95 |
I'm pretty good at some things
96 |
awards
97 |
A bit of boasting
98 |
repo
99 |
Take a look at some of my work
100 |
cv
101 |
Check out my CV [pdf - 197KB]
102 |
contact
103 |
Bring on the spam
104 |
website
105 |
How I built this
106 |
all
107 |
Tell me everything
108 |
clear
109 |
Clears the terminal of all output
110 |
111 |
112 | ), 113 | about: ( 114 |
115 |

116 | Hey there! Thanks for taking such a keen interest in me. Hopefully 117 | you're not gonna spam or stalk me... Okay, I guess if you must stalk 118 | me, just give me fair warning so I can look presentable when you 119 | arrive at my door. 120 |

121 |

122 | Right, so, where to begin? Well, my parents met in... Nah, just 123 | kidding. 124 |
125 | As you probably know, my name is {glow("Craig Feldman")}. I'm a{" "} 126 | {getAge(new Date(1992, 12, 23))} year old {glow("Computer Scientist")}{" "} 127 | born and bred in the beautiful South Africa and currently living in 128 | Cape Town. 129 |

130 |

131 | I graduated with distinction from the University of Cape Town with a 132 | Bachelor of Business Science degree in Computer Science. It comprised 133 | of four years of computer science courses, as well as many business 134 | courses (for example, I completed three years of economics, stats, and 135 | finance). 136 |

137 |

138 | I also have an MSc degree in Computer Science from the University of 139 | Oxford, where I was awarded a full academic scholarship. Studying 140 | abroad was an amazing experience - highlights include early morning 141 | rowing, croquet, formal dinners, and just exploring Oxford with 142 | amazing people and friends. 143 |

144 |

145 | Some of my interests include: machine learning, the blockchain and 146 | cryptography, and leveraging these tools to help solve problems, 147 | particularly in the {glow("fintech")} space. I'm also pretty into fly 148 | fishing! 149 |

150 |

151 | My previous formal work experience includes: 152 |

187 |

188 |

189 | Nowadays I'm developing a method to download food... I wish! I am 190 | currently working at{" "} 191 | 196 | Stitch 197 | 198 | , developing a single API for payments and financial data in Africa. 199 |

200 |

201 | Please feel free to get in touch with me to discuss any cool 202 | opportunities. My contact details can be found by typing 'contact', 203 | and if you would like to check out my {glow("CV")}, simply type 'cv' 204 | or click{" "} 205 | 206 | here 207 | 208 | . 209 |

210 |
211 | ), 212 | projects: ( 213 | <> 214 |

215 | I'm always working on comp sciey (not really a word) things. Why don't 216 | you check out a few of my public code repositories? Just type 'repo' 217 | to get the links. 218 |

219 |

220 | I've also dabbled in producing a{" "} 221 | 226 | property-management portal 227 | {" "} 228 | that provides property managers and buildings with some really cool 229 | software and tools. The project uses TypeScript, Node.js, React (with 230 | Material-UI components) and Firebase. 231 |

232 |

233 | You can also check out my MSc thesis{" "} 234 | 235 | An investigation into the applicability of a blockchain based voting 236 | system 237 | {" "} 238 | - this one took a while! 239 |

240 | 241 | ), 242 | contact: ( 243 | <> 244 |
245 |
Email
246 |
247 | craig@craigfeldman.com 248 |
249 |
Smoke signals
250 |
general Cape Town region
251 |
myspace
252 |
just kidding
253 |
254 | 255 | ), 256 | awards: ( 257 | <> 258 |
259 |
2016
260 |
University of Oxford full scholarship
261 |
262 | Standard Bank Africa Chairman's Scholarship ( 263 | 268 | view scholarship 269 | 270 | ) 271 |
272 | 273 |
2015
274 |
Dean's Merit List
275 | 276 |
2014
277 |
Dean's Merit List
278 |
BSG Prize (Best 3rd year Computer Science student)
279 |
Class Medal (1st place) for all 3 Computer Science courses
280 |
Commerce Faculty Scholarship
281 | 282 |
2013
283 |
Dean's Merit List
284 |
Computer Science Merit Award (top 5%)
285 |
Class Medal for Inferential Statistics
286 |
Computer Science Merit Award (top 5%)
287 |
Commerce Faculty Scholarship
288 | 289 |
2012
290 |
Dean's Merit List
291 |
Computer Science Merit Award (top 5%)
292 |
293 | 294 | ), 295 | repo: ( 296 | <> 297 |
    298 |
  • 299 | 304 | GitHub 305 | {" "} 306 | - Unfortunately, I could only make a small subset of my projects 307 | public. 308 |
  • 309 |
  • 310 | 315 | Bitbucket 316 | {" "} 317 | - A few university projects. 318 |
  • 319 |
320 | 321 | ), 322 | skills: ( 323 | <> 324 |
Languages
325 |
326 |
TypeScript
327 |
328 | ##{" "} 329 | 330 | ############# 331 | {" "} 332 | ## 333 |
334 |
Go
335 |
336 | ##{" "} 337 | 338 | ############ 339 | 340 | {" "} 341 | ## 342 |
343 |
Kotlin
344 |
345 | ##{" "} 346 | 347 | ########### 348 | 349 | {" "} 350 | ## 351 |
352 |
Java
353 |
354 | ##{" "} 355 | 356 | ########### 357 | 358 | {" "} 359 | ## 360 |
361 |
C# and C++
362 |
363 | ##{" "} 364 | 365 | ######## 366 | 367 | {" "} 368 | ## 369 |
370 |
Python
371 |
372 | ##{" "} 373 | 374 | ##### 375 | 376 | {" "} 377 | ## 378 |
379 |
380 | 381 |
Cloud & Infrastructure
382 |
383 |
GCP / Firebase
384 |
385 | ##{" "} 386 | 387 | ######### 388 | 389 | {" "} 390 | ## 391 |
392 |
Azure
393 |
394 | ##{" "} 395 | 396 | ######### 397 | 398 | {" "} 399 | ## 400 |
401 |
AWS
402 |
403 | ##{" "} 404 | 405 | ######## 406 | 407 | {" "} 408 | ## 409 |
410 |
411 | Infrastructure
412 | 413 | (Docker, Kubernetes, DBs, etc.) 414 | 415 |
416 |
417 | ##{" "} 418 | 419 | ######### 420 | 421 | {" "} 422 | ## 423 |
424 |
425 | 426 |
Web
427 |
428 |
React
429 |
430 | ##{" "} 431 | 432 | ############ 433 | 434 | {" "} 435 | ## 436 |
437 |
Angular
438 |
439 | ##{" "} 440 | 441 | ##### 442 | 443 | {" "} 444 | ## 445 |
446 |
General web development
447 |
448 | ##{" "} 449 | 450 | ######### 451 | 452 | {" "} 453 | ## 454 |
455 |
456 | 457 | ), 458 | website: ( 459 | <> 460 |

461 | I built this website from scratch using {glow("React")} and{" "} 462 | {glow("TypeScript")}. It is a rewrite of my{" "} 463 | 468 | previous 469 | {" "} 470 | website that used{" "} 471 | 476 | JQuery Terminal Plugin 477 | {" "} 478 | (and some inspiration from{" "} 479 | 484 | Ronnie Pyne 485 | 486 | ). 487 |

488 |

489 | The source code for this site can be found on{" "} 490 | 495 | GitHub 496 | 497 | . Feel free to use this website for inspiration, or go ahead and copy 498 | some of the code! If you do, all I ask is that you give this site a 499 | mention :) 500 |

501 | 502 | ), 503 | }; 504 | 505 | const processCommand = (input: string) => { 506 | logEvent(analytics, "command_received", { command: input }); 507 | 508 | // Store a record of this command with a ref to allow us to scroll it into view. 509 | // Note: We use a ref callback here because setting the ref directly, then clearing output seems to set the ref to null. 510 | const commandRecord = ( 511 |
(scrollRef.current = el)} 513 | className="terminal-command-record" 514 | > 515 | {terminalPrompt}{" "} 516 | {input} 517 |
518 | ); 519 | 520 | // Add command to to history if the command is not empty 521 | if (input.trim()) { 522 | setHistory([...history, input]); 523 | setHistoryIndex(history.length + 1); 524 | } 525 | 526 | // Now process command, ignoring case 527 | const inputCommand = input.toLowerCase(); 528 | if (!isValidCommand(inputCommand)) { 529 | setOutput([ 530 | ...output, 531 | commandRecord, 532 |
533 | 534 |
, 535 | ]); 536 | } else if (isEchoCommand(inputCommand)) { 537 | setOutput([ 538 | ...output, 539 | commandRecord, 540 |
{commands[inputCommand]}
, 541 | ]); 542 | } else if (isUtilityCommand(inputCommand)) { 543 | switch (inputCommand) { 544 | case "clear": { 545 | setOutput([]); 546 | break; 547 | } 548 | case "all": { 549 | // Output all commands in a custom order. 550 | const allCommandsOutput = [ 551 | "about", 552 | "awards", 553 | "skills", 554 | "projects", 555 | "repo", 556 | "contact", 557 | "website", 558 | ].map((command) => ( 559 | <> 560 |
{command}
561 |
562 | {commands[command as EchoCommand]} 563 |
564 | 565 | )); 566 | 567 | setOutput([commandRecord, ...allCommandsOutput]); 568 | break; 569 | } 570 | case "cv": { 571 | setOutput([...output, commandRecord]); 572 | downloadFile("CV.pdf", "Craig Feldman - Curriculum Vitae.pdf"); 573 | break; 574 | } 575 | } 576 | } 577 | }; 578 | 579 | const getHistory = (direction: "up" | "down") => { 580 | let updatedIndex; 581 | if (direction === "up") { 582 | updatedIndex = historyIndex === 0 ? 0 : historyIndex - 1; 583 | } else { 584 | updatedIndex = 585 | historyIndex === history.length ? history.length : historyIndex + 1; 586 | } 587 | setHistoryIndex(updatedIndex); 588 | return updatedIndex === history.length ? "" : history[updatedIndex]; 589 | }; 590 | 591 | const getAutocomplete = (input: string) => { 592 | const matchingCommands = allCommands.filter((c) => c.startsWith(input)); 593 | if (matchingCommands.length === 1) { 594 | return matchingCommands[0]; 595 | } else { 596 | const commandRecord = ( 597 |
(scrollRef.current = el)} 599 | className="terminal-command-record" 600 | > 601 | {terminalPrompt}{" "} 602 | {input} 603 |
604 | ); 605 | setOutput([...output, commandRecord, matchingCommands.join(" ")]); 606 | return input; 607 | } 608 | }; 609 | 610 | const focusOnInput = (event: React.KeyboardEvent) => { 611 | if (event.key === "Tab") { 612 | // Prevent tab from moving focus 613 | event.preventDefault(); 614 | } 615 | inputRef.current?.focus(); 616 | }; 617 | 618 | return ( 619 |
620 |
621 | {banner && } 622 | {welcomeMessage && ( 623 | 624 | )} 625 | 626 | 634 |
635 |
636 | ); 637 | }; 638 | 639 | export default Terminal; 640 | -------------------------------------------------------------------------------- /src/components/TerminalOutput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type OutputProps = { 4 | outputs: (string | JSX.Element)[]; 5 | }; 6 | const TerminalOutput = (props: OutputProps) => { 7 | const outputList = props.outputs.map((o, key) =>
{o}
); 8 | return <>{outputList}; 9 | }; 10 | 11 | export default TerminalOutput; 12 | -------------------------------------------------------------------------------- /src/components/WelcomeMessage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | type WelcomeMessageProps = { 4 | message: string; 5 | inputRef: React.RefObject; 6 | }; 7 | const WelcomeMessage = (props: WelcomeMessageProps) => { 8 | const welcomeMessageRef = React.useRef(null); 9 | useEffect(() => { 10 | if (props.inputRef.current) { 11 | props.inputRef.current.disabled = true; 12 | } 13 | let index = 0; 14 | const typeText = setInterval(() => { 15 | if (!welcomeMessageRef.current) { 16 | return; 17 | } 18 | welcomeMessageRef.current.insertAdjacentText( 19 | "beforeend", 20 | props.message[index++] 21 | ); 22 | if (index === props.message.length) { 23 | clearInterval(typeText); 24 | if (props.inputRef.current) { 25 | props.inputRef.current.disabled = false; 26 | props.inputRef.current.focus(); 27 | } 28 | } 29 | }, 30); 30 | }, [props.inputRef, props.message]); 31 | return ( 32 |
33 | ); 34 | }; 35 | 36 | export default WelcomeMessage; 37 | -------------------------------------------------------------------------------- /src/firebase.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getAnalytics } from "firebase/analytics"; 3 | 4 | const firebaseConfig = { 5 | apiKey: "AIzaSyC2B7qHjCFzv1Aq5kfjIbIe_psLG35iskc", 6 | authDomain: "personal-website-6a2a9.firebaseapp.com", 7 | databaseURL: "https://personal-website-6a2a9.firebaseio.com", 8 | projectId: "personal-website-6a2a9", 9 | storageBucket: "personal-website-6a2a9.appspot.com", 10 | messagingSenderId: "280241671766", 11 | appId: "1:280241671766:web:bc863440cededa05f075f7", 12 | measurementId: "G-H7KL5J0KG8", 13 | }; 14 | 15 | // Initialize Firebase 16 | const app = initializeApp(firebaseConfig); 17 | const analytics = getAnalytics(app); 18 | 19 | export { analytics }; 20 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | } 6 | 7 | body, 8 | html, 9 | #root { 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | const container = document.getElementById("root"); 8 | const root = createRoot(container!); 9 | root.render( 10 | 11 | 12 | 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://bit.ly/CRA-PWA 18 | serviceWorker.unregister(); 19 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.href 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------