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/ folderNexus 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
canvasmethods andwindow.requestAnimationFrameto 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:
initis called to initialize the showcase, and configured options fromshowcaseInitOptionsare passed in.drawis called whenever the showcase is focused in the carousel, andclearDrawis called whenever the showcase becomes inactive.spawnis called when the main action button on the landing page is clicked.spawnPromptis the action text on the button.
resetis called when a user clicks on the reset button in options.optionsis an array of constants or enums representing the possible shwowcase options.setOption(option, val)will be called by Nexus to setvalfor one option fromoptions.optionsInputAttributesis 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>
);
};