Hackweek project!
nvm use
npm ci
npm run dev
Then go to http://localhost:8888.
All pushes to the main
branch trigger a Github Action which deploys the code to production, which is at https://drawduel.com. Usually completes within 30 seconds.
They key directories in this project are
src
├── frontend
├── backend
└── agnostic
TypeScript + Preact. Contains all of the frontend code for the project like: UI components, CSS, websocket client, general frontend utilities, etc.
Frontend code is built using vite.
There are two production entry points: index & game. There is one development entry point: development.
The index entry point is the main home page of the site. Its template is at /index.html
and it loads /src/frontend/index/index.tsx
which loads /src/frontend/index/IndexApp.tsx
which renders the main home page. If you want to make changes to this page you'll likely put them in IndexApp.tsx
. See this page on production here https://drawduel.com.
The game entry point is the page of the site where actual draw duel games are played. Its template is at /game/index.html
and it loads /src/frontend/game/game.tsx
which loads /src/frontend/game/GameApp.tsx
which renders the game page. If you want to make changes to this page you'll likely put them in GameApp.tsx
. See this page on production here https://drawduel.com/game/.
the development entry point, as the name suggests, only gets built in development. If you're working on code that might not be ready to go into the main game, but you'd like a place to test it, and also share it with other devs, you can put it in the development entry point. Its template is at /development/index.html
and it loads /src/frontend/development/development.tsx
which loads /src/frontend/development/DevelopmentApp.tsx
which renders the development page. If you'd like to make changes to this page you'll likely put them in DevelopmentApp.tsx
. See this page in development here http://localhost:8888/development/.
Global CSS should be put in /src/frontend/global.css
which is imported by all entry points. To scope CSS to a specific component, put it in a *.module.css
file and import it within that component, see lineInput.tsx
and LineInput.module.css
as an example.
"There's no client-side routing library, should I add one?"
No. This project is simple enough that it doesn't need one.
"There's no state management library, should I add one?"
I don't think we'll need one. Let's see how far we get by just using useContext
and then we'll re-evaluate if things start getting too confusing or messy.
TypeScript + Express + socket.io. Contains all of the backend code for the project like: http routing, websocket server, general backend utilities, etc.
Backend code is run using tsx.
There's a single entry point: /src/backend/server.ts
.
"Where's the database? Should I add one?"
There's no database. This project doesn't need one, all state is ephemeral.
"How is production behind SSL but I don't see any SSL code in the project?"
We proxy through Cloudflare, which gives us SSL/TLS, http -> https redirects, and www to non-www redirects for free!
TypeScript. Contains all code that can or needs to run on both the frontend and backend like: game logic, general utilities, etc.
State logic is spread across three key files: gameState.ts
, clientContext.ts
, and serverContext.ts
. If you'd like to see a reference PR that touches all three of these files to implement a new feature in the game (changing player name) you can see that here.
This file contains definitions of the game state and of game events that can advance the game state.
This file contains the definition of the client context. The main two parts of the client context are the game state and the client state. The game state represents the state of the game, and should stay in sync with the server at all times. The client state represents state specific to this client, and that the server doesn't need to know about or shouldn't know about.
This file contains the definition of the the server context. Inside the server context is a map of game ids to server game contexts, since the server can handle multiple concurrent games. Inside each server game context is the game state and server state. The game state represents the state of the game, and should be synced with all clients at all times. The server state represents state specific to this game for all clients, but is information that the clients don't need to know about or shouldn't know about.
All imports should use an absolute url:
// ❌ - No
import { randomShortId } from '../../agnostic/random.ts';
import './global.css';
// ✅ - Yes!
import { randomShortId } from 'src/agnostic/random.ts';
import 'src/frontend/global.css';
All imports should contain an extension:
// ❌ - No
import Button from 'src/frontend/components/Button';
import utils from 'src/agnostic/utils';
import 'src/frontend/global';
// ✅ - Yes!
import Button from 'src/frontend/components/Button.tsx';
import utils from 'src/agnostic/utils.ts';
import 'src/frontend/global.css';
Import only specific functions from lodash
, not the entire lib:
// ❌ - No
import { isObject } from 'lodash-es';
// ✅ - Yes!
import isObject from 'lodash-es/isObject';
Prefix type imports with the type
keyword:
// ❌ - No
import { GameState, PlayerId } from 'src/agnostic/gameState.ts';
// ✅ - Yes!
import { type GameState, type PlayerId } from 'src/agnostic/gameState.ts';
Prefer functional components over class components:
// ❌ - No
import { Component } from 'preact';
interface MyComponentProps {
// etc
}
class MyComponent extends Component<MyComponentProps> {
// etc
}
// ✅ - Yes!
import { type FunctionalComponent } from 'preact';
interface MyComponentProps {
// etc
}
const MyComponent: FunctionalComponent<MyComponentProps> = (props) => {
// etc
};
Prefer named exports over default export:
// ❌ - No
const MyComponent: FunctionalComponent = () => {
// etc
};
export default MyComponent;
// ✅ - Yes!
export const MyComponent: FunctionalComponent = () => {
// etc
};
Since this is a preact
and not a react
project, use the class
attribute instead of the className
attribute in JSX:
// ❌ - No
const MyComponent: FunctionalComponent = () => {
return <button className="button">button</button>;
};
// ✅ - Yes!
const MyComponent: FunctionalComponent = () => {
return <button class="button">button</button>;
};
Also, since this is a preact
project and not a react
project, use the onInput
event handler on input elements instead of onChange
:
// ❌ - No
const MyComponent: FunctionalComponent = (props) => {
return <input type="text" onChange={props.onChange} />;
};
// ✅ - Yes!
const MyComponent: FunctionalComponent = (props) => {
return <input type="text" onInput={props.onInput} />;
};
We should probably should add eslint to this project to catch those cases when we have time. Don't want to go down an eslint config rabbit hole right now.
Since this is largely a TypeScript and Preact project it'd good to use an IDE with strong TypeScript support like VSCode and install the Preact DevTools extension for your browser.
Since this is a multiplayer game we'll have impersonate many players at once to test stuff in local dev. Convenient support for this is already implemented in this project and it's called "personas". To create or use a player "persona" just append a dash and some string after the game id in the game url. For example, if this is the game url:
http://localhost:8888/game/#test
You can create three "personas" to play the game with these urls, as an example:
- http://localhost:8888/game/#test-alex
- http://localhost:8888/game/#test-bob
- http://localhost:8888/game/#test-chris
The "personas" are managed entirely by the client and the server isn't aware of them. They also persist between games, although the only player-facing data within a "persona" is the player name.
If you're doing exploratory coding to try out some ideas and you want to share those with other devs by pushing to main
but don't want to affect the actual production site then add your code to the development entry point, i.e. DevelopmentApp.tsx
.
"During hackweek should I push my work directly to main
or make PRs and get reviews?"
I trust you to use your discretion on a case-by-case basis 😊
The production server is a Ubuntu VPS. The drawduel server is run as a systemd service. The systemd service config file for drawduel is in the production
directory of this project, among other production-only files. Handy commands for managing the drawduel service on production:
# start drawduel
systemctl start drawduel
# stop drawduel
systemctl stop drawduel
# tells systemd to reload config files
# use to run before restart if they
# may have changed
systemctl daemon-reload
# restart drawduel
systemctl restart drawduel
# see status of drawduel
systemctl status drawduel
# enable drawduel on startup
systemctl enable drawduel
# disable drawduel on startup
systemctl disable drawduel
# see all drawduel logs (even across restarts)
journalctl -u drawduel
# see real-time drawduel logs (similar to tail -f)
journalctl -u drawduel -f
# see 50 most recent drawduel logs
journalctl -u drawduel -n 50