Les images et les vidéos
Tu l'auras peut être remarqué, l'API de la NASA ne nous retourne pas forcément que des images. Nous avons parfois des vidéos. En l'état actuel des choses, celles-ci ne sont pas prises en compte. On va modifier notre code pour que ça fonctionne mieux.

Pour ça, on va utiliser un élément de l'API qu'on n'utilisait pas jusqu'à maintenant : "media_type". Et voilà le résultat :
{"language":"text/html","content":"<template>\n <modal v-show=\"open\" @closeModal=\"close\">\n <template #title>Plus d'info</template>\n <template #default>\n Quelques explications à propos du site...\n </template>\n </modal>\n <h1 id=\"welcome\">Bienvenue sur notre site d'avis de photos !</h1>\n <button @click=\"open = !open\" class=\"button centered\">{{ openCloseText }}</button>\n\n <slider :slides=\"photos\" id=\"slider\">\n <template #default=\"{slide : photo, key : index}\">\n <h2>Photo {{ index+1 }}/{{ photos.length }}</h2>\n <photo \n :url=\"photo.url\" \n :copyright=\"photo.copyright\"\n :date=\"photo.date\"\n :media-type=\"photo.media_type\"\n v-model:title=\"photo.title\"\n v-model:explanation=\"photo.explanation\"\n\n ></photo>\n </template>\n </slider>\n</template>\n\n<script>\nimport Photo from './components/Photo.vue'\nimport Slider from './components/Slider.vue'\nimport Modal from './components/Modal.vue'\n\nconst API_URL = 'https://api.nasa.gov/planetary/apod?start_date=2021-02-01&end_date=2021-02-21&api_key=DEMO_KEY'\nexport default {\n name: 'App',\n components: {\n Photo, Slider, Modal\n },\n data() {\n return {\n photos: [],\n open:false,\n }\n },\n created() {\n fetch(API_URL)\n .then(result => result.json())\n .then(result => {this.photos = result})\n },\n computed: {\n openCloseText() {\n return open ? 'En savoir +' : 'Fermer la modal'\n }\n },\n methods: {\n close(message) {\n this.open = false\n console.log(message)\n }\n }\n}\n</script>\n\n<style>\n #app{\n font-family: 'Roboto', sans-serif;\n }\n\n #welcome {\n text-align:center;\n }\n\n .button{\n display:block;\n border:none;\n padding:10px 15px;\n font-size:1.2em;\n color:white;\n background: #818CF8;\n cursor:pointer;\n }\n\n .centered{\n margin:10px auto;\n }\n\n #slider{\n width:80%;\n margin:auto;\n }\n</style>\n","filename":"App.vue"}
{"language":"text/html","content":"<template>\n <figure class=\"photo\" v-bind=\"$attrs\">\n <iframe v-if=\"mediaType == 'video'\" id=\"ytplayer\" type=\"text/html\" width=\"640\" height=\"360\" :src=\"url\"\n frameborder=\"0\"/>\n <img v-else :src=\"url\" :alt=\"title + 'prise par' +copyright\" />\n <figcaption>\n <h2>{{ title }} prise le {{ formattedDate }} par {{ copyright }}</h2>\n <p>{{ explanation }}</p>\n </figcaption>\n <div>\n <label for=\"title\">Modifier le titre : </label>\n <input\n id=\"title\"\n :value=\"title\"\n @input=\"$emit('update:title', $event.target.value)\"\n >\n </div>\n <div>\n <label for=\"explanation\">Modifier l'explication : </label>\n <textarea \n id=\"explanation\"\n :value=\"explanation\"\n @input=\"$emit('update:explanation', $event.target.value)\"\n ></textarea>\n </div>\n </figure>\n <button @click=\"currentTab = 'Views'\" class=\"button tab\" :class=\"{underline:(currentTab == 'Views')}\">Avis</button>\n <button @click=\"currentTab = 'ViewForm'\" class=\"button tab\" :class=\"{underline:(currentTab == 'ViewForm')}\">Rédiger un avis</button>\n <keep-alive>\n <component :is=\"currentTab\" v-bind=\"dynamicProps\" @add-view=\"storeView\"></component>\n </keep-alive>\n</template>\n\n<script>\nimport Views from './Views.vue'\nimport ViewForm from './ViewForm.vue'\n\nexport default {\n components:{\n Views, ViewForm\n },\n emits:['update:title', 'update:explanation'],\n props:{\n url: {\n type:String, \n required:true,\n default:'http://uneimagepardefaut.png'\n },\n title: {\n type:String,\n required:true,\n },\n mediaType:{\n type:String,\n required:true,\n default:\"image\"\n },\n date: {\n type:String,\n required:true,\n default:Date.now()\n },\n explanation: {\n type:String,\n required:true,\n },\n copyright: {\n type:String,\n required:false,\n default:'un inconnu'\n }\n },\n data() {\n return {\n currentTab:'Views',\n views: [ ]\n }\n },\n computed: {\n formattedDate() {\n let date = new Date(this.date)\n let day = Number(date.getDate()) >= 10 ? date.getDate() : '0'+date.getDate()\n return `${day}/${date.getMonth()}/${date.getFullYear()}`\n },\n dynamicProps() {\n if(this.currentTab == 'Views') {\n return { views:this.views }\n }\n return null\n }\n }, \n methods:{\n storeView(author, content) {\n this.views.push({author:author, content:content})\n }\n }\n}\n</script>\n\n<style scoped>\n .underline{\n text-decoration: underline\n }\n .tab{\n display:inline-block;\n border-radius: 20px 20px 0 0;\n }\n\n .photo img{\n max-height:300px;\n }\n\n .photo{\n text-align:center;\n }\n</style>","filename":"Photo.vue"}
Le design du slider et son accessibilité
De manière à rendre le slider un peu plus "beau" en terme de design et de le rendre plus accessible, j'ai modifié un peu le composant pour accepter la navigation au clavier et redesigné les boutons de droite et gauche.
Voici le résultat :
{"language":"text/html","content":"<template>\n <div class=\"slider\">\n <button @click=\"slide(-1)\" aria-label=\"Aller à gauche\" class=\"button arrow\" tabindex=\"0\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"30\" height=\"30\" fill=\"currentColor\" class=\"bi bi-arrow-left\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z\"/>\n </svg>\n </button>\n <template v-for=\"(slide, key) in slides\" :key=\"key\">\n <div v-if=\"index === key\" class=\"inner\">\n <slot :slide=\"slide\" :key=\"key\" class=\"content\"></slot>\n </div>\n\n </template>\n <button @click=\"slide(1)\" aria-label=\"Aller à droite\" class=\"button arrow\" tabindex=\"0\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"30\" height=\"30\" fill=\"currentColor\" class=\"bi bi-arrow-right\" viewBox=\"0 0 16 16\">\n <path fill-rule=\"evenodd\" d=\"M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z\"/>\n </svg>\n </button>\n </div>\n</template>\n\n<script>\n export default {\n props:['slides'],\n data() {\n return {\n index:0\n }\n },\n mounted() {\n document.addEventListener('keydown', this.listenKeyboard);\n },\n methods:{\n slide(operation) {\n this.index = (((this.index + operation)%this.slides.length)+this.slides.length)%this.slides.length\n },\n listenKeyboard(event) {\n const key = event.key;\n if (key.startsWith('Arrow')) {\n event.preventDefault();\n }\n if (key === 'ArrowRight') {\n this.slide(1);\n }\n if (key === 'ArrowLeft') {\n this.slide(-1);\n }\n }\n }\n }\n</script>\n\n<style scoped>\n .arrow{\n display:flex;\n justify-content: center;\n align-items: center;\n border-radius:50%;\n width:50px;\n height:50px;\n }\n .slider{\n display:flex;\n justify-content: space-between;\n align-items: center;\n flex-wrap:wrap;\n }\n\n .inner{\n width:80%;\n border:1px solid black;\n }\n</style>","filename":"Slider.vue"}

La suite de ton apprentissage
Maintenant que tu maitrises les composants avec Vue, de belles applications s'offrent à toi ! Mais elles pourraient être encore plus belles, notamment se rapprocher d'une SPA, grâce à Vue Routeur par exemple.
Il pourrait également être super intéressant de découvrir L'API de composition et la reusability ! Ne t'en fais pas, je serai encore là pour t'aiguiller sur tous ces points ;)

Tu peux l'être, tu as vu un beau morceau de Vue.JS dans ce cours. Félicitations ;)
Ce cours est terminé. Tu souhaites continuer ? Les cours sur l'API de composition et Vue Routeur arrivent prochainement sur training-dev ;). En attendant, tu peux pratiquer Vue.JS avec des projets perso et écouter inlassablement Hans Zimmer, Two Steps From Hell, Armin Van Buuren ou encore Don Diablo :D
J'ai terminé cette partie