).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 |
153 | -
154 | working on asset management software at{" "}
155 |
160 | Fundamental Software
161 |
162 | ;
163 |
164 | -
165 | working for a great content creation app called{" "}
166 |
171 | Over
172 |
173 | ;
174 |
175 | -
176 | helping people to buy, store, and learn about cryptocurrency at{" "}
177 |
182 | Luno
183 |
184 | .
185 |
186 |
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 |
--------------------------------------------------------------------------------