baublet/w8mngr

View on GitHub
client/components/Foods/FoodListItem.tsx

Summary

Maintainability
D
1 day
Test Coverage
import cx from "classnames";
import React from "react";
import { Link, useHistory } from "react-router-dom";

import { or } from "../../../shared";
import { foodLogLocalStorage } from "../../helpers";
import { GhostInvertedButton } from "../Button/GhostInverted";
import { SystemGhostIconButton } from "../Button/SystemGhostIcon";
import { Input } from "../Forms";
import { Add } from "../Icons/Add";
import { ItemHeading } from "../Type/ItemHeading";

export function FoodsListItem({
  id,
  name,
  description,
  previewImageUrl,
  measurements,
}: {
  id: string;
  name: string;
  description?: string | null;
  previewImageUrl?: string | null;
  measurements: {
    edges: {
      node: {
        id: string;
        amount: number;
        measurement: string;
        calories: number;
        fat: number;
        carbs: number;
        protein: number;
      };
    }[];
  };
}) {
  const [showAll, setShowAll] = React.useState(false);

  const trimmedMeasurements = React.useMemo(() => {
    if (measurements.edges.length < 3 || showAll) {
      return measurements;
    }
    return {
      edges: measurements.edges.slice(0, 3),
    };
  }, [showAll]);

  const hasMore =
    trimmedMeasurements.edges.length !== measurements.edges.length;

  return (
    <div
      className={`
      hover:bg-slate-50
      hover:bg-opacity-50
      border
      border-slate-100
      hover:border-slate-300
      rounded-lg
      p-4
      hover:shadow-slate-500/10
      hover:shadow-lg
    `}
    >
      <div
        className={`
flex
flex-col
w-full
`}
      >
        <Link
          to={`/foods/edit/${id}`}
          className="flex w-full justify-start items-center text-left"
          title={`Edit Food: ${name}`}
        >
          {previewImageUrl && (
            <div className="self-start">
              <img
                src={previewImageUrl}
                className="w-24 h-auth rounded"
                title={or(description, "Food preview")}
              />
            </div>
          )}
          <div
            className={cx("flex w-full flex-col", {
              "ml-4": previewImageUrl,
            })}
          >
            <ItemHeading>{name}</ItemHeading>
            {description && (
              <div className="block mt-2 text-slate-700 text-opacity-80 leading-tight">
                {description.substr(0, 240)}
                {description.length > 240 ? "..." : ""}
              </div>
            )}
          </div>
        </Link>
        {trimmedMeasurements.edges.length === 0 ? null : (
          <div className="group mt-2">
            {trimmedMeasurements.edges.map((measurement) => (
              <Measurement
                key={measurement.node.id}
                {...measurement.node}
                foodName={name}
              />
            ))}
            <div className="w-full flex justify-end">
              {hasMore ? (
                <GhostInvertedButton
                  className="bg-slate-100 bg-opacity-50 text-slate-300 font-bold uppercase"
                  onClick={() => setShowAll(true)}
                  size="extra-small"
                  full
                >
                  {Math.abs(
                    trimmedMeasurements.edges.length - measurements.edges.length
                  )}{" "}
                  more
                </GhostInvertedButton>
              ) : null}
            </div>
            <div className="mt-2 p-2 gap-4 border-t border-slate-100 w-full hidden sm:flex">
              <Label className="w-3/12 sm:w-2/12 lg:w-1/12">amount</Label>
              <Label className="w-4/12 sm:3/12 lg:w-4/12">measurement</Label>
              <Label className="w-3/12 lg:w-3/12">calories</Label>
              <Label className="w-3/12 lg:w-1/12">fat</Label>
              <Label className="w-4/12 lg:w-1/12">carbs</Label>
              <Label className="w-3/12 lg:w-1/12">protein</Label>
              <div className="w-3/12 lg:w-1/12 flex justify-end"></div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function Label({
  children,
  className,
}: React.PropsWithChildren<{ className?: string }>) {
  return (
    <div
      className={cx(
        "text-xs text-slate-400 group-hover:text-slate-600",
        className
      )}
    >
      {children}
    </div>
  );
}

function Measurement({
  foodName,
  amount,
  measurement,
  calories,
  carbs,
  fat,
  protein,
}: {
  foodName: string;
  id: string;
  amount: number;
  measurement: string;
  calories: number;
  fat: number;
  carbs: number;
  protein: number;
}) {
  const [uiMutableAmount, setAmount] = React.useState(amount);
  const amountString = uiMutableAmount.toString();
  const { push } = useHistory();

  const logThisEntry = () => {
    const foodLogStorage = foodLogLocalStorage();
    foodLogStorage.addItem({
      description: `${amountString} ${measurement} ${foodName}`,
      calories: measurementStringToNumberOrUndefined(
        getMeasurementWithMultiplier({
          currentAmount: uiMutableAmount,
          originalAmount: amount,
          measurementValue: calories,
        })
      ),
      fat: measurementStringToNumberOrUndefined(
        getMeasurementWithMultiplier({
          currentAmount: uiMutableAmount,
          originalAmount: amount,
          measurementValue: fat,
        })
      ),
      carbs: measurementStringToNumberOrUndefined(
        getMeasurementWithMultiplier({
          currentAmount: uiMutableAmount,
          originalAmount: amount,
          measurementValue: carbs,
        })
      ),
      protein: measurementStringToNumberOrUndefined(
        getMeasurementWithMultiplier({
          currentAmount: uiMutableAmount,
          originalAmount: amount,
          measurementValue: protein,
        })
      ),
    });
    push("/foodlog");
  };

  return (
    <div className="w-full flex items-center gap-4 border-t p-2 border-slate-100 text-xs uppercase text-slate-500 hover:bg-slate-100 rounded">
      <div className="flex flex-wrap sm:flex-nowrap items-center gap-4 flex-grow w-full">
        <div className="w-3/12 sm:w-2/12 lg:w-1/12">
          <Input
            onChange={(value) => {
              const amount = parseInt(value, 10);
              if (isNaN(amount)) {
                setAmount(0);
                return;
              }
              setAmount(amount);
            }}
            type="text"
            value={amountString}
            placeholder={amountString}
            label="amount"
            showLabel={false}
          />
        </div>
        <div className="w-4/12 sm:3/12 lg:w-4/12">{measurement}</div>
        <div className="w-3/12 lg:w-3/12 text-lg font-thin">
          {getMeasurementWithMultiplier({
            currentAmount: uiMutableAmount,
            originalAmount: amount,
            measurementValue: calories,
          })}
          <Label className="sm:hidden">calories</Label>
        </div>
        <div className="w-3/12 lg:w-1/12 text-lg font-thin">
          {getMeasurementWithMultiplier({
            currentAmount: uiMutableAmount,
            originalAmount: amount,
            measurementValue: fat,
          })}
          <span className="opacity-75 lowercase"> g</span>
          <Label className="sm:hidden">fat</Label>
        </div>
        <div className="w-4/12 lg:w-1/12 text-lg font-thin">
          {getMeasurementWithMultiplier({
            currentAmount: uiMutableAmount,
            originalAmount: amount,
            measurementValue: carbs,
          })}
          <span className="opacity-75 lowercase"> g</span>
          <Label className="sm:hidden">carbs</Label>
        </div>
        <div className="w-3/12 lg:w-1/12 text-lg font-thin">
          {getMeasurementWithMultiplier({
            currentAmount: uiMutableAmount,
            originalAmount: amount,
            measurementValue: protein,
          })}
          <span className="opacity-75 lowercase"> g</span>
          <Label className="sm:hidden">protein</Label>
        </div>
      </div>
      <div className="w-3/12 lg:w-1/12 flex justify-end">
        <SystemGhostIconButton
          onClick={logThisEntry}
          title={`Log ${amountString} ${measurement} ${foodName}`}
        >
          <Add />
        </SystemGhostIconButton>
      </div>
    </div>
  );
}

export function measurementStringToNumberOrUndefined(
  subject?: string
): number | undefined {
  if (subject === undefined) {
    return undefined;
  }
  const numeric = parseInt(subject.replace(/,/g, ""), 10);
  if (isNaN(numeric)) {
    return undefined;
  }
  return numeric;
}

export function getMeasurementWithMultiplier({
  currentAmount,
  measurementValue,
  originalAmount,
}: {
  originalAmount: number;
  currentAmount: number;
  measurementValue: number;
}): string {
  const originalAmountOr1 = originalAmount === 0 ? 1 : originalAmount;
  const currentAmountOr1 = currentAmount === 0 ? 1 : currentAmount;
  const multiplier = currentAmountOr1 / originalAmountOr1;
  const finalNumber = Math.ceil(measurementValue * multiplier);
  if (isNaN(finalNumber)) {
    return "0";
  }
  return finalNumber.toLocaleString();
}