Overview

This feature is a this-or-that mini game intended to teach the user about how food choices impact blood glucose levels. The user is presented with two food options and are tasked with selecting one or the other and watching how their glycemic load changes based on their choices.

User Story

As a player, the user wants to be able to see reflections of their real life within the game through the everyday choices they make in food and how it impacts their glucose levels. This game helps simulate those choices in a controlled environment so the user can better see how their health is impacted by their choices.

  • User selects a food from two options
  • The backend provides estimated glucose impact for each food via glycemic load value
  • The user is responsible for ensuring their glycemic load stays low

Features

Frontend

The game opens with instructions explaining glycemic load, the game, and its purpose. It uses “cards” in Dexcom Green with the name and an image of each food. Hovering on a card gives a short phrase describing the food, and there is a total glycemix load counter at the bottom, with a default of 0. On hover, the cards tilt left or right, and on click, the glycemic load count increases and the two food choices disappear, getting replaced with the next set of foods.

Glycemic load values are taken from online databases here.

HTML

%%html
<h2 style="text-align: center;">Make the best choices for your body to keep your glucose levels low!</h2>

<button class="help-btn toggle-help-btn">Help</button>

<div class="foodchoice-tabs">
  <div class="foodchoice-tab active" data-tab="introduction">Introduction</div>
  <div class="foodchoice-tab" data-tab="food-choice">Food Choice Game</div>
</div>

<div class="foodchoice-content active" id="introduction">
    <div class="introduction-bar">
      <h2>Smart Food Choices Made Simple with the Glycemic Load Game</h2>
    <!--Instructions here-->
  </div>
</div>


<div class="help" id="help">
    <p class="help-instructions"><strong>Background</strong></p>
    <!--Instructions here-->
    <br>
    <p class="help-instructions"><strong>Instructions</strong></p>
    <!--Instructions here-->
    <button class="help-btn toggle-help-btn">OK</button>
</div>

CSS

%%html
<style>

.container {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
}

/*By default, the help box is shown*/
.overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background-color: #66D7D1;
    z-index: 9999;
    display: flex;
    justify-content: center;
    align-items: center;
}
.help-box {
    max-width: 900px;
    padding: 30px;
    background: transparent;
    color: #000;
    text-align: center;
}
.help {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    background-color: #66D7D1;
    z-index: 10;
    padding: 20px 40px;
    box-sizing: border-box;
    border: 2px solid transparent;
    border-radius: 10px;
    display: block;
}

.card-container {
    display: flex;
    justify-content: center;
    gap: 20px;
    margin-top: 20px;
}
/*Design for the food cards*/
.food-card {
    width: 300px;
    height: 300px;
    border: 2px solid transparent;
    border-radius: 10px;
    background-color: #58A618;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    transition: transform 0.3s ease;
    padding: 10px;
    cursor: pointer;
}
.food-card img {
    display: block;
    width: 200px;
    height: 200px;
    justify-content: center;
}
.food-card div {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    margin-top: 10px;
}

/* Tilt on hover*/
.food-card:nth-child(odd):hover {
  transform: rotate(-2deg);
}
.food-card:nth-child(even):hover {
  transform: rotate(2deg);
}
.food-card:first-child {
    margin-right: 300px;
}
</style>

JS

import { pythonURI, fetchOptions } from '/glucoquest_frontend/assets/js/api/config.js';

// turn the help box on and off with a button press
function toggleHelp() {
    const helpBox = document.getElementById("help");
    if (helpBox.style.display === 'none') {
        helpBox.style.display = 'block';
    } else {
        helpBox.style.display = 'none';
    }
}

document.querySelectorAll('.toggle-help-btn').forEach(btn => {
    btn.addEventListener('click', toggleHelp);
});

// game logic
let totalGL = 0;
let currentPairNumber = 1;


async function fetchFoodPair(pairNumber) {
    let response = await fetch(`${pythonURI}/api/foodchoice?number=${pairNumber}`);
    let data = await response.json();
    if (data.length === 0) {
        document.getElementById("card-container").innerHTML = "<h1 style='text-align:center;'>Good job making healthy choices!</h1>";
        return;
    }
    displayFoodPair(data);
}

async function displayFoodPair(pair) {
    let container = document.getElementById("card-container");
    container.innerHTML = "";

    pair.forEach(async food => {
        let foodCard = document.createElement("div");
        foodCard.classList.add("food-card");
        foodCard.setAttribute("data-glycemic", food.glycemic_load);
        foodCard.setAttribute("data-id", food.id);
        let imgSrc = food.image ? `/glucoquest_frontend/${food.image}` : 'default-image.jpg';

        foodCard.innerHTML = `
            <img src="${imgSrc}" alt="${food.food}">
            <div>
                <span style="color: black">${food.food}</span>
            </div>
            <div class="tooltip" id="${food.id}">Loading info...</div>
        `;

        container.appendChild(foodCard);

        try {
            const response = await fetch(new URL(`${pythonURI}/api/foodchoice/info/${food.id}`), fetchOptions);
            if (!response.ok) {
                document.getElementById(`${food.id}`).textContent = "Info not available";
                throw new Error('Failed to fetch info: ' + response.statusText);
            }
            const data = await response.json();
            document.getElementById(`${food.id}`).textContent = data.info;
        } catch (error) {
            console.error(error);
        }

        foodCard.onclick = () => {
            totalGL = roundToTwoDecimals(totalGL + food.glycemic_load);
            document.getElementById("total-gl").textContent = totalGL;

            const selectedFood = food;
            const otherFood = pair.find(f => f !== food);
            const message = checkFoodChoice(selectedFood, otherFood);
            showGlycemicLoad(currentPairNumber, selectedFood, otherFood);
            currentPairNumber++;
            fetchFoodPair(currentPairNumber);
        };
    });
}

fetchFoodPair(currentPairNumber);

Backend

I have created both a model and an API for my feature and stored the different foods there. The foods are paired and not random, so the same 2 foods are shown at the same time every time. This ensures that the foods being compared are a fair comparison for options in a meal or snack.

I do this by assigning each pair a number in the backend alongside food name, glycemic index, and image (stored locally to ensure fast loading times). Since images are stored locally, I also have a function that converts them to base64 so that they can be called within the backend/in python.

Model

class Food(db.Model):
    __tablename__ = 'foods'
    id = db.Column(Integer, primary_key=True)
    number = db.Column(Integer)
    food = db.Column(String, nullable=False)
    glycemic_index = db.Column(Integer, nullable=False)
    image = db.Column(String)

"""I then define all CRUD methods"""

def initFoods(): 
    food_data = [
        {
            "number": 1,
            "food": "Apple",
            "glycemic_index": 41,
            "image": "apple.png",
        },
        {
            "number": 1,
            "food": "Banana",
            "glycemic_index": 62,
            "image": "banana.png",
        },
        {
            "number": 2,
            "food": "Waffles",
            "glycemic_index": 77,
            "image": "waffle.png",
        },
        {
            "number": 2,
            "food": "Oatmeal",
            "glycemic_index": 53,
            "image": "oatmeal.png",
        }] 
"""etc"""

API

@food_api.route('/', methods=['GET'])
def get_food():
    try:
        pair_number = request.args.get('number', type=int)

        foods = Food.query.all()

        # Group foods by number
        food_dict = {}
        for food in foods:
            food_dict.setdefault(food.number, []).append(food)

        # Use the number if provided, otherwise pick first valid pair
        if pair_number:
            selected_foods = food_dict.get(pair_number, [])[:2]
        else:
            selected_foods = next((foods[:2] for foods in food_dict.values() if len(foods) >= 2), [])

        # If no foods matched  return empty
        if not selected_foods:
            return jsonify([]), 200

        food_data = [
            {
                'number': food.number,
                'food': food.food,
                'glycemic_index': food.glycemic_index,
                'image': f"/images/food/{food.image}" if food.image else None # ensure this filepath matches your filepath
            }
            for food in selected_foods
        ]

        return jsonify(food_data), 200
    except Exception as e:
        return jsonify({'error': 'Failed to fetch foods', 'message': str(e)}), 500