Galería de imágenes con VueJS y Supabase

Galería de imágenes con VueJS y Supabase

Aprende a integrar el sistema de almacenamiento de Supabase en tu aplicación VueJS

Introducción

En este tutorial, aprenderemos cómo crear una galería de imágenes utilizando dos poderosas herramientas: Vue.js y Supabase.

Vue.js es un framework de JavaScript progresivo que nos permite crear interfaces de usuario interactivas de forma fácil y eficiente. Por otro lado, Supabase es una plataforma de desarrollo web que brinda servicios back-end como bases de datos y almacenamiento de archivos, lo que nos permite crear fácilmente aplicaciones web completas.

A lo largo del proceso, aprenderemos a integrar VueJS y Supabase, implementar la carga de archivos y mostrar las imágenes de manera dinámica en nuestra aplicación.

Proyecto

Nos centraremos en integrar el alojamiento de Supabase en una aplicación Vue.js para crear un formulario de carga de imágenes y una galería que mostrará las imágenes almacenadas en Supabase.

Consideraciones

Para sacar el mayor provecho de este tutorial, se recomienda cumplir con los siguientes requisitos mínimos:

  • Conocimientos básicos de HTML, CSS y JavaScript: necesita conocer los conceptos básicos de HTML para crear la estructura básica de su aplicación web, CSS para diseñarla y JavaScript para agregar interactividad.

  • Conceptos básicos de Vue.js: es útil tener un conocimiento básico de Vue.js, incluida la creación de componentes, el uso de comandos y la gestión del estado mediante VueX (aunque esto no es necesario en este proyecto, pero puede ser útil en proyectos futuros).

  • Cuenta de Supabase y conceptos básicos: debe registrarse en Supabase para acceder al alojamiento y otros servicios adicionales. Además, es útil tener una comprensión básica de cómo funciona Supabase y cómo interactuar con él a través de su API.

Creación del proyecto VueJS

Comencemos creando el proyecto VueJS con el siguiente comando:

npm create vite@latest vuejs-supabase -- --template vue

Este comando creará un directorio vuejs-supabase , entra al directorio e instala las dependencias con npm install .

Vamos a usar bootstrap para los estilos, lo instalamos con el comando npm install bootstrap , luego modificamos el fichero src/main.js como sigue:

import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/js/bootstrap.js';

import './style.css';

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

Componente Form

Comenzamos creando el componente src/components/Form.vue :

<template>
    <div class="col-3">
        <div class="card">
          <div class="card-body">
            <h2 class="display-5 text-center">Subir Imágen</h2>
            <form>
              <div class="form-group mb-3 text-center">
                <img src="https://fakeimg.pl/200x200/?text=IMG" alt="vista previa" id="preview" class="img-thumbnail">
              </div>
              <div class="form-group mb-3">
                <label for="formFile" class="form-label">Imágen</label>
                <input class="form-control" type="file" id="formFile">
              </div>
              <div class="form-group mb-3">
                <label for="title">Título</label>
                <input type="text" class="form-control" id="title">
              </div>
              <div class="form-group d-grid">
                <button type="submit" class="btn btn-primary">Subir</button>
              </div>
            </form>
          </div>
        </div>
      </div>
</template>

Establecemos una sección para el componente del formulario. Definimos una vista previa, un campo de tipo file para el archivo y un campo title para ponerle un título.

Lo importamos al componente principal src/App.vue .

<script setup>
import Form from './components/Form.vue';
</script>

<template>
  <div class="container">
    <div class="row pt-5">
      <Form />
    </div>
  </div>
</template>

Vamos a crear las funciones para la vista previa y el envío del formulario. En el componente src/components/Form.vue añadimos el siguiente código:

<script setup>
import { ref } from 'vue';


const title = ref('')
const fileInput = ref('')

function handleSubmit() {
  console.log(title.value)
  console.log(fileInput.value.files[0])
  document.querySelector('#preview').src = "https://fakeimg.pl/200x200/?text=IMG";
}

function handleFileChange(event) {
  var file = event.target.files[0];
  var url = URL.createObjectURL(file);
  document.querySelector('#preview').src = url;
};
</script>

La función handleFileChange se encarga de mostrar la imagen que vamos a subir en la vista previa del formulario.

La función handleSubmit por ahora muestra los datos por consola. También declaramos dos variables reactivas title y fileInput que se encargan de recuperar los datos del formulario que luego vamos a enviar a supabase. Tenemos que vincular a los componentes como sigue:

<script setup>
import { ref } from 'vue';


const title = ref('')
const fileInput = ref('')

function handleSubmit() {
  console.log(title.value)
  console.log(fileInput.value.files[0])
  document.querySelector('#preview').src = "https://fakeimg.pl/200x200/?text=IMG";
}

function handleFileChange(event) {
  var file = event.target.files[0];
  var url = URL.createObjectURL(file);
  document.querySelector('#preview').src = url;
};
</script>

<template>
    <div class="col-3">
        <div class="card">
          <div class="card-body">
            <h2 class="display-5 text-center">Subir Imagen</h2>
            <!-- Vinculamos la función handleSubmit al evento submit del formulario -->
            <form @submit.prevent="handleSubmit">
              <div class="form-group mb-3 text-center">
                <img src="https://fakeimg.pl/200x200/?text=IMG" alt="vista previa" id="preview" class="img-thumbnail">
              </div>
              <div class="form-group mb-3">
                <label for="formFile" class="form-label">Imágen</label>
                <!-- Vinculamos la función handleFileChange al evento change del input y le añadimos la variable reactiva fileInput -->
                <input class="form-control" type="file" id="formFile" @change="handleFileChange" ref="fileInput">
              </div>
              <div class="form-group mb-3">
                <label for="title">Título</label>
                <!-- Le añadimos la variable reactiva title -->
                <input type="text" class="form-control" id="title" v-model="title">
              </div>
              <div class="form-group d-grid">
                <button type="submit" class="btn btn-primary">Subir</button>
              </div>
            </form>
          </div>
        </div>
      </div>
</template>

Finalizamos con el formulario, ahora continuamos con la galería que mostrará las imágenes.

Creamos el componente src/components/Gallery.vue .

<template>
    <div class="col-9">
        <h2 class="display-5 text-center p-3">Galería</h2>
        <hr>
        <div class="grid-container">
            <figure class="box-image">
                <img src="https://picsum.photos/600/300" class="image">
                <figcaption class="title-image d-flex justify-content-center align-items-center display-5">Título</figcaption>
            </figure>
            <figure class="box-image">
                <img src="https://picsum.photos/600/300" class="image">
                <figcaption class="title-image d-flex justify-content-center align-items-center display-5">Título</figcaption>
            </figure>
            <figure class="box-image">
                <img src="https://picsum.photos/600/300" class="image">
                <figcaption class="title-image d-flex justify-content-center align-items-center display-5">Título</figcaption>
            </figure>
        </div>
    </div>
</template>

Tenemos una grilla de imágenes, dentro de la cuadrícula tenemos un elemento figure que contiene la imagen. Utilizamos una imagen de picsum.photos de forma auxiliar por el momento.

Modificamos el fichero src/style.css para definir los estilos de la cuadrícula de imágenes.

.grid-container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 300px));
  gap: 20px;
}

.box-image {
  position: relative;
  width: 100%;
  margin: 0;
  overflow: hidden;
  border-radius: 10px;
  cursor: pointer;
}

.title-image {
  position: absolute;
  top: 300px;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0 0 0 / 0.6);
  color: #ffffff;
  transition: 0.3s ease-in-out;
}

.box-image:hover .title-image {
  top: 0px;
}

.image {
  width: 100%;
  object-fit: cover;
}

Ahora importamos el componente src/components/Gallery.vue justo debajo del componente del formulario.

<script setup>
import Form from './components/Form.vue';
import Gallery from './components/Gallery.vue';
</script>

<template>
  <div class="container">
    <div class="row pt-5">
      <Form />
      <Gallery />
    </div>
  </div>
</template>

Obtenemos la siguiente pantalla.

Ahora los elementos figure que se muestran están deforma estática, vamos a crear un componente para poder mostrarlos de forma dinámica.

Componente BoxImage

Creamos el componente src/components/BoxImage.vue :

<script setup>
const { img } = defineProps({img: { type: Object, required: true}});
</script>
<template>
    <figure class="box-image">
        <img :src="img.url" class="image">
        <figcaption class="title-image d-flex justify-content-center align-items-center display-5">{{img.title}}</figcaption>
    </figure>
</template>

Definimos el props img el cuál es un objeto que contiene la url y el título de la imagen. Pasamos la url a la imagen por medio de <img :src="img.url"> y pasamos el título dentro del figcaption con {{ img.title }} . Ahora debemos importar este componente dentro de la galería como sigue:

<script setup>
import { ref } from 'vue'
import BoxImage from './BoxImage.vue'

const data = ref([
  {
    "id": "0",
    "title": "Polaroid",
    "url": "https://picsum.photos/600/300?image=0"
  },
  {
    "id": "1",
    "title": "Skyscraper",
    "url": "https://picsum.photos/600/300?image=1"
  },
  {
    "id": "2",
    "title": "Sunset",
    "url": "https://picsum.photos/600/300?image=2"
  },
  {
    "id": "3",
    "title": "Nature",
    "url": "https://picsum.photos/600/300?image=3"
  },
  {
    "id": "4",
    "title": "Abstract",
    "url": "https://picsum.photos/600/300?image=4"
  },
  {
    "id": "5",
    "title": "City",
    "url": "https://picsum.photos/600/300?image=5"
  },
  {
    "id": "6",
    "title": "Mountains",
    "url": "https://picsum.photos/600/300?image=6"
  },
  {
    "id": "7",
    "title": "Ocean",
    "url": "https://picsum.photos/600/300?image=7"
  },
  {
    "id": "8",
    "title": "Forest",
    "url": "https://picsum.photos/600/300?image=8"
  },
  {
    "id": "9",
    "title": "Bridge",
    "url": "https://picsum.photos/600/300?image=9"
  }
])
</script>
<template>
    <div class="col-9">
        <h2 class="display-5 text-center p-3">Galería</h2>
        <hr>
        <div class="grid-container">
            <BoxImage v-for="img in data" :key="img.id" :img="img"/>
        </div>
    </div>
</template>

Definimos un array de objetos data que contiene nuestras imágenes, el cuál iteramos con <BoxImage v-for"img in data" :key="img.id" :img="img"> , por cada item en data creamos una nueva instancia del componente BoxImage. Con esto obtenemos la siguiente pantalla:

Ya tenemos todo listo en el front-end, ahora pasamos a configurar nuestro proyecto en supabase.

Supabase

Una vez que hayas ingresado a tu cuenta de Supabase, en el dashboard, crea un nuevo proyecto, yo lo llamé "vuejs-supabase".

Una vez finalice la configuración, crea un fichero .env.local en la raíz de tu proyecto, haz clic en el botón "connect" en la esquina superior derecha, en la opción de "App Frameworks" elige la "VueJS" y copia las variables de entorno que aparecen en la caja de texto. Son los tokens que te permitirán conectarte al proyecto de Supabase.

Debes pegar estas variables de entorno en el fichero .env.local . Sin embargo, debemos realizar un ligero cambio. Como creamos nuestro proyecto VueJS con Vite, tenemos que añadir a las variables el prefijo VITE:


VITE_SUPABASE_URL=https://rbjwpbyyzrsejoomunlr.supabase.co
VITE_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJiandwYnl5enJzZWpvb211bmxyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTU3OTg3NzcsImV4cCI6MjAzMTM3NDc3N30.SvR03dwUbR1qrO4duqz7wkJSVjdeVlxvK7t3USd5iDQ

Continuamos con la creación de la tabla en la base de datos y el bucket en el storage para las imágenes.

Tabla Images

Ve a la opción "Table Editor" y haz clic en el botón "Create a new table". Coloca el nombre "images" en el campo Name. Opcionalmente puedes añadir una descripción. Como no vamos a usar autenticación, deshabilitamos la opción "Enable Row Level Security". En la sección Columns añadimos los campos title y url_image, de tipo VarChar.

Continuamos con la creación del bucket images para las imágenes.

Bucket Images

Ve a la opción "Storage", haz clic en el botón "New bucket" y ponle el nombre "images". Establece el bucket como público y guarda. Ahora, debemos establecer las políticas del bucket, que determinan qué tipo de operaciones podemos realizar en el bucket. Haz clic en la opción "Policies", luego en el botón "New policy", elige la opción "For full customization", y selecciona cada una de las operaciones que deseas realizar en el bucket. Finalmente, guarda.

Ya tenemos todo listo para operar con la base de datos y el bucket. Continuemos con la instalación de las dependencias de Supabase y la función para subir imágenes y guardar los datos en la tabla.

Instalación de la librería cliente de Supabase

La librería cliente supabase-js proporciona una interfaz conveniente para trabajar con Supabase desde una aplicación VueJS. Para instalarla, ejecuta el siguiente comando:

npm install @supabase/supabase-js

Continuemos con la configuración.

Configuración del cliente supabase

Crea un fichero src/supabase/supabase.js y añade el siguiente código:

import { createClient } from "@supabase/supabase-js";

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_KEY;

export const supabase = createClient(supabaseUrl, supabaseKey);

Este código configura e inicializa un cliente de Supabase. Importa la función createClient del paquete @supabase/supabase-js. Esta función crea una instancia del cliente de Supabase, que te permitirá interactuar con los servicios de Supabase, como la base de datos, autenticación y almacenamiento. supabaseUrl y supabaseKey son constantes que almacenan la URL del proyecto de Supabase y la clave de acceso, respectivamente. import.meta.env es una forma de acceder a las variables de entorno definidas en archivos de configuración (como .env.local) en un proyecto utilizando Vite. VITE_SUPABASE_URL y VITE_SUPABASE_KEY son las variables de entorno que deben estar definidas en tu archivo .env.local. Se utiliza la función createClient con supabaseUrl y supabaseKey para crear una instancia del cliente de Supabase. Esta instancia (supabase) se exporta para que pueda ser utilizada en otras partes de la aplicación. Con esta instancia, podrás realizar operaciones como consultar la base de datos, subir archivos, etc.

Subir imagen y guardar datos en la tabla

Lo primero que debemos hacer es importar la instancia del cliente de supabase en el componente del formulario src/components/Form.vue :

import { supabase } from '../supabase/supabase';

Una vez importamos la instancia, modificamos la función handleSubmit como sigue:

async function handleSubmit(e) {
  //Obtenemos el fichero y lo subimos al bucket images de nuestro storage
  const file = fileInput.value.files[0];
  await supabase.storage.from('images').upload(file.name, file);

  //Obtenemos la url pública de la image que acabamos de subir al bucket
  const { data: { publicUrl } } = await supabase.storage.from('images').getPublicUrl(file.name);

  document.querySelector('#preview').src = "https://fakeimg.pl/200x200/?text=IMG";

  //Insertamos un nuevo registro en la tabla images con el título y la url pública
  await supabase.from('images').insert([{
    title: title.value,
    url_image: publicUrl,
  },]).select();

  //Reseteamos el formulario y reseteamos las variables reactivas
  e.target.reset();
  title.value = "";
  fileInput.value = "";

}

Este código define una función handleSubmit que gestiona el proceso de subir una imagen a Supabase Storage, obtener su URL pública, insertar un registro en una tabla de base de datos de Supabase con esa URL, y finalmente, resetear el formulario.

const file = fileInput.value.files[0]; : Se obtiene el primer archivo seleccionado por el usuario desde el input de archivo. fileInput es una referencia al elemento de input de tipo file en el DOM.

await supabase.storage.from('images').upload(file.name, file); : Utiliza el método upload del cliente de Supabase Storage para subir el archivo al bucket llamado 'images'. file.name es el nombre del archivo y file es el objeto del archivo.

const { data: { publicUrl } } = await supabase.storage.from('images').getPublicUrl(file.name); : Utiliza el método getPublicUrl para obtener la URL pública del archivo subido. publicUrl es la URL que permite acceder al archivo de forma pública.

javascriptCopiar códigoawait supabase.from('images').insert([{
  title: title.value,
  url_image: publicUrl,
}]).select();

Inserta un nuevo registro en la tabla 'images' de la base de datos de Supabase. El registro incluye el título (title.value) y la URL de la imagen (publicUrl). Se usa select() para devolver el registro insertado.

e.target.reset();
title.value = "";
fileInput.value = "";

Resetea el formulario después de la subida. e.target.reset() limpia todos los campos del formulario. Además, se restablecen manualmente los valores de title y fileInput a una cadena vacía.

Continuemos con la recuperación de los datos para mostrarlos en la grilla de imágenes.

Seleccionar registros de la tabla images

Para obtener todos los registros de la tabla images y mostralos en la grilla debemos realizar los siguientes cambios en el componente src/components/Gallery.vue :

import { onMounted, ref } from 'vue'
import BoxImage from './BoxImage.vue'
import { supabase } from '../supabase/supabase';

const arrayImages = ref([]);

onMounted(async() => {  
  let { data } = await supabase.from('images').select('*');
  arrayImages.value = data;  

})

Importamos onMounted, una función de Vue Composition API que se usa para ejecutar un bloque de código cuando el componente se monta (es decir, se inserta en el DOM).

Renombramos la variable reactiva data a un nombre más descriptivo, arrayImages.

La línea let { data } = await supabase.from('images').select('*'); realiza una consulta a la tabla images en Supabase para seleccionar todos los registros. La función supabase.from('images').select('*') devuelve una promesa que se resuelve con un objeto que contiene los datos (data). Luego, asignamos los datos obtenidos de Supabase a arrayImages.value. Dado que arrayImages es una referencia reactiva, cualquier cambio en su valor provocará una actualización automática en la vista donde se usa arrayImages.

Debemos pasar los datos de arrayImages al componente BoxImage.vue :

<BoxImage v-for="img in arrayImages" :key="img.id" :img="img"/>

Dentro del componente BoxImage.vue debemos corregir el valor que le pasamos al atributo dinámico :src de la etiqueta img, ya que el campo que guarda la url pública lo llamamos url_image :

<template>
    <figure class="box-image">
        <!-- Cambiamos el valor de img.url a img.url_public -->
        <img :src="img.url_image" class="image">
        <figcaption class="title-image d-flex justify-content-center align-items-center display-5">{{img.title}}</figcaption>
    </figure>
</template>

Al guardar todos los cambios ya deberías ver la imagen que subimos a Supabase en la grilla.

Sin embargo tenemos un problema, si subimos una nueva imagen, la grilla no se actualiza. Debemos refrescar la página para obtener la nueva imagen.

Para resolver esto debemos configurar un canal de suscripción en Supabase para escuchar cambios en la tabla images, lo que nos permitirá actualizar los datos en la aplicación Vue.js en tiempo real.

Método Subscribe

Volvemos a Supabase, en el Table Editor, hacemos clic sobre la tabla images y luego en el botón "Realtime off" que está en la esquina superior derecha y lo habilitamos.

Ahora volvemos al componente Gallery.vue y justo debajo de arrayImages creamos la siguiente función:

const channels = supabase.channel('custom-insert-channel')
  .on(
    'postgres_changes',
    { event: 'INSERT', schema: 'public', table: 'images' },
    (payload) => {
      arrayImages.value.push(payload.new);
    }
  )
  .subscribe()

supabase.channel('custom-insert-channel') crea un nuevo canal de Supabase con el nombre 'custom-insert-channel'. Este canal se utiliza para recibir eventos en tiempo real relacionados con los cambios en la base de datos.

.on configura un oyente de eventos para el canal creado. 'postgres_changes' especifica el tipo de evento a escuchar, que en este caso son los cambios en PostgreSQL. { event: 'INSERT', schema: 'public', table: 'images' } es un objeto que define el filtro para los eventos que queremos escuchar. Aquí estamos interesados en eventos de tipo INSERT en la tabla images del esquema public.

(payload) => { arrayImages.value.push(payload.new); } es una función callback que se ejecuta cada vez que ocurre un evento que coincide con el filtro. El payload contiene los datos del evento, y payload.new contiene el nuevo registro que se ha insertado en la tabla images. Dentro del callback, arrayImages.value.push(payload.new); agrega el nuevo registro al array reactivo arrayImages. Esto asegura que la lista de imágenes en la aplicación Vue.js se actualice automáticamente cuando se inserte una nueva imagen en la base de datos.

Con esto hemos finalizado el tutorial.

Conclusión

En este tutorial, hemos aprendido cómo integrar Vue.js con Supabase para crear una aplicación web dinámica y reactiva. Hemos cubierto los siguientes aspectos:

  1. Configuración de Supabase y Vue.js: Desde la instalación de las dependencias hasta la configuración inicial de ambas herramientas.

  2. Subida de Imágenes: Implementamos un formulario para subir imágenes a Supabase Storage y obtener su URL pública.

  3. Almacenamiento en Base de Datos: Guardamos los detalles de las imágenes en una tabla de la base de datos de Supabase.

  4. Mostrar Imágenes en una Galería: Creamos una galería que muestra todas las imágenes almacenadas, recuperándolas de la base de datos.

  5. Actualización en Tiempo Real: Configuramos suscripciones en Supabase para actualizar automáticamente la galería cada vez que se inserta una nueva imagen.

Este proyecto no solo muestra las capacidades de Supabase como backend, sino también cómo Vue.js facilita la creación de interfaces de usuario reactivas y eficientes. Al combinar ambas herramientas, pudimos crear una aplicación moderna y funcional con una experiencia de usuario fluida y en tiempo real.

Puedes encontrar el proyecto completo en mi repositorio de GitHub haciendo clic AQUÍ.

Espero que lo hayas encontrado entretenido, instructivo y claro. Si tienes alguna duda, puedes hacérmelo saber en los comentarios. Pronto estaré subiendo más tutoriales.

Nos vemos en la próxima. Saludos!👋😊