Persistent Profile and Dynamic Menu

Integrating Persistent Profile and a Dynamic Menu

In the previous lesson, you built a simple persistence layer and wired it into the app so the player profile can be saved and restored. In this lesson, you will put that to work by:

  • Updating the reducer to record lifetime stats (totalRevenue, totalCustomersServed, bestRevenue, totalGamesPlayed, winStreak, and more).
  • Reading those stats in the main menu and rendering them live.
  • Adding a light polish pass to the menu styles.

By the end, the menu will show real-time stats that survive page reloads, creating a sense of progress.

Update the Reducer to Track and Persist Profile Stats

You will connect profile updates into the core game loop. The reducer will increment totals after each game, update best revenue, and maintain a win streak. Because you restored the profile in the previous lesson, these updates will be written back to storage automatically.

src/reducer/gameReducer.js

JavaScript
export function getInitialState(profile = null) {
  return {
    // ...other state...
    profile: profile || {
      totalRevenue: 0,
      totalCustomersServed: 0,
      bestRevenue: 0,
      hardestDifficultyBeaten: null,
      totalGamesPlayed: 0,
      winStreak: 0
    },
    // ...other state...
  };
}

export function gameReducer(state, action) {
  switch (action.type) {
    case 'START_GAME': {
      return {
        ...getInitialState(state.profile),
        phase: 'PLAYING',
        ui: { screen: 'game' }
      };
    }
    case 'TICK': {
      // ...game logic...

      // Check Loss Condition
      if (newQueue >= state.config.maxQueue) {
        // Update profile stats on loss
        const newProfile = {
          ...state.profile,
          totalRevenue: state.profile.totalRevenue + state.shop.revenue,
          totalCustomersServed: state.profile.totalCustomersServed + state.shop.customersServed,
          bestRevenue: Math.max(state.profile.bestRevenue, state.shop.revenue),
          totalGamesPlayed: state.profile.totalGamesPlayed + 1,
          winStreak: 0
        };

        return {
          ...state,
          phase: 'RESULTS',
          ui: { ...state.ui, screen: 'results' },
          profile: newProfile,
          result: {
            won: false,
            reason: 'Queue overflow!',
            stats: {
              revenue: state.shop.revenue,
              customersServed: state.shop.customersServed
            }
          }
        };
      }

      if (newTime === 0) {
        // Update profile stats on win
        const newProfile = {
          ...state.profile,
          totalRevenue: state.profile.totalRevenue + state.shop.revenue,
          totalCustomersServed: state.profile.totalCustomersServed + state.shop.customersServed,
          bestRevenue: Math.max(state.profile.bestRevenue, state.shop.revenue),
          totalGamesPlayed: state.profile.totalGamesPlayed + 1,
          winStreak: state.profile.winStreak + 1
        };

        return {
          ...state,
          timers: { ...state.timers, timeRemaining: newTime, gameHour, gameMinute },
          shop: { ...state.shop, queueLength: newQueue },
          resources: { ...state.resources, satisfaction: newSatisfaction },
          phase: 'RESULTS',
          ui: { ...state.ui, screen: 'results' },
          profile: newProfile,
          result: {
            won: true,
            reason: 'Shift complete!',
            stats: {
              revenue: state.shop.revenue,
              customersServed: state.shop.customersServed
            }
          }
        };
      }

      // ...rest of TICK...
      return {
        ...state,
        timers: { ...state.timers, timeRemaining: newTime, gameHour, gameMinute },
        shop: { ...state.shop, queueLength: newQueue },
        resources: { ...state.resources, satisfaction: newSatisfaction }
      };
    }
    default:
      return state;
  }
}

Explanation:

  • Only getInitialState, START_GAME, and the relevant TICK logic are shown.
  • These are the parts that update and persist the profile stats as described in the lesson (such as total revenue, customers served, best revenue, games played, and win streak).

Render Live Stats in the Menu

The menu reads the profile from the global state and displays a quick snapshot. Buttons let the player start a game or navigate to settings.

src/components/MenuScreen.jsx

JSX
import React from 'react';
import { useGameContext } from '../context/GameContext';
import './MenuScreen.css';

export default function MenuScreen() {
  const { state, dispatch } = useGameContext();
  const { profile } = state;

  return (
    <div className="menu-screen">
      <div className="menu-container">
        <h1 className="game-title">Game Menu</h1>

        <div className="stats-summary">
          <div className="stat-pill">🏆 Best: ${profile.bestRevenue}</div>
          <div className="stat-pill">🎮 Games: {profile.totalGamesPlayed}</div>
          <div className="stat-pill">🔥 Streak: {profile.winStreak}</div>
        </div>

        <div className="menu-actions">
          <button
            className="large-btn"
            onClick={() => dispatch({ type: 'START_GAME' })}
          >
            Start Game
          </button>
          <button
            className="large-btn"
            onClick={() => dispatch({ type: 'SET_SCREEN', payload: 'settings' })}
          >
            Settings
          </button>
        </div>
      </div>
    </div>
  );
}

Explanation:

  • useGameContext gives you state and dispatch. You read state.profile for the stats.
  • The stat-pill elements show best revenue, total games, and the current win streak.
  • Buttons dispatch reducer actions, so the menu reacts instantly.

Polish the Menu UI

These styles add structure, subtle animation, and clear call-to-action buttons.

src/components/MenuScreen.css

CSS
.menu-screen {
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
  overflow-x: hidden;
  animation: fadeIn 0.5s ease;
}

.menu-container {
  max-width: 100%;
  box-sizing: border-box;
  background: rgba(0, 0, 0, 0.6);
  backdrop-filter: blur(10px);
  border-radius: 20px;
  padding: 40px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
  border: 2px solid rgba(139, 69, 19, 0.5);
}

.game-title {
  font-size: 3rem;
  text-align: center;
  margin-bottom: 10px;
  background: linear-gradient(135deg, #D2691E 0%, #8B4513 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  animation: pulse 2s infinite;
}

.stats-summary {
  display: flex;
  justify-content: center;
  gap: 15px;
  margin-bottom: 30px;
  flex-wrap: wrap;
}

.stat-pill {
  background: rgba(255, 255, 255, 0.1);
  padding: 8px 16px;
  border-radius: 20px;
  font-size: 0.9rem;
  color: #e0e0e0;
  border: 1px solid rgba(255, 255, 255, 0.1);
}

.menu-actions {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.large-btn {
  font-size: 1.3rem;
  padding: 18px 36px;
  background: linear-gradient(135deg, #D2691E 0%, #8B4513 100%);
  color: #fff;
  border: none;
  border-radius: 8px;
  font-weight: bold;
  cursor: pointer;
  transition: background 0.2s, transform 0.1s;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}

.large-btn:hover,
.large-btn:focus {
  background: linear-gradient(135deg, #8B4513 0%, #D2691E 100%);
  transform: translateY(-2px) scale(1.03);
}

@media (max-width: 600px) {
  .menu-container { padding: 16px; }
  .game-title { font-size: 1.75rem; }
  .large-btn { font-size: 1rem; padding: 12px 20px; }
  .stat-pill { padding: 6px 10px; font-size: 0.8rem; }
}

Explanation:

  • The container uses a blurred, translucent panel for focus.
  • stat-pill styles create compact badges for key metrics.
  • large-btn provides strong visual affordance with hover/focus feedback.

Summary and Next Steps

You connected the saved profile to gameplay and surfaced it in the UI:

  • The reducer now updates lifetime stats after each game (including best revenue, win streak, total revenue, customers served, and games played).
  • The menu displays those stats in real time and looks polished.

This makes progress visible and motivating. Ready to lock it in? Head to the practice section — let’s make these changes feel second nature.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal