Skip to main content

Week 3

Goals

  • Ensure everyone has a React project installed and GitHub repo initialized
  • Leran about React project structure
  • Setup project scaffolding
  • Define and structure data
  • Implement some basic components

Project Structure

Key Files

node_modules

Where the third party libraries like react are installed; never needs to be touched.

public

Where the public assets exist (images, audio, etc).

src

Where all of our code is stored.

We start out only having App.tsx.

index.html

The container for our app.

Also has the entry point ot our app in <script...

package.json

Some metadata on the project and its dependencies.

Also has some develpment dependencies.

Components

Components are the building blocks a React app - which is what we will be building! They help us write clean, moular, and efficient code.

ex:

Grid of cards -> Card is a component -> Button in card is component

We build all components individually, then combine them to form a page.

Essentially, a react app is a tree of components iwth the app as the root bringing everything together.

Setup

Let's navigate into our project directory and install the initial dependencies.

npx create-react-app blueprint_kudos --template typescript
cd blueprint_kudos
npm install

Create a tsconfig.json file

{
  "compilerOptions": {
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "es2022",
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  }
}

Now that we're in our project, lets focus on styling.

We're going to use Tailwind CSS. It's a framework that lets us style our components by adding class names directly in our HTML, instead of writing separate CSS files. It's super productive.

Let's install Tailwind and a few helper packages:

npm install tailwindcss postcss autoprefixer class-variance-authority clsx tailwind-merge
npx tailwindcss init -p

After you run that last command, you'll notice a couple of new files in your project, including tailwind.config.js. This is where we tell Tailwind where to look for our files. Let's configure it.

Note: you don't need to know what these exact commnads mean, but do remember from our first session back in Week 1 what a dependency is - it will help you understand why this step is necessary.

Open up tailwind.config.js and replace its contents with this:

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{ts,tsx}'],
  theme: { extend: {} },
  plugins: [],
}

This just tells Tailwind to scan all our .ts and .tsx files inside the src folder for class names.

Finally, we need to actually include Tailwind's styles in our app. Open src/index.css and replace everything in there with these three lines:

@tailwind base;
@tailwind components;
@tailwind utilities;

/* This part is optional but nice for dark mode */
:root {
  color-scheme: light dark;
}

And that's it for setup! Let's run the dev server to make sure everything is working.

npm run dev

Defining Data

Before we start building components, it's a really good idea to think about the shape of our data.

What does a "Developer" look like? What information does a "Kudos" contain?

Using TypeScript to define these shapes, or "types," helps us avoid a ton of bugs later.

Let's create a new folder src/lib to hold some of our shared logic. Inside it, create a file named types.ts.

src/lib/types.ts:

export type Role = 'techLead' | 'developer';

export type Developer = {
  id: string;
  name: string;
  avatarUrl?: string;
  teamId: string;
};

export type Team = {
  id: string;
  name: string;
};

export type Kudos = {
  id: string;
  fromId: string; // developer or TL giving kudos
  toId: string;   // recipient developer
  message: string;
  createdAt: string;
  sprint?: string;
  tag?: 'collaboration' | 'quality' | 'initiative';
};

See how clear that is? Now, whenever we work with a Developer object, TypeScript will make sure it has an id, a name, etc.

Next, let's create a little file for small, reusable functions. We'll call this file utils.ts.

src/lib/utils.ts:

// a simple function to generate a unique-ish ID
export const uid = () => Math.random().toString(36).slice(2, 9);

// a helper to get the current timestamp in a standard format
export const nowIso = () => new Date().toISOString();


// note: we'll see how this works when we build our components.
export function cn(...classes: Array<string | false | null | undefined>) {
  return classes.filter(Boolean).join(' ');
}

Managing States

This part of the session may be a little more abstract, but it asks an important question: how do all our different components (the sidebar, the dashboard, the modal, etc) share information and stay in sync?

We need a single, central "source of truth." This is often called state management.

For this we leverage a concept called context - and need a respective file for it.

Let's create our context file.

src/lib/app-context.tsx:

import React, { createContext, useContext, useMemo, useReducer } from 'react';
import type { Developer, Team, Kudos, Role } from './types';
import { uid, nowIso } from './utils';

// first, we define the shape of our entire application's state
type State = {
  role: Role;
  teams: Team[];
  developers: Developer[];
  kudos: Kudos[];
  activeDeveloperId?: string;
  giveKudosOpen: boolean;
  giveKudosRecipientId?: string;
};

// next, we define all the possible actions that can change our state
type Action =
  | { type: 'switchRole'; role: Role }
  | { type: 'setActiveDeveloper'; developerId?: string }
  | { type: 'openGiveKudos'; toId?: string }
  | { type: 'closeGiveKudos' }
  | { type: 'createKudos'; toId: string; fromId: string; message: string; tag?: Kudos['tag'] };

// our initial data so the app isn't empty when it loads
const initialState: State = {
  role: 'techLead',
  teams: [
    { id: 't1', name: 'Alpha' },
    { id: 't2', name: 'Beta' },
  ],
  developers: [
    { id: 'd1', name: 'Alex Johnson', teamId: 't1' },
    { id: 'd2', name: 'Sam Lee', teamId: 't1' },
    { id: 'd3', name: 'Priya Patel', teamId: 't2' },
  ],
  kudos: [],
  giveKudosOpen: false,
};

// the reducer function
function reducer(state: State, action: Action): State {
  switch (action.type) {
    // for each action, we return a *new* copy of the state with the changes
    case 'switchRole':
      return { ...state, role: action.role };
    case 'setActiveDeveloper':
      return { ...state, activeDeveloperId: action.developerId };
    case 'openGiveKudos':
      return { ...state, giveKudosOpen: true, giveKudosRecipientId: action.toId };
    case 'closeGiveKudos':
      return { ...state, giveKudosOpen: false, giveKudosRecipientId: undefined };
    case 'createKudos': {
      const newKudos: Kudos = {
        id: uid(),
        toId: action.toId,
        fromId: action.fromId, // we'll hardcode this for now
        message: action.message,
        createdAt: nowIso(),
        tag: action.tag,
      };
      // add the new kudos to the *beginning* of the array
      return { ...state, kudos: [newKudos, ...state.kudos], giveKudosOpen: false };
    }
    default:
      return state;
  }
}

// creating the actual context and a provider component
const AppContext = createContext<{ state: State; dispatch: React.Dispatch<Action> } | null>(null);

export function AppProvider({ children }: { children: React.ReactNode }) {
  const [state, dispatch] = useReducer(reducer, initialState);
  const value = useMemo(() => ({ state, dispatch }), [state]);
  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

// custom hook to make it easy for components to access the state
export function useApp() {
  const ctx = useContext(AppContext);
  if (!ctx) throw new Error('useApp must be used within AppProvider');
  return ctx;
}

// "Selectors" are helpers to get specific, computed data from the state
export const selectors = {
  developerById: (id?: string) => (s: State) => s.developers.find((d) => d.id === id),
  developersByTeam: (teamId: string) => (s: State) => s.developers.filter((d) => d.teamId === teamId),
  kudosForDeveloper: (devId: string) => (s: State) => s.kudos.filter((k) => k.toId === devId),
};

...that was a lot of code.

I know it seems like a lot, but this is the 'brain' of our app, so it is important we do it right.

Building Components

Instead of pulling in a UI library from an external source, let's create a few of our own simple, reusable components.

Think of these as the bricks of a house. We'll build them once and reuse them everywhere.

Let's create a src/components/ui folder. Inside it, we'll create our Button, Card, Input, and Textarea.

For our buttons...

src/components/ui/button.tsx:

import React from 'react';
import { cn } from '../../lib/utils'; // Remember our helper?

type Props = React.ButtonHTMLAttributes<HTMLButtonElement> & { variant?: 'primary' | 'ghost' };
export function Button({ className, variant = 'primary', ...props }: Props) {
  const base = 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none h-9 px-4';
  const styles =
    variant === 'primary'
      ? 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-400'
      : 'bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800';
  // Here's where `cn` is useful, merging base styles, variant styles, and any custom classes
  return <button className={cn(base, styles, className)} {...props} />;
}

For our cards...

src/components/ui/card.tsx:

import React from 'react';
import { cn } from '../../lib/utils';

export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return <div className={cn('rounded-lg border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900', className)} {...props} />;
}
export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return <div className={cn('p-4 border-b border-gray-200 dark:border-gray-800', className)} {...props} />;
}
export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
  return <div className={cn('p-4 space-y-2', className)} {...props} />;
}

For our inputs...

src/components/ui/input.tsx:

import React from 'react';
import { cn } from '../../lib/utils';

export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
  ({ className, ...props }, ref) => (
    <input
      ref={ref}
      className={cn('w-full rounded-md border border-gray-300 dark:border-gray-700 bg-transparent px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400', className)}
      {...props}
    />
  )
);
Input.displayName = 'Input';

And finally, for our text areas...

src/components/ui/textarea.tsx:

import React from 'react';
import { cn } from '../../lib/utils';

export const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
  ({ className, ...props }, ref) => (
    <textarea
      ref={ref}
      className={cn('w-full rounded-md border border-gray-300 dark:border-gray-700 bg-transparent px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400', className)}
      {...props}
    />
  )
);
Textarea.displayName = 'Textarea';

We now have a consistent set of UI components to build our features with!