Cerrando vista Profile

21/27

Lectura

Ya casi hemos terminado la vista de Profile. Para terminar el (gran) componente de MainBlock nos queda una última cosa y ya podríamos pasar al componente de los artesanos.

Lo que vamos a implementar ahora es que cuando hagamos click en un héroe, tanto si haces click en el rostro del hero en TopHeroe o en HeroIco (primera columna de la tabla), cambiemos a la ruta de detalle del héroe, es decir, a la vista Hero.

Para refrescar contenidos, estas son las rutas que habíamos definido:

const routerOptions = [
  { path: '/', name: 'Home' },
  { path: '/region/:region/profile/:battleTag', name: 'Profile' },
  { path: '/region/:region/profile/:battleTag/hero/:heroId', name: 'Hero' },
  { path: '/about', name: 'About' },
  { path: '/error', name: 'Error' },
  { path: '*', redirect: { name: 'Home' } }
]

Queremos que al hacer click en la “cara” de un hero, la app cambie de ruta y cargue la vista de Hero. Tenemos dos casos en los que controlar este comportamiento, por lo tanto, para reutilizar, vamos a crear un mixin que, más tarde, lo usaremos en múltiples componentes.

Creamos nuestro mixin en la carpeta /mixins. Como se trata de una función (method) que va a navegar a la vista del hero, podemos llamarle goToHero.js. Va a tener este contenido:

export default {
  methods: {
    /**
     * Go to hero Id
     * @param heroId {String | Number}
     */
    goToHero (heroId) {
      // Sacar los datos de la URL
      const { region, battleTag } = this.$route.params
      // Navegar a la ruta "Hero"
      this.$router.push({ name: 'Hero', params: { region, battleTag, heroId } })
    }
  }
}

Para poder usarlo, primero lo importamos y después lo declaramos en el componente. Vamos a usarlo en dos componentes:

  • TopHero.vue

  <script>
+ import goToHero from '@/mixins/goToHero'

  import { formatNumber } from '@/filters/numeral'

  export default {
    name: 'TopHero',
+   mixins: [goToHero],
    filters: {
      formatNumber
  },
    props: {
      hero: {
        type: Object,
        required: true
    }
  },
    computed: {
      heroClass () {
        const gender = this.hero.gender === 0 ? 'male' : 'female'
        return `hero-${this.hero.classSlug} ${gender}`
      }
    }
  }
  </script>
- <div class="hero-portrait-wrapper mb-5 mb-sm-0">
+ <div class="hero-portrait-wrapper mb-5 mb-sm-0" @click="goToHero(hero.id)">
  • HeroIco.vue

  <script>
+ import goToHero from '@/mixins/goToHero'

  export default {
    name: 'HeroIco',
+   mixins: [goToHero],
    props: {
      hero: {
        required: true,
        type: Object
      }
    },
    computed: {
      heroClassImg () {
        const gender = this.hero.gender === 1 ? 'female' : 'male'
        const hardcore = this.hero.hardcore ? 'border-danger' : ''
        return `hero-${this.hero.classSlug} ${gender} ${hardcore}`
      }
    }
  }
  </script>
- <div class="hero-ico d-flex align-items-center">
+ <div class="hero-ico d-flex align-items-center" @click="goToHero(hero.id)">

Si pruebas a hacer click en estos elementos, debería funcionarte y cambiarte de vista.

preview-1.pngSin embargo, tenemos un problema que tal vez hayas visto. La interfaz no es muy intuitiva, pues cuando pasamos el ratón por encima, el cursor no cambia a pointer 👆.

Podemos solucionarlo con una clase de CSS. Vamos a crear una clase CSS en nuestro fichero global de estilos CSS /assets/css/main.styl.

Al final de este fichero, agregamos lo siguiente:

// ---------------------
// Utils
// ---------------------

.hover-cursor-pointer
  &:hover
    cursor pointer

Y aplicamos los cambios, es decir, agregamos la clase CSS .hover-cursor-pointer a los dos componentes:

  • TopHero.vue
- <div class="hero-portrait-wrapper mb-5 mb-sm-0" @click="goToHero(hero.id)">
+ <div class="hero-portrait-wrapper mb-5 mb-sm-0 hover-cursor-pointer" @click="goToHero(hero.id)">
  • HeroIco.vue
- <div class="hero-ico d-flex align-items-center" @click="goToHero(hero.id)">
+ <div class="hero-ico d-flex align-items-center hover-cursor-pointer" @click="goToHero(hero.id)">

¡Estupendo! El componente MainBlock ya está listo (¡Por fin!). Podemos pasar a desarrollar el componente de los artesanos.
Y en cuanto terminemos con los artesanos podríamos dar por finalizada esta vista, la vista Profile.

ArtisansBlock

Este componente es también bastante sencillo: va a mostrar datos básicos de los tres artesanos (herrero, joyero y mística).

📗 Más info de artesanos y artesanía en Diablo III: https://eu.diablo3.com/es/game/guide/items/crafting

Para empezar, vamos a crear todos los archivos necesarios. Dentro de la carpeta /views/Profile, al mismo nivel que /MainBlock, creamos una nueva llamada /ArtisansBlock. Dentro de esta creamos Index.vue y ArtisanItem.vue.

Ahora toca actualizar el componente vista /views/Profile/Index.vue, y agregarle el nuevo componente de ArtisansBlock que acabamos de crear.

  • /views/Profile/Index.vue

En el bloque de JavaScript, importamos y habilitamos el componente (as usual):

// /views/Profile/Index.vue
import setError from '@/mixins/setError'
import { getApiAccount } from '@/api/search'

import BaseLoading from '@/components/BaseLoading'
import MainBlock from './MainBlock/Index'
// Importar
import ArtisansBlock from './ArtisansBlock/Index'

export default {
  name: 'ProfileView',
  mixins: [ setError ],
  components: {
    BaseLoading,
    ArtisansBlock,
    // Habilitar componente
    MainBlock
  }
  // ...
}

Usamos el componente. El HTML es el siguiente:

 <template>
  <div class="profile-view">
    <BaseLoading v-if="isLoading"/>

    <template v-if="profileData !== null">
      <MainBlock :profile-data="profileData"/>
      <ArtisansBlock :artisans-data="artisansData" />
    </template>
  </div>
</template>

El componente está recibiendo artisansData como prop. Vamos a crear una computed property que genere el objeto que necesitamos, los datos de los tres artesanos y en los dos modos (normal y hardcore):

computed: {
  artisansData () {
    return {
      blacksmith: this.profileData.blacksmith,
      blacksmithHardcore: this.profileData.blacksmithHardcore,
      jeweler: this.profileData.jeweler,
      jewelerHardcore: this.profileData.jewelerHardcore,
      mystic: this.profileData.mystic,
      mysticHardcore: this.profileData.mysticHardcore
    }
  }
},

El código completo del componente /Profile/Index.vue es este:

<template>
  <div class="profile-view">
    <BaseLoading v-if="isLoading"/>
    <template v-if="profileData !== null">
      <MainBlock :profile-data="profileData"/>
      <ArtisansBlock :artisans-data="artisansData" />
    </template>
  </div>
</template>

<script>
import setError from '@/mixins/setError'
import { getApiAccount } from '@/api/search'

import BaseLoading from '@/components/BaseLoading'
import MainBlock from './MainBlock/Index'
import ArtisansBlock from './ArtisansBlock/Index'

export default {
  name: 'ProfileView',
  mixins: [
    setError
  ],
  components: {
    BaseLoading,
    MainBlock,
    ArtisansBlock
  },
  data () {
    return {
      isLoading: false,
      profileData: null
    }
  },
  computed: {
    artisansData () {
      return {
        blacksmith: this.profileData.blacksmith,
        blacksmithHardcore: this.profileData.blacksmithHardcore,
        jeweler: this.profileData.jeweler,
        jewelerHardcore: this.profileData.jewelerHardcore,
        mystic: this.profileData.mystic,
        mysticHardcore: this.profileData.mysticHardcore
      }
    }
  },
  created () {
    this.isLoading = true
    const { region, battleTag: account } = this.$route.params
    this.fetchData(region, account)
  },
  methods: {
    /**
       * Obtener los datos de la API
       * Guardarlos en 'profileData'
       * @param region {String}
       * @param account {String}
       */
    fetchData (region, account) {
      getApiAccount({
        region,
        account
      })
        .then(({ data }) => {
          this.profileData = data
        })
        .catch((err) => {
          this.profileData = null
          const errObj = {
            routeParams: this.$route.params,
            message: err.message
          }
          if (err.response) {
            errObj.data = err.response.data
            errObj.status = err.response.status
          }
          this.setApiErr(errObj)
          this.$router.push({ name: 'Error' })
        })
        .finally(() => {
          this.isLoading = false
        })
    }
  }
}
</script>

Ahora, para probar que funcione, podemos darle este contenido al componente de ArtisansBlock/Index.vue

<template>
  <div>
    <h1>Artesanos</h1>
  </div>
</template>

<script>
export default {
  name: 'ArtisansBlock'
}
</script>

Se debería ver así:

preview-2.png

Ahora vamos a entrar al detalle en el componente de artesanos (/ArtisansBlock/Index.vue). Lo primero es definir las props que va a recibir:

<script>

import ArtisanItem from './ArtisanItem'

export default {
  name: 'ArtisansBlock',
  components: { ArtisanItem },
  props: {
    artisansData: {
      type: Object,
      required: true
    }
  }
}
</script>

Además, necesitamos crear un array que agrupe los datos de los artesanos. ¿Adivinas cómo lo vamos a hacer? Una computed property, ¡correcto!

computed: {
  artisansInfo () {
    return [
      {
        name: 'blacksmith',
        icon: 'hammer',
        emoji: '⚒',
        color: '#ffb74d',
        normal: this.artisansData.blacksmith,
        hardcore: this.artisansData.blacksmithHardcore
      },
      {
        name: 'jeweler',
        icon: 'gem',
        emoji: '💎',
        color: '#4dd0e1',
        normal: this.artisansData.jeweler,
        hardcore: this.artisansData.jewelerHardcore
      },
      {
        name: 'mystic',
        icon: 'hat-wizard',
        emoji: '🔮',
        color: '#ba68c8',
        normal: this.artisansData.mystic,
        hardcore: this.artisansData.mysticHardcore
      }
    ]
  }
}

😎 Truqui: si estás en MacOS, puedes pulsar la combinación de teclas Control + Cmd + Tecla Espacio para sacar un selector de emojis. 👇

emojis.png

¿Eres usuario de Windows? Con la combinación Win + . puedes hacer lo mismo 😉


Hemos creado un array con tres items. Cada item corresponde a un artesano. Cada uno tiene los dos datos de su tipo que ha recibido de las props.
Además, le hemos dado un color, un emoji y un ícono (icon, que hace referencia a FontAwesome, la librería de íconos que ya conoces)

Ahora el HTML:

<template>
  <div class="artisan-list">
    <hr class="bg-light my-5">
    <h3 class="font-diablo mb-4">Artisans</h3>
    <b-row>
      <b-col lg="4" v-for="artisan in artisansInfo" :key="artisan.name">
        <ArtisanItem :artisan="artisan"/>
      </b-col>
    </b-row>
  </div>
</template>

El código completo de /ArtisansBlock/Index.vue es el siguiente:

<template>
  <div class="artisan-list">
    <hr class="bg-light my-5">
    <h3 class="font-diablo mb-4">Artisans</h3>
    <b-row>
      <b-col lg="4" v-for="artisan in artisansInfo" :key="artisan.name">
        <ArtisanItem :artisan="artisan"/>
      </b-col>
    </b-row>
  </div>
</template>

<script>

import ArtisanItem from './ArtisanItem'

export default {
  name: 'ArtisansBlock',
  components: { ArtisanItem },
  props: {
    artisansData: {
      type: Object,
      required: true
    }
  },
  computed: {
    artisansInfo () {
      return [
        {
          name: 'blacksmith',
          icon: 'hammer',
          emoji: '⚒️',
          color: '#ffb74d',
          normal: this.artisansData.blacksmith,
          hardcore: this.artisansData.blacksmithHardcore
        },
        {
          name: 'jeweler',
          icon: 'gem',
          emoji: '💎',
          color: '#4dd0e1',
          normal: this.artisansData.jeweler,
          hardcore: this.artisansData.jewelerHardcore
        },
        {
          name: 'mystic',
          icon: 'hat-wizard',
          emoji: '🔮',
          color: '#ba68c8',
          normal: this.artisansData.mystic,
          hardcore: this.artisansData.mysticHardcore
        }
      ]
    }
  }
}
</script>

El objeto que hemos generado con la computed property, que es el que se va a usar para iterar y generar los tres componentes de artesanos dinámicamente, tiene este contenido:

[
  {
    "name":"blacksmith",
    "icon":"hammer",
    "emoji":"⚒️",
    "color":"#ffb74d",
    "normal":{
      "slug":"blacksmith",
      "level":12
    },
    "hardcore":{
      "slug":"blacksmith",
      "level":0
    }
  },
  {
    "name":"jeweler",
    "icon":"gem",
    "emoji":"💎",
    "color":"#4dd0e1",
    "normal":{
      "slug":"jeweler",
      "level":12
    },
    "hardcore":{
      "slug":"jeweler",
      "level":0
    }
  },
  {
    "name":"mystic",
    "icon":"hat-wizard",
    "emoji":"🔮",
    "color":"#ba68c8",
    "normal":{
      "slug":"mystic",
      "level":12
    },
    "hardcore":{
      "slug":"mystic",
      "level":0
    }
  }
]

ArtisanItem

Ahora nos queda terminar el componente /ArtisansBlock/ArtisanItem.vue. Vamos a dejar definidos los bloques de JavaScript y de CSS:

<script>
export default {
  name: 'ArtisanItem',
  props: {
    artisan: {
      required: true,
      type: Object
    }
  }
}
</script>

<style lang="stylus" scoped>
  .artisan-item
    .icon
      width 80px
      height 80px
      background-color #404850

</style>

El HTML lo explicamos un poquito más, aunque es muy simple, ya verás. ¿Recuerdas que hablamos de los emojis anteriormente? En esta ocasión le estamos pasando, a través de las props, ¡emojis al componente! 😏

Usarlo es tan sencillo como esto:

  • Opción uno
<template>
  <div class="artisan-item d-flex bg-dark p-3 mb-2 rounded">

    <!-- Bloque Ícono / Emoji -->
    <div class="icon d-flex justify-content-center align-items-center rounded-circle mr-2">
      <!-- ¡Usar Emoji! -->
      <span class="display-4">{{ artisan.emoji}}</span>
    </div>

    <!-- Bloque Contenido -->
    <div class="content">
      <h5 class="font-weight-bold text-capitalize"> {{ artisan.name }} </h5>

      <!-- Si hay artesano normal -->
      <p v-if="artisan.normal.level" class="m-0 font-weight-normal">
        Level {{artisan.normal.level}} (normal)
      </p>

      <!-- Si hay artesano hardcore -->
      <p v-if="artisan.hardcore.level" class="mb-0 font-weight-normal text-muted">
        Level {{ artisan.hardcore.level }}
        <span class="text-danger">(hardcore)</span>
      </p>
    </div>
  </div>
</template>

Esto es así porque puedes jugar la historia en modo normal y no tener los artesanos del modo hardcore.

Si abres el navegador, tu app se debería ver maravillosamente así:

preview-3.pngNos queda ver la segunda opción, usando iconos de FontAwesome.

  • Opción dos
<!-- Bloque Ícono / Emoji -->
<div class="icon d-flex justify-content-center align-items-center rounded-circle mr-2">
  <!-- <span class="display-4">{{ artisan.emoji}}</span> -->
  <!-- Usar Ícono -->
  <font-awesome-icon :icon="artisan.icon" class="fa-2x" :style="{color: artisan.color}"/>
</div>

Con esta opción se vería así:

preview-4.png¿Cuál te gusta más? ¿Cuál crees que es mejor? Deja tus comentarios y explica tu respuesta.
La decisión es tuya, tú eliges con que opción te quedas. En mi caso voy a usar la opción uno, la de los emojis.


¡Maravilloso! 🎉 Hemos terminado con la vista de Profile. Ahora debemos continuar con la vista de detalle del héroe, donde cargaremos los items y los skills de nuestro personaje.
Lo vemos en el siguiente tema.

Aportes 10

Preguntas 0

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesión.

A mi me gusta más la opción de los emojis pero por cuestiones de que la página se vea igual para todos es mejor usar los íconos de FontAwesome, porque si alguien mira la página desde un dispositivo que no tiene esos íconos no le aparecería nada jaja

¿Qué opción has usado y por qué? Deja tu respuesta aquí: 👇

Yo me quedo con la opción uno, creo es mas colorida !

La opción de los emojis me parece que se ve mejor, por lo menos para mi.

Me gustaron mas los emojis se ven mas elegantes

Excelente lectura, de momento me quedo con la opción #1 para variar, ya que usualmente se suele hacer uso del mecanismo tradicional #2, les coparto mi proyecto aunque sin hacer mis ajustes personales como es de costumbre:

Decidí dejar los font awesome para dar un toque uniforme a la app.
Así miraremos el mismo resultado en cada cliente.

Me quede con la opción 2, se me hizo más elegante.

Lectura número 21 mi odio hacía el corrector de lint sigue en aumento, mi único guía y salvador ha sido npm run lint

Me gusto mas con iconos pero se me hace mas profesional con Font al final lo deje con los iconos.