En este post vamos a aprender a crear una aplicación VUE que será capaz de:

  • Registrar usuarios
  • Hacer Login con un usuario existente
  • Recuperar la contraseña de un usuario
  • Subir archivos asociados a una cuenta
  • Listar y descargar los archivos de un usuario

Para ello vamos a utilizar Firebase como backend, en concreto vamos a utilizar el módulo de Authentication para administrar los usuarios y el módulo de Storage para alamacenar los archivos del usuario.

Configuración inicial Firebase

Para poder enlazar el proyecto de Firebase con VUE, debemos de crear una cuenta de servicio de Firebase, para ello accedemos a la consola de nuestro proyecto Firebase.

Accedemos a los ajustes

Configuración del proyecto

Seleccionamos el apartado de cuentas de servicio y generamos una nueva clave privada, esto nos descargará un fichero JSON que tendremos que importar en nuestro proyecto VUE.

Configuración inicial VUE

Para crear el proyecto de VUE, primero debemos asegurarnos de que tenemos instalado NodeJS y el CLI de VUE, lo podemos instalar con el comando:

npm install -g vue-cli

Ahora ya podemos crear el proyecto de VUE, para ello abrimos la consola del sistema en la ruta en la que queramos generar el proyecto, cuando estemos en la ruta, creamos el proyecto con el siguiente comando:

vue create [nombreDeTuProyecto]

*Si lo hacemos desde Windows, debemos usar cmd no PowerShell

Cuando tengamos el proyecto creado, lo abrimos con algún editor de código, en mi caso voy a usar VSCode.

Instalamos las dependencias que vamos a necesitar:

Vue-Router : Nos permite añadir routing a nuestro proyecto

npm install --save vue-router

Firebase : Nos permite acceder a las funcionalidades de Firebase

npm install --save firebase

Sweetalert2 : Nos ayuda a generar alertas para el usuario

npm install --save sweetalert2

Implementación del Router

El router nos va a permitir navegar entre componentes, es decir cargar un componente u otro dependiendo de la ruta en la que nos encontremos, importaremos el router en el fichero main.js

//main.js

//importación del router
import Vue from 'vue';
import App from './App.vue';
import VueRouter from 'vue-router';

//añadimos el router al proyecto
Vue.use(VueRouter);

//creamos las rutas que vamos a necesitar
const routes = [
  {path:"/", component: Home},
  {path:"/register", component: Register},
  {path:"/login", component: Login},
  {path:"/dashboard", component: Dashboard},
  {path:"/upload", component:UploadFile},
  {path:"/home", component :Home},
  {path:"/forgot", component: ForgotPassword}
];

//creamos el objeto router
const router = new VueRouter({
  routes,
  mode:"history"
})

//añadimos el router al arrancar la app
new Vue({
  router,
  render: h => h(App),
}).$mount('#app')


Componentes

Ahora vamos a ir viendo los componentes que tiene la aplicación, funcionalidad, estructura y métodos clave.

NavBar.vue

Este componente se encargará de mostrar las distintas opciones de navegación y en caso de que un usuario tenga iniciada la sesión, mostrará sus datos en el desplegable ‘Profile’ y dará la opción de cerrar la sesión:

Mostrar Rutas

Para mostrar un botón por cada ruta a la que podemos navegar, dentro del template del componente debemos crear tantos <router-link> como opciones queramos tener en nuestro NavBar, en mi caso he usado una plantilla de bootstrap, cada <router-link> tiene una propiedad «to=» que señala la ruta a la que se quiere navegar cuando se haga click en el elemento.

//NavBar.vue

<template>
  <nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container-fluid">
      <a class="navbar-brand"><img src="../assets/images/logo_transparent.png" width="50px"/>OpenDrive</a>
      <button
        class="navbar-toggler"
        type="button"
        data-bs-toggle="collapse"
        data-bs-target="#navbarNavAltMarkup"
        aria-controls="navbarNavAltMarkup"
        aria-expanded="false"
        aria-label="Toggle navigation"
      >
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarNavAltMarkup">
        <div class="navbar-nav">
          <router-link class="nav-link" to="/home">Home</router-link>
          <router-link class="nav-link" to="/login">Login</router-link>
          <router-link class="nav-link" to="/register">Register</router-link>
          <router-link class="nav-link" to="/upload">Upload</router-link>
          <router-link class="nav-link" to="/dashboard">Dashboard</router-link>
        </div>
        <span class="nav-item dropdown">
          <a
            class="nav-link dropdown-toggle"
            href="#"
            id="navbarDropdown"
            role="button"
            data-bs-toggle="dropdown"
            aria-expanded="false"
            @click="mostrarNombre()"
          >
            Profile
            <img
              id="icon"
              class=""
              src="https://cdn-icons-png.flaticon.com/512/848/848043.png"
            />
          </a>
          <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
            <li><a class="dropdown-item disabled">{{user}}</a></li>
            <li><hr class="dropdown-divider"></li>
            <li>
              <a @click="signOut()" class="dropdown-item" href="#"
                >Close Session</a
              >
            </li>
          </ul>
        </span>
      </div>
    </div>
  </nav>
</template>

Mostrar información del usuario si existe

Para mostrar la información del usuario, si existe, dentro del desplegable, mostramos el valor de la variable user, está inicializada vacía, es decir si no existe un usuario, se pinta vacío, si existe un usuario, se guarda el nombre dentro de la variable y se pinta el nombre.

Esto se consigue llamando al método mostrarNombre() cada vez que se hace click en el desplegable

<script>
import { getAuth } from "firebase/auth";
import Swal from "sweetalert2";
export default {
  name: "NavBar",
  data () {
    return {
      auth : "",
      user:""
    }
  },
  methods: {
    signOut() {
      Swal.fire({
        title: "Are you sure?",
        text: "You will close your session",
        icon: "info",
        showCancelButton: true,
        confirmButtonColor: "#3085d6",
        cancelButtonColor: "#d33",
        confirmButtonText: "Close Session",
      }).then((result) => {
        if (result.isConfirmed) {
          this.auth = getAuth();
          this.auth.signOut();
          this.$router.push("/home");
          Swal.fire({
            position: "top-end",
            icon: "success",
            title: "Session Closed",
            showConfirmButton: false,
            timer: 1500,
          });
        }
      });
    },
    mostrarNombre() {
      const auth = getAuth();
      if (auth.currentUser != null) {
        this.user = auth.currentUser.email;
      }else {
        this.user = "";
      }
    }
  },
};
</script>

Cerrar sesión de un usuario

Para cerrar la sesión de un usuario llamamos al método signOut que a su vez llama al método signOut() de nuestro objeto auth, que es la referencia a nuestro módulo de autenticación en Firebase, para confirmar que el usuario quiere cerrar la sesión mostramos una alerta en la que el usuario puede confirmar la acción o cancelarla.

Register.vue

Este componente se encarga de registrar un usuario y mostrar el resultado de su registro, tanto si ha sido correcto, como si ha habido algún error

Registrar Usuario

Para registrar un usuario debemos crear una referencia de nuestro módulo authentication de Firebase, para ello empleamos el método getAuth().

Cuando tenemos la referencia de nuestro módulo vamos a usar el método createUserWithEmailAndPassword() de Firebase, este recibirá por parámetro nuestra referencia de auth, el email que queremos registrar y la contraseña con la que queremos registrar al usuario.

Al tratarse de un formulario, debemos tener en cuenta que cuando hagamos submit no queremos que tenga el comportamiento por defecto, es decir, no queremos que se recargue la página por lo que en el evento submit del formulario debemos poner un prevent.

//Register.vue

<template>
  <body class="text-center">
    <main class="form-signin">
      <form @submit.prevent="register()">
        <img class="mb-4" src="../../assets/images/logo_transparent.png" alt="" width="100" height="100" />
        <h1 class="h3 mb-3 fw-normal">Register</h1>

        <div class="form-floating">
          <input
            type="email"
            class="form-control"
            id="floatingInput"
            placeholder="name@example.com"
            v-model="email"
          />
          <label for="floatingInput">Email address</label>
        </div>
        <div class="form-floating">
          <input
            type="password"
            class="form-control"
            id="floatingPassword"
            placeholder="Password"
            v-model="password"
          />
          <label for="floatingPassword">Password</label>
        </div>
        <button class="w-100 btn btn-lg btn-primary" type="submit">
          Register
        </button>
        <p class="mt-5 mb-3 text-muted">&copy; Diego Plaza 2021</p>
      </form>
    </main>
  </body>
</template>

<script>
import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";
import Swal from "sweetalert2";

export default {
  name: "Register",
  data() {
    return {
      email: "",
      password: ""
    };
  },
  methods: {
    register() {
      const auth = getAuth();
      createUserWithEmailAndPassword(auth, this.email, this.password)
        .then((res) => {
          const user = res.user;
          console.log(user);
          Swal.fire({
            position: "top-end",
            icon: "success",
            title:
              "Usuario <b>" + this.email + "</b> Registrado correctamente",
            showConfirmButton: false,
            timer: 3000,
          });
        })
        .catch((err) => {
          //console.log(err.message);
          Swal.fire({
            icon: "error",
            title: "Error.",
            text: err.message,
            footer: '<a href="">Why do I have this issue?</a>',
          });
        });
    },
  },
};
</script>

El método createUserWithEmailAndPassword() devuelve una promesa en la que podemos comprobar si el resultado ha sido correcto o no, en caso de que sea correcto mostramos una alerta en la que informamos al usuario de que se ha registrado correctamente.

En caso de que no haya sido correcto, mostramos un error al usuario indicando el motivo.

Login.vue

Este componente se encarga de validar el inicio de sesión del usuario, mostrar información sobre errores si se producen y redirigirte a tu dashboard si el inicio de sesión es válido.

Además nos da la opción de recuperar la contraseña si se nos ha olvidado.

Iniciar Sesión

Para iniciar la sesión nos encontramos con un formulario que nos pide el usuario y la contraseña con la que queremos entrar, si existe algún error nos aparecerá una ventana que nos informará del error.

Tenemos que tener en cuenta que cuando el usuario hace click en Login no tenemos que recargar la página, que es el comportamiento por defecto del formulario, por lo que tenemos que hacer un prevent en el evento submit del formulario

//login.vue

<template>
  <body class="text-center">
    <main class="form-signin">
      <form @submit.prevent="login()">
        <img class="mb-4" src="../../assets/images/logo_transparent.png" alt="" width="100" height="100" />
        <h1 class="h3 mb-3 fw-normal">Login</h1>

        <div class="form-floating">
          <input
            type="email"
            class="form-control"
            id="floatingInput"
            placeholder="name@example.com"
            v-model="email"
          />
          <label for="floatingInput">Email address</label>
        </div>
        <div class="form-floating">
          <input
            type="password"
            class="form-control"
            id="floatingPassword"
            placeholder="Password"
            v-model="password"
          />
          <label for="floatingPassword">Password</label>
        </div>

        <div class="checkbox mb-3">
          <router-link to="/forgot">
            Forgot Password?
          </router-link>
        </div>
        <button class="w-100 btn btn-lg btn-primary" type="submit">
          Login
        </button>
        <p class="mt-5 mb-3 text-muted">&copy; Diego Plaza 2021</p>
      </form>
    </main>
  </body>
</template>

<script>
import { getAuth, signInWithEmailAndPassword, onAuthStateChanged } from "firebase/auth";
import Swal from "sweetalert2";

export default {
  name: "Login",
  data() {
    return {
      email: "",
      password: "",
    };
  },
  methods: {
    login() {
      const auth = getAuth();
      auth.setPersistence("local");
      signInWithEmailAndPassword(auth, this.email, this.password)
        .then((res) => {
          console.log(res);
          this.$router.push("/dashboard");
        })
        .catch((err) => {
          console.log(err.message);
          Swal.fire({
            icon: "error",
            title: "Error.",
            text: err.message,
            footer: '<a href="">Why do I have this issue?</a>',
          });
        });

      onAuthStateChanged(auth, (user) => {
        if (user) {
          const uid = user.uid;
          console.log(uid);
        }
      });
    },
  },
};
</script>

Recuperar Contraseña

Para recuperar la contraseña incluimos un <router-link> que nos lleve al componente de recuperación de contraseña

ForgotPassword.vue

Este componente se encargará de enviar un correo de recuperación de contraseña al correo que escribamos en la parte superior, el correo debe estar asociado a una cuenta existente, es decir el correo tiene que estar registrado.

Recuperación de Contraseña

Para recuperar la contraseña debemos crear una referencia a nuestro módulo auth de Firebase, cuando hagamos click en el botón sendEmail, llamaremos al método sendEmail(), que a su vez llama al método sendPasswordResetEmail(), que necesita como parámetros nuestra instancia de auth y el correo que obtenemos de la caja de texto.

//ForgotPassword.vue

<template>
  <div>
    <label class="form-label"> Email to send Password recover Link </label>
    <input type="text" class="form-control" v-model="email" />
    <button class="btn btn-success" @click="sendEmail">Send Email</button>
  </div>
</template>

<script>
import { getAuth, sendPasswordResetEmail } from "firebase/auth";
import Swal from "sweetalert2";

export default {
  name: "ForgotPassword",
  data() {
    return {
      email: "",
    };
  },
  methods: {
    sendEmail() {
      const auth = getAuth();
      sendPasswordResetEmail(auth, this.email).then((res) => {
          console.log(res);
        this.$router.push("/login");
        Swal.fire({
          position: "top-end",
          icon: "success",
          title: "Email sended",
          showConfirmButton: false,
          timer: 1500,
        });
      });
    },
  },
};
</script>

Si el email se envía correctamente mostramos una alerta con SweetAlert2 para que el usuario sepa que se ha enviado correctamente y le redirigimos al login con this.$router.push(«/login»);

UploadFile.vue

Este es un componente protegido, es decir, solo podemos acceder al mismo cuando tengamos una sesión iniciada, si intentamos acceder sin una sesión iniciada nos redirigirá a Home.

Este componente se encarga de recibir los ficheros que seleccionemos en el input, subirlos a Firebase Storage, indicar el progreso de subida y mostrar un mensaje cuando el archivo se haya subido correctamente

Seleccionar un fichero

Cuando el usuario selecciona un archivo, el input file llama al evento change que tendremos asociado a un método que nos guardará en una variable el archivo seleccionado por el usuario

Subir un fichero

Para subir un fichero vamos a necesitar una referencia a nuestro Storage de Firebase, las carpetas dentro de nuestro storage van a estar identificadas por el uid del usuario que tenga iniciada la sesión, de manera que con auth podamos obtener el uid del usuario actual y meternos en su carpeta dentro del storage.

Cuando hacemos click en el botón upload, llamaremos a nuestro método uploadFile(), en este método se crea una referencia a nuestro storage con el método getStorage().

Cuando ya tenemos una referencia a nuestro storage necesitamos decirle donde queremos guardar el archivo, como hemos dicho antes, vamos a guardarlo dentro de una carpeta que tendrá como nombre el uid del usuario. Tendremos que crear una referencia a esa carpeta, para ello usamos el método ref(), al que tenemos que pasar como parámetros la referencia anterior a nuestro Storage y la ruta en la que queremos guardar el elemento ( «/» + getAuth().currentUser.uid + «/» + this.selectedFile[i].name ).

Una vez tengamos la referencia a nuestra ruta (ref) tenemos que crear un objeto de tipo uploadTask, que nos va a permitir ir comprobando el progreso de carga, para ello usamos el método uploadBytesResumable(), al que tenemos que pasar como parámetro la referencia de la ruta y el archivo que hemos seleccionado, que si recordamos está en la variable selectedFile.

const uploadTask = uploadBytesResumable(
            storageRef,
            this.selectedFile[i]
          );

Cuando tenemos nuestro uploadTask debemos capturar el evento » state_changed» que nos devolverá un objeto snapshot en el cual podemos capturar el progreso de subida, este progreso lo guararemos en la variable progress que está asociada con la propiedad del elemento progress del template.

uploadTask.on("state_changed", (snapshot) => {
            this.progress = Math.floor(
              (snapshot.bytesTransferred / snapshot.totalBytes) * 100
            );

Cuando el uploadTask ha finalizado mostramos una alerta para informar al usuario de que ha finalizado la subida.

//UploadFile.vue

<div class="container">
    <h1>UploadFile</h1>
    <input type="file" @change="selectFile" />
    <button class="btn btn-primary" @click="uploadFile()">UploadFile</button>
    <br />
    <p>Progress</p>
    <progress max="100" :value="progress"></progress
    ><span>{{ progress }}%</span>
    <ul class="list-group">
      <li
        class="list-group-item"
        v-for="(file, index) in selectedFile"
        :key="file + index"
      >
        {{ file.name }}
      </li>
    </ul>
  </div>
</template>

<script>
import { getStorage, ref, uploadBytesResumable } from "firebase/storage";
import { getAuth } from "firebase/auth";
import Swal from "sweetalert2";

export default {
  name: "UploadFile",
  data() {
    return {
      selectedFile: null,
      progress: 0,
    };
  },
  mounted() {
    if (getAuth().currentUser == null) {
      this.$router.push("/home");
    }
  },
  methods: {
    selectFile(event) {
      this.progress = 0;
      console.log(event.target.files);
      this.selectedFile = event.target.files;
      //console.log(this.selectedFile);
      for (var i = 0; i < this.selectedFile.length; i++) {
        this.selectedFile[i].progress = 0;
      }
    },
    uploadFile() {
      try {
        this.totalBytes = 0;
        this.totalTranfer = 0;
        const storage = getStorage();
        for (var i = 0; i < this.selectedFile.length; i++) {
          console.log(
            "el tipos es" + this.selectedFile[i] + this.selectedFile[i].name
          );
          const storageRef = ref(
            storage,
            "/" + getAuth().currentUser.uid + "/" + this.selectedFile[i].name
          );
          this.totalBytes += this.selectedFile[i].size;
          const uploadTask = uploadBytesResumable(
            storageRef,
            this.selectedFile[i]
          );
          uploadTask.on("state_changed", (snapshot) => {
            this.progress = Math.floor(
              (snapshot.bytesTransferred / snapshot.totalBytes) * 100
            );
            snapshot.task.then((res) => {
              console.log(res + "subidooo");
              Swal.fire({
                position: "top-end",
                icon: "success",
                title: "File " + res.metadata.name + " Uploaded",
                showConfirmButton: false,
                timer: 1500,
              });
            });
          });
        }
      } catch (err) {
        console.log(err);
      }
    },
  },
};
</script>

Dashboard.vue

Este también es un componente protegido, solo podemos acceder al mismo cuando tengamos una sesión iniciada, si intentamos acceder sin una sesión iniciada nos redirigirá a Home.

Este componente se va a encargar de mostrarnos los ficheros que haya subido el usuario, proporcionar la opción de descargar el fichero con un botón, mustrar una pantalla de carga mientras se cargan los elementos y cambiar el icono del archivo en función de su extensión.

Listado de archivos

Para poder listar los ficheros que tiene un usuario dentro del storage, debemos saber cual es la carpeta del usuario, como hemos dicho antes, cada usuario, tiene una carpeta identificada con el uid del usuario, por lo que necesitaremos utilizar el metodo getAuth().currentUser.uid de Firebase auth.

const auth = getAuth();

Ahora debemos crear una referencia al storage del usuario, para ello usaremos el método getStorage() de Firebase Storage.

const storage = getStorage();

Cuando tengamos las dos referencias que necesitmos, vamos a crear otra referencia a la carpeta del usuario que tiene la sesión iniciada, para ello usamos el método ref() de Firebase Storage, al que le tenemos que pasar por parámetro la referencia de nuestro módulo de Storage y la carpeta que queremos, que si recordamos coincide con el uid del usuario actual.

const storageRef = ref(storage, auth.currentUser.uid);

Ahora que ya tenemos la referencia a nuestra carpeta, debemos recorrer todos los elementos que contiene para poder obtener su información y mostrarla, para ello vamos a usar el método listAll() de Firebase Storage, al que tenemos que pasar por parámetro la referencia a nuestra carpeta y que nos devolverá un resultado que contendrá la lista de archivos del usuario.

Selección del icono

Para elegir el icono recorremos un archivo JSON que contiene algunos formatos de archivo y su icono asociado, el archivo comienza teniendo un icono genérico y si el formato es reconocido en el JSON se cambia el icono

//ejemplo JSON formatos

{
            "formato": "mkv",
            "icono": "https://cdn-icons-png.flaticon.com/512/482/482059.png"
        },
        {
            "formato": "mp4",
            "icono": "https://cdn-icons-png.flaticon.com/512/482/482059.png"
        },
        {
            "formato": "avi",
            "icono": "https://cdn-icons-png.flaticon.com/512/482/482059.png"
        }
}
//Dashboard.vue
<template>
  <div>
    <h1>Dashboard</h1>
    <!-- <h2>{{ userData }}</h2> -->
    <div v-if="totalitems != totalLoaded" class="loading">Loading…</div>
    <h2>User Files</h2>
    <div class="row row-cols-1 row-cols-sm-2 row-cols-md-5 g-3">
      <div
        class="col"
        v-for="(file, index) in userFilesList"
        :key="file + index"
      >
        <div class="card shadow-sm">
          <img :src="file.icono" />

          <div class="card-body">
            <h5 class="card-text">{{ file.name }}</h5>
            <div class="d-flex justify-content-between align-items-center">
              <div class="btn-group">
                <a :href="file.url" class="btn btn-sm btn-outline-secondary"
                  >Download</a
                >
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { getAuth } from "firebase/auth";
import { getDownloadURL, getStorage, ref, listAll } from "firebase/storage";
import { logo } from "../../assets/logo.png";
import formats from "../../assets/json/files";

export default {
  name: "Dashboard",
  data() {
    return {
      userData: "",
      userFilesList: [],
      totalLoaded: 0,
      totalitems : 0
    };
  },
  components : {
    
  },
  methods: {
    getUserData() {
      const auth = getAuth();
      console.log(auth.currentUser);
      this.userData = auth.currentUser;
    },
    async listAllm() {
      try {
        const storage = getStorage();
        const archivoLocal = logo;
        const auth = getAuth();

        console.log("el tipos es" + archivoLocal);
        console.log("/" + auth.currentUser.uid + "/");
        const storageRef = ref(storage, auth.currentUser.uid);
        listAll(storageRef).then((res) => {
          console.log(res);
          this.totalitems = res.items.length;

          res.items.forEach((itemRef) => {
            //var urlIcon = "";
            getDownloadURL(itemRef).then((url) => {
              console.log(url);
              var icono = "https://cdn-icons-png.flaticon.com/512/633/633585.png";
              formats.formatos.forEach((formato) => {
                console.log(formato);
                console.log(
                  itemRef.name
                    .substring(
                      itemRef.name.lastIndexOf(".") + 1,
                      itemRef.name.length
                    )
                    .toLowerCase()
                );
                if (
                  itemRef.name
                    .substring(
                      itemRef.name.lastIndexOf(".") + 1,
                      itemRef.name.length
                    )
                    .toLowerCase() == formato.formato
                ) {
                  icono = formato.icono;
                }
              });
              this.userFilesList.push({
                name: itemRef.name,
                // url:url
                url: url,
                icono: icono,
              });
              this.totalLoaded +=1;
            });
          }
         );
        }
        );
        
      } catch (err) {
        console.log(err);
      }
    },
  },
  mounted() {
    this.getUserData();
    this.listAllm();
    if (getAuth().currentUser == null) {
      this.$router.push("/home");
    }
  },
};
</script>

Obtención de URL de descarga

Para obtener la url de descarga de cada fichero se usa el metodo getDownloadURL() de Firebase Storage, al que tenemos que pasar por parámetro la referencia al objeto del cual queremos obtener el URL de descarga, como nosotros iteramos sobre la lista de resultados del método listAll() y en cada vuelta obtenemos una referencia a un archivo del usuario, aprovechamos para obtener la URL de descarga.

Mostrar archivos en template

Dentro del template del componente existe un bucle que dibuja cada archivo que se ha guardado dentro de la variable userFilesList, que de momento está vacía, para rellenarla, una vez tenemos la información del archivo, es decir, el nombre, el icono y la url de descarga que hemos ido obteniendo en pasos anteriores, hacemos un push a esta lista con un objeto que creamos a nuestro gusto, pero conteniendo los datos que hemos ido obteniendo.

this.userFilesList.push({
                name: itemRef.name,
                url: url,
                icono: icono,
              });

Finalmente en el bucle del template sleccionamos la información que queremos:

<div
        class="col"
        v-for="(file, index) in userFilesList"
        :key="file + index"
      >
        <div class="card shadow-sm">
          <img :src="file.icono" />

          <div class="card-body">
            <h5 class="card-text">{{ file.name }}</h5>
            <div class="d-flex justify-content-between align-items-center">
              <div class="btn-group">
                <a :href="file.url" class="btn btn-sm btn-outline-secondary"
                  >Download</a
                >
              </div>
            </div>
          </div>
        </div>
      </div>

Autor/a: Diego Plaza Rodán

Curso: Desarrollo Web Full Stack, MultiCloud y Multiplataforma

Centro: Tajamar

Año académico: 2021-2022

GitHub: https://github.com/plaza19/opendrive_front.git

Documentación oficial Firebase: https://firebase.google.com/docs

Leave a Comment

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.