This is Nexus, my personal website, live at thilantran.com.
Nexus is built with React, and uses Gatsby as a static site generator for easy hosting on Netlify.
To build:
$ yarn install
$ gatsby develop # or gatsby build to serve from public/ folder
Nexus is no ordinary, bland portfolio website. The landing page of Nexus features a carousel of unique, eye-catching web development showcases that users can switch between and customize with options.
Currently, these showcases are:
- Conway's Game of Life coded in JS using
canvas
methods andwindow.requestAnimationFrame
to draw performant animations.- Includes options to change colors, trace cell path and change trace lengths, as well as feeding in a 1D cellular automata as input into the 2D automata.
- An animation of a random 1D cellular automata from one of the 256 Wolfram codes, also coded in JS using
canvas
.- Includes options to change colors and switch between all rules or only pretty rules.
- A simulation of particles and collisions again using JS and
canvas
.- Includes color and gravity options.
I plan to add more showcases as I go along, and Nexus is designed to be modular enough so that additional showcases can be loaded in with ease.
A JS showcase module has the following interface:
import {
init,
draw,
clearDraw,
spawn,
spawnPrompt
reset,
options,
setOption,
optionsInputAttributes
} from 'showcase.js';
To load a new showcase module, import it in src/components/App.js
, and add it to the showcases
array along with
any initial options for the init
function in showcaseInitOptions
.
Nexus will call the module's functions as follows:
init
is called to initialize the showcase, and configured options fromshowcaseInitOptions
are passed in.draw
is called whenever the showcase is focused in the carousel, andclearDraw
is called whenever the showcase becomes inactive.spawn
is called when the main action button on the landing page is clicked.spawnPrompt
is the action text on the button.
reset
is called when a user clicks on the reset button in options.options
is an array of constants or enums representing the possible shwowcase options.setOption(option, val)
will be called by Nexus to setval
for one option fromoptions
.optionsInputAttributes
is an array that is used to initialize the options menu for the showcase.
Example optionsInputAttributes
:
const optionInputAttributes = [
{
desc: 'Greyscale', // user-friendly description for option
attr: { // attributes that are spread onto the option's input element
type: 'radio',
name: 'color',
id: 'greyscale'
},
option: options.greyscale, // the specific option to set with setOption (enum or constant)
init: false // the initial value ('checked' for radio / checkboxes, or 'value' otherwise)
},
... // rest of options
]
Originally Nexus was written entirely in native JS, without any libraries,
but I decided to port it to React / Gatsby in order to make future development less of a hassle,
and to take advantage of bundlers like Webpack to make the distribution more compressed
(eg. using webpack image compression plugins at first, and gatsby-image
later on).
I also took the opportunity to try out Gatsby and use it as a framework.
The original native JS implementation can still be accessed under pure-js/
.
When I transitioned from my native JS to my React implementation,
I ran into issues where buttons or elements with onClick
events would require two presses on mobile to activate.
Note that my React code was essentially a direct port of my native JS implementation,
which did not have this double click issue.
I ended up writing a custom hook to use the touchstart
, touchend
, and touchmove
events
to simulate a click event on mobile (if touch is supported), which avoids the double click issue.
useResponsiveClick
hook and usage:
// abridged hook implementation:
const useResponsiveClick = ( onClick, delay = 300) => {
const [touchDown, setTouchDown] = useState(null);
const onTouchMove = () => setTouchDown(null);
const onTouchStart = () => setTouchDown(new Date());
const onTouchEnd = (evt) => {
if (touchDown && new Date() - touchDown < delay) {
onClick(evt);
setTouchDown(null);
evt.preventDefault(); // don't trigger redundant click event that can occcur
}
};
return { onTouchMove, onTouchStart, onTouchEnd };
};
// hook usage:
import { useResponsiveClick } from './hooks';
const responsiveButton = (props) => {
const buttonEvents = useResponsiveClick(props.onClick):
return (
<div className="button" {...buttonEvents}>
{props.buttonText}
</div>
);
};