All files / munch-ease-frontend/src/components/shopping MealSelectionList.vue

0% Statements 0/163
0% Branches 0/1
0% Functions 0/1
0% Lines 0/163

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164                                                                                                                                                                                                                                                                                                                                       
<template>
  <ul>
    <li
      v-for="meal in meals"
      :key="meal.id"
    >
      <!-- Have a checkbox and card for each meal, show the image and name -->
      <!-- Emit meal-selected event on checked and meal-unselected on unchecked -->
      <input
        :id="String(meal.id)"
        type="checkbox"
        :checked="isChecked(meal)"
        :disabled="!!disabled"
        @change="mealCheckChanged"
      >
      <label
        :for="String(meal.id)"
        :style="getImageStyling(meal)"
      >
        {{ formatDate(meal.suggestedDate) }}
      </label>
    </li>
  </ul>
</template>

<script setup lang="ts">
import type { Meal } from '@/domain/types'

// MealSelectionList only needs minimal fields from Meal; recipes and extraIngredients are optional for image lookup
type MealDisplay = Pick<Meal, 'id' | 'suggestedDate'> & Partial<Pick<Meal, 'recipes' | 'extraIngredients'>>

const props = defineProps<{ meals: MealDisplay[]; checked: MealDisplay[]; disabled?: boolean }>()
const emit = defineEmits<{
  (e: 'meal-selected', meal: MealDisplay): void
  (e: 'meal-unselected', meal: MealDisplay): void
}>()

const nth = (d: number) => {
  if (d > 3 && d < 21) return 'th'
  switch (d % 10) {
    case 1:
      return 'st'
    case 2:
      return 'nd'
    case 3:
      return 'rd'
    default:
      return 'th'
  }
}


const formatDate = (date: Date | null) => {
  if (!date) return ''
  const day = date.getDate()
  return `${date.toLocaleDateString('en-AU', { weekday: 'short' })} ${day}${nth(day)}`
}

const getUrl = (meal: MealDisplay) => {
  // First non empty value in meal.recipes[*].recipe image URLs
  if (meal.recipes) {
    for (const mr of meal.recipes) {
      const imgs = mr.recipe?.imageUrls
      if (imgs && imgs.length && imgs[0]) {
        return imgs[0]
      }
    }
  }

  // First meal.extraIngredients with a product with an image
  if (meal.extraIngredients) {
    for (const ingredient of meal.extraIngredients) {
      const p = ingredient.product
      if (p?.imgLarge) return p.imgLarge
      if (p?.imgSmall) return p.imgSmall
    }
  }
  return null
}

function getImageStyling(meal: MealDisplay) {
  const imageUrl = getUrl(meal)
  if (!imageUrl) return null
  const opacity = 0.7
  return {
    background: `linear-gradient(to bottom, rgba(255, 255, 255, ${opacity}) 0%, rgba(255, 255, 255, ${opacity}) 100%), url('${imageUrl}') center/cover no-repeat`,
  }
}

function mealCheckChanged(event: Event) {
  const target = event.target
  if (!target || !(target instanceof HTMLInputElement)) return
  const mealId = parseInt(target.id)
  const meal = props.meals.find((m) => m.id === mealId)
  if (!meal) return
  if (target.checked) emit('meal-selected', meal)
  else emit('meal-unselected', meal)
}

function isChecked(meal: MealDisplay) {
  return props.checked.some((m) => m.id === meal.id)
}
</script>

<style scoped>
ul {
  padding: 0;
}

li {
  display: inline-block;
  list-style-type: none;
  border: 1px solid #ccc;
  border-radius: 0.5em;
  margin-bottom: 1em;
  padding: none;
  overflow: hidden;
  text-align: center;
}

/* Hide the default checkbox formatting, and format the card instead */
input[type='checkbox'] {
  display: none;
}

label {
  display: inline-block;
  padding: 0.1vh 0.3em;
  /* Help the visibility of the text over the image */
  background-color: rgba(255, 255, 255, 0.9);
  border: 3px solid transparent;
  border-radius: 0.5em;
  margin: 0;
  font-weight: normal;
  cursor: pointer;
  color: #3d5447;
}

input[type='checkbox']:checked + label {
  border: 3px solid #3d5447;
  text-shadow: #ccc 0 0 0.1em;
}

input[type='checkbox']:disabled + label {
  cursor: not-allowed;
}

/* Show the image as the background image of the card */
.meal-image {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Position the text in the center of the card */
label {
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: larger;
  font-weight: bold;
}
</style>