Vue adventure: image gallery

Vue adventure: image gallery

Since this is my first post on this blog, you are probably unaware that I maintain a way to complex system primarily used for registering rides and reporting damage to vehicles.

Unfortunately recently the feature for reporting damage to vehicles has started to be used more frequently, which exposed some shortcomings.

Can you spot the problem?

Next to the fact that the image consumes the entire width of the screen, it's also not fully loading... and as of writing this part of the article I have no clue as to why. Refreshing it a few times causes it to fully load, but this only works on Firefox.

The first thing I decided to tackle is creating the basic components and generating a grid "preview" from that, I ended up with the following setup:

<media-gallery>
    <media-gallery-item type="photo" url="<SOME PRETTY PICTURE>"></media-gallery-item>
</media-gallery>

With some mock data that ended up looking like this:

So now we can at least see all pictures a driver has uploaded... but it's a little too small to see any detailed damage, to solve this I added a modal which pops up when the user clicks on a thumbnail. It's a bit basic, but it will (hopefully) do for now:

Now we can see all pictures attached to a damage report, click on them to enlarge them (and of course we can close out of the modal to go back to the overview.) So for now I'm going to consider this mission accomplished!

Components / Stylesheets

MediaGallery.vue

<script setup>

</script>

<template>
  <div class="gallery">
    <slot></slot>
  </div>
</template>

<style scoped>

</style>

MediaGalleryItem.vue

<script setup>
import {nextTick, onMounted, ref} from "vue";

const props = defineProps(['type', 'path', 'alt']);

const modal = ref(null);
const id = ref(null);
const ytId = ref(null);

const showModal = () => {
    modal.value.show();
};

onMounted(() => {
  id.value = Math.random().toString(16);

  if (props.type === 'youtube') {
    ytId.value = props.path.substr(props.path.indexOf('?v=') + 3);
  }

  nextTick(() => {
    modal.value = new window.bootstrap.Modal(`#modal-${id.value}`);
  });
});
</script>

<template>
  <div v-if="type === 'image'" @click="showModal()" class="gallery-thumb" :style="`background: url(${props.path}); background-position: center center; background-size: cover;`" />
  <div v-if="type === 'youtube' && ytId !== null" @click="showModal()" class="gallery-thumb" :style="`background: url('http://i3.ytimg.com/vi/${ytId}/hqdefault.jpg'); background-position: center center; background-size: cover;`" />
  <div class="modal modal-xl fade" :id="`modal-${id}`" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title">{{ props.alt ?? 'Media' }}</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          <img v-if="type === 'image'" :src="props.path" :alt="props.alt ?? ''" style="width: 100%;">
          <iframe v-if="type === 'youtube'" height="615" style="width: 100%;" :src="`https://www.youtube.com/embed/${ytId}`" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Sluiten</button>
        </div>
      </div>
    </div>
  </div>
</template>

gallery.scss

.gallery {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: left;
  align-content: center;
}

.gallery-thumb {
  cursor: pointer;
  margin-right: 5px;
  margin-bottom: 5px;
  width: 150px !important;
  height: 150px;
  border-style: solid;
  border-radius: 25px;
  border-width: 1px;
  border-color: var(--theme-content-border-color);
}