Skip to content

Tim-W-James/timjames.dev

Repository files navigation


Logo

timjames.dev

Personal site for Tim W James - Portfolio, Blog, and more
Deployed to timjames.dev 🌐

CI Release Renovate Linkedin

Netlify Status


Table of Contents
  1. About The Project
  2. Getting Started
  3. Usage
  4. Development
  5. License
  6. Contact

About The Project

screenshot

Goals

  • Portfolio: display and demo past projects
  • Blog: display blog posts from dev.to/timwjames
  • This project also serves as an environment for me to experiment with new technologies and tools. Many of these tools are overkill of a project of this scale (e.g., Redux), but this project acts as useful reference and proof-of-concept.

Built With

Design:

Development:

  • React: frontend framework
  • Redux: state management for React
  • Vite: frontend build tool and dev server. Configured in vite.config.ts
  • TypeScript 4.7: types for js. Configured in tsconfig.json
  • SASS: CSS preprocessor. This repo uses a custom setup to auto-generate scoped type definitions for CSS classes, go here for further details
  • Tailwind: Utility-first CSS framework. This repo uses a custom setup to auto-generate type definitions for Tailwind classes, go here for further details
  • ESLint: Linter/code analyzer for TypeScript. Configured in .eslintrc.cjs with rules from AirBnB and SonarJS
  • Stylelint: Linter/code analyzer for SCSS. Configured in .stylelintrc.cjs
  • Prettier: Formatter. Configured in .prettierrc.cjs
  • Vitest: unit testing framework. Configured in vite.config.ts > test
  • Storybook: view, document and test individual components and pages. Configured in .storybook/main.cjs. Automatically deployed to Github Pages
  • Playwright: end-to-end tests. Configured in playwright.config.ts and located in e2e/
  • pnpm: configuration for the pnpm package manager for better performance, lockfiles and monorepo support. See steps below if you wish to use a different package manager
  • Husky: pre-commit Git hooks to lint, format and run tests. Configured in .husky
  • Renovate: GitHub bot for automatic dependency updates. Configured in renovate.json
  • GitHub Actions: GitHub CI/CD pipeline. Used to ensure builds, linting rules and tests pass for any Pull Request against the main branch. Configured in .github/workflows

Deployment:

Getting Started

Prerequisites

  • Install node for the version in .nvmrc or use nvm:

    nvm install && nvm use
  • Install the pnpm package manager. One option is corepack for automatic installation, which is an experimental node feature that must be enabled using:

    corepack enable

Installation

  • Clone the repo:

    git clone https://github.com/Tim-W-James/timjames.dev.git
  • Install dependencies with pnpm:

    pnpm i
  • Create a .env file and specify the following:

    # Get the reCAPTCHA key from https://www.google.com/recaptcha/admin/site/599894418
    VITE_SITE_RECAPTCHA_KEY=123
    STORYBOOK_SITE_RECAPTCHA_KEY=123

Usage

  • Build to dist and preview:

    pnpm build
    pnpm preview

Development

  • Start a development environment:

    pnpm dev

Testing

  • Run unit tests in watch mode (automatically reruns tests when source code changes):

    pnpm test
  • Run coverage tests and output results to coverage:

    pnpm coverage
  • View individual components or pages and run interaction tests:

    pnpm storybook
  • Run Storybook tests to ensure all stories render and interaction tests pass (requires Storybook to be running, or use :ci):

    pnpm storybook:test
  • Run End-to-End tests with Playwright (:headed to view the tests being executed in the browser) for Firefox, Chromium, and Webkit in both desktop and mobile viewports:

    pnpm e2e
  • View visual regression tests on Chromatic

Writing New Tests

This repo has several layers of tests:

  • Unit tests for TypeScript utilities (those in src/utils): use Vitest.

  • Unit tests for React components:

    • First, consider creating a Storybook story for the component, including decorators, args, etc.
    • Storybook allows us to document and preview components in insolation, and we can reuse stories in our tests without having to duplicate logic
    • Use React Testing Library to render the component from Storybook, then write tests
    • Tests should use an arrange-act-assert pattern and follow the React Testing Library query priorities. Testing playground is a useful tool for finding good queries
    • Storybook interaction tests can be used too, as this allows actions to be viewed visually. However, this can result in tests being duplicated. Tests should not be part of interactions where possible, instead they should be used to document complex behaviour of a component (e.g., a form being filled out)
  • Accessibility tests:

    • The Storybook ally addon can be used to check for accessibility issues
    • The Netlify Lighthouse plugin also run on build to detect further accessibility and performance issues
  • End-to-end tests: Playwright tests in e2e/, with the testing library API

  • Visual regression tests:

    • Avoid using snapshot tests

    • Visual regression tests are run in CI using Chromatic, which allows changes to be reviewed and approved

    • Upload a new build manually using:

      npx chromatic --project-token <your-project-token>
    • Chromatic also publishes Storybook here

A note on code coverage: when the component is exported from Storybook, coverage of the component itself will not be tracked correctly. For this reason, minimum coverage requirements are not enabled.

Deployment

  • Initialize Netlify CLI:

    npx netlify init

    or

    pnpm init:netlify
  • Build locally:

    pnpm build:netlify
  • Deploy to preview server:

    pnpm run deploy

    Note that any Pull Request will automatically be deployed to a Netlify preview server.

  • Deploy to production: continuous deployment to Netlify domain timjames.netlify.app on the main branch.

Code Style

Styling

This repo takes a utility-first approach to styling. HTML tags (and React components) describe semantics (e.g., header, section). Where possible, CSS classes should instead describe utilities (e.g., typography, layout).

When applying styles, consider the following:

  1. Before adding CSS classes, consider whether the default HTML styles from the base styles in main.scss are sufficient
  2. Style the component with existing Tailwind classes
  3. If existing Tailwind classes need to be customized (e.g., font-family), configure the theme in tailwind.config.cjs
  4. If there is no existing Tailwind class for the use case, and the style is deemed to be reusable:
    1. For a CSS feature Tailwind doesn’t support, create a custom utility in main.scss
    2. For complex classes (i.e., more than 1 CSS property), semantic classes (e.g., card, btn) can be created via component classes in main.scss. However, be careful to avoid hasty abstractions
  5. If the style is not deemed to be reusable, and custom CSS needs to be used, create an SCSS module in the same directory as the parent component

Typed CSS

Avoid applying classes directly to a component, as this does not provide type safety. Instead, use a custom utility function. For example:

import cn from "@styles/cssUtils";
...
<div className={cn("container p-5", { "text-lg": true })}>
...

This works by generating CSS class names for any Tailwind or global classes from the compiled CSS, and generating a union type in cssClasses.d.ts. This is automatically generated during development (run pnpm dev). Note that unused Tailwind classes are purged and excluded from the type definition, so when adding a new class your IDE will complain momentarily until the file is saved and types are re-generated.

Due to a limitation with how TypeScript infers template literals, if a class exists which is a prefix, it will break other classes that use that prefix. For example: flex-col will be marked as invalid because flex is a class. As a workaround, you can pass these classes as separate parameters:

cn("flex p-5", "flex-col", "flex-wrap");

To use scoped SCSS modules with type safety, separate ./...module.scss.d.ts files are generated in the directory of it's parent component. This can be used with the cnScoped function, for example:

import { cnScoped } from "@styles/cssUtils";
import styles from "./component.module.scss";
...
<div className={cnScoped(styles)(styles._component, "container p-5", {
  "text-lg": true,
})}>
...

Some things to note:

  • Classes in SCSS modules are named with a _ prefix and are lowerCamelCase
  • Using the styles import is required for the module to be compiled and avoid name collisions, and gives intellisense
  • The ClassNames type provides cnScoped with a union of all class names in that SCSS module, so that it knows what valid classes are in scope. Note the extra () since the function needs to by curried.

Project Structure

  • Source Code: src
    • Entry point and routes: index.tsx
    • Root component: App.tsx
    • Common components: components. Has alias @components. Group by type for layout, buttons, forms, etc.
    • Common hooks: hooks. Has alias @hooks
    • Common utils: utils. Has alias @utils
    • Common API functionality: services. Has alias @services
    • Pages: pages. Has alias @pages
    • Feature specific code: features. Has alias @features. Nest subfolders for components, utils, hooks, etc. depending on the scope they apply to
    • Root Redux State: app
    • Context and Redux Slices: context. Has alias @context
    • Constants: constants. Has alias @constants
    • Data: data. Has alias @data
  • Unit Tests: place tests adjacent to source code. Mock data goes in src/mocks
  • Storybook Stories: place stories adjacent to source code. Config in .storybook
  • End-to-End Tests: e2e
  • CSS Styling:
    • Use main.scss for base styles and custom Tailwind
    • Put global SCSS variables and mixins in src/styles. These are automatically imported via vite.config.ts -> preprocessorOptions
    • Place page or component specific styles adjacent to source code, using scoped .modules
  • Global TypeScript Types: types
  • Web Accessible Files (robots.txt, manifest.json, etc.): public
  • Site Assets (favicon.ico, images, etc.): public/assets. Has alias @assets

Define path alias in tsconfig.paths.json.

I recommend using VSCode file nesting for a cleaner file tree.

Documentation

  • Document code with JSDoc

  • Document components or pages with Storybook and run with:

    pnpm storybook

    Storybook is automatically deployed to Github Pages

License

Distributed under the MIT License. See LICENSE.txt for more information.

Contact

Email: tim.james.work9800@gmail.com

Project Link: https://github.com/Tim-W-James/timjames.dev

↑ Back to Top ↑